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