#![allow(clippy::doc_markdown)]
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone)]
pub struct TdPressure {
period: usize,
pressures: VecDeque<f64>,
volumes: VecDeque<f64>,
last_value: Option<f64>,
}
impl TdPressure {
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
pressures: VecDeque::with_capacity(period),
volumes: VecDeque::with_capacity(period),
last_value: None,
})
}
pub const fn period(&self) -> usize {
self.period
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdPressure {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let range = candle.high - candle.low;
let bar_pressure = if range > 0.0 {
((candle.close - candle.open) / range) * candle.volume
} else {
0.0
};
if self.pressures.len() == self.period {
self.pressures.pop_front();
self.volumes.pop_front();
}
self.pressures.push_back(bar_pressure);
self.volumes.push_back(candle.volume);
if self.pressures.len() < self.period {
return None;
}
let n = self.period as f64;
let mean_p: f64 = self.pressures.iter().sum::<f64>() / n;
let mean_v: f64 = self.volumes.iter().sum::<f64>() / n;
let v = if mean_v == 0.0 {
0.0
} else {
100.0 * mean_p / mean_v
};
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.pressures.clear();
self.volumes.clear();
self.last_value = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDPressure"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(open: f64, high: f64, low: f64, close: f64, volume: f64, ts: i64) -> Candle {
Candle::new_unchecked(open, high, low, close, volume, ts)
}
#[test]
fn pure_bullish_candles_yield_full_positive_pressure() {
let candles: Vec<Candle> = (0..20)
.map(|i| c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)))
.collect();
let mut p = TdPressure::new(5).unwrap();
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 100.0, epsilon = 1e-12);
}
#[test]
fn pure_bearish_candles_yield_full_negative_pressure() {
let candles: Vec<Candle> = (0..20)
.map(|i| c(11.0, 11.0, 9.0, 9.0, 100.0, i64::from(i)))
.collect();
let mut p = TdPressure::new(5).unwrap();
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, -100.0, epsilon = 1e-12);
}
#[test]
fn neutral_doji_close_eq_open_yields_zero() {
let candles: Vec<Candle> = (0..20)
.map(|i| c(10.0, 11.0, 9.0, 10.0, 100.0, i64::from(i)))
.collect();
let mut p = TdPressure::new(5).unwrap();
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn zero_range_bars_contribute_zero() {
let mut candles = Vec::new();
for i in 0..5 {
candles.push(c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)));
}
candles.push(c(10.0, 10.0, 10.0, 10.0, 0.0, 5));
for i in 6..11 {
candles.push(c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)));
}
let mut p = TdPressure::new(5).unwrap();
for v in p.batch(&candles).into_iter().flatten() {
assert!(v.is_finite(), "non-finite output: {v}");
assert!((-100.0..=100.0).contains(&v), "out of range: {v}");
}
}
#[test]
fn flat_zero_volume_window_emits_zero() {
let candles: Vec<Candle> = (0..10)
.map(|i| c(10.0, 11.0, 9.0, 10.5, 0.0, i64::from(i)))
.collect();
let mut p = TdPressure::new(5).unwrap();
let last = p.batch(&candles).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..60)
.map(|i| {
let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
c(m, m + 1.0, m - 1.0, m + 0.3, 100.0, i64::from(i))
})
.collect();
let mut a = TdPressure::new(5).unwrap();
let mut b = TdPressure::new(5).unwrap();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn rejects_zero_period() {
assert!(matches!(TdPressure::new(0), Err(Error::PeriodZero)));
}
#[test]
fn reset_clears_state() {
let candles: Vec<Candle> = (0..20)
.map(|i| c(9.0, 11.0, 9.0, 11.0, 100.0, i64::from(i)))
.collect();
let mut p = TdPressure::new(5).unwrap();
p.batch(&candles);
assert!(p.is_ready());
p.reset();
assert!(!p.is_ready());
assert_eq!(p.update(candles[0]), None);
assert_eq!(p.value(), None);
}
#[test]
fn accessors_and_metadata() {
let p = TdPressure::new(5).unwrap();
assert_eq!(p.period(), 5);
assert_eq!(p.warmup_period(), 5);
assert_eq!(p.name(), "TDPressure");
}
}