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 Ok(Self {
60 period,
61 prev_close: None,
62 window: VecDeque::with_capacity(period),
63 sum: 0.0,
64 })
65 }
66
67 pub const fn period(&self) -> usize {
69 self.period
70 }
71}
72
73impl Indicator for Tsv {
74 type Input = Candle;
75 type Output = f64;
76
77 fn update(&mut self, candle: Candle) -> Option<f64> {
78 let Some(prev) = self.prev_close else {
79 self.prev_close = Some(candle.close);
80 return None;
81 };
82 let flow = (candle.close - prev) * candle.volume;
83 self.prev_close = Some(candle.close);
84
85 if self.window.len() == self.period {
86 self.sum -= self.window.pop_front().expect("non-empty");
87 }
88 self.window.push_back(flow);
89 self.sum += flow;
90 if self.window.len() < self.period {
91 return None;
92 }
93 Some(self.sum)
94 }
95
96 fn reset(&mut self) {
97 self.prev_close = None;
98 self.window.clear();
99 self.sum = 0.0;
100 }
101
102 fn warmup_period(&self) -> usize {
103 self.period + 1
105 }
106
107 fn is_ready(&self) -> bool {
108 self.window.len() == self.period
109 }
110
111 fn name(&self) -> &'static str {
112 "TSV"
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119 use crate::traits::BatchExt;
120 use approx::assert_relative_eq;
121
122 fn c(close: f64, volume: f64, ts: i64) -> Candle {
123 Candle::new(close, close, close, close, volume, ts).unwrap()
124 }
125
126 #[test]
127 fn rejects_zero_period() {
128 assert!(matches!(Tsv::new(0), Err(Error::PeriodZero)));
129 }
130
131 #[test]
132 fn accessors_and_metadata() {
133 let t = Tsv::new(18).unwrap();
134 assert_eq!(t.period(), 18);
135 assert_eq!(t.name(), "TSV");
136 assert_eq!(t.warmup_period(), 19);
137 }
138
139 #[test]
140 fn constant_close_yields_zero() {
141 let candles: Vec<Candle> = (0..30).map(|i| c(10.0, 100.0, i)).collect();
143 let mut t = Tsv::new(5).unwrap();
144 for v in t.batch(&candles).into_iter().flatten() {
145 assert_relative_eq!(v, 0.0, epsilon = 1e-12);
146 }
147 }
148
149 #[test]
150 fn reference_window_sum() {
151 let mut t = Tsv::new(3).unwrap();
161 let out = t.batch(&[
162 c(10.0, 50.0, 0),
163 c(11.0, 100.0, 1),
164 c(13.0, 200.0, 2),
165 c(12.0, 150.0, 3),
166 c(14.0, 50.0, 4),
167 c(15.0, 200.0, 5),
168 ]);
169 assert!(out[0].is_none() && out[1].is_none() && out[2].is_none());
170 assert_relative_eq!(out[3].unwrap(), 350.0, epsilon = 1e-9);
171 assert_relative_eq!(out[4].unwrap(), 350.0, epsilon = 1e-9);
172 assert_relative_eq!(out[5].unwrap(), 150.0, epsilon = 1e-9);
173 }
174
175 #[test]
176 fn batch_equals_streaming() {
177 let candles: Vec<Candle> = (0..80i64)
178 .map(|i| {
179 let f = i as f64;
180 c(
181 100.0 + (f * 0.3).sin() * 5.0,
182 50.0 + (i % 7) as f64 * 10.0,
183 i,
184 )
185 })
186 .collect();
187 let mut a = Tsv::new(18).unwrap();
188 let mut b = Tsv::new(18).unwrap();
189 assert_eq!(
190 a.batch(&candles),
191 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
192 );
193 }
194
195 #[test]
196 fn reset_clears_state() {
197 let candles: Vec<Candle> = (0..40).map(|i| c(10.0 + i as f64, 100.0, i)).collect();
198 let mut t = Tsv::new(10).unwrap();
199 t.batch(&candles);
200 assert!(t.is_ready());
201 t.reset();
202 assert!(!t.is_ready());
203 assert_eq!(t.update(candles[0]), None);
204 }
205}