Skip to content

A/D Oscillator (Williams) ​

Larry Williams' volume-less Accumulation/Distribution line measured against its own 13-bar simple moving average, so it oscillates around zero instead of drifting like the cumulative line. Positive when accumulation is running ahead of its recent average, negative when distribution is. This is not TA-Lib's ADOSC — that is the Chaikin A/D Oscillator, which Wickra ships as ChaikinOscillator.

Quick reference ​

FieldValue
FamilyVolume
Input typeCandle (uses high, low, close)
Output typef64 — zero-centred oscillator
Output rangeunbounded but mean-reverting around 0
Default parametersnone — AdOscillator::new() (fixed 13-bar signal)
Warmup period14
InterpretationWilliams A/D line minus its 13-bar SMA
Namename() == "WilliamsAdOscillator"; binding class AdOscillator

Formula ​

text
TR_h_t  = max(close_{t−1}, high_t)
TR_l_t  = min(close_{t−1}, low_t)
WAD_t   = WAD_{t−1} + (close_t − TR_l_t)   if close_t > close_{t−1}   (accumulation)
WAD_t   = WAD_{t−1} + (close_t − TR_h_t)   if close_t < close_{t−1}   (distribution)
WAD_t   = WAD_{t−1}                          if close_t == close_{t−1}
WADOSC_t = WAD_t − SMA(WAD, 13)_t

The underlying line is Larry Williams' volume-less A/D (1972): it anchors the move on the prior close (a true high/low, the same idea behind true range) and sums the directional component. The oscillator subtracts the line's own 13-bar simple moving average, which removes the drift and centres the series on zero — so it reads as a momentum/mean-reversion signal rather than a cumulative level.

TA-Lib's ADOSC is a different indicator: the Chaikin A/D Oscillator, EMA(ADL, 3) − EMA(ADL, 10) on the volume-weighted A/D line. If you are porting TA-Lib code that calls ADOSC, use ChaikinOscillator, not this indicator.

See crates/wickra-core/src/indicators/ad_oscillator.rs (the 13-bar signal constant SIGNAL_PERIOD lives at ad_oscillator.rs:8; the update at ad_oscillator.rs:86).

Parameters ​

NameTypeDefaultValid rangeSourceDescription
(none)———ad_oscillator.rs:8, :66AdOscillator::new() takes no arguments; the signal length is fixed at the classic 13 bars (SIGNAL_PERIOD).

Inputs / Outputs ​

From crates/wickra-core/src/indicators/ad_oscillator.rs:

rust
use wickra::{AdOscillator, Candle, Indicator};
// AdOscillator: Input = Candle, Output = f64
const _: fn(&mut AdOscillator, Candle) -> Option<f64> = <AdOscillator as Indicator>::update;

name() returns "WilliamsAdOscillator". Every binding (Python, Node, WASM, C, Go, Java, C#, R) exposes the class as AdOscillator — earlier releases used ADOSC in Python, Node and WASM, which was removed because TA-Lib's ADOSC is the Chaikin oscillator. Python streams as float | None and batches AdOscillator().batch(high, low, close) to a 1-D array.array('d') (NaN for warmup). Node streams as number | null via update(high, low, close) and batches batch(high, low, close) with NaN placeholders.

Warmup ​

AdOscillator::new().warmup_period() == 14. The first bar only seeds the previous-close anchor and returns None (unit test seed_bar_returns_none); the A/D line then feeds the 13-bar signal SMA, so the first oscillator value lands at input index 13 (the 14th bar). The unit tests accessors_and_metadata (warmup_period() == 14) and warmup_emits_at_warmup_period (first 13 outputs None, out[13] is Some) pin this.

Edge cases ​

  • Flat market. A market that never accumulates or distributes leaves the line constant, so the oscillator sits at exactly 0.0 once warm. The unit test flat_market_oscillates_at_zero pins this.
  • Identity vs the raw line. The oscillator equals the standalone Wad line passed through line − SMA(line, 13), bar for bar — pinned by equals_wad_line_minus_its_sma.
  • Seed bar. The very first candle returns None (it only records the prior close); pinned by seed_bar_returns_none.
  • Reset. reset() clears the prior close, the accumulator and the signal SMA; the next update restarts the warmup. Pinned by reset_clears_state.

Examples ​

Rust ​

rust
use wickra::{AdOscillator, BatchExt, Candle, Indicator};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // A flat market never accumulates or distributes: the line is constant, so
    // the oscillator is exactly zero once the 13-bar signal SMA has filled.
    let candles: Vec<Candle> = (0..16)
        .map(|i| Candle::new(50.0, 50.0, 50.0, 50.0, 100.0, i).unwrap())
        .collect();
    let mut adosc = AdOscillator::new();
    let out = adosc.batch(&candles);
    println!("warmup_period = {}", adosc.warmup_period());
    println!("out[12] = {:?}, out[13] = {:?}", out[12], out[13]);
    Ok(())
}

Output:

warmup_period = 14
out[12] = None, out[13] = Some(0.0)

out[0..13] are all None (warmup); from index 13 the flat series yields Some(0.0), matching the warmup_emits_at_warmup_period and flat_market_oscillates_at_zero unit tests.

Python ​

python
import numpy as np
import wickra as ta

# Flat market -> oscillator is zero once the 13-bar signal SMA fills.
flat = np.full(16, 50.0)
adosc = ta.AdOscillator()
out = adosc.batch(flat, flat, flat)  # high, low, close
print('warmup:', adosc.warmup_period())
print('out[13]:', out[13])

Output:

warmup: 14
out[13]: 0.0

Node ​

javascript
const wickra = require('wickra');

const flat = Array.from({ length: 16 }, () => 50.0);
const adosc = new wickra.AdOscillator();
const out = adosc.batch(flat, flat, flat); // high, low, close
console.log('out[13]:', out[13]);

Output:

out[13]: 0

Streaming ​

rust
use wickra::{AdOscillator, Candle, Indicator};

let mut adosc = AdOscillator::new();
let feed: Vec<Candle> = Vec::new(); // your live OHLCV candle feed
for bar in feed {
    if let Some(osc) = adosc.update(bar) {
        // osc > 0: accumulation is running ahead of its 13-bar average
        // osc < 0: distribution is. Watch zero-line crossings and divergences.
        let _ = osc;
    }
}

Interpretation ​

  1. Zero line. Because the signal SMA is subtracted, the oscillator is mean-reverting around zero. Crossings up through zero flag fresh accumulation; crossings down flag distribution.
  2. Volume-free. Like the underlying Williams A/D line, this uses only high/low/close — useful for instruments where volume is poor or absent, where the Chaikin-style Adl (volume-weighted) is not reliable.
  3. Divergence. Price makes a new high while the oscillator makes a lower high → weakening accumulation, a classic divergence warning.
  4. Vs the cumulative line. Use Wad (the raw drifting line) for long-horizon divergence against price; use this oscillator for a bounded, zero-centred read of the same flow.

Common pitfalls ​

  • Confusing it with the raw Wad line. Wad drifts without bound; AdOscillator is Wad − SMA(Wad, 13) and oscillates around zero. They answer different questions.
  • Confusing it with Adl. Adl is the volume-weighted Chaikin A/D line; this oscillator is volume-less (Williams').
  • Treating it as TA-Lib ADOSC. TA-Lib's ADOSC is the Chaikin A/D Oscillator (EMA(ADL, 3) − EMA(ADL, 10)), available in Wickra as ChaikinOscillator. Its values have nothing in common with this Williams variant.
  • Reading the absolute level. Only the sign, zero-line crossings and divergences carry signal — not the magnitude.

References ​

  • Larry Williams, How I Made One Million Dollars Last Year Trading Commodities (1973) — the volume-less Accumulation/Distribution line.
  • TA-Lib ADOSC — for reference only: it is the Chaikin A/D Oscillator (see ChaikinOscillator), not this indicator.

See also ​