Deepens the **Volume** family (B8) with seven indicators (440 -> 447):
- **VolumeRsi** — Wilder RSI computed on signed volume flow.
- **WilliamsAd** — Williams Accumulation/Distribution cumulative line (distinct from Chaikin A/D).
- **TwiggsMoneyFlow** — true-range volume accumulation with Wilder smoothing (distinct from CMF).
- **TradeVolumeIndex** — tick-direction volume accumulation past a min-tick threshold (distinct from TSV).
- **IntradayIntensity** — volume weighted by close position within the bar range.
- **BetterVolume** — VSA volume-vs-spread effort/result classifier.
- **VolumeWeightedMacd** — MACD computed on VWMA with signal line and histogram (struct output).
("Up/Down Volume Ratio" already ships from A2.) All Candle input; the six scalar stops emit f64, VolumeWeightedMacd a {macd, signal, histogram} struct. Hand-written Python/Node/WASM bindings for the volume signature. Verified locally: 3620 core lib + 405 doc tests, clippy clean, 522 node tests, 865 pytest, counter 447.
207 lines
6.2 KiB
Rust
207 lines
6.2 KiB
Rust
//! Williams Accumulation/Distribution (WAD) — Larry Williams' cumulative line.
|
||
|
||
use crate::ohlcv::Candle;
|
||
use crate::traits::Indicator;
|
||
|
||
/// Williams Accumulation/Distribution — a cumulative price-only line that adds
|
||
/// the day's accumulation on up-closes and subtracts the day's distribution on
|
||
/// down-closes.
|
||
///
|
||
/// ```text
|
||
/// if close > prev_close: AD = close − min(low, prev_close) (true low)
|
||
/// if close < prev_close: AD = close − max(high, prev_close) (true high)
|
||
/// if close = prev_close: AD = 0
|
||
/// WAD_t = WAD_{t−1} + AD
|
||
/// ```
|
||
///
|
||
/// Larry Williams' A/D line (distinct from Chaikin's volume-based
|
||
/// [`Adl`](crate::Adl)) uses **no volume at all** — it measures accumulation as
|
||
/// how far price closed above the *true low* on up-days and distribution as how
|
||
/// far it closed below the *true high* on down-days, then accumulates the result.
|
||
/// A rising WAD that diverges from a flat or falling price is the classic
|
||
/// accumulation signal; a falling WAD under a rising price warns of distribution.
|
||
///
|
||
/// The line is unbounded and its absolute level is meaningless — only its slope
|
||
/// and divergences against price matter. The first candle has no previous close,
|
||
/// so it seeds the reference and emits nothing; thereafter every bar emits the
|
||
/// running total. Each `update` is O(1).
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```
|
||
/// use wickra_core::{Candle, Indicator, Wad};
|
||
///
|
||
/// let mut indicator = Wad::new();
|
||
/// let mut last = None;
|
||
/// for i in 0..20 {
|
||
/// let base = 100.0 + f64::from(i);
|
||
/// let c = Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1_000.0, 0).unwrap();
|
||
/// last = indicator.update(c);
|
||
/// }
|
||
/// assert!(last.is_some());
|
||
/// ```
|
||
#[derive(Debug, Clone, Default)]
|
||
pub struct Wad {
|
||
prev_close: Option<f64>,
|
||
line: f64,
|
||
last: Option<f64>,
|
||
}
|
||
|
||
impl Wad {
|
||
/// Construct a new Williams A/D line. The line is parameter-free.
|
||
#[must_use]
|
||
pub fn new() -> Self {
|
||
Self::default()
|
||
}
|
||
|
||
/// Current value if available.
|
||
pub const fn value(&self) -> Option<f64> {
|
||
self.last
|
||
}
|
||
}
|
||
|
||
impl Indicator for Wad {
|
||
type Input = Candle;
|
||
type Output = f64;
|
||
|
||
fn update(&mut self, candle: Candle) -> Option<f64> {
|
||
let Some(prev_close) = self.prev_close else {
|
||
self.prev_close = Some(candle.close);
|
||
return None;
|
||
};
|
||
let ad = if candle.close > prev_close {
|
||
candle.close - candle.low.min(prev_close)
|
||
} else if candle.close < prev_close {
|
||
candle.close - candle.high.max(prev_close)
|
||
} else {
|
||
0.0
|
||
};
|
||
self.line += ad;
|
||
self.prev_close = Some(candle.close);
|
||
self.last = Some(self.line);
|
||
Some(self.line)
|
||
}
|
||
|
||
fn reset(&mut self) {
|
||
self.prev_close = None;
|
||
self.line = 0.0;
|
||
self.last = None;
|
||
}
|
||
|
||
fn warmup_period(&self) -> usize {
|
||
// The first bar only seeds the reference close; the first value lands on
|
||
// the second bar.
|
||
2
|
||
}
|
||
|
||
fn is_ready(&self) -> bool {
|
||
self.last.is_some()
|
||
}
|
||
|
||
fn name(&self) -> &'static str {
|
||
"Wad"
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::traits::BatchExt;
|
||
use approx::assert_relative_eq;
|
||
|
||
fn candle(high: f64, low: f64, close: f64) -> Candle {
|
||
Candle::new_unchecked(low, high, low, close, 1_000.0, 0)
|
||
}
|
||
|
||
#[test]
|
||
fn accessors_and_metadata() {
|
||
let wad = Wad::new();
|
||
assert_eq!(wad.warmup_period(), 2);
|
||
assert_eq!(wad.name(), "Wad");
|
||
assert!(!wad.is_ready());
|
||
assert_eq!(wad.value(), None);
|
||
}
|
||
|
||
#[test]
|
||
fn first_bar_seeds_without_output() {
|
||
let mut wad = Wad::new();
|
||
assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
|
||
assert!(wad.update(candle(102.0, 100.0, 101.0)).is_some());
|
||
}
|
||
|
||
#[test]
|
||
fn up_close_accumulates() {
|
||
// close rises from 100 -> 101; true low = min(low, prev_close) = min(100,100)=100;
|
||
// AD = 101 - 100 = 1.
|
||
let mut wad = Wad::new();
|
||
wad.update(candle(101.0, 99.0, 100.0));
|
||
let v = wad.update(candle(102.0, 100.0, 101.0)).unwrap();
|
||
assert_relative_eq!(v, 1.0, epsilon = 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn down_close_distributes() {
|
||
// close falls 100 -> 99; true high = max(high, prev_close) = max(101,100)=101;
|
||
// AD = 99 - 101 = -2.
|
||
let mut wad = Wad::new();
|
||
wad.update(candle(102.0, 100.0, 100.0));
|
||
let v = wad.update(candle(101.0, 98.0, 99.0)).unwrap();
|
||
assert_relative_eq!(v, -2.0, epsilon = 1e-9);
|
||
}
|
||
|
||
#[test]
|
||
fn unchanged_close_adds_nothing() {
|
||
let mut wad = Wad::new();
|
||
wad.update(candle(101.0, 99.0, 100.0));
|
||
let v = wad.update(candle(105.0, 95.0, 100.0)).unwrap();
|
||
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
|
||
}
|
||
|
||
#[test]
|
||
fn pure_uptrend_is_monotone() {
|
||
let mut wad = Wad::new();
|
||
let candles: Vec<Candle> = (0..30)
|
||
.map(|i| {
|
||
let base = 100.0 + f64::from(i);
|
||
candle(base + 1.0, base - 1.0, base)
|
||
})
|
||
.collect();
|
||
let mut prev = f64::NEG_INFINITY;
|
||
for v in wad.batch(&candles).into_iter().flatten() {
|
||
assert!(v >= prev, "WAD must rise in an uptrend");
|
||
prev = v;
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn reset_clears_state() {
|
||
let mut wad = Wad::new();
|
||
let candles: Vec<Candle> = (0..10)
|
||
.map(|i| {
|
||
let base = 100.0 + f64::from(i);
|
||
candle(base + 1.0, base - 1.0, base)
|
||
})
|
||
.collect();
|
||
wad.batch(&candles);
|
||
assert!(wad.is_ready());
|
||
wad.reset();
|
||
assert!(!wad.is_ready());
|
||
assert_eq!(wad.value(), None);
|
||
assert_eq!(wad.update(candle(101.0, 99.0, 100.0)), None);
|
||
}
|
||
|
||
#[test]
|
||
fn batch_equals_streaming() {
|
||
let candles: Vec<Candle> = (0..80)
|
||
.map(|i| {
|
||
let base = 100.0 + (f64::from(i) * 0.3).sin() * 8.0;
|
||
candle(base + 2.0, base - 2.0, base + 0.5)
|
||
})
|
||
.collect();
|
||
let batch = Wad::new().batch(&candles);
|
||
let mut b = Wad::new();
|
||
let streamed: Vec<_> = candles.iter().map(|c| b.update(*c)).collect();
|
||
assert_eq!(batch, streamed);
|
||
}
|
||
}
|