wickra_core/indicators/
tsv.rs1use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9#[derive(Debug, Clone)]
43pub struct Tsv {
44 period: usize,
45 prev_close: Option<f64>,
46 window: VecDeque<f64>,
47 sum: f64,
48}
49
50impl Tsv {
51 pub fn new(period: usize) -> Result<Self> {
56 if period == 0 {
57 return Err(Error::PeriodZero);
58 }
59 if period > crate::error::MAX_PERIOD {
60 return Err(Error::InvalidPeriod {
61 message: crate::error::PERIOD_ABOVE_MAX,
62 });
63 }
64 Ok(Self {
65 period,
66 prev_close: None,
67 window: VecDeque::with_capacity(period),
68 sum: 0.0,
69 })
70 }
71
72 pub const fn period(&self) -> usize {
74 self.period
75 }
76}
77
78impl Indicator for Tsv {
79 type Input = Candle;
80 type Output = f64;
81
82 #[inline]
83 fn update(&mut self, candle: Candle) -> Option<f64> {
84 let Some(prev) = self.prev_close else {
85 self.prev_close = Some(candle.close);
86 return None;
87 };
88 let flow = (candle.close - prev) * candle.volume;
89 self.prev_close = Some(candle.close);
90
91 if self.window.len() == self.period {
92 self.sum -= self.window.pop_front().expect("non-empty");
93 }
94 self.window.push_back(flow);
95 self.sum += flow;
96 if self.window.len() < self.period {
97 return None;
98 }
99 Some(self.sum)
100 }
101
102 fn reset(&mut self) {
103 self.prev_close = None;
104 self.window.clear();
105 self.sum = 0.0;
106 }
107
108 #[inline]
109 fn warmup_period(&self) -> usize {
110 self.period + 1
112 }
113
114 #[inline]
115 fn is_ready(&self) -> bool {
116 self.window.len() == self.period
117 }
118
119 #[inline]
120 fn name(&self) -> &'static str {
121 "TSV"
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::traits::BatchExt;
129 use approx::assert_relative_eq;
130
131 fn c(close: f64, volume: f64, ts: i64) -> Candle {
132 Candle::new(close, close, close, close, volume, ts).unwrap()
133 }
134
135 #[test]
136 fn rejects_zero_period() {
137 assert!(matches!(Tsv::new(0), Err(Error::PeriodZero)));
138 }
139
140 #[test]
141 fn accessors_and_metadata() {
142 let t = Tsv::new(18).unwrap();
143 assert_eq!(t.period(), 18);
144 assert_eq!(t.name(), "TSV");
145 assert_eq!(t.warmup_period(), 19);
146 }
147
148 #[test]
149 fn constant_close_yields_zero() {
150 let candles: Vec<Candle> = (0..30).map(|i| c(10.0, 100.0, i)).collect();
152 let mut t = Tsv::new(5).unwrap();
153 for v in t.batch(&candles).into_iter().flatten() {
154 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
155 }
156 }
157
158 #[test]
159 fn reference_window_sum() {
160 let mut t = Tsv::new(3).unwrap();
170 let out = t.batch(&[
171 c(10.0, 50.0, 0),
172 c(11.0, 100.0, 1),
173 c(13.0, 200.0, 2),
174 c(12.0, 150.0, 3),
175 c(14.0, 50.0, 4),
176 c(15.0, 200.0, 5),
177 ]);
178 assert!(out[0].is_none() && out[1].is_none() && out[2].is_none());
179 assert_relative_eq!(out[3].unwrap(), 350.0, epsilon = 1e-9);
180 assert_relative_eq!(out[4].unwrap(), 350.0, epsilon = 1e-9);
181 assert_relative_eq!(out[5].unwrap(), 150.0, epsilon = 1e-9);
182 }
183
184 #[test]
185 fn batch_equals_streaming() {
186 let candles: Vec<Candle> = (0..80i64)
187 .map(|i| {
188 let f = i as f64;
189 c(
190 100.0 + (f * 0.3).sin() * 5.0,
191 50.0 + (i % 7) as f64 * 10.0,
192 i,
193 )
194 })
195 .collect();
196 let mut a = Tsv::new(18).unwrap();
197 let mut b = Tsv::new(18).unwrap();
198 assert_eq!(
199 a.batch(&candles),
200 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
201 );
202 }
203
204 #[test]
205 fn reset_clears_state() {
206 let candles: Vec<Candle> = (0..40).map(|i| c(10.0 + i as f64, 100.0, i)).collect();
207 let mut t = Tsv::new(10).unwrap();
208 t.batch(&candles);
209 assert!(t.is_ready());
210 t.reset();
211 assert!(!t.is_ready());
212 assert_eq!(t.update(candles[0]), None);
213 }
214}