wickra_core/indicators/
intraday_intensity.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
42pub struct IntradayIntensity {
43 last: Option<f64>,
44}
45
46impl IntradayIntensity {
47 #[must_use]
49 pub fn new() -> Self {
50 Self::default()
51 }
52
53 pub const fn value(&self) -> Option<f64> {
55 self.last
56 }
57}
58
59impl Indicator for IntradayIntensity {
60 type Input = Candle;
61 type Output = f64;
62
63 #[inline]
64 fn update(&mut self, candle: Candle) -> Option<f64> {
65 let range = candle.high - candle.low;
66 let ii = if range > 0.0 {
67 candle.volume * (2.0 * candle.close - candle.high - candle.low) / range
68 } else {
69 0.0
70 };
71 self.last = Some(ii);
72 Some(ii)
73 }
74
75 fn reset(&mut self) {
76 self.last = None;
77 }
78
79 #[inline]
80 fn warmup_period(&self) -> usize {
81 1
82 }
83
84 #[inline]
85 fn is_ready(&self) -> bool {
86 self.last.is_some()
87 }
88
89 #[inline]
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 each_bar_is_independent() {
153 let mut iii = IntradayIntensity::new();
156 let a = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap();
157 let b = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap();
158 assert_relative_eq!(a, 1_000.0, epsilon = 1e-9);
159 assert_relative_eq!(b, -400.0, epsilon = 1e-9);
160 }
161
162 #[test]
163 fn reset_clears_state() {
164 let mut iii = IntradayIntensity::new();
165 iii.batch(&[
166 candle(110.0, 100.0, 108.0, 1.0),
167 candle(110.0, 100.0, 102.0, 1.0),
168 ]);
169 assert!(iii.is_ready());
170 iii.reset();
171 assert!(!iii.is_ready());
172 assert_eq!(iii.value(), None);
173 }
174
175 #[test]
176 fn batch_equals_streaming() {
177 let candles: Vec<Candle> = (0..80)
178 .map(|i| {
179 let base = 100.0 + (f64::from(i) * 0.3).sin() * 6.0;
180 candle(base + 2.0, base - 2.0, base + 0.7, 1_000.0 + f64::from(i))
181 })
182 .collect();
183 let batch = IntradayIntensity::new().batch(&candles);
184 let mut b = IntradayIntensity::new();
185 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
186 assert_eq!(batch, streamed);
187 }
188}