Skip to main content

wickra_core/indicators/
signed_volume.rs

1//! Signed Volume โ€” per-trade volume signed by aggressor side.
2
3use crate::microstructure::Trade;
4use crate::traits::Indicator;
5
6/// Signed Volume โ€” the size of each trade signed by its aggressor side.
7///
8/// ```text
9/// signedVolume = size ยท (+1 if buy, โˆ’1 if sell)
10/// ```
11///
12/// A positive value is buyer-initiated flow, a negative value seller-initiated.
13/// It is the per-trade building block of [`crate::CumulativeVolumeDelta`] and
14/// trade-flow imbalance.
15///
16/// `Input = Trade`, `Output = f64`. Stateless; ready after the first trade.
17///
18/// # Example
19///
20/// ```
21/// use wickra_core::{Indicator, SignedVolume, Side, Trade};
22///
23/// let mut sv = SignedVolume::new();
24/// let buy = Trade::new(100.0, 2.0, Side::Buy, 0).unwrap();
25/// assert_eq!(sv.update(buy), Some(2.0));
26/// let sell = Trade::new(100.0, 3.0, Side::Sell, 1).unwrap();
27/// assert_eq!(sv.update(sell), Some(-3.0));
28/// ```
29#[derive(Debug, Clone, Default)]
30pub struct SignedVolume {
31    has_emitted: bool,
32}
33
34impl SignedVolume {
35    /// Construct a new signed-volume indicator.
36    pub const fn new() -> Self {
37        Self { has_emitted: false }
38    }
39}
40
41impl Indicator for SignedVolume {
42    type Input = Trade;
43    type Output = f64;
44
45    #[inline]
46    fn update(&mut self, trade: Trade) -> Option<f64> {
47        self.has_emitted = true;
48        Some(trade.size * trade.side.sign())
49    }
50
51    fn reset(&mut self) {
52        self.has_emitted = false;
53    }
54
55    #[inline]
56    fn warmup_period(&self) -> usize {
57        1
58    }
59
60    #[inline]
61    fn is_ready(&self) -> bool {
62        self.has_emitted
63    }
64
65    #[inline]
66    fn name(&self) -> &'static str {
67        "SignedVolume"
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74    use crate::microstructure::Side;
75    use crate::traits::BatchExt;
76
77    fn trade(size: f64, side: Side, ts: i64) -> Trade {
78        Trade::new(100.0, size, side, ts).unwrap()
79    }
80
81    #[test]
82    fn accessors_and_metadata() {
83        let sv = SignedVolume::new();
84        assert_eq!(sv.name(), "SignedVolume");
85        assert_eq!(sv.warmup_period(), 1);
86        assert!(!sv.is_ready());
87    }
88
89    #[test]
90    fn buy_is_positive() {
91        let mut sv = SignedVolume::new();
92        assert_eq!(sv.update(trade(2.0, Side::Buy, 0)), Some(2.0));
93        assert!(sv.is_ready());
94    }
95
96    #[test]
97    fn sell_is_negative() {
98        let mut sv = SignedVolume::new();
99        assert_eq!(sv.update(trade(3.0, Side::Sell, 0)), Some(-3.0));
100    }
101
102    #[test]
103    fn zero_size_is_zero() {
104        let mut sv = SignedVolume::new();
105        assert_eq!(sv.update(trade(0.0, Side::Buy, 0)), Some(0.0));
106    }
107
108    #[test]
109    fn batch_equals_streaming() {
110        let trades: Vec<Trade> = (0..20)
111            .map(|i| {
112                let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
113                trade(1.0 + (i % 4) as f64, side, i)
114            })
115            .collect();
116        let mut a = SignedVolume::new();
117        let mut b = SignedVolume::new();
118        assert_eq!(
119            a.batch(&trades),
120            trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
121        );
122    }
123
124    #[test]
125    fn reset_clears_state() {
126        let mut sv = SignedVolume::new();
127        sv.update(trade(1.0, Side::Buy, 0));
128        assert!(sv.is_ready());
129        sv.reset();
130        assert!(!sv.is_ready());
131    }
132}