1use chrono::{DateTime, Utc};
19use ibapi::market_data::historical::{
20 BarSize as HistoricalBarSize, BarTimestamp, 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> {
37 let spec = bar_type.spec();
38 let aggregation = spec.aggregation;
39 let step = spec.step.get();
40
41 let bar_size = match (aggregation, step) {
42 (BarAggregation::Second, 1) => HistoricalBarSize::Sec,
44 (BarAggregation::Second, 5) => HistoricalBarSize::Sec5,
45 (BarAggregation::Second, 15) => HistoricalBarSize::Sec15,
46 (BarAggregation::Second, 30) => HistoricalBarSize::Sec30,
47 (BarAggregation::Minute, 1) => HistoricalBarSize::Min,
49 (BarAggregation::Minute, 2) => HistoricalBarSize::Min2,
50 (BarAggregation::Minute, 3) => HistoricalBarSize::Min3,
51 (BarAggregation::Minute, 5) => HistoricalBarSize::Min5,
52 (BarAggregation::Minute, 10) => HistoricalBarSize::Min10,
53 (BarAggregation::Minute, 15) => HistoricalBarSize::Min15,
54 (BarAggregation::Minute, 20) => HistoricalBarSize::Min20,
55 (BarAggregation::Minute, 30) => HistoricalBarSize::Min30,
56 (BarAggregation::Hour, 1) => HistoricalBarSize::Hour,
58 (BarAggregation::Hour, 2) => HistoricalBarSize::Hour2,
59 (BarAggregation::Hour, 3) => HistoricalBarSize::Hour3,
60 (BarAggregation::Hour, 4) => HistoricalBarSize::Hour4,
61 (BarAggregation::Hour, 8) => HistoricalBarSize::Hour8,
62 (BarAggregation::Day, 1) => HistoricalBarSize::Day,
64 (BarAggregation::Week, 1) => HistoricalBarSize::Week,
66 (BarAggregation::Month, 1) => HistoricalBarSize::Month,
68 _ => {
69 anyhow::bail!("Unsupported bar aggregation/step combination: {aggregation:?}/{step}",);
70 }
71 };
72
73 Ok(bar_size)
74}
75
76#[must_use]
78pub fn price_type_to_ib_what_to_show(price_type: PriceType) -> HistoricalWhatToShow {
79 match price_type {
80 PriceType::Last => HistoricalWhatToShow::Trades,
81 PriceType::Bid => HistoricalWhatToShow::Bid,
82 PriceType::Ask => HistoricalWhatToShow::Ask,
83 PriceType::Mid => HistoricalWhatToShow::MidPoint,
84 _ => HistoricalWhatToShow::Trades, }
86}
87
88#[must_use]
89pub fn apply_price_magnifier(price: f64, price_magnifier: i32) -> f64 {
90 if price_magnifier > 0 {
91 price / f64::from(price_magnifier)
92 } else {
93 price
94 }
95}
96
97#[must_use]
98pub fn apply_bar_price_magnifier(
99 ib_bar: &ibapi::market_data::historical::Bar,
100 price_magnifier: i32,
101) -> ibapi::market_data::historical::Bar {
102 ibapi::market_data::historical::Bar {
103 date: ib_bar.date,
104 open: apply_price_magnifier(ib_bar.open, price_magnifier),
105 high: apply_price_magnifier(ib_bar.high, price_magnifier),
106 low: apply_price_magnifier(ib_bar.low, price_magnifier),
107 close: apply_price_magnifier(ib_bar.close, price_magnifier),
108 volume: ib_bar.volume,
109 wap: apply_price_magnifier(ib_bar.wap, price_magnifier),
110 count: ib_bar.count,
111 }
112}
113
114fn _validate_bar_prices(open: &mut f64, high: &mut f64, low: &mut f64, close: &f64) {
117 if *high < *low || *high < *open || *high < *close || *low > *open || *low > *close {
118 tracing::warn!(
119 "Invalid bar prices detected: O:{}, H:{}, L:{}, C:{}. Correcting using close price",
120 open,
121 high,
122 low,
123 close
124 );
125 *open = *close;
126 *high = *close;
127 *low = *close;
128 }
129}
130
131pub fn ib_bar_to_nautilus_bar(
137 ib_bar: &ibapi::market_data::historical::Bar,
138 bar_type: BarType,
139 price_precision: u8,
140 size_precision: u8,
141) -> anyhow::Result<Bar> {
142 let ts_event = ib_bar_timestamp_to_unix_nanos(&ib_bar.date);
144 let ts_init = ts_event; let mut open = ib_bar.open;
148 let mut high = ib_bar.high;
149 let mut low = ib_bar.low;
150 let close = ib_bar.close;
151 _validate_bar_prices(&mut open, &mut high, &mut low, &close);
152
153 let open_price = Price::new(open, price_precision);
155 let high_price = Price::new(high, price_precision);
156 let low_price = Price::new(low, price_precision);
157 let close_price = Price::new(close, price_precision);
158
159 let volume = if ib_bar.volume < 0.0 {
161 Quantity::zero(size_precision)
162 } else {
163 Quantity::new(ib_bar.volume, size_precision)
164 };
165
166 Ok(Bar::new(
167 bar_type,
168 open_price,
169 high_price,
170 low_price,
171 close_price,
172 volume,
173 ts_event,
174 ts_init,
175 ))
176}
177
178#[must_use]
180pub fn ib_bar_timestamp_to_unix_nanos(dt: &BarTimestamp) -> UnixNanos {
181 match dt {
182 BarTimestamp::Date(date) => ib_timestamp_to_unix_nanos(&date.midnight().assume_utc()),
183 BarTimestamp::DateTime(dt) => ib_timestamp_to_unix_nanos(dt),
184 }
185}
186
187#[must_use]
189pub fn ib_timestamp_to_unix_nanos(dt: &OffsetDateTime) -> UnixNanos {
190 let timestamp = dt.unix_timestamp_nanos();
191 UnixNanos::from(timestamp as u64)
192}
193
194pub 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(
209 start: Option<DateTime<Utc>>,
210 end: Option<DateTime<Utc>>,
211) -> anyhow::Result<IBDuration> {
212 match (start, end) {
213 (Some(start_dt), Some(end_dt)) => {
214 let duration = end_dt.signed_duration_since(start_dt);
215 let days = duration.num_days();
216
217 if days > 0 && days <= i32::MAX as i64 {
218 Ok((days as i32).days())
219 } else {
220 let seconds = duration.num_seconds();
222 if seconds > 0 && seconds <= i32::MAX as i64 {
223 Ok((seconds as i32).seconds())
224 } else {
225 Ok(1.days())
227 }
228 }
229 }
230 (None, Some(_)) => {
231 Ok(1.days())
233 }
234 (Some(_), None) => {
235 Ok(1.days())
237 }
238 (None, None) => {
239 Ok(1.days())
241 }
242 }
243}
244
245pub fn calculate_duration_segments(
250 start: DateTime<Utc>,
251 end: DateTime<Utc>,
252) -> Vec<(DateTime<Utc>, IBDuration)> {
253 let mut results = Vec::new();
254 let duration = end.signed_duration_since(start);
255 let mut total_seconds = duration.num_seconds();
256
257 if total_seconds <= 0 {
258 return results;
259 }
260
261 let years = total_seconds / (365 * 24 * 3600);
262 total_seconds %= 365 * 24 * 3600;
263 let days = total_seconds / (24 * 3600);
264 total_seconds %= 24 * 3600;
265 let seconds = total_seconds;
266
267 if years > 0 {
268 results.push((end, (years as i32).years()));
269 }
270
271 if days > 0 {
272 let minus_years_duration = chrono::Duration::days(years * 365);
273 let minus_years_date = end - minus_years_duration;
274 results.push((minus_years_date, (days as i32).days()));
275 }
276
277 if seconds > 0 {
278 let minus_years_duration = chrono::Duration::days(years * 365);
279 let minus_days_duration = chrono::Duration::days(days);
280 let minus_days_date = end - minus_years_duration - minus_days_duration;
281 results.push((minus_days_date, (seconds as i32).seconds()));
282 }
283
284 results
285}
286
287#[cfg(test)]
288mod tests {
289 use nautilus_model::{
290 data::{BarSpecification, BarType},
291 enums::{AggregationSource, BarAggregation, PriceType},
292 identifiers::{InstrumentId, Symbol, Venue},
293 };
294 use rstest::rstest;
295 use time::macros::datetime;
296
297 use super::*;
298
299 fn create_test_instrument_id() -> InstrumentId {
300 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
301 }
302
303 #[rstest]
304 fn test_bar_type_to_ib_bar_size_seconds() {
305 let instrument_id = create_test_instrument_id();
306 let bar_type = BarType::new(
307 instrument_id,
308 BarSpecification::new(1, BarAggregation::Second, PriceType::Last),
309 AggregationSource::External,
310 );
311 let result = bar_type_to_ib_bar_size(&bar_type);
312 assert!(result.is_ok());
313 assert_eq!(result.unwrap(), HistoricalBarSize::Sec);
314
315 let bar_type = BarType::new(
316 instrument_id,
317 BarSpecification::new(5, BarAggregation::Second, PriceType::Last),
318 AggregationSource::External,
319 );
320 let result = bar_type_to_ib_bar_size(&bar_type);
321 assert!(result.is_ok());
322 assert_eq!(result.unwrap(), HistoricalBarSize::Sec5);
323 }
324
325 #[rstest]
326 fn test_bar_type_to_ib_bar_size_minutes() {
327 let instrument_id = create_test_instrument_id();
328 let bar_type = BarType::new(
329 instrument_id,
330 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
331 AggregationSource::External,
332 );
333 let result = bar_type_to_ib_bar_size(&bar_type);
334 assert!(result.is_ok());
335 assert_eq!(result.unwrap(), HistoricalBarSize::Min);
336
337 let bar_type = BarType::new(
338 instrument_id,
339 BarSpecification::new(15, BarAggregation::Minute, PriceType::Last),
340 AggregationSource::External,
341 );
342 let result = bar_type_to_ib_bar_size(&bar_type);
343 assert!(result.is_ok());
344 assert_eq!(result.unwrap(), HistoricalBarSize::Min15);
345 }
346
347 #[rstest]
348 fn test_bar_type_to_ib_bar_size_hours() {
349 let instrument_id = create_test_instrument_id();
350 let bar_type = BarType::new(
351 instrument_id,
352 BarSpecification::new(1, BarAggregation::Hour, PriceType::Last),
353 AggregationSource::External,
354 );
355 let result = bar_type_to_ib_bar_size(&bar_type);
356 assert!(result.is_ok());
357 assert_eq!(result.unwrap(), HistoricalBarSize::Hour);
358 }
359
360 #[rstest]
361 fn test_bar_type_to_ib_bar_size_days() {
362 let instrument_id = create_test_instrument_id();
363 let bar_type = BarType::new(
364 instrument_id,
365 BarSpecification::new(1, BarAggregation::Day, PriceType::Last),
366 AggregationSource::External,
367 );
368 let result = bar_type_to_ib_bar_size(&bar_type);
369 assert!(result.is_ok());
370 assert_eq!(result.unwrap(), HistoricalBarSize::Day);
371 }
372
373 #[rstest]
374 fn test_bar_type_to_ib_bar_size_unsupported() {
375 let instrument_id = create_test_instrument_id();
376 let bar_type = BarType::new(
377 instrument_id,
378 BarSpecification::new(12, BarAggregation::Minute, PriceType::Last),
379 AggregationSource::External,
380 );
381 let result = bar_type_to_ib_bar_size(&bar_type);
382 assert!(result.is_err());
383 }
384
385 #[rstest]
386 fn test_price_type_to_ib_what_to_show() {
387 assert_eq!(
388 price_type_to_ib_what_to_show(PriceType::Last),
389 HistoricalWhatToShow::Trades
390 );
391 assert_eq!(
392 price_type_to_ib_what_to_show(PriceType::Bid),
393 HistoricalWhatToShow::Bid
394 );
395 assert_eq!(
396 price_type_to_ib_what_to_show(PriceType::Ask),
397 HistoricalWhatToShow::Ask
398 );
399 assert_eq!(
400 price_type_to_ib_what_to_show(PriceType::Mid),
401 HistoricalWhatToShow::MidPoint
402 );
403 }
404
405 #[rstest]
406 fn test_ib_bar_to_nautilus_bar() {
407 let ib_bar = ibapi::market_data::historical::Bar {
408 date: datetime!(2024-01-01 10:00:00 UTC).into(),
409 open: 150.0,
410 high: 151.0,
411 low: 149.0,
412 close: 150.5,
413 volume: 1000.0,
414 wap: 150.25,
415 count: 100,
416 };
417
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 = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
425 assert!(result.is_ok());
426 let bar = result.unwrap();
427 assert_eq!(bar.open.as_f64(), 150.0);
428 assert_eq!(bar.high.as_f64(), 151.0);
429 assert_eq!(bar.low.as_f64(), 149.0);
430 assert_eq!(bar.close.as_f64(), 150.5);
431 assert_eq!(bar.volume.as_f64(), 1000.0);
432 }
433
434 #[rstest]
435 fn test_ib_bar_to_nautilus_bar_negative_volume() {
436 let ib_bar = ibapi::market_data::historical::Bar {
437 date: datetime!(2024-01-01 10:00:00 UTC).into(),
438 open: 150.0,
439 high: 151.0,
440 low: 149.0,
441 close: 150.5,
442 volume: -1.0, wap: 150.25,
444 count: 100,
445 };
446
447 let instrument_id = create_test_instrument_id();
448 let bar_type = BarType::new(
449 instrument_id,
450 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
451 AggregationSource::External,
452 );
453 let result = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
454 assert!(result.is_ok());
455 let bar = result.unwrap();
456 assert_eq!(bar.volume.as_f64(), 0.0);
458 }
459
460 #[rstest]
461 fn test_ib_timestamp_to_unix_nanos() {
462 let dt = datetime!(2024-01-01 10:00:00 UTC);
463 let result = ib_timestamp_to_unix_nanos(&dt);
464 assert!(result.as_i64() > 0);
465 }
466
467 #[rstest]
468 fn test_chrono_to_ib_datetime() {
469 let dt = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z").unwrap();
470 let utc_dt = dt.with_timezone(&Utc);
471 let result = chrono_to_ib_datetime(&utc_dt);
472 assert_eq!(result.year(), 2024);
473 assert_eq!(result.month(), time::Month::January);
474 assert_eq!(result.day(), 1);
475 }
476
477 #[rstest]
478 fn test_calculate_duration_with_start_and_end() {
479 let start = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z")
480 .unwrap()
481 .with_timezone(&Utc);
482 let end = DateTime::parse_from_rfc3339("2024-01-02T10:00:00Z")
483 .unwrap()
484 .with_timezone(&Utc);
485 let result = calculate_duration(Some(start), Some(end));
486 assert!(result.is_ok());
487 let duration = result.unwrap();
489 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
490 }
491
492 #[rstest]
493 fn test_calculate_duration_no_start() {
494 let end = DateTime::parse_from_rfc3339("2024-01-02T10:00:00Z")
495 .unwrap()
496 .with_timezone(&Utc);
497 let result = calculate_duration(None, Some(end));
498 assert!(result.is_ok());
499 let duration = result.unwrap();
501 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
502 }
503
504 #[rstest]
505 fn test_calculate_duration_no_end() {
506 let start = DateTime::parse_from_rfc3339("2024-01-01T10:00:00Z")
507 .unwrap()
508 .with_timezone(&Utc);
509 let result = calculate_duration(Some(start), None);
510 assert!(result.is_ok());
511 let duration = result.unwrap();
513 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
514 }
515
516 #[rstest]
517 fn test_calculate_duration_segments() {
518 let now = Utc::now();
520 let start = now - chrono::Duration::days(365 + 182); let segments = calculate_duration_segments(start, now);
522
523 assert!(!segments.is_empty());
524 assert!(segments.len() >= 2);
526
527 let dur1 = &segments[0].1;
529 assert!(dur1.to_string().contains("1 Y") || dur1.to_string().contains("1Y"));
530 }
531}