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 {
64 Self {
65 utc_offset_minutes,
66 day_key: None,
67 high: f64::NEG_INFINITY,
68 low: f64::INFINITY,
69 last: None,
70 }
71 }
72
73 pub const fn utc_offset_minutes(&self) -> i32 {
75 self.utc_offset_minutes
76 }
77
78 pub const fn value(&self) -> Option<SessionHighLowOutput> {
80 self.last
81 }
82}
83
84impl Indicator for SessionHighLow {
85 type Input = Candle;
86 type Output = SessionHighLowOutput;
87
88 #[inline]
89 fn update(&mut self, candle: Candle) -> Option<SessionHighLowOutput> {
90 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
91 let key = (civil.year, civil.month, civil.day);
92 if self.day_key == Some(key) {
93 if candle.high > self.high {
94 self.high = candle.high;
95 }
96 if candle.low < self.low {
97 self.low = candle.low;
98 }
99 } else {
100 self.day_key = Some(key);
101 self.high = candle.high;
102 self.low = candle.low;
103 }
104 let out = SessionHighLowOutput {
105 high: self.high,
106 low: self.low,
107 };
108 self.last = Some(out);
109 Some(out)
110 }
111
112 fn reset(&mut self) {
113 self.day_key = None;
114 self.high = f64::NEG_INFINITY;
115 self.low = f64::INFINITY;
116 self.last = None;
117 }
118
119 #[inline]
120 fn warmup_period(&self) -> usize {
121 1
122 }
123
124 #[inline]
125 fn is_ready(&self) -> bool {
126 self.last.is_some()
127 }
128
129 #[inline]
130 fn name(&self) -> &'static str {
131 "SessionHighLow"
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138 use crate::traits::BatchExt;
139 use approx::assert_relative_eq;
140
141 const HOUR: i64 = 3_600_000;
142
143 fn c(high: f64, low: f64, ts: i64) -> Candle {
144 let mid = f64::midpoint(high, low);
145 Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
146 }
147
148 #[test]
149 fn metadata_and_accessors() {
150 let shl = SessionHighLow::new(-300);
151 assert_eq!(shl.utc_offset_minutes(), -300);
152 assert_eq!(shl.name(), "SessionHighLow");
153 assert_eq!(shl.warmup_period(), 1);
154 assert!(!shl.is_ready());
155 assert!(shl.value().is_none());
156 }
157
158 #[test]
159 fn tracks_high_low_within_day() {
160 let mut shl = SessionHighLow::new(0);
161 let first = shl.update(c(105.0, 99.0, 0)).unwrap();
162 assert_relative_eq!(first.high, 105.0);
163 assert_relative_eq!(first.low, 99.0);
164 assert!(shl.is_ready());
165 let second = shl.update(c(108.0, 100.0, HOUR)).unwrap();
166 assert_relative_eq!(second.high, 108.0);
167 assert_relative_eq!(second.low, 99.0);
168 let third = shl.update(c(106.0, 101.0, 2 * HOUR)).unwrap();
170 assert_relative_eq!(third.high, 108.0);
171 assert_relative_eq!(third.low, 99.0);
172 let fourth = shl.update(c(107.0, 95.0, 3 * HOUR)).unwrap();
174 assert_relative_eq!(fourth.high, 108.0);
175 assert_relative_eq!(fourth.low, 95.0);
176 }
177
178 #[test]
179 fn re_anchors_on_new_day() {
180 let mut shl = SessionHighLow::new(0);
181 shl.update(c(105.0, 99.0, 0));
182 shl.update(c(108.0, 100.0, HOUR));
183 let next = shl.update(c(51.0, 49.0, 24 * HOUR)).unwrap();
184 assert_relative_eq!(next.high, 51.0);
185 assert_relative_eq!(next.low, 49.0);
186 }
187
188 #[test]
189 fn utc_offset_shifts_day_boundary() {
190 let pre = 23 * HOUR; let post = 24 * HOUR; let mut utc = SessionHighLow::new(0);
195 utc.update(c(105.0, 99.0, pre));
196 let rolled = utc.update(c(108.0, 100.0, post)).unwrap();
197 assert_relative_eq!(rolled.high, 108.0);
198 assert_relative_eq!(rolled.low, 100.0); let mut shifted = SessionHighLow::new(120);
201 shifted.update(c(105.0, 99.0, pre));
202 let same = shifted.update(c(108.0, 100.0, post)).unwrap();
203 assert_relative_eq!(same.high, 108.0);
204 assert_relative_eq!(same.low, 99.0); }
206
207 #[test]
208 fn reset_clears_state() {
209 let mut shl = SessionHighLow::new(0);
210 shl.update(c(105.0, 99.0, 0));
211 shl.reset();
212 assert!(!shl.is_ready());
213 assert!(shl.value().is_none());
214 let after = shl.update(c(60.0, 50.0, HOUR)).unwrap();
215 assert_relative_eq!(after.high, 60.0);
216 assert_relative_eq!(after.low, 50.0);
217 }
218
219 #[test]
220 fn batch_equals_streaming() {
221 let candles: Vec<Candle> = (0..30)
222 .map(|i| {
223 c(
224 100.0 + f64::from(i),
225 90.0 + f64::from(i) * 0.5,
226 i64::from(i) * HOUR,
227 )
228 })
229 .collect();
230 let mut a = SessionHighLow::new(0);
231 let mut b = SessionHighLow::new(0);
232 assert_eq!(
233 a.batch(&candles),
234 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
235 );
236 }
237}