Files
wickra/crates/wickra-core/src/indicators/cvd.rs
T
kingchencandGitHub 5867f71450 feat: trade-flow microstructure indicators (part 2 of 4) (#113)
* feat(core): add 3 trade-flow microstructure indicators

SignedVolume (per-trade size signed by aggressor), CumulativeVolumeDelta
(running signed-volume total), and TradeImbalance (rolling buy/sell volume
imbalance over a trade window). All consume the Trade type, with full unit
coverage. Extends the Microstructure family.

* feat(bindings): expose trade-flow microstructure indicators

Python, Node and WASM bindings for SignedVolume, CumulativeVolumeDelta and
TradeImbalance. Each takes a trade via update(price, size, is_buy); Python and
Node expose a batch over three parallel arrays, WASM exposes per-trade update.
Regenerates node index.d.ts/.js.

* test(bindings,fuzz,bench): cover trade-flow microstructure indicators

Python and Node: reference values, streaming-vs-batch, lifecycle/repr and input
validation (zero window, negative size, non-positive price, mismatched batch
lengths). New indicator_update_trade fuzz target. Synthetic trade-tape benches
(signed_volume cheapest, trade_imbalance windowed/expensive).

* docs: add trade-flow indicators + bump counter to 227

README Microstructure family row gains signed volume / CVD / trade imbalance and
the counter goes 224 -> 227; CHANGELOG records the trade-flow indicators.
2026-06-01 16:38:48 +02:00

128 lines
3.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Cumulative Volume Delta — running sum of signed trade volume.
use crate::microstructure::Trade;
use crate::traits::Indicator;
/// Cumulative Volume Delta (CVD) — the running sum of [signed volume].
///
/// ```text
/// CVDₜ = CVDₜ₋₁ + sizeₜ · (+1 if buy, 1 if sell)
/// ```
///
/// CVD is an unbounded running total: a rising line signals net buying pressure
/// over the session, a falling line net selling. Divergence between CVD and
/// price is a classic absorption / exhaustion signal. Call [`reset`] at the
/// start of each session to re-anchor the cumulative total at zero.
///
/// `Input = Trade`, `Output = f64`. Ready after the first trade.
///
/// [signed volume]: crate::SignedVolume
/// [`reset`]: crate::Indicator::reset
///
/// # Example
///
/// ```
/// use wickra_core::{CumulativeVolumeDelta, Indicator, Side, Trade};
///
/// let mut cvd = CumulativeVolumeDelta::new();
/// assert_eq!(cvd.update(Trade::new(100.0, 5.0, Side::Buy, 0).unwrap()), Some(5.0));
/// assert_eq!(cvd.update(Trade::new(100.0, 2.0, Side::Sell, 1).unwrap()), Some(3.0));
/// ```
#[derive(Debug, Clone, Default)]
pub struct CumulativeVolumeDelta {
cumulative: f64,
has_emitted: bool,
}
impl CumulativeVolumeDelta {
/// Construct a new CVD indicator with a zero running total.
pub const fn new() -> Self {
Self {
cumulative: 0.0,
has_emitted: false,
}
}
}
impl Indicator for CumulativeVolumeDelta {
type Input = Trade;
type Output = f64;
fn update(&mut self, trade: Trade) -> Option<f64> {
self.has_emitted = true;
self.cumulative += trade.size * trade.side.sign();
Some(self.cumulative)
}
fn reset(&mut self) {
self.cumulative = 0.0;
self.has_emitted = false;
}
fn warmup_period(&self) -> usize {
1
}
fn is_ready(&self) -> bool {
self.has_emitted
}
fn name(&self) -> &'static str {
"CumulativeVolumeDelta"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::microstructure::Side;
use crate::traits::BatchExt;
fn trade(size: f64, side: Side, ts: i64) -> Trade {
Trade::new(100.0, size, side, ts).unwrap()
}
#[test]
fn accessors_and_metadata() {
let cvd = CumulativeVolumeDelta::new();
assert_eq!(cvd.name(), "CumulativeVolumeDelta");
assert_eq!(cvd.warmup_period(), 1);
assert!(!cvd.is_ready());
}
#[test]
fn accumulates_signed_volume() {
let mut cvd = CumulativeVolumeDelta::new();
assert_eq!(cvd.update(trade(5.0, Side::Buy, 0)), Some(5.0));
assert_eq!(cvd.update(trade(2.0, Side::Sell, 1)), Some(3.0));
assert_eq!(cvd.update(trade(4.0, Side::Sell, 2)), Some(-1.0));
assert!(cvd.is_ready());
}
#[test]
fn batch_equals_streaming() {
let trades: Vec<Trade> = (0..20)
.map(|i| {
let side = if i % 3 == 0 { Side::Sell } else { Side::Buy };
trade(1.0 + (i % 4) as f64, side, i)
})
.collect();
let mut a = CumulativeVolumeDelta::new();
let mut b = CumulativeVolumeDelta::new();
assert_eq!(
a.batch(&trades),
trades.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn reset_re_anchors_at_zero() {
let mut cvd = CumulativeVolumeDelta::new();
cvd.update(trade(5.0, Side::Buy, 0));
cvd.reset();
assert!(!cvd.is_ready());
// After reset the running total starts again from zero.
assert_eq!(cvd.update(trade(2.0, Side::Buy, 1)), Some(2.0));
}
}