1use ibapi::market_data::{
19 historical::{
20 BarSize as HistoricalBarSize, BarTimestamp, Duration as IBDuration, ToDuration,
21 WhatToShow as HistoricalWhatToShow,
22 },
23 realtime::WhatToShow as RealtimeWhatToShow,
24};
25use jiff::Timestamp;
26use nautilus_core::UnixNanos;
27use nautilus_model::{
28 data::{Bar, BarSpecification, BarType},
29 enums::{BarAggregation, PriceType},
30 types::{Price, Quantity},
31};
32use time::OffsetDateTime;
33
34pub fn bar_type_to_ib_bar_size(bar_type: &BarType) -> anyhow::Result<HistoricalBarSize> {
40 let spec = bar_type.spec();
41 let aggregation = spec.aggregation;
42 let step = spec.step.get();
43
44 let bar_size = match (aggregation, step) {
45 (BarAggregation::Second, 1) => HistoricalBarSize::Sec,
47 (BarAggregation::Second, 5) => HistoricalBarSize::Sec5,
48 (BarAggregation::Second, 15) => HistoricalBarSize::Sec15,
49 (BarAggregation::Second, 30) => HistoricalBarSize::Sec30,
50 (BarAggregation::Minute, 1) => HistoricalBarSize::Min,
52 (BarAggregation::Minute, 2) => HistoricalBarSize::Min2,
53 (BarAggregation::Minute, 3) => HistoricalBarSize::Min3,
54 (BarAggregation::Minute, 5) => HistoricalBarSize::Min5,
55 (BarAggregation::Minute, 10) => HistoricalBarSize::Min10,
56 (BarAggregation::Minute, 15) => HistoricalBarSize::Min15,
57 (BarAggregation::Minute, 20) => HistoricalBarSize::Min20,
58 (BarAggregation::Minute, 30) => HistoricalBarSize::Min30,
59 (BarAggregation::Hour, 1) => HistoricalBarSize::Hour,
61 (BarAggregation::Hour, 2) => HistoricalBarSize::Hour2,
62 (BarAggregation::Hour, 3) => HistoricalBarSize::Hour3,
63 (BarAggregation::Hour, 4) => HistoricalBarSize::Hour4,
64 (BarAggregation::Hour, 8) => HistoricalBarSize::Hour8,
65 (BarAggregation::Day, 1) => HistoricalBarSize::Day,
67 (BarAggregation::Week, 1) => HistoricalBarSize::Week,
69 (BarAggregation::Month, 1) => HistoricalBarSize::Month,
71 _ => {
72 anyhow::bail!("Unsupported bar aggregation/step combination: {aggregation:?}/{step}",);
73 }
74 };
75
76 Ok(bar_size)
77}
78
79#[must_use]
81pub fn price_type_to_ib_what_to_show(price_type: PriceType) -> HistoricalWhatToShow {
82 match price_type {
83 PriceType::Last => HistoricalWhatToShow::Trades,
84 PriceType::Bid => HistoricalWhatToShow::Bid,
85 PriceType::Ask => HistoricalWhatToShow::Ask,
86 PriceType::Mid => HistoricalWhatToShow::MidPoint,
87 _ => HistoricalWhatToShow::Trades, }
89}
90
91#[must_use]
98fn uses_agg_trades(is_crypto: bool, price_type: PriceType) -> bool {
99 is_crypto && price_type == PriceType::Last
100}
101
102#[must_use]
105pub fn price_type_to_ib_what_to_show_for_security(
106 price_type: PriceType,
107 is_crypto: bool,
108) -> HistoricalWhatToShow {
109 if uses_agg_trades(is_crypto, price_type) {
110 return HistoricalWhatToShow::AggTrades;
111 }
112 price_type_to_ib_what_to_show(price_type)
113}
114
115#[must_use]
119pub fn price_type_to_ib_realtime_what_to_show(price_type: PriceType) -> RealtimeWhatToShow {
120 match price_type {
121 PriceType::Last => RealtimeWhatToShow::Trades,
122 PriceType::Bid => RealtimeWhatToShow::Bid,
123 PriceType::Ask => RealtimeWhatToShow::Ask,
124 PriceType::Mid => RealtimeWhatToShow::MidPoint,
125 _ => RealtimeWhatToShow::Trades, }
127}
128
129#[must_use]
132pub fn price_type_to_ib_realtime_what_to_show_for_security(
133 price_type: PriceType,
134 is_crypto: bool,
135) -> RealtimeWhatToShow {
136 if uses_agg_trades(is_crypto, price_type) {
137 return RealtimeWhatToShow::AggTrades;
138 }
139 price_type_to_ib_realtime_what_to_show(price_type)
140}
141
142#[must_use]
143pub fn apply_price_magnifier(price: f64, price_magnifier: i32) -> f64 {
144 if price_magnifier > 0 {
145 price / f64::from(price_magnifier)
146 } else {
147 price
148 }
149}
150
151#[must_use]
152pub fn apply_bar_price_magnifier(
153 ib_bar: &ibapi::market_data::historical::Bar,
154 price_magnifier: i32,
155) -> ibapi::market_data::historical::Bar {
156 ibapi::market_data::historical::Bar {
157 date: ib_bar.date,
158 open: apply_price_magnifier(ib_bar.open, price_magnifier),
159 high: apply_price_magnifier(ib_bar.high, price_magnifier),
160 low: apply_price_magnifier(ib_bar.low, price_magnifier),
161 close: apply_price_magnifier(ib_bar.close, price_magnifier),
162 volume: ib_bar.volume,
163 wap: apply_price_magnifier(ib_bar.wap, price_magnifier),
164 count: ib_bar.count,
165 }
166}
167
168fn _validate_bar_prices(open: &mut f64, high: &mut f64, low: &mut f64, close: &f64) {
171 if *high < *low || *high < *open || *high < *close || *low > *open || *low > *close {
172 tracing::warn!(
173 "Invalid bar prices detected: O:{}, H:{}, L:{}, C:{}. Correcting using close price",
174 open,
175 high,
176 low,
177 close
178 );
179 *open = *close;
180 *high = *close;
181 *low = *close;
182 }
183}
184
185pub fn ib_bar_to_nautilus_bar(
193 ib_bar: &ibapi::market_data::historical::Bar,
194 bar_type: BarType,
195 price_precision: u8,
196 size_precision: u8,
197) -> anyhow::Result<Bar> {
198 let ts_event = bar_close_from_open(
199 ib_bar_timestamp_to_unix_nanos(&ib_bar.date),
200 &bar_type.spec(),
201 );
202 let ts_init = ts_event;
203
204 let mut open = ib_bar.open;
206 let mut high = ib_bar.high;
207 let mut low = ib_bar.low;
208 let close = ib_bar.close;
209 _validate_bar_prices(&mut open, &mut high, &mut low, &close);
210
211 let open_price = Price::new(open, price_precision);
213 let high_price = Price::new(high, price_precision);
214 let low_price = Price::new(low, price_precision);
215 let close_price = Price::new(close, price_precision);
216
217 let volume = if ib_bar.volume < 0.0 {
219 Quantity::zero(size_precision)
220 } else {
221 Quantity::new(ib_bar.volume, size_precision)
222 };
223
224 Ok(Bar::new(
225 bar_type,
226 open_price,
227 high_price,
228 low_price,
229 close_price,
230 volume,
231 ts_event,
232 ts_init,
233 ))
234}
235
236#[must_use]
240pub fn bar_close_from_open(open: UnixNanos, spec: &BarSpecification) -> UnixNanos {
241 let is_day = spec.aggregation == BarAggregation::Day;
242 let duration_ns = match spec.aggregation {
243 BarAggregation::Second
244 | BarAggregation::Minute
245 | BarAggregation::Hour
246 | BarAggregation::Day => spec.timedelta().as_nanos(),
247 _ => return open,
248 };
249 let Ok(duration_ns) = u64::try_from(duration_ns) else {
250 return open;
251 };
252 let close = open.saturating_add_ns(duration_ns);
253 if is_day {
254 close.saturating_sub_ns(1_u64)
255 } else {
256 close
257 }
258}
259
260#[must_use]
262pub fn ib_bar_timestamp_to_unix_nanos(dt: &BarTimestamp) -> UnixNanos {
263 match dt {
264 BarTimestamp::Date(date) => ib_timestamp_to_unix_nanos(&date.midnight().assume_utc()),
265 BarTimestamp::DateTime(dt) => ib_timestamp_to_unix_nanos(dt),
266 }
267}
268
269#[must_use]
271pub fn ib_timestamp_to_unix_nanos(dt: &OffsetDateTime) -> UnixNanos {
272 let timestamp = dt.unix_timestamp_nanos();
273 UnixNanos::from(timestamp as u64)
274}
275
276pub fn jiff_to_ib_datetime(dt: &Timestamp) -> OffsetDateTime {
278 OffsetDateTime::from_unix_timestamp_nanos(dt.as_nanosecond())
279 .unwrap_or_else(|_| OffsetDateTime::now_utc())
280}
281
282pub fn calculate_duration(
288 start: Option<Timestamp>,
289 end: Option<Timestamp>,
290) -> anyhow::Result<IBDuration> {
291 match (start, end) {
292 (Some(start_dt), Some(end_dt)) => {
293 let duration = end_dt.duration_since(start_dt);
294 let days = duration.as_secs() / (24 * 60 * 60);
295
296 if days > 0 && days <= i32::MAX as i64 {
297 Ok((days as i32).days())
298 } else {
299 let seconds = duration.as_secs();
301 if seconds > 0 && seconds <= i32::MAX as i64 {
302 Ok((seconds as i32).seconds())
303 } else {
304 Ok(1.days())
306 }
307 }
308 }
309 (None, Some(_)) => {
310 Ok(1.days())
312 }
313 (Some(_), None) => {
314 Ok(1.days())
316 }
317 (None, None) => {
318 Ok(1.days())
320 }
321 }
322}
323
324pub fn calculate_duration_segments(
329 start: Timestamp,
330 end: Timestamp,
331) -> Vec<(Timestamp, IBDuration)> {
332 let mut results = Vec::new();
333 let duration = end.duration_since(start);
334 let mut total_seconds = duration.as_secs();
335
336 if total_seconds <= 0 {
337 return results;
338 }
339
340 let years = total_seconds / (365 * 24 * 3600);
341 total_seconds %= 365 * 24 * 3600;
342 let days = total_seconds / (24 * 3600);
343 total_seconds %= 24 * 3600;
344 let seconds = total_seconds;
345
346 if years > 0 {
347 results.push((end, (years as i32).years()));
348 }
349
350 if days > 0 {
351 let minus_years_duration = jiff::SignedDuration::from_hours(24 * (years * 365));
352 let minus_years_date = end - minus_years_duration;
353 results.push((minus_years_date, (days as i32).days()));
354 }
355
356 if seconds > 0 {
357 let minus_years_duration = jiff::SignedDuration::from_hours(24 * (years * 365));
358 let minus_days_duration = jiff::SignedDuration::from_hours(24 * (days));
359 let minus_days_date = end - minus_years_duration - minus_days_duration;
360 results.push((minus_days_date, (seconds as i32).seconds()));
361 }
362
363 results
364}
365
366pub fn bar_request_segments(
372 segments: Vec<(Timestamp, IBDuration)>,
373 is_continuous_future: bool,
374) -> Vec<(Option<Timestamp>, IBDuration)> {
375 if is_continuous_future {
376 let now = Timestamp::now();
379 let end_in_past = segments
380 .first()
381 .is_some_and(|(end, _)| *end < now - jiff::SignedDuration::from_secs(1));
382
383 if end_in_past || segments.len() > 1 {
384 tracing::warn!(
385 "Continuous futures cannot use an explicit end_date_time (IB error 10339); \
386 the request is anchored to the current time using only the first duration \
387 segment, so the returned bars may not cover the full requested range"
388 );
389 }
390
391 segments
392 .into_iter()
393 .take(1)
394 .map(|(_, d)| (None, d))
395 .collect()
396 } else {
397 segments
398 .into_iter()
399 .map(|(end, d)| (Some(end), d))
400 .collect()
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use nautilus_model::{
407 data::{BarSpecification, BarType},
408 enums::{AggregationSource, BarAggregation, PriceType},
409 identifiers::{InstrumentId, Symbol, Venue},
410 };
411 use rstest::rstest;
412 use time::macros::datetime;
413
414 use super::*;
415
416 fn create_test_instrument_id() -> InstrumentId {
417 InstrumentId::new(Symbol::from("AAPL"), Venue::from("NASDAQ"))
418 }
419
420 #[rstest]
421 fn test_bar_type_to_ib_bar_size_seconds() {
422 let instrument_id = create_test_instrument_id();
423 let bar_type = BarType::new(
424 instrument_id,
425 BarSpecification::new(1, BarAggregation::Second, PriceType::Last),
426 AggregationSource::External,
427 );
428 let result = bar_type_to_ib_bar_size(&bar_type);
429 assert!(result.is_ok());
430 assert_eq!(result.unwrap(), HistoricalBarSize::Sec);
431
432 let bar_type = BarType::new(
433 instrument_id,
434 BarSpecification::new(5, BarAggregation::Second, PriceType::Last),
435 AggregationSource::External,
436 );
437 let result = bar_type_to_ib_bar_size(&bar_type);
438 assert!(result.is_ok());
439 assert_eq!(result.unwrap(), HistoricalBarSize::Sec5);
440 }
441
442 #[rstest]
443 fn test_bar_type_to_ib_bar_size_minutes() {
444 let instrument_id = create_test_instrument_id();
445 let bar_type = BarType::new(
446 instrument_id,
447 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
448 AggregationSource::External,
449 );
450 let result = bar_type_to_ib_bar_size(&bar_type);
451 assert!(result.is_ok());
452 assert_eq!(result.unwrap(), HistoricalBarSize::Min);
453
454 let bar_type = BarType::new(
455 instrument_id,
456 BarSpecification::new(15, BarAggregation::Minute, PriceType::Last),
457 AggregationSource::External,
458 );
459 let result = bar_type_to_ib_bar_size(&bar_type);
460 assert!(result.is_ok());
461 assert_eq!(result.unwrap(), HistoricalBarSize::Min15);
462 }
463
464 #[rstest]
465 fn test_bar_type_to_ib_bar_size_hours() {
466 let instrument_id = create_test_instrument_id();
467 let bar_type = BarType::new(
468 instrument_id,
469 BarSpecification::new(1, BarAggregation::Hour, PriceType::Last),
470 AggregationSource::External,
471 );
472 let result = bar_type_to_ib_bar_size(&bar_type);
473 assert!(result.is_ok());
474 assert_eq!(result.unwrap(), HistoricalBarSize::Hour);
475 }
476
477 #[rstest]
478 fn test_bar_type_to_ib_bar_size_days() {
479 let instrument_id = create_test_instrument_id();
480 let bar_type = BarType::new(
481 instrument_id,
482 BarSpecification::new(1, BarAggregation::Day, PriceType::Last),
483 AggregationSource::External,
484 );
485 let result = bar_type_to_ib_bar_size(&bar_type);
486 assert!(result.is_ok());
487 assert_eq!(result.unwrap(), HistoricalBarSize::Day);
488 }
489
490 #[rstest]
491 fn test_bar_type_to_ib_bar_size_unsupported() {
492 let instrument_id = create_test_instrument_id();
493 let bar_type = BarType::new(
494 instrument_id,
495 BarSpecification::new(12, BarAggregation::Minute, PriceType::Last),
496 AggregationSource::External,
497 );
498 let result = bar_type_to_ib_bar_size(&bar_type);
499 assert!(result.is_err());
500 }
501
502 #[rstest]
503 fn test_price_type_to_ib_what_to_show() {
504 assert_eq!(
505 price_type_to_ib_what_to_show(PriceType::Last),
506 HistoricalWhatToShow::Trades
507 );
508 assert_eq!(
509 price_type_to_ib_what_to_show(PriceType::Bid),
510 HistoricalWhatToShow::Bid
511 );
512 assert_eq!(
513 price_type_to_ib_what_to_show(PriceType::Ask),
514 HistoricalWhatToShow::Ask
515 );
516 assert_eq!(
517 price_type_to_ib_what_to_show(PriceType::Mid),
518 HistoricalWhatToShow::MidPoint
519 );
520 }
521
522 #[rstest]
523 fn test_price_type_to_ib_what_to_show_for_security_crypto() {
524 assert_eq!(
527 price_type_to_ib_what_to_show_for_security(PriceType::Last, true),
528 HistoricalWhatToShow::AggTrades
529 );
530 assert_eq!(
532 price_type_to_ib_what_to_show_for_security(PriceType::Bid, true),
533 HistoricalWhatToShow::Bid
534 );
535 assert_eq!(
536 price_type_to_ib_what_to_show_for_security(PriceType::Ask, true),
537 HistoricalWhatToShow::Ask
538 );
539 assert_eq!(
540 price_type_to_ib_what_to_show_for_security(PriceType::Mid, true),
541 HistoricalWhatToShow::MidPoint
542 );
543 }
544
545 #[rstest]
546 fn test_price_type_to_ib_what_to_show_for_security_non_crypto() {
547 assert_eq!(
550 price_type_to_ib_what_to_show_for_security(PriceType::Last, false),
551 HistoricalWhatToShow::Trades
552 );
553 assert_eq!(
554 price_type_to_ib_what_to_show_for_security(PriceType::Bid, false),
555 HistoricalWhatToShow::Bid
556 );
557 assert_eq!(
558 price_type_to_ib_what_to_show_for_security(PriceType::Mid, false),
559 HistoricalWhatToShow::MidPoint
560 );
561 }
562
563 #[rstest]
564 fn test_aggtrades_wire_string() {
565 assert_eq!(HistoricalWhatToShow::AggTrades.to_string(), "AGGTRADES");
568 assert_eq!(RealtimeWhatToShow::AggTrades.to_string(), "AGGTRADES");
569 }
570
571 #[rstest]
572 fn test_price_type_to_ib_realtime_what_to_show() {
573 assert!(matches!(
575 price_type_to_ib_realtime_what_to_show(PriceType::Last),
576 RealtimeWhatToShow::Trades
577 ));
578 assert!(matches!(
579 price_type_to_ib_realtime_what_to_show(PriceType::Bid),
580 RealtimeWhatToShow::Bid
581 ));
582 assert!(matches!(
583 price_type_to_ib_realtime_what_to_show(PriceType::Ask),
584 RealtimeWhatToShow::Ask
585 ));
586 assert!(matches!(
587 price_type_to_ib_realtime_what_to_show(PriceType::Mid),
588 RealtimeWhatToShow::MidPoint
589 ));
590 }
591
592 #[rstest]
593 fn test_price_type_to_ib_realtime_what_to_show_for_security_crypto() {
594 assert!(matches!(
599 price_type_to_ib_realtime_what_to_show_for_security(PriceType::Last, true),
600 RealtimeWhatToShow::AggTrades
601 ));
602 assert!(matches!(
604 price_type_to_ib_realtime_what_to_show_for_security(PriceType::Mid, true),
605 RealtimeWhatToShow::MidPoint
606 ));
607 assert!(matches!(
608 price_type_to_ib_realtime_what_to_show_for_security(PriceType::Bid, true),
609 RealtimeWhatToShow::Bid
610 ));
611 assert!(matches!(
613 price_type_to_ib_realtime_what_to_show_for_security(PriceType::Last, false),
614 RealtimeWhatToShow::Trades
615 ));
616 }
617
618 #[rstest]
619 fn test_ib_bar_to_nautilus_bar() {
620 let ib_bar = ibapi::market_data::historical::Bar {
621 date: datetime!(2024-01-01 10:00:00 UTC).into(),
622 open: 150.0,
623 high: 151.0,
624 low: 149.0,
625 close: 150.5,
626 volume: 1000.0,
627 wap: 150.25,
628 count: 100,
629 };
630
631 let instrument_id = create_test_instrument_id();
632 let bar_type = BarType::new(
633 instrument_id,
634 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
635 AggregationSource::External,
636 );
637 let result = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
638 assert!(result.is_ok());
639 let bar = result.unwrap();
640 assert_eq!(bar.open.as_f64(), 150.0);
641 assert_eq!(bar.high.as_f64(), 151.0);
642 assert_eq!(bar.low.as_f64(), 149.0);
643 assert_eq!(bar.close.as_f64(), 150.5);
644 assert_eq!(bar.volume.as_f64(), 1000.0);
645 let close = ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 10:01:00 UTC));
646 assert_eq!(bar.ts_event.as_u64(), close.as_u64());
647 assert_eq!(bar.ts_init.as_u64(), close.as_u64());
648 }
649
650 #[rstest]
651 fn test_ib_bar_to_nautilus_bar_negative_volume() {
652 let ib_bar = ibapi::market_data::historical::Bar {
653 date: datetime!(2024-01-01 10:00:00 UTC).into(),
654 open: 150.0,
655 high: 151.0,
656 low: 149.0,
657 close: 150.5,
658 volume: -1.0, wap: 150.25,
660 count: 100,
661 };
662
663 let instrument_id = create_test_instrument_id();
664 let bar_type = BarType::new(
665 instrument_id,
666 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last),
667 AggregationSource::External,
668 );
669 let result = ib_bar_to_nautilus_bar(&ib_bar, bar_type, 2, 0);
670 assert!(result.is_ok());
671 let bar = result.unwrap();
672 assert_eq!(bar.volume.as_f64(), 0.0);
674 }
675
676 #[rstest]
677 fn test_bar_close_from_open_intraday() {
678 let open = ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 10:00:00 UTC));
679
680 let spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
681 assert_eq!(
682 bar_close_from_open(open, &spec).as_u64(),
683 ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 10:00:01 UTC)).as_u64(),
684 );
685
686 let spec = BarSpecification::new(5, BarAggregation::Second, PriceType::Last);
687 assert_eq!(
688 bar_close_from_open(open, &spec).as_u64(),
689 ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 10:00:05 UTC)).as_u64(),
690 );
691
692 let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
693 assert_eq!(
694 bar_close_from_open(open, &spec).as_u64(),
695 ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 10:01:00 UTC)).as_u64(),
696 );
697
698 let spec = BarSpecification::new(1, BarAggregation::Hour, PriceType::Last);
699 assert_eq!(
700 bar_close_from_open(open, &spec).as_u64(),
701 ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 11:00:00 UTC)).as_u64(),
702 );
703 }
704
705 #[rstest]
706 fn test_bar_close_from_open_day() {
707 let open = ib_timestamp_to_unix_nanos(&datetime!(2024-01-01 00:00:00 UTC));
708 let spec = BarSpecification::new(1, BarAggregation::Day, PriceType::Last);
709 assert_eq!(
710 bar_close_from_open(open, &spec).as_u64(),
711 open.as_u64() + 86_400_000_000_000 - 1,
712 );
713 }
714
715 #[rstest]
716 fn test_bar_close_from_open_week_month() {
717 let open = ib_timestamp_to_unix_nanos(&datetime!(2024-01-13 00:00:00 UTC));
718
719 let spec = BarSpecification::new(1, BarAggregation::Week, PriceType::Last);
720 assert_eq!(bar_close_from_open(open, &spec).as_u64(), open.as_u64());
721
722 let spec = BarSpecification::new(1, BarAggregation::Month, PriceType::Last);
723 assert_eq!(bar_close_from_open(open, &spec).as_u64(), open.as_u64());
724 }
725
726 #[rstest]
727 fn test_ib_timestamp_to_unix_nanos() {
728 let dt = datetime!(2024-01-01 10:00:00 UTC);
729 let result = ib_timestamp_to_unix_nanos(&dt);
730 assert!(result.as_i64() > 0);
731 }
732
733 #[rstest]
734 fn test_jiff_to_ib_datetime() {
735 let utc_dt = "2024-01-01T10:00:00Z".parse::<Timestamp>().unwrap();
736 let result = jiff_to_ib_datetime(&utc_dt);
737 assert_eq!(result.year(), 2024);
738 assert_eq!(result.month(), time::Month::January);
739 assert_eq!(result.day(), 1);
740 }
741
742 #[rstest]
743 fn test_calculate_duration_with_start_and_end() {
744 let start = "2024-01-01T10:00:00Z".parse::<Timestamp>().unwrap();
745 let end = "2024-01-02T10:00:00Z".parse::<Timestamp>().unwrap();
746 let result = calculate_duration(Some(start), Some(end));
747 assert!(result.is_ok());
748 let duration = result.unwrap();
750 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
751 }
752
753 #[rstest]
754 fn test_calculate_duration_no_start() {
755 let end = "2024-01-02T10:00:00Z".parse::<Timestamp>().unwrap();
756 let result = calculate_duration(None, Some(end));
757 assert!(result.is_ok());
758 let duration = result.unwrap();
760 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
761 }
762
763 #[rstest]
764 fn test_calculate_duration_no_end() {
765 let start = "2024-01-01T10:00:00Z".parse::<Timestamp>().unwrap();
766 let result = calculate_duration(Some(start), None);
767 assert!(result.is_ok());
768 let duration = result.unwrap();
770 assert!(duration.to_string().contains("1 D") || duration.to_string().contains("1D"));
771 }
772
773 #[rstest]
774 fn test_calculate_duration_segments() {
775 let now = Timestamp::now();
777 let start = now - jiff::SignedDuration::from_hours(24 * (365 + 182)); let segments = calculate_duration_segments(start, now);
779
780 assert!(!segments.is_empty());
781 assert!(segments.len() >= 2);
783
784 let dur1 = &segments[0].1;
786 assert!(dur1.to_string().contains("1 Y") || dur1.to_string().contains("1Y"));
787 }
788
789 #[rstest]
790 fn test_bar_request_segments_attaches_end_dates_when_not_continuous() {
791 let end = "2025-01-01T00:00:00Z".parse::<Timestamp>().unwrap();
792 let earlier = "2024-06-01T00:00:00Z".parse::<Timestamp>().unwrap();
793 let segments = vec![(end, IBDuration::years(1)), (earlier, IBDuration::days(30))];
794
795 let result = bar_request_segments(segments, false);
796
797 assert_eq!(result.len(), 2);
798 assert_eq!(result[0].0, Some(end));
799 assert_eq!(result[1].0, Some(earlier));
800 }
801
802 #[rstest]
803 fn test_bar_request_segments_drops_end_date_and_keeps_only_first_for_continuous() {
804 let end = "2025-01-01T00:00:00Z".parse::<Timestamp>().unwrap();
805 let earlier = "2024-06-01T00:00:00Z".parse::<Timestamp>().unwrap();
806 let segments = vec![(end, IBDuration::years(1)), (earlier, IBDuration::days(30))];
807
808 let result = bar_request_segments(segments, true);
809
810 assert_eq!(result.len(), 1);
811 assert_eq!(result[0].0, None);
812 assert_eq!(result[0].1, IBDuration::years(1));
813 }
814
815 #[rstest]
816 fn test_bar_request_segments_empty_input_yields_nothing_for_continuous() {
817 let result = bar_request_segments(vec![], true);
818 assert!(result.is_empty());
819 }
820}