wickra_core/indicators/
td_pressure.rs1#![allow(clippy::doc_markdown)]
2
3use std::collections::VecDeque;
27
28use crate::error::{Error, Result};
29use crate::ohlcv::Candle;
30use crate::traits::Indicator;
31
32#[derive(Debug, Clone)]
34pub struct TdPressure {
35 period: usize,
36 pressures: VecDeque<f64>,
37 volumes: VecDeque<f64>,
38 last_value: Option<f64>,
39}
40
41impl TdPressure {
42 pub fn new(period: usize) -> Result<Self> {
49 if period == 0 {
50 return Err(Error::PeriodZero);
51 }
52 Ok(Self {
53 period,
54 pressures: VecDeque::with_capacity(period),
55 volumes: VecDeque::with_capacity(period),
56 last_value: None,
57 })
58 }
59
60 pub const fn period(&self) -> usize {
62 self.period
63 }
64
65 pub const fn value(&self) -> Option<f64> {
67 self.last_value
68 }
69}
70
71impl Indicator for TdPressure {
72 type Input = Candle;
73 type Output = f64;
74
75 fn update(&mut self, candle: Candle) -> Option<f64> {
76 let range = candle.high - candle.low;
77 let bar_pressure = if range > 0.0 {
78 ((candle.close - candle.open) / range) * candle.volume
79 } else {
80 0.0
81 };
82
83 if self.pressures.len() == self.period {
84 self.pressures.pop_front();
85 self.volumes.pop_front();
86 }
87 self.pressures.push_back(bar_pressure);
88 self.volumes.push_back(candle.volume);
89 if self.pressures.len() < self.period {
90 return None;
91 }
92 let n = self.period as f64;
93 let mean_p: f64 = self.pressures.iter().sum::<f64>() / n;
94 let mean_v: f64 = self.volumes.iter().sum::<f64>() / n;
95 let v = if mean_v == 0.0 {
96 0.0
97 } else {
98 100.0 * mean_p / mean_v
99 };
100 self.last_value = Some(v);
101 Some(v)
102 }
103
104 fn reset(&mut self) {
105 self.pressures.clear();
106 self.volumes.clear();
107 self.last_value = None;
108 }
109
110 fn warmup_period(&self) -> usize {
111 self.period
112 }
113
114 fn is_ready(&self) -> bool {
115 self.last_value.is_some()
116 }
117
118 fn name(&self) -> &'static str {
119 "TDPressure"
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use crate::traits::BatchExt;
127 use approx::assert_relative_eq;
128
129 fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
130 Candle::new_unchecked(open, high, low, close, volume, ts)
131 }
132
133 #[test]
134 fn pure_bullish_candles_yield_full_positive_pressure() {
135 let candles: Vec<Candle> = (0..20)
139 .map(|i| c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)))
140 .collect();
141 let mut p = TdPressure::new(5).unwrap();
142 let last = p.batch(&candles).into_iter().flatten().last().unwrap();
143 assert_relative_eq!(last, 100.0, epsilon = 1e-12);
144 }
145
146 #[test]
147 fn pure_bearish_candles_yield_full_negative_pressure() {
148 let candles: Vec<Candle> = (0..20)
149 .map(|i| c(11.0, 11.0, 9.0, 9.0, 100.0, i64::from(i)))
150 .collect();
151 let mut p = TdPressure::new(5).unwrap();
152 let last = p.batch(&candles).into_iter().flatten().last().unwrap();
153 assert_relative_eq!(last, -100.0, epsilon = 1e-12);
154 }
155
156 #[test]
157 fn neutral_doji_close_eq_open_yields_zero() {
158 let candles: Vec<Candle> = (0..20)
159 .map(|i| c(10.0, 11.0, 9.0, 10.0, 100.0, i64::from(i)))
160 .collect();
161 let mut p = TdPressure::new(5).unwrap();
162 let last = p.batch(&candles).into_iter().flatten().last().unwrap();
163 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
164 }
165
166 #[test]
167 fn zero_range_bars_contribute_zero() {
168 let mut candles = Vec::new();
171 for i in 0..5 {
172 candles.push(c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)));
173 }
174 candles.push(c(10.0, 10.0, 10.0, 10.0, 0.0, 5));
176 for i in 6..11 {
177 candles.push(c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)));
178 }
179 let mut p = TdPressure::new(5).unwrap();
180 for v in p.batch(&candles).into_iter().flatten() {
181 assert!(v.is_finite(), "non-finite output: {v}");
182 assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
183 }
184 }
185
186 #[test]
187 fn flat_zero_volume_window_emits_zero() {
188 let candles: Vec<Candle> = (0..10)
189 .map(|i| c(10.0, 11.0, 9.0, 10.5, 0.0, i64::from(i)))
190 .collect();
191 let mut p = TdPressure::new(5).unwrap();
192 let last = p.batch(&candles).into_iter().flatten().last().unwrap();
195 assert_relative_eq!(last, 0.0, epsilon = 1e-12);
196 }
197
198 #[test]
199 fn batch_equals_streaming() {
200 let candles: Vec<Candle> = (0..60)
201 .map(|i| {
202 let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
203 c(m, m + 1.0, m - 1.0, m + 0.3, 100.0, i64::from(i))
204 })
205 .collect();
206 let mut a = TdPressure::new(5).unwrap();
207 let mut b = TdPressure::new(5).unwrap();
208 assert_eq!(
209 a.batch(&candles),
210 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
211 );
212 }
213
214 #[test]
215 fn rejects_zero_period() {
216 assert!(matches!(TdPressure::new(0), Err(Error::PeriodZero)));
217 }
218
219 #[test]
220 fn reset_clears_state() {
221 let candles: Vec<Candle> = (0..20)
222 .map(|i| c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)))
223 .collect();
224 let mut p = TdPressure::new(5).unwrap();
225 p.batch(&candles);
226 assert!(p.is_ready());
227 p.reset();
228 assert!(!p.is_ready());
229 assert_eq!(p.update(candles[0]), None);
230 assert_eq!(p.value(), None);
231 }
232
233 #[test]
234 fn accessors_and_metadata() {
235 let p = TdPressure::new(5).unwrap();
236 assert_eq!(p.period(), 5);
237 assert_eq!(p.warmup_period(), 5);
238 assert_eq!(p.name(), "TDPressure");
239 }
240}