wickra_core/indicators/
inertia.rs1use crate::error::{Error, Result};
4use crate::indicators::linreg::LinearRegression;
5use crate::indicators::rvi::Rvi;
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
36pub struct Inertia {
37 rvi_period: usize,
38 linreg_period: usize,
39 rvi: Rvi,
40 linreg: LinearRegression,
41}
42
43impl Inertia {
44 pub fn new(rvi_period: usize, linreg_period: usize) -> Result<Self> {
47 if rvi_period == 0 || linreg_period == 0 {
48 return Err(Error::PeriodZero);
49 }
50 Ok(Self {
51 rvi_period,
52 linreg_period,
53 rvi: Rvi::new(rvi_period)?,
54 linreg: LinearRegression::new(linreg_period)?,
55 })
56 }
57
58 pub fn classic() -> Self {
60 Self::new(14, 20).expect("classic Inertia parameters are valid")
61 }
62
63 pub const fn periods(&self) -> (usize, usize) {
65 (self.rvi_period, self.linreg_period)
66 }
67}
68
69impl Indicator for Inertia {
70 type Input = Candle;
71 type Output = f64;
72
73 #[inline]
74 fn update(&mut self, candle: Candle) -> Option<f64> {
75 let rvi = self.rvi.update(candle)?;
76 self.linreg.update(rvi)
77 }
78
79 fn reset(&mut self) {
80 self.rvi.reset();
81 self.linreg.reset();
82 }
83
84 #[inline]
85 fn warmup_period(&self) -> usize {
86 self.rvi_period + self.linreg_period - 1
89 }
90
91 #[inline]
92 fn is_ready(&self) -> bool {
93 self.linreg.is_ready()
94 }
95
96 #[inline]
97 fn name(&self) -> &'static str {
98 "Inertia"
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 candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
109 Candle::new(open, high, low, close, 1.0, ts).unwrap()
110 }
111
112 #[test]
113 fn rejects_zero_period() {
114 assert!(matches!(Inertia::new(0, 20), Err(Error::PeriodZero)));
115 assert!(matches!(Inertia::new(14, 0), Err(Error::PeriodZero)));
116 }
117
118 #[test]
119 fn accessors_and_metadata() {
120 let inertia = Inertia::classic();
121 assert_eq!(inertia.periods(), (14, 20));
122 assert_eq!(inertia.warmup_period(), 33);
123 assert_eq!(inertia.name(), "Inertia");
124 }
125
126 #[test]
127 fn classic_factory() {
128 assert_eq!(Inertia::classic().periods(), (14, 20));
129 }
130
131 #[test]
132 fn warmup_emits_first_value_at_warmup_period() {
133 let mut inertia = Inertia::new(3, 4).unwrap();
136 assert_eq!(inertia.warmup_period(), 6);
137 for i in 0..5 {
138 assert_eq!(inertia.update(candle(10.0, 11.0, 9.0, 10.5, i)), None);
139 }
140 assert!(inertia.update(candle(10.0, 11.0, 9.0, 10.5, 5)).is_some());
141 }
142
143 #[test]
144 fn constant_rvi_yields_constant_inertia() {
145 let mut inertia = Inertia::new(3, 4).unwrap();
148 let mut last = None;
149 for i in 0..40 {
150 last = inertia.update(candle(10.0, 11.0, 9.0, 10.5, i));
151 }
152 let v = last.unwrap();
154 assert_relative_eq!(v, 0.25, epsilon = 1e-12);
155 }
156
157 #[test]
158 fn batch_equals_streaming() {
159 let candles: Vec<Candle> = (0..80_i64)
160 .map(|i| {
161 let o = 100.0 + (i as f64 * 0.3).sin() * 5.0;
162 let c = o + (i as f64 * 0.1).cos();
163 candle(o, o.max(c) + 0.5, o.min(c) - 0.5, c, i)
164 })
165 .collect();
166 let batch = Inertia::classic().batch(&candles);
167 let mut b = Inertia::classic();
168 let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
169 assert_eq!(batch, streamed);
170 }
171
172 #[test]
173 fn reset_clears_state() {
174 let mut inertia = Inertia::classic();
175 for i in 0..50 {
176 inertia.update(candle(10.0, 11.0, 9.0, 10.5, i));
177 }
178 assert!(inertia.is_ready());
179 inertia.reset();
180 assert!(!inertia.is_ready());
181 assert_eq!(inertia.update(candle(10.0, 11.0, 9.0, 10.5, 0)), None);
182 }
183}