wickra_core/indicators/
session_range.rs1use crate::calendar::civil_from_timestamp;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct SessionRangeOutput {
14 pub asia: f64,
16 pub eu: f64,
18 pub us: f64,
20}
21
22#[derive(Debug, Clone, Copy)]
23struct Extent {
24 high: f64,
25 low: f64,
26}
27
28impl Extent {
29 const EMPTY: Self = Self {
30 high: f64::NEG_INFINITY,
31 low: f64::INFINITY,
32 };
33
34 fn add(&mut self, candle: Candle) {
35 if candle.high > self.high {
36 self.high = candle.high;
37 }
38 if candle.low < self.low {
39 self.low = candle.low;
40 }
41 }
42
43 fn range(self) -> f64 {
44 if self.high >= self.low {
45 self.high - self.low
46 } else {
47 0.0
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
77pub struct SessionRange {
78 utc_offset_minutes: i32,
79 day_key: Option<(i64, u32, u32)>,
80 sessions: [Extent; 3],
81 last: Option<SessionRangeOutput>,
82}
83
84impl SessionRange {
85 pub const fn new(utc_offset_minutes: i32) -> Self {
87 Self {
88 utc_offset_minutes,
89 day_key: None,
90 sessions: [Extent::EMPTY; 3],
91 last: None,
92 }
93 }
94
95 pub const fn utc_offset_minutes(&self) -> i32 {
97 self.utc_offset_minutes
98 }
99
100 pub const fn value(&self) -> Option<SessionRangeOutput> {
102 self.last
103 }
104
105 fn snapshot(&self) -> SessionRangeOutput {
106 SessionRangeOutput {
107 asia: self.sessions[0].range(),
108 eu: self.sessions[1].range(),
109 us: self.sessions[2].range(),
110 }
111 }
112}
113
114impl Indicator for SessionRange {
115 type Input = Candle;
116 type Output = SessionRangeOutput;
117
118 fn update(&mut self, candle: Candle) -> Option<SessionRangeOutput> {
119 let civil = civil_from_timestamp(candle.timestamp, self.utc_offset_minutes);
120 let key = (civil.year, civil.month, civil.day);
121 if self.day_key != Some(key) {
122 self.day_key = Some(key);
123 self.sessions = [Extent::EMPTY; 3];
124 }
125 let session = (civil.hour / 8) as usize; self.sessions[session].add(candle);
127 let out = self.snapshot();
128 self.last = Some(out);
129 Some(out)
130 }
131
132 fn reset(&mut self) {
133 self.day_key = None;
134 self.sessions = [Extent::EMPTY; 3];
135 self.last = None;
136 }
137
138 fn warmup_period(&self) -> usize {
139 1
140 }
141
142 fn is_ready(&self) -> bool {
143 self.last.is_some()
144 }
145
146 fn name(&self) -> &'static str {
147 "SessionRange"
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use crate::traits::BatchExt;
155 use approx::assert_relative_eq;
156
157 const HOUR: i64 = 3_600_000;
158
159 fn c(high: f64, low: f64, ts: i64) -> Candle {
160 let mid = f64::midpoint(high, low);
161 Candle::new(mid, high, low, mid, 1.0, ts).unwrap()
162 }
163
164 #[test]
165 fn metadata_and_accessors() {
166 let sr = SessionRange::new(60);
167 assert_eq!(sr.utc_offset_minutes(), 60);
168 assert_eq!(sr.name(), "SessionRange");
169 assert_eq!(sr.warmup_period(), 1);
170 assert!(!sr.is_ready());
171 assert!(sr.value().is_none());
172 }
173
174 #[test]
175 fn assigns_bars_to_sessions() {
176 let mut sr = SessionRange::new(0);
177 let asia = sr.update(c(104.0, 98.0, 2 * HOUR)).unwrap();
178 assert_relative_eq!(asia.asia, 6.0);
179 assert_relative_eq!(asia.eu, 0.0);
180 assert_relative_eq!(asia.us, 0.0);
181 assert!(sr.is_ready());
182 let eu = sr.update(c(110.0, 100.0, 10 * HOUR)).unwrap();
183 assert_relative_eq!(eu.eu, 10.0);
184 let us = sr.update(c(120.0, 118.0, 20 * HOUR)).unwrap();
185 assert_relative_eq!(us.us, 2.0);
186 assert_relative_eq!(us.asia, 6.0);
187 }
188
189 #[test]
190 fn widens_within_one_session() {
191 let mut sr = SessionRange::new(0);
192 sr.update(c(104.0, 98.0, HOUR));
193 let wider = sr.update(c(106.0, 95.0, 3 * HOUR)).unwrap();
194 assert_relative_eq!(wider.asia, 11.0);
195 }
196
197 #[test]
198 fn resets_sessions_on_new_day() {
199 let mut sr = SessionRange::new(0);
200 sr.update(c(104.0, 98.0, 2 * HOUR));
201 sr.update(c(110.0, 100.0, 10 * HOUR));
202 let next = sr.update(c(101.0, 99.0, (24 + 2) * HOUR)).unwrap();
203 assert_relative_eq!(next.asia, 2.0);
204 assert_relative_eq!(next.eu, 0.0);
205 }
206
207 #[test]
208 fn utc_offset_moves_bar_between_sessions() {
209 let mut utc = SessionRange::new(0);
211 let a = utc.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
212 assert_relative_eq!(a.asia, 6.0);
213 assert_relative_eq!(a.eu, 0.0);
214
215 let mut shifted = SessionRange::new(120);
216 let e = shifted.update(c(104.0, 98.0, 7 * HOUR)).unwrap();
217 assert_relative_eq!(e.asia, 0.0);
218 assert_relative_eq!(e.eu, 6.0);
219 }
220
221 #[test]
222 fn reset_clears_state() {
223 let mut sr = SessionRange::new(0);
224 sr.update(c(104.0, 98.0, 2 * HOUR));
225 sr.reset();
226 assert!(!sr.is_ready());
227 assert!(sr.value().is_none());
228 }
229
230 #[test]
231 fn batch_equals_streaming() {
232 let candles: Vec<Candle> = (0..40)
233 .map(|i| {
234 c(
235 100.0 + f64::from(i % 5),
236 95.0 - f64::from(i % 3),
237 i64::from(i) * HOUR,
238 )
239 })
240 .collect();
241 let mut a = SessionRange::new(0);
242 let mut b = SessionRange::new(0);
243 assert_eq!(
244 a.batch(&candles),
245 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
246 );
247 }
248}