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    fn update(&mut self, trade: Trade) -> Option<f64> {
46        self.has_emitted = true;
47        Some(trade.size * trade.side.sign())
48    }
49
50    fn reset(&mut self) {
51        self.has_emitted = false;
52    }
53
54    fn warmup_period(&self) -> usize {
55        1
56    }
57
58    fn is_ready(&self) -> bool {
59        self.has_emitted
60    }
61
62    fn name(&self) -> &'static str {
63        "SignedVolume"
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70    use crate::microstructure::Side;
71    use crate::traits::BatchExt;
72
73    fn trade(size: f64, side: Side, ts: i64) -> Trade {
74        Trade::new(100.0, size, side, ts).unwrap()
75    }
76
77    #[test]
78    fn accessors_and_metadata() {
79        let sv = SignedVolume::new();
80        assert_eq!(sv.name(), "SignedVolume");
81        assert_eq!(sv.warmup_period(), 1);
82        assert!(!sv.is_ready());
83    }
84
85    #[test]
86    fn buy_is_positive() {
87        let mut sv = SignedVolume::new();
88        assert_eq!(sv.update(trade(2.0, Side::Buy, 0)), Some(2.0));
89        assert!(sv.is_ready());
90    }
91
92    #[test]
93    fn sell_is_negative() {
94        let mut sv = SignedVolume::new();
95        assert_eq!(sv.update(trade(3.0, Side::Sell, 0)), Some(-3.0));
96    }
97
98    #[test]
99    fn zero_size_is_zero() {
100        let mut sv = SignedVolume::new();
101        assert_eq!(sv.update(trade(0.0, Side::Buy, 0)), Some(0.0));
102    }
103
104    #[test]
105    fn batch_equals_streaming() {
106        let trades: Vec<Trade> = (0..20)
107            .map(|i| {
108                let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
109                trade(1.0 + (i % 4) as f64, side, i)
110            })
111            .collect();
112        let mut a = SignedVolume::new();
113        let mut b = SignedVolume::new();
114        assert_eq!(
115            a.batch(&trades),
116            trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
117        );
118    }
119
120    #[test]
121    fn reset_clears_state() {
122        let mut sv = SignedVolume::new();
123        sv.update(trade(1.0, Side::Buy, 0));
124        assert!(sv.is_ready());
125        sv.reset();
126        assert!(!sv.is_ready());
127    }
128}