Skip to main content

nautilus_interactive_brokers/data/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Parsing utilities for converting Interactive Brokers data to Nautilus types.
17
18use ibapi::contracts::{OptionComputation, tick_types::TickType};
19use nautilus_core::UnixNanos;
20use nautilus_model::{
21    data::{
22        Bar, BarType, IndexPriceUpdate, QuoteTick, TradeTick, greeks::OptionGreekValues,
23        option_chain::OptionGreeks,
24    },
25    enums::{AggressorSide, BookAction, GreeksConvention},
26    identifiers::{InstrumentId, TradeId},
27    types::{Price, Quantity},
28};
29
30/// Parse IB tick price and size data into a QuoteTick.
31///
32/// This builds a quote from individual tick updates. You typically need to accumulate
33/// bid/ask prices and sizes from multiple tick updates before creating a QuoteTick.
34///
35/// # Arguments
36///
37/// * `instrument_id` - The instrument identifier
38/// * `bid_price` - Bid price (if available)
39/// * `ask_price` - Ask price (if available)
40/// * `bid_size` - Bid size (if available)
41/// * `ask_size` - Ask size (if available)
42/// * `price_precision` - Price precision for the instrument
43/// * `size_precision` - Size precision for the instrument
44/// * `ts_event` - Event timestamp
45/// * `ts_init` - Initialization timestamp
46///
47/// # Errors
48///
49/// Returns an error if price or size conversion fails.
50#[allow(clippy::too_many_arguments)]
51pub fn parse_quote_tick(
52    instrument_id: InstrumentId,
53    bid_price: Option<f64>,
54    ask_price: Option<f64>,
55    bid_size: Option<f64>,
56    ask_size: Option<f64>,
57    price_precision: u8,
58    size_precision: u8,
59    ts_event: UnixNanos,
60    ts_init: UnixNanos,
61) -> anyhow::Result<QuoteTick> {
62    let bid = bid_price.map(|p| Price::new(p, price_precision));
63    let ask = ask_price.map(|p| Price::new(p, price_precision));
64    let bid_qty = bid_size.map(|s| Quantity::new(s, size_precision));
65    let ask_qty = ask_size.map(|s| Quantity::new(s, size_precision));
66
67    Ok(QuoteTick::new(
68        instrument_id,
69        bid.unwrap_or_else(|| Price::zero(price_precision)),
70        ask.unwrap_or_else(|| Price::zero(price_precision)),
71        bid_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
72        ask_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
73        ts_event,
74        ts_init,
75    ))
76}
77
78/// Parse IB trade tick data into a TradeTick.
79///
80/// # Arguments
81///
82/// * `instrument_id` - The instrument identifier
83/// * `price` - Trade price
84/// * `size` - Trade size
85/// * `price_precision` - Price precision for the instrument
86/// * `size_precision` - Size precision for the instrument
87/// * `ts_event` - Event timestamp
88/// * `ts_init` - Initialization timestamp
89/// * `trade_id` - Optional trade ID (will be generated if not provided)
90///
91/// # Errors
92///
93/// Returns an error if price or size conversion fails.
94#[allow(clippy::too_many_arguments)]
95pub fn parse_trade_tick(
96    instrument_id: InstrumentId,
97    price: f64,
98    size: f64,
99    price_precision: u8,
100    size_precision: u8,
101    ts_event: UnixNanos,
102    ts_init: UnixNanos,
103    trade_id: Option<TradeId>,
104) -> anyhow::Result<TradeTick> {
105    let trade_price = Price::new(price, price_precision);
106    let trade_size = Quantity::new(size, size_precision);
107    let aggressor_side = AggressorSide::NoAggressor; // IB doesn't provide this directly
108    let trade_id = trade_id
109        .unwrap_or_else(|| crate::common::parse::generate_ib_trade_id(ts_event, price, size));
110
111    Ok(TradeTick::new(
112        instrument_id,
113        trade_price,
114        trade_size,
115        aggressor_side,
116        trade_id,
117        ts_event,
118        ts_init,
119    ))
120}
121
122/// Parse IB index price data into an [`IndexPriceUpdate`].
123///
124/// # Errors
125///
126/// Returns an error if the price conversion fails.
127pub fn parse_index_price(
128    instrument_id: InstrumentId,
129    price: f64,
130    price_precision: u8,
131    price_magnifier: i32,
132    ts_event: UnixNanos,
133    ts_init: UnixNanos,
134) -> anyhow::Result<IndexPriceUpdate> {
135    let converted_price = if price_magnifier > 0 {
136        price / price_magnifier as f64
137    } else {
138        price
139    };
140
141    Ok(IndexPriceUpdate::new(
142        instrument_id,
143        Price::new(converted_price, price_precision),
144        ts_event,
145        ts_init,
146    ))
147}
148
149/// Parse an IB option computation into Nautilus option greeks when the tick carries model greeks.
150#[must_use]
151pub fn parse_option_computation_to_option_greeks(
152    instrument_id: InstrumentId,
153    computation: &OptionComputation,
154    ts_event: UnixNanos,
155    ts_init: UnixNanos,
156) -> Option<OptionGreeks> {
157    match computation.field {
158        TickType::ModelOption | TickType::DelayedModelOption => Some(OptionGreeks {
159            instrument_id,
160            greeks: OptionGreekValues {
161                delta: computation.delta.unwrap_or_default(),
162                gamma: computation.gamma.unwrap_or_default(),
163                vega: computation.vega.unwrap_or_default(),
164                theta: computation.theta.unwrap_or_default(),
165                rho: 0.0, // IB does not publish rho in tickOptionComputation
166            },
167            convention: GreeksConvention::BlackScholes,
168            mark_iv: computation.implied_volatility,
169            bid_iv: None,
170            ask_iv: None,
171            underlying_price: computation.underlying_price,
172            open_interest: None,
173            ts_event,
174            ts_init,
175        }),
176        _ => None,
177    }
178}
179
180/// Parse open interest from an IB tick type when present.
181#[must_use]
182pub fn parse_option_open_interest(tick_type: &TickType, value: f64) -> Option<f64> {
183    match tick_type {
184        TickType::OpenInterest
185        | TickType::OptionCallOpenInterest
186        | TickType::OptionPutOpenInterest => Some(value),
187        _ => None,
188    }
189}
190
191/// Parse IB real-time bar data into a Bar.
192///
193/// # Arguments
194///
195/// * `bar_type` - The bar type specification
196/// * `open` - Opening price
197/// * `high` - High price
198/// * `low` - Low price
199/// * `close` - Closing price
200/// * `volume` - Volume
201/// * `price_precision` - Price precision for the instrument
202/// * `size_precision` - Size precision for the instrument
203/// * `ts_event` - Event timestamp
204/// * `ts_init` - Initialization timestamp
205///
206/// # Errors
207///
208/// Returns an error if price or size conversion fails.
209#[allow(clippy::too_many_arguments)]
210pub fn parse_realtime_bar(
211    bar_type: BarType,
212    open: f64,
213    high: f64,
214    low: f64,
215    close: f64,
216    volume: f64,
217    price_precision: u8,
218    size_precision: u8,
219    ts_event: UnixNanos,
220    ts_init: UnixNanos,
221) -> anyhow::Result<Bar> {
222    let open_price = Price::new(open, price_precision);
223    let high_price = Price::new(high, price_precision);
224    let low_price = Price::new(low, price_precision);
225    let close_price = Price::new(close, price_precision);
226    let bar_volume = Quantity::new(volume, size_precision);
227
228    Ok(Bar::new(
229        bar_type,
230        open_price,
231        high_price,
232        low_price,
233        close_price,
234        bar_volume,
235        ts_event,
236        ts_init,
237    ))
238}
239
240/// Parse IB market depth operation to BookAction.
241///
242/// # Arguments
243///
244/// * `operation` - IB market depth operation (0=insert, 1=update, 2=delete)
245///
246/// # Returns
247///
248/// Returns the corresponding BookAction.
249#[must_use]
250pub fn parse_market_depth_operation(operation: i32) -> BookAction {
251    match operation {
252        0 => BookAction::Add,
253        1 => BookAction::Update,
254        2 => BookAction::Delete,
255        _ => BookAction::Add, // Default to Add for unknown operations
256    }
257}
258
259// Note: ib_timestamp_to_unix_nanos moved to convert.rs
260
261#[cfg(test)]
262mod tests {
263    use ibapi::contracts::{OptionComputation, tick_types::TickType};
264    use nautilus_core::UnixNanos;
265    use nautilus_model::{
266        data::{BarSpecification, BarType},
267        enums::{AggregationSource, BarAggregation, PriceType},
268        identifiers::{InstrumentId, Symbol, Venue},
269    };
270    use rstest::rstest;
271
272    use super::*;
273
274    fn create_test_instrument_id() -> InstrumentId {
275        InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
276    }
277
278    #[rstest]
279    fn test_parse_quote_tick_with_all_fields() {
280        let instrument_id = create_test_instrument_id();
281        let result = parse_quote_tick(
282            instrument_id,
283            Some(150.25),
284            Some(150.30),
285            Some(100.0),
286            Some(200.0),
287            2,
288            0,
289            UnixNanos::new(0),
290            UnixNanos::new(0),
291        );
292        assert!(result.is_ok());
293        let quote = result.unwrap();
294        assert_eq!(quote.bid_price.as_f64(), 150.25);
295        assert_eq!(quote.ask_price.as_f64(), 150.30);
296        assert_eq!(quote.bid_size.as_f64(), 100.0);
297        assert_eq!(quote.ask_size.as_f64(), 200.0);
298    }
299
300    #[rstest]
301    fn test_parse_quote_tick_with_partial_fields() {
302        let instrument_id = create_test_instrument_id();
303        let result = parse_quote_tick(
304            instrument_id,
305            Some(150.25),
306            None,
307            Some(100.0),
308            None,
309            2,
310            0,
311            UnixNanos::new(0),
312            UnixNanos::new(0),
313        );
314        assert!(result.is_ok());
315        let quote = result.unwrap();
316        assert_eq!(quote.bid_price.as_f64(), 150.25);
317        assert_eq!(quote.ask_price.as_f64(), 0.0); // Should default to zero
318        assert_eq!(quote.bid_size.as_f64(), 100.0);
319        assert_eq!(quote.ask_size.as_f64(), 0.0); // Should default to zero
320    }
321
322    #[rstest]
323    fn test_parse_quote_tick_with_no_fields() {
324        let instrument_id = create_test_instrument_id();
325        let result = parse_quote_tick(
326            instrument_id,
327            None,
328            None,
329            None,
330            None,
331            2,
332            0,
333            UnixNanos::new(0),
334            UnixNanos::new(0),
335        );
336        assert!(result.is_ok());
337        let quote = result.unwrap();
338        assert_eq!(quote.bid_price.as_f64(), 0.0);
339        assert_eq!(quote.ask_price.as_f64(), 0.0);
340    }
341
342    #[rstest]
343    fn test_parse_trade_tick_with_trade_id() {
344        let instrument_id = create_test_instrument_id();
345        let trade_id = TradeId::from("TRADE-001");
346        let result = parse_trade_tick(
347            instrument_id,
348            150.25,
349            100.0,
350            2,
351            0,
352            UnixNanos::new(0),
353            UnixNanos::new(0),
354            Some(trade_id),
355        );
356        assert!(result.is_ok());
357        let trade = result.unwrap();
358        assert_eq!(trade.price.as_f64(), 150.25);
359        assert_eq!(trade.size.as_f64(), 100.0);
360        assert_eq!(trade.trade_id, trade_id);
361    }
362
363    #[rstest]
364    fn test_parse_trade_tick_without_trade_id() {
365        let instrument_id = create_test_instrument_id();
366        let result = parse_trade_tick(
367            instrument_id,
368            150.25,
369            100.0,
370            2,
371            0,
372            UnixNanos::new(1000),
373            UnixNanos::new(1000),
374            None,
375        );
376        assert!(result.is_ok());
377        let trade = result.unwrap();
378        assert_eq!(trade.price.as_f64(), 150.25);
379        assert_eq!(trade.size.as_f64(), 100.0);
380        // Trade ID should be auto-generated
381        assert!(!trade.trade_id.to_string().is_empty());
382    }
383
384    #[rstest]
385    fn test_parse_index_price_with_price_magnifier() {
386        let instrument_id = create_test_instrument_id();
387        let result = parse_index_price(
388            instrument_id,
389            452525.0,
390            2,
391            100,
392            UnixNanos::new(0),
393            UnixNanos::new(0),
394        );
395        assert!(result.is_ok());
396        let index_price = result.unwrap();
397        assert_eq!(index_price.value.as_f64(), 4525.25);
398    }
399
400    #[rstest]
401    fn test_parse_index_price_without_price_magnifier() {
402        let instrument_id = create_test_instrument_id();
403        let result = parse_index_price(
404            instrument_id,
405            4525.25,
406            2,
407            1,
408            UnixNanos::new(0),
409            UnixNanos::new(0),
410        );
411        assert!(result.is_ok());
412        let index_price = result.unwrap();
413        assert_eq!(index_price.value.as_f64(), 4525.25);
414    }
415
416    #[rstest]
417    fn test_parse_realtime_bar() {
418        let instrument_id = create_test_instrument_id();
419        let bar_type = BarType::new(
420            instrument_id,
421            BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
422            AggregationSource::External,
423        );
424        let result = parse_realtime_bar(
425            bar_type,
426            150.0,
427            151.0,
428            149.0,
429            150.5,
430            1000.0,
431            2,
432            0,
433            UnixNanos::new(0),
434            UnixNanos::new(0),
435        );
436        assert!(result.is_ok());
437        let bar = result.unwrap();
438        assert_eq!(bar.open.as_f64(), 150.0);
439        assert_eq!(bar.high.as_f64(), 151.0);
440        assert_eq!(bar.low.as_f64(), 149.0);
441        assert_eq!(bar.close.as_f64(), 150.5);
442        assert_eq!(bar.volume.as_f64(), 1000.0);
443    }
444
445    #[rstest]
446    fn test_parse_market_depth_operation_insert() {
447        let action = parse_market_depth_operation(0);
448        assert_eq!(action, BookAction::Add);
449    }
450
451    #[rstest]
452    fn test_parse_market_depth_operation_update() {
453        let action = parse_market_depth_operation(1);
454        assert_eq!(action, BookAction::Update);
455    }
456
457    #[rstest]
458    fn test_parse_market_depth_operation_delete() {
459        let action = parse_market_depth_operation(2);
460        assert_eq!(action, BookAction::Delete);
461    }
462
463    #[rstest]
464    fn test_parse_market_depth_operation_unknown() {
465        let action = parse_market_depth_operation(99);
466        // Should default to Add for unknown operations
467        assert_eq!(action, BookAction::Add);
468    }
469
470    #[rstest]
471    fn test_parse_quote_tick_precision() {
472        let instrument_id = create_test_instrument_id();
473        let result = parse_quote_tick(
474            instrument_id,
475            Some(150.255),
476            Some(150.305),
477            Some(100.5),
478            Some(200.5),
479            2, // Price precision: 2 decimal places
480            0, // Size precision: 0 decimal places
481            UnixNanos::new(0),
482            UnixNanos::new(0),
483        );
484        assert!(result.is_ok());
485        let quote = result.unwrap();
486        // Prices should be rounded to 2 decimal places
487        assert_eq!(quote.bid_price.as_f64(), 150.26); // Rounded up
488        assert_eq!(quote.ask_price.as_f64(), 150.31); // Rounded up
489    }
490
491    #[rstest]
492    fn test_parse_option_computation_to_option_greeks_from_model_tick() {
493        let instrument_id = create_test_instrument_id();
494        let greeks = parse_option_computation_to_option_greeks(
495            instrument_id,
496            &OptionComputation {
497                field: TickType::ModelOption,
498                implied_volatility: Some(0.25),
499                delta: Some(0.55),
500                gamma: Some(0.02),
501                vega: Some(0.15),
502                theta: Some(-0.05),
503                underlying_price: Some(155.0),
504                ..Default::default()
505            },
506            UnixNanos::new(10),
507            UnixNanos::new(11),
508        )
509        .unwrap();
510
511        assert_eq!(greeks.instrument_id, instrument_id);
512        assert_eq!(greeks.delta, 0.55);
513        assert_eq!(greeks.gamma, 0.02);
514        assert_eq!(greeks.vega, 0.15);
515        assert_eq!(greeks.theta, -0.05);
516        assert_eq!(greeks.rho, 0.0);
517        assert_eq!(greeks.mark_iv, Some(0.25));
518        assert_eq!(greeks.underlying_price, Some(155.0));
519        assert_eq!(greeks.open_interest, None);
520        assert_eq!(greeks.ts_event, UnixNanos::new(10));
521        assert_eq!(greeks.ts_init, UnixNanos::new(11));
522    }
523
524    #[rstest]
525    fn test_parse_option_computation_to_option_greeks_ignores_non_model_tick() {
526        let instrument_id = create_test_instrument_id();
527        let greeks = parse_option_computation_to_option_greeks(
528            instrument_id,
529            &OptionComputation {
530                field: TickType::BidOption,
531                implied_volatility: Some(0.24),
532                ..Default::default()
533            },
534            UnixNanos::new(10),
535            UnixNanos::new(11),
536        );
537
538        assert!(greeks.is_none());
539    }
540
541    #[rstest]
542    fn test_parse_option_open_interest_supports_option_interest_tick_types() {
543        assert_eq!(
544            parse_option_open_interest(&TickType::OptionCallOpenInterest, 1234.0),
545            Some(1234.0)
546        );
547        assert_eq!(
548            parse_option_open_interest(&TickType::OptionPutOpenInterest, 5678.0),
549            Some(5678.0)
550        );
551        assert_eq!(
552            parse_option_open_interest(&TickType::OpenInterest, 42.0),
553            Some(42.0)
554        );
555        assert_eq!(parse_option_open_interest(&TickType::Bid, 42.0), None);
556    }
557}