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 fn update(&mut self, candle: Candle) -> Option<f64> {
64 let range = candle.high - candle.low;
65 let ii = if range > 0.0 {
66 candle.volume * (2.0 * candle.close - candle.high - candle.low) / range
67 } else {
68 0.0
69 };
70 self.last = Some(ii);
71 Some(ii)
72 }
73
74 fn reset(&mut self) {
75 self.last = None;
76 }
77
78 fn warmup_period(&self) -> usize {
79 1
80 }
81
82 fn is_ready(&self) -> bool {
83 self.last.is_some()
84 }
85
86 fn name(&self) -> &'static str {
87 "IntradayIntensity"
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::traits::BatchExt;
95 use approx::assert_relative_eq;
96
97 fn candle(high: f64, low: f64, close: f64, volume: f64) -> Candle {
98 Candle::new_unchecked(low, high, low, close, volume, 0)
99 }
100
101 #[test]
102 fn accessors_and_metadata() {
103 let iii = IntradayIntensity::new();
104 assert_eq!(iii.warmup_period(), 1);
105 assert_eq!(iii.name(), "IntradayIntensity");
106 assert!(!iii.is_ready());
107 assert_eq!(iii.value(), None);
108 }
109
110 #[test]
111 fn first_bar_emits() {
112 let mut iii = IntradayIntensity::new();
114 let v = iii.update(candle(102.0, 100.0, 102.0, 500.0)).unwrap();
116 assert_relative_eq!(v, 500.0, epsilon = 1e-9);
117 }
118
119 #[test]
120 fn close_on_high_adds_full_volume() {
121 let mut iii = IntradayIntensity::new();
122 let v = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap();
123 assert_relative_eq!(v, 1_000.0, epsilon = 1e-9);
124 }
125
126 #[test]
127 fn close_on_low_subtracts_full_volume() {
128 let mut iii = IntradayIntensity::new();
129 let v = iii.update(candle(110.0, 100.0, 100.0, 1_000.0)).unwrap();
130 assert_relative_eq!(v, -1_000.0, epsilon = 1e-9);
131 }
132
133 #[test]
134 fn close_at_midpoint_adds_nothing() {
135 let mut iii = IntradayIntensity::new();
136 let v = iii.update(candle(110.0, 100.0, 105.0, 1_000.0)).unwrap();
137 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
138 }
139
140 #[test]
141 fn zero_range_adds_nothing() {
142 let mut iii = IntradayIntensity::new();
143 let v = iii.update(candle(100.0, 100.0, 100.0, 1_000.0)).unwrap();
144 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
145 }
146
147 #[test]
148 fn each_bar_is_independent() {
149 let mut iii = IntradayIntensity::new();
152 let a = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap();
153 let b = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap();
154 assert_relative_eq!(a, 1_000.0, epsilon = 1e-9);
155 assert_relative_eq!(b, -400.0, epsilon = 1e-9);
156 }
157
158 #[test]
159 fn reset_clears_state() {
160 let mut iii = IntradayIntensity::new();
161 iii.batch(&[
162 candle(110.0, 100.0, 108.0, 1.0),
163 candle(110.0, 100.0, 102.0, 1.0),
164 ]);
165 assert!(iii.is_ready());
166 iii.reset();
167 assert!(!iii.is_ready());
168 assert_eq!(iii.value(), None);
169 }
170
171 #[test]
172 fn batch_equals_streaming() {
173 let candles: Vec<Candle> = (0..80)
174 .map(|i| {
175 let base = 100.0 + (f64::from(i) * 0.3).sin() * 6.0;
176 candle(base + 2.0, base - 2.0, base + 0.7, 1_000.0 + f64::from(i))
177 })
178 .collect();
179 let batch = IntradayIntensity::new().batch(&candles);
180 let mut b = IntradayIntensity::new();
181 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
182 assert_eq!(batch, streamed);
183 }
184}