1use std::{
19 collections::HashMap,
20 fmt::{Debug, Display},
21 hash::Hash,
22 num::{NonZero, NonZeroUsize},
23 str::FromStr,
24};
25
26use chrono::{DateTime, Datelike, Duration, SubsecRound, TimeDelta, Timelike, Utc};
27use derive_builder::Builder;
28use indexmap::IndexMap;
29use nautilus_core::{
30 UnixNanos,
31 correctness::{FAILED, check_predicate_true},
32 datetime::{add_n_months, subtract_n_months},
33 serialization::Serializable,
34};
35use serde::{Deserialize, Deserializer, Serialize, Serializer};
36
37use super::HasTsInit;
38use crate::{
39 enums::{AggregationSource, BarAggregation, PriceType},
40 identifiers::InstrumentId,
41 types::{Price, Quantity, fixed::FIXED_SIZE_BINARY},
42};
43
44pub const BAR_SPEC_1_SECOND_LAST: BarSpecification = BarSpecification {
45 step: NonZero::new(1).unwrap(),
46 aggregation: BarAggregation::Second,
47 price_type: PriceType::Last,
48};
49pub const BAR_SPEC_1_MINUTE_LAST: BarSpecification = BarSpecification {
50 step: NonZero::new(1).unwrap(),
51 aggregation: BarAggregation::Minute,
52 price_type: PriceType::Last,
53};
54pub const BAR_SPEC_3_MINUTE_LAST: BarSpecification = BarSpecification {
55 step: NonZero::new(3).unwrap(),
56 aggregation: BarAggregation::Minute,
57 price_type: PriceType::Last,
58};
59pub const BAR_SPEC_5_MINUTE_LAST: BarSpecification = BarSpecification {
60 step: NonZero::new(5).unwrap(),
61 aggregation: BarAggregation::Minute,
62 price_type: PriceType::Last,
63};
64pub const BAR_SPEC_15_MINUTE_LAST: BarSpecification = BarSpecification {
65 step: NonZero::new(15).unwrap(),
66 aggregation: BarAggregation::Minute,
67 price_type: PriceType::Last,
68};
69pub const BAR_SPEC_30_MINUTE_LAST: BarSpecification = BarSpecification {
70 step: NonZero::new(30).unwrap(),
71 aggregation: BarAggregation::Minute,
72 price_type: PriceType::Last,
73};
74pub const BAR_SPEC_1_HOUR_LAST: BarSpecification = BarSpecification {
75 step: NonZero::new(1).unwrap(),
76 aggregation: BarAggregation::Hour,
77 price_type: PriceType::Last,
78};
79pub const BAR_SPEC_2_HOUR_LAST: BarSpecification = BarSpecification {
80 step: NonZero::new(2).unwrap(),
81 aggregation: BarAggregation::Hour,
82 price_type: PriceType::Last,
83};
84pub const BAR_SPEC_4_HOUR_LAST: BarSpecification = BarSpecification {
85 step: NonZero::new(4).unwrap(),
86 aggregation: BarAggregation::Hour,
87 price_type: PriceType::Last,
88};
89pub const BAR_SPEC_6_HOUR_LAST: BarSpecification = BarSpecification {
90 step: NonZero::new(6).unwrap(),
91 aggregation: BarAggregation::Hour,
92 price_type: PriceType::Last,
93};
94pub const BAR_SPEC_12_HOUR_LAST: BarSpecification = BarSpecification {
95 step: NonZero::new(12).unwrap(),
96 aggregation: BarAggregation::Hour,
97 price_type: PriceType::Last,
98};
99pub const BAR_SPEC_1_DAY_LAST: BarSpecification = BarSpecification {
100 step: NonZero::new(1).unwrap(),
101 aggregation: BarAggregation::Day,
102 price_type: PriceType::Last,
103};
104pub const BAR_SPEC_2_DAY_LAST: BarSpecification = BarSpecification {
105 step: NonZero::new(2).unwrap(),
106 aggregation: BarAggregation::Day,
107 price_type: PriceType::Last,
108};
109pub const BAR_SPEC_3_DAY_LAST: BarSpecification = BarSpecification {
110 step: NonZero::new(3).unwrap(),
111 aggregation: BarAggregation::Day,
112 price_type: PriceType::Last,
113};
114pub const BAR_SPEC_5_DAY_LAST: BarSpecification = BarSpecification {
115 step: NonZero::new(5).unwrap(),
116 aggregation: BarAggregation::Day,
117 price_type: PriceType::Last,
118};
119pub const BAR_SPEC_1_WEEK_LAST: BarSpecification = BarSpecification {
120 step: NonZero::new(1).unwrap(),
121 aggregation: BarAggregation::Week,
122 price_type: PriceType::Last,
123};
124pub const BAR_SPEC_1_MONTH_LAST: BarSpecification = BarSpecification {
125 step: NonZero::new(1).unwrap(),
126 aggregation: BarAggregation::Month,
127 price_type: PriceType::Last,
128};
129pub const BAR_SPEC_3_MONTH_LAST: BarSpecification = BarSpecification {
130 step: NonZero::new(3).unwrap(),
131 aggregation: BarAggregation::Month,
132 price_type: PriceType::Last,
133};
134pub const BAR_SPEC_6_MONTH_LAST: BarSpecification = BarSpecification {
135 step: NonZero::new(6).unwrap(),
136 aggregation: BarAggregation::Month,
137 price_type: PriceType::Last,
138};
139pub const BAR_SPEC_12_MONTH_LAST: BarSpecification = BarSpecification {
140 step: NonZero::new(12).unwrap(),
141 aggregation: BarAggregation::Month,
142 price_type: PriceType::Last,
143};
144
145pub fn get_bar_interval(bar_type: &BarType) -> TimeDelta {
151 let spec = bar_type.spec();
152
153 match spec.aggregation {
154 BarAggregation::Millisecond => TimeDelta::milliseconds(spec.step.get() as i64),
155 BarAggregation::Second => TimeDelta::seconds(spec.step.get() as i64),
156 BarAggregation::Minute => TimeDelta::minutes(spec.step.get() as i64),
157 BarAggregation::Hour => TimeDelta::hours(spec.step.get() as i64),
158 BarAggregation::Day => TimeDelta::days(spec.step.get() as i64),
159 BarAggregation::Week => TimeDelta::days(7 * spec.step.get() as i64),
160 BarAggregation::Month => TimeDelta::days(30 * spec.step.get() as i64), BarAggregation::Year => TimeDelta::days(365 * spec.step.get() as i64), _ => panic!("Aggregation not time based"),
163 }
164}
165
166pub fn get_bar_interval_ns(bar_type: &BarType) -> UnixNanos {
172 let interval_ns = get_bar_interval(bar_type)
173 .num_nanoseconds()
174 .expect("Invalid bar interval") as u64;
175 UnixNanos::from(interval_ns)
176}
177
178pub fn get_time_bar_start(
185 now: DateTime<Utc>,
186 bar_type: &BarType,
187 time_bars_origin: Option<TimeDelta>,
188) -> DateTime<Utc> {
189 let spec = bar_type.spec();
190 let step = spec.step.get() as i64;
191 let origin_offset: TimeDelta = time_bars_origin.unwrap_or_else(TimeDelta::zero);
192
193 match spec.aggregation {
194 BarAggregation::Millisecond => {
195 find_closest_smaller_time(now, origin_offset, Duration::milliseconds(step))
196 }
197 BarAggregation::Second => {
198 find_closest_smaller_time(now, origin_offset, Duration::seconds(step))
199 }
200 BarAggregation::Minute => {
201 find_closest_smaller_time(now, origin_offset, Duration::minutes(step))
202 }
203 BarAggregation::Hour => {
204 find_closest_smaller_time(now, origin_offset, Duration::hours(step))
205 }
206 BarAggregation::Day => find_closest_smaller_time(now, origin_offset, Duration::days(step)),
207 BarAggregation::Week => {
208 let mut start_time = now.trunc_subsecs(0)
209 - Duration::seconds(now.second() as i64)
210 - Duration::minutes(now.minute() as i64)
211 - Duration::hours(now.hour() as i64)
212 - TimeDelta::days(now.weekday().num_days_from_monday() as i64);
213 start_time += origin_offset;
214
215 if now < start_time {
216 start_time -= Duration::weeks(step);
217 }
218
219 start_time
220 }
221 BarAggregation::Month => {
222 let mut start_time = DateTime::from_naive_utc_and_offset(
224 chrono::NaiveDate::from_ymd_opt(now.year(), 1, 1)
225 .expect("valid date")
226 .and_hms_opt(0, 0, 0)
227 .expect("valid time"),
228 Utc,
229 );
230 start_time += origin_offset;
231
232 if now < start_time {
233 start_time =
234 subtract_n_months(start_time, 12).expect("Failed to subtract 12 months");
235 }
236
237 let months_step = step as u32;
238 while start_time <= now {
239 start_time =
240 add_n_months(start_time, months_step).expect("Failed to add months in loop");
241 }
242
243 start_time =
244 subtract_n_months(start_time, months_step).expect("Failed to subtract months_step");
245 start_time
246 }
247 BarAggregation::Year => {
248 let step_i32 = step as i32;
249
250 let year_start = |y: i32| {
252 DateTime::from_naive_utc_and_offset(
253 chrono::NaiveDate::from_ymd_opt(y, 1, 1)
254 .expect("valid date")
255 .and_hms_opt(0, 0, 0)
256 .expect("valid time"),
257 Utc,
258 ) + origin_offset
259 };
260
261 let mut year = now.year();
262 if year_start(year) > now {
263 year -= step_i32;
264 }
265
266 while year_start(year + step_i32) <= now {
267 year += step_i32;
268 }
269
270 year_start(year)
271 }
272 _ => panic!(
273 "Aggregation type {} not supported for time bars",
274 spec.aggregation
275 ),
276 }
277}
278
279fn find_closest_smaller_time(
284 now: DateTime<Utc>,
285 daily_time_origin: TimeDelta,
286 period: TimeDelta,
287) -> DateTime<Utc> {
288 let day_start = now.trunc_subsecs(0)
290 - Duration::seconds(now.second() as i64)
291 - Duration::minutes(now.minute() as i64)
292 - Duration::hours(now.hour() as i64);
293 let base_time = day_start + daily_time_origin;
294
295 let time_difference = now - base_time;
296 let period_ns = period.num_nanoseconds().unwrap_or(1);
297
298 let num_periods = time_difference
301 .num_nanoseconds()
302 .unwrap_or(0)
303 .div_euclid(period_ns);
304
305 base_time + TimeDelta::nanoseconds(num_periods * period_ns)
306}
307
308#[repr(C)]
311#[derive(
312 Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize, Builder,
313)]
314#[cfg_attr(
315 feature = "python",
316 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
317)]
318#[cfg_attr(
319 feature = "python",
320 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
321)]
322pub struct BarSpecification {
323 pub step: NonZeroUsize,
325 pub aggregation: BarAggregation,
327 pub price_type: PriceType,
329}
330
331impl BarSpecification {
332 pub fn new_checked(
342 step: usize,
343 aggregation: BarAggregation,
344 price_type: PriceType,
345 ) -> anyhow::Result<Self> {
346 let step = NonZeroUsize::new(step)
347 .ok_or(anyhow::anyhow!("Invalid step: {step} (must be non-zero)"))?;
348 Ok(Self {
349 step,
350 aggregation,
351 price_type,
352 })
353 }
354
355 #[must_use]
361 pub fn new(step: usize, aggregation: BarAggregation, price_type: PriceType) -> Self {
362 Self::new_checked(step, aggregation, price_type).expect(FAILED)
363 }
364
365 pub fn timedelta(&self) -> TimeDelta {
377 match self.aggregation {
378 BarAggregation::Millisecond => Duration::milliseconds(self.step.get() as i64),
379 BarAggregation::Second => Duration::seconds(self.step.get() as i64),
380 BarAggregation::Minute => Duration::minutes(self.step.get() as i64),
381 BarAggregation::Hour => Duration::hours(self.step.get() as i64),
382 BarAggregation::Day => Duration::days(self.step.get() as i64),
383 BarAggregation::Week => Duration::days(self.step.get() as i64 * 7),
384 BarAggregation::Month => Duration::days(self.step.get() as i64 * 30), BarAggregation::Year => Duration::days(self.step.get() as i64 * 365), _ => panic!(
387 "Timedelta not supported for aggregation type: {:?}",
388 self.aggregation
389 ),
390 }
391 }
392
393 pub fn is_time_aggregated(&self) -> bool {
403 matches!(
404 self.aggregation,
405 BarAggregation::Millisecond
406 | BarAggregation::Second
407 | BarAggregation::Minute
408 | BarAggregation::Hour
409 | BarAggregation::Day
410 | BarAggregation::Week
411 | BarAggregation::Month
412 | BarAggregation::Year
413 )
414 }
415
416 pub fn is_threshold_aggregated(&self) -> bool {
424 matches!(
425 self.aggregation,
426 BarAggregation::Tick
427 | BarAggregation::TickImbalance
428 | BarAggregation::Volume
429 | BarAggregation::VolumeImbalance
430 | BarAggregation::Value
431 | BarAggregation::ValueImbalance
432 )
433 }
434
435 pub fn is_information_aggregated(&self) -> bool {
440 matches!(
441 self.aggregation,
442 BarAggregation::TickRuns | BarAggregation::VolumeRuns | BarAggregation::ValueRuns
443 )
444 }
445}
446
447impl Display for BarSpecification {
448 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
449 write!(f, "{}-{}-{}", self.step, self.aggregation, self.price_type)
450 }
451}
452
453#[repr(C)]
456#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
457#[cfg_attr(
458 feature = "python",
459 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
460)]
461#[cfg_attr(
462 feature = "python",
463 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.model")
464)]
465pub enum BarType {
466 Standard {
467 instrument_id: InstrumentId,
469 spec: BarSpecification,
471 aggregation_source: AggregationSource,
473 },
474 Composite {
475 instrument_id: InstrumentId,
477 spec: BarSpecification,
479 aggregation_source: AggregationSource,
481
482 composite_step: usize,
484 composite_aggregation: BarAggregation,
486 composite_aggregation_source: AggregationSource,
488 },
489}
490
491impl BarType {
492 #[must_use]
494 pub fn new(
495 instrument_id: InstrumentId,
496 spec: BarSpecification,
497 aggregation_source: AggregationSource,
498 ) -> Self {
499 Self::Standard {
500 instrument_id,
501 spec,
502 aggregation_source,
503 }
504 }
505
506 pub fn new_composite(
508 instrument_id: InstrumentId,
509 spec: BarSpecification,
510 aggregation_source: AggregationSource,
511
512 composite_step: usize,
513 composite_aggregation: BarAggregation,
514 composite_aggregation_source: AggregationSource,
515 ) -> Self {
516 Self::Composite {
517 instrument_id,
518 spec,
519 aggregation_source,
520
521 composite_step,
522 composite_aggregation,
523 composite_aggregation_source,
524 }
525 }
526
527 pub fn is_standard(&self) -> bool {
529 match &self {
530 Self::Standard { .. } => true,
531 Self::Composite { .. } => false,
532 }
533 }
534
535 pub fn is_composite(&self) -> bool {
537 match &self {
538 Self::Standard { .. } => false,
539 Self::Composite { .. } => true,
540 }
541 }
542
543 #[must_use]
545 pub fn standard(&self) -> Self {
546 match self {
547 &b @ Self::Standard { .. } => b,
548 Self::Composite {
549 instrument_id,
550 spec,
551 aggregation_source,
552 ..
553 } => Self::new(*instrument_id, *spec, *aggregation_source),
554 }
555 }
556
557 #[must_use]
559 pub fn composite(&self) -> Self {
560 match self {
561 &b @ Self::Standard { .. } => b, Self::Composite {
563 instrument_id,
564 spec,
565 aggregation_source: _,
566
567 composite_step,
568 composite_aggregation,
569 composite_aggregation_source,
570 } => Self::new(
571 *instrument_id,
572 BarSpecification::new(*composite_step, *composite_aggregation, spec.price_type),
573 *composite_aggregation_source,
574 ),
575 }
576 }
577
578 pub fn instrument_id(&self) -> InstrumentId {
580 match &self {
581 Self::Standard { instrument_id, .. } | Self::Composite { instrument_id, .. } => {
582 *instrument_id
583 }
584 }
585 }
586
587 pub fn spec(&self) -> BarSpecification {
589 match &self {
590 Self::Standard { spec, .. } | Self::Composite { spec, .. } => *spec,
591 }
592 }
593
594 pub fn aggregation_source(&self) -> AggregationSource {
596 match &self {
597 Self::Standard {
598 aggregation_source, ..
599 }
600 | Self::Composite {
601 aggregation_source, ..
602 } => *aggregation_source,
603 }
604 }
605
606 #[must_use]
612 pub fn id_spec_key(&self) -> (InstrumentId, BarSpecification) {
613 (self.instrument_id(), self.spec())
614 }
615}
616
617#[derive(thiserror::Error, Debug)]
618#[error("Error parsing `BarType` from '{input}', invalid token: '{token}' at position {position}")]
619pub struct BarTypeParseError {
620 input: String,
621 token: String,
622 position: usize,
623}
624
625impl FromStr for BarType {
626 type Err = BarTypeParseError;
627
628 #[allow(clippy::needless_collect)] fn from_str(s: &str) -> Result<Self, Self::Err> {
630 let parts: Vec<&str> = s.split('@').collect();
631 let standard = parts[0];
632 let composite_str = parts.get(1);
633
634 let pieces: Vec<&str> = standard.rsplitn(5, '-').collect();
635 let rev_pieces: Vec<&str> = pieces.into_iter().rev().collect();
636 if rev_pieces.len() != 5 {
637 return Err(BarTypeParseError {
638 input: s.to_string(),
639 token: String::new(),
640 position: 0,
641 });
642 }
643
644 let instrument_id =
645 InstrumentId::from_str(rev_pieces[0]).map_err(|_| BarTypeParseError {
646 input: s.to_string(),
647 token: rev_pieces[0].to_string(),
648 position: 0,
649 })?;
650
651 let step = rev_pieces[1].parse().map_err(|_| BarTypeParseError {
652 input: s.to_string(),
653 token: rev_pieces[1].to_string(),
654 position: 1,
655 })?;
656 let aggregation =
657 BarAggregation::from_str(rev_pieces[2]).map_err(|_| BarTypeParseError {
658 input: s.to_string(),
659 token: rev_pieces[2].to_string(),
660 position: 2,
661 })?;
662 let price_type = PriceType::from_str(rev_pieces[3]).map_err(|_| BarTypeParseError {
663 input: s.to_string(),
664 token: rev_pieces[3].to_string(),
665 position: 3,
666 })?;
667 let aggregation_source =
668 AggregationSource::from_str(rev_pieces[4]).map_err(|_| BarTypeParseError {
669 input: s.to_string(),
670 token: rev_pieces[4].to_string(),
671 position: 4,
672 })?;
673
674 if let Some(composite_str) = composite_str {
675 let composite_pieces: Vec<&str> = composite_str.rsplitn(3, '-').collect();
676 let rev_composite_pieces: Vec<&str> = composite_pieces.into_iter().rev().collect();
677 if rev_composite_pieces.len() != 3 {
678 return Err(BarTypeParseError {
679 input: s.to_string(),
680 token: String::new(),
681 position: 5,
682 });
683 }
684
685 let composite_step =
686 rev_composite_pieces[0]
687 .parse()
688 .map_err(|_| BarTypeParseError {
689 input: s.to_string(),
690 token: rev_composite_pieces[0].to_string(),
691 position: 5,
692 })?;
693 let composite_aggregation =
694 BarAggregation::from_str(rev_composite_pieces[1]).map_err(|_| {
695 BarTypeParseError {
696 input: s.to_string(),
697 token: rev_composite_pieces[1].to_string(),
698 position: 6,
699 }
700 })?;
701 let composite_aggregation_source = AggregationSource::from_str(rev_composite_pieces[2])
702 .map_err(|_| BarTypeParseError {
703 input: s.to_string(),
704 token: rev_composite_pieces[2].to_string(),
705 position: 7,
706 })?;
707
708 Ok(Self::new_composite(
709 instrument_id,
710 BarSpecification::new(step, aggregation, price_type),
711 aggregation_source,
712 composite_step,
713 composite_aggregation,
714 composite_aggregation_source,
715 ))
716 } else {
717 Ok(Self::Standard {
718 instrument_id,
719 spec: BarSpecification::new(step, aggregation, price_type),
720 aggregation_source,
721 })
722 }
723 }
724}
725
726impl<T: AsRef<str>> From<T> for BarType {
727 fn from(value: T) -> Self {
728 Self::from_str(value.as_ref()).expect(FAILED)
729 }
730}
731
732impl Display for BarType {
733 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734 match &self {
735 Self::Standard {
736 instrument_id,
737 spec,
738 aggregation_source,
739 } => {
740 write!(f, "{instrument_id}-{spec}-{aggregation_source}")
741 }
742 Self::Composite {
743 instrument_id,
744 spec,
745 aggregation_source,
746
747 composite_step,
748 composite_aggregation,
749 composite_aggregation_source,
750 } => {
751 write!(
752 f,
753 "{}-{}-{}@{}-{}-{}",
754 instrument_id,
755 spec,
756 aggregation_source,
757 *composite_step,
758 *composite_aggregation,
759 *composite_aggregation_source
760 )
761 }
762 }
763 }
764}
765
766impl Serialize for BarType {
767 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
768 where
769 S: Serializer,
770 {
771 serializer.serialize_str(&self.to_string())
772 }
773}
774
775impl<'de> Deserialize<'de> for BarType {
776 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
777 where
778 D: Deserializer<'de>,
779 {
780 let s: String = Deserialize::deserialize(deserializer)?;
781 Self::from_str(&s).map_err(serde::de::Error::custom)
782 }
783}
784
785#[repr(C)]
787#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)]
788#[serde(tag = "type")]
789#[cfg_attr(
790 feature = "python",
791 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
792)]
793#[cfg_attr(
794 feature = "python",
795 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
796)]
797pub struct Bar {
798 pub bar_type: BarType,
800 pub open: Price,
802 pub high: Price,
804 pub low: Price,
806 pub close: Price,
808 pub volume: Quantity,
810 pub ts_event: UnixNanos,
812 pub ts_init: UnixNanos,
814}
815
816impl Bar {
817 #[allow(clippy::too_many_arguments)]
830 pub fn new_checked(
831 bar_type: BarType,
832 open: Price,
833 high: Price,
834 low: Price,
835 close: Price,
836 volume: Quantity,
837 ts_event: UnixNanos,
838 ts_init: UnixNanos,
839 ) -> anyhow::Result<Self> {
840 check_predicate_true(high >= open, "high >= open")?;
841 check_predicate_true(high >= low, "high >= low")?;
842 check_predicate_true(high >= close, "high >= close")?;
843 check_predicate_true(low <= close, "low <= close")?;
844 check_predicate_true(low <= open, "low <= open")?;
845
846 Ok(Self {
847 bar_type,
848 open,
849 high,
850 low,
851 close,
852 volume,
853 ts_event,
854 ts_init,
855 })
856 }
857
858 #[allow(clippy::too_many_arguments)]
867 pub fn new(
868 bar_type: BarType,
869 open: Price,
870 high: Price,
871 low: Price,
872 close: Price,
873 volume: Quantity,
874 ts_event: UnixNanos,
875 ts_init: UnixNanos,
876 ) -> Self {
877 Self::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
878 .expect(FAILED)
879 }
880
881 pub fn instrument_id(&self) -> InstrumentId {
882 self.bar_type.instrument_id()
883 }
884
885 #[must_use]
887 pub fn get_metadata(
888 bar_type: &BarType,
889 price_precision: u8,
890 size_precision: u8,
891 ) -> HashMap<String, String> {
892 let mut metadata = HashMap::new();
893 let instrument_id = bar_type.instrument_id();
894 metadata.insert("bar_type".to_string(), bar_type.to_string());
895 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
896 metadata.insert("price_precision".to_string(), price_precision.to_string());
897 metadata.insert("size_precision".to_string(), size_precision.to_string());
898 metadata
899 }
900
901 #[must_use]
903 pub fn get_fields() -> IndexMap<String, String> {
904 let mut metadata = IndexMap::new();
905 metadata.insert("open".to_string(), FIXED_SIZE_BINARY.to_string());
906 metadata.insert("high".to_string(), FIXED_SIZE_BINARY.to_string());
907 metadata.insert("low".to_string(), FIXED_SIZE_BINARY.to_string());
908 metadata.insert("close".to_string(), FIXED_SIZE_BINARY.to_string());
909 metadata.insert("volume".to_string(), FIXED_SIZE_BINARY.to_string());
910 metadata.insert("ts_event".to_string(), "UInt64".to_string());
911 metadata.insert("ts_init".to_string(), "UInt64".to_string());
912 metadata
913 }
914}
915
916impl Display for Bar {
917 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
918 write!(
919 f,
920 "{},{},{},{},{},{},{}",
921 self.bar_type, self.open, self.high, self.low, self.close, self.volume, self.ts_event
922 )
923 }
924}
925
926impl Serializable for Bar {}
927
928impl HasTsInit for Bar {
929 fn ts_init(&self) -> UnixNanos {
930 self.ts_init
931 }
932}
933
934#[cfg(test)]
935mod tests {
936 use std::str::FromStr;
937
938 use chrono::TimeZone;
939 use nautilus_core::serialization::msgpack::{FromMsgPack, ToMsgPack};
940 use rstest::rstest;
941
942 use super::*;
943 use crate::identifiers::{Symbol, Venue};
944
945 #[rstest]
946 fn test_bar_specification_new_invalid() {
947 let result = BarSpecification::new_checked(0, BarAggregation::Tick, PriceType::Last);
948 assert!(result.is_err());
949 }
950
951 #[rstest]
952 #[should_panic(expected = "Invalid step: 0 (must be non-zero)")]
953 fn test_bar_specification_new_checked_with_invalid_step_panics() {
954 let aggregation = BarAggregation::Tick;
955 let price_type = PriceType::Last;
956
957 let _ = BarSpecification::new(0, aggregation, price_type);
958 }
959
960 #[rstest]
961 #[case(BarAggregation::Millisecond, 1, TimeDelta::milliseconds(1))]
962 #[case(BarAggregation::Millisecond, 10, TimeDelta::milliseconds(10))]
963 #[case(BarAggregation::Second, 1, TimeDelta::seconds(1))]
964 #[case(BarAggregation::Second, 15, TimeDelta::seconds(15))]
965 #[case(BarAggregation::Minute, 1, TimeDelta::minutes(1))]
966 #[case(BarAggregation::Minute, 60, TimeDelta::minutes(60))]
967 #[case(BarAggregation::Hour, 1, TimeDelta::hours(1))]
968 #[case(BarAggregation::Hour, 4, TimeDelta::hours(4))]
969 #[case(BarAggregation::Day, 1, TimeDelta::days(1))]
970 #[case(BarAggregation::Day, 2, TimeDelta::days(2))]
971 #[case(BarAggregation::Week, 1, TimeDelta::days(7))]
972 #[case(BarAggregation::Week, 2, TimeDelta::days(14))]
973 #[case(BarAggregation::Month, 1, TimeDelta::days(30))]
974 #[case(BarAggregation::Month, 3, TimeDelta::days(90))]
975 #[case(BarAggregation::Year, 1, TimeDelta::days(365))]
976 #[case(BarAggregation::Year, 2, TimeDelta::days(730))]
977 #[should_panic(expected = "Aggregation not time based")]
978 #[case(BarAggregation::Tick, 1, TimeDelta::zero())]
979 fn test_get_bar_interval(
980 #[case] aggregation: BarAggregation,
981 #[case] step: usize,
982 #[case] expected: TimeDelta,
983 ) {
984 let bar_type = BarType::Standard {
985 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
986 spec: BarSpecification::new(step, aggregation, PriceType::Last),
987 aggregation_source: AggregationSource::Internal,
988 };
989
990 let interval = get_bar_interval(&bar_type);
991 assert_eq!(interval, expected);
992 }
993
994 #[rstest]
995 #[case(BarAggregation::Millisecond, 1, UnixNanos::from(1_000_000))]
996 #[case(BarAggregation::Millisecond, 10, UnixNanos::from(10_000_000))]
997 #[case(BarAggregation::Second, 1, UnixNanos::from(1_000_000_000))]
998 #[case(BarAggregation::Second, 10, UnixNanos::from(10_000_000_000))]
999 #[case(BarAggregation::Minute, 1, UnixNanos::from(60_000_000_000))]
1000 #[case(BarAggregation::Minute, 60, UnixNanos::from(3_600_000_000_000))]
1001 #[case(BarAggregation::Hour, 1, UnixNanos::from(3_600_000_000_000))]
1002 #[case(BarAggregation::Hour, 4, UnixNanos::from(14_400_000_000_000))]
1003 #[case(BarAggregation::Day, 1, UnixNanos::from(86_400_000_000_000))]
1004 #[case(BarAggregation::Day, 2, UnixNanos::from(172_800_000_000_000))]
1005 #[case(BarAggregation::Week, 1, UnixNanos::from(604_800_000_000_000))]
1006 #[case(BarAggregation::Week, 2, UnixNanos::from(1_209_600_000_000_000))]
1007 #[case(BarAggregation::Month, 1, UnixNanos::from(2_592_000_000_000_000))]
1008 #[case(BarAggregation::Month, 3, UnixNanos::from(7_776_000_000_000_000))]
1009 #[case(BarAggregation::Year, 1, UnixNanos::from(31_536_000_000_000_000))]
1010 #[case(BarAggregation::Year, 2, UnixNanos::from(63_072_000_000_000_000))]
1011 #[should_panic(expected = "Aggregation not time based")]
1012 #[case(BarAggregation::Tick, 1, UnixNanos::from(0))]
1013 fn test_get_bar_interval_ns(
1014 #[case] aggregation: BarAggregation,
1015 #[case] step: usize,
1016 #[case] expected: UnixNanos,
1017 ) {
1018 let bar_type = BarType::Standard {
1019 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1020 spec: BarSpecification::new(step, aggregation, PriceType::Last),
1021 aggregation_source: AggregationSource::Internal,
1022 };
1023
1024 let interval_ns = get_bar_interval_ns(&bar_type);
1025 assert_eq!(interval_ns, expected);
1026 }
1027
1028 #[rstest]
1029 #[case::millisecond(
1030 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), BarAggregation::Millisecond,
1032 1,
1033 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), )]
1035 #[rstest]
1036 #[case::millisecond(
1037 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), BarAggregation::Millisecond,
1039 10,
1040 Utc.timestamp_opt(1658349296, 120_000_000).unwrap(), )]
1042 #[case::second(
1043 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1044 BarAggregation::Millisecond,
1045 1000,
1046 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap()
1047 )]
1048 #[case::second(
1049 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1050 BarAggregation::Second,
1051 1,
1052 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap()
1053 )]
1054 #[case::second(
1055 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1056 BarAggregation::Second,
1057 5,
1058 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 55).unwrap()
1059 )]
1060 #[case::second(
1061 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1062 BarAggregation::Second,
1063 60,
1064 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 0).unwrap()
1065 )]
1066 #[case::minute(
1067 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1068 BarAggregation::Minute,
1069 1,
1070 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 0).unwrap()
1071 )]
1072 #[case::minute(
1073 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1074 BarAggregation::Minute,
1075 5,
1076 Utc.with_ymd_and_hms(2024, 7, 21, 12, 30, 0).unwrap()
1077 )]
1078 #[case::minute(
1079 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1080 BarAggregation::Minute,
1081 60,
1082 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1083 )]
1084 #[case::hour(
1085 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1086 BarAggregation::Hour,
1087 1,
1088 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1089 )]
1090 #[case::hour(
1091 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1092 BarAggregation::Hour,
1093 2,
1094 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1095 )]
1096 #[case::day(
1097 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1098 BarAggregation::Day,
1099 1,
1100 Utc.with_ymd_and_hms(2024, 7, 21, 0, 0, 0).unwrap()
1101 )]
1102 fn test_get_time_bar_start(
1103 #[case] now: DateTime<Utc>,
1104 #[case] aggregation: BarAggregation,
1105 #[case] step: usize,
1106 #[case] expected: DateTime<Utc>,
1107 ) {
1108 let bar_type = BarType::Standard {
1109 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1110 spec: BarSpecification::new(step, aggregation, PriceType::Last),
1111 aggregation_source: AggregationSource::Internal,
1112 };
1113
1114 let start_time = get_time_bar_start(now, &bar_type, None);
1115 assert_eq!(start_time, expected);
1116 }
1117
1118 #[rstest]
1119 fn test_bar_spec_string_reprs() {
1120 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1121 assert_eq!(bar_spec.to_string(), "1-MINUTE-BID");
1122 assert_eq!(format!("{bar_spec}"), "1-MINUTE-BID");
1123 }
1124
1125 #[rstest]
1126 fn test_bar_type_parse_valid() {
1127 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1128 let bar_type = BarType::from(input);
1129
1130 assert_eq!(
1131 bar_type.instrument_id(),
1132 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1133 );
1134 assert_eq!(
1135 bar_type.spec(),
1136 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1137 );
1138 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1139 assert_eq!(bar_type, BarType::from(input));
1140 }
1141
1142 #[rstest]
1143 fn test_bar_type_from_str_with_utf8_symbol() {
1144 let non_ascii_instrument = "TËST-PÉRP.BINANCE";
1145 let non_ascii_bar_type = "TËST-PÉRP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1146
1147 let bar_type = BarType::from_str(non_ascii_bar_type).unwrap();
1148
1149 assert_eq!(
1150 bar_type.instrument_id(),
1151 InstrumentId::from_str(non_ascii_instrument).unwrap()
1152 );
1153 assert_eq!(
1154 bar_type.spec(),
1155 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1156 );
1157 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1158 assert_eq!(bar_type.to_string(), non_ascii_bar_type);
1159 }
1160
1161 #[rstest]
1162 fn test_bar_type_composite_parse_valid() {
1163 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL";
1164 let bar_type = BarType::from(input);
1165 let standard = bar_type.standard();
1166
1167 assert_eq!(
1168 bar_type.instrument_id(),
1169 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1170 );
1171 assert_eq!(
1172 bar_type.spec(),
1173 BarSpecification::new(2, BarAggregation::Minute, PriceType::Last,)
1174 );
1175 assert_eq!(bar_type.aggregation_source(), AggregationSource::Internal);
1176 assert_eq!(bar_type, BarType::from(input));
1177 assert!(bar_type.is_composite());
1178
1179 assert_eq!(
1180 standard.instrument_id(),
1181 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1182 );
1183 assert_eq!(
1184 standard.spec(),
1185 BarSpecification::new(2, BarAggregation::Minute, PriceType::Last,)
1186 );
1187 assert_eq!(standard.aggregation_source(), AggregationSource::Internal);
1188 assert!(standard.is_standard());
1189
1190 let composite = bar_type.composite();
1191 let composite_input = "BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1192
1193 assert_eq!(
1194 composite.instrument_id(),
1195 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1196 );
1197 assert_eq!(
1198 composite.spec(),
1199 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last,)
1200 );
1201 assert_eq!(composite.aggregation_source(), AggregationSource::External);
1202 assert_eq!(composite, BarType::from(composite_input));
1203 assert!(composite.is_standard());
1204 }
1205
1206 #[rstest]
1207 fn test_bar_type_parse_invalid_token_pos_0() {
1208 let input = "BTCUSDT-PERP-1-MINUTE-LAST-INTERNAL";
1209 let result = BarType::from_str(input);
1210
1211 assert_eq!(
1212 result.unwrap_err().to_string(),
1213 format!(
1214 "Error parsing `BarType` from '{input}', invalid token: 'BTCUSDT-PERP' at position 0"
1215 )
1216 );
1217 }
1218
1219 #[rstest]
1220 fn test_bar_type_parse_invalid_token_pos_1() {
1221 let input = "BTCUSDT-PERP.BINANCE-INVALID-MINUTE-LAST-INTERNAL";
1222 let result = BarType::from_str(input);
1223
1224 assert_eq!(
1225 result.unwrap_err().to_string(),
1226 format!(
1227 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 1"
1228 )
1229 );
1230 }
1231
1232 #[rstest]
1233 fn test_bar_type_parse_invalid_token_pos_2() {
1234 let input = "BTCUSDT-PERP.BINANCE-1-INVALID-LAST-INTERNAL";
1235 let result = BarType::from_str(input);
1236
1237 assert_eq!(
1238 result.unwrap_err().to_string(),
1239 format!(
1240 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 2"
1241 )
1242 );
1243 }
1244
1245 #[rstest]
1246 fn test_bar_type_parse_invalid_token_pos_3() {
1247 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-INVALID-INTERNAL";
1248 let result = BarType::from_str(input);
1249
1250 assert_eq!(
1251 result.unwrap_err().to_string(),
1252 format!(
1253 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 3"
1254 )
1255 );
1256 }
1257
1258 #[rstest]
1259 fn test_bar_type_parse_invalid_token_pos_4() {
1260 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-BID-INVALID";
1261 let result = BarType::from_str(input);
1262
1263 assert!(result.is_err());
1264 assert_eq!(
1265 result.unwrap_err().to_string(),
1266 format!(
1267 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 4"
1268 )
1269 );
1270 }
1271
1272 #[rstest]
1273 fn test_bar_type_parse_invalid_token_pos_5() {
1274 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@INVALID-MINUTE-EXTERNAL";
1275 let result = BarType::from_str(input);
1276
1277 assert!(result.is_err());
1278 assert_eq!(
1279 result.unwrap_err().to_string(),
1280 format!(
1281 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 5"
1282 )
1283 );
1284 }
1285
1286 #[rstest]
1287 fn test_bar_type_parse_invalid_token_pos_6() {
1288 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-INVALID-EXTERNAL";
1289 let result = BarType::from_str(input);
1290
1291 assert!(result.is_err());
1292 assert_eq!(
1293 result.unwrap_err().to_string(),
1294 format!(
1295 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 6"
1296 )
1297 );
1298 }
1299
1300 #[rstest]
1301 fn test_bar_type_parse_invalid_token_pos_7() {
1302 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-MINUTE-INVALID";
1303 let result = BarType::from_str(input);
1304
1305 assert!(result.is_err());
1306 assert_eq!(
1307 result.unwrap_err().to_string(),
1308 format!(
1309 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 7"
1310 )
1311 );
1312 }
1313
1314 #[rstest]
1315 fn test_bar_type_equality() {
1316 let instrument_id1 = InstrumentId {
1317 symbol: Symbol::new("AUD/USD"),
1318 venue: Venue::new("SIM"),
1319 };
1320 let instrument_id2 = InstrumentId {
1321 symbol: Symbol::new("GBP/USD"),
1322 venue: Venue::new("SIM"),
1323 };
1324 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1325 let bar_type1 = BarType::Standard {
1326 instrument_id: instrument_id1,
1327 spec: bar_spec,
1328 aggregation_source: AggregationSource::External,
1329 };
1330 let bar_type2 = BarType::Standard {
1331 instrument_id: instrument_id1,
1332 spec: bar_spec,
1333 aggregation_source: AggregationSource::External,
1334 };
1335 let bar_type3 = BarType::Standard {
1336 instrument_id: instrument_id2,
1337 spec: bar_spec,
1338 aggregation_source: AggregationSource::External,
1339 };
1340 assert_eq!(bar_type1, bar_type1);
1341 assert_eq!(bar_type1, bar_type2);
1342 assert_ne!(bar_type1, bar_type3);
1343 }
1344
1345 #[rstest]
1346 fn test_bar_type_id_spec_key_ignores_aggregation_source() {
1347 let bar_type_external = BarType::from_str("ESM4.XCME-1-MINUTE-LAST-EXTERNAL").unwrap();
1348 let bar_type_internal = BarType::from_str("ESM4.XCME-1-MINUTE-LAST-INTERNAL").unwrap();
1349
1350 assert_ne!(bar_type_external, bar_type_internal);
1352
1353 assert_eq!(
1355 bar_type_external.id_spec_key(),
1356 bar_type_internal.id_spec_key()
1357 );
1358
1359 let (instrument_id, spec) = bar_type_external.id_spec_key();
1361 assert_eq!(instrument_id, bar_type_external.instrument_id());
1362 assert_eq!(spec, bar_type_external.spec());
1363 }
1364
1365 #[rstest]
1366 fn test_bar_type_comparison() {
1367 let instrument_id1 = InstrumentId {
1368 symbol: Symbol::new("AUD/USD"),
1369 venue: Venue::new("SIM"),
1370 };
1371
1372 let instrument_id2 = InstrumentId {
1373 symbol: Symbol::new("GBP/USD"),
1374 venue: Venue::new("SIM"),
1375 };
1376 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1377 let bar_spec2 = BarSpecification::new(2, BarAggregation::Minute, PriceType::Bid);
1378 let bar_type1 = BarType::Standard {
1379 instrument_id: instrument_id1,
1380 spec: bar_spec,
1381 aggregation_source: AggregationSource::External,
1382 };
1383 let bar_type2 = BarType::Standard {
1384 instrument_id: instrument_id1,
1385 spec: bar_spec,
1386 aggregation_source: AggregationSource::External,
1387 };
1388 let bar_type3 = BarType::Standard {
1389 instrument_id: instrument_id2,
1390 spec: bar_spec,
1391 aggregation_source: AggregationSource::External,
1392 };
1393 let bar_type4 = BarType::Composite {
1394 instrument_id: instrument_id2,
1395 spec: bar_spec2,
1396 aggregation_source: AggregationSource::Internal,
1397
1398 composite_step: 1,
1399 composite_aggregation: BarAggregation::Minute,
1400 composite_aggregation_source: AggregationSource::External,
1401 };
1402
1403 assert!(bar_type1 <= bar_type2);
1404 assert!(bar_type1 < bar_type3);
1405 assert!(bar_type3 > bar_type1);
1406 assert!(bar_type3 >= bar_type1);
1407 assert!(bar_type4 >= bar_type1);
1408 }
1409
1410 #[rstest]
1411 fn test_bar_new() {
1412 let bar_type = BarType::from("AAPL.XNAS-1-MINUTE-LAST-INTERNAL");
1413 let open = Price::from("100.0");
1414 let high = Price::from("105.0");
1415 let low = Price::from("95.0");
1416 let close = Price::from("102.0");
1417 let volume = Quantity::from("1000");
1418 let ts_event = UnixNanos::from(1_000_000);
1419 let ts_init = UnixNanos::from(2_000_000);
1420
1421 let bar = Bar::new(bar_type, open, high, low, close, volume, ts_event, ts_init);
1422
1423 assert_eq!(bar.bar_type, bar_type);
1424 assert_eq!(bar.open, open);
1425 assert_eq!(bar.high, high);
1426 assert_eq!(bar.low, low);
1427 assert_eq!(bar.close, close);
1428 assert_eq!(bar.volume, volume);
1429 assert_eq!(bar.ts_event, ts_event);
1430 assert_eq!(bar.ts_init, ts_init);
1431 }
1432
1433 #[rstest]
1434 #[case("100.0", "90.0", "95.0", "92.0")] #[case("100.0", "105.0", "110.0", "102.0")] #[case("100.0", "105.0", "95.0", "110.0")] #[case("100.0", "105.0", "95.0", "90.0")] #[case("100.0", "110.0", "105.0", "108.0")] #[case("100.0", "90.0", "110.0", "120.0")] fn test_bar_new_checked_conditions(
1441 #[case] open: &str,
1442 #[case] high: &str,
1443 #[case] low: &str,
1444 #[case] close: &str,
1445 ) {
1446 let bar_type = BarType::from("AAPL.XNAS-1-MINUTE-LAST-INTERNAL");
1447 let open = Price::from(open);
1448 let high = Price::from(high);
1449 let low = Price::from(low);
1450 let close = Price::from(close);
1451 let volume = Quantity::from("1000");
1452 let ts_event = UnixNanos::from(1_000_000);
1453 let ts_init = UnixNanos::from(2_000_000);
1454
1455 let result = Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init);
1456
1457 assert!(result.is_err());
1458 }
1459
1460 #[rstest]
1461 fn test_bar_equality() {
1462 let instrument_id = InstrumentId {
1463 symbol: Symbol::new("AUDUSD"),
1464 venue: Venue::new("SIM"),
1465 };
1466 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1467 let bar_type = BarType::Standard {
1468 instrument_id,
1469 spec: bar_spec,
1470 aggregation_source: AggregationSource::External,
1471 };
1472 let bar1 = Bar {
1473 bar_type,
1474 open: Price::from("1.00001"),
1475 high: Price::from("1.00004"),
1476 low: Price::from("1.00002"),
1477 close: Price::from("1.00003"),
1478 volume: Quantity::from("100000"),
1479 ts_event: UnixNanos::default(),
1480 ts_init: UnixNanos::from(1),
1481 };
1482
1483 let bar2 = Bar {
1484 bar_type,
1485 open: Price::from("1.00000"),
1486 high: Price::from("1.00004"),
1487 low: Price::from("1.00002"),
1488 close: Price::from("1.00003"),
1489 volume: Quantity::from("100000"),
1490 ts_event: UnixNanos::default(),
1491 ts_init: UnixNanos::from(1),
1492 };
1493 assert_eq!(bar1, bar1);
1494 assert_ne!(bar1, bar2);
1495 }
1496
1497 #[rstest]
1498 fn test_json_serialization() {
1499 let bar = Bar::default();
1500 let serialized = bar.to_json_bytes().unwrap();
1501 let deserialized = Bar::from_json_bytes(serialized.as_ref()).unwrap();
1502 assert_eq!(deserialized, bar);
1503 }
1504
1505 #[rstest]
1506 fn test_msgpack_serialization() {
1507 let bar = Bar::default();
1508 let serialized = bar.to_msgpack_bytes().unwrap();
1509 let deserialized = Bar::from_msgpack_bytes(serialized.as_ref()).unwrap();
1510 assert_eq!(deserialized, bar);
1511 }
1512}