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
30fn checked_quantity(size: f64, precision: u8) -> anyhow::Result<Quantity> {
31 let quantity = Quantity::new_checked(size, precision)
32 .map_err(|e| anyhow::anyhow!("invalid quantity size={size}: {e}"))?;
33 let tolerance = 10_f64.powi(-i32::from(precision)) * 1e-9;
34 if (quantity.as_f64() - size).abs() > tolerance {
35 anyhow::bail!(
36 "quantity size={} cannot be represented with precision={}",
37 size,
38 precision
39 );
40 }
41 Ok(quantity)
42}
43
44#[allow(clippy::too_many_arguments)]
53pub fn parse_quote_tick(
54 instrument_id: InstrumentId,
55 bid_price: Option<f64>,
56 ask_price: Option<f64>,
57 bid_size: Option<f64>,
58 ask_size: Option<f64>,
59 price_precision: u8,
60 size_precision: u8,
61 ts_event: UnixNanos,
62 ts_init: UnixNanos,
63) -> anyhow::Result<QuoteTick> {
64 let bid = bid_price.map(|p| Price::new(p, price_precision));
65 let ask = ask_price.map(|p| Price::new(p, price_precision));
66 let bid_qty = bid_size
67 .map(|s| checked_quantity(s, size_precision))
68 .transpose()?;
69 let ask_qty = ask_size
70 .map(|s| checked_quantity(s, size_precision))
71 .transpose()?;
72
73 Ok(QuoteTick::new(
74 instrument_id,
75 bid.unwrap_or_else(|| Price::zero(price_precision)),
76 ask.unwrap_or_else(|| Price::zero(price_precision)),
77 bid_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
78 ask_qty.unwrap_or_else(|| Quantity::zero(size_precision)),
79 ts_event,
80 ts_init,
81 ))
82}
83
84#[allow(clippy::too_many_arguments)]
90pub fn parse_trade_tick(
91 instrument_id: InstrumentId,
92 price: f64,
93 size: f64,
94 price_precision: u8,
95 size_precision: u8,
96 ts_event: UnixNanos,
97 ts_init: UnixNanos,
98 trade_id: Option<TradeId>,
99) -> anyhow::Result<TradeTick> {
100 let trade_price = Price::new(price, price_precision);
101 let trade_size = checked_quantity(size, size_precision)?;
102 let aggressor_side = AggressorSide::NoAggressor; let trade_id = trade_id
104 .unwrap_or_else(|| crate::common::parse::generate_ib_trade_id(ts_event, price, size));
105
106 Ok(TradeTick::new(
107 instrument_id,
108 trade_price,
109 trade_size,
110 aggressor_side,
111 trade_id,
112 ts_event,
113 ts_init,
114 ))
115}
116
117pub fn parse_index_price(
123 instrument_id: InstrumentId,
124 price: f64,
125 price_precision: u8,
126 price_magnifier: i32,
127 ts_event: UnixNanos,
128 ts_init: UnixNanos,
129) -> anyhow::Result<IndexPriceUpdate> {
130 let converted_price = if price_magnifier > 0 {
131 price / price_magnifier as f64
132 } else {
133 price
134 };
135
136 Ok(IndexPriceUpdate::new(
137 instrument_id,
138 Price::new(converted_price, price_precision),
139 ts_event,
140 ts_init,
141 ))
142}
143
144#[must_use]
146pub fn parse_option_computation_to_option_greeks(
147 instrument_id: InstrumentId,
148 computation: &OptionComputation,
149 ts_event: UnixNanos,
150 ts_init: UnixNanos,
151) -> Option<OptionGreeks> {
152 match computation.field {
153 TickType::ModelOption | TickType::DelayedModelOption => Some(OptionGreeks {
154 instrument_id,
155 greeks: OptionGreekValues {
156 delta: computation.delta.unwrap_or_default(),
157 gamma: computation.gamma.unwrap_or_default(),
158 vega: computation.vega.unwrap_or_default(),
159 theta: computation.theta.unwrap_or_default(),
160 rho: 0.0, },
162 convention: GreeksConvention::BlackScholes,
163 mark_iv: computation.implied_volatility,
164 bid_iv: None,
165 ask_iv: None,
166 underlying_price: computation.underlying_price,
167 open_interest: None,
168 ts_event,
169 ts_init,
170 }),
171 _ => None,
172 }
173}
174
175#[must_use]
177pub fn parse_option_open_interest(tick_type: &TickType, value: f64) -> Option<f64> {
178 match tick_type {
179 TickType::OpenInterest
180 | TickType::OptionCallOpenInterest
181 | TickType::OptionPutOpenInterest => Some(value),
182 _ => None,
183 }
184}
185
186#[allow(clippy::too_many_arguments)]
192pub fn parse_realtime_bar(
193 bar_type: BarType,
194 open: f64,
195 high: f64,
196 low: f64,
197 close: f64,
198 volume: f64,
199 price_precision: u8,
200 size_precision: u8,
201 ts_event: UnixNanos,
202 ts_init: UnixNanos,
203) -> anyhow::Result<Bar> {
204 let open_price = Price::new(open, price_precision);
205 let high_price = Price::new(high, price_precision);
206 let low_price = Price::new(low, price_precision);
207 let close_price = Price::new(close, price_precision);
208 let bar_volume = Quantity::new(volume, size_precision);
209
210 Ok(Bar::new(
211 bar_type,
212 open_price,
213 high_price,
214 low_price,
215 close_price,
216 bar_volume,
217 ts_event,
218 ts_init,
219 ))
220}
221
222#[must_use]
224pub fn parse_market_depth_operation(operation: i32) -> BookAction {
225 match operation {
226 0 => BookAction::Add,
227 1 => BookAction::Update,
228 2 => BookAction::Delete,
229 _ => BookAction::Add, }
231}
232
233#[cfg(test)]
236mod tests {
237 use ibapi::contracts::{OptionComputation, tick_types::TickType};
238 use nautilus_core::UnixNanos;
239 use nautilus_model::{
240 data::{BarSpecification, BarType},
241 enums::{AggregationSource, BarAggregation, PriceType},
242 identifiers::{InstrumentId, Symbol, Venue},
243 };
244 use rstest::rstest;
245
246 use super::*;
247
248 fn create_test_instrument_id() -> InstrumentId {
249 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
250 }
251
252 #[rstest]
253 fn test_parse_quote_tick_with_all_fields() {
254 let instrument_id = create_test_instrument_id();
255 let result = parse_quote_tick(
256 instrument_id,
257 Some(150.25),
258 Some(150.30),
259 Some(100.0),
260 Some(200.0),
261 2,
262 0,
263 UnixNanos::new(0),
264 UnixNanos::new(0),
265 );
266 assert!(result.is_ok());
267 let quote = result.unwrap();
268 assert_eq!(quote.bid_price.as_f64(), 150.25);
269 assert_eq!(quote.ask_price.as_f64(), 150.30);
270 assert_eq!(quote.bid_size.as_f64(), 100.0);
271 assert_eq!(quote.ask_size.as_f64(), 200.0);
272 }
273
274 #[rstest]
275 fn test_parse_quote_tick_with_partial_fields() {
276 let instrument_id = create_test_instrument_id();
277 let result = parse_quote_tick(
278 instrument_id,
279 Some(150.25),
280 None,
281 Some(100.0),
282 None,
283 2,
284 0,
285 UnixNanos::new(0),
286 UnixNanos::new(0),
287 );
288 assert!(result.is_ok());
289 let quote = result.unwrap();
290 assert_eq!(quote.bid_price.as_f64(), 150.25);
291 assert_eq!(quote.ask_price.as_f64(), 0.0); assert_eq!(quote.bid_size.as_f64(), 100.0);
293 assert_eq!(quote.ask_size.as_f64(), 0.0); }
295
296 #[rstest]
297 fn test_parse_quote_tick_with_no_fields() {
298 let instrument_id = create_test_instrument_id();
299 let result = parse_quote_tick(
300 instrument_id,
301 None,
302 None,
303 None,
304 None,
305 2,
306 0,
307 UnixNanos::new(0),
308 UnixNanos::new(0),
309 );
310 assert!(result.is_ok());
311 let quote = result.unwrap();
312 assert_eq!(quote.bid_price.as_f64(), 0.0);
313 assert_eq!(quote.ask_price.as_f64(), 0.0);
314 }
315
316 #[rstest]
317 fn test_parse_trade_tick_with_trade_id() {
318 let instrument_id = create_test_instrument_id();
319 let trade_id = TradeId::from("TRADE-001");
320 let result = parse_trade_tick(
321 instrument_id,
322 150.25,
323 100.0,
324 2,
325 0,
326 UnixNanos::new(0),
327 UnixNanos::new(0),
328 Some(trade_id),
329 );
330 assert!(result.is_ok());
331 let trade = result.unwrap();
332 assert_eq!(trade.price.as_f64(), 150.25);
333 assert_eq!(trade.size.as_f64(), 100.0);
334 assert_eq!(trade.trade_id, trade_id);
335 }
336
337 #[rstest]
338 fn test_parse_trade_tick_without_trade_id() {
339 let instrument_id = create_test_instrument_id();
340 let result = parse_trade_tick(
341 instrument_id,
342 150.25,
343 100.0,
344 2,
345 0,
346 UnixNanos::new(1000),
347 UnixNanos::new(1000),
348 None,
349 );
350 assert!(result.is_ok());
351 let trade = result.unwrap();
352 assert_eq!(trade.price.as_f64(), 150.25);
353 assert_eq!(trade.size.as_f64(), 100.0);
354 assert!(!trade.trade_id.to_string().is_empty());
356 }
357
358 #[rstest]
359 fn test_parse_trade_tick_rejects_unrepresentable_size() {
360 let instrument_id = create_test_instrument_id();
361 let result = parse_trade_tick(
362 instrument_id,
363 150.25,
364 0.5,
365 2,
366 0,
367 UnixNanos::new(1000),
368 UnixNanos::new(1000),
369 None,
370 );
371
372 assert!(result.is_err());
373 }
374
375 #[rstest]
376 fn test_parse_quote_tick_rejects_unrepresentable_size() {
377 let instrument_id = create_test_instrument_id();
378 let result = parse_quote_tick(
379 instrument_id,
380 Some(150.25),
381 Some(150.30),
382 Some(0.5),
383 Some(200.0),
384 2,
385 0,
386 UnixNanos::new(0),
387 UnixNanos::new(0),
388 );
389
390 assert!(result.is_err());
391 }
392
393 #[rstest]
394 fn test_parse_index_price_with_price_magnifier() {
395 let instrument_id = create_test_instrument_id();
396 let result = parse_index_price(
397 instrument_id,
398 452525.0,
399 2,
400 100,
401 UnixNanos::new(0),
402 UnixNanos::new(0),
403 );
404 assert!(result.is_ok());
405 let index_price = result.unwrap();
406 assert_eq!(index_price.value.as_f64(), 4525.25);
407 }
408
409 #[rstest]
410 fn test_parse_index_price_without_price_magnifier() {
411 let instrument_id = create_test_instrument_id();
412 let result = parse_index_price(
413 instrument_id,
414 4525.25,
415 2,
416 1,
417 UnixNanos::new(0),
418 UnixNanos::new(0),
419 );
420 assert!(result.is_ok());
421 let index_price = result.unwrap();
422 assert_eq!(index_price.value.as_f64(), 4525.25);
423 }
424
425 #[rstest]
426 fn test_parse_realtime_bar() {
427 let instrument_id = create_test_instrument_id();
428 let bar_type = BarType::new(
429 instrument_id,
430 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
431 AggregationSource::External,
432 );
433 let result = parse_realtime_bar(
434 bar_type,
435 150.0,
436 151.0,
437 149.0,
438 150.5,
439 1000.0,
440 2,
441 0,
442 UnixNanos::new(0),
443 UnixNanos::new(0),
444 );
445 assert!(result.is_ok());
446 let bar = result.unwrap();
447 assert_eq!(bar.open.as_f64(), 150.0);
448 assert_eq!(bar.high.as_f64(), 151.0);
449 assert_eq!(bar.low.as_f64(), 149.0);
450 assert_eq!(bar.close.as_f64(), 150.5);
451 assert_eq!(bar.volume.as_f64(), 1000.0);
452 }
453
454 #[rstest]
455 fn test_parse_market_depth_operation_insert() {
456 let action = parse_market_depth_operation(0);
457 assert_eq!(action, BookAction::Add);
458 }
459
460 #[rstest]
461 fn test_parse_market_depth_operation_update() {
462 let action = parse_market_depth_operation(1);
463 assert_eq!(action, BookAction::Update);
464 }
465
466 #[rstest]
467 fn test_parse_market_depth_operation_delete() {
468 let action = parse_market_depth_operation(2);
469 assert_eq!(action, BookAction::Delete);
470 }
471
472 #[rstest]
473 fn test_parse_market_depth_operation_unknown() {
474 let action = parse_market_depth_operation(99);
475 assert_eq!(action, BookAction::Add);
477 }
478
479 #[rstest]
480 fn test_parse_quote_tick_precision() {
481 let instrument_id = create_test_instrument_id();
482 let result = parse_quote_tick(
483 instrument_id,
484 Some(150.255),
485 Some(150.305),
486 Some(100.0),
487 Some(200.0),
488 2, 0, UnixNanos::new(0),
491 UnixNanos::new(0),
492 );
493 assert!(result.is_ok());
494 let quote = result.unwrap();
495 assert_eq!(quote.bid_price.as_f64(), 150.26); assert_eq!(quote.ask_price.as_f64(), 150.31); }
499
500 #[rstest]
501 fn test_parse_option_computation_to_option_greeks_from_model_tick() {
502 let instrument_id = create_test_instrument_id();
503 let greeks = parse_option_computation_to_option_greeks(
504 instrument_id,
505 &OptionComputation {
506 field: TickType::ModelOption,
507 implied_volatility: Some(0.25),
508 delta: Some(0.55),
509 gamma: Some(0.02),
510 vega: Some(0.15),
511 theta: Some(-0.05),
512 underlying_price: Some(155.0),
513 ..Default::default()
514 },
515 UnixNanos::new(10),
516 UnixNanos::new(11),
517 )
518 .unwrap();
519
520 assert_eq!(greeks.instrument_id, instrument_id);
521 assert_eq!(greeks.delta, 0.55);
522 assert_eq!(greeks.gamma, 0.02);
523 assert_eq!(greeks.vega, 0.15);
524 assert_eq!(greeks.theta, -0.05);
525 assert_eq!(greeks.rho, 0.0);
526 assert_eq!(greeks.mark_iv, Some(0.25));
527 assert_eq!(greeks.underlying_price, Some(155.0));
528 assert_eq!(greeks.open_interest, None);
529 assert_eq!(greeks.ts_event, UnixNanos::new(10));
530 assert_eq!(greeks.ts_init, UnixNanos::new(11));
531 }
532
533 #[rstest]
534 fn test_parse_option_computation_to_option_greeks_ignores_non_model_tick() {
535 let instrument_id = create_test_instrument_id();
536 let greeks = parse_option_computation_to_option_greeks(
537 instrument_id,
538 &OptionComputation {
539 field: TickType::BidOption,
540 implied_volatility: Some(0.24),
541 ..Default::default()
542 },
543 UnixNanos::new(10),
544 UnixNanos::new(11),
545 );
546
547 assert!(greeks.is_none());
548 }
549
550 #[rstest]
551 fn test_parse_option_open_interest_supports_option_interest_tick_types() {
552 assert_eq!(
553 parse_option_open_interest(&TickType::OptionCallOpenInterest, 1234.0),
554 Some(1234.0)
555 );
556 assert_eq!(
557 parse_option_open_interest(&TickType::OptionPutOpenInterest, 5678.0),
558 Some(5678.0)
559 );
560 assert_eq!(
561 parse_option_open_interest(&TickType::OpenInterest, 42.0),
562 Some(42.0)
563 );
564 assert_eq!(parse_option_open_interest(&TickType::Bid, 42.0), None);
565 }
566}