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")
317)]
318pub struct BarSpecification {
319 pub step: NonZeroUsize,
321 pub aggregation: BarAggregation,
323 pub price_type: PriceType,
325}
326
327impl BarSpecification {
328 pub fn new_checked(
338 step: usize,
339 aggregation: BarAggregation,
340 price_type: PriceType,
341 ) -> anyhow::Result<Self> {
342 let step = NonZeroUsize::new(step)
343 .ok_or(anyhow::anyhow!("Invalid step: {step} (must be non-zero)"))?;
344 Ok(Self {
345 step,
346 aggregation,
347 price_type,
348 })
349 }
350
351 #[must_use]
357 pub fn new(step: usize, aggregation: BarAggregation, price_type: PriceType) -> Self {
358 Self::new_checked(step, aggregation, price_type).expect(FAILED)
359 }
360
361 pub fn timedelta(&self) -> TimeDelta {
373 match self.aggregation {
374 BarAggregation::Millisecond => Duration::milliseconds(self.step.get() as i64),
375 BarAggregation::Second => Duration::seconds(self.step.get() as i64),
376 BarAggregation::Minute => Duration::minutes(self.step.get() as i64),
377 BarAggregation::Hour => Duration::hours(self.step.get() as i64),
378 BarAggregation::Day => Duration::days(self.step.get() as i64),
379 BarAggregation::Week => Duration::days(self.step.get() as i64 * 7),
380 BarAggregation::Month => Duration::days(self.step.get() as i64 * 30), BarAggregation::Year => Duration::days(self.step.get() as i64 * 365), _ => panic!(
383 "Timedelta not supported for aggregation type: {:?}",
384 self.aggregation
385 ),
386 }
387 }
388
389 pub fn is_time_aggregated(&self) -> bool {
399 matches!(
400 self.aggregation,
401 BarAggregation::Millisecond
402 | BarAggregation::Second
403 | BarAggregation::Minute
404 | BarAggregation::Hour
405 | BarAggregation::Day
406 | BarAggregation::Week
407 | BarAggregation::Month
408 | BarAggregation::Year
409 )
410 }
411
412 pub fn is_threshold_aggregated(&self) -> bool {
420 matches!(
421 self.aggregation,
422 BarAggregation::Tick
423 | BarAggregation::TickImbalance
424 | BarAggregation::Volume
425 | BarAggregation::VolumeImbalance
426 | BarAggregation::Value
427 | BarAggregation::ValueImbalance
428 )
429 }
430
431 pub fn is_information_aggregated(&self) -> bool {
436 matches!(
437 self.aggregation,
438 BarAggregation::TickRuns | BarAggregation::VolumeRuns | BarAggregation::ValueRuns
439 )
440 }
441}
442
443impl Display for BarSpecification {
444 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445 write!(f, "{}-{}-{}", self.step, self.aggregation, self.price_type)
446 }
447}
448
449#[repr(C)]
452#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
453#[cfg_attr(
454 feature = "python",
455 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
456)]
457pub enum BarType {
458 Standard {
459 instrument_id: InstrumentId,
461 spec: BarSpecification,
463 aggregation_source: AggregationSource,
465 },
466 Composite {
467 instrument_id: InstrumentId,
469 spec: BarSpecification,
471 aggregation_source: AggregationSource,
473
474 composite_step: usize,
476 composite_aggregation: BarAggregation,
478 composite_aggregation_source: AggregationSource,
480 },
481}
482
483impl BarType {
484 #[must_use]
486 pub fn new(
487 instrument_id: InstrumentId,
488 spec: BarSpecification,
489 aggregation_source: AggregationSource,
490 ) -> Self {
491 Self::Standard {
492 instrument_id,
493 spec,
494 aggregation_source,
495 }
496 }
497
498 pub fn new_composite(
500 instrument_id: InstrumentId,
501 spec: BarSpecification,
502 aggregation_source: AggregationSource,
503
504 composite_step: usize,
505 composite_aggregation: BarAggregation,
506 composite_aggregation_source: AggregationSource,
507 ) -> Self {
508 Self::Composite {
509 instrument_id,
510 spec,
511 aggregation_source,
512
513 composite_step,
514 composite_aggregation,
515 composite_aggregation_source,
516 }
517 }
518
519 pub fn is_standard(&self) -> bool {
521 match &self {
522 Self::Standard { .. } => true,
523 Self::Composite { .. } => false,
524 }
525 }
526
527 pub fn is_composite(&self) -> bool {
529 match &self {
530 Self::Standard { .. } => false,
531 Self::Composite { .. } => true,
532 }
533 }
534
535 #[must_use]
537 pub fn standard(&self) -> Self {
538 match self {
539 &b @ Self::Standard { .. } => b,
540 Self::Composite {
541 instrument_id,
542 spec,
543 aggregation_source,
544 ..
545 } => Self::new(*instrument_id, *spec, *aggregation_source),
546 }
547 }
548
549 #[must_use]
551 pub fn composite(&self) -> Self {
552 match self {
553 &b @ Self::Standard { .. } => b, Self::Composite {
555 instrument_id,
556 spec,
557 aggregation_source: _,
558
559 composite_step,
560 composite_aggregation,
561 composite_aggregation_source,
562 } => Self::new(
563 *instrument_id,
564 BarSpecification::new(*composite_step, *composite_aggregation, spec.price_type),
565 *composite_aggregation_source,
566 ),
567 }
568 }
569
570 pub fn instrument_id(&self) -> InstrumentId {
572 match &self {
573 Self::Standard { instrument_id, .. } | Self::Composite { instrument_id, .. } => {
574 *instrument_id
575 }
576 }
577 }
578
579 pub fn spec(&self) -> BarSpecification {
581 match &self {
582 Self::Standard { spec, .. } | Self::Composite { spec, .. } => *spec,
583 }
584 }
585
586 pub fn aggregation_source(&self) -> AggregationSource {
588 match &self {
589 Self::Standard {
590 aggregation_source, ..
591 }
592 | Self::Composite {
593 aggregation_source, ..
594 } => *aggregation_source,
595 }
596 }
597
598 #[must_use]
604 pub fn id_spec_key(&self) -> (InstrumentId, BarSpecification) {
605 (self.instrument_id(), self.spec())
606 }
607}
608
609#[derive(thiserror::Error, Debug)]
610#[error("Error parsing `BarType` from '{input}', invalid token: '{token}' at position {position}")]
611pub struct BarTypeParseError {
612 input: String,
613 token: String,
614 position: usize,
615}
616
617impl FromStr for BarType {
618 type Err = BarTypeParseError;
619
620 #[allow(clippy::needless_collect)] fn from_str(s: &str) -> Result<Self, Self::Err> {
622 let parts: Vec<&str> = s.split('@').collect();
623 let standard = parts[0];
624 let composite_str = parts.get(1);
625
626 let pieces: Vec<&str> = standard.rsplitn(5, '-').collect();
627 let rev_pieces: Vec<&str> = pieces.into_iter().rev().collect();
628 if rev_pieces.len() != 5 {
629 return Err(BarTypeParseError {
630 input: s.to_string(),
631 token: String::new(),
632 position: 0,
633 });
634 }
635
636 let instrument_id =
637 InstrumentId::from_str(rev_pieces[0]).map_err(|_| BarTypeParseError {
638 input: s.to_string(),
639 token: rev_pieces[0].to_string(),
640 position: 0,
641 })?;
642
643 let step = rev_pieces[1].parse().map_err(|_| BarTypeParseError {
644 input: s.to_string(),
645 token: rev_pieces[1].to_string(),
646 position: 1,
647 })?;
648 let aggregation =
649 BarAggregation::from_str(rev_pieces[2]).map_err(|_| BarTypeParseError {
650 input: s.to_string(),
651 token: rev_pieces[2].to_string(),
652 position: 2,
653 })?;
654 let price_type = PriceType::from_str(rev_pieces[3]).map_err(|_| BarTypeParseError {
655 input: s.to_string(),
656 token: rev_pieces[3].to_string(),
657 position: 3,
658 })?;
659 let aggregation_source =
660 AggregationSource::from_str(rev_pieces[4]).map_err(|_| BarTypeParseError {
661 input: s.to_string(),
662 token: rev_pieces[4].to_string(),
663 position: 4,
664 })?;
665
666 if let Some(composite_str) = composite_str {
667 let composite_pieces: Vec<&str> = composite_str.rsplitn(3, '-').collect();
668 let rev_composite_pieces: Vec<&str> = composite_pieces.into_iter().rev().collect();
669 if rev_composite_pieces.len() != 3 {
670 return Err(BarTypeParseError {
671 input: s.to_string(),
672 token: String::new(),
673 position: 5,
674 });
675 }
676
677 let composite_step =
678 rev_composite_pieces[0]
679 .parse()
680 .map_err(|_| BarTypeParseError {
681 input: s.to_string(),
682 token: rev_composite_pieces[0].to_string(),
683 position: 5,
684 })?;
685 let composite_aggregation =
686 BarAggregation::from_str(rev_composite_pieces[1]).map_err(|_| {
687 BarTypeParseError {
688 input: s.to_string(),
689 token: rev_composite_pieces[1].to_string(),
690 position: 6,
691 }
692 })?;
693 let composite_aggregation_source = AggregationSource::from_str(rev_composite_pieces[2])
694 .map_err(|_| BarTypeParseError {
695 input: s.to_string(),
696 token: rev_composite_pieces[2].to_string(),
697 position: 7,
698 })?;
699
700 Ok(Self::new_composite(
701 instrument_id,
702 BarSpecification::new(step, aggregation, price_type),
703 aggregation_source,
704 composite_step,
705 composite_aggregation,
706 composite_aggregation_source,
707 ))
708 } else {
709 Ok(Self::Standard {
710 instrument_id,
711 spec: BarSpecification::new(step, aggregation, price_type),
712 aggregation_source,
713 })
714 }
715 }
716}
717
718impl<T: AsRef<str>> From<T> for BarType {
719 fn from(value: T) -> Self {
720 Self::from_str(value.as_ref()).expect(FAILED)
721 }
722}
723
724impl Display for BarType {
725 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726 match &self {
727 Self::Standard {
728 instrument_id,
729 spec,
730 aggregation_source,
731 } => {
732 write!(f, "{instrument_id}-{spec}-{aggregation_source}")
733 }
734 Self::Composite {
735 instrument_id,
736 spec,
737 aggregation_source,
738
739 composite_step,
740 composite_aggregation,
741 composite_aggregation_source,
742 } => {
743 write!(
744 f,
745 "{}-{}-{}@{}-{}-{}",
746 instrument_id,
747 spec,
748 aggregation_source,
749 *composite_step,
750 *composite_aggregation,
751 *composite_aggregation_source
752 )
753 }
754 }
755 }
756}
757
758impl Serialize for BarType {
759 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
760 where
761 S: Serializer,
762 {
763 serializer.serialize_str(&self.to_string())
764 }
765}
766
767impl<'de> Deserialize<'de> for BarType {
768 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
769 where
770 D: Deserializer<'de>,
771 {
772 let s: String = Deserialize::deserialize(deserializer)?;
773 Self::from_str(&s).map_err(serde::de::Error::custom)
774 }
775}
776
777#[repr(C)]
779#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)]
780#[serde(tag = "type")]
781#[cfg_attr(
782 feature = "python",
783 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
784)]
785pub struct Bar {
786 pub bar_type: BarType,
788 pub open: Price,
790 pub high: Price,
792 pub low: Price,
794 pub close: Price,
796 pub volume: Quantity,
798 pub ts_event: UnixNanos,
800 pub ts_init: UnixNanos,
802}
803
804impl Bar {
805 #[allow(clippy::too_many_arguments)]
818 pub fn new_checked(
819 bar_type: BarType,
820 open: Price,
821 high: Price,
822 low: Price,
823 close: Price,
824 volume: Quantity,
825 ts_event: UnixNanos,
826 ts_init: UnixNanos,
827 ) -> anyhow::Result<Self> {
828 check_predicate_true(high >= open, "high >= open")?;
829 check_predicate_true(high >= low, "high >= low")?;
830 check_predicate_true(high >= close, "high >= close")?;
831 check_predicate_true(low <= close, "low <= close")?;
832 check_predicate_true(low <= open, "low <= open")?;
833
834 Ok(Self {
835 bar_type,
836 open,
837 high,
838 low,
839 close,
840 volume,
841 ts_event,
842 ts_init,
843 })
844 }
845
846 #[allow(clippy::too_many_arguments)]
855 pub fn new(
856 bar_type: BarType,
857 open: Price,
858 high: Price,
859 low: Price,
860 close: Price,
861 volume: Quantity,
862 ts_event: UnixNanos,
863 ts_init: UnixNanos,
864 ) -> Self {
865 Self::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init)
866 .expect(FAILED)
867 }
868
869 pub fn instrument_id(&self) -> InstrumentId {
870 self.bar_type.instrument_id()
871 }
872
873 #[must_use]
875 pub fn get_metadata(
876 bar_type: &BarType,
877 price_precision: u8,
878 size_precision: u8,
879 ) -> HashMap<String, String> {
880 let mut metadata = HashMap::new();
881 let instrument_id = bar_type.instrument_id();
882 metadata.insert("bar_type".to_string(), bar_type.to_string());
883 metadata.insert("instrument_id".to_string(), instrument_id.to_string());
884 metadata.insert("price_precision".to_string(), price_precision.to_string());
885 metadata.insert("size_precision".to_string(), size_precision.to_string());
886 metadata
887 }
888
889 #[must_use]
891 pub fn get_fields() -> IndexMap<String, String> {
892 let mut metadata = IndexMap::new();
893 metadata.insert("open".to_string(), FIXED_SIZE_BINARY.to_string());
894 metadata.insert("high".to_string(), FIXED_SIZE_BINARY.to_string());
895 metadata.insert("low".to_string(), FIXED_SIZE_BINARY.to_string());
896 metadata.insert("close".to_string(), FIXED_SIZE_BINARY.to_string());
897 metadata.insert("volume".to_string(), FIXED_SIZE_BINARY.to_string());
898 metadata.insert("ts_event".to_string(), "UInt64".to_string());
899 metadata.insert("ts_init".to_string(), "UInt64".to_string());
900 metadata
901 }
902}
903
904impl Display for Bar {
905 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
906 write!(
907 f,
908 "{},{},{},{},{},{},{}",
909 self.bar_type, self.open, self.high, self.low, self.close, self.volume, self.ts_event
910 )
911 }
912}
913
914impl Serializable for Bar {}
915
916impl HasTsInit for Bar {
917 fn ts_init(&self) -> UnixNanos {
918 self.ts_init
919 }
920}
921
922#[cfg(test)]
923mod tests {
924 use std::str::FromStr;
925
926 use chrono::TimeZone;
927 use nautilus_core::serialization::msgpack::{FromMsgPack, ToMsgPack};
928 use rstest::rstest;
929
930 use super::*;
931 use crate::identifiers::{Symbol, Venue};
932
933 #[rstest]
934 fn test_bar_specification_new_invalid() {
935 let result = BarSpecification::new_checked(0, BarAggregation::Tick, PriceType::Last);
936 assert!(result.is_err());
937 }
938
939 #[rstest]
940 #[should_panic(expected = "Invalid step: 0 (must be non-zero)")]
941 fn test_bar_specification_new_checked_with_invalid_step_panics() {
942 let aggregation = BarAggregation::Tick;
943 let price_type = PriceType::Last;
944
945 let _ = BarSpecification::new(0, aggregation, price_type);
946 }
947
948 #[rstest]
949 #[case(BarAggregation::Millisecond, 1, TimeDelta::milliseconds(1))]
950 #[case(BarAggregation::Millisecond, 10, TimeDelta::milliseconds(10))]
951 #[case(BarAggregation::Second, 1, TimeDelta::seconds(1))]
952 #[case(BarAggregation::Second, 15, TimeDelta::seconds(15))]
953 #[case(BarAggregation::Minute, 1, TimeDelta::minutes(1))]
954 #[case(BarAggregation::Minute, 60, TimeDelta::minutes(60))]
955 #[case(BarAggregation::Hour, 1, TimeDelta::hours(1))]
956 #[case(BarAggregation::Hour, 4, TimeDelta::hours(4))]
957 #[case(BarAggregation::Day, 1, TimeDelta::days(1))]
958 #[case(BarAggregation::Day, 2, TimeDelta::days(2))]
959 #[case(BarAggregation::Week, 1, TimeDelta::days(7))]
960 #[case(BarAggregation::Week, 2, TimeDelta::days(14))]
961 #[case(BarAggregation::Month, 1, TimeDelta::days(30))]
962 #[case(BarAggregation::Month, 3, TimeDelta::days(90))]
963 #[case(BarAggregation::Year, 1, TimeDelta::days(365))]
964 #[case(BarAggregation::Year, 2, TimeDelta::days(730))]
965 #[should_panic(expected = "Aggregation not time based")]
966 #[case(BarAggregation::Tick, 1, TimeDelta::zero())]
967 fn test_get_bar_interval(
968 #[case] aggregation: BarAggregation,
969 #[case] step: usize,
970 #[case] expected: TimeDelta,
971 ) {
972 let bar_type = BarType::Standard {
973 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
974 spec: BarSpecification::new(step, aggregation, PriceType::Last),
975 aggregation_source: AggregationSource::Internal,
976 };
977
978 let interval = get_bar_interval(&bar_type);
979 assert_eq!(interval, expected);
980 }
981
982 #[rstest]
983 #[case(BarAggregation::Millisecond, 1, UnixNanos::from(1_000_000))]
984 #[case(BarAggregation::Millisecond, 10, UnixNanos::from(10_000_000))]
985 #[case(BarAggregation::Second, 1, UnixNanos::from(1_000_000_000))]
986 #[case(BarAggregation::Second, 10, UnixNanos::from(10_000_000_000))]
987 #[case(BarAggregation::Minute, 1, UnixNanos::from(60_000_000_000))]
988 #[case(BarAggregation::Minute, 60, UnixNanos::from(3_600_000_000_000))]
989 #[case(BarAggregation::Hour, 1, UnixNanos::from(3_600_000_000_000))]
990 #[case(BarAggregation::Hour, 4, UnixNanos::from(14_400_000_000_000))]
991 #[case(BarAggregation::Day, 1, UnixNanos::from(86_400_000_000_000))]
992 #[case(BarAggregation::Day, 2, UnixNanos::from(172_800_000_000_000))]
993 #[case(BarAggregation::Week, 1, UnixNanos::from(604_800_000_000_000))]
994 #[case(BarAggregation::Week, 2, UnixNanos::from(1_209_600_000_000_000))]
995 #[case(BarAggregation::Month, 1, UnixNanos::from(2_592_000_000_000_000))]
996 #[case(BarAggregation::Month, 3, UnixNanos::from(7_776_000_000_000_000))]
997 #[case(BarAggregation::Year, 1, UnixNanos::from(31_536_000_000_000_000))]
998 #[case(BarAggregation::Year, 2, UnixNanos::from(63_072_000_000_000_000))]
999 #[should_panic(expected = "Aggregation not time based")]
1000 #[case(BarAggregation::Tick, 1, UnixNanos::from(0))]
1001 fn test_get_bar_interval_ns(
1002 #[case] aggregation: BarAggregation,
1003 #[case] step: usize,
1004 #[case] expected: UnixNanos,
1005 ) {
1006 let bar_type = BarType::Standard {
1007 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1008 spec: BarSpecification::new(step, aggregation, PriceType::Last),
1009 aggregation_source: AggregationSource::Internal,
1010 };
1011
1012 let interval_ns = get_bar_interval_ns(&bar_type);
1013 assert_eq!(interval_ns, expected);
1014 }
1015
1016 #[rstest]
1017 #[case::millisecond(
1018 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), BarAggregation::Millisecond,
1020 1,
1021 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), )]
1023 #[rstest]
1024 #[case::millisecond(
1025 Utc.timestamp_opt(1658349296, 123_000_000).unwrap(), BarAggregation::Millisecond,
1027 10,
1028 Utc.timestamp_opt(1658349296, 120_000_000).unwrap(), )]
1030 #[case::second(
1031 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1032 BarAggregation::Millisecond,
1033 1000,
1034 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap()
1035 )]
1036 #[case::second(
1037 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1038 BarAggregation::Second,
1039 1,
1040 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap()
1041 )]
1042 #[case::second(
1043 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1044 BarAggregation::Second,
1045 5,
1046 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 55).unwrap()
1047 )]
1048 #[case::second(
1049 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1050 BarAggregation::Second,
1051 60,
1052 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 0).unwrap()
1053 )]
1054 #[case::minute(
1055 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1056 BarAggregation::Minute,
1057 1,
1058 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 0).unwrap()
1059 )]
1060 #[case::minute(
1061 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1062 BarAggregation::Minute,
1063 5,
1064 Utc.with_ymd_and_hms(2024, 7, 21, 12, 30, 0).unwrap()
1065 )]
1066 #[case::minute(
1067 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1068 BarAggregation::Minute,
1069 60,
1070 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1071 )]
1072 #[case::hour(
1073 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1074 BarAggregation::Hour,
1075 1,
1076 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1077 )]
1078 #[case::hour(
1079 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1080 BarAggregation::Hour,
1081 2,
1082 Utc.with_ymd_and_hms(2024, 7, 21, 12, 0, 0).unwrap()
1083 )]
1084 #[case::day(
1085 Utc.with_ymd_and_hms(2024, 7, 21, 12, 34, 56).unwrap(),
1086 BarAggregation::Day,
1087 1,
1088 Utc.with_ymd_and_hms(2024, 7, 21, 0, 0, 0).unwrap()
1089 )]
1090 fn test_get_time_bar_start(
1091 #[case] now: DateTime<Utc>,
1092 #[case] aggregation: BarAggregation,
1093 #[case] step: usize,
1094 #[case] expected: DateTime<Utc>,
1095 ) {
1096 let bar_type = BarType::Standard {
1097 instrument_id: InstrumentId::from("BTCUSDT-PERP.BINANCE"),
1098 spec: BarSpecification::new(step, aggregation, PriceType::Last),
1099 aggregation_source: AggregationSource::Internal,
1100 };
1101
1102 let start_time = get_time_bar_start(now, &bar_type, None);
1103 assert_eq!(start_time, expected);
1104 }
1105
1106 #[rstest]
1107 fn test_bar_spec_string_reprs() {
1108 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1109 assert_eq!(bar_spec.to_string(), "1-MINUTE-BID");
1110 assert_eq!(format!("{bar_spec}"), "1-MINUTE-BID");
1111 }
1112
1113 #[rstest]
1114 fn test_bar_type_parse_valid() {
1115 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1116 let bar_type = BarType::from(input);
1117
1118 assert_eq!(
1119 bar_type.instrument_id(),
1120 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1121 );
1122 assert_eq!(
1123 bar_type.spec(),
1124 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1125 );
1126 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1127 assert_eq!(bar_type, BarType::from(input));
1128 }
1129
1130 #[rstest]
1131 fn test_bar_type_from_str_with_utf8_symbol() {
1132 let non_ascii_instrument = "TËST-PÉRP.BINANCE";
1133 let non_ascii_bar_type = "TËST-PÉRP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1134
1135 let bar_type = BarType::from_str(non_ascii_bar_type).unwrap();
1136
1137 assert_eq!(
1138 bar_type.instrument_id(),
1139 InstrumentId::from_str(non_ascii_instrument).unwrap()
1140 );
1141 assert_eq!(
1142 bar_type.spec(),
1143 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last)
1144 );
1145 assert_eq!(bar_type.aggregation_source(), AggregationSource::External);
1146 assert_eq!(bar_type.to_string(), non_ascii_bar_type);
1147 }
1148
1149 #[rstest]
1150 fn test_bar_type_composite_parse_valid() {
1151 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-MINUTE-EXTERNAL";
1152 let bar_type = BarType::from(input);
1153 let standard = bar_type.standard();
1154
1155 assert_eq!(
1156 bar_type.instrument_id(),
1157 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1158 );
1159 assert_eq!(
1160 bar_type.spec(),
1161 BarSpecification::new(2, BarAggregation::Minute, PriceType::Last,)
1162 );
1163 assert_eq!(bar_type.aggregation_source(), AggregationSource::Internal);
1164 assert_eq!(bar_type, BarType::from(input));
1165 assert!(bar_type.is_composite());
1166
1167 assert_eq!(
1168 standard.instrument_id(),
1169 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1170 );
1171 assert_eq!(
1172 standard.spec(),
1173 BarSpecification::new(2, BarAggregation::Minute, PriceType::Last,)
1174 );
1175 assert_eq!(standard.aggregation_source(), AggregationSource::Internal);
1176 assert!(standard.is_standard());
1177
1178 let composite = bar_type.composite();
1179 let composite_input = "BTCUSDT-PERP.BINANCE-1-MINUTE-LAST-EXTERNAL";
1180
1181 assert_eq!(
1182 composite.instrument_id(),
1183 InstrumentId::from("BTCUSDT-PERP.BINANCE")
1184 );
1185 assert_eq!(
1186 composite.spec(),
1187 BarSpecification::new(1, BarAggregation::Minute, PriceType::Last,)
1188 );
1189 assert_eq!(composite.aggregation_source(), AggregationSource::External);
1190 assert_eq!(composite, BarType::from(composite_input));
1191 assert!(composite.is_standard());
1192 }
1193
1194 #[rstest]
1195 fn test_bar_type_parse_invalid_token_pos_0() {
1196 let input = "BTCUSDT-PERP-1-MINUTE-LAST-INTERNAL";
1197 let result = BarType::from_str(input);
1198
1199 assert_eq!(
1200 result.unwrap_err().to_string(),
1201 format!(
1202 "Error parsing `BarType` from '{input}', invalid token: 'BTCUSDT-PERP' at position 0"
1203 )
1204 );
1205 }
1206
1207 #[rstest]
1208 fn test_bar_type_parse_invalid_token_pos_1() {
1209 let input = "BTCUSDT-PERP.BINANCE-INVALID-MINUTE-LAST-INTERNAL";
1210 let result = BarType::from_str(input);
1211
1212 assert_eq!(
1213 result.unwrap_err().to_string(),
1214 format!(
1215 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 1"
1216 )
1217 );
1218 }
1219
1220 #[rstest]
1221 fn test_bar_type_parse_invalid_token_pos_2() {
1222 let input = "BTCUSDT-PERP.BINANCE-1-INVALID-LAST-INTERNAL";
1223 let result = BarType::from_str(input);
1224
1225 assert_eq!(
1226 result.unwrap_err().to_string(),
1227 format!(
1228 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 2"
1229 )
1230 );
1231 }
1232
1233 #[rstest]
1234 fn test_bar_type_parse_invalid_token_pos_3() {
1235 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-INVALID-INTERNAL";
1236 let result = BarType::from_str(input);
1237
1238 assert_eq!(
1239 result.unwrap_err().to_string(),
1240 format!(
1241 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 3"
1242 )
1243 );
1244 }
1245
1246 #[rstest]
1247 fn test_bar_type_parse_invalid_token_pos_4() {
1248 let input = "BTCUSDT-PERP.BINANCE-1-MINUTE-BID-INVALID";
1249 let result = BarType::from_str(input);
1250
1251 assert!(result.is_err());
1252 assert_eq!(
1253 result.unwrap_err().to_string(),
1254 format!(
1255 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 4"
1256 )
1257 );
1258 }
1259
1260 #[rstest]
1261 fn test_bar_type_parse_invalid_token_pos_5() {
1262 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@INVALID-MINUTE-EXTERNAL";
1263 let result = BarType::from_str(input);
1264
1265 assert!(result.is_err());
1266 assert_eq!(
1267 result.unwrap_err().to_string(),
1268 format!(
1269 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 5"
1270 )
1271 );
1272 }
1273
1274 #[rstest]
1275 fn test_bar_type_parse_invalid_token_pos_6() {
1276 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-INVALID-EXTERNAL";
1277 let result = BarType::from_str(input);
1278
1279 assert!(result.is_err());
1280 assert_eq!(
1281 result.unwrap_err().to_string(),
1282 format!(
1283 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 6"
1284 )
1285 );
1286 }
1287
1288 #[rstest]
1289 fn test_bar_type_parse_invalid_token_pos_7() {
1290 let input = "BTCUSDT-PERP.BINANCE-2-MINUTE-LAST-INTERNAL@1-MINUTE-INVALID";
1291 let result = BarType::from_str(input);
1292
1293 assert!(result.is_err());
1294 assert_eq!(
1295 result.unwrap_err().to_string(),
1296 format!(
1297 "Error parsing `BarType` from '{input}', invalid token: 'INVALID' at position 7"
1298 )
1299 );
1300 }
1301
1302 #[rstest]
1303 fn test_bar_type_equality() {
1304 let instrument_id1 = InstrumentId {
1305 symbol: Symbol::new("AUD/USD"),
1306 venue: Venue::new("SIM"),
1307 };
1308 let instrument_id2 = InstrumentId {
1309 symbol: Symbol::new("GBP/USD"),
1310 venue: Venue::new("SIM"),
1311 };
1312 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1313 let bar_type1 = BarType::Standard {
1314 instrument_id: instrument_id1,
1315 spec: bar_spec,
1316 aggregation_source: AggregationSource::External,
1317 };
1318 let bar_type2 = BarType::Standard {
1319 instrument_id: instrument_id1,
1320 spec: bar_spec,
1321 aggregation_source: AggregationSource::External,
1322 };
1323 let bar_type3 = BarType::Standard {
1324 instrument_id: instrument_id2,
1325 spec: bar_spec,
1326 aggregation_source: AggregationSource::External,
1327 };
1328 assert_eq!(bar_type1, bar_type1);
1329 assert_eq!(bar_type1, bar_type2);
1330 assert_ne!(bar_type1, bar_type3);
1331 }
1332
1333 #[rstest]
1334 fn test_bar_type_id_spec_key_ignores_aggregation_source() {
1335 let bar_type_external = BarType::from_str("ESM4.XCME-1-MINUTE-LAST-EXTERNAL").unwrap();
1336 let bar_type_internal = BarType::from_str("ESM4.XCME-1-MINUTE-LAST-INTERNAL").unwrap();
1337
1338 assert_ne!(bar_type_external, bar_type_internal);
1340
1341 assert_eq!(
1343 bar_type_external.id_spec_key(),
1344 bar_type_internal.id_spec_key()
1345 );
1346
1347 let (instrument_id, spec) = bar_type_external.id_spec_key();
1349 assert_eq!(instrument_id, bar_type_external.instrument_id());
1350 assert_eq!(spec, bar_type_external.spec());
1351 }
1352
1353 #[rstest]
1354 fn test_bar_type_comparison() {
1355 let instrument_id1 = InstrumentId {
1356 symbol: Symbol::new("AUD/USD"),
1357 venue: Venue::new("SIM"),
1358 };
1359
1360 let instrument_id2 = InstrumentId {
1361 symbol: Symbol::new("GBP/USD"),
1362 venue: Venue::new("SIM"),
1363 };
1364 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1365 let bar_spec2 = BarSpecification::new(2, BarAggregation::Minute, PriceType::Bid);
1366 let bar_type1 = BarType::Standard {
1367 instrument_id: instrument_id1,
1368 spec: bar_spec,
1369 aggregation_source: AggregationSource::External,
1370 };
1371 let bar_type2 = BarType::Standard {
1372 instrument_id: instrument_id1,
1373 spec: bar_spec,
1374 aggregation_source: AggregationSource::External,
1375 };
1376 let bar_type3 = BarType::Standard {
1377 instrument_id: instrument_id2,
1378 spec: bar_spec,
1379 aggregation_source: AggregationSource::External,
1380 };
1381 let bar_type4 = BarType::Composite {
1382 instrument_id: instrument_id2,
1383 spec: bar_spec2,
1384 aggregation_source: AggregationSource::Internal,
1385
1386 composite_step: 1,
1387 composite_aggregation: BarAggregation::Minute,
1388 composite_aggregation_source: AggregationSource::External,
1389 };
1390
1391 assert!(bar_type1 <= bar_type2);
1392 assert!(bar_type1 < bar_type3);
1393 assert!(bar_type3 > bar_type1);
1394 assert!(bar_type3 >= bar_type1);
1395 assert!(bar_type4 >= bar_type1);
1396 }
1397
1398 #[rstest]
1399 fn test_bar_new() {
1400 let bar_type = BarType::from("AAPL.XNAS-1-MINUTE-LAST-INTERNAL");
1401 let open = Price::from("100.0");
1402 let high = Price::from("105.0");
1403 let low = Price::from("95.0");
1404 let close = Price::from("102.0");
1405 let volume = Quantity::from("1000");
1406 let ts_event = UnixNanos::from(1_000_000);
1407 let ts_init = UnixNanos::from(2_000_000);
1408
1409 let bar = Bar::new(bar_type, open, high, low, close, volume, ts_event, ts_init);
1410
1411 assert_eq!(bar.bar_type, bar_type);
1412 assert_eq!(bar.open, open);
1413 assert_eq!(bar.high, high);
1414 assert_eq!(bar.low, low);
1415 assert_eq!(bar.close, close);
1416 assert_eq!(bar.volume, volume);
1417 assert_eq!(bar.ts_event, ts_event);
1418 assert_eq!(bar.ts_init, ts_init);
1419 }
1420
1421 #[rstest]
1422 #[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(
1429 #[case] open: &str,
1430 #[case] high: &str,
1431 #[case] low: &str,
1432 #[case] close: &str,
1433 ) {
1434 let bar_type = BarType::from("AAPL.XNAS-1-MINUTE-LAST-INTERNAL");
1435 let open = Price::from(open);
1436 let high = Price::from(high);
1437 let low = Price::from(low);
1438 let close = Price::from(close);
1439 let volume = Quantity::from("1000");
1440 let ts_event = UnixNanos::from(1_000_000);
1441 let ts_init = UnixNanos::from(2_000_000);
1442
1443 let result = Bar::new_checked(bar_type, open, high, low, close, volume, ts_event, ts_init);
1444
1445 assert!(result.is_err());
1446 }
1447
1448 #[rstest]
1449 fn test_bar_equality() {
1450 let instrument_id = InstrumentId {
1451 symbol: Symbol::new("AUDUSD"),
1452 venue: Venue::new("SIM"),
1453 };
1454 let bar_spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Bid);
1455 let bar_type = BarType::Standard {
1456 instrument_id,
1457 spec: bar_spec,
1458 aggregation_source: AggregationSource::External,
1459 };
1460 let bar1 = Bar {
1461 bar_type,
1462 open: Price::from("1.00001"),
1463 high: Price::from("1.00004"),
1464 low: Price::from("1.00002"),
1465 close: Price::from("1.00003"),
1466 volume: Quantity::from("100000"),
1467 ts_event: UnixNanos::default(),
1468 ts_init: UnixNanos::from(1),
1469 };
1470
1471 let bar2 = Bar {
1472 bar_type,
1473 open: Price::from("1.00000"),
1474 high: Price::from("1.00004"),
1475 low: Price::from("1.00002"),
1476 close: Price::from("1.00003"),
1477 volume: Quantity::from("100000"),
1478 ts_event: UnixNanos::default(),
1479 ts_init: UnixNanos::from(1),
1480 };
1481 assert_eq!(bar1, bar1);
1482 assert_ne!(bar1, bar2);
1483 }
1484
1485 #[rstest]
1486 fn test_json_serialization() {
1487 let bar = Bar::default();
1488 let serialized = bar.to_json_bytes().unwrap();
1489 let deserialized = Bar::from_json_bytes(serialized.as_ref()).unwrap();
1490 assert_eq!(deserialized, bar);
1491 }
1492
1493 #[rstest]
1494 fn test_msgpack_serialization() {
1495 let bar = Bar::default();
1496 let serialized = bar.to_msgpack_bytes().unwrap();
1497 let deserialized = Bar::from_msgpack_bytes(serialized.as_ref()).unwrap();
1498 assert_eq!(deserialized, bar);
1499 }
1500}