E4: commit the documentation sources

The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.

The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
This commit is contained in:
kingchenc
2026-05-22 16:18:04 +02:00
parent 94cab88278
commit 278b6afaa4
33 changed files with 6635 additions and 0 deletions
@@ -0,0 +1,190 @@
# OBV (On-Balance Volume)
> A cumulative signed-volume series: each candle adds its volume on an up
> close, subtracts on a down close, and leaves the running total unchanged
> on a flat close. The shape of the OBV curve, not its absolute level, is
> what carries information.
## Quick reference
| Item | Value |
|---------------------|--------------------------------------------------------------|
| Family | Volume |
| Sub-category | cumulative |
| Input type | `Candle` (uses `close` and `volume`) |
| Output type | `f64` |
| Output range | unbounded (signed, integer-of-volume in spirit) |
| Default parameters | none |
| Warmup period | `1` |
| Interpretation | divergence vs price signals accumulation / distribution |
## Formula
For each candle `t > 0` (after the seed):
```
if close_t > close_{t-1}: OBV_t = OBV_{t-1} + volume_t
if close_t < close_{t-1}: OBV_t = OBV_{t-1} - volume_t
if close_t == close_{t-1}: OBV_t = OBV_{t-1}
```
The first candle initialises the running total to `0.0` and emits
that value (`crates/wickra-core/src/indicators/obv.rs:42-55`).
## Parameters
`Obv::new()` takes no parameters. Python: `wickra.OBV()`. Node:
`new w.OBV()`.
## Inputs / Outputs
```rust
impl Indicator for Obv {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { 1 }
}
```
- **Python streaming.** Accepts a 6-tuple or dict candle; returns
`float | None`.
- **Python batch.** `OBV.batch(close, volume)` takes two equal-length
1-D `numpy.ndarray` columns and returns a 1-D `np.ndarray`. The
first value is `0.0`, never `NaN`.
- **Node streaming.** Not exposed; the Node binding ships only
`batch` for `OBV`.
- **Node batch.** `obv.batch(close, volume)` returns `Array<number>`
of the same length.
## Warmup
`warmup_period() == 1`. The very first candle emits `0.0` by
convention (the "baseline" — there is no prior close to compare
against, so the indicator starts the running total at zero). Every
subsequent candle emits the updated cumulative total.
## Edge cases
- **First bar.** Always emits `0.0` (pinned test
`first_candle_baseline_zero`). This is the canonical OBV convention
used by Granville's original formulation.
- **Equal closes.** A candle with `close_t == close_{t-1}` does not
change the running total — the volume is discarded. (`obv.rs:46-50`).
- **Down close.** Subtracts the bar's volume, so OBV can go strongly
negative on a sustained downtrend; that is expected and meaningful.
- **Zero volume.** A zero-volume bar adds or subtracts `0`, so OBV
is unchanged regardless of close direction.
- **NaN / infinity.** `Candle::new` rejects non-finite OHLCV values
before they reach OBV.
- **Reset.** `reset()` zeroes the running total and clears the
`has_emitted` / `prev_close` state.
## Examples
### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, Obv};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?, // baseline -> 0
Candle::new(10.0, 11.0, 10.0, 11.0, 20.0, 0)?, // up -> +20
Candle::new(11.0, 11.0, 10.5, 10.5, 30.0, 0)?, // down -> -30
Candle::new(10.5, 10.5, 10.5, 10.5, 40.0, 0)?, // flat -> 0
Candle::new(10.5, 12.0, 10.5, 12.0, 10.0, 0)?, // up -> +10
];
let mut obv = Obv::new();
println!("{:?}", obv.batch(&candles));
Ok(())
}
```
Output:
```
[Some(0.0), Some(20.0), Some(-10.0), Some(-10.0), Some(0.0)]
```
Hand check: baseline `0`, then `0 + 20 = 20`, then `20 - 30 = -10`,
then `-10` (flat close discards the 40), then `-10 + 10 = 0`.
### Python
```python
import numpy as np
import wickra as ta
obv = ta.OBV()
c = np.array([10.0, 11.0, 10.5, 10.5, 12.0])
v = np.array([100.0, 20.0, 30.0, 40.0, 10.0])
print(obv.batch(c, v))
```
Output:
```
[ 0. 20. -10. -10. 0.]
```
### Node
```js
const w = require('wickra');
const obv = new w.OBV();
console.log(obv.batch(
[10, 11, 10.5, 10.5, 12],
[100, 20, 30, 40, 10],
));
```
Output:
```
[ 0, 20, -10, -10, 0 ]
```
## Interpretation
- **Divergence is the signal.** OBV's absolute level depends entirely
on where the series started and is therefore meaningless on its
own. The interpretable signal is the *shape* of OBV relative to
price: a new price high without a new OBV high (bearish divergence)
suggests the rally is not being confirmed by accumulating buy
volume, and vice versa.
- **Trend confirmation.** A rising OBV that tracks a rising price is
confirmation of the trend; a flattening OBV under a still-rising
price is the canonical warning of distribution.
- **Smoothing.** Many traders apply an SMA or EMA to OBV (e.g. 20-period
SMA) and treat crossings of that smoothed line as buy/sell triggers.
## Common pitfalls
- **Absolute value is arbitrary.** Comparing OBV values across
different start times or different instruments is meaningless —
only slopes, divergences, and crossings of derived smoothers carry
signal.
- **Flat closes discard volume.** A candle that closes exactly at the
previous close contributes nothing to OBV no matter how heavy its
volume. Some practitioners prefer A/D-style alternatives (e.g.
Chaikin Money Flow) that distribute the volume according to where
in the bar's range the close landed, precisely to avoid this
discontinuity.
## References
- Joseph Granville, *Granville's New Strategy of Daily Stock Market
Timing for Maximum Profit*, Prentice-Hall, 1976. The OBV
construction was first popularised in Granville's earlier 1963
work and refined in his subsequent books.
## See also
- [VWAP](Indicator-Vwap.md) — volume-weighted price benchmark; OBV and
VWAP are the two canonical volume-aware indicators in the panel.
- [MFI](../momentum/Indicator-Mfi.md) — money-flow index, an oscillator blending
typical price with volume.
- [SMA](../trend/Indicator-Sma.md) / [EMA](../trend/Indicator-Ema.md) — the smoothers
most commonly layered on top of OBV to define trade triggers.
@@ -0,0 +1,290 @@
# VWAP (Volume-Weighted Average Price)
> The volume-weighted mean of typical price; the institutional benchmark for
> "fair" intraday execution. Wickra ships both the unbounded cumulative
> session VWAP and a finite-window `RollingVwap`.
## Quick reference
| Item | Value |
|---------------------|----------------------------------------------------------------|
| Family | Volume |
| Sub-category | cumulative (`Vwap`) / rolling (`RollingVwap`) |
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
| Output type | `f64` |
| Output range | unbounded (price-units) |
| Default parameters | none for `Vwap`; `period` required for `RollingVwap` |
| Warmup period | `1` for `Vwap`, `period` for `RollingVwap` |
| Interpretation | intraday fair-price benchmark for execution |
## Formula
Both variants use the typical price `tp_t = (H_t + L_t + C_t) / 3`
(see `Candle::typical_price` in `crates/wickra-core/src/ohlcv.rs:104-108`).
Cumulative VWAP:
```
VWAP_t = ( Σ_{i=1..t} tp_i * v_i ) / ( Σ_{i=1..t} v_i )
```
Rolling VWAP over the last `period` candles:
```
RollingVWAP_t = ( Σ_{i=t-period+1..t} tp_i * v_i ) / ( Σ_{i=t-period+1..t} v_i )
```
Both forms gate their output: when the relevant volume sum is `0.0`, no
value is emitted (`vwap.rs:50, 121`).
---
## `Vwap` (cumulative)
The session VWAP. State grows forever; call `reset()` at session
boundaries (e.g. the start of the trading day) to restart accumulation.
### Parameters
`Vwap::new()` takes no parameters. Python: `wickra.VWAP()`. Node:
`new w.VWAP()`.
### Inputs / Outputs
```rust
impl Indicator for Vwap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { 1 }
}
```
- **Rust input.** A full `Candle`; the indicator multiplies
`typical_price() * volume` and accumulates.
- **Python batch.** `VWAP.batch(high, low, close, volume)` returns a 1-D
`np.ndarray` with `NaN` for any prefix where the cumulative volume is
still `0`.
- **Node batch.** `vwap.batch(high, low, close, volume)` returns
`Array<number>` with `NaN` for the same prefix.
### Warmup
`warmup_period() == 1`. Provided the first candle has positive volume,
the indicator emits on tick 1. If the first `k` candles all have
`volume == 0`, no output is emitted until the first candle with
non-zero volume — `RollingVwap`'s warmup gating is independent of
this volume-gating logic and applies on top of it.
### Edge cases
- **Zero-volume bar.** A candle with `volume == 0` does not advance the
running sums in any visible way and (if it is the *first* such bar
the indicator has seen) keeps the output at `None`. The implementation
short-circuits with `if self.sum_v == 0.0 { return None; }`
(`vwap.rs:50`).
- **Constant input.** Identical candles produce a flat VWAP equal to
their typical price.
- **Session boundaries.** There is no automatic reset; the caller is
responsible for invoking `reset()` at the start of each new session.
- **NaN / infinity.** `Candle::new` rejects non-finite OHLCV values
before they can reach the indicator.
- **Reset.** `reset()` zeroes both running sums and unsets the `has_emitted`
flag.
### Examples
#### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, Vwap};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?, // tp = 10
Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 0)?, // tp = 20
Candle::new(30.0, 30.0, 30.0, 30.0, 1.0, 0)?, // tp = 30
Candle::new(40.0, 40.0, 40.0, 40.0, 2.0, 0)?, // tp = 40
];
let mut v = Vwap::new();
println!("{:?}", v.batch(&candles));
Ok(())
}
```
Output:
```
[Some(10.0), Some(17.5), Some(20.0), Some(25.714285714285715)]
```
Hand check at `t = 2`: `(10*1 + 20*3) / (1+3) = 70/4 = 17.5`.
At `t = 4`: `(10*1 + 20*3 + 30*1 + 40*2) / (1+3+1+2) = 180/7 ≈ 25.7142857`.
#### Python
```python
import numpy as np
import wickra as ta
vw = ta.VWAP()
h = np.array([10.0, 20.0, 30.0, 40.0])
l = np.array([10.0, 20.0, 30.0, 40.0])
c = np.array([10.0, 20.0, 30.0, 40.0])
v = np.array([ 1.0, 3.0, 1.0, 2.0])
print(vw.batch(h, l, c, v))
```
Output:
```
[10. 17.5 20. 25.71428571]
```
#### Node
```js
const w = require('wickra');
const vw = new w.VWAP();
console.log(vw.batch(
[10, 20, 30, 40],
[10, 20, 30, 40],
[10, 20, 30, 40],
[ 1, 3, 1, 2],
));
```
Output:
```
[ 10, 17.5, 20, 25.714285714285715 ]
```
---
## `RollingVwap` (finite window)
A rolling-window variant for streaming bots that want a finite-memory
fair-price benchmark instead of an unbounded session aggregate.
### Parameters
| Name | Type | Default | Constraint | Source |
|----------|---------|--------------|------------|----------------------------------------------|
| `period` | `usize` | (no default) | `> 0` | `RollingVwap::new` (`vwap.rs:89`) |
`period == 0` returns `Error::PeriodZero`. `RollingVwap` is exposed in
Rust only — Python's `VWAP` / Node's `VWAP` correspond to the cumulative
form.
### Inputs / Outputs
```rust
impl Indicator for RollingVwap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64>;
fn warmup_period(&self) -> usize { self.period }
}
```
The window stores `(typical_price * volume, volume)` pairs and runs
incremental `sum_pv` / `sum_v` aggregates, so each `update` is O(1).
### Warmup
`warmup_period() == period`. The first `period - 1` candles return
`None`; the `period`-th candle emits the first value provided the rolling
volume sum is positive. If the entire window has `volume == 0`, the
indicator stays at `None`.
### Edge cases
- **Window slides.** Once `window.len() == period`, the oldest
`(pv, v)` pair is subtracted from the running sums before the new
pair is added.
- **Zero-volume window.** If every candle in the window has zero
volume, `sum_v == 0` and the indicator suppresses output until a
positive-volume candle is in scope.
- **Reset.** `reset()` clears the window and both running sums.
- **`is_ready()`.** Returns `true` only when the window is full **and**
`sum_v > 0` (`vwap.rs:138`).
### Examples
#### Rust
```rust
use wickra::{BatchExt, Candle, Indicator, RollingVwap};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let candles = vec![
Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?,
Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 0)?,
Candle::new(30.0, 30.0, 30.0, 30.0, 1.0, 0)?,
Candle::new(40.0, 40.0, 40.0, 40.0, 2.0, 0)?,
];
let mut rv = RollingVwap::new(3)?;
println!("{:?}", rv.batch(&candles));
Ok(())
}
```
Output:
```
[None, None, Some(20.0), Some(28.333333333333332)]
```
Hand check at `t = 3` with window `[10@1, 20@3, 30@1]`:
`(10 + 60 + 30) / (1+3+1) = 100/5 = 20.0`.
At `t = 4` with window `[20@3, 30@1, 40@2]`:
`(60 + 30 + 80) / (3+1+2) = 170/6 ≈ 28.333`.
(`RollingVwap` is currently exposed only in the Rust API; the Python
`VWAP` and Node `VWAP` classes correspond to the cumulative form.)
## Interpretation
- **Execution benchmark.** "Beat VWAP" is the canonical buy-side
execution mandate: an aggressive algo that ends up paying *below*
VWAP on the day is considered to have earned alpha relative to a
passive participation strategy.
- **Mean reversion.** Intraday strategies often fade extensions away
from VWAP, treating the VWAP line as a magnet.
- **Trend filter.** Some systems trade only longs above VWAP and only
shorts below it; the line acts as a session-aware bias toggle.
## Common pitfalls
- **Forgetting to reset.** Call `reset()` at session start (or on each
new trading day) — otherwise you average yesterday's tape into
today's signal and the line drifts permanently behind current
price action.
- **Zero-volume warmup.** Several common data sources include
pre-session candles with `volume = 0` for "no print this minute".
Cumulative VWAP returns `None` until at least one positive-volume
candle has been seen; downstream code should treat `None` /
`NaN` / `null` as "not yet ready," not as "VWAP is zero."
- **Typical price vs close.** Wickra uses typical price
`(H + L + C) / 3`, not close. A naive implementation that uses
close will produce noticeably different numbers on bars with wide
intraday ranges.
## References
- The VWAP construct emerged in institutional execution literature in
the late 1980s and early 1990s; it has no single attributed
inventor. The textbook reference for its role as an execution
benchmark is Bertsimas & Lo, "Optimal control of execution costs,"
*Journal of Financial Markets*, 1998.
## See also
- [OBV](Indicator-Obv.md) — cumulative signed-volume measure that pairs
well with VWAP as a divergence flag.
- [MFI](../momentum/Indicator-Mfi.md) — money-flow oscillator that also blends
typical price with volume.
- [Bollinger Bands](../volatility/Indicator-BollingerBands.md) — non-volume volatility
envelope, often layered alongside VWAP on intraday charts.