1use 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#[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#[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; 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
122pub 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#[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, },
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#[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#[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#[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, }
257}
258
259#[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); assert_eq!(quote.bid_size.as_f64(), 100.0);
319 assert_eq!(quote.ask_size.as_f64(), 0.0); }
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 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 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, 0, UnixNanos::new(0),
482 UnixNanos::new(0),
483 );
484 assert!(result.is_ok());
485 let quote = result.unwrap();
486 assert_eq!(quote.bid_price.as_f64(), 150.26); assert_eq!(quote.ask_price.as_f64(), 150.31); }
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}