kestrel_chartkit/
intrabar.rs1use crate::model::Bar;
11use crate::timeframe::{Timeframe, TimeframeError};
12
13#[derive(Debug, Clone, PartialEq)]
15pub struct IntrabarGroup {
16 pub parent_timestamp: i64,
19 pub children: Vec<Bar>,
21}
22
23#[derive(Debug, Clone)]
26pub struct IntrabarGrouper {
27 parent_tf: Timeframe,
28 utc_offset_seconds: i32,
29 current_parent_start: Option<i64>,
30 current_children: Vec<Bar>,
31}
32
33impl IntrabarGrouper {
34 pub fn new(parent_tf: Timeframe) -> Result<Self, TimeframeError> {
35 Self::with_utc_offset(parent_tf, 0)
36 }
37
38 pub fn with_utc_offset(
39 parent_tf: Timeframe,
40 utc_offset_seconds: i32,
41 ) -> Result<Self, TimeframeError> {
42 Ok(Self {
43 parent_tf: parent_tf.validate()?,
44 utc_offset_seconds,
45 current_parent_start: None,
46 current_children: Vec::new(),
47 })
48 }
49
50 pub fn reset(&mut self) {
51 self.current_parent_start = None;
52 self.current_children.clear();
53 }
54
55 pub fn on_child_bar(&mut self, bar: &Bar) -> Option<IntrabarGroup> {
59 let parent_start = self
60 .parent_tf
61 .bucket_start(bar.timestamp, self.utc_offset_seconds);
62
63 let completed = match self.current_parent_start {
64 Some(start) if start == parent_start => None,
65 Some(start) => Some(IntrabarGroup {
66 parent_timestamp: start,
67 children: std::mem::take(&mut self.current_children),
68 }),
69 None => None,
70 };
71
72 if completed.is_some() || self.current_parent_start.is_none() {
73 self.current_parent_start = Some(parent_start);
74 }
75 self.current_children.push(bar.clone());
76
77 completed
78 }
79
80 pub fn current_children(&self) -> &[Bar] {
84 &self.current_children
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 fn child_bar(ts: i64) -> Bar {
93 Bar::new(ts, 100.0, 101.0, 99.0, 100.0, 10.0)
94 }
95
96 #[test]
97 fn test_groups_children_uniquely_under_parent_bucket() {
98 let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
99
100 for ts in [0, 60, 120, 180, 240] {
102 let completed = grouper.on_child_bar(&child_bar(ts));
103 assert!(completed.is_none());
104 }
105 assert_eq!(grouper.current_children().len(), 5);
106
107 let completed = grouper.on_child_bar(&child_bar(300)).unwrap();
109 assert_eq!(completed.parent_timestamp, 0);
110 assert_eq!(completed.children.len(), 5);
111 assert_eq!(
112 completed
113 .children
114 .iter()
115 .map(|b| b.timestamp)
116 .collect::<Vec<_>>(),
117 vec![0, 60, 120, 180, 240]
118 );
119
120 assert_eq!(grouper.current_children().len(), 1);
122 }
123
124 #[test]
125 fn test_reset_clears_in_progress_group() {
126 let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
127 grouper.on_child_bar(&child_bar(0));
128 assert_eq!(grouper.current_children().len(), 1);
129
130 grouper.reset();
131 assert!(grouper.current_children().is_empty());
132
133 let completed = grouper.on_child_bar(&child_bar(600));
135 assert!(completed.is_none());
136 assert_eq!(grouper.current_children().len(), 1);
137 }
138
139 #[test]
140 fn test_consistent_with_bar_resampler_bucketing() {
141 use crate::timeframe::BarResampler;
142
143 let mut resampler = BarResampler::new(Timeframe::Minute(5)).unwrap();
144 let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
145
146 let mut resampled_completed = None;
147 let mut grouped_completed = None;
148 for ts in [0, 60, 120, 180, 240, 300] {
149 let bar = child_bar(ts);
150 if let Some(c) = resampler.on_bar(&bar).completed_bar {
151 resampled_completed = Some(c);
152 }
153 if let Some(g) = grouper.on_child_bar(&bar) {
154 grouped_completed = Some(g);
155 }
156 }
157
158 assert_eq!(
159 resampled_completed.unwrap().timestamp,
160 grouped_completed.unwrap().parent_timestamp
161 );
162 }
163}