The original taxonomy was four classical families plus a statistics group, with the F1-F12 expansion slotted in as sub-categories. This regroups the whole 71-indicator catalogue into eight top-level families, each with at least five members: Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9), Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5), Volume (9), Price Statistics (7). - Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71 indicator pages moved with `git mv`. Every internal cross-link is normalised to `../<family>/Indicator-X.md`, each page's `Family` field is set to its new family, and two pre-existing `../Indicator-Chaining.md` links (should have been `../../`) are corrected. A link check confirms every relative wiki link resolves. - Indicators-Overview.md fully rewritten around the eight families; Home.md indicator reference and the README family table follow suit. - Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the 46-indicator expansion (25 -> 71) and the eight-family taxonomy. - Tests: Node indicators.test.js and Python test_new_indicators.py cover all eight new indicators (Node 91/91, Python 117/117 green). cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green.
258 lines
9.2 KiB
Markdown
258 lines
9.2 KiB
Markdown
# Bollinger Bands
|
|
|
|
> An SMA centerline wrapped in symmetric standard-deviation envelopes; the
|
|
> classical reading is that price persistently outside a band signals a
|
|
> volatility-driven trend, not a reversal.
|
|
|
|
## Quick reference
|
|
|
|
| Item | Value |
|
|
|---------------------|--------------------------------------------------------------------------------|
|
|
| Family | Volatility & Bands |
|
|
| Input type | `f64` (typically the close price) |
|
|
| Output type | `BollingerOutput { upper: f64, middle: f64, lower: f64, stddev: f64 }` |
|
|
| Output range | unbounded; `lower ≤ middle ≤ upper`, `stddev ≥ 0` |
|
|
| Default parameters | `period = 20`, `multiplier = 2.0` |
|
|
| Warmup period | `period` (20 for defaults) |
|
|
| Interpretation | width tracks recent volatility; price tags band on momentum |
|
|
|
|
## Formula
|
|
|
|
Each step uses the trailing window of the last `period` inputs:
|
|
|
|
```
|
|
mean = (1/n) * Σ x_i
|
|
var = (1/n) * Σ (x_i - mean)^2 (population variance, denominator = n)
|
|
stddev = sqrt(var)
|
|
upper = mean + multiplier * stddev
|
|
middle = mean
|
|
lower = mean - multiplier * stddev
|
|
```
|
|
|
|
Wickra computes `var` from the streaming sums `Σ x` and `Σ x²` as
|
|
`Σx²/n - (Σx/n)²` and clamps to `0.0` to absorb catastrophic cancellation on
|
|
near-constant inputs (`crates/wickra-core/src/indicators/bollinger.rs:82`).
|
|
|
|
## Parameters
|
|
|
|
| Name | Type | Default | Constraint | Source |
|
|
|--------------|---------|---------|----------------------|----------------------------------------------------------|
|
|
| `period` | `usize` | `20` | `> 0` | `BollingerBands::new` (`bollinger.rs:43`) |
|
|
| `multiplier` | `f64` | `2.0` | finite and `> 0.0` | `BollingerBands::new` (`bollinger.rs:47`) |
|
|
|
|
Python defaults come from `#[pyo3(signature = (period=20, multiplier=2.0))]`
|
|
in `bindings/python/src/lib.rs`. Invalid inputs raise `ValueError` in Python
|
|
and return `Error::PeriodZero` / `Error::NonPositiveMultiplier` in Rust.
|
|
|
|
## Inputs / Outputs
|
|
|
|
Rust signature:
|
|
|
|
```rust
|
|
impl Indicator for BollingerBands {
|
|
type Input = f64;
|
|
type Output = BollingerOutput;
|
|
fn update(&mut self, input: f64) -> Option<BollingerOutput>;
|
|
fn warmup_period(&self) -> usize { self.period }
|
|
}
|
|
```
|
|
|
|
`BollingerOutput` fields: `upper`, `middle`, `lower`, `stddev`.
|
|
|
|
- **Python streaming** (`update`) returns the 4-tuple `(upper, middle, lower, stddev)`
|
|
or `None` during warmup.
|
|
- **Python batch** (`batch`) returns a 2-D `numpy.ndarray` of shape `(n, 4)` with
|
|
columns `[upper, middle, lower, stddev]`; warmup rows are entirely `NaN`.
|
|
- **Node streaming** (`update`) returns a `{ upper, middle, lower, stddev }`
|
|
object or `null` during warmup.
|
|
- **Node batch** (`batch`) returns a flat `Array<number>` of length `n * 4`
|
|
interleaved per row: `[u0, m0, l0, s0, u1, m1, l1, s1, …]`. Warmup rows
|
|
are four consecutive `NaN`s.
|
|
|
|
## Warmup
|
|
|
|
`warmup_period() == period`. The first `period - 1` inputs return `None`; the
|
|
`period`-th input emits the first `BollingerOutput`. Verified for `period = 5`:
|
|
the first non-`None` value appears on the 5th input (index 4).
|
|
|
|
## Edge cases
|
|
|
|
- **Constant input.** With a flat series the population stddev collapses to
|
|
exactly `0.0`, so `upper == middle == lower == mean`. The library guards
|
|
against tiny negative floating-point values from catastrophic cancellation
|
|
by clamping the variance with `.max(0.0)`.
|
|
- **Flat range / squeeze.** Real markets never give exactly `0.0`, but very
|
|
low-volatility windows produce visibly narrow bands; the upper and lower
|
|
bands collapse onto the middle band (the "Bollinger squeeze").
|
|
- **NaN / infinity input.** The implementation skips non-finite inputs:
|
|
`if !input.is_finite() { return self.current(); }`. The window is not
|
|
advanced and the previous `BollingerOutput` (or `None`) is returned.
|
|
- **Multiplier validation.** `multiplier <= 0` or non-finite returns
|
|
`Error::NonPositiveMultiplier`. `period == 0` returns `Error::PeriodZero`.
|
|
- **Reset.** `reset()` clears the window and both running sums, returning the
|
|
indicator to a freshly-constructed state.
|
|
|
|
## Examples
|
|
|
|
### Rust
|
|
|
|
```rust
|
|
use wickra::{BatchExt, BollingerBands, Indicator};
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut bb = BollingerBands::new(5, 2.0)?;
|
|
let out = bb.batch(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]);
|
|
for (i, v) in out.into_iter().enumerate() {
|
|
println!("i={i} -> {:?}", v);
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
i=0 -> None
|
|
i=1 -> None
|
|
i=2 -> None
|
|
i=3 -> None
|
|
i=4 -> Some(BollingerOutput { upper: 5.759591794226543, middle: 3.8, lower: 1.8404082057734565, stddev: 0.9797958971132716 })
|
|
i=5 -> Some(BollingerOutput { upper: 5.379795897113269, middle: 4.4, lower: 3.420204102886732, stddev: 0.48989794855663404 })
|
|
i=6 -> Some(BollingerOutput { upper: 7.190890230020663, middle: 5.0, lower: 2.809109769979336, stddev: 1.095445115010332 })
|
|
i=7 -> Some(BollingerOutput { upper: 9.577708763999665, middle: 6.0, lower: 2.422291236000335, stddev: 1.7888543819998326 })
|
|
```
|
|
|
|
The first emission at `i=4` uses the window `[2, 4, 4, 4, 5]` with mean
|
|
`3.8` and population stddev `sqrt(0.96) ≈ 0.9797959`.
|
|
|
|
### Python
|
|
|
|
```python
|
|
import numpy as np
|
|
import wickra as ta
|
|
|
|
bb = ta.BollingerBands(5, 2.0)
|
|
prices = np.array([2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0], dtype=float)
|
|
out = bb.batch(prices)
|
|
print("shape:", out.shape)
|
|
print("row 4:", out[4])
|
|
print("row 7:", out[7])
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
shape: (8, 4)
|
|
row 4: [5.75959179 3.8 1.84040821 0.9797959 ]
|
|
row 7: [9.57770876 6. 2.42229124 1.78885438]
|
|
```
|
|
|
|
Streaming variant returns a 4-tuple `(upper, middle, lower, stddev)` per
|
|
tick or `None` during warmup:
|
|
|
|
```python
|
|
import wickra as ta
|
|
|
|
bb = ta.BollingerBands(5, 2.0)
|
|
for p in [2.0, 4.0, 4.0, 4.0, 5.0, 9.0]:
|
|
print(p, "->", bb.update(p))
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
2.0 -> None
|
|
4.0 -> None
|
|
4.0 -> None
|
|
4.0 -> None
|
|
5.0 -> (5.759591794226543, 3.8, 1.8404082057734565, 0.9797958971132716)
|
|
9.0 -> (9.078143885933063, 5.2, 1.321856114066938, 1.939071942966531)
|
|
```
|
|
|
|
### Node
|
|
|
|
```js
|
|
const w = require('wickra');
|
|
|
|
const bb = new w.BollingerBands(5, 2.0);
|
|
const flat = bb.batch([2, 4, 4, 4, 5, 5, 7, 9]);
|
|
console.log('length:', flat.length);
|
|
console.log('row 4 [upper, middle, lower, stddev]:', flat.slice(16, 20));
|
|
console.log('row 7 [upper, middle, lower, stddev]:', flat.slice(28, 32));
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
length: 32
|
|
row 4 [upper, middle, lower, stddev]: [ 5.759591794226543, 3.8, 1.8404082057734565, 0.9797958971132716 ]
|
|
row 7 [upper, middle, lower, stddev]: [ 9.577708763999665, 6, 2.422291236000335, 1.7888543819998326 ]
|
|
```
|
|
|
|
Streaming returns the named object `{ upper, middle, lower, stddev }`:
|
|
|
|
```js
|
|
const w = require('wickra');
|
|
|
|
const bb = new w.BollingerBands(5, 2.0);
|
|
[2, 4, 4, 4, 5].forEach(p => console.log(p, '->', bb.update(p)));
|
|
```
|
|
|
|
Output:
|
|
|
|
```
|
|
2 -> null
|
|
4 -> null
|
|
4 -> null
|
|
4 -> null
|
|
5 -> {
|
|
upper: 5.759591794226543,
|
|
middle: 3.8,
|
|
lower: 1.8404082057734565,
|
|
stddev: 0.9797958971132716
|
|
}
|
|
```
|
|
|
|
## Interpretation
|
|
|
|
- **Bandwidth as volatility.** `(upper - lower) / middle` is the Bollinger
|
|
bandwidth; a multi-month low in bandwidth is the classic "squeeze" that
|
|
often precedes an expansion move.
|
|
- **Tags vs breakouts.** A single touch of the upper band is not a sell
|
|
signal in Bollinger's own framework; persistent closes outside the band
|
|
("walking the band") signal trend continuation, not exhaustion.
|
|
- **%b position.** `(price - lower) / (upper - lower)` normalises position
|
|
inside the channel and is useful as a feature for cross-asset comparison.
|
|
|
|
## Common pitfalls
|
|
|
|
- **Stddev convention.** Wickra uses **population** standard deviation
|
|
(denominator `n`, not `n - 1`). This matches Bollinger's original
|
|
formulation and every reference implementation (TA-Lib, pandas-ta);
|
|
switching to the sample variant would mis-align bands by a factor of
|
|
`sqrt(n / (n - 1))` and break parity with other tools.
|
|
- **Partial rows.** In the Python 2-D batch result, do not slice an
|
|
individual column out and use it for analysis without checking for
|
|
`NaN` — every warmup row is `NaN` across all four columns. Filter with
|
|
`mask = ~np.isnan(out[:, 0])` before reading any single column.
|
|
- **Flat batch length in Node.** The Node `batch` returns `n * 4` numbers
|
|
interleaved per row, not four parallel arrays. Reshape with
|
|
`Array.from({ length: n }, (_, i) => flat.slice(i * 4, i * 4 + 4))`
|
|
if you want per-row records.
|
|
|
|
## References
|
|
|
|
- John Bollinger, *Bollinger on Bollinger Bands*, McGraw-Hill, 2001 (the
|
|
original publication of the indicator dates to the early 1980s).
|
|
- Wilder's *New Concepts in Technical Trading Systems* (1978) for the
|
|
surrounding family of volatility envelopes.
|
|
|
|
## See also
|
|
|
|
- [Keltner Channels](../volatility-bands/Indicator-Keltner.md) — same envelope shape but band
|
|
width is driven by ATR instead of stddev.
|
|
- [Donchian Channels](../volatility-bands/Indicator-Donchian.md) — rolling high/low envelope
|
|
with no smoothing.
|
|
- [ATR](../volatility-bands/Indicator-Atr.md) — the volatility scale most commonly used to
|
|
size Bollinger-style stops.
|