Skip to main content

ibapi/market_data/realtime/
mod.rs

1//! Real-time market data API.
2//!
3//! ## Canonical paths
4//!
5//! Real-time methods are inherent methods on `ibapi::Client` — call
6//! `client.realtime_bars(...)`, `client.tick_by_tick(...)`,
7//! `client.market_depth(...)`, etc. The public types (`Bar`, `BidAsk`,
8//! `MidPoint`, `Trade`, `TickTypes`, `Quote`, `MarketDataBuilder`, `RealtimeBarsBuilder`,
9//! `TickByTickBuilder`, `MarketDepthBuilder`, etc.) live at
10//! `ibapi::market_data::realtime::*` and via `ibapi::prelude::*`.
11//!
12//! The `realtime::sync` and `realtime::r#async` submodules are where the
13//! impls live; they carry no public items of their own. Prefer the canonical
14//! `Client` method calls and the `market_data::realtime::*` type spellings.
15
16use serde::{Deserialize, Serialize};
17use time::OffsetDateTime;
18
19use crate::common::error_helpers;
20use crate::contracts::OptionComputation;
21use crate::messages::{IncomingMessages, Notice, ResponseMessage, MARKET_DEPTH_RESET_CODE, NO_SECURITY_DEFINITION_CODE};
22use crate::subscriptions::{DecoderContext, StreamDecoder};
23use crate::Error;
24
25// Common modules
26pub(crate) mod common;
27
28mod builder;
29pub use builder::{MarketDataBuilder, MarketDepthBuilder, RealtimeBarsBuilder, TickByTickBuilder};
30
31pub mod generic_tick;
32
33mod quote;
34pub use quote::Quote;
35
36// Feature-specific implementations
37#[doc(hidden)]
38#[cfg(feature = "sync")]
39pub mod sync;
40
41#[doc(hidden)]
42#[cfg(feature = "async")]
43pub mod r#async;
44
45use crate::contracts::tick_types::TickType;
46
47// === Models ===
48
49/// Represents `BidAsk` tick by tick realtime tick.
50#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
51#[derive(Debug, Serialize, Deserialize, PartialEq)]
52pub struct BidAsk {
53    /// The spread's date and time (either as a yyyymmss hh:mm:ss formatted string or as system time according to the request). Time zone is the TWS time zone chosen on login.
54    pub time: OffsetDateTime,
55    /// tick-by-tick real-time tick bid price
56    pub bid_price: f64,
57    /// tick-by-tick real-time tick ask price
58    pub ask_price: f64,
59    /// tick-by-tick real-time tick bid size
60    pub bid_size: f64,
61    /// tick-by-tick real-time tick ask size
62    pub ask_size: f64,
63    /// tick-by-tick real-time bid/ask tick attribs (bit 0 - bid past low, bit 1 - ask past high)
64    pub bid_ask_attribute: BidAskAttribute,
65}
66
67impl StreamDecoder<BidAsk> for BidAsk {
68    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::TickByTick];
69
70    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
71        match message.message_type() {
72            IncomingMessages::TickByTick => common::decoders::decode_bid_ask_tick(message),
73            _ => Err(Error::unexpected_response(message)),
74        }
75    }
76
77    fn cancel_message(_server_version: i32, request_id: Option<i32>, _context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
78        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel bid/ask ticks")?;
79        common::encoders::encode_cancel_tick_by_tick(request_id)
80    }
81
82    /// Live: after 200 TWS answers the cancel with 300 (no such request).
83    fn rejection_ends_request(notice: &Notice) -> bool {
84        notice.code == NO_SECURITY_DEFINITION_CODE
85    }
86}
87
88/// Attributes for bid/ask tick data.
89#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
90#[derive(Debug, Serialize, Deserialize, PartialEq)]
91pub struct BidAskAttribute {
92    /// Indicates if the bid price is past the daily low.
93    pub bid_past_low: bool,
94    /// Indicates if the ask price is past the daily high.
95    pub ask_past_high: bool,
96}
97
98/// Represents `MidPoint` tick by tick realtime tick.
99#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
100#[derive(Debug, Serialize, Deserialize, PartialEq)]
101pub struct MidPoint {
102    /// The trade's date and time (either as a yyyymmss hh:mm:ss formatted string or as system time according to the request). Time zone is the TWS time zone chosen on login.
103    pub time: OffsetDateTime,
104    /// mid point
105    pub mid_point: f64,
106}
107
108impl StreamDecoder<MidPoint> for MidPoint {
109    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::TickByTick];
110
111    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
112        match message.message_type() {
113            IncomingMessages::TickByTick => common::decoders::decode_mid_point_tick(message),
114            _ => Err(Error::unexpected_response(message)),
115        }
116    }
117
118    fn cancel_message(_server_version: i32, request_id: Option<i32>, _context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
119        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel mid point ticks")?;
120        common::encoders::encode_cancel_tick_by_tick(request_id)
121    }
122
123    /// Live: after 200 TWS answers the cancel with 300 (no such request).
124    fn rejection_ends_request(notice: &Notice) -> bool {
125        notice.code == NO_SECURITY_DEFINITION_CODE
126    }
127}
128
129/// Represents a real-time bar with OHLCV data
130#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
131#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
132pub struct Bar {
133    /// The timestamp of the bar in market timezone
134    pub date: OffsetDateTime,
135    /// Opening price during the bar period
136    pub open: f64,
137    /// Highest price during the bar period
138    pub high: f64,
139    /// Lowest price during the bar period
140    pub low: f64,
141    /// Closing price of the bar period
142    pub close: f64,
143    /// Total volume traded during the bar period
144    pub volume: f64,
145    /// Volume weighted average price
146    pub wap: f64,
147    /// Number of trades during the bar period
148    pub count: i32,
149}
150
151impl StreamDecoder<Bar> for Bar {
152    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::RealTimeBars];
153
154    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
155        match message.message_type() {
156            IncomingMessages::RealTimeBars => common::decoders::decode_realtime_bar(message),
157            _ => Err(Error::unexpected_response(message)),
158        }
159    }
160
161    fn cancel_message(_server_version: i32, request_id: Option<i32>, _context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
162        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel realtime bars")?;
163        common::encoders::encode_cancel_realtime_bars(request_id)
164    }
165
166    /// Live: after 200 TWS answers the cancel with 300 (no such request).
167    fn rejection_ends_request(notice: &Notice) -> bool {
168        notice.code == NO_SECURITY_DEFINITION_CODE
169    }
170}
171
172/// Represents `Last` or `AllLast` tick-by-tick real-time tick.
173#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
174#[derive(Debug, Serialize, Deserialize, PartialEq)]
175pub struct Trade {
176    /// The trade's date and time (either as a yyyymmss hh:mm:ss formatted string or as system time according to the request). Time zone is the TWS time zone chosen on login.
177    pub time: OffsetDateTime,
178    /// Tick last price
179    pub price: f64,
180    /// Tick last size
181    pub size: f64,
182    /// Tick attributes (bit 0 - past limit, bit 1 - unreported)
183    pub trade_attribute: TradeAttribute,
184    /// Tick exchange
185    pub exchange: String,
186    /// Tick special conditions
187    pub special_conditions: String,
188}
189
190impl StreamDecoder<Trade> for Trade {
191    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::TickByTick];
192
193    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
194        match message.message_type() {
195            IncomingMessages::TickByTick => common::decoders::decode_trade_tick(message),
196            _ => Err(Error::unexpected_response(message)),
197        }
198    }
199
200    fn cancel_message(_server_version: i32, request_id: Option<i32>, _context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
201        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel trade ticks")?;
202        common::encoders::encode_cancel_tick_by_tick(request_id)
203    }
204
205    /// Live: after 200 TWS answers the cancel with 300 (no such request).
206    fn rejection_ends_request(notice: &Notice) -> bool {
207        notice.code == NO_SECURITY_DEFINITION_CODE
208    }
209}
210
211/// Attributes for trade tick data.
212#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
213#[derive(Debug, Serialize, Deserialize, PartialEq)]
214pub struct TradeAttribute {
215    /// Indicates if the trade occurred past the limit price.
216    pub past_limit: bool,
217    /// Indicates if the trade was unreported.
218    pub unreported: bool,
219}
220
221/// Specifies the type of data to show for real-time bars.
222#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
223#[derive(Clone, Debug, Copy)]
224pub enum WhatToShow {
225    /// Trade data.
226    Trades,
227    /// Midpoint between bid and ask.
228    MidPoint,
229    /// Bid prices.
230    Bid,
231    /// Ask prices.
232    Ask,
233    /// Aggregated trade data (wire value `AGGTRADES`).
234    ///
235    /// Required for crypto contracts: TWS rejects `TRADES` for crypto with
236    /// error 10299 on `reqRealTimeBars`, so crypto trade-price bars must be
237    /// requested as `AGGTRADES`.
238    AggTrades,
239}
240
241impl std::fmt::Display for WhatToShow {
242    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
243        match self {
244            Self::Trades => write!(f, "TRADES"),
245            Self::MidPoint => write!(f, "MIDPOINT"),
246            Self::Bid => write!(f, "BID"),
247            Self::Ask => write!(f, "ASK"),
248            Self::AggTrades => write!(f, "AGGTRADES"),
249        }
250    }
251}
252
253/// Market depth data types.
254///
255/// Apply `MarketDepth` / `MarketDepthL2` rows to the book you hold; on
256/// [`Reset`](Self::Reset), drop every row first. The stream stays open
257/// through a reset. Code 316 ("Market depth data has been HALTED") is
258/// terminal: the stream ends with `Err`, and the caller re-subscribes.
259#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
260#[derive(Debug, Serialize, Deserialize, PartialEq)]
261pub enum MarketDepths {
262    /// Level-1 depth update.
263    MarketDepth(MarketDepth),
264    /// Level-2 (per exchange/MPID) depth update.
265    MarketDepthL2(MarketDepthL2),
266    /// TWS discarded the book on its side (code 317, "Market depth data has
267    /// been RESET"): empty every row held, then rebuild from the updates that
268    /// follow. Delivered as data so it survives `filter_data` / `iter_data`.
269    Reset,
270}
271
272#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
273#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
274/// Returns the order book.
275pub struct MarketDepth {
276    /// The order book's row being updated
277    pub position: i32,
278    /// 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').
279    pub operation: i32,
280    /// 0 for ask, 1 for bid
281    pub side: i32,
282    /// The order's price.
283    pub price: f64,
284    /// The order's size.
285    pub size: f64,
286}
287
288/// Returns the order book.
289#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
290#[derive(Debug, Default, Serialize, Deserialize, PartialEq)]
291pub struct MarketDepthL2 {
292    /// The order book's row being updated
293    pub position: i32,
294    /// The exchange holding the order if isSmartDepth is True, otherwise the MPID of the market maker
295    pub market_maker: String,
296    /// 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').
297    pub operation: i32,
298    /// 0 for ask, 1 for bid
299    pub side: i32,
300    /// The order's price.
301    pub price: f64,
302    /// The order's size.
303    pub size: f64,
304    /// Flag indicating if this is smart depth response (aggregate data from multiple exchanges, v974+)
305    pub smart_depth: bool,
306}
307
308impl StreamDecoder<MarketDepths> for MarketDepths {
309    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::MarketDepth, IncomingMessages::MarketDepthL2];
310
311    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
312        match message.message_type() {
313            IncomingMessages::MarketDepth => Ok(MarketDepths::MarketDepth(common::decoders::decode_market_depth(message)?)),
314            IncomingMessages::MarketDepthL2 => Ok(MarketDepths::MarketDepthL2(common::decoders::decode_market_depth_l2(message)?)),
315            _ => Err(Error::unexpected_response(message)),
316        }
317    }
318
319    fn data_from_notice(notice: &Notice) -> Option<Self> {
320        (notice.code == MARKET_DEPTH_RESET_CODE).then_some(MarketDepths::Reset)
321    }
322
323    fn cancel_message(_server_version: i32, request_id: Option<i32>, context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
324        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel market depth")?;
325        common::encoders::encode_cancel_market_depth(request_id, context.map(|c| c.is_smart_depth).unwrap_or(false))
326    }
327
328    /// Live: after 200 TWS answers the cancel with 310 (no such request).
329    fn rejection_ends_request(notice: &Notice) -> bool {
330        notice.code == NO_SECURITY_DEFINITION_CODE
331    }
332}
333
334/// Stores depth market data description.
335#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
336#[derive(Debug, Default)]
337pub struct DepthMarketDataDescription {
338    /// The exchange name
339    pub exchange_name: String,
340    /// The security type
341    pub security_type: String,
342    /// The listing exchange name
343    pub listing_exchange: String,
344    /// The service data type
345    pub service_data_type: String,
346    /// The aggregated group
347    pub aggregated_group: Option<String>,
348}
349
350/// Various types of market data ticks.
351#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
352#[derive(Debug)]
353pub enum TickTypes {
354    /// Price update tick.
355    Price(TickPrice),
356    /// Size update tick.
357    Size(TickSize),
358    /// Textual market data message.
359    String(TickString),
360    /// Generic numeric tick (e.g., index values).
361    Generic(TickGeneric),
362    /// Option computation tick.
363    OptionComputation(OptionComputation),
364    /// Snapshot request completed for this ticker.
365    SnapshotEnd,
366    /// Tick-by-tick request parameter information.
367    RequestParameters(TickRequestParameters),
368    /// Combined price and size tick.
369    PriceSize(TickPriceSize),
370    /// Active market data type for this subscription (real-time / frozen / delayed / delayed-frozen).
371    MarketDataType(crate::market_data::MarketDataType),
372}
373
374impl StreamDecoder<TickTypes> for TickTypes {
375    const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[
376        IncomingMessages::TickPrice,
377        IncomingMessages::TickSize,
378        IncomingMessages::TickString,
379        IncomingMessages::TickGeneric,
380        IncomingMessages::TickOptionComputation,
381        IncomingMessages::TickSnapshotEnd,
382        IncomingMessages::TickReqParams,
383        IncomingMessages::MarketDataType,
384    ];
385
386    fn decode(_context: &DecoderContext, message: &ResponseMessage) -> Result<Self, Error> {
387        match message.message_type() {
388            IncomingMessages::TickPrice => common::decoders::decode_tick_price(message),
389            IncomingMessages::TickSize => common::decoders::decode_tick_size(message).map(TickTypes::Size),
390            IncomingMessages::TickString => common::decoders::decode_tick_string(message).map(TickTypes::String),
391            IncomingMessages::TickGeneric => common::decoders::decode_tick_generic(message).map(TickTypes::Generic),
392            IncomingMessages::TickOptionComputation => common::decoders::decode_tick_option_computation(message).map(TickTypes::OptionComputation),
393            IncomingMessages::TickReqParams => common::decoders::decode_tick_request_parameters(message).map(TickTypes::RequestParameters),
394            IncomingMessages::MarketDataType => common::decoders::decode_market_data_type(message).map(TickTypes::MarketDataType),
395            IncomingMessages::TickSnapshotEnd => Ok(TickTypes::SnapshotEnd),
396            _ => Err(Error::unexpected_response(message)),
397        }
398    }
399
400    fn cancel_message(_server_version: i32, request_id: Option<i32>, _context: Option<&DecoderContext>) -> Result<Vec<u8>, Error> {
401        let request_id = error_helpers::require_request_id_for(request_id, "encode cancel market data")?;
402        common::encoders::encode_cancel_market_data(request_id)
403    }
404
405    /// Live: after 200 TWS answers the cancel with 300 (no such request).
406    fn rejection_ends_request(notice: &Notice) -> bool {
407        notice.code == NO_SECURITY_DEFINITION_CODE
408    }
409
410    fn is_snapshot_end(&self) -> bool {
411        matches!(self, TickTypes::SnapshotEnd)
412    }
413}
414
415/// Price tick data.
416#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
417#[derive(Debug, Default)]
418pub struct TickPrice {
419    /// Type of price tick (bid, ask, last, etc.).
420    pub tick_type: TickType,
421    /// The price value.
422    pub price: f64,
423    /// Additional attributes for the price tick.
424    pub attributes: TickAttribute,
425}
426
427/// Attributes associated with price ticks.
428#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
429#[derive(Debug, PartialEq, Default)]
430pub struct TickAttribute {
431    /// Indicates if the order can be automatically executed.
432    pub can_auto_execute: bool,
433    /// Indicates if the price is past the limit.
434    pub past_limit: bool,
435    /// Indicates if this is a pre-market opening price.
436    pub pre_open: bool,
437}
438
439/// Size tick data.
440#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
441#[derive(Debug, Default)]
442pub struct TickSize {
443    /// Type of size tick (bid size, ask size, etc.).
444    pub tick_type: TickType,
445    /// The size value.
446    pub size: f64,
447}
448
449/// Combined price and size tick data.
450#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
451#[derive(Debug, Default)]
452pub struct TickPriceSize {
453    /// Type of price tick.
454    pub price_tick_type: TickType,
455    /// The price value.
456    pub price: f64,
457    /// Price tick attributes.
458    pub attributes: TickAttribute,
459    /// Type of size tick.
460    pub size_tick_type: TickType,
461    /// The size value.
462    pub size: f64,
463}
464
465/// String-based tick data.
466#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
467#[derive(Debug, Default)]
468pub struct TickString {
469    /// Type of string tick.
470    pub tick_type: TickType,
471    /// The string value.
472    pub value: String,
473}
474
475/// Generic tick data.
476#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
477#[derive(Debug, Default)]
478pub struct TickGeneric {
479    /// Type of generic tick.
480    pub tick_type: TickType,
481    /// The numeric value.
482    pub value: f64,
483}
484
485/// Parameters related to tick data requests.
486#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
487#[derive(Debug, Default)]
488pub struct TickRequestParameters {
489    /// Minimum tick increment.
490    pub min_tick: f64,
491    /// Best Bid/Offer exchange.
492    pub bbo_exchange: String,
493    /// Snapshot permissions code.
494    pub snapshot_permissions: i32,
495}
496
497#[cfg(test)]
498#[path = "mod_tests.rs"]
499mod tests;