wickra_core/indicators/
intraday_intensity.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
43pub struct IntradayIntensity {
44 iii: f64,
45 last: Option<f64>,
46}
47
48impl IntradayIntensity {
49 #[must_use]
51 pub fn new() -> Self {
52 Self::default()
53 }
54
55 pub const fn value(&self) -> Option<f64> {
57 self.last
58 }
59}
60
61impl Indicator for IntradayIntensity {
62 type Input = Candle;
63 type Output = f64;
64
65 fn update(&mut self, candle: Candle) -> Option<f64> {
66 let range = candle.high - candle.low;
67 let ii = if range > 0.0 {
68 candle.volume * (2.0 * candle.close - candle.high - candle.low) / range
69 } else {
70 0.0
71 };
72 self.iii += ii;
73 self.last = Some(self.iii);
74 Some(self.iii)
75 }
76
77 fn reset(&mut self) {
78 self.iii = 0.0;
79 self.last = None;
80 }
81
82 fn warmup_period(&self) -> usize {
83 1
84 }
85
86 fn is_ready(&self) -> bool {
87 self.last.is_some()
88 }
89
90 fn name(&self) -> &'static str {
91 "IntradayIntensity"
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::traits::BatchExt;
99 use approx::assert_relative_eq;
100
101 fn candle(high: f64, low: f64, close: f64, volume: f64) -> Candle {
102 Candle::new_unchecked(low, high, low, close, volume, 0)
103 }
104
105 #[test]
106 fn accessors_and_metadata() {
107 let iii = IntradayIntensity::new();
108 assert_eq!(iii.warmup_period(), 1);
109 assert_eq!(iii.name(), "IntradayIntensity");
110 assert!(!iii.is_ready());
111 assert_eq!(iii.value(), None);
112 }
113
114 #[test]
115 fn first_bar_emits() {
116 let mut iii = IntradayIntensity::new();
118 let v = iii.update(candle(102.0, 100.0, 102.0, 500.0)).unwrap();
120 assert_relative_eq!(v, 500.0, epsilon = 1e-9);
121 }
122
123 #[test]
124 fn close_on_high_adds_full_volume() {
125 let mut iii = IntradayIntensity::new();
126 let v = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap();
127 assert_relative_eq!(v, 1_000.0, epsilon = 1e-9);
128 }
129
130 #[test]
131 fn close_on_low_subtracts_full_volume() {
132 let mut iii = IntradayIntensity::new();
133 let v = iii.update(candle(110.0, 100.0, 100.0, 1_000.0)).unwrap();
134 assert_relative_eq!(v, -1_000.0, epsilon = 1e-9);
135 }
136
137 #[test]
138 fn close_at_midpoint_adds_nothing() {
139 let mut iii = IntradayIntensity::new();
140 let v = iii.update(candle(110.0, 100.0, 105.0, 1_000.0)).unwrap();
141 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
142 }
143
144 #[test]
145 fn zero_range_adds_nothing() {
146 let mut iii = IntradayIntensity::new();
147 let v = iii.update(candle(100.0, 100.0, 100.0, 1_000.0)).unwrap();
148 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
149 }
150
151 #[test]
152 fn accumulates_across_bars() {
153 let mut iii = IntradayIntensity::new();
154 iii.update(candle(110.0, 100.0, 110.0, 1_000.0)); let v = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap(); assert_relative_eq!(v, 600.0, epsilon = 1e-9);
157 }
158
159 #[test]
160 fn reset_clears_state() {
161 let mut iii = IntradayIntensity::new();
162 iii.batch(&[
163 candle(110.0, 100.0, 108.0, 1.0),
164 candle(110.0, 100.0, 102.0, 1.0),
165 ]);
166 assert!(iii.is_ready());
167 iii.reset();
168 assert!(!iii.is_ready());
169 assert_eq!(iii.value(), None);
170 }
171
172 #[test]
173 fn batch_equals_streaming() {
174 let candles: Vec<Candle> = (0..80)
175 .map(|i| {
176 let base = 100.0 + (f64::from(i) * 0.3).sin() * 6.0;
177 candle(base + 2.0, base - 2.0, base + 0.7, 1_000.0 + f64::from(i))
178 })
179 .collect();
180 let batch = IntradayIntensity::new().batch(&candles);
181 let mut b = IntradayIntensity::new();
182 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
183 assert_eq!(batch, streamed);
184 }
185}