From a9ec64c980b7cb083d871171485776ed05940360 Mon Sep 17 00:00:00 2001 From: Wil Boayue Date: Thu, 24 Oct 2024 20:33:58 -0700 Subject: [PATCH] Implements market depth (#139) --- examples/market_data.rs | 26 ++ examples/market_depth.rs | 17 + examples/market_depth_exchanges.rs | 15 + examples/tick_by_tick.rs | 8 +- src/client.rs | 87 +++- src/contracts/decoders.rs | 6 +- src/market_data/realtime.rs | 405 +++++++++++++------ src/market_data/realtime/decoders.rs | 248 +++++++++--- src/market_data/realtime/decoders/tests.rs | 56 +++ src/market_data/realtime/encoders.rs | 233 +++++------ src/market_data/realtime/encoders/tests.rs | 92 +++++ src/market_data/realtime/tests.rs | 89 +++- src/market_data/realtime/tick_types.rs | 105 +++++ src/messages.rs | 49 ++- src/messages/shared_channel_configuration.rs | 4 + src/messages/tests.rs | 14 +- src/server_versions.rs | 2 +- src/testdata/responses.rs | 16 + 18 files changed, 1158 insertions(+), 314 deletions(-) create mode 100644 examples/market_data.rs create mode 100644 examples/market_depth.rs create mode 100644 examples/market_depth_exchanges.rs create mode 100644 src/market_data/realtime/decoders/tests.rs create mode 100644 src/market_data/realtime/encoders/tests.rs create mode 100644 src/market_data/realtime/tick_types.rs diff --git a/examples/market_data.rs b/examples/market_data.rs new file mode 100644 index 00000000..e1d3c9c4 --- /dev/null +++ b/examples/market_data.rs @@ -0,0 +1,26 @@ +use ibapi::{contracts::Contract, market_data::realtime::TickTypes, Client}; + +// This example demonstrates how to request realtime market data for a contract. + +fn main() { + env_logger::init(); + + let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + + let contract = Contract::stock("AAPL"); + let generic_ticks = &[]; + let snapshot = false; + let regulatory_snapshot = false; + + let subscription = client + .market_data(&contract, generic_ticks, snapshot, regulatory_snapshot) + .expect("error requesting market data"); + + for tick in &subscription { + println!("{tick:?}"); + + if let TickTypes::SnapshotEnd = tick { + subscription.cancel(); + } + } +} diff --git a/examples/market_depth.rs b/examples/market_depth.rs new file mode 100644 index 00000000..10bb4b41 --- /dev/null +++ b/examples/market_depth.rs @@ -0,0 +1,17 @@ +use ibapi::contracts::Contract; +use ibapi::Client; + +// This example demonstrates how to request market depth data. + +fn main() { + env_logger::init(); + + let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + + let contract = Contract::stock("AAPL"); + + let subscription = client.market_depth(&contract, 5, true).expect("error requesting market depth"); + for row in &subscription { + println!("row: {row:?}") + } +} diff --git a/examples/market_depth_exchanges.rs b/examples/market_depth_exchanges.rs new file mode 100644 index 00000000..a321774e --- /dev/null +++ b/examples/market_depth_exchanges.rs @@ -0,0 +1,15 @@ +use ibapi::Client; + +// This example demonstrates how to request market depth exchanges. + +fn main() { + env_logger::init(); + + let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + + let exchanges = client.market_depth_exchanges().expect("error requesting market depth exchanges"); + + for exchange in &exchanges { + println!("{exchange:?}"); + } +} diff --git a/examples/tick_by_tick.rs b/examples/tick_by_tick.rs index 4cd511ee..f7ff2fc8 100644 --- a/examples/tick_by_tick.rs +++ b/examples/tick_by_tick.rs @@ -11,7 +11,7 @@ fn main() { .version("1.0") .author("Wil Boayue ).default_value("localhost:4002")) + .arg(arg!(--connection_string ).default_value("127.0.0.1:4002")) .arg(arg!(--last )) .arg(arg!(--all_last )) .arg(arg!(--bid_ask )) @@ -46,7 +46,7 @@ fn stream_last(client: &mut Client, _symbol: &str) -> anyhow::Result<()> { let contract = contract_gc(); let ticks = client.tick_by_tick_last(&contract, 0, false)?; - for (i, tick) in ticks.enumerate() { + for (i, tick) in ticks.iter().enumerate() { println!("{}: {i:?} {tick:?}", contract.symbol); } @@ -80,7 +80,7 @@ fn stream_all_last(client: &Client, _symbol: &str) -> anyhow::Result<()> { let contract = contract_es(); let ticks = client.tick_by_tick_all_last(&contract, 0, false)?; - for (i, tick) in ticks.enumerate().take(60) { + for (i, tick) in ticks.iter().enumerate().take(60) { println!("tick: {i:?} {tick:?}"); } @@ -91,7 +91,7 @@ fn stream_bid_ask(client: &mut Client, _symbol: &str) -> anyhow::Result<()> { let contract = contract_es(); let ticks = client.tick_by_tick_bid_ask(&contract, 0, false)?; - for (i, tick) in ticks.enumerate() { + for (i, tick) in ticks.iter().enumerate() { println!("tick: {i:?} {tick:?}"); } diff --git a/src/client.rs b/src/client.rs index 41074a24..90518954 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,7 +12,7 @@ use crate::accounts::{AccountSummaries, AccountUpdate, AccountUpdateMulti, Famil use crate::contracts::{Contract, OptionComputation}; use crate::errors::Error; use crate::market_data::historical::{self, HistogramEntry}; -use crate::market_data::realtime::{self, Bar, BarSize, MidPoint, WhatToShow}; +use crate::market_data::realtime::{self, Bar, BarSize, DepthMarketDataDescription, MarketDepths, MidPoint, TickTypes, WhatToShow}; use crate::market_data::MarketDataType; use crate::messages::{IncomingMessages, OutgoingMessages}; use crate::messages::{RequestMessage, ResponseMessage}; @@ -1009,7 +1009,7 @@ impl Client { contract: &Contract, number_of_ticks: i32, ignore_size: bool, - ) -> Result + 'a, Error> { + ) -> Result, Error> { realtime::tick_by_tick_all_last(self, contract, number_of_ticks, ignore_size) } @@ -1024,7 +1024,7 @@ impl Client { contract: &Contract, number_of_ticks: i32, ignore_size: bool, - ) -> Result + 'a, Error> { + ) -> Result, Error> { realtime::tick_by_tick_bid_ask(self, contract, number_of_ticks, ignore_size) } @@ -1039,7 +1039,7 @@ impl Client { contract: &Contract, number_of_ticks: i32, ignore_size: bool, - ) -> Result + 'a, Error> { + ) -> Result, Error> { realtime::tick_by_tick_last(self, contract, number_of_ticks, ignore_size) } @@ -1079,6 +1079,85 @@ impl Client { market_data::switch_market_data_type(self, market_data_type) } + /// Requests the contract's market depth (order book). + /// + /// # Arguments + /// + /// * `contract` - The Contract for which the depth is being requested. + /// * `number_of_rows` - The number of rows on each side of the order book. + /// * `is_smart_depth` - Flag indicates that this is smart depth request. + /// + /// # Examples + /// + /// ```no_run + /// use ibapi::Client; + /// use ibapi::market_data::{MarketDataType}; + /// + /// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed"); + /// + /// let market_data_type = MarketDataType::Live; + /// client.switch_market_data_type(market_data_type).expect("request failed"); + /// println!("market data switched: {:?}", market_data_type); + /// ``` + pub fn market_depth<'a>( + &'a self, + contract: &Contract, + number_of_rows: i32, + is_smart_depth: bool, + ) -> Result, Error> { + realtime::market_depth(self, contract, number_of_rows, is_smart_depth) + } + + /// Requests venues for which market data is returned to market_depth (those with market makers) + /// + /// # Examples + /// + /// ```no_run + /// ``` + pub fn market_depth_exchanges(&self) -> Result, Error> { + realtime::market_depth_exchanges(self) + } + + /// Requests real time market data. + /// + /// Returns market data for an instrument either in real time or 10-15 minutes delayed data. + /// + /// # Arguments + /// + /// * `contract` - Contract for which the data is being requested. + /// * `generic_ticks` - IDs of the available generic ticks: + /// - 100 Option Volume (currently for stocks) + /// - 101 Option Open Interest (currently for stocks) + /// - 104 Historical Volatility (currently for stocks) + /// - 105 Average Option Volume (currently for stocks) + /// - 106 Option Implied Volatility (currently for stocks) + /// - 162 Index Future Premium + /// - 165 Miscellaneous Stats + /// - 221 Mark Price (used in TWS P&L computations) + /// - 225 Auction values (volume, price and imbalance) + /// - 233 RTVolume - contains the last trade price, last trade size, last trade time, total volume, VWAP, and single trade flag. + /// - 236 Shortable + /// - 256 Inventory + /// - 258 Fundamental Ratios + /// - 411 Realtime Historical Volatility + /// - 456 IBDividends + /// * `snapshot` - for users with corresponding real time market data subscriptions. A true value will return a one-time snapshot, while a false value will provide streaming data. + /// * `regulatory_snapshot` - snapshot for US stocks requests NBBO snapshots for users which have "US Securities Snapshot Bundle" subscription but not corresponding Network A, B, or C subscription necessary for streaming market data. One-time snapshot of current market price that will incur a fee of 1 cent to the account per snapshot. + /// + /// # Examples + /// + /// ```no_run + /// ``` + pub fn market_data( + &self, + contract: &Contract, + generic_ticks: &[&str], + snapshot: bool, + regulatory_snapshot: bool, + ) -> Result, Error> { + realtime::market_data(self, contract, generic_ticks, snapshot, regulatory_snapshot) + } + // == Internal Use == #[cfg(test)] diff --git a/src/contracts/decoders.rs b/src/contracts/decoders.rs index 089f300a..74c5cdb6 100644 --- a/src/contracts/decoders.rs +++ b/src/contracts/decoders.rs @@ -2,7 +2,7 @@ use crate::{contracts::tick_types::TickType, contracts::SecurityType, messages:: use super::{Contract, ContractDescription, ContractDetails, MarketRule, OptionComputation, PriceIncrement}; -pub(crate) fn decode_contract_details(server_version: i32, message: &mut ResponseMessage) -> Result { +pub(super) fn decode_contract_details(server_version: i32, message: &mut ResponseMessage) -> Result { message.skip(); // message type let mut message_version = 8; @@ -124,7 +124,7 @@ fn read_last_trade_date(contract: &mut ContractDetails, last_trade_date_or_contr Ok(()) } -pub(crate) fn decode_contract_descriptions(server_version: i32, message: &mut ResponseMessage) -> Result, Error> { +pub(super) fn decode_contract_descriptions(server_version: i32, message: &mut ResponseMessage) -> Result, Error> { message.skip(); // message type let _request_id = message.next_int()?; @@ -166,7 +166,7 @@ pub(crate) fn decode_contract_descriptions(server_version: i32, message: &mut Re Ok(contract_descriptions) } -pub(crate) fn decode_market_rule(message: &mut ResponseMessage) -> Result { +pub(super) fn decode_market_rule(message: &mut ResponseMessage) -> Result { message.skip(); // message type let mut market_rule = MarketRule { diff --git a/src/market_data/realtime.rs b/src/market_data/realtime.rs index 33e62d29..8fa5e74d 100644 --- a/src/market_data/realtime.rs +++ b/src/market_data/realtime.rs @@ -1,12 +1,12 @@ -use log::error; +use log::debug; use time::OffsetDateTime; use crate::client::{ResponseContext, Subscribable, Subscription}; +use crate::contracts::tick_types::TickType; use crate::contracts::Contract; -use crate::messages::{IncomingMessages, RequestMessage, ResponseMessage}; +use crate::messages::{IncomingMessages, Notice, OutgoingMessages, RequestMessage, ResponseMessage, MESSAGE_INDEX}; use crate::orders::TagValue; use crate::server_versions; -use crate::transport::InternalSubscription; use crate::ToField; use crate::{Client, Error}; @@ -49,6 +49,19 @@ pub struct BidAsk { pub bid_ask_attribute: BidAskAttribute, } +impl Subscribable for BidAsk { + const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::TickByTick]; + + fn decode(_server_version: i32, message: &mut ResponseMessage) -> Result { + decoders::decode_bid_ask_tick(message) + } + + fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { + let request_id = request_id.expect("Request ID required to encode cancel realtime bars"); + encoders::encode_cancel_tick_by_tick(request_id) + } +} + #[derive(Debug)] pub struct BidAskAttribute { pub bid_past_low: bool, @@ -67,15 +80,12 @@ impl Subscribable for MidPoint { const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::TickByTick]; fn decode(_server_version: i32, message: &mut ResponseMessage) -> Result { - decoders::mid_point_tick(message) + decoders::decode_mid_point_tick(message) } fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { - if let Some(request_id) = request_id { - encoders::cancel_tick_by_tick(request_id) - } else { - Err(Error::Simple("Request ID required to encode cancel mid point ticks".into())) - } + let request_id = request_id.expect("Request ID required to encode cancel mid point ticks"); + encoders::encode_cancel_tick_by_tick(request_id) } } @@ -99,11 +109,8 @@ impl Subscribable for Bar { } fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { - if let Some(request_id) = request_id { - encoders::encode_cancel_realtime_bars(request_id) - } else { - Err(Error::Simple("Request ID required to encode cancel realtime bars".into())) - } + let request_id = request_id.expect("Request ID required to encode cancel realtime bars"); + encoders::encode_cancel_realtime_bars(request_id) } } @@ -117,7 +124,7 @@ pub struct Trade { pub price: f64, /// Tick last size pub size: i64, - /// Tick attribs (bit 0 - past limit, bit 1 - unreported) + /// Tick attributes (bit 0 - past limit, bit 1 - unreported) pub trade_attribute: TradeAttribute, /// Tick exchange pub exchange: String, @@ -133,11 +140,8 @@ impl Subscribable for Trade { } fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { - if let Some(request_id) = request_id { - encoders::cancel_tick_by_tick(request_id) - } else { - Err(Error::Simple("Request ID required to encode cancel realtime bars".into())) - } + let request_id = request_id.expect("Request ID required to encode cancel realtime bars"); + encoders::encode_cancel_tick_by_tick(request_id) } } @@ -172,6 +176,194 @@ impl ToField for WhatToShow { } } +#[derive(Debug)] +pub enum MarketDepths { + MarketDepth(MarketDepth), + MarketDepthL2(MarketDepthL2), + Notice(Notice), +} + +#[derive(Debug, Default)] +/// Returns the order book. +pub struct MarketDepth { + /// The order book's row being updated + pub position: i32, + /// How to refresh the row: 0 - insert (insert this new order into the row identified by 'position')· 1 - update (update the existing order in the row identified by 'position')· 2 - delete (delete the existing order at the row identified by 'position'). + pub operation: i32, + /// 0 for ask, 1 for bid + pub side: i32, + // The order's price + pub price: f64, + // The order's size + pub size: f64, +} + +/// Returns the order book. +#[derive(Debug, Default)] +pub struct MarketDepthL2 { + /// The order book's row being updated + pub position: i32, + /// The exchange holding the order if isSmartDepth is True, otherwise the MPID of the market maker + pub market_maker: String, + /// How to refresh the row: 0 - insert (insert this new order into the row identified by 'position')· 1 - update (update the existing order in the row identified by 'position')· 2 - delete (delete the existing order at the row identified by 'position'). + pub operation: i32, + /// 0 for ask, 1 for bid + pub side: i32, + // The order's price + pub price: f64, + // The order's size + pub size: f64, + /// Flag indicating if this is smart depth response (aggregate data from multiple exchanges, v974+) + pub smart_depth: bool, +} + +impl Subscribable for MarketDepths { + const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::MarketDepth, IncomingMessages::MarketDepthL2, IncomingMessages::Error]; + + fn decode(server_version: i32, message: &mut ResponseMessage) -> Result { + match message.message_type() { + IncomingMessages::MarketDepth => Ok(MarketDepths::MarketDepth(decoders::decode_market_depth(message)?)), + IncomingMessages::MarketDepthL2 => Ok(MarketDepths::MarketDepthL2(decoders::decode_market_depth_l2(server_version, message)?)), + IncomingMessages::Error => Ok(MarketDepths::Notice(Notice::from(message))), + _ => Err(Error::NotImplemented), + } + } + + fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { + let request_id = request_id.expect("Request ID required to encode cancel realtime bars"); + encoders::encode_cancel_tick_by_tick(request_id) + } +} + +/// Stores depth market data description. +#[derive(Debug, Default)] +pub struct DepthMarketDataDescription { + /// The exchange name + pub exchange_name: String, + /// The security type + pub security_type: String, + /// The listing exchange name + pub listing_exchange: String, + /// The service data type + pub service_data_type: String, + /// The aggregated group + pub aggregated_group: Option, +} + +#[derive(Debug)] +pub enum TickTypes { + Price(TickPrice), + Size(TickSize), + String(TickString), + EFP(TickEFP), + Generic(TickGeneric), + OptionComputation(TickOptionComputation), + SnapshotEnd, + Notice(Notice), + RequestParameters(TickRequestParameters), + PriceSize(TickPriceSize), +} + +impl Subscribable for TickTypes { + const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[ + IncomingMessages::TickPrice, + IncomingMessages::TickSize, + IncomingMessages::TickString, + IncomingMessages::TickEFP, + IncomingMessages::TickGeneric, + IncomingMessages::TickOptionComputation, + IncomingMessages::TickSnapshotEnd, + IncomingMessages::Error, + IncomingMessages::TickReqParams, + ]; + + fn decode(server_version: i32, message: &mut ResponseMessage) -> Result { + match message.message_type() { + IncomingMessages::TickPrice => Ok(decoders::decode_tick_price(server_version, message)?), + IncomingMessages::TickSize => Ok(TickTypes::Size(decoders::decode_tick_size(message)?)), + IncomingMessages::TickString => Ok(TickTypes::String(decoders::decode_tick_string(message)?)), + IncomingMessages::TickEFP => Ok(TickTypes::EFP(decoders::decode_tick_efp(message)?)), + IncomingMessages::TickGeneric => Ok(TickTypes::Generic(decoders::decode_tick_generic(message)?)), + IncomingMessages::TickOptionComputation => Ok(TickTypes::OptionComputation(decoders::decode_tick_option_computation( + server_version, + message, + )?)), + IncomingMessages::TickReqParams => Ok(TickTypes::RequestParameters(decoders::decode_tick_request_parameters(message)?)), + IncomingMessages::TickSnapshotEnd => Ok(TickTypes::SnapshotEnd), + IncomingMessages::Error => Ok(TickTypes::Notice(Notice::from(&message))), + _ => Err(Error::NotImplemented), + } + } + + fn cancel_message(_server_version: i32, request_id: Option, _context: &ResponseContext) -> Result { + let request_id = request_id.expect("Request ID required to encode cancel realtime bars"); + encoders::encode_cancel_market_data(request_id) + } +} + +#[derive(Debug, Default)] +pub struct TickPrice { + pub tick_type: TickType, + pub price: f64, + pub attributes: TickAttribute, +} + +#[derive(Debug, PartialEq, Default)] +pub struct TickAttribute { + pub can_auto_execute: bool, + pub past_limit: bool, + pub pre_open: bool, +} + +#[derive(Debug, Default)] +pub struct TickSize { + pub tick_type: TickType, + pub size: f64, +} + +#[derive(Debug, Default)] +pub struct TickPriceSize { + pub price_tick_type: TickType, + pub price: f64, + pub attributes: TickAttribute, + pub size_tick_type: TickType, + pub size: f64, +} + +#[derive(Debug, Default)] +pub struct TickString { + pub tick_type: TickType, + pub value: String, +} + +#[derive(Debug, Default)] +pub struct TickEFP { + pub tick_type: TickType, + pub basis_points: f64, + pub formatted_basis_points: String, + pub implied_futures_price: f64, + pub hold_days: i32, + pub future_last_trade_date: String, + pub dividend_impact: f64, + pub dividends_to_last_trade_date: f64, +} + +#[derive(Debug, Default)] +pub struct TickGeneric { + pub tick_type: TickType, + pub value: f64, +} + +#[derive(Debug, Default)] +pub struct TickOptionComputation {} + +#[derive(Debug, Default)] +pub struct TickRequestParameters { + pub min_tick: f64, + pub bbo_exchange: String, + pub snapshot_permissions: i32, +} + // === Implementation === // Requests realtime bars. @@ -183,15 +375,6 @@ pub(crate) fn realtime_bars<'a>( use_rth: bool, options: Vec, ) -> Result, Error> { - client.check_server_version(server_versions::REAL_TIME_BARS, "It does not support real time bars.")?; - - if !contract.trading_class.is_empty() || contract.contract_id > 0 { - client.check_server_version( - server_versions::TRADING_CLASS, - "It does not support ConId nor TradingClass parameters in reqRealTimeBars.", - )?; - } - let request_id = client.next_request_id(); let request = encoders::encode_request_realtime_bars(client.server_version(), request_id, contract, bar_size, what_to_show, use_rth, options)?; let subscription = client.send_request(request_id, request)?; @@ -205,20 +388,16 @@ pub(crate) fn tick_by_tick_all_last<'a>( contract: &Contract, number_of_ticks: i32, ignore_size: bool, -) -> Result + 'a, Error> { +) -> Result, Error> { validate_tick_by_tick_request(client, contract, number_of_ticks, ignore_size)?; let server_version = client.server_version(); let request_id = client.next_request_id(); - let message = encoders::tick_by_tick(server_version, request_id, contract, "AllLast", number_of_ticks, ignore_size)?; - let responses = client.send_request(request_id, message)?; + let request = encoders::encode_tick_by_tick(server_version, request_id, contract, "AllLast", number_of_ticks, ignore_size)?; + let subscription = client.send_request(request_id, request)?; - Ok(TradeIterator { - client, - request_id, - responses, - }) + Ok(Subscription::new(client, subscription, ResponseContext::default())) } // Validates that server supports the given request. @@ -228,7 +407,7 @@ fn validate_tick_by_tick_request(client: &Client, _contract: &Contract, number_o if number_of_ticks != 0 || ignore_size { client.check_server_version( server_versions::TICK_BY_TICK_IGNORE_SIZE, - "It does not support ignoreSize and numberOfTicks parameters in tick-by-tick requests.", + "It does not support ignore_size and number_of_ticks parameters in tick-by-tick requests.", )?; } @@ -241,20 +420,16 @@ pub(crate) fn tick_by_tick_last<'a>( contract: &Contract, number_of_ticks: i32, ignore_size: bool, -) -> Result, Error> { +) -> Result, Error> { validate_tick_by_tick_request(client, contract, number_of_ticks, ignore_size)?; let server_version = client.server_version(); let request_id = client.next_request_id(); - let message = encoders::tick_by_tick(server_version, request_id, contract, "Last", number_of_ticks, ignore_size)?; - let responses = client.send_request(request_id, message)?; + let request = encoders::encode_tick_by_tick(server_version, request_id, contract, "Last", number_of_ticks, ignore_size)?; + let subscription = client.send_request(request_id, request)?; - Ok(TradeIterator { - client, - request_id, - responses, - }) + Ok(Subscription::new(client, subscription, ResponseContext::default())) } // Requests tick by tick BidAsk ticks. @@ -263,20 +438,16 @@ pub(crate) fn tick_by_tick_bid_ask<'a>( contract: &Contract, number_of_ticks: i32, ignore_size: bool, -) -> Result, Error> { +) -> Result, Error> { validate_tick_by_tick_request(client, contract, number_of_ticks, ignore_size)?; let server_version = client.server_version(); let request_id = client.next_request_id(); - let message = encoders::tick_by_tick(server_version, request_id, contract, "BidAsk", number_of_ticks, ignore_size)?; - let responses = client.send_request(request_id, message)?; + let request = encoders::encode_tick_by_tick(server_version, request_id, contract, "BidAsk", number_of_ticks, ignore_size)?; + let subscription = client.send_request(request_id, request)?; - Ok(BidAskIterator { - client, - request_id, - responses, - }) + Ok(Subscription::new(client, subscription, ResponseContext::default())) } // Requests tick by tick MidPoint ticks. @@ -291,87 +462,75 @@ pub(crate) fn tick_by_tick_midpoint<'a>( let server_version = client.server_version(); let request_id = client.next_request_id(); - let message = encoders::tick_by_tick(server_version, request_id, contract, "MidPoint", number_of_ticks, ignore_size)?; - let subscription = client.send_request(request_id, message)?; + let request = encoders::encode_tick_by_tick(server_version, request_id, contract, "MidPoint", number_of_ticks, ignore_size)?; + let subscription = client.send_request(request_id, request)?; Ok(Subscription::new(client, subscription, ResponseContext::default())) } -// Iterators - -/// TradeIterator supports iteration over [Trade] ticks. -pub(crate) struct TradeIterator<'a> { +pub(crate) fn market_depth<'a>( client: &'a Client, - request_id: i32, - responses: InternalSubscription, -} - -impl<'a> Drop for TradeIterator<'a> { - // Ensures tick by tick request is cancelled - fn drop(&mut self) { - cancel_tick_by_tick(self.client, self.request_id); + contract: &Contract, + number_of_rows: i32, + is_smart_depth: bool, +) -> Result, Error> { + if is_smart_depth { + client.check_server_version(server_versions::SMART_DEPTH, "It does not support SMART depth request.")?; } -} - -impl<'a> Iterator for TradeIterator<'a> { - type Item = Trade; - - /// Advances the iterator and returns the next value. - fn next(&mut self) -> Option { - loop { - match self.responses.next() { - Some(Ok(mut message)) => match message.message_type() { - IncomingMessages::TickByTick => match decoders::decode_trade_tick(&mut message) { - Ok(tick) => return Some(tick), - Err(e) => error!("unexpected message {message:?}: {e:?}"), - }, - _ => error!("unexpected message {message:?}"), - }, - // TODO enumerate - _ => return None, - } - } + if !contract.primary_exchange.is_empty() { + client.check_server_version( + server_versions::MKT_DEPTH_PRIM_EXCHANGE, + "It does not support primary_exchange parameter in request_market_depth", + )?; } -} -/// BidAskIterator supports iteration over [BidAsk] ticks. -pub(crate) struct BidAskIterator<'a> { - client: &'a Client, - request_id: i32, - responses: InternalSubscription, -} + let request_id = client.next_request_id(); + let request = encoders::encode_request_market_depth(client.server_version, request_id, contract, number_of_rows, is_smart_depth)?; + let subscription = client.send_request(request_id, request)?; -/// Cancels the tick by tick request -fn cancel_tick_by_tick(client: &Client, request_id: i32) { - if client.server_version() >= server_versions::TICK_BY_TICK { - let message = encoders::cancel_tick_by_tick(request_id).unwrap(); - client.message_bus.cancel_subscription(request_id, &message).unwrap(); - } + Ok(Subscription::new(client, subscription, ResponseContext::default())) } -impl<'a> Drop for BidAskIterator<'a> { - // Ensures tick by tick request is cancelled - fn drop(&mut self) { - cancel_tick_by_tick(self.client, self.request_id); +// Requests venues for which market data is returned to market_depth (those with market makers) +pub fn market_depth_exchanges(client: &Client) -> Result, Error> { + client.check_server_version( + server_versions::REQ_MKT_DEPTH_EXCHANGES, + "It does not support market depth exchanges requests.", + )?; + + let request = encoders::encode_request_market_depth_exchanges()?; + let subscription = client.send_shared_request(OutgoingMessages::RequestMktDepthExchanges, request)?; + let response = subscription.next(); + + match response { + Some(Ok(mut message)) => Ok(decoders::decode_market_depth_exchanges(client.server_version(), &mut message)?), + Some(Err(Error::ConnectionReset)) => { + debug!("connection reset. retrying market_depth_exchanges"); + market_depth_exchanges(client) + } + Some(Err(e)) => Err(e), + None => Ok(Vec::new()), } } -impl<'a> Iterator for BidAskIterator<'a> { - type Item = BidAsk; - - /// Advances the iterator and returns the next value. - fn next(&mut self) -> Option { - loop { - match self.responses.next() { - Some(Ok(mut message)) => match message.message_type() { - IncomingMessages::TickByTick => match decoders::bid_ask_tick(&mut message) { - Ok(tick) => return Some(tick), - Err(e) => error!("unexpected message {message:?}: {e:?}"), - }, - _ => error!("unexpected message {message:?}"), - }, - _ => return None, - } - } - } +// Requests real time market data. +pub fn market_data<'a>( + client: &'a Client, + contract: &Contract, + generic_ticks: &[&str], + snapshot: bool, + regulatory_snapshot: bool, +) -> Result, Error> { + let request_id = client.next_request_id(); + let request = encoders::encode_request_market_data( + client.server_version(), + request_id, + contract, + generic_ticks, + snapshot, + regulatory_snapshot, + )?; + let subscription = client.send_request(request_id, request)?; + + Ok(Subscription::new(client, subscription, ResponseContext::default())) } diff --git a/src/market_data/realtime/decoders.rs b/src/market_data/realtime/decoders.rs index fc3174e6..f09b414d 100644 --- a/src/market_data/realtime/decoders.rs +++ b/src/market_data/realtime/decoders.rs @@ -1,9 +1,16 @@ -use crate::messages::ResponseMessage; +use crate::contracts::tick_types::TickType; use crate::Error; +use crate::{messages::ResponseMessage, server_versions}; -use super::{Bar, BidAsk, BidAskAttribute, MidPoint, Trade, TradeAttribute}; +use super::{ + Bar, BidAsk, BidAskAttribute, DepthMarketDataDescription, MarketDepth, MarketDepthL2, MidPoint, TickEFP, TickGeneric, TickOptionComputation, + TickPrice, TickPriceSize, TickRequestParameters, TickSize, TickString, TickTypes, Trade, TradeAttribute, +}; -pub(crate) fn decode_realtime_bar(message: &mut ResponseMessage) -> Result { +#[cfg(test)] +mod tests; + +pub(super) fn decode_realtime_bar(message: &mut ResponseMessage) -> Result { message.skip(); // message type message.skip(); // message version message.skip(); // message request id @@ -20,7 +27,7 @@ pub(crate) fn decode_realtime_bar(message: &mut ResponseMessage) -> Result Result { +pub(super) fn decode_trade_tick(message: &mut ResponseMessage) -> Result { message.skip(); // message type message.skip(); // message request id @@ -50,7 +57,7 @@ pub(crate) fn decode_trade_tick(message: &mut ResponseMessage) -> Result Result { +pub(super) fn decode_bid_ask_tick(message: &mut ResponseMessage) -> Result { message.skip(); // message type message.skip(); // message request id @@ -79,7 +86,7 @@ pub(crate) fn bid_ask_tick(message: &mut ResponseMessage) -> Result Result { +pub(super) fn decode_mid_point_tick(message: &mut ResponseMessage) -> Result { message.skip(); // message type message.skip(); // message request id @@ -94,62 +101,191 @@ pub(crate) fn mid_point_tick(message: &mut ResponseMessage) -> Result Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + let depth = MarketDepth { + position: message.next_int()?, + operation: message.next_int()?, + side: message.next_int()?, + price: message.next_double()?, + size: message.next_double()?, + }; + + Ok(depth) +} + +pub(super) fn decode_market_depth_l2(server_version: i32, message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + let mut depth = MarketDepthL2 { + position: message.next_int()?, + market_maker: message.next_string()?, + operation: message.next_int()?, + side: message.next_int()?, + price: message.next_double()?, + size: message.next_double()?, + ..Default::default() + }; + + if server_version >= server_versions::SMART_DEPTH { + depth.smart_depth = message.next_bool()?; } - #[test] - fn decode_bid_ask() { - let mut message = ResponseMessage::from("99\09000\03\01678745793\03895.50\03896.00\09\011\01\0"); - - let results = bid_ask_tick(&mut message); - - if let Ok(bid_ask) = results { - assert_eq!(bid_ask.time, OffsetDateTime::from_unix_timestamp(01678745793).unwrap(), "bid_ask.time"); - assert_eq!(bid_ask.bid_price, 3895.5, "bid_ask.bid_price"); - assert_eq!(bid_ask.ask_price, 3896.0, "bid_ask.ask_price"); - assert_eq!(bid_ask.bid_size, 9, "bid_ask.bid_size"); - assert_eq!(bid_ask.ask_size, 11, "bid_ask.ask_size"); - assert_eq!(bid_ask.bid_ask_attribute.bid_past_low, true, "bid_ask.bid_ask_attribute.bid_past_low"); - assert_eq!(bid_ask.bid_ask_attribute.ask_past_high, false, "bid_ask.bid_ask_attribute.ask_past_high"); - } else if let Err(err) = results { - assert!(false, "error decoding trade tick: {err}"); - } + Ok(depth) +} + +pub(super) fn decode_market_depth_exchanges(server_version: i32, message: &mut ResponseMessage) -> Result, Error> { + message.skip(); // message type + + let count = message.next_int()?; + let mut descriptions = Vec::with_capacity(count as usize); + + for _ in 0..count { + let description = if server_version >= server_versions::SERVICE_DATA_TYPE { + DepthMarketDataDescription { + exchange_name: message.next_string()?, + security_type: message.next_string()?, + listing_exchange: message.next_string()?, + service_data_type: message.next_string()?, + aggregated_group: Some(message.next_string()?), + } + } else { + DepthMarketDataDescription { + exchange_name: message.next_string()?, + security_type: message.next_string()?, + listing_exchange: "".into(), + service_data_type: if message.next_bool()? { "Deep2".into() } else { "Deep".into() }, + aggregated_group: None, + } + }; + + descriptions.push(description); } - #[test] - fn decode_mid_point() { - let mut message = ResponseMessage::from("99\09000\04\01678746113\03896.875\0"); + Ok(descriptions) +} + +pub(super) fn decode_tick_price(server_version: i32, message: &mut ResponseMessage) -> Result { + message.skip(); // message type + let message_version = message.next_int()?; + message.skip(); // message request id + + let mut tick_price = TickPrice { + tick_type: TickType::from(message.next_int()?), + price: message.next_double()?, + ..Default::default() + }; - let results = mid_point_tick(&mut message); + let size = if message_version >= 2 { message.next_double()? } else { f64::MAX }; - if let Ok(mid_point) = results { - assert_eq!(mid_point.time, OffsetDateTime::from_unix_timestamp(1678746113).unwrap(), "mid_point.time"); - assert_eq!(mid_point.mid_point, 3896.875, "mid_point.mid_point"); - } else if let Err(err) = results { - assert!(false, "error decoding mid point tick: {err}"); + if message_version >= 3 { + let mask = message.next_int()?; + + if server_version >= server_versions::PAST_LIMIT { + tick_price.attributes.can_auto_execute = mask & 0x1 != 0; + tick_price.attributes.past_limit = mask & 0x2 != 0; + + if server_version >= server_versions::PRE_OPEN_BID_ASK { + tick_price.attributes.pre_open = mask & 0x4 != 0; + } } } + + let size_tick_type = match tick_price.tick_type { + TickType::Bid => TickType::BidSize, + TickType::Ask => TickType::AskSize, + TickType::Last => TickType::LastSize, + TickType::DelayedBid => TickType::DelayedBidSize, + TickType::DelayedAsk => TickType::DelayedAskSize, + TickType::DelayedLast => TickType::DelayedLastSize, + _ => TickType::Unknown, + }; + + if message_version < 2 || size_tick_type == TickType::Unknown { + Ok(TickTypes::Price(tick_price)) + } else { + Ok(TickTypes::PriceSize(TickPriceSize { + price_tick_type: tick_price.tick_type, + price: tick_price.price, + attributes: tick_price.attributes, + size_tick_type, + size, + })) + } +} + +pub(super) fn decode_tick_size(message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + Ok(TickSize { + tick_type: TickType::from(message.next_int()?), + size: message.next_double()?, + }) +} + +pub(super) fn decode_tick_string(message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + Ok(TickString { + tick_type: TickType::from(message.next_int()?), + value: message.next_string()?, + }) +} + +pub(super) fn decode_tick_efp(message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + Ok(TickEFP { + tick_type: TickType::from(message.next_int()?), + basis_points: message.next_double()?, + formatted_basis_points: message.next_string()?, + implied_futures_price: message.next_double()?, + hold_days: message.next_int()?, + future_last_trade_date: message.next_string()?, + dividend_impact: message.next_double()?, + dividends_to_last_trade_date: message.next_double()?, + }) +} + +pub(super) fn decode_tick_generic(message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + Ok(TickGeneric { + tick_type: TickType::from(message.next_int()?), + value: message.next_double()?, + }) +} + +pub(super) fn decode_tick_option_computation(server_version: i32, message: &mut ResponseMessage) -> Result { + // use crate::contracts::decoders::decode_option_computation(); + + message.skip(); // message type + message.skip(); // message version + message.skip(); // message request id + + Ok(TickOptionComputation {}) +} + +pub(super) fn decode_tick_request_parameters(message: &mut ResponseMessage) -> Result { + message.skip(); // message type + message.skip(); // message request id + + Ok(TickRequestParameters { + min_tick: message.next_double()?, + bbo_exchange: message.next_string()?, + snapshot_permissions: message.next_int()?, + }) } diff --git a/src/market_data/realtime/decoders/tests.rs b/src/market_data/realtime/decoders/tests.rs new file mode 100644 index 00000000..8e9368a8 --- /dev/null +++ b/src/market_data/realtime/decoders/tests.rs @@ -0,0 +1,56 @@ +use time::OffsetDateTime; + +use super::*; + +#[test] +fn decode_trade() { + let mut message = ResponseMessage::from("99\09000\01\01678740829\03895.25\07\02\0\0\0"); + + let results = decode_trade_tick(&mut message); + + if let Ok(trade) = results { + assert_eq!(trade.tick_type, "1", "trade.tick_type"); + assert_eq!(trade.time, OffsetDateTime::from_unix_timestamp(1678740829).unwrap(), "trade.time"); + assert_eq!(trade.price, 3895.25, "trade.price"); + assert_eq!(trade.size, 7, "trade.size"); + assert_eq!(trade.trade_attribute.past_limit, false, "trade.trade_attribute.past_limit"); + assert_eq!(trade.trade_attribute.unreported, true, "trade.trade_attribute.unreported"); + assert_eq!(trade.exchange, "", "trade.exchange"); + assert_eq!(trade.special_conditions, "", "trade.special_conditions"); + } else if let Err(err) = results { + assert!(false, "error decoding trade tick: {err}"); + } +} + +#[test] +fn decode_bid_ask() { + let mut message = ResponseMessage::from("99\09000\03\01678745793\03895.50\03896.00\09\011\01\0"); + + let results = decode_bid_ask_tick(&mut message); + + if let Ok(bid_ask) = results { + assert_eq!(bid_ask.time, OffsetDateTime::from_unix_timestamp(01678745793).unwrap(), "bid_ask.time"); + assert_eq!(bid_ask.bid_price, 3895.5, "bid_ask.bid_price"); + assert_eq!(bid_ask.ask_price, 3896.0, "bid_ask.ask_price"); + assert_eq!(bid_ask.bid_size, 9, "bid_ask.bid_size"); + assert_eq!(bid_ask.ask_size, 11, "bid_ask.ask_size"); + assert_eq!(bid_ask.bid_ask_attribute.bid_past_low, true, "bid_ask.bid_ask_attribute.bid_past_low"); + assert_eq!(bid_ask.bid_ask_attribute.ask_past_high, false, "bid_ask.bid_ask_attribute.ask_past_high"); + } else if let Err(err) = results { + assert!(false, "error decoding trade tick: {err}"); + } +} + +#[test] +fn decode_mid_point() { + let mut message = ResponseMessage::from("99\09000\04\01678746113\03896.875\0"); + + let results = decode_mid_point_tick(&mut message); + + if let Ok(mid_point) = results { + assert_eq!(mid_point.time, OffsetDateTime::from_unix_timestamp(1678746113).unwrap(), "mid_point.time"); + assert_eq!(mid_point.mid_point, 3896.875, "mid_point.mid_point"); + } else if let Err(err) = results { + assert!(false, "error decoding mid point tick: {err}"); + } +} diff --git a/src/market_data/realtime/encoders.rs b/src/market_data/realtime/encoders.rs index f04679f0..8b240984 100644 --- a/src/market_data/realtime/encoders.rs +++ b/src/market_data/realtime/encoders.rs @@ -1,11 +1,15 @@ use super::{BarSize, WhatToShow}; use crate::contracts::Contract; +use crate::contracts::SecurityType; use crate::messages::OutgoingMessages; use crate::messages::RequestMessage; use crate::orders::TagValue; use crate::{server_versions, Error}; -pub(crate) fn encode_request_realtime_bars( +#[cfg(test)] +mod tests; + +pub(super) fn encode_request_realtime_bars( server_version: i32, ticker_id: i32, contract: &Contract, @@ -52,7 +56,7 @@ pub(crate) fn encode_request_realtime_bars( Ok(packet) } -pub(crate) fn encode_cancel_realtime_bars(request_id: i32) -> Result { +pub(super) fn encode_cancel_realtime_bars(request_id: i32) -> Result { const VERSION: i32 = 1; let mut message = RequestMessage::default(); @@ -64,7 +68,7 @@ pub(crate) fn encode_cancel_realtime_bars(request_id: i32) -> Result Result { +pub(super) fn encode_cancel_tick_by_tick(request_id: i32) -> Result { let mut message = RequestMessage::default(); message.push_field(&OutgoingMessages::CancelTickByTickData); @@ -107,125 +111,126 @@ pub(crate) fn cancel_tick_by_tick(request_id: i32) -> Result Result { + const VERSION: i32 = 5; + + let mut message = RequestMessage::new(); + + message.push_field(&OutgoingMessages::RequestMarketDepth); + message.push_field(&VERSION); + message.push_field(&request_id); + // Contract fields + if server_version >= server_versions::TRADING_CLASS { + message.push_field(&contract.contract_id); + } + message.push_field(&contract.symbol); + message.push_field(&contract.security_type); + message.push_field(&contract.last_trade_date_or_contract_month); + message.push_field(&contract.strike); + message.push_field(&contract.right); + message.push_field(&contract.multiplier); + message.push_field(&contract.exchange); + if server_version >= server_versions::MKT_DEPTH_PRIM_EXCHANGE { + message.push_field(&contract.primary_exchange); + } + message.push_field(&contract.currency); + message.push_field(&contract.local_symbol); + if server_version >= server_versions::TRADING_CLASS { + message.push_field(&contract.trading_class); + } + message.push_field(&number_of_rows); + if server_version >= server_versions::SMART_DEPTH { + message.push_field(&is_smart_depth); + } + if server_version >= server_versions::LINKING { + message.push_field(&""); + } - use super::*; + Ok(message) +} - #[test] - fn cancel_tick_by_tick() { - let request_id = 9000; +pub(super) fn encode_request_market_depth_exchanges() -> Result { + let mut message = RequestMessage::new(); - let results = super::cancel_tick_by_tick(request_id); + message.push_field(&OutgoingMessages::RequestMktDepthExchanges); - match results { - Ok(message) => { - assert_eq!(message[0], "98", "message.type"); - assert_eq!(message[1], request_id.to_string(), "message.request_id"); - } - Err(err) => { - assert!(false, "error encoding cancel_tick_by_tick request: {err}"); - } - } - } + Ok(message) +} - #[test] - fn cancel_realtime_bars() { - let request_id = 9000; - - let results = super::encode_cancel_realtime_bars(request_id); - - match results { - Ok(message) => { - assert_eq!(message[0], OutgoingMessages::CancelRealTimeBars.to_field(), "message.type"); - assert_eq!(message[1], "1", "message.version"); - assert_eq!(message[2], request_id.to_string(), "message.request_id"); - } - Err(err) => { - assert!(false, "error encoding cancel_tick_by_tick request: {err}"); - } +pub(super) fn encode_request_market_data( + server_version: i32, + request_id: i32, + contract: &Contract, + generic_ticks: &[&str], + snapshot: bool, + regulatory_snapshot: bool, +) -> Result { + const VERSION: i32 = 11; + + let mut message = RequestMessage::new(); + + message.push_field(&OutgoingMessages::RequestMarketData); + message.push_field(&VERSION); + message.push_field(&request_id); + message.push_field(&contract.contract_id); + message.push_field(&contract.symbol); + message.push_field(&contract.security_type); + message.push_field(&contract.last_trade_date_or_contract_month); + message.push_field(&contract.strike); + message.push_field(&contract.right); + message.push_field(&contract.multiplier); + message.push_field(&contract.exchange); + message.push_field(&contract.primary_exchange); + message.push_field(&contract.currency); + message.push_field(&contract.local_symbol); + message.push_field(&contract.trading_class); + + if contract.security_type == SecurityType::Spread { + message.push_field(&contract.combo_legs.len()); + + for leg in &contract.combo_legs { + message.push_field(&leg.contract_id); + message.push_field(&leg.ratio); + message.push_field(&leg.action); + message.push_field(&leg.exchange); } } - #[test] - fn tick_by_tick() { - let request_id = 9000; - let server_version = server_versions::TICK_BY_TICK; - let contract = contract_samples::simple_future(); - let tick_type = "AllLast"; - let number_of_ticks = 1; - let ignore_size = true; - - let results = super::tick_by_tick(server_version, request_id, &contract, tick_type, number_of_ticks, ignore_size); - - match results { - Ok(message) => { - assert_eq!(message[0], OutgoingMessages::RequestTickByTickData.to_field(), "message.type"); - assert_eq!(message[1], request_id.to_field(), "message.request_id"); - assert_eq!(message[2], contract.contract_id.to_field(), "message.contract_id"); - assert_eq!(message[3], contract.symbol, "message.symbol"); - assert_eq!(message[4], contract.security_type.to_field(), "message.security_type"); - assert_eq!( - message[5], contract.last_trade_date_or_contract_month, - "message.last_trade_date_or_contract_month" - ); - assert_eq!(message[6], contract.strike.to_field(), "message.strike"); - assert_eq!(message[7], contract.right, "message.right"); - assert_eq!(message[8], contract.multiplier, "message.multiplier"); - assert_eq!(message[9], contract.exchange, "message.exchange"); - assert_eq!(message[10], contract.primary_exchange, "message.primary_exchange"); - assert_eq!(message[11], contract.currency, "message.currency"); - assert_eq!(message[12], contract.local_symbol, "message.local_symbol"); - assert_eq!(message[13], contract.trading_class, "message.trading_class"); - assert_eq!(message[14], tick_type, "message.tick_type"); - } - Err(err) => { - assert!(false, "error encoding tick_by_tick request: {err}"); - } - } + if let Some(delta_neutral_contract) = &contract.delta_neutral_contract { + message.push_field(&true); + message.push_field(&delta_neutral_contract.contract_id); + message.push_field(&delta_neutral_contract.delta); + message.push_field(&delta_neutral_contract.price); + } else { + message.push_field(&false); } - #[test] - fn realtime_bars() { - let request_id = 9000; - let server_version = server_versions::TICK_BY_TICK; - let contract = contract_samples::simple_future(); - let bar_size = BarSize::Sec5; - let what_to_show = WhatToShow::Trades; - let use_rth = true; - let options = vec![]; - - let results = super::encode_request_realtime_bars(server_version, request_id, &contract, &bar_size, &what_to_show, use_rth, options); - - match results { - Ok(message) => { - assert_eq!(message[0], OutgoingMessages::RequestRealTimeBars.to_field(), "message.type"); - assert_eq!(message[1], "8", "message.version"); - assert_eq!(message[2], request_id.to_field(), "message.request_id"); - assert_eq!(message[3], contract.contract_id.to_field(), "message.contract_id"); - assert_eq!(message[4], contract.symbol, "message.symbol"); - assert_eq!(message[5], contract.security_type.to_field(), "message.security_type"); - assert_eq!( - message[6], contract.last_trade_date_or_contract_month, - "message.last_trade_date_or_contract_month" - ); - assert_eq!(message[7], contract.strike.to_field(), "message.strike"); - assert_eq!(message[8], contract.right, "message.right"); - assert_eq!(message[9], contract.multiplier, "message.multiplier"); - assert_eq!(message[10], contract.exchange, "message.exchange"); - assert_eq!(message[11], contract.primary_exchange, "message.primary_exchange"); - assert_eq!(message[12], contract.currency, "message.currency"); - assert_eq!(message[13], contract.local_symbol, "message.local_symbol"); - assert_eq!(message[14], contract.trading_class, "message.trading_class"); - assert_eq!(message[15], "0", "message.bar_size"); - assert_eq!(message[16], what_to_show.to_field(), "message.what_to_show"); // implement to_field - assert_eq!(message[17], use_rth.to_field(), "message.use_rth"); - assert_eq!(message[18], "", "message.options"); // TODO what should this be? - } - Err(err) => { - assert!(false, "error encoding realtime_bars request: {err}"); - } - } + message.push_field(&generic_ticks.join(",")); + message.push_field(&snapshot); + + if server_version >= server_versions::REQ_SMART_COMPONENTS { + message.push_field(®ulatory_snapshot); } + + message.push_field(&""); + + Ok(message) +} + +pub(super) fn encode_cancel_market_data(request_id: i32) -> Result { + let mut message = RequestMessage::new(); + + const VERSION: i32 = 1; + + message.push_field(&OutgoingMessages::CancelMarketData); + message.push_field(&VERSION); + message.push_field(&request_id); + + Ok(message) } diff --git a/src/market_data/realtime/encoders/tests.rs b/src/market_data/realtime/encoders/tests.rs new file mode 100644 index 00000000..7fa0ea25 --- /dev/null +++ b/src/market_data/realtime/encoders/tests.rs @@ -0,0 +1,92 @@ +use crate::{contracts::contract_samples, ToField}; + +use super::*; + +#[test] +fn test_cancel_tick_by_tick() { + let request_id = 9000; + let message = super::encode_cancel_tick_by_tick(request_id).expect("error encoding cancel_tick_by_tick"); + + assert_eq!(message[0], "98", "message.type"); + assert_eq!(message[1], request_id.to_string(), "message.request_id"); +} + +#[test] +fn test_cancel_realtime_bars() { + let request_id = 9000; + + let message = super::encode_cancel_realtime_bars(request_id).expect("error encoding cancel_tick_by_tick"); + + assert_eq!(message[0], OutgoingMessages::CancelRealTimeBars.to_field(), "message.type"); + assert_eq!(message[1], "1", "message.version"); + assert_eq!(message[2], request_id.to_string(), "message.request_id"); +} + +#[test] +fn test_tick_by_tick() { + let request_id = 9000; + let server_version = server_versions::TICK_BY_TICK; + let contract = contract_samples::simple_future(); + let tick_type = "AllLast"; + let number_of_ticks = 1; + let ignore_size = true; + + let message = super::encode_tick_by_tick(server_version, request_id, &contract, tick_type, number_of_ticks, ignore_size) + .expect("error encoding tick_by_tick"); + + assert_eq!(message[0], OutgoingMessages::RequestTickByTickData.to_field(), "message.type"); + assert_eq!(message[1], request_id.to_field(), "message.request_id"); + assert_eq!(message[2], contract.contract_id.to_field(), "message.contract_id"); + assert_eq!(message[3], contract.symbol, "message.symbol"); + assert_eq!(message[4], contract.security_type.to_field(), "message.security_type"); + assert_eq!( + message[5], contract.last_trade_date_or_contract_month, + "message.last_trade_date_or_contract_month" + ); + assert_eq!(message[6], contract.strike.to_field(), "message.strike"); + assert_eq!(message[7], contract.right, "message.right"); + assert_eq!(message[8], contract.multiplier, "message.multiplier"); + assert_eq!(message[9], contract.exchange, "message.exchange"); + assert_eq!(message[10], contract.primary_exchange, "message.primary_exchange"); + assert_eq!(message[11], contract.currency, "message.currency"); + assert_eq!(message[12], contract.local_symbol, "message.local_symbol"); + assert_eq!(message[13], contract.trading_class, "message.trading_class"); + assert_eq!(message[14], tick_type, "message.tick_type"); +} + +#[test] +fn test_realtime_bars() { + let request_id = 9000; + let server_version = server_versions::TICK_BY_TICK; + let contract = contract_samples::simple_future(); + let bar_size = BarSize::Sec5; + let what_to_show = WhatToShow::Trades; + let use_rth = true; + let options = vec![]; + + let message = super::encode_request_realtime_bars(server_version, request_id, &contract, &bar_size, &what_to_show, use_rth, options) + .expect("error encoding realtime_bars"); + + assert_eq!(message[0], OutgoingMessages::RequestRealTimeBars.to_field(), "message.type"); + assert_eq!(message[1], "8", "message.version"); + assert_eq!(message[2], request_id.to_field(), "message.request_id"); + assert_eq!(message[3], contract.contract_id.to_field(), "message.contract_id"); + assert_eq!(message[4], contract.symbol, "message.symbol"); + assert_eq!(message[5], contract.security_type.to_field(), "message.security_type"); + assert_eq!( + message[6], contract.last_trade_date_or_contract_month, + "message.last_trade_date_or_contract_month" + ); + assert_eq!(message[7], contract.strike.to_field(), "message.strike"); + assert_eq!(message[8], contract.right, "message.right"); + assert_eq!(message[9], contract.multiplier, "message.multiplier"); + assert_eq!(message[10], contract.exchange, "message.exchange"); + assert_eq!(message[11], contract.primary_exchange, "message.primary_exchange"); + assert_eq!(message[12], contract.currency, "message.currency"); + assert_eq!(message[13], contract.local_symbol, "message.local_symbol"); + assert_eq!(message[14], contract.trading_class, "message.trading_class"); + assert_eq!(message[15], "0", "message.bar_size"); + assert_eq!(message[16], what_to_show.to_field(), "message.what_to_show"); // implement to_field + assert_eq!(message[17], use_rth.to_field(), "message.use_rth"); + assert_eq!(message[18], "", "message.options"); // TODO what should this be? +} diff --git a/src/market_data/realtime/tests.rs b/src/market_data/realtime/tests.rs index b501beac..7e0bd7e7 100644 --- a/src/market_data/realtime/tests.rs +++ b/src/market_data/realtime/tests.rs @@ -6,12 +6,15 @@ use time::OffsetDateTime; use crate::contracts::contract_samples; use crate::messages::OutgoingMessages; use crate::stubs::MessageBusStub; +use crate::testdata::responses::{ + MARKET_DEPTH_1, MARKET_DEPTH_2, MARKET_DEPTH_3, MARKET_DEPTH_4, MARKET_DEPTH_5, MARKET_DEPTH_6, MARKET_DEPTH_7, MARKET_DEPTH_8, MARKET_DEPTH_9, +}; use crate::ToField; use super::*; #[test] -fn realtime_bars() { +fn test_realtime_bars() { let message_bus = Arc::new(MessageBusStub { request_messages: RwLock::new(vec![]), response_messages: vec!["50|3|9001|1678323335|4028.75|4029.00|4028.25|4028.50|2|4026.75|1|".to_owned()], @@ -90,9 +93,91 @@ fn realtime_bars() { } #[test] -fn what_to_show() { +fn test_what_to_show() { assert_eq!(WhatToShow::Trades.to_string(), "TRADES"); assert_eq!(WhatToShow::MidPoint.to_string(), "MIDPOINT"); assert_eq!(WhatToShow::Bid.to_string(), "BID"); assert_eq!(WhatToShow::Ask.to_string(), "ASK"); } + +#[test] +fn test_market_depth() { + let message_bus = Arc::new(MessageBusStub { + request_messages: RwLock::new(vec![]), + response_messages: vec![ + MARKET_DEPTH_1.into(), + MARKET_DEPTH_2.into(), + MARKET_DEPTH_3.into(), + MARKET_DEPTH_4.into(), + MARKET_DEPTH_5.into(), + MARKET_DEPTH_6.into(), + MARKET_DEPTH_7.into(), + MARKET_DEPTH_8.into(), + MARKET_DEPTH_9.into(), + ], + }); + + let client = Client::stubbed(message_bus, server_versions::SIZE_RULES); + + let contract = Contract::stock("AAPL"); + let number_of_rows = 5; + let smart_depth = true; + + let subscription = client + .market_depth(&contract, number_of_rows, smart_depth) + .expect("error requesting market depth"); + + // Verify Request + + let request_messages = client.message_bus.request_messages(); + + assert_eq!(request_messages.len(), 1); + + let message = &request_messages[0]; + + const VERSION: i32 = 5; + assert_eq!(message[0], OutgoingMessages::RequestMarketDepth.to_field(), "message.message_type"); + assert_eq!(message[1], VERSION.to_field(), "message.version"); + assert_eq!(message[2], 9000.to_field(), "message.request_id"); + assert_eq!(message[3], contract.contract_id.to_field(), "message.contract_id"); + assert_eq!(message[4], contract.symbol.to_field(), "message.symbol"); + assert_eq!(message[5], contract.security_type.to_field(), "message.security_type"); + assert_eq!( + message[6], + contract.last_trade_date_or_contract_month.to_field(), + "message.last_trade_date_or_contract_month" + ); + assert_eq!(message[7], contract.strike.to_field(), "message.strike"); + assert_eq!(message[8], contract.right.to_field(), "message.right"); + assert_eq!(message[9], contract.multiplier.to_field(), "message.multiplier"); + assert_eq!(message[10], contract.exchange.to_field(), "message.exchange"); + assert_eq!(message[11], contract.primary_exchange.to_field(), "message.primary_exchange"); + assert_eq!(message[12], contract.currency.to_field(), "message.currency"); + assert_eq!(message[13], contract.local_symbol.to_field(), "message.local_symbol"); + assert_eq!(message[14], contract.trading_class.to_field(), "message.trading_class"); + assert_eq!(message[15], number_of_rows.to_field(), "message.number_of_rows"); + assert_eq!(message[16], smart_depth.to_field(), "message.smart_depth"); + assert_eq!(message[17], "", "message.options"); + + // Verify Responses + + let responses: Vec = subscription.iter().take(5).collect(); + + if let MarketDepths::MarketDepthL2(depth) = &responses[0] { + assert_eq!(depth.position, 0, "depth.position"); + assert_eq!(depth.side, 1, "depth.side"); + assert_eq!(depth.market_maker, "OVERNIGHT", "depth.market_maker"); + assert_eq!(depth.price, 235.84, "depth.price"); + assert_eq!(depth.size, 300.0, "depth.size"); + } else { + panic!("unexpected response"); + } + + if let MarketDepths::MarketDepthL2(depth) = &responses[1] { + assert_eq!(depth.position, 0, "depth.position"); + assert_eq!(depth.side, 0, "depth.side"); + assert_eq!(depth.market_maker, "OVERNIGHT", "depth.market_maker"); + } else { + panic!("unexpected response"); + } +} diff --git a/src/market_data/realtime/tick_types.rs b/src/market_data/realtime/tick_types.rs new file mode 100644 index 00000000..5469e22e --- /dev/null +++ b/src/market_data/realtime/tick_types.rs @@ -0,0 +1,105 @@ +/* +Description Generic tick required Delivery Method Tick Id +Disable Default Market Data Disables standard market data stream and allows the TWS & API feed to prioritize other listed generic tick types. mdoff – – +Bid Size Number of contracts or lots offered at the bid price. – IBApi.EWrapper.tickSize 0 +Bid Price Highest priced bid for the contract. – IBApi.EWrapper.tickPrice 1 +Ask Price Lowest price offer on the contract. – IBApi.EWrapper.tickPrice 2 +Ask Size Number of contracts or lots offered at the ask price. – IBApi.EWrapper.tickSize 3 +Last Price Last price at which the contract traded (does not include some trades in RTVolume). – IBApi.EWrapper.tickPrice 4 +Last Size Number of contracts or lots traded at the last price. – IBApi.EWrapper.tickSize 5 +High High price for the day. – IBApi.EWrapper.tickPrice 6 +Low Low price for the day. – IBApi.EWrapper.tickPrice 7 +Volume Trading volume for the day for the selected contract (US Stocks: multiplier 100). – IBApi.EWrapper.tickSize 8 +Close Price “The last available closing price for the previous day. For US Equities we use corporate action processing to get the closing price so the close price is adjusted to reflect forward and reverse splits and cash and stock dividends.” – IBApi.EWrapper.tickPrice 9 +Bid Option Computation Computed Greeks and implied volatility based on the underlying stock price and the option bid price. See Option Greeks – IBApi.EWrapper.tickOptionComputation 10 +Ask Option Computation Computed Greeks and implied volatility based on the underlying stock price and the option ask price. See Option Greeks – IBApi.EWrapper.tickOptionComputation 11 +Last Option Computation Computed Greeks and implied volatility based on the underlying stock price and the option last traded price. See Option Greeks – IBApi.EWrapper.tickOptionComputation 12 +Model Option Computation Computed Greeks and implied volatility based on the underlying stock price and the option model price. Correspond to greeks shown in TWS. See Option Greeks – IBApi.EWrapper.tickOptionComputation 13 +Open Tick Current session’s opening price. Before open will refer to previous day. The official opening price requires a market data subscription to the native exchange of the instrument. – IBApi.EWrapper.tickPrice 14 +Low 13 Weeks Lowest price for the last 13 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 15 +High 13 Weeks Highest price for the last 13 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 16 +Low 26 Weeks Lowest price for the last 26 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 17 +High 26 Weeks Highest price for the last 26 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 18 +Low 52 Weeks Lowest price for the last 52 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 19 +High 52 Weeks Highest price for the last 52 weeks. For stocks only. 165 IBApi.EWrapper.tickPrice 20 +Average Volume The average daily trading volume over 90 days. Multiplier of 100. For stocks only. 165 IBApi.EWrapper.tickSize 21 +Open Interest “(Deprecated not currently in use) Total number of options that are not closed.” – IBApi.EWrapper.tickSize 22 +Option Historical Volatility The 30-day historical volatility (currently for stocks). 104 IBApi.EWrapper.tickGeneric 23 +Option Implied Volatility “A prediction of how volatile an underlying will be in the future. The IB 30-day volatility is the at-market volatility estimated for a maturity thirty calendar days forward of the current trading day and is based on option prices from two consecutive expiration months.” 106 IBApi.EWrapper.tickGeneric 24 +Option Bid Exchange Not Used. – IBApi.EWrapper.tickString 25 +Option Ask Exchange Not Used. – IBApi.EWrapper.tickString 26 +Option Call Open Interest Call option open interest. 101 IBApi.EWrapper.tickSize 27 +Option Put Open Interest Put option open interest. 101 IBApi.EWrapper.tickSize 28 +Option Call Volume Call option volume for the trading day. 100 IBApi.EWrapper.tickSize 29 +Option Put Volume Put option volume for the trading day. 100 IBApi.EWrapper.tickSize 30 +Index Future Premium The number of points that the index is over the cash index. 162 IBApi.EWrapper.tickGeneric 31 +Bid Exchange “For stock and options identifies the exchange(s) posting the bid price. See Component Exchanges” – IBApi.EWrapper.tickString 32 +Ask Exchange “For stock and options identifies the exchange(s) posting the ask price. See Component Exchanges” – IBApi.EWrapper.tickString 33 +Auction Volume The number of shares that would trade if no new orders were received and the auction were held now. 225 IBApi.EWrapper.tickSize 34 +Auction Price The price at which the auction would occur if no new orders were received and the auction were held now- the indicative price for the auction. Typically received after Auction imbalance (tick type 36) 225 IBApi.EWrapper.tickPrice 35 +Auction Imbalance The number of unmatched shares for the next auction; returns how many more shares are on one side of the auction than the other. Typically received after Auction Volume (tick type 34) 225 IBApi.EWrapper.tickSize 36 +Mark Price “The mark price is the current theoretical calculated value of an instrument. Since it is a calculated value it will typically have many digits of precision.” 232 IBApi.EWrapper.tickPrice 37 +Bid EFP Computation Computed EFP bid price – IBApi.EWrapper.tickEFP 38 +Ask EFP Computation Computed EFP ask price – IBApi.EWrapper.tickEFP 39 +Last EFP Computation Computed EFP last price – IBApi.EWrapper.tickEFP 40 +Open EFP Computation Computed EFP open price – IBApi.EWrapper.tickEFP 41 +High EFP Computation Computed high EFP traded price for the day – IBApi.EWrapper.tickEFP 42 +Low EFP Computation Computed low EFP traded price for the day – IBApi.EWrapper.tickEFP 43 +Close EFP Computation Computed closing EFP price for previous day – IBApi.EWrapper.tickEFP 44 +Last Timestamp Time of the last trade (in UNIX time). – IBApi.EWrapper.tickString 45 +Shortable Describes the level of difficulty with which the contract can be sold short. See Shortable 236 IBApi.EWrapper.tickGeneric 46 +RT Volume (Time & Sales) “Last trade details (Including both “”Last”” and “”Unreportable Last”” trades). See RT Volume” 233 IBApi.EWrapper.tickString 48 +Halted Indicates if a contract is halted. See Halted – IBApi.EWrapper.tickGeneric 49 +Bid Yield Implied yield of the bond if it is purchased at the current bid. – IBApi.EWrapper.tickPrice 50 +Ask Yield Implied yield of the bond if it is purchased at the current ask. – IBApi.EWrapper.tickPrice 51 +Last Yield Implied yield of the bond if it is purchased at the last price. – IBApi.EWrapper.tickPrice 52 +Custom Option Computation Greek values are based off a user customized price. – IBApi.EWrapper.tickOptionComputation 53 +Trade Count Trade count for the day. 293 IBApi.EWrapper.tickGeneric 54 +Trade Rate Trade count per minute. 294 IBApi.EWrapper.tickGeneric 55 +Volume Rate Volume per minute. 295 IBApi.EWrapper.tickGeneric 56 +Last RTH Trade Last Regular Trading Hours traded price. 318 IBApi.EWrapper.tickPrice 57 +RT Historical Volatility 30-day real time historical volatility. 411 IBApi.EWrapper.tickGeneric 58 +IB Dividends Contract’s dividends. See IB Dividends. 456 IBApi.EWrapper.tickString 59 +Bond Factor Multiplier The bond factor is a number that indicates the ratio of the current bond principal to the original principal 460 IBApi.EWrapper.tickGeneric 60 +Regulatory Imbalance The imbalance that is used to determine which at-the-open or at-the-close orders can be entered following the publishing of the regulatory imbalance. 225 IBApi.EWrapper.tickSize 61 +News Contract’s news feed. 292 IBApi.EWrapper.tickString 62 +Short-Term Volume 3 Minutes The past three minutes volume. Interpolation may be applied. For stocks only. 595 IBApi.EWrapper.tickSize 63 +Short-Term Volume 5 Minutes The past five minutes volume. Interpolation may be applied. For stocks only. 595 IBApi.EWrapper.tickSize 64 +Short-Term Volume 10 Minutes The past ten minutes volume. Interpolation may be applied. For stocks only. 595 IBApi.EWrapper.tickSize 65 +Delayed Bid Delayed bid price. See Market Data Types. – IBApi.EWrapper.tickPrice 66 +Delayed Ask Delayed ask price. See Market Data Types. – IBApi.EWrapper.tickPrice 67 +Delayed Last Delayed last traded price. See Market Data Types. – IBApi.EWrapper.tickPrice 68 +Delayed Bid Size Delayed bid size. See Market Data Types. – IBApi.EWrapper.tickSize 69 +Delayed Ask Size Delayed ask size. See Market Data Types. – IBApi.EWrapper.tickSize 70 +Delayed Last Size Delayed last size. See Market Data Types. – IBApi.EWrapper.tickSize 71 +Delayed High Price Delayed highest price of the day. See Market Data Types. – IBApi.EWrapper.tickPrice 72 +Delayed Low Price Delayed lowest price of the day. See Market Data Types – IBApi.EWrapper.tickPrice 73 +Delayed Volume Delayed traded volume of the day. See Market Data Types – IBApi.EWrapper.tickSize 74 +Delayed Close The prior day’s closing price. – IBApi.EWrapper.tickPrice 75 +Delayed Open Not currently available – IBApi.EWrapper.tickPrice 76 +RT Trade Volume “Last trade details that excludes “”Unreportable Trades””. See RT Trade Volume” 375 IBApi.EWrapper.tickString 77 +Creditman mark price Not currently available IBApi.EWrapper.tickPrice 78 +Creditman slow mark price Slower mark price update used in system calculations 619 IBApi.EWrapper.tickPrice 79 +Delayed Bid Option Computed greeks based on delayed bid price. See Market Data Types and Option Greeks. IBApi.EWrapper.tickPrice 80 +Delayed Ask Option Computed greeks based on delayed ask price. See Market Data Types and Option Greeks. IBApi.EWrapper.tickPrice 81 +Delayed Last Option Computed greeks based on delayed last price. See Market Data Types and Option Greeks. IBApi.EWrapper.tickPrice 82 +Delayed Model Option Computed Greeks and model’s implied volatility based on delayed stock and option prices. IBApi.EWrapper.tickPrice 83 +Last Exchange Exchange of last traded price IBApi.EWrapper.tickString 84 +Last Regulatory Time Timestamp (in Unix ms time) of last trade returned with regulatory snapshot IBApi.EWrapper.tickString 85 +Futures Open Interest Total number of outstanding futures contracts. *HSI open interest requested with generic tick 101 588 IBApi.EWrapper.tickSize 86 +Average Option Volume Average volume of the corresponding option contracts(TWS Build 970+ is required) 105 IBApi.EWrapper.tickSize 87 +Delayed Last Timestamp Delayed time of the last trade (in UNIX time) (TWS Build 970+ is required) IBApi.EWrapper.tickString 88 +Shortable Shares Number of shares available to short (TWS Build 974+ is required) 236 IBApi.EWrapper.tickSize 89 +ETF Nav Close Today’s closing price of ETF’s Net Asset Value (NAV). Calculation is based on prices of ETF’s underlying securities. 578 IBApi.EWrapper.tickPrice 92 +ETF Nav Prior Close Yesterday’s closing price of ETF’s Net Asset Value (NAV). Calculation is based on prices of ETF’s underlying securities. 578 IBApi.EWrapper.tickPrice 93 +ETF Nav Bid The bid price of ETF’s Net Asset Value (NAV). Calculation is based on prices of ETF’s underlying securities. 576 IBApi.EWrapper.tickPrice 94 +ETF Nav Ask The ask price of ETF’s Net Asset Value (NAV). Calculation is based on prices of ETF’s underlying securities. 576 IBApi.EWrapper.tickPrice 95 +ETF Nav Last The last price of Net Asset Value (NAV). For ETFs: Calculation is based on prices of ETF’s underlying securities. For NextShares: Value is provided by NASDAQ 577 IBApi.EWrapper.tickPrice 96 +ETF Nav Frozen Last ETF Nav Last for Frozen data 623 IBApi.EWrapper.tickPrice 97 +ETF Nav High The high price of ETF’s Net Asset Value (NAV) 614 IBApi.EWrapper.tickPrice 98 +ETF Nav Low The low price of ETF’s Net Asset Value (NAV) 614 IBApi.EWrapper.tickPrice 99 +Estimated IPO – Midpoint Midpoint is calculated based on IPO price range 586 IBApi.EWrapper.tickGeneric 101 +Final IPO Price Final price for IPO 586 IBApi.EWrapper.tickGeneric 102 +Delayed Yield Bid Delayed implied yield of the bond if it is purchased at the current bid. – IBApi.EWrapper.tickPrice 103 +Delayed Yield Ask Delayed implied yield of the bond if it is purchased at the current ask. – IBApi.EWrapper.tickPrice 104 +*/ \ No newline at end of file diff --git a/src/messages.rs b/src/messages.rs index 46d0515f..5a660448 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,16 +1,26 @@ use std::ops::Index; use std::str::{self, FromStr}; +use std::fmt::Display; use log::debug; use time::OffsetDateTime; use crate::{Error, ToField}; +pub(crate) mod shared_channel_configuration; +#[cfg(test)] +mod tests; + const INFINITY_STR: &str = "Infinity"; const UNSET_DOUBLE: &str = "1.7976931348623157E308"; const UNSET_INTEGER: &str = "2147483647"; const UNSET_LONG: &str = "9223372036854775807"; +// Index of message text in the response message +pub(crate) const MESSAGE_INDEX: usize = 4; +// Index of message code in the response message +pub(crate) const CODE_INDEX: usize = 3; + #[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)] pub enum IncomingMessages { NotValid = -1, @@ -36,7 +46,7 @@ pub enum IncomingMessages { ScannerData = 20, TickOptionComputation = 21, TickGeneric = 45, - Tickstring = 46, + TickString = 46, TickEFP = 47, //TICK EFP 47 CurrentTime = 49, RealTimeBars = 50, @@ -123,7 +133,7 @@ impl From for IncomingMessages { 20 => IncomingMessages::ScannerData, 21 => IncomingMessages::TickOptionComputation, 45 => IncomingMessages::TickGeneric, - 46 => IncomingMessages::Tickstring, + 46 => IncomingMessages::TickString, 47 => IncomingMessages::TickEFP, //TICK EFP 47 49 => IncomingMessages::CurrentTime, 50 => IncomingMessages::RealTimeBars, @@ -220,7 +230,16 @@ pub fn request_id_index(kind: IncomingMessages) -> Option { | IncomingMessages::AccountSummary | IncomingMessages::AccountSummaryEnd | IncomingMessages::AccountUpdateMulti - | IncomingMessages::AccountUpdateMultiEnd => Some(2), + | IncomingMessages::AccountUpdateMultiEnd + | IncomingMessages::MarketDepth + | IncomingMessages::MarketDepthL2 + | IncomingMessages::TickSnapshotEnd + | IncomingMessages::TickPrice + | IncomingMessages::TickSize + | IncomingMessages::TickString + | IncomingMessages::TickEFP + | IncomingMessages::TickReqParams + | IncomingMessages::TickGeneric => Some(2), _ => { debug!("could not determine request id index for {kind:?}"); None @@ -552,6 +571,24 @@ impl ResponseMessage { } } -pub(crate) mod shared_channel_configuration; -#[cfg(test)] -mod tests; +/// An error message from the TWS API. +#[derive(Debug, Clone)] +pub struct Notice { + pub code: i32, + pub message: String, +} + +impl Notice { + #[allow(private_interfaces)] + pub fn from(message: &ResponseMessage) -> Notice { + let code = message.peek_int(CODE_INDEX).unwrap_or(-1); + let message = message.peek_string(MESSAGE_INDEX); + Notice { code, message } + } +} + +impl Display for Notice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "[{}] {}", self.code, self.message) + } +} \ No newline at end of file diff --git a/src/messages/shared_channel_configuration.rs b/src/messages/shared_channel_configuration.rs index b1ed7cc0..81c26f98 100644 --- a/src/messages/shared_channel_configuration.rs +++ b/src/messages/shared_channel_configuration.rs @@ -48,4 +48,8 @@ pub(crate) const CHANNEL_MAPPINGS: &[ChannelMapping] = &[ request: OutgoingMessages::RequestMarketDataType, responses: &[IncomingMessages::MarketDataType], }, + ChannelMapping { + request: OutgoingMessages::RequestMktDepthExchanges, + responses: &[IncomingMessages::MktDepthExchanges], + }, ]; diff --git a/src/messages/tests.rs b/src/messages/tests.rs index 2b0f495e..29b86bcd 100644 --- a/src/messages/tests.rs +++ b/src/messages/tests.rs @@ -1,5 +1,6 @@ use crate::contracts::{ComboLegOpenClose, SecurityType}; use crate::orders::{Action, OrderCondition, OrderOpenClose, Rule80A}; +use crate::testdata::responses::NOTICE_DATA_FARM_CONNECTION; use super::*; @@ -179,7 +180,7 @@ fn test_incoming_message_from_i32() { assert_eq!(IncomingMessages::from(20), IncomingMessages::ScannerData); assert_eq!(IncomingMessages::from(21), IncomingMessages::TickOptionComputation); assert_eq!(IncomingMessages::from(45), IncomingMessages::TickGeneric); - assert_eq!(IncomingMessages::from(46), IncomingMessages::Tickstring); + assert_eq!(IncomingMessages::from(46), IncomingMessages::TickString); assert_eq!(IncomingMessages::from(47), IncomingMessages::TickEFP); assert_eq!(IncomingMessages::from(49), IncomingMessages::CurrentTime); assert_eq!(IncomingMessages::from(50), IncomingMessages::RealTimeBars); @@ -274,3 +275,14 @@ fn test_request_id_index() { fn test_request_id_index_invalid() { assert_eq!(request_id_index(IncomingMessages::NotValid), None); } + +#[test] +fn test_notice() { + let message = ResponseMessage::from(NOTICE_DATA_FARM_CONNECTION); + + let notice = Notice::from(&message); + + assert_eq!(notice.code, 2107); + assert_eq!(notice.message, "HMDS data farm connection is inactive."); + assert_eq!(format!("{notice}"), "[2107] HMDS data farm connection is inactive."); +} \ No newline at end of file diff --git a/src/server_versions.rs b/src/server_versions.rs index acd46ead..a1041585 100644 --- a/src/server_versions.rs +++ b/src/server_versions.rs @@ -6,7 +6,7 @@ // pub const CURRENT_TIME: i32 = 33; pub const REAL_TIME_BARS: i32 = 34; pub const SCALE_ORDERS: i32 = 35; -// pub const SNAPSHOT_MKT_DATA: i32 = 35; +pub const SNAPSHOT_MKT_DATA: i32 = 35; pub const SSHORT_COMBO_LEGS: i32 = 35; pub const WHAT_IF_ORDERS: i32 = 36; pub const CONTRACT_CONID: i32 = 37; diff --git a/src/testdata/responses.rs b/src/testdata/responses.rs index 31db5796..5c8b7207 100644 --- a/src/testdata/responses.rs +++ b/src/testdata/responses.rs @@ -1,5 +1,9 @@ //pub const POSITION: &str = "61\03\0DU1234567\076792991\0TSLA\0STK\0\00.0\0\0\0NASDAQ\0USD\0TSLA\0NMS\0500\0196.77\0"; +// error + +pub const NOTICE_DATA_FARM_CONNECTION: &str = "4|2|-1|2107|HMDS data farm connection is inactive.|"; + // accounts pub const MANAGED_ACCOUNT: &str = "15|1|DU1234567,DU7654321|"; @@ -12,3 +16,15 @@ pub const ACCOUNT_UPDATE_MULTI_END: &str = "74|1|9000||"; // contracts pub const MARKET_RULE: &str = "93|26|1|0|0.01|"; + +// Market Depth + +pub const MARKET_DEPTH_1: &str = "13|1|9000|0|OVERNIGHT|0|1|235.84|300|1||"; +pub const MARKET_DEPTH_2: &str = "13|1|9000|0|OVERNIGHT|0|0|236.09|200|1||"; +pub const MARKET_DEPTH_3: &str = "4|2|9000|2152|Exchanges - Depth: IEX; Top: BYX; AMEX; PEARL; MEMX; EDGEA; OVERNIGHT; CHX; NYSENAT; IBEOS; PSX; LTSE; ISE; DRCTEDGE; Need additional market data permissions - Depth: BATS; ARCA; ISLAND; BEX; NYSE; ||"; +pub const MARKET_DEPTH_4: &str = "13|1|9000|1|OVERNIGHT|0|1|235.84|300|1||"; +pub const MARKET_DEPTH_5: &str = "13|1|9000|0|IBEOS|1|1|235.84|100|1||"; +pub const MARKET_DEPTH_6: &str = "13|1|9000|1|IBEOS|0|0|236.26|100|1||"; +pub const MARKET_DEPTH_7: &str = "13|1|9000|1|OVERNIGHT|1|1|235.84|200|1||"; +pub const MARKET_DEPTH_8: &str = "13|1|9000|0|OVERNIGHT|1|1|235.84|200|1||"; +pub const MARKET_DEPTH_9: &str = "13|1|9000|1|IBEOS|1|1|235.82|100|1||";