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