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