wickra_core/indicators/
session_high_low.rs1use crate::calendar::civil_from_timestamp;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
11pub struct SessionHighLowOutput {
12 pub high: f64,
14 pub low: f64,
16}
17
18#[derive(Debug, Clone)]
46pub struct SessionHighLow {
47 utc_offset_minutes: i32,
48 day_key: Option<(i64, u32, u32)>,
49 high: f64,
50 low: f64,
51 last: Option<SessionHighLowOutput>,
52}
53
54impl SessionHighLow {
55 pub const fn new(utc_offset_minutes: i32) -> Self {
57 Self {
58 utc_offset_minutes,
59 day_key: None,
60 high: f64::NEG_INFINITY,
61 low: f64::INFINITY,
62 last: None,
63 }
64 }
65
66 pub const fn utc_offset_minutes(&self) -> i32 {
68 self.utc_offset_minutes
69 }
70
71 pub const fn value(&self) -> Option<SessionHighLowOutput> {
73 self.last
74 }
75}
76
77impl Indicator for SessionHighLow {
78 type Input = Candle;
79 type Output = SessionHighLowOutput;
80
81 fn update(&mut self, candle: Candle) -> Option<SessionHighLowOutput> {
82 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
83 let key = (civil.year, civil.month, civil.day);
84 if self.day_key == Some(key) {
85 if candle.high > self.high {
86 self.high = candle.high;
87 }
88 if candle.low < self.low {
89 self.low = candle.low;
90 }
91 } else {
92 self.day_key = Some(key);
93 self.high = candle.high;
94 self.low = candle.low;
95 }
96 let out = SessionHighLowOutput {
97 high: self.high,
98 low: self.low,
99 };
100 self.last = Some(out);
101 Some(out)
102 }
103
104 fn reset(&mut self) {
105 self.day_key = None;
106 self.high = f64::NEG_INFINITY;
107 self.low = f64::INFINITY;
108 self.last = None;
109 }
110
111 fn warmup_period(&self) -> usize {
112 1
113 }
114
115 fn is_ready(&self) -> bool {
116 self.last.is_some()
117 }
118
119 fn name(&self) -> &'static str {
120 "SessionHighLow"
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::traits::BatchExt;
128 use approx::assert_relative_eq;
129
130 const HOUR: i64 = 3_600_000;
131
132 fn c(high: f64, low: f64, ts: i64) -> Candle {
133 let mid = f64::midpoint(high, low);
134 Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
135 }
136
137 #[test]
138 fn metadata_and_accessors() {
139 let shl = SessionHighLow::new(-300);
140 assert_eq!(shl.utc_offset_minutes(), -300);
141 assert_eq!(shl.name(), "SessionHighLow");
142 assert_eq!(shl.warmup_period(), 1);
143 assert!(!shl.is_ready());
144 assert!(shl.value().is_none());
145 }
146
147 #[test]
148 fn tracks_high_low_within_day() {
149 let mut shl = SessionHighLow::new(0);
150 let first = shl.update(c(105.0, 99.0, 0)).unwrap();
151 assert_relative_eq!(first.high, 105.0);
152 assert_relative_eq!(first.low, 99.0);
153 assert!(shl.is_ready());
154 let second = shl.update(c(108.0, 100.0, HOUR)).unwrap();
155 assert_relative_eq!(second.high, 108.0);
156 assert_relative_eq!(second.low, 99.0);
157 let third = shl.update(c(106.0, 101.0, 2 * HOUR)).unwrap();
159 assert_relative_eq!(third.high, 108.0);
160 assert_relative_eq!(third.low, 99.0);
161 let fourth = shl.update(c(107.0, 95.0, 3 * HOUR)).unwrap();
163 assert_relative_eq!(fourth.high, 108.0);
164 assert_relative_eq!(fourth.low, 95.0);
165 }
166
167 #[test]
168 fn re_anchors_on_new_day() {
169 let mut shl = SessionHighLow::new(0);
170 shl.update(c(105.0, 99.0, 0));
171 shl.update(c(108.0, 100.0, HOUR));
172 let next = shl.update(c(51.0, 49.0, 24 * HOUR)).unwrap();
173 assert_relative_eq!(next.high, 51.0);
174 assert_relative_eq!(next.low, 49.0);
175 }
176
177 #[test]
178 fn utc_offset_shifts_day_boundary() {
179 let pre = 23 * HOUR; let post = 24 * HOUR; let mut utc = SessionHighLow::new(0);
184 utc.update(c(105.0, 99.0, pre));
185 let rolled = utc.update(c(108.0, 100.0, post)).unwrap();
186 assert_relative_eq!(rolled.high, 108.0);
187 assert_relative_eq!(rolled.low, 100.0); let mut shifted = SessionHighLow::new(120);
190 shifted.update(c(105.0, 99.0, pre));
191 let same = shifted.update(c(108.0, 100.0, post)).unwrap();
192 assert_relative_eq!(same.high, 108.0);
193 assert_relative_eq!(same.low, 99.0); }
195
196 #[test]
197 fn reset_clears_state() {
198 let mut shl = SessionHighLow::new(0);
199 shl.update(c(105.0, 99.0, 0));
200 shl.reset();
201 assert!(!shl.is_ready());
202 assert!(shl.value().is_none());
203 let after = shl.update(c(60.0, 50.0, HOUR)).unwrap();
204 assert_relative_eq!(after.high, 60.0);
205 assert_relative_eq!(after.low, 50.0);
206 }
207
208 #[test]
209 fn batch_equals_streaming() {
210 let candles: Vec<Candle> = (0..30)
211 .map(|i| {
212 c(
213 100.0 + f64::from(i),
214 90.0 + f64::from(i) * 0.5,
215 i64::from(i) * HOUR,
216 )
217 })
218 .collect();
219 let mut a = SessionHighLow::new(0);
220 let mut b = SessionHighLow::new(0);
221 assert_eq!(
222 a.batch(&candles),
223 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
224 );
225 }
226}