1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! Lower-timeframe (intrabar) child-bar grouping.
//!
//! [`BarResampler`](crate::timeframe::BarResampler) goes from lower-timeframe (LTF) bars to an
//! *aggregated* higher-timeframe (HTF) bar. Some Pine calculations
//! (`request.security_lower_tf`-style: intrabar delta, aggressor volume, absorption) instead need
//! the **full ordered sequence** of LTF child bars that belong to each HTF parent bucket, not just
//! their aggregate. [`IntrabarGrouper`] provides that, reusing the same `Timeframe::bucket_start`
//! bucketing [`BarResampler`] uses, so the two stay consistent with each other.
use crate::model::Bar;
use crate::timeframe::{Timeframe, TimeframeError};
/// A parent bucket's complete, time-ordered child-bar sequence.
#[derive(Debug, Clone, PartialEq)]
pub struct IntrabarGroup {
/// The parent bucket's open timestamp (same convention as
/// [`crate::timeframe::ResamplerOutput::completed_bar`]).
pub parent_timestamp: i64,
/// Child bars in arrival order. Never empty.
pub children: Vec<Bar>,
}
/// Groups incoming lower-timeframe (child) bars by the higher-timeframe (parent) bucket they fall
/// into, using the same bucketing as [`crate::timeframe::BarResampler`].
#[derive(Debug, Clone)]
pub struct IntrabarGrouper {
parent_tf: Timeframe,
utc_offset_seconds: i32,
current_parent_start: Option<i64>,
current_children: Vec<Bar>,
}
impl IntrabarGrouper {
pub fn new(parent_tf: Timeframe) -> Result<Self, TimeframeError> {
Self::with_utc_offset(parent_tf, 0)
}
pub fn with_utc_offset(
parent_tf: Timeframe,
utc_offset_seconds: i32,
) -> Result<Self, TimeframeError> {
Ok(Self {
parent_tf: parent_tf.validate()?,
utc_offset_seconds,
current_parent_start: None,
current_children: Vec::new(),
})
}
pub fn reset(&mut self) {
self.current_parent_start = None;
self.current_children.clear();
}
/// Feeds one lower-timeframe child bar. Returns the *previous* parent bucket's complete,
/// ordered child sequence once a child bar belonging to a new parent bucket arrives — the
/// child-bar analogue of [`crate::timeframe::BarResampler::on_bar`]'s `completed_bar`.
pub fn on_child_bar(&mut self, bar: &Bar) -> Option<IntrabarGroup> {
let parent_start = self
.parent_tf
.bucket_start(bar.timestamp, self.utc_offset_seconds);
let completed = match self.current_parent_start {
Some(start) if start == parent_start => None,
Some(start) => Some(IntrabarGroup {
parent_timestamp: start,
children: std::mem::take(&mut self.current_children),
}),
None => None,
};
if completed.is_some() || self.current_parent_start.is_none() {
self.current_parent_start = Some(parent_start);
}
self.current_children.push(bar.clone());
completed
}
/// The still-forming parent bucket's child bars so far. Grows/repaints as more child bars
/// arrive, mirroring [`crate::timeframe::ResamplerOutput::current_unconfirmed`]'s lookahead
/// caveat: only [`IntrabarGrouper::on_child_bar`]'s returned [`IntrabarGroup`] is confirmed.
pub fn current_children(&self) -> &[Bar] {
&self.current_children
}
}
#[cfg(test)]
mod tests {
use super::*;
fn child_bar(ts: i64) -> Bar {
Bar::new(ts, 100.0, 101.0, 99.0, 100.0, 10.0)
}
#[test]
fn test_groups_children_uniquely_under_parent_bucket() {
let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
// Five 1-minute children belong to the same 5-minute parent bucket [0, 300).
for ts in [0, 60, 120, 180, 240] {
let completed = grouper.on_child_bar(&child_bar(ts));
assert!(completed.is_none());
}
assert_eq!(grouper.current_children().len(), 5);
// A child at t=300 starts the next parent bucket, completing the first.
let completed = grouper.on_child_bar(&child_bar(300)).unwrap();
assert_eq!(completed.parent_timestamp, 0);
assert_eq!(completed.children.len(), 5);
assert_eq!(
completed
.children
.iter()
.map(|b| b.timestamp)
.collect::<Vec<_>>(),
vec![0, 60, 120, 180, 240]
);
// The new bucket now holds exactly the t=300 child.
assert_eq!(grouper.current_children().len(), 1);
}
#[test]
fn test_reset_clears_in_progress_group() {
let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
grouper.on_child_bar(&child_bar(0));
assert_eq!(grouper.current_children().len(), 1);
grouper.reset();
assert!(grouper.current_children().is_empty());
// After reset, the next child starts a fresh bucket rather than completing the old one.
let completed = grouper.on_child_bar(&child_bar(600));
assert!(completed.is_none());
assert_eq!(grouper.current_children().len(), 1);
}
#[test]
fn test_consistent_with_bar_resampler_bucketing() {
use crate::timeframe::BarResampler;
let mut resampler = BarResampler::new(Timeframe::Minute(5)).unwrap();
let mut grouper = IntrabarGrouper::new(Timeframe::Minute(5)).unwrap();
let mut resampled_completed = None;
let mut grouped_completed = None;
for ts in [0, 60, 120, 180, 240, 300] {
let bar = child_bar(ts);
if let Some(c) = resampler.on_bar(&bar).completed_bar {
resampled_completed = Some(c);
}
if let Some(g) = grouper.on_child_bar(&bar) {
grouped_completed = Some(g);
}
}
assert_eq!(
resampled_completed.unwrap().timestamp,
grouped_completed.unwrap().parent_timestamp
);
}
}