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