1use chrono::{DateTime, Utc};
19use ibapi::market_data::historical::{
20 BarSize as HistoricalBarSize, Duration as IBDuration, ToDuration,
21 WhatToShow as HistoricalWhatToShow,
22};
23use nautilus_core::UnixNanos;
24use nautilus_model::{
25 data::{Bar, BarType},
26 enums::{BarAggregation, PriceType},
27 types::{Price, Quantity},
28};
29use time::OffsetDateTime;
30
31pub fn bar_type_to_ib_bar_size(bar_type: &BarType) -> anyhow::Result<HistoricalBarSize> {
41 let spec = bar_type.spec();
42 let aggregation = spec.aggregation;
43 let step = spec.step.get();
44
45 let bar_size = match (aggregation, step) {
46 (BarAggregation::Second, 1) => HistoricalBarSize::Sec,
48 (BarAggregation::Second, 5) => HistoricalBarSize::Sec5,
49 (BarAggregation::Second, 15) => HistoricalBarSize::Sec15,
50 (BarAggregation::Second, 30) => HistoricalBarSize::Sec30,
51 (BarAggregation::Minute, 1) => HistoricalBarSize::Min,
53 (BarAggregation::Minute, 2) => HistoricalBarSize::Min2,
54 (BarAggregation::Minute, 3) => HistoricalBarSize::Min3,
55 (BarAggregation::Minute, 5) => HistoricalBarSize::Min5,
56 (BarAggregation::Minute, 10) => HistoricalBarSize::Min15, (BarAggregation::Minute, 15) => HistoricalBarSize::Min15,
58 (BarAggregation::Minute, 20) => HistoricalBarSize::Min20,
59 (BarAggregation::Minute, 30) => HistoricalBarSize::Min30,
60 (BarAggregation::Hour, 1) => HistoricalBarSize::Hour,
62 (BarAggregation::Hour, 2) => HistoricalBarSize::Hour2,
63 (BarAggregation::Hour, 3) => HistoricalBarSize::Hour3,
64 (BarAggregation::Hour, 4) => HistoricalBarSize::Hour4,
65 (BarAggregation::Hour, 8) => HistoricalBarSize::Hour8,
66 (BarAggregation::Day, 1) => HistoricalBarSize::Day,
68 (BarAggregation::Week, 1) => HistoricalBarSize::Week,
70 (BarAggregation::Month, 1) => HistoricalBarSize::Month,
72 _ => {
73 anyhow::bail!("Unsupported bar aggregation/step combination: {aggregation:?}/{step}",);
74 }
75 };
76
77 Ok(bar_size)
78}
79
80#[must_use]
90pub fn price_type_to_ib_what_to_show(price_type: PriceType) -> HistoricalWhatToShow {
91 match price_type {
92 PriceType::Last => HistoricalWhatToShow::Trades,
93 PriceType::Bid => HistoricalWhatToShow::Bid,
94 PriceType::Ask => HistoricalWhatToShow::Ask,
95 PriceType::Mid => HistoricalWhatToShow::MidPoint,
96 _ => HistoricalWhatToShow::Trades, }
98}
99
100fn _validate_bar_prices(open: &mut f64, high: &mut f64, low: &mut f64, close: &f64) {
103 if *high < *low || *high < *open || *high < *close || *low > *open || *low > *close {
104 tracing::warn!(
105 "Invalid bar prices detected: O:{}, H:{}, L:{}, C:{}. Correcting using close price",
106 open,
107 high,
108 low,
109 close
110 );
111 *open = *close;
112 *high = *close;
113 *low = *close;
114 }
115}
116
117pub fn ib_bar_to_nautilus_bar(
130 ib_bar: &ibapi::market_data::historical::Bar,
131 bar_type: BarType,
132 price_precision: u8,
133 size_precision: u8,
134) -> anyhow::Result<Bar> {
135 let ts_event = ib_timestamp_to_unix_nanos(&ib_bar.date);
137 let ts_init = ts_event; let mut open = ib_bar.open;
141 let mut high = ib_bar.high;
142 let mut low = ib_bar.low;
143 let close = ib_bar.close;
144 _validate_bar_prices(&mut open, &mut high, &mut low, &close);
145
146 let open_price = Price::new(open, price_precision);
148 let high_price = Price::new(high, price_precision);
149 let low_price = Price::new(low, price_precision);
150 let close_price = Price::new(close, price_precision);
151
152 let volume = if ib_bar.volume < 0.0 {
154 Quantity::zero(size_precision)
155 } else {
156 Quantity::new(ib_bar.volume, size_precision)
157 };
158
159 Ok(Bar::new(
160 bar_type,
161 open_price,
162 high_price,
163 low_price,
164 close_price,
165 volume,
166 ts_event,
167 ts_init,
168 ))
169}
170
171#[must_use]
181pub fn ib_timestamp_to_unix_nanos(dt: &OffsetDateTime) -> UnixNanos {
182 let timestamp = dt.unix_timestamp_nanos();
183 UnixNanos::from(timestamp as u64)
184}
185
186pub fn chrono_to_ib_datetime(dt: &DateTime<Utc>) -> OffsetDateTime {
196 let timestamp = dt.timestamp();
197 let nanos = dt.timestamp_subsec_nanos();
198 let total_nanos = timestamp as i128 * 1_000_000_000 + nanos as i128;
199 OffsetDateTime::from_unix_timestamp_nanos(total_nanos)
200 .unwrap_or_else(|_| OffsetDateTime::now_utc())
201}
202
203pub fn calculate_duration(
218 start: Option<DateTime<Utc>>,
219 end: Option<DateTime<Utc>>,
220) -> anyhow::Result<IBDuration> {
221 match (start, end) {
222 (Some(start_dt), Some(end_dt)) => {
223 let duration = end_dt.signed_duration_since(start_dt);
224 let days = duration.num_days();
225
226 if days > 0 && days <= i32::MAX as i64 {
227 Ok((days as i32).days())
228 } else {
229 let seconds = duration.num_seconds();
231 if seconds > 0 && seconds <= i32::MAX as i64 {
232 Ok((seconds as i32).seconds())
233 } else {
234 Ok(1.days())
236 }
237 }
238 }
239 (None, Some(_)) => {
240 Ok(1.days())
242 }
243 (Some(_), None) => {
244 Ok(1.days())
246 }
247 (None, None) => {
248 Ok(1.days())
250 }
251 }
252}
253
254pub fn calculate_duration_segments(
268 start: DateTime<Utc>,
269 end: DateTime<Utc>,
270) -> Vec<(DateTime<Utc>, IBDuration)> {
271 let mut results = Vec::new();
272 let duration = end.signed_duration_since(start);
273 let mut total_seconds = duration.num_seconds();
274
275 if total_seconds <= 0 {
276 return results;
277 }
278
279 let years = total_seconds / (365 * 24 * 3600);
280 total_seconds %= 365 * 24 * 3600;
281 let days = total_seconds / (24 * 3600);
282 total_seconds %= 24 * 3600;
283 let seconds = total_seconds;
284
285 if years > 0 {
286 results.push((end, (years as i32).years()));
287 }
288
289 if days > 0 {
290 let minus_years_duration = chrono::Duration::days(years * 365);
291 let minus_years_date = end - minus_years_duration;
292 results.push((minus_years_date, (days as i32).days()));
293 }
294
295 if seconds > 0 {
296 let minus_years_duration = chrono::Duration::days(years * 365);
297 let minus_days_duration = chrono::Duration::days(days);
298 let minus_days_date = end - minus_years_duration - minus_days_duration;
299 results.push((minus_days_date, (seconds as i32).seconds()));
300 }
301
302 results
303}
304
305#[cfg(test)]
306mod tests {
307 use nautilus_model::{
308 data::{BarSpecification, BarType},
309 enums::{AggregationSource, BarAggregation, PriceType},
310 identifiers::{InstrumentId, Symbol, Venue},
311 };
312 use rstest::rstest;
313 use time::macros::datetime;
314
315 use super::*;
316
317 fn create_test_instrument_id() -> InstrumentId {
318 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
319 }
320
321 #[rstest]
322 fn test_bar_type_to_ib_bar_size_seconds() {
323 let instrument_id = create_test_instrument_id();
324 let bar_type = BarType::new(
325 instrument_id,
326 BarSpecification::new(1, BarAggregation::Second, PriceType::Last),
327 AggregationSource::External,
328 );
329 let result = bar_type_to_ib_bar_size(&bar_type);
330 assert!(result.is_ok());
331 assert_eq!(result.unwrap(), HistoricalBarSize::Sec);
332
333 let bar_type = BarType::new(
334 instrument_id,
335 BarSpecification::new(5, BarAggregation::Second, PriceType::Last),
336 AggregationSource::External,
337 );
338 let result = bar_type_to_ib_bar_size(&bar_type);
339 assert!(result.is_ok());
340 assert_eq!(result.unwrap(), HistoricalBarSize::Sec5);
341 }
342
343 #[rstest]
344 fn test_bar_type_to_ib_bar_size_minutes() {
345 let instrument_id = create_test_instrument_id();
346 let bar_type = BarType::new(
347 instrument_id,
348 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
349 AggregationSource::External,
350 );
351 let result = bar_type_to_ib_bar_size(&bar_type);
352 assert!(result.is_ok());
353 assert_eq!(result.unwrap(), HistoricalBarSize::Min);
354
355 let bar_type = BarType::new(
356 instrument_id,
357 BarSpecification::new(15, BarAggregation::Minute, PriceType::Last),
358 AggregationSource::External,
359 );
360 let result = bar_type_to_ib_bar_size(&bar_type);
361 assert!(result.is_ok());
362 assert_eq!(result.unwrap(), HistoricalBarSize::Min15);
363 }
364
365 #[rstest]
366 fn test_bar_type_to_ib_bar_size_hours() {
367 let instrument_id = create_test_instrument_id();
368 let bar_type = BarType::new(
369 instrument_id,
370 BarSpecification::new(1, BarAggregation::Hour, PriceType::Last),
371 AggregationSource::External,
372 );
373 let result = bar_type_to_ib_bar_size(&bar_type);
374 assert!(result.is_ok());
375 assert_eq!(result.unwrap(), HistoricalBarSize::Hour);
376 }
377
378 #[rstest]
379 fn test_bar_type_to_ib_bar_size_days() {
380 let instrument_id = create_test_instrument_id();
381 let bar_type = BarType::new(
382 instrument_id,
383 BarSpecification::new(1, BarAggregation::Day, PriceType::Last),
384 AggregationSource::External,
385 );
386 let result = bar_type_to_ib_bar_size(&bar_type);
387 assert!(result.is_ok());
388 assert_eq!(result.unwrap(), HistoricalBarSize::Day);
389 }
390
391 #[rstest]
392 fn test_bar_type_to_ib_bar_size_unsupported() {
393 let instrument_id = create_test_instrument_id();
394 let bar_type = BarType::new(
395 instrument_id,
396 BarSpecification::new(99, BarAggregation::Minute, PriceType::Last),
397 AggregationSource::External,
398 );
399 let result = bar_type_to_ib_bar_size(&bar_type);
400 assert!(result.is_err());
401 }
402
403 #[rstest]
404 fn test_price_type_to_ib_what_to_show() {
405 assert_eq!(
406 price_type_to_ib_what_to_show(PriceType::Last),
407 HistoricalWhatToShow::Trades
408 );
409 assert_eq!(
410 price_type_to_ib_what_to_show(PriceType::Bid),
411 HistoricalWhatToShow::Bid
412 );
413 assert_eq!(
414 price_type_to_ib_what_to_show(PriceType::Ask),
415 HistoricalWhatToShow::Ask
416 );
417 assert_eq!(
418 price_type_to_ib_what_to_show(PriceType::Mid),
419 HistoricalWhatToShow::MidPoint
420 );
421 }
422
423 #[rstest]
424 fn test_ib_bar_to_nautilus_bar() {
425 let ib_bar = ibapi::market_data::historical::Bar {
426 date: datetime!(2024-01-01 10:00:00 UTC),
427 open: 150.0,
428 high: 151.0,
429 low: 149.0,
430 close: 150.5,
431 volume: 1000.0,
432 wap: 150.25,
433 count: 100,
434 };
435
436 let instrument_id = create_test_instrument_id();
437 let bar_type = BarType::new(
438 instrument_id,
439 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
440 AggregationSource::External,
441 );
442 let result = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
443 assert!(result.is_ok());
444 let bar = result.unwrap();
445 assert_eq!(bar.open.as_f64(), 150.0);
446 assert_eq!(bar.high.as_f64(), 151.0);
447 assert_eq!(bar.low.as_f64(), 149.0);
448 assert_eq!(bar.close.as_f64(), 150.5);
449 assert_eq!(bar.volume.as_f64(), 1000.0);
450 }
451
452 #[rstest]
453 fn test_ib_bar_to_nautilus_bar_negative_volume() {
454 let ib_bar = ibapi::market_data::historical::Bar {
455 date: datetime!(2024-01-01 10:00:00 UTC),
456 open: 150.0,
457 high: 151.0,
458 low: 149.0,
459 close: 150.5,
460 volume: -1.0, wap: 150.25,
462 count: 100,
463 };
464
465 let instrument_id = create_test_instrument_id();
466 let bar_type = BarType::new(
467 instrument_id,
468 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
469 AggregationSource::External,
470 );
471 let result = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
472 assert!(result.is_ok());
473 let bar = result.unwrap();
474 assert_eq!(bar.volume.as_f64(), 0.0);
476 }
477
478 #[rstest]
479 fn test_ib_timestamp_to_unix_nanos() {
480 let dt = datetime!(2024-01-01 10:00:00 UTC);
481 let result = ib_timestamp_to_unix_nanos(&dt);
482 assert!(result.as_i64() > 0);
483 }
484
485 #[rstest]
486 fn test_chrono_to_ib_datetime() {
487 let dt = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z").unwrap();
488 let utc_dt = dt.with_timezone(&Utc);
489 let result = chrono_to_ib_datetime(&utc_dt);
490 assert_eq!(result.year(), 2024);
491 assert_eq!(result.month(), time::Month::January);
492 assert_eq!(result.day(), 1);
493 }
494
495 #[rstest]
496 fn test_calculate_duration_with_start_and_end() {
497 let start = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z")
498 .unwrap()
499 .with_timezone(&Utc);
500 let end = DateTime::parse_from_rfc3339("2024-01-02T10:00:00Z")
501 .unwrap()
502 .with_timezone(&Utc);
503 let result = calculate_duration(Some(start), Some(end));
504 assert!(result.is_ok());
505 let duration = result.unwrap();
507 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
508 }
509
510 #[rstest]
511 fn test_calculate_duration_no_start() {
512 let end = DateTime::parse_from_rfc3339("2024-01-02T10:00:00Z")
513 .unwrap()
514 .with_timezone(&Utc);
515 let result = calculate_duration(None, Some(end));
516 assert!(result.is_ok());
517 let duration = result.unwrap();
519 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
520 }
521
522 #[rstest]
523 fn test_calculate_duration_no_end() {
524 let start = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z")
525 .unwrap()
526 .with_timezone(&Utc);
527 let result = calculate_duration(Some(start), None);
528 assert!(result.is_ok());
529 let duration = result.unwrap();
531 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
532 }
533
534 #[rstest]
535 fn test_calculate_duration_segments() {
536 let now = Utc::now();
538 let start = now - chrono::Duration::days(365 + 182); let segments = calculate_duration_segments(start, now);
540
541 assert!(!segments.is_empty());
542 assert!(segments.len() >= 2);
544
545 let dur1 = &segments[0].1;
547 assert!(dur1.to_string().contains("1 Y") || dur1.to_string().contains("1Y"));
548 }
549}