wickra_core/indicators/
force_index.rs1use crate::error::Result;
4use crate::indicators::ema::Ema;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
38pub struct ForceIndex {
39 period: usize,
40 prev_close: Option<f64>,
41 ema: Ema,
42}
43
44impl ForceIndex {
45 pub fn new(period: usize) -> Result<Self> {
50 Ok(Self {
51 period,
52 prev_close: None,
53 ema: Ema::new(period)?,
54 })
55 }
56
57 pub const fn period(&self) -> usize {
59 self.period
60 }
61}
62
63impl Indicator for ForceIndex {
64 type Input = Candle;
65 type Output = f64;
66
67 #[inline]
68 fn update(&mut self, candle: Candle) -> Option<f64> {
69 let Some(prev) = self.prev_close else {
70 self.prev_close = Some(candle.close);
72 return None;
73 };
74 let raw = (candle.close - prev) * candle.volume;
75 self.prev_close = Some(candle.close);
76 self.ema.update(raw)
77 }
78
79 fn reset(&mut self) {
80 self.prev_close = None;
81 self.ema.reset();
82 }
83
84 #[inline]
85 fn warmup_period(&self) -> usize {
86 self.period + 1
89 }
90
91 #[inline]
92 fn is_ready(&self) -> bool {
93 self.ema.is_ready()
94 }
95
96 #[inline]
97 fn name(&self) -> &'static str {
98 "ForceIndex"
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::traits::BatchExt;
106 use approx::assert_relative_eq;
107
108 fn c(close: f64, volume: f64, ts: i64) -> Candle {
109 Candle::new(close, close, close, close, volume, ts).unwrap()
110 }
111
112 #[test]
113 fn reference_values() {
114 let mut fi = ForceIndex::new(1).unwrap();
119 let out = fi.batch(&[c(10.0, 100.0, 0), c(12.0, 100.0, 1), c(11.0, 200.0, 2)]);
120 assert!(out[0].is_none());
121 assert_relative_eq!(out[1].unwrap(), 200.0, epsilon = 1e-9);
122 assert_relative_eq!(out[2].unwrap(), -200.0, epsilon = 1e-9);
123 }
124
125 #[test]
126 fn pure_uptrend_is_positive() {
127 let candles: Vec<Candle> = (1..40)
130 .map(|i| c(f64::from(i), 100.0, i64::from(i)))
131 .collect();
132 let mut fi = ForceIndex::new(13).unwrap();
133 for v in fi.batch(&candles).into_iter().flatten() {
134 assert!(v > 0.0, "force {v} should be positive in an uptrend");
135 }
136 }
137
138 #[test]
139 fn pure_downtrend_is_negative() {
140 let candles: Vec<Candle> = (1..40)
141 .rev()
142 .map(|i| c(f64::from(i), 100.0, i64::from(i)))
143 .collect();
144 let mut fi = ForceIndex::new(13).unwrap();
145 for v in fi.batch(&candles).into_iter().flatten() {
146 assert!(v < 0.0, "force {v} should be negative in a downtrend");
147 }
148 }
149
150 #[test]
151 fn first_value_on_period_plus_one_candle() {
152 let candles: Vec<Candle> = (0..12).map(|i| c(10.0 + i as f64, 50.0, i)).collect();
153 let mut fi = ForceIndex::new(5).unwrap();
154 let out = fi.batch(&candles);
155 for (i, v) in out.iter().enumerate().take(5) {
156 assert!(v.is_none(), "index {i} must be None during warmup");
157 }
158 assert!(out[5].is_some(), "first force lands at index period");
159 assert_eq!(fi.warmup_period(), 6);
160 }
161
162 #[test]
163 fn rejects_zero_period() {
164 assert!(ForceIndex::new(0).is_err());
165 }
166
167 #[test]
170 fn accessors_and_metadata() {
171 let fi = ForceIndex::new(13).unwrap();
172 assert_eq!(fi.period(), 13);
173 assert_eq!(fi.name(), "ForceIndex");
174 }
175
176 #[test]
177 fn reset_clears_state() {
178 let candles: Vec<Candle> = (0..30).map(|i| c(10.0 + i as f64, 50.0, i)).collect();
179 let mut fi = ForceIndex::new(13).unwrap();
180 fi.batch(&candles);
181 assert!(fi.is_ready());
182 fi.reset();
183 assert!(!fi.is_ready());
184 assert_eq!(fi.update(candles[0]), None);
185 }
186
187 #[test]
188 fn batch_equals_streaming() {
189 let candles: Vec<Candle> = (0..80)
190 .map(|i| {
191 let close = 100.0 + (i as f64 * 0.3).sin() * 8.0;
192 c(close, 10.0 + (i % 5) as f64, i)
193 })
194 .collect();
195 let mut a = ForceIndex::new(13).unwrap();
196 let mut b = ForceIndex::new(13).unwrap();
197 assert_eq!(
198 a.batch(&candles),
199 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
200 );
201 }
202}