Skip to main content

wickra_core/indicators/
tsv.rs

1//! Time Segmented Volume (Worden).
2
3use std::collections::VecDeque;
4
5use crate::error::{Error, Result};
6use crate::ohlcv::Candle;
7use crate::traits::Indicator;
8
9/// Time Segmented Volume (Don Worden) — a rolling sum of *signed* volume
10/// weighted by the bar's close-to-close move.
11///
12/// Each bar's contribution is the close change times the bar volume. Summed
13/// over a fixed window, the result quantifies the net accumulation (positive)
14/// or distribution (negative) over that span:
15///
16/// ```text
17/// flow_t = (close_t − close_{t−1}) · volume_t          (signed money flow)
18/// TSV_t  = Σ_{i = t−period+1}^{t} flow_i               (rolling window sum)
19/// ```
20///
21/// The first candle only seeds `close_{t−1}`; the first flow lands at bar 2,
22/// and the first TSV emission lands once the window has accumulated `period`
23/// flows — i.e. at bar `period + 1`. Worden's original TC2000 implementation
24/// often charts an additional EMA smoothing of TSV as a signal line; that is
25/// left to the caller via [`crate::Ema`] composition.
26///
27/// # Example
28///
29/// ```
30/// use wickra_core::{Candle, Indicator, Tsv};
31///
32/// let mut indicator = Tsv::new(18).unwrap();
33/// let mut last = None;
34/// for i in 0..80 {
35///     let base = 100.0 + f64::from(i);
36///     let candle =
37///         Candle::new(base, base + 2.0, base - 2.0, base + 1.0, 10.0, i64::from(i)).unwrap();
38///     last = indicator.update(candle);
39/// }
40/// assert!(last.is_some());
41/// ```
42#[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    /// Construct a new TSV with the given rolling window length.
52    ///
53    /// # Errors
54    /// Returns [`Error::PeriodZero`] if `period == 0`.
55    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    /// Configured window length.
68    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        // One seed bar for `prev_close`, then `period` flows to fill the window.
104        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        // Flat close -> every flow is zero -> rolling sum stays at zero.
142        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        // closes  = [10, 11, 13, 12, 14, 15]
152        // volumes = [.., 100, 200, 150, 50, 200]
153        // flows   = [None, (1)*100=100, (2)*200=400, (-1)*150=-150, (2)*50=100, (1)*200=200]
154        // period = 3: first emission at bar index 3 (the 4th flow, since one bar seeds).
155        // Wait: bar 0 seeds, bars 1..5 produce 5 flows. Window of 3 fills at the
156        // 3rd flow, i.e. bar index 3.
157        //   bar 3 -> window = [100, 400, -150] -> sum = 350.
158        //   bar 4 -> window = [400, -150, 100] -> sum = 350.
159        //   bar 5 -> window = [-150, 100, 200] -> sum = 150.
160        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}