docs(wiki): migrate documentation out of repo into GitHub Wiki
The 84 markdown files under docs/wiki/ are now published to the project's GitHub Wiki at https://github.com/kingchenc/wickra/wiki — a separate git repository (https://github.com/kingchenc/wickra.wiki.git) that GitHub hosts natively with its own UI, search and history. The flat layout that the GitHub Wiki requires has been generated, all internal cross-links rewritten, and a _Sidebar.md groups the 71 indicators by their canonical 8 families. Effects: - docs/wiki/ is removed from the main repo (-84 files). docs/README.md now just points readers at the Wiki. - PR template + CONTRIBUTING text updated to point at the Wiki instead of the in-repo path. The Wiki repo is separately cloneable and editable via the GitHub web UI. - examples/wasm/README.md cross-link fixed to use the Wiki URL. - The (still in-repo) CHANGELOG keeps its historical references to docs/wiki/ paths — those describe what the tree looked like at past releases and stay accurate as history. - README.md, license, all source unaffected. The Wiki itself ships with _Sidebar.md / _Footer.md generated from the 8-families taxonomy and 503/503 cross-links resolved.
This commit is contained in:
@@ -24,8 +24,9 @@
|
||||
- [ ] New behaviour has tests; bug fixes have a regression test.
|
||||
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings
|
||||
and their type stubs (if applicable).
|
||||
- [ ] Documentation under `docs/wiki/` and the `README.md` is updated
|
||||
(if applicable).
|
||||
- [ ] The relevant page on the [project Wiki](https://github.com/kingchenc/wickra/wiki)
|
||||
and the `README.md` are updated (if applicable). Wiki edits go to a
|
||||
separate repository: `https://github.com/kingchenc/wickra.wiki.git`.
|
||||
- [ ] An entry was added under `## [Unreleased]` in `CHANGELOG.md`.
|
||||
|
||||
## Notes for reviewers
|
||||
|
||||
+5
-3
@@ -22,7 +22,7 @@ when proposing features or depending on Wickra elsewhere.
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/wiki/` | Documentation sources. |
|
||||
| `docs/` | Pointer to the project Wiki, which holds all documentation. |
|
||||
|
||||
## Building and testing
|
||||
|
||||
@@ -75,8 +75,10 @@ wasm-pack test --node bindings/wasm
|
||||
of `update` calls.
|
||||
- **Bindings.** A change to a public indicator API must be mirrored across the
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
|
||||
- **Docs.** Update the relevant page under `docs/wiki/` and the `README.md`
|
||||
when behaviour or the public API changes.
|
||||
- **Docs.** Update the relevant page on the
|
||||
[project Wiki](https://github.com/kingchenc/wickra/wiki) and the
|
||||
`README.md` when behaviour or the public API changes. The Wiki lives in
|
||||
a separate git repository: `https://github.com/kingchenc/wickra.wiki.git`.
|
||||
- **Changelog.** Add an entry under `## [Unreleased]` in `CHANGELOG.md`.
|
||||
|
||||
## Commit and pull-request workflow
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Documentation
|
||||
|
||||
Wickra's full documentation lives in the **[GitHub Wiki](https://github.com/kingchenc/wickra/wiki)**.
|
||||
|
||||
That includes:
|
||||
|
||||
- **Quickstarts** for [Rust](https://github.com/kingchenc/wickra/wiki/Quickstart-Rust.md),
|
||||
[Python](https://github.com/kingchenc/wickra/wiki/Quickstart-Python.md),
|
||||
[Node](https://github.com/kingchenc/wickra/wiki/Quickstart-Node.md), and
|
||||
[WASM](https://github.com/kingchenc/wickra/wiki/Quickstart-WASM.md).
|
||||
- A per-indicator deep dive for every one of the **71 indicators** across
|
||||
the eight families (Moving Averages, Momentum Oscillators, Trend &
|
||||
Directional, Price Oscillators, Volatility & Bands, Trailing Stops,
|
||||
Volume, Price Statistics) — see the
|
||||
[indicators overview](https://github.com/kingchenc/wickra/wiki/Indicators-Overview.md).
|
||||
- **Reference pages**: [warmup periods](https://github.com/kingchenc/wickra/wiki/Warmup-Periods.md),
|
||||
[streaming vs batch](https://github.com/kingchenc/wickra/wiki/Streaming-vs-Batch.md),
|
||||
[indicator chaining](https://github.com/kingchenc/wickra/wiki/Indicator-Chaining.md), and the
|
||||
[data layer](https://github.com/kingchenc/wickra/wiki/Data-Layer.md).
|
||||
- **Guides**: [Cookbook](https://github.com/kingchenc/wickra/wiki/Cookbook.md),
|
||||
[TA-Lib migration](https://github.com/kingchenc/wickra/wiki/TA-Lib-Migration.md),
|
||||
[FAQ](https://github.com/kingchenc/wickra/wiki/FAQ.md).
|
||||
|
||||
## Editing the wiki
|
||||
|
||||
The wiki is a separate git repository at `https://github.com/kingchenc/wickra.wiki.git`.
|
||||
Clone it locally if you want to bulk-edit; otherwise the GitHub web UI's "Edit" button on any
|
||||
wiki page is fine for one-off changes.
|
||||
@@ -1,185 +0,0 @@
|
||||
# Cookbook
|
||||
|
||||
Practical strategy recipes built on Wickra's streaming indicators. Each
|
||||
recipe is a small, runnable snippet you can drop into a backtest loop or a
|
||||
live trading bot. Both paths share the same indicator state, so the same
|
||||
recipe works in either mode — see [Streaming vs Batch](Streaming-vs-Batch.md).
|
||||
|
||||
## 1. RSI mean reversion
|
||||
|
||||
Enter when RSI crosses out of an extreme; flatten when it returns to
|
||||
neutral.
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
rsi = ta.RSI(14)
|
||||
position = 0 # 0 flat, +1 long, −1 short
|
||||
for price in price_feed:
|
||||
v = rsi.update(price)
|
||||
if v is None:
|
||||
continue
|
||||
if position == 0 and v < 30:
|
||||
position = 1
|
||||
print(f"BUY at {price:.2f}")
|
||||
elif position == 1 and v > 50:
|
||||
position = 0
|
||||
print(f"EXIT long at {price:.2f}")
|
||||
elif position == 0 and v > 70:
|
||||
position = -1
|
||||
print(f"SHORT at {price:.2f}")
|
||||
elif position == -1 and v < 50:
|
||||
position = 0
|
||||
print(f"COVER short at {price:.2f}")
|
||||
```
|
||||
|
||||
## 2. MACD histogram crossover
|
||||
|
||||
Trade in the direction of a MACD-histogram sign change. Zero-crossings of
|
||||
the histogram (`MACD − signal`) are the canonical trigger and lead the
|
||||
slower MACD-vs-signal line cross.
|
||||
|
||||
```rust
|
||||
use wickra::{Indicator, MacdIndicator};
|
||||
|
||||
let mut macd = MacdIndicator::classic(); // (12, 26, 9)
|
||||
let mut last_hist: Option<f64> = None;
|
||||
for &price in &prices {
|
||||
if let Some(v) = macd.update(price) {
|
||||
if let Some(prev) = last_hist {
|
||||
if prev <= 0.0 && v.histogram > 0.0 {
|
||||
println!("BUY: MACD histogram turned positive at {price:.2}");
|
||||
} else if prev >= 0.0 && v.histogram < 0.0 {
|
||||
println!("SELL: MACD histogram turned negative at {price:.2}");
|
||||
}
|
||||
}
|
||||
last_hist = Some(v.histogram);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Bollinger band breakout
|
||||
|
||||
Trade in the direction of a band-piercing close, taking the bands as a
|
||||
dynamic support / resistance.
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
bb = ta.BollingerBands(20, 2.0)
|
||||
for price in price_feed:
|
||||
out = bb.update(price)
|
||||
if out is None:
|
||||
continue
|
||||
upper, _middle, lower, _stddev = out
|
||||
if price > upper:
|
||||
print(f"BREAKOUT (long): {price:.2f} > upper {upper:.2f}")
|
||||
elif price < lower:
|
||||
print(f"BREAKOUT (short): {price:.2f} < lower {lower:.2f}")
|
||||
```
|
||||
|
||||
## 4. ADX-gated trend filter
|
||||
|
||||
Take EMA-crossover signals only when ADX confirms a trend is in place.
|
||||
This is a textbook way to silence whipsaws in a ranging market.
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
ema_fast = ta.EMA(20)
|
||||
ema_slow = ta.EMA(50)
|
||||
adx = ta.ADX(14)
|
||||
|
||||
for high, low, close in candle_feed:
|
||||
f = ema_fast.update(close)
|
||||
s = ema_slow.update(close)
|
||||
a = adx.update(high, low, close) # (plus_di, minus_di, adx) or None
|
||||
if f is None or s is None or a is None:
|
||||
continue
|
||||
_, _, adx_v = a
|
||||
if adx_v < 25:
|
||||
continue # ranging market — skip
|
||||
if f > s:
|
||||
print(f"LONG: EMA20 > EMA50, ADX={adx_v:.1f}")
|
||||
elif f < s:
|
||||
print(f"SHORT: EMA20 < EMA50, ADX={adx_v:.1f}")
|
||||
```
|
||||
|
||||
## 5. Multi-timeframe confirmation
|
||||
|
||||
Only take a 1-minute entry when the 1-hour trend agrees. With Wickra you
|
||||
keep one streaming indicator per timeframe and feed each only the candles
|
||||
that belong to it. `wickra-data`'s [`Resampler`](Data-Layer.md) rolls one
|
||||
candle stream up into a coarser one; the canonical example is
|
||||
`examples/rust/src/bin/multi_timeframe.rs`.
|
||||
|
||||
```rust
|
||||
use wickra::{Indicator, Rsi};
|
||||
|
||||
let mut rsi_1m = Rsi::new(14)?;
|
||||
let mut rsi_1h = Rsi::new(14)?;
|
||||
|
||||
for candle in one_min_candles {
|
||||
let fast = rsi_1m.update(candle.close);
|
||||
|
||||
if candle.is_hour_close {
|
||||
let slow = rsi_1h.update(candle.close);
|
||||
if let (Some(f), Some(s)) = (fast, slow) {
|
||||
if f > 70.0 && s > 50.0 {
|
||||
println!("strong overbought (1m {f:.1} / 1h {s:.1})");
|
||||
} else if f < 30.0 && s < 50.0 {
|
||||
println!("strong oversold (1m {f:.1} / 1h {s:.1})");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. SuperTrend trailing stop
|
||||
|
||||
`SuperTrend` is a single-line ATR-banded trailing stop with explicit flip
|
||||
logic — drop it into a long-only loop to manage exits:
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
st = ta.SuperTrend(10, 3.0)
|
||||
position = 0 # 0 flat, +1 long
|
||||
for high, low, close in candle_feed:
|
||||
out = st.update(high, low, close)
|
||||
if out is None:
|
||||
continue
|
||||
value, direction = out
|
||||
if direction > 0 and position == 0:
|
||||
position = 1
|
||||
print(f"BUY at {close:.2f}, stop={value:.2f}")
|
||||
elif direction < 0 and position == 1:
|
||||
position = 0
|
||||
print(f"EXIT at {close:.2f} (SuperTrend flipped)")
|
||||
```
|
||||
|
||||
## 7. Chained indicators
|
||||
|
||||
When you want an indicator computed *over the output of another*, use the
|
||||
Rust `Chain` combinator. The chain itself implements `Indicator`, so you
|
||||
can nest, stack, and feed it into anything that takes an indicator.
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Chain, Ema, Rsi};
|
||||
|
||||
// RSI(7) of EMA(14)-smoothed closes.
|
||||
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
let out: Vec<Option<f64>> = chain.batch(&prices);
|
||||
```
|
||||
|
||||
See [Indicator Chaining](Indicator-Chaining.md) for the chained-warmup rule
|
||||
and three-stage examples.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicators Overview](Indicators-Overview.md) — pick the right indicator
|
||||
for the question you are asking.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — why these recipes work
|
||||
bit-identically in both modes.
|
||||
- [Data Layer](Data-Layer.md) — `Resampler` and the bundled BTCUSDT
|
||||
datasets for live multi-timeframe work.
|
||||
@@ -1,194 +0,0 @@
|
||||
# Data Layer (`wickra-data`)
|
||||
|
||||
`wickra-data` is a separate crate that feeds candles into Wickra's indicators.
|
||||
It is not part of `wickra-core` — depend on it explicitly:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
wickra = "0.1"
|
||||
wickra-data = "0.1"
|
||||
```
|
||||
|
||||
It provides four pieces:
|
||||
|
||||
- a streaming OHLCV **CSV reader**,
|
||||
- a **tick-to-candle aggregator**,
|
||||
- a **candle resampler** for multi-timeframe analysis,
|
||||
- an optional **Binance Spot WebSocket** kline feed (feature `live-binance`).
|
||||
|
||||
## CSV reader
|
||||
|
||||
`CandleReader` streams OHLCV rows out of a CSV file into validated `Candle`
|
||||
values.
|
||||
|
||||
```rust
|
||||
use wickra_data::csv::CandleReader;
|
||||
|
||||
let mut reader = CandleReader::open("ohlcv.csv")?;
|
||||
let candles = reader.read_all()?; // Vec<Candle>
|
||||
|
||||
// Or stream row by row without buffering the whole file:
|
||||
let mut reader = CandleReader::open("ohlcv.csv")?;
|
||||
for candle in reader.candles() {
|
||||
let candle = candle?;
|
||||
// feed `candle` into an indicator...
|
||||
}
|
||||
```
|
||||
|
||||
The reader is defensive about real-world files:
|
||||
|
||||
- The first line **must** be a header naming the columns
|
||||
`timestamp,open,high,low,close,volume`. A missing column, or a file with no
|
||||
header at all, is rejected with a clear `Error::Malformed` instead of
|
||||
silently consuming the first data row.
|
||||
- A leading UTF-8 byte-order mark (Excel exports one) is stripped.
|
||||
- Whitespace around values is trimmed.
|
||||
- Each row is validated through `Candle::new`, so an inconsistent OHLC row
|
||||
(e.g. `high < low`) surfaces as an error.
|
||||
|
||||
## Tick aggregator
|
||||
|
||||
`TickAggregator` rolls a stream of trade `Tick`s up into `Candle`s of an
|
||||
arbitrary timeframe. The timeframe's bucket size is in the same unit as the
|
||||
tick timestamps (milliseconds for Binance, seconds for daily bars, …).
|
||||
Build a `Timeframe` with `Timeframe::new` (a raw bucket size), the
|
||||
`millis` / `seconds` / `one_minute_ms` shortcuts, or the `minutes` / `hours` /
|
||||
`days` constructors — each of the last three builds on **seconds**, so
|
||||
`Timeframe::minutes(5)` is a 300-second bucket.
|
||||
|
||||
```rust
|
||||
use wickra_data::aggregator::{TickAggregator, Timeframe};
|
||||
use wickra_core::Tick;
|
||||
|
||||
let mut agg = TickAggregator::new(Timeframe::one_minute_ms());
|
||||
|
||||
for tick in trade_feed {
|
||||
// push returns every candle that closed because of this tick —
|
||||
// empty while the bar grows, one candle when a bar boundary is crossed.
|
||||
for closed in agg.push(tick)? {
|
||||
// feed `closed` into an indicator...
|
||||
}
|
||||
}
|
||||
|
||||
// Capture the final, still-open bar at the end of the stream.
|
||||
if let Some(last) = agg.flush()? {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Out-of-order ticks — across or within a bucket — are rejected with
|
||||
`Error::Malformed` rather than silently corrupting a bar.
|
||||
|
||||
### Gap filling
|
||||
|
||||
By default a tick that jumps across one or more empty buckets simply opens
|
||||
the next non-empty bar, leaving a time hole in the output. Enable
|
||||
`with_gap_fill` to emit a flat placeholder candle
|
||||
(`open == high == low == close`, `volume == 0`) for every skipped bucket, so
|
||||
downstream indicators see an unbroken, evenly spaced series:
|
||||
|
||||
```rust
|
||||
let mut agg = TickAggregator::new(Timeframe::one_minute_ms()).with_gap_fill(true);
|
||||
```
|
||||
|
||||
## Resampler
|
||||
|
||||
`Resampler` rolls an existing candle stream up to a coarser timeframe — for
|
||||
example 1-minute bars into 5-minute bars, without touching the original tick
|
||||
stream.
|
||||
|
||||
```rust
|
||||
use wickra_data::aggregator::Timeframe;
|
||||
use wickra_data::resample::{resample_all, Resampler};
|
||||
|
||||
// One-shot over an iterator:
|
||||
let five_min = resample_all(Timeframe::millis(5 * 60_000), one_min_candles)?;
|
||||
|
||||
// Or incrementally:
|
||||
let mut r = Resampler::new(Timeframe::millis(60 * 60_000)); // 1-hour bars
|
||||
for candle in one_min_candles {
|
||||
if let Some(closed) = r.push(candle?)? {
|
||||
// a coarser bar just closed
|
||||
}
|
||||
}
|
||||
let last = r.flush()?;
|
||||
```
|
||||
|
||||
The output timeframe's bucket must be a multiple of the input timeframe's
|
||||
bucket — picking sensible aggregations (1m → 5m → 1h) is the caller's
|
||||
responsibility. A candle that arrives in a bucket earlier than the open bar
|
||||
is rejected as out of order.
|
||||
|
||||
## Binance live feed
|
||||
|
||||
With the `live-binance` feature enabled, `BinanceKlineStream` connects to the
|
||||
Binance Spot WebSocket and yields closed klines as candles.
|
||||
|
||||
```toml
|
||||
wickra-data = { version = "0.1", features = ["live-binance"] }
|
||||
```
|
||||
|
||||
```rust
|
||||
use wickra::{Indicator, Rsi};
|
||||
use wickra_data::live::binance::{BinanceKlineStream, Interval};
|
||||
|
||||
let mut stream =
|
||||
BinanceKlineStream::connect(&["BTCUSDT".into()], Interval::OneMinute).await?;
|
||||
let mut rsi = Rsi::new(14)?;
|
||||
|
||||
while let Some(event) = stream.next_event().await? {
|
||||
if event.is_closed {
|
||||
if let Some(v) = rsi.update(event.candle.close) {
|
||||
println!("RSI = {v:.2}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The stream is resilient: it reconnects with exponential backoff after a
|
||||
dropped connection, skips non-kline frames (subscription acks, heartbeats),
|
||||
applies a read timeout and message-size limits, and tracks a closed flag so a
|
||||
deliberately closed stream is not reused.
|
||||
|
||||
A runnable example lives at `examples/rust/src/bin/live_binance.rs`:
|
||||
|
||||
```bash
|
||||
cargo run -p wickra-examples --bin live_binance
|
||||
```
|
||||
|
||||
## Example datasets
|
||||
|
||||
The repository ships seven ready-to-use OHLCV datasets under
|
||||
`examples/data/`, one per timeframe, holding real Binance **BTCUSDT** spot
|
||||
candles in the standard `timestamp,open,high,low,close,volume` layout the
|
||||
`CandleReader` reads. The timestamp is each candle's open time in milliseconds.
|
||||
|
||||
| File | Timeframe | Rows |
|
||||
| --- | --- | --- |
|
||||
| `btcusdt-1m.csv` | 1 minute | 50 000 |
|
||||
| `btcusdt-5m.csv` | 5 minutes | 10 000 |
|
||||
| `btcusdt-15m.csv` | 15 minutes | 10 000 |
|
||||
| `btcusdt-1h.csv` | 1 hour | 10 000 |
|
||||
| `btcusdt-12h.csv` | 12 hours | 5 000 |
|
||||
| `btcusdt-1d.csv` | 1 day | full history |
|
||||
| `btcusdt-1month.csv` | 1 month | full history |
|
||||
|
||||
The monthly file is named `btcusdt-1month.csv` rather than `btcusdt-1M.csv` so
|
||||
it does not collide with `btcusdt-1m.csv` on case-insensitive filesystems. The
|
||||
indicator benchmarks and the `example_data` integration test both run against
|
||||
these files.
|
||||
|
||||
Regenerate them with the latest market history — this downloads from the
|
||||
Binance REST API and needs the system `curl` (shipped with Windows 10+, macOS
|
||||
and Linux):
|
||||
|
||||
```bash
|
||||
cargo run -p wickra-examples --bin fetch_btcusdt
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Rust](Quickstart-Rust.md) — the core indicator API.
|
||||
- [Indicators Overview](Indicators-Overview.md) — every indicator and its
|
||||
parameters.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,123 +0,0 @@
|
||||
# FAQ
|
||||
|
||||
Frequently asked questions about Wickra. If yours is not here, check the
|
||||
[issue tracker](https://github.com/kingchenc/wickra/issues) or open a new
|
||||
issue.
|
||||
|
||||
## Will batch and streaming produce the same result?
|
||||
|
||||
Yes — bit-identical, by construction. `batch(prices)` is a one-line wrapper
|
||||
that calls `update(p)` for every `p` in the input. The same unit test —
|
||||
`batch_equals_streaming` — pins this for every indicator. See
|
||||
[Streaming vs Batch](Streaming-vs-Batch.md) for the full contract.
|
||||
|
||||
## What does `warmup_period()` mean?
|
||||
|
||||
It's the number of inputs an indicator needs before it emits its first
|
||||
non-`None` value. For RSI(14) that's 15 (14 diffs plus the seed); for
|
||||
SMA(20) it's 20; for MACD(12, 26, 9) it's 34 (`slow + signal − 1`). After
|
||||
warmup the indicator never goes back to `None`. The complete table lives
|
||||
at [Warmup Periods](Warmup-Periods.md).
|
||||
|
||||
## Why am I getting `None` / `NaN` for the first N values?
|
||||
|
||||
That's the warmup. Use `is_ready()` (or the corresponding `isReady()` in
|
||||
Node, `is_ready()` in Python) to gate your code on "do I have a real
|
||||
value yet?" rather than counting inputs yourself:
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
rsi = ta.RSI(14)
|
||||
for price in feed:
|
||||
rsi.update(price)
|
||||
if rsi.is_ready():
|
||||
...
|
||||
```
|
||||
|
||||
## Which indicator should I use for X?
|
||||
|
||||
A short cheat-sheet (full version at the bottom of
|
||||
[Indicators Overview](Indicators-Overview.md)):
|
||||
|
||||
- **trend direction** → MA family (`SMA`, `EMA`, `HMA`, `T3`, `KAMA`)
|
||||
- **trend strength** → `ADX`, `ChoppinessIndex`, `VerticalHorizontalFilter`
|
||||
- **overbought / oversold** → `RSI`, `Stochastic`, `Williams %R`, `MFI`
|
||||
- **volatility** → `ATR`, `TrueRange`, `ChaikinVolatility`, `StdDev`
|
||||
- **breakout level** → `Donchian`, `BollingerBands`
|
||||
- **trailing stop** → `PSAR`, `SuperTrend`, `ChandelierExit`,
|
||||
`AtrTrailingStop`
|
||||
- **volume confirmation** → `OBV`, `ChaikinMoneyFlow`, `VWAP`
|
||||
|
||||
## Is a single indicator instance thread-safe?
|
||||
|
||||
No. `update` mutates state, so a single instance must not be shared across
|
||||
threads. Each thread should own its own indicator. For multi-asset
|
||||
parallelism, the Rust crate provides `BatchExt::batch_parallel`, which
|
||||
fans out over many series each with its own fresh instance behind the
|
||||
default `parallel` feature (rayon). Node's `worker_threads` gives the
|
||||
same shape from JavaScript — see `examples/node/parallel_assets.js`.
|
||||
|
||||
## Does Wickra need a system compiler to install?
|
||||
|
||||
No. Every published wheel (PyPI), npm package, and crate ships pre-built
|
||||
artefacts. `pip install wickra` and `npm install wickra` are
|
||||
no-prerequisite installs on Linux, macOS, and Windows x64 / arm64. The
|
||||
only time you need a toolchain is when you are building Wickra from
|
||||
source.
|
||||
|
||||
## How do I handle non-finite inputs (NaN / Inf)?
|
||||
|
||||
The scalar indicators (`SMA`, `EMA`, `WMA`, `RSI`, `ROC`, …) return the
|
||||
most recent valid value when fed a non-finite input, leaving their state
|
||||
untouched. That lets a missing price in your feed pass through without
|
||||
poisoning the rest of the series. `ATR` and the volume-aware indicators
|
||||
reject non-finite volume at the `Candle::new` boundary, so an aggregator
|
||||
that overflows surfaces an error instead of producing a corrupted candle
|
||||
(see [Data Layer](Data-Layer.md)).
|
||||
|
||||
## How fast is Wickra?
|
||||
|
||||
The streaming path is O(1) per `update` — the per-tick cost does not grow
|
||||
with how much history you have already seen. The README has a benchmark
|
||||
table comparing Wickra against `finta` and `talipp`; the gap is roughly
|
||||
10–30× on batch workloads and ~17× per tick on a streaming RSI seeded
|
||||
with 2 000 historical bars.
|
||||
|
||||
## How do I add a custom indicator?
|
||||
|
||||
Implement the `Indicator` trait in
|
||||
`crates/wickra-core/src/indicators/<your_name>.rs`, wire it through the
|
||||
bindings, and add reference-value plus `batch == streaming` equivalence
|
||||
tests. The complete how-to and the project's standards are in
|
||||
[CONTRIBUTING.md](https://github.com/kingchenc/wickra/blob/main/CONTRIBUTING.md).
|
||||
|
||||
## Where do I get historical OHLCV data to test with?
|
||||
|
||||
The repo ships seven real BTCUSDT datasets at
|
||||
`examples/data/btcusdt-{1m,5m,15m,1h,12h,1d,1month}.csv` (50 000 / 10 000 /
|
||||
10 000 / 10 000 / 5 000 / 3 200 / 105 candles respectively). Refresh them
|
||||
with the latest market history via
|
||||
`cargo run -p wickra-examples --bin fetch_btcusdt`. See
|
||||
[Data Layer](Data-Layer.md) for the full story.
|
||||
|
||||
## How is Wickra different from TA-Lib / pandas-ta / talipp?
|
||||
|
||||
* TA-Lib and pandas-ta are batch-only — every new tick triggers a full
|
||||
recomputation. Wickra updates in O(1). The numerical results are the
|
||||
same; the speed gap shows up in live trading and large backtests.
|
||||
* talipp is streaming-first like Wickra but Python-only and slower per
|
||||
update.
|
||||
* `finta` is batch-only and pure-Python.
|
||||
* `ta-lib-python` and TA-Lib both require C build tooling on Windows;
|
||||
Wickra ships pre-built native wheels.
|
||||
|
||||
See the [TA-Lib Migration](TA-Lib-Migration.md) guide for a direct
|
||||
function-by-function mapping.
|
||||
|
||||
## See also
|
||||
|
||||
- [Home](Home.md) — wiki index.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — the central design idea.
|
||||
- [TA-Lib Migration](TA-Lib-Migration.md) — function-by-function mapping
|
||||
table.
|
||||
- [Cookbook](Cookbook.md) — practical strategy recipes.
|
||||
@@ -1,183 +0,0 @@
|
||||
# Wickra
|
||||
|
||||
Wickra is a streaming-first technical-indicators library. Every indicator is
|
||||
implemented in Rust as an O(1) state machine that consumes one input at a
|
||||
time, and the same engine is exposed through ergonomic bindings for Python,
|
||||
Node.js, WebAssembly, and Rust itself. The same `update` call you write inside
|
||||
a live trading loop also drives the historical backtest of that same
|
||||
strategy — there is no second code path that drifts behind the streaming one.
|
||||
|
||||
The project ships 71 indicators across eight families — moving averages,
|
||||
momentum oscillators, trend & directional, price oscillators, volatility &
|
||||
bands, trailing stops, volume, and price statistics — plus a small set of
|
||||
supporting types (`Candle`, `Tick`, `Chain`). The Rust core forbids `unsafe`,
|
||||
so every binding inherits a
|
||||
memory-safe implementation. Install is one command on every supported
|
||||
platform: `pip install wickra`, `cargo add wickra`, `npm install wickra` — no
|
||||
system compilers, no C dependencies, no headers.
|
||||
|
||||
Wickra is licensed under the **PolyForm Noncommercial 1.0.0** license.
|
||||
Personal projects, research, hobby trading bots, education, non-profits, and
|
||||
government use are all permitted; commercial sale of the software or of
|
||||
services built around it is not. If you want to use Wickra commercially,
|
||||
open an issue on GitHub to discuss a separate license.
|
||||
|
||||
## Published versions
|
||||
|
||||
| Registry | Package | Version |
|
||||
|-----------|----------------|---------|
|
||||
| crates.io | `wickra` | 0.2.1 |
|
||||
| crates.io | `wickra-core` | 0.2.1 |
|
||||
| crates.io | `wickra-data` | 0.2.1 |
|
||||
| PyPI | `wickra` | 0.2.1 |
|
||||
| npm | `wickra` | 0.2.1 |
|
||||
| npm | `wickra-wasm` | 0.2.1 |
|
||||
|
||||
Release notes and tagged builds:
|
||||
<https://github.com/kingchenc/wickra/releases>.
|
||||
|
||||
## Wiki contents
|
||||
|
||||
- [Quickstart: Python](Quickstart-Python.md) — `pip install wickra`, a batch
|
||||
RSI on a NumPy array, a streaming RSI loop, and the multi-column NaN
|
||||
pattern that MACD and friends share.
|
||||
- [Quickstart: Rust](Quickstart-Rust.md) — `cargo add wickra`, batch and
|
||||
streaming via the `Indicator` and `BatchExt` traits, and the `Chain`
|
||||
combinator.
|
||||
- [Quickstart: Node](Quickstart-Node.md) — `npm install wickra`, basic
|
||||
`SMA` and `MACD` calls, and the install surface. Windows x64 was
|
||||
previously blocked by an npm spam filter on `wickra-win32-x64-msvc`;
|
||||
that was resolved with npm Support, and 0.2.1 is the first release in
|
||||
which `npm install wickra` works end-to-end on Windows.
|
||||
- [Quickstart: WASM](Quickstart-WASM.md) — `npm install wickra-wasm`,
|
||||
building with `wasm-pack`, and running indicators client-side in a
|
||||
browser or bundler.
|
||||
- [Data Layer](Data-Layer.md) — the `wickra-data` crate: the CSV reader,
|
||||
the tick-to-candle aggregator, the multi-timeframe resampler, and the
|
||||
Binance live feed.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — the conceptual difference
|
||||
between Wickra's O(1) `update` and the recompute-everything loops in
|
||||
batch-only libraries, with the benchmark numbers from the project README.
|
||||
- [Warmup Periods](Warmup-Periods.md) — a verified table of every
|
||||
indicator's `warmup_period()`, plus the reasoning behind the off-by-one
|
||||
cases (RSI(14) needs 15 inputs because it needs 14 diffs).
|
||||
- [Indicator Chaining](Indicator-Chaining.md) — `Chain::new(first, second)`
|
||||
and `.then(third)`, with a worked EMA(14) → RSI(7) example and the rule
|
||||
for stacked warmups.
|
||||
- [Cookbook](Cookbook.md) — copy-paste strategy recipes built on streaming
|
||||
indicators (RSI mean reversion, MACD crossover, Bollinger breakout,
|
||||
ADX-gated trend, multi-timeframe, SuperTrend trailing stop).
|
||||
- [TA-Lib Migration](TA-Lib-Migration.md) — function-by-function mapping
|
||||
table from TA-Lib's `talib.X(...)` calls to the equivalent Wickra
|
||||
expressions.
|
||||
- [FAQ](FAQ.md) — quick answers to the most common questions about
|
||||
warmup, NaN handling, thread safety, and the streaming-vs-batch contract.
|
||||
|
||||
### Indicator reference
|
||||
|
||||
Start with [Indicators-Overview.md](Indicators-Overview.md) for the full
|
||||
eight-family taxonomy and the shared `Indicator` trait surface. The
|
||||
per-indicator pages below cover formulas, parameters, warmup behaviour, edge
|
||||
cases, and verified Rust / Python / Node examples. They are grouped by family,
|
||||
mirroring the `indicators/<family>/` directory layout.
|
||||
|
||||
**Moving Averages** — smooth the price series to surface direction.
|
||||
|
||||
- [Indicator-Sma.md](indicators/moving-averages/Indicator-Sma.md)
|
||||
- [Indicator-Ema.md](indicators/moving-averages/Indicator-Ema.md)
|
||||
- [Indicator-Wma.md](indicators/moving-averages/Indicator-Wma.md)
|
||||
- [Indicator-Dema.md](indicators/moving-averages/Indicator-Dema.md)
|
||||
- [Indicator-Tema.md](indicators/moving-averages/Indicator-Tema.md)
|
||||
- [Indicator-Hma.md](indicators/moving-averages/Indicator-Hma.md)
|
||||
- [Indicator-Kama.md](indicators/moving-averages/Indicator-Kama.md)
|
||||
- [Indicator-Smma.md](indicators/moving-averages/Indicator-Smma.md)
|
||||
- [Indicator-Trima.md](indicators/moving-averages/Indicator-Trima.md)
|
||||
- [Indicator-Zlema.md](indicators/moving-averages/Indicator-Zlema.md)
|
||||
- [Indicator-T3.md](indicators/moving-averages/Indicator-T3.md)
|
||||
- [Indicator-Vwma.md](indicators/moving-averages/Indicator-Vwma.md)
|
||||
|
||||
**Momentum Oscillators** — measure the rate of price change.
|
||||
|
||||
- [Indicator-Rsi.md](indicators/momentum-oscillators/Indicator-Rsi.md)
|
||||
- [Indicator-Stochastic.md](indicators/momentum-oscillators/Indicator-Stochastic.md)
|
||||
- [Indicator-Cci.md](indicators/momentum-oscillators/Indicator-Cci.md)
|
||||
- [Indicator-Roc.md](indicators/momentum-oscillators/Indicator-Roc.md)
|
||||
- [Indicator-WilliamsR.md](indicators/momentum-oscillators/Indicator-WilliamsR.md)
|
||||
- [Indicator-Mfi.md](indicators/momentum-oscillators/Indicator-Mfi.md)
|
||||
- [Indicator-AwesomeOscillator.md](indicators/momentum-oscillators/Indicator-AwesomeOscillator.md)
|
||||
- [Indicator-Mom.md](indicators/momentum-oscillators/Indicator-Mom.md)
|
||||
- [Indicator-Cmo.md](indicators/momentum-oscillators/Indicator-Cmo.md)
|
||||
- [Indicator-Tsi.md](indicators/momentum-oscillators/Indicator-Tsi.md)
|
||||
- [Indicator-Pmo.md](indicators/momentum-oscillators/Indicator-Pmo.md)
|
||||
- [Indicator-StochRsi.md](indicators/momentum-oscillators/Indicator-StochRsi.md)
|
||||
- [Indicator-UltimateOscillator.md](indicators/momentum-oscillators/Indicator-UltimateOscillator.md)
|
||||
|
||||
**Trend & Directional** — is there a trend, and which way?
|
||||
|
||||
- [Indicator-MacdIndicator.md](indicators/trend-directional/Indicator-MacdIndicator.md)
|
||||
- [Indicator-Adx.md](indicators/trend-directional/Indicator-Adx.md)
|
||||
- [Indicator-Aroon.md](indicators/trend-directional/Indicator-Aroon.md)
|
||||
- [Indicator-Trix.md](indicators/trend-directional/Indicator-Trix.md)
|
||||
- [Indicator-AroonOscillator.md](indicators/trend-directional/Indicator-AroonOscillator.md)
|
||||
- [Indicator-Vortex.md](indicators/trend-directional/Indicator-Vortex.md)
|
||||
- [Indicator-MassIndex.md](indicators/trend-directional/Indicator-MassIndex.md)
|
||||
- [Indicator-ChoppinessIndex.md](indicators/trend-directional/Indicator-ChoppinessIndex.md)
|
||||
- [Indicator-VerticalHorizontalFilter.md](indicators/trend-directional/Indicator-VerticalHorizontalFilter.md)
|
||||
|
||||
**Price Oscillators** — difference-of-averages momentum around zero.
|
||||
|
||||
- [Indicator-Ppo.md](indicators/price-oscillators/Indicator-Ppo.md)
|
||||
- [Indicator-Dpo.md](indicators/price-oscillators/Indicator-Dpo.md)
|
||||
- [Indicator-Coppock.md](indicators/price-oscillators/Indicator-Coppock.md)
|
||||
- [Indicator-AcceleratorOscillator.md](indicators/price-oscillators/Indicator-AcceleratorOscillator.md)
|
||||
- [Indicator-BalanceOfPower.md](indicators/price-oscillators/Indicator-BalanceOfPower.md)
|
||||
|
||||
**Volatility & Bands** — dispersion measures and price envelopes.
|
||||
|
||||
- [Indicator-Atr.md](indicators/volatility-bands/Indicator-Atr.md)
|
||||
- [Indicator-BollingerBands.md](indicators/volatility-bands/Indicator-BollingerBands.md)
|
||||
- [Indicator-Keltner.md](indicators/volatility-bands/Indicator-Keltner.md)
|
||||
- [Indicator-Donchian.md](indicators/volatility-bands/Indicator-Donchian.md)
|
||||
- [Indicator-Natr.md](indicators/volatility-bands/Indicator-Natr.md)
|
||||
- [Indicator-StdDev.md](indicators/volatility-bands/Indicator-StdDev.md)
|
||||
- [Indicator-UlcerIndex.md](indicators/volatility-bands/Indicator-UlcerIndex.md)
|
||||
- [Indicator-HistoricalVolatility.md](indicators/volatility-bands/Indicator-HistoricalVolatility.md)
|
||||
- [Indicator-BollingerBandwidth.md](indicators/volatility-bands/Indicator-BollingerBandwidth.md)
|
||||
- [Indicator-PercentB.md](indicators/volatility-bands/Indicator-PercentB.md)
|
||||
- [Indicator-TrueRange.md](indicators/volatility-bands/Indicator-TrueRange.md)
|
||||
- [Indicator-ChaikinVolatility.md](indicators/volatility-bands/Indicator-ChaikinVolatility.md)
|
||||
|
||||
**Trailing Stops** — ATR-driven stop-loss trackers.
|
||||
|
||||
- [Indicator-Psar.md](indicators/trailing-stops/Indicator-Psar.md)
|
||||
- [Indicator-SuperTrend.md](indicators/trailing-stops/Indicator-SuperTrend.md)
|
||||
- [Indicator-ChandelierExit.md](indicators/trailing-stops/Indicator-ChandelierExit.md)
|
||||
- [Indicator-ChandeKrollStop.md](indicators/trailing-stops/Indicator-ChandeKrollStop.md)
|
||||
- [Indicator-AtrTrailingStop.md](indicators/trailing-stops/Indicator-AtrTrailingStop.md)
|
||||
|
||||
**Volume** — price moves weighted or confirmed by traded volume.
|
||||
|
||||
- [Indicator-Obv.md](indicators/volume/Indicator-Obv.md)
|
||||
- [Indicator-Vwap.md](indicators/volume/Indicator-Vwap.md)
|
||||
- [Indicator-Adl.md](indicators/volume/Indicator-Adl.md)
|
||||
- [Indicator-VolumePriceTrend.md](indicators/volume/Indicator-VolumePriceTrend.md)
|
||||
- [Indicator-ChaikinMoneyFlow.md](indicators/volume/Indicator-ChaikinMoneyFlow.md)
|
||||
- [Indicator-ChaikinOscillator.md](indicators/volume/Indicator-ChaikinOscillator.md)
|
||||
- [Indicator-ForceIndex.md](indicators/volume/Indicator-ForceIndex.md)
|
||||
- [Indicator-EaseOfMovement.md](indicators/volume/Indicator-EaseOfMovement.md)
|
||||
|
||||
**Price Statistics** — per-bar transforms and rolling regressions.
|
||||
|
||||
- [Indicator-TypicalPrice.md](indicators/price-statistics/Indicator-TypicalPrice.md)
|
||||
- [Indicator-MedianPrice.md](indicators/price-statistics/Indicator-MedianPrice.md)
|
||||
- [Indicator-WeightedClose.md](indicators/price-statistics/Indicator-WeightedClose.md)
|
||||
- [Indicator-LinearRegression.md](indicators/price-statistics/Indicator-LinearRegression.md)
|
||||
- [Indicator-LinRegSlope.md](indicators/price-statistics/Indicator-LinRegSlope.md)
|
||||
- [Indicator-ZScore.md](indicators/price-statistics/Indicator-ZScore.md)
|
||||
- [Indicator-LinRegAngle.md](indicators/price-statistics/Indicator-LinRegAngle.md)
|
||||
|
||||
## See also
|
||||
|
||||
- Source code: <https://github.com/kingchenc/wickra>
|
||||
- Releases: <https://github.com/kingchenc/wickra/releases>
|
||||
- Issue tracker: <https://github.com/kingchenc/wickra/issues>
|
||||
@@ -1,179 +0,0 @@
|
||||
# Indicator Chaining
|
||||
|
||||
`Chain<A, B>` wires the output of one indicator straight into the input of
|
||||
another. Both stages must agree on `f64` as the bridging type, which is the
|
||||
case for the vast majority of price-in / value-out indicators. The chain
|
||||
itself is an `Indicator`, so chains can be nested arbitrarily and used
|
||||
anywhere a single indicator is accepted.
|
||||
|
||||
This page documents the public API of `Chain` in
|
||||
`crates/wickra-core/src/traits.rs`, the worked EMA(14) → RSI(7) example
|
||||
that the doctest pins, and the warmup-stacking rule.
|
||||
|
||||
## Construction
|
||||
|
||||
```rust
|
||||
use wickra::{Chain, Ema, Rsi};
|
||||
|
||||
// Two stages.
|
||||
let chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
|
||||
// Three stages — note the `.then(third)` builder method.
|
||||
let triple = Chain::new(Ema::new(14)?, Ema::new(5)?).then(Rsi::new(7)?);
|
||||
```
|
||||
|
||||
The type of `triple` is `Chain<Chain<Ema, Ema>, Rsi>`. You can keep chaining
|
||||
indefinitely; the `Chain<A, B>` produced at each step also implements
|
||||
`Indicator<Input = f64, Output = ...>`, which is the constraint
|
||||
`.then(third)` needs to satisfy.
|
||||
|
||||
Both `first()` and `second()` accessors return references to the underlying
|
||||
stages if you need to inspect them; the chain owns its stages by value.
|
||||
|
||||
## Worked example: EMA(14) → RSI(7)
|
||||
|
||||
This is the canonical chain example from the doctest on `Chain` in
|
||||
`crates/wickra-core/src/traits.rs`:
|
||||
|
||||
```rust
|
||||
use wickra::{Chain, Ema, Indicator, Rsi};
|
||||
|
||||
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
for i in 1..=21 {
|
||||
chain.update(f64::from(i));
|
||||
}
|
||||
assert!(chain.is_ready());
|
||||
```
|
||||
|
||||
The semantic shape of the chain is: smooth the input series with an
|
||||
EMA(14), then compute an RSI(7) over the **smoothed** series — not the
|
||||
raw inputs. `chain.update(price)` is the only thing your caller code ever
|
||||
sees; the EMA-then-RSI plumbing is internal to the `Chain` value.
|
||||
|
||||
The chain emits its first non-`None` value at input **21**:
|
||||
|
||||
```rust
|
||||
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
for i in 1..=22 {
|
||||
if let Some(v) = chain.update(f64::from(i)) {
|
||||
println!("chain emitted at input #{i}: {v}");
|
||||
}
|
||||
}
|
||||
println!("chain.warmup_period() = {}", chain.warmup_period());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
chain emitted at input #21: 100
|
||||
chain emitted at input #22: 100
|
||||
chain.warmup_period() = 22
|
||||
```
|
||||
|
||||
(The value is `100` because the input is the monotonic ramp `1, 2, ..., 22`;
|
||||
an RSI on a strictly increasing series is `100` by construction. The point
|
||||
is the *timing* of the first emission.)
|
||||
|
||||
## Why first emission is at input 21, and `warmup_period` is 22
|
||||
|
||||
`Chain::warmup_period` is implemented conservatively:
|
||||
|
||||
```rust
|
||||
fn warmup_period(&self) -> usize {
|
||||
self.first.warmup_period() + self.second.warmup_period()
|
||||
}
|
||||
```
|
||||
|
||||
For `Chain::new(Ema::new(14)?, Rsi::new(7)?)` this expands to
|
||||
`14 + 8 = 22` (RSI(7)'s warmup is `period + 1 = 8` — see
|
||||
[Warmup Periods](Warmup-Periods.md) for the off-by-one detail).
|
||||
|
||||
In practice the chain emits one input earlier than that conservative sum
|
||||
because the moment EMA(14) starts producing values is input 14, and RSI(7)
|
||||
needs 8 EMA outputs to seed, so RSI(7) is ready on EMA output number 8,
|
||||
which corresponds to input 14 + 7 = **21**. The conservative formula
|
||||
`first.warmup + second.warmup` ignores this overlap; treat
|
||||
`warmup_period()` as an upper bound and `is_ready()` as the source of truth
|
||||
for "can I read a value yet":
|
||||
|
||||
```rust
|
||||
if chain.is_ready() {
|
||||
if let Some(v) = chain.update(price) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## State, reset, and `Send`
|
||||
|
||||
`Chain<A, B>` propagates `reset()` to both stages:
|
||||
|
||||
```rust
|
||||
fn reset(&mut self) {
|
||||
self.first.reset();
|
||||
self.second.reset();
|
||||
}
|
||||
```
|
||||
|
||||
So calling `chain.reset()` returns the whole pipeline to the state of a
|
||||
freshly constructed `Chain::new(A::new(...), B::new(...))`. Because both
|
||||
stages are owned by value and the trait `Indicator` is auto-derive-friendly,
|
||||
the chain inherits `Clone`, `Debug`, and (where each stage is) `Send`.
|
||||
|
||||
The `batch` extension comes through `BatchExt` automatically — there's
|
||||
nothing chain-specific to call:
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Chain, Ema, Rsi};
|
||||
|
||||
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
let out: Vec<Option<f64>> = chain.batch(&prices);
|
||||
```
|
||||
|
||||
## Stages from different families
|
||||
|
||||
The bridging type is `f64`, so anything `Indicator<Input = f64, Output = f64>`
|
||||
can serve as the first stage and anything `Indicator<Input = f64>` can serve
|
||||
as the second (or third, or fourth). Indicators that consume `Candle` /
|
||||
`Tick` (`Atr`, `Adx`, `Stochastic`, `Mfi`, `Vwap`, `Psar`, `Keltner`,
|
||||
`Donchian`, `Aroon`, `AwesomeOscillator`, `Obv`) cannot sit at the second
|
||||
stage of a chain because their `Input` is not `f64`. They can still be used
|
||||
as standalone indicators alongside a chain — wire them in your own loop.
|
||||
|
||||
A multi-output indicator like `MacdIndicator` or `BollingerBands` can sit
|
||||
last in a chain (its `Output` is a struct, but the chain only requires
|
||||
`Input = f64` on the **second** stage). For example,
|
||||
`Chain::new(Sma::new(20)?, MacdIndicator::classic())` is a valid type:
|
||||
MACD-of-smoothed-prices.
|
||||
|
||||
## Python and Node
|
||||
|
||||
`Chain` is currently a Rust-only construct; the Python and Node bindings
|
||||
expose individual indicators only. The straightforward equivalent in those
|
||||
languages is a manual two-step loop:
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
ema = ta.EMA(14)
|
||||
rsi = ta.RSI(7)
|
||||
|
||||
for price in prices:
|
||||
smoothed = ema.update(price)
|
||||
if smoothed is not None:
|
||||
chained = rsi.update(smoothed)
|
||||
if chained is not None:
|
||||
...
|
||||
```
|
||||
|
||||
This is exactly what `Chain::update` does in Rust, transcribed to the
|
||||
binding's `update` method. No information is lost.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Rust](Quickstart-Rust.md) — the `Chain` example in context.
|
||||
- [Warmup Periods](Warmup-Periods.md) — the underlying stage formulas, and
|
||||
the RSI `period + 1` off-by-one.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — `Chain` works with both
|
||||
paths automatically, via `BatchExt`.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,212 +0,0 @@
|
||||
# Indicators Overview
|
||||
|
||||
Wickra ships **71 indicators** organised into **eight families**. Each family
|
||||
collects indicators that answer the same kind of question and groups at least
|
||||
five of them, so the taxonomy here maps one-to-one onto the
|
||||
`docs/wiki/indicators/<family>/` directory layout.
|
||||
|
||||
Every indicator is an O(1) state machine that consumes one input at a time
|
||||
and produces either `Option<f64>` (Rust), `float | None` (Python), or
|
||||
`number | null` (Node). Inputs are either a `f64` close price or an OHLCV
|
||||
`Candle` (Rust) / dict-or-tuple (Python) / column arrays (Node). The full
|
||||
trait surface and warmup-period semantics are covered in
|
||||
[Quickstart: Rust](Quickstart-Rust.md) and [Warmup Periods](Warmup-Periods.md).
|
||||
|
||||
The "Output range" column is the value bounds an indicator emits once warm;
|
||||
"unbounded" means it tracks the price scale of the input. The "Warmup" column
|
||||
quotes `warmup_period()` as the indicator reports it — the **exact**
|
||||
first-emission index: the first non-`None` output lands on input
|
||||
`warmup_period()` (0-indexed `warmup_period() - 1`).
|
||||
|
||||
The eight families:
|
||||
|
||||
| # | Family | Count | What it answers |
|
||||
|---|--------|-------|-----------------|
|
||||
| 1 | [Moving Averages](#moving-averages) | 12 | Where is the smoothed trend line? |
|
||||
| 2 | [Momentum Oscillators](#momentum-oscillators) | 13 | How fast is price changing; is it overbought? |
|
||||
| 3 | [Trend & Directional](#trend--directional) | 9 | Is there a trend, and which way? |
|
||||
| 4 | [Price Oscillators](#price-oscillators) | 5 | Difference-of-averages momentum around zero. |
|
||||
| 5 | [Volatility & Bands](#volatility--bands) | 12 | How wide is the range; where are the envelopes? |
|
||||
| 6 | [Trailing Stops](#trailing-stops) | 5 | Where is the stop-loss for this trend? |
|
||||
| 7 | [Volume](#volume) | 8 | Is volume confirming the move? |
|
||||
| 8 | [Price Statistics](#price-statistics) | 7 | Per-bar price transforms and rolling regressions. |
|
||||
|
||||
## Moving Averages
|
||||
|
||||
Smooth the price series to surface direction. All are single-input,
|
||||
single-output (`f64 → f64`) except `Vwma`, which weights by volume.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Sma` | Equal-weighted rolling mean over `period` closes. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Sma.md](indicators/moving-averages/Indicator-Sma.md) |
|
||||
| `Ema` | EMA with `α = 2 / (period + 1)`, SMA-seeded. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Ema.md](indicators/moving-averages/Indicator-Ema.md) |
|
||||
| `Wma` | Linear weights `1, 2, …, period`; newest bar matters most. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Wma.md](indicators/moving-averages/Indicator-Wma.md) |
|
||||
| `Dema` | Mulloy's `2·EMA − EMA(EMA)`; removes first-order EMA lag. | `f64` | `f64` | unbounded (price scale) | `period` | `2·period − 1` | [Indicator-Dema.md](indicators/moving-averages/Indicator-Dema.md) |
|
||||
| `Tema` | Mulloy's `3·EMA − 3·EMA(EMA) + EMA(EMA(EMA))`. | `f64` | `f64` | unbounded (price scale) | `period` | `3·period − 2` | [Indicator-Tema.md](indicators/moving-averages/Indicator-Tema.md) |
|
||||
| `Hma` | Hull's near-zero-lag `WMA(2·WMA(n/2) − WMA(n), √n)`. | `f64` | `f64` | unbounded (price scale) | `period` | `period + round(√period) − 1` | [Indicator-Hma.md](indicators/moving-averages/Indicator-Hma.md) |
|
||||
| `Kama` | Kaufman's adaptive average; efficiency ratio picks α per bar. | `f64` | `f64` | unbounded (price scale) | `(er_period=10, fast=2, slow=30)` | `er_period + 1` | [Indicator-Kama.md](indicators/moving-averages/Indicator-Kama.md) |
|
||||
| `Smma` | Wilder's RMA: SMA-seeded exponential average, `1/period` factor. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Smma.md](indicators/moving-averages/Indicator-Smma.md) |
|
||||
| `Trima` | A `period`-window SMA applied twice; triangular weights. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Trima.md](indicators/moving-averages/Indicator-Trima.md) |
|
||||
| `Zlema` | EMA of the de-lagged series `2·price − price[lag]`. | `f64` | `f64` | unbounded (price scale) | `period` | `lag + period` | [Indicator-Zlema.md](indicators/moving-averages/Indicator-Zlema.md) |
|
||||
| `T3` | Tillson's six-EMA cascade recombined with a volume factor `v`. | `f64` | `f64` | unbounded (price scale) | `(period, v=0.7)` (Python) | `6·period − 5` | [Indicator-T3.md](indicators/moving-averages/Indicator-T3.md) |
|
||||
| `Vwma` | Rolling mean of closes weighted by each bar's volume. | `Candle` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Vwma.md](indicators/moving-averages/Indicator-Vwma.md) |
|
||||
|
||||
## Momentum Oscillators
|
||||
|
||||
Measure the *rate* of price change. Several are bounded by construction
|
||||
(0–100 / ±100 oscillators), the rest are difference-driven.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Rsi` | Wilder's RSI; smoothed `gain / (gain + loss) × 100`. | `f64` | `f64` | `[0, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Rsi.md](indicators/momentum-oscillators/Indicator-Rsi.md) |
|
||||
| `Stochastic` | `%K = (close − low_n)/(high_n − low_n) × 100`, smoothed into `%D`. | `Candle` | `(k, d)` | each in `[0, 100]` | `(k_period=14, d_period=3)` (Python) | `k_period + d_period − 1` | [Indicator-Stochastic.md](indicators/momentum-oscillators/Indicator-Stochastic.md) |
|
||||
| `Cci` | `(typical − SMA(typical)) / (0.015 · mean_dev)`. | `Candle` | `f64` | unbounded (typically `±100`–`±200`) | `period = 20` (Python) | `period` | [Indicator-Cci.md](indicators/momentum-oscillators/Indicator-Cci.md) |
|
||||
| `Roc` | `(price − price_n) / price_n × 100`; raw percentage change. | `f64` | `f64` | unbounded around zero | `period` | `period + 1` | [Indicator-Roc.md](indicators/momentum-oscillators/Indicator-Roc.md) |
|
||||
| `WilliamsR` | `−100 × (high_n − close) / (high_n − low_n)`. | `Candle` | `f64` | `[−100, 0]` | `period = 14` (Python) | `period` | [Indicator-WilliamsR.md](indicators/momentum-oscillators/Indicator-WilliamsR.md) |
|
||||
| `Mfi` | "Volume-weighted RSI": Wilder smoothing of money-flow ratios. | `Candle` | `f64` | `[0, 100]` | `period = 14` (Python) | `period` | [Indicator-Mfi.md](indicators/momentum-oscillators/Indicator-Mfi.md) |
|
||||
| `AwesomeOscillator` | `SMA(median, fast) − SMA(median, slow)`; zero-line crossover. | `Candle` | `f64` | unbounded around zero | `(fast=5, slow=34)` (Python) | `slow_period` | [Indicator-AwesomeOscillator.md](indicators/momentum-oscillators/Indicator-AwesomeOscillator.md) |
|
||||
| `Mom` | `price − price[period]`; raw price-difference momentum. | `f64` | `f64` | unbounded around zero | `period = 10` (Python) | `period + 1` | [Indicator-Mom.md](indicators/momentum-oscillators/Indicator-Mom.md) |
|
||||
| `Cmo` | Chande Momentum Oscillator; `100·(Σgain − Σloss)/(Σgain + Σloss)`. | `f64` | `f64` | `[−100, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Cmo.md](indicators/momentum-oscillators/Indicator-Cmo.md) |
|
||||
| `Tsi` | True Strength Index; double-EMA-smoothed momentum ratio. | `f64` | `f64` | ≈ `[−100, 100]` | `(long=25, short=13)` (Python) | `long + short` | [Indicator-Tsi.md](indicators/momentum-oscillators/Indicator-Tsi.md) |
|
||||
| `Pmo` | DecisionPoint Price Momentum Oscillator; doubly-smoothed ROC. | `f64` | `f64` | unbounded around zero | `(smoothing1=35, smoothing2=20)` (Python) | `2` | [Indicator-Pmo.md](indicators/momentum-oscillators/Indicator-Pmo.md) |
|
||||
| `StochRsi` | Stochastic Oscillator applied to the RSI series. | `f64` | `f64` | `[0, 100]` | `(rsi_period=14, stoch_period=14)` (Python) | `rsi_period + stoch_period` | [Indicator-StochRsi.md](indicators/momentum-oscillators/Indicator-StochRsi.md) |
|
||||
| `UltimateOscillator` | Larry Williams' weighted three-timeframe buying-pressure oscillator. | `Candle` | `f64` | `[0, 100]` | `(short=7, mid=14, long=28)` (Python) | `max(short,mid,long) + 1` | [Indicator-UltimateOscillator.md](indicators/momentum-oscillators/Indicator-UltimateOscillator.md) |
|
||||
|
||||
## Trend & Directional
|
||||
|
||||
Answer whether a trend exists and which way it points — directional systems,
|
||||
crossover packages and trend-versus-range filters.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `MacdIndicator` | `EMA(fast) − EMA(slow)` plus a signal EMA and the histogram. | `f64` | `(macd, signal, histogram)` | unbounded around zero | `(fast=12, slow=26, signal=9)` (Python) | `slow + signal − 1` | [Indicator-MacdIndicator.md](indicators/trend-directional/Indicator-MacdIndicator.md) |
|
||||
| `Adx` | Wilder's directional system: `+DI`, `−DI` and the `ADX` strength index. | `Candle` | `(plus_di, minus_di, adx)` | each in `[0, 100]` | `period = 14` (Python) | `2·period` | [Indicator-Adx.md](indicators/trend-directional/Indicator-Adx.md) |
|
||||
| `Aroon` | Bars-since-high and bars-since-low scaled to `[0, 100]`. | `Candle` | `(up, down)` | each in `[0, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Aroon.md](indicators/trend-directional/Indicator-Aroon.md) |
|
||||
| `Trix` | Rate of change of a triple-smoothed EMA, `× 10000`. | `f64` | `f64` | unbounded around zero | `period = 15` (Python) | `3·period − 1` | [Indicator-Trix.md](indicators/trend-directional/Indicator-Trix.md) |
|
||||
| `AroonOscillator` | `AroonUp − AroonDown`; the two Aroon lines as one gauge. | `Candle` | `f64` | `[−100, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-AroonOscillator.md](indicators/trend-directional/Indicator-AroonOscillator.md) |
|
||||
| `Vortex` | Vortex Indicator `VI+` / `VI−`; crossings mark trend onset. | `Candle` | `(plus, minus)` | each `>= 0` | `period = 14` (Python) | `period + 1` | [Indicator-Vortex.md](indicators/trend-directional/Indicator-Vortex.md) |
|
||||
| `MassIndex` | Dorsey's range-expansion sum of the EMA-of-range ratio. | `Candle` | `f64` | `> 0` | `(ema_period=9, sum_period=25)` (Python) | `2·ema_period + sum_period − 2` | [Indicator-MassIndex.md](indicators/trend-directional/Indicator-MassIndex.md) |
|
||||
| `ChoppinessIndex` | Summed true range over the high-low span, log-scaled. | `Candle` | `f64` | `[0, 100]` | `period = 14` (Python) | `period` | [Indicator-ChoppinessIndex.md](indicators/trend-directional/Indicator-ChoppinessIndex.md) |
|
||||
| `VerticalHorizontalFilter` | Net price move divided by total move over `period`. | `f64` | `f64` | `[0, 1]` | `period = 28` (Python) | `period + 1` | [Indicator-VerticalHorizontalFilter.md](indicators/trend-directional/Indicator-VerticalHorizontalFilter.md) |
|
||||
|
||||
## Price Oscillators
|
||||
|
||||
Difference-of-averages and intrabar oscillators that swing around a zero line.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Ppo` | Percentage Price Oscillator; `100·(EMA_fast − EMA_slow)/EMA_slow`. | `f64` | `f64` | unbounded around zero (percent) | `(fast=12, slow=26)` (Python) | `slow` | [Indicator-Ppo.md](indicators/price-oscillators/Indicator-Ppo.md) |
|
||||
| `Dpo` | Detrended Price Oscillator; `price[t − period/2 − 1] − SMA(period)`. | `f64` | `f64` | unbounded around zero | `period = 20` (Python) | `max(period, period/2 + 2)` | [Indicator-Dpo.md](indicators/price-oscillators/Indicator-Dpo.md) |
|
||||
| `Coppock` | Coppock Curve; `WMA(ROC(long) + ROC(short), wma_period)`. | `f64` | `f64` | unbounded around zero | `(roc_long=14, roc_short=11, wma_period=10)` (Python) | `max(roc_long, roc_short) + wma_period` | [Indicator-Coppock.md](indicators/price-oscillators/Indicator-Coppock.md) |
|
||||
| `AcceleratorOscillator` | `AO − SMA(AO, signal)`; the acceleration of momentum. | `Candle` | `f64` | unbounded around zero | `(ao_fast=5, ao_slow=34, signal_period=5)` (Python) | `ao_slow + signal_period − 1` | [Indicator-AcceleratorOscillator.md](indicators/price-oscillators/Indicator-AcceleratorOscillator.md) |
|
||||
| `BalanceOfPower` | `(close − open) / (high − low)`; intrabar buyer/seller control. | `Candle` | `f64` | `[−1, +1]` | (no parameters) | `1` | [Indicator-BalanceOfPower.md](indicators/price-oscillators/Indicator-BalanceOfPower.md) |
|
||||
|
||||
## Volatility & Bands
|
||||
|
||||
Indicators that measure dispersion / range and those that draw an envelope
|
||||
around price.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Atr` | Wilder-smoothed True Range; per-bar absolute volatility. | `Candle` | `f64` | `[0, ∞)` (price scale) | `period = 14` (Python) | `period` | [Indicator-Atr.md](indicators/volatility-bands/Indicator-Atr.md) |
|
||||
| `BollingerBands` | SMA middle band with `±multiplier × population_stddev` bands. | `f64` | `(upper, middle, lower, stddev)` | unbounded (price scale) | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-BollingerBands.md](indicators/volatility-bands/Indicator-BollingerBands.md) |
|
||||
| `Keltner` | EMA middle band with `±multiplier × ATR` bands. | `Candle` | `(upper, middle, lower)` | unbounded (price scale) | `(ema_period=20, atr_period=10, multiplier=2.0)` (Python) | `max(ema_period, atr_period)` | [Indicator-Keltner.md](indicators/volatility-bands/Indicator-Keltner.md) |
|
||||
| `Donchian` | Highest high and lowest low over `period` bars. | `Candle` | `(upper, middle, lower)` | unbounded (price scale) | `period = 20` (Python) | `period` | [Indicator-Donchian.md](indicators/volatility-bands/Indicator-Donchian.md) |
|
||||
| `Natr` | `100·ATR/close`; ATR as a percentage. | `Candle` | `f64` | `[0, ∞)` (percent) | `period = 14` (Python) | `period` | [Indicator-Natr.md](indicators/volatility-bands/Indicator-Natr.md) |
|
||||
| `StdDev` | Rolling population standard deviation of price. | `f64` | `f64` | `[0, ∞)` (price scale) | `period = 20` (Python) | `period` | [Indicator-StdDev.md](indicators/volatility-bands/Indicator-StdDev.md) |
|
||||
| `UlcerIndex` | RMS of trailing-high drawdowns; downside-only risk. | `f64` | `f64` | `[0, ∞)` (percent) | `period = 14` (Python) | `2·period − 1` | [Indicator-UlcerIndex.md](indicators/volatility-bands/Indicator-UlcerIndex.md) |
|
||||
| `HistoricalVolatility` | Annualised sample stddev of log returns. | `f64` | `f64` | `[0, ∞)` (annualised percent) | `(period=20, trading_periods=252)` (Python) | `period + 1` | [Indicator-HistoricalVolatility.md](indicators/volatility-bands/Indicator-HistoricalVolatility.md) |
|
||||
| `BollingerBandwidth` | `(upper − lower) / middle` of the Bollinger Bands. | `f64` | `f64` | `[0, ∞)` | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-BollingerBandwidth.md](indicators/volatility-bands/Indicator-BollingerBandwidth.md) |
|
||||
| `PercentB` | `(price − lower) / (upper − lower)`; price position in the bands. | `f64` | `f64` | unbounded (`0`–`1` inside) | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-PercentB.md](indicators/volatility-bands/Indicator-PercentB.md) |
|
||||
| `TrueRange` | `max(H−L, |H−prevC|, |L−prevC|)`; raw single-bar volatility. | `Candle` | `f64` | `[0, ∞)` (price scale) | (no parameters) | `1` | [Indicator-TrueRange.md](indicators/volatility-bands/Indicator-TrueRange.md) |
|
||||
| `ChaikinVolatility` | Rate of change of an EMA-smoothed high-low spread. | `Candle` | `f64` | unbounded around zero (percent) | `(ema_period=10, roc_period=10)` (Python) | `ema_period + roc_period` | [Indicator-ChaikinVolatility.md](indicators/volatility-bands/Indicator-ChaikinVolatility.md) |
|
||||
|
||||
## Trailing Stops
|
||||
|
||||
ATR-driven stop-loss trackers: per-bar levels that follow a trend and flip
|
||||
when price closes through them.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Psar` | Wilder's Parabolic Stop-and-Reverse; flips sides on a crossing. | `Candle` | `f64` | unbounded (price scale) | `(af_start=0.02, af_step=0.02, af_max=0.20)` (Python) | `2` | [Indicator-Psar.md](indicators/trailing-stops/Indicator-Psar.md) |
|
||||
| `SuperTrend` | ATR-banded trailing stop with explicit flip logic. | `Candle` | `(value, direction)` | `value` price scale; `direction` `±1` | `(atr_period=10, multiplier=3.0)` (Python) | `atr_period` | [Indicator-SuperTrend.md](indicators/trailing-stops/Indicator-SuperTrend.md) |
|
||||
| `ChandelierExit` | `highest_high − k·ATR` (long) and `lowest_low + k·ATR` (short). | `Candle` | `(long_stop, short_stop)` | unbounded (price scale) | `(period=22, multiplier=3.0)` (Python) | `period` | [Indicator-ChandelierExit.md](indicators/trailing-stops/Indicator-ChandelierExit.md) |
|
||||
| `ChandeKrollStop` | Two-stage ATR stop: extreme-based, then smoothed. | `Candle` | `(stop_long, stop_short)` | unbounded (price scale) | `(atr_period=10, atr_multiplier=1.0, stop_period=9)` (Python) | `atr_period + stop_period − 1` | [Indicator-ChandeKrollStop.md](indicators/trailing-stops/Indicator-ChandeKrollStop.md) |
|
||||
| `AtrTrailingStop` | A single line trailing the close by `k·ATR`, ratcheting. | `Candle` | `f64` | unbounded (price scale) | `(atr_period=14, multiplier=3.0)` (Python) | `atr_period` | [Indicator-AtrTrailingStop.md](indicators/trailing-stops/Indicator-AtrTrailingStop.md) |
|
||||
|
||||
## Volume
|
||||
|
||||
Price moves weighted or confirmed by traded volume. All take `Candle` input.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `Obv` | On-Balance Volume: cumulative signed volume. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-Obv.md](indicators/volume/Indicator-Obv.md) |
|
||||
| `Vwap` | Cumulative volume-weighted average price from the stream start; a sibling `RollingVwap(period)` is exposed for a finite window. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` (cumulative); `period` (rolling) | [Indicator-Vwap.md](indicators/volume/Indicator-Vwap.md) (cumulative + [rolling](indicators/volume/Indicator-Vwap.md#rollingvwap-finite-window)) |
|
||||
| `Adl` | Accumulation/Distribution Line; cumulative range-weighted volume. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-Adl.md](indicators/volume/Indicator-Adl.md) |
|
||||
| `VolumePriceTrend` | Cumulative `volume · ROC`; volume weighted by percentage move. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-VolumePriceTrend.md](indicators/volume/Indicator-VolumePriceTrend.md) |
|
||||
| `ChaikinMoneyFlow` | Summed money-flow volume over summed volume across `period` bars. | `Candle` | `f64` | `[−1, +1]` | `period = 20` (Python) | `period` | [Indicator-ChaikinMoneyFlow.md](indicators/volume/Indicator-ChaikinMoneyFlow.md) |
|
||||
| `ChaikinOscillator` | `EMA(ADL, fast) − EMA(ADL, slow)`; the MACD of the ADL. | `Candle` | `f64` | unbounded around zero | `(fast=3, slow=10)` (Python) | `slow` | [Indicator-ChaikinOscillator.md](indicators/volume/Indicator-ChaikinOscillator.md) |
|
||||
| `ForceIndex` | `EMA((close − prev_close) · volume, period)`. | `Candle` | `f64` | unbounded around zero | `period = 13` (Python) | `period + 1` | [Indicator-ForceIndex.md](indicators/volume/Indicator-ForceIndex.md) |
|
||||
| `EaseOfMovement` | `SMA` of distance travelled per unit of volume. | `Candle` | `f64` | unbounded around zero | `(period=14, divisor=1e8)` (Python) | `period + 1` | [Indicator-EaseOfMovement.md](indicators/volume/Indicator-EaseOfMovement.md) |
|
||||
|
||||
## Price Statistics
|
||||
|
||||
Per-bar price transforms and rolling least-squares regressions.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `TypicalPrice` | `(high + low + close) / 3`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-TypicalPrice.md](indicators/price-statistics/Indicator-TypicalPrice.md) |
|
||||
| `MedianPrice` | `(high + low) / 2`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-MedianPrice.md](indicators/price-statistics/Indicator-MedianPrice.md) |
|
||||
| `WeightedClose` | `(high + low + 2·close) / 4`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-WeightedClose.md](indicators/price-statistics/Indicator-WeightedClose.md) |
|
||||
| `LinearRegression` | Endpoint of the rolling least-squares line. | `f64` | `f64` | unbounded (price scale) | `period = 14` (Python) | `period` | [Indicator-LinearRegression.md](indicators/price-statistics/Indicator-LinearRegression.md) |
|
||||
| `LinRegSlope` | Slope of the rolling least-squares line. | `f64` | `f64` | unbounded around zero | `period = 14` (Python) | `period` | [Indicator-LinRegSlope.md](indicators/price-statistics/Indicator-LinRegSlope.md) |
|
||||
| `ZScore` | `(price − SMA(n)) / population_stddev(n)`. | `f64` | `f64` | unbounded around zero | `period = 20` (Python) | `period` | [Indicator-ZScore.md](indicators/price-statistics/Indicator-ZScore.md) |
|
||||
| `LinRegAngle` | The rolling regression slope as a degree angle. | `f64` | `f64` | `(−90°, +90°)` | `period = 14` (Python) | `period` | [Indicator-LinRegAngle.md](indicators/price-statistics/Indicator-LinRegAngle.md) |
|
||||
|
||||
## Pick the right indicator for…
|
||||
|
||||
A short cheat-sheet of "I want X, which indicator?" answers, grounded in
|
||||
what each indicator actually computes.
|
||||
|
||||
- **Fast trend filter, minimal lag.** `Hma` for smoothness + responsiveness,
|
||||
`Tema` for further lag reduction at the cost of noise, `Kama` for
|
||||
adaptiveness instead of fixed lag.
|
||||
- **Slow trend filter.** `Sma` is the simplest; `Ema` responds slightly
|
||||
faster with the same smoothness budget.
|
||||
- **Trend-following crossovers.** Two-line crossovers are the textbook entry;
|
||||
`MacdIndicator` packages the idea with a signal line and histogram.
|
||||
- **Trend strength — is there a trend at all?** `Adx` (`> 25` trending,
|
||||
`< 20` ranging); `ChoppinessIndex` / `VerticalHorizontalFilter` answer the
|
||||
same question without a direction.
|
||||
- **Overbought / oversold.** `Rsi` is the default; `Stochastic` for faster
|
||||
signals; `WilliamsR` for an inverted scale; `Mfi` for a volume-aware RSI.
|
||||
- **Volatility level vs. momentum.** `Atr` / `TrueRange` for the level;
|
||||
`ChaikinVolatility` for whether ranges are expanding or contracting.
|
||||
- **Breakout level.** `Donchian` upper/lower bands are the Turtle-style
|
||||
trigger.
|
||||
- **Trailing stop.** `Psar`, `SuperTrend`, `ChandelierExit`,
|
||||
`ChandeKrollStop` and `AtrTrailingStop` are a whole family of them.
|
||||
- **Volume confirmation.** `Obv` is the simplest; `ChaikinMoneyFlow` is a
|
||||
bounded balance; `Vwap` / `RollingVwap` give a volume-weighted reference.
|
||||
- **Mean reversion.** `ZScore` flags statistically stretched prices;
|
||||
`BollingerBandwidth` / `PercentB` locate price within the bands.
|
||||
|
||||
## Source-of-truth files
|
||||
|
||||
Every claim above can be checked against the source in
|
||||
[`crates/wickra-core/src/indicators/`](https://github.com/kingchenc/wickra/tree/main/crates/wickra-core/src/indicators)
|
||||
— one file per indicator. The Rust unit tests inside each module are the
|
||||
ground truth for sample values. Python defaults (the `period = 14` etc.) come
|
||||
from the `#[pyo3(signature = …)]` attributes in
|
||||
[`bindings/python/src/lib.rs`](https://github.com/kingchenc/wickra/blob/main/bindings/python/src/lib.rs);
|
||||
indicators without a Python default require an explicit argument.
|
||||
|
||||
## See also
|
||||
|
||||
- [Warmup Periods](Warmup-Periods.md) — verified table of every indicator's
|
||||
`warmup_period()`.
|
||||
- [Indicator Chaining](Indicator-Chaining.md) — combining indicators with
|
||||
`Chain` and the stacked-warmup rule.
|
||||
- [Quickstart: Rust](Quickstart-Rust.md), [Quickstart: Python](Quickstart-Python.md),
|
||||
[Quickstart: Node](Quickstart-Node.md) — language-specific API surfaces.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,174 +0,0 @@
|
||||
# Quickstart: Node
|
||||
|
||||
A five-minute tour of the Wickra Node.js binding. The binding is generated by
|
||||
[napi-rs](https://napi.rs/), so it's a native addon — no WebAssembly, no
|
||||
slow JS reimplementation.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm install wickra
|
||||
```
|
||||
|
||||
> **Windows install (0.2.1+).** Earlier patch releases were blocked on
|
||||
> Windows x64 because the platform-specific sub-package
|
||||
> `wickra-win32-x64-msvc` was held back by npm's automated spam filter, so
|
||||
> `require('wickra')` threw `Error: Cannot find module
|
||||
> 'wickra-win32-x64-msvc'` after a successful `npm install`. npm Support
|
||||
> released the name on 2026-05-22; 0.2.1 is the first version in which
|
||||
> Windows x64 installs cleanly end-to-end (version numbers `0.1.1`–`0.1.4`
|
||||
> of that sub-package remain burned and cannot be republished — see the
|
||||
> npm registry page for `wickra-win32-x64-msvc`). Linux x64, Linux arm64
|
||||
> and macOS (x64 + arm64) were unaffected throughout.
|
||||
|
||||
## A first run
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
console.log('wickra', wickra.version());
|
||||
|
||||
// Simple moving average over a fixed window.
|
||||
const sma = new wickra.SMA(3);
|
||||
console.log(sma.batch([2, 4, 6, 8, 10]));
|
||||
// -> [ NaN, NaN, 4, 6, 8 ]
|
||||
```
|
||||
|
||||
Two things to notice:
|
||||
|
||||
1. The `batch` return type is a regular JavaScript `Array<number>`. Warmup
|
||||
slots are `NaN`, not `null` or `undefined`, so the array is
|
||||
`Number.isFinite`-checkable in one pass.
|
||||
2. `new wickra.SMA(0)` does **not** throw. Constructors in the Node binding
|
||||
currently cannot raise errors from JS (a napi-rs 2.16 limitation), so
|
||||
pathological values like `period = 0` are clamped to the smallest valid
|
||||
window. This is exactly the behaviour pinned by
|
||||
`bindings/node/__tests__/smoke.test.js` ("zero period is clamped to a
|
||||
valid window").
|
||||
|
||||
## Streaming
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const sma = new wickra.SMA(3);
|
||||
for (const price of [2, 4, 6, 8, 10]) {
|
||||
const value = sma.update(price);
|
||||
console.log('update', price, '->', value);
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
update 2 -> null
|
||||
update 4 -> null
|
||||
update 6 -> 4
|
||||
update 8 -> 6
|
||||
update 10 -> 8
|
||||
```
|
||||
|
||||
`update` returns either a JavaScript `number` or `null` while the indicator
|
||||
is still warming up. (Compare with `batch`, where warmup slots are `NaN`.
|
||||
The asymmetry exists because the streaming API surfaces "no value yet"
|
||||
through the JS type system, whereas the batch result is shaped as a numeric
|
||||
array for downstream numeric code.)
|
||||
|
||||
## MACD: streaming and batch
|
||||
|
||||
`MACD` is the canonical multi-output indicator. The Node API surfaces this in
|
||||
two shapes:
|
||||
|
||||
- `update(price)` returns either `null` (during warmup) or
|
||||
`{ macd, signal, histogram }`.
|
||||
- `batch(prices)` returns a **flat** `Array<number>` of length `prices.length * 3`,
|
||||
laid out as `[macd_0, signal_0, hist_0, macd_1, signal_1, hist_1, ...]`,
|
||||
with each warmup row written as three `NaN`s.
|
||||
|
||||
Streaming form:
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const macd = new wickra.MACD(12, 26, 9);
|
||||
let last = null;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
last = macd.update(100 + i * 0.5);
|
||||
}
|
||||
console.log(last);
|
||||
// -> { macd: 3.5, signal: 3.500000000000001, histogram: -8.881784197001252e-16 }
|
||||
```
|
||||
|
||||
Batch form (note the flat layout):
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const prices = Array.from({ length: 40 }, (_, i) => 100 + i * 0.5);
|
||||
const macd = new wickra.MACD(12, 26, 9);
|
||||
const flat = macd.batch(prices);
|
||||
|
||||
console.log('total values:', flat.length); // -> 120 (= 40 * 3)
|
||||
console.log('row 33 :', flat.slice(99, 102)); // -> [ 3.5, 3.5, 0 ]
|
||||
console.log('row 39 :', flat.slice(117, 120)); // -> [ 3.5, 3.5000000..., -8.88e-16 ]
|
||||
|
||||
// Reshape into 3-tuples if you want:
|
||||
const rows = [];
|
||||
for (let i = 0; i < flat.length; i += 3) {
|
||||
rows.push({ macd: flat[i], signal: flat[i + 1], histogram: flat[i + 2] });
|
||||
}
|
||||
```
|
||||
|
||||
`MACD(12, 26, 9)` emits its first non-NaN row at index 33 because the
|
||||
underlying Rust `warmup_period()` is `slow + signal − 1 = 34` (the first
|
||||
ready row is `warmup_period - 1` in 0-indexed terms).
|
||||
|
||||
## API surface
|
||||
|
||||
The complete TypeScript definitions live at
|
||||
`bindings/node/index.d.ts`. Every indicator class exposes some subset of:
|
||||
|
||||
| Member | Notes |
|
||||
|---------------------------|------------------------------------------------------------------------|
|
||||
| `constructor(...)` | Pathological values are clamped, not thrown. |
|
||||
| `update(...)` | Returns the indicator output or `null` during warmup. |
|
||||
| `batch(...)` | Single-output: flat `Array<number>` with `NaN` warmup.<br>Multi-output: flat interleaved `Array<number>`. |
|
||||
| `reset()` | Returns to a freshly-constructed state. |
|
||||
| `isReady()` | `true` once the first value has been emitted. |
|
||||
| `warmupPeriod()` | Present on every indicator class (single- and multi-output, scalar- and candle-input) since `0.2.1`. |
|
||||
|
||||
A complete reference run lives in `bindings/node/__tests__/smoke.test.js`:
|
||||
|
||||
```bash
|
||||
cd bindings/node
|
||||
npm install
|
||||
npm run build # only needed if you cloned the repo (Windows: see above)
|
||||
npm test
|
||||
```
|
||||
|
||||
## Building from source (Windows workaround)
|
||||
|
||||
If you are on Windows x64 and want to use Wickra today, build the binding
|
||||
locally from the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/kingchenc/wickra
|
||||
cd wickra/bindings/node
|
||||
npm install
|
||||
npm run build # requires a Rust toolchain (rustup) on PATH
|
||||
npm test
|
||||
```
|
||||
|
||||
`npm run build` produces `wickra.win32-x64-msvc.node` in the binding
|
||||
directory; the platform loader in `index.js` then picks it up before falling
|
||||
through to the npm sub-package and the install just works.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Python](Quickstart-Python.md) — sibling binding with NumPy
|
||||
shapes.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — why `update` is the primary
|
||||
entry point, not `batch`.
|
||||
- [Warmup Periods](Warmup-Periods.md) — exact `warmup_period()` for every
|
||||
indicator.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,181 +0,0 @@
|
||||
# Quickstart: Python
|
||||
|
||||
A five-minute tour of the Wickra Python binding. By the end you will have run
|
||||
a batch RSI over a NumPy array, fed the same indicator one tick at a time,
|
||||
and read a multi-column MACD result correctly during warmup.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
pip install wickra
|
||||
```
|
||||
|
||||
The published wheels target Python 3.9 – 3.12 on Linux x86_64, macOS
|
||||
(Intel + Apple Silicon), and Windows x86_64. The only runtime dependency is
|
||||
`numpy >= 1.22`. No system compiler, no C headers, no Rust toolchain are
|
||||
needed to install — Wickra ships pre-built native wheels.
|
||||
|
||||
Verify the install:
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
print(ta.__version__)
|
||||
```
|
||||
|
||||
## Batch: RSI over a NumPy array
|
||||
|
||||
`Indicator.batch(prices)` takes a 1-D `numpy.ndarray` of `float64` closes and
|
||||
returns a 1-D `numpy.ndarray` of `float64` outputs. Warmup steps come back as
|
||||
`NaN` so the result aligns 1:1 with your input prices and slots straight into
|
||||
a pandas column or a NumPy mask.
|
||||
|
||||
The first 15 prices below are the classic Wilder textbook example. RSI(14)
|
||||
emits its first value at index 14 (the 15th input) because it needs 14
|
||||
diffs to seed Wilder's smoothing.
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
prices = np.array([
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
||||
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
||||
46.03, 46.41, 46.22, 45.64,
|
||||
], dtype=float)
|
||||
|
||||
rsi = ta.RSI(14)
|
||||
values = rsi.batch(prices)
|
||||
|
||||
print(values.dtype, values.shape)
|
||||
print("warmup count:", int(np.isnan(values).sum()))
|
||||
print("first value :", float(values[14]))
|
||||
print("last value :", float(values[-1]))
|
||||
```
|
||||
|
||||
Running this prints:
|
||||
|
||||
```
|
||||
float64 (20,)
|
||||
warmup count: 14
|
||||
first value : 70.46413502109705
|
||||
last value : 57.91502067008556
|
||||
```
|
||||
|
||||
The exact first value `70.464` matches Wilder's published table; this is the
|
||||
same input/output pair the Rust test suite pins as `classic_wilder_textbook_values`
|
||||
in `crates/wickra-core/src/indicators/rsi.rs`.
|
||||
|
||||
## Streaming: feed one price at a time
|
||||
|
||||
The same `RSI` instance can be driven tick-by-tick with `update()`. Each call
|
||||
is O(1) and returns either a `float` or `None` while the indicator is still
|
||||
warming up.
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
rsi = ta.RSI(14)
|
||||
prices = [
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
||||
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
||||
46.03, 46.41,
|
||||
]
|
||||
|
||||
for tick, price in enumerate(prices, start=1):
|
||||
value = rsi.update(price)
|
||||
if value is not None:
|
||||
print(f"tick {tick:2d} close={price:.2f} rsi={value:.4f}")
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
tick 15 close=46.28 rsi=70.4641
|
||||
tick 16 close=46.00 rsi=66.2496
|
||||
tick 17 close=46.03 rsi=66.4809
|
||||
tick 18 close=46.41 rsi=69.3469
|
||||
```
|
||||
|
||||
Tick 15 is the first emission because `RSI(14).warmup_period() == 15`. Before
|
||||
that, `update()` returns `None`. After warmup the indicator never goes back
|
||||
to `None`: each subsequent tick produces a steady value.
|
||||
|
||||
The full set of streaming-state methods is:
|
||||
|
||||
| Method | Returns | Notes |
|
||||
|-----------------------|----------------|--------------------------------------------|
|
||||
| `update(price)` | `float`/`None` | O(1) state transition, `None` during warmup |
|
||||
| `batch(prices)` | `np.ndarray` | replays `update`, `NaN` during warmup |
|
||||
| `reset()` | `None` | returns to a freshly-constructed state |
|
||||
| `is_ready()` | `bool` | `True` once the first value has been emitted |
|
||||
| `warmup_period()` | `int` | inputs required before the first value |
|
||||
|
||||
## MACD: a multi-column indicator and its warmup NaNs
|
||||
|
||||
Some indicators emit several values at once. `MACD` returns three: the MACD
|
||||
line, the signal line, and the histogram. The Python `batch` reflects that
|
||||
shape directly — instead of a 1-D `float64` vector you get a 2-D
|
||||
`(n_rows, n_columns)` array, and each warmup row is filled with `NaN` across
|
||||
every column. (`Stochastic` follows the same pattern with two columns,
|
||||
`Bollinger Bands` with four, `Keltner`/`Donchian`/`ADX` with three.)
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
prices = np.linspace(100.0, 120.0, 40)
|
||||
macd = ta.MACD(12, 26, 9)
|
||||
out = macd.batch(prices)
|
||||
|
||||
print("shape :", out.shape)
|
||||
print("warmup rows :", int(np.isnan(out[:, 0]).sum()))
|
||||
print("first ready :", int(np.argmax(~np.isnan(out[:, 0]))))
|
||||
print("row 33 :", out[33])
|
||||
print("row 39 :", out[39])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
shape : (40, 3)
|
||||
warmup rows : 33
|
||||
first ready : 33
|
||||
row 33 : [ 3.58974359e+00 3.58974359e+00 -1.77635684e-15]
|
||||
row 39 : [3.58974359e+00 3.58974359e+00 6.21724894e-15]
|
||||
```
|
||||
|
||||
Two things to notice:
|
||||
|
||||
1. `MACD(12, 26, 9).warmup_period()` is `slow + signal - 1 = 34`, and indeed
|
||||
row `34 - 1 = 33` is the first row where every column is finite. Earlier
|
||||
rows are entirely `NaN`; you should not slice a partial row out and use,
|
||||
say, the `signal` column independently of the `macd` column.
|
||||
2. Columns are positional — `out[:, 0]` is MACD, `out[:, 1]` is signal,
|
||||
`out[:, 2]` is histogram. The streaming form returns the same triple as a
|
||||
plain Python tuple: `(macd, signal, histogram)`.
|
||||
|
||||
To filter a warmup-aware mask cleanly:
|
||||
|
||||
```python
|
||||
ready = ~np.isnan(out[:, 0])
|
||||
clean_rows = out[ready]
|
||||
```
|
||||
|
||||
`ready` is a single boolean column you can apply to every column at once
|
||||
because the warmup pattern is identical across all of them.
|
||||
|
||||
## A deeper example
|
||||
|
||||
`examples/python/backtest.py` in the repo runs a full panel of indicators
|
||||
(RSI, EMA, Bollinger, MACD, ATR, ADX, OBV) over an OHLCV CSV and prints a
|
||||
summary. It's a good template for "I have historical data on disk, give me a
|
||||
table of indicator values" workflows; for live workflows, see
|
||||
`examples/python/live_trading.py`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Rust](Quickstart-Rust.md) — same API surface in Rust.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — why the streaming path is
|
||||
the primary one, not a convenience.
|
||||
- [Warmup Periods](Warmup-Periods.md) — the full table of warmup counts.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,142 +0,0 @@
|
||||
# Quickstart: Rust
|
||||
|
||||
A five-minute tour of the Wickra Rust crate. By the end you will have run a
|
||||
batch SMA, fed an RSI tick by tick, and composed two indicators with `Chain`.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
cargo add wickra
|
||||
```
|
||||
|
||||
The default features pull in `parallel` (rayon-based `batch_parallel`); turn
|
||||
them off with `cargo add wickra --no-default-features` if you want a leaner
|
||||
build. The `wickra` crate is a thin façade that re-exports everything from
|
||||
`wickra-core`; you can also depend on `wickra-core` directly if you want to
|
||||
skip the façade.
|
||||
|
||||
The published crate is at version `0.2.1` on
|
||||
[crates.io](https://crates.io/crates/wickra).
|
||||
|
||||
## The `Indicator` trait in 30 seconds
|
||||
|
||||
Every indicator implements the same trait:
|
||||
|
||||
```rust
|
||||
pub trait Indicator {
|
||||
type Input;
|
||||
type Output;
|
||||
fn update(&mut self, input: Self::Input) -> Option<Self::Output>;
|
||||
fn reset(&mut self);
|
||||
fn warmup_period(&self) -> usize;
|
||||
fn is_ready(&self) -> bool;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
```
|
||||
|
||||
`update` is O(1) in the input length. The companion trait `BatchExt` is a
|
||||
blanket extension that adds a `batch(&[Self::Input])` method to every
|
||||
indicator — its default implementation is literally a loop over `update`, so
|
||||
batch and streaming results are bit-for-bit identical.
|
||||
|
||||
## Batch and streaming side by side
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Rsi, Sma};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 1. Batch: SMA(3) over five prices.
|
||||
let mut sma = Sma::new(3)?;
|
||||
let out: Vec<Option<f64>> = sma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
println!("{:?}", out);
|
||||
// -> [None, None, Some(2.0), Some(3.0), Some(4.0)]
|
||||
|
||||
// 2. Streaming: feed Wilder's textbook example into RSI(14).
|
||||
let mut rsi = Rsi::new(14)?;
|
||||
let prices = [
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42, 45.84, 46.08,
|
||||
45.89, 46.03, 45.61, 46.28, 46.28, 46.00, 46.03, 46.41,
|
||||
];
|
||||
for (tick, price) in prices.iter().enumerate() {
|
||||
if let Some(v) = rsi.update(*price) {
|
||||
println!("tick {:2} close={:.2} rsi={:.4}", tick + 1, price, v);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
The streaming loop prints:
|
||||
|
||||
```
|
||||
tick 15 close=46.28 rsi=70.4641
|
||||
tick 16 close=46.00 rsi=66.2496
|
||||
tick 17 close=46.03 rsi=66.4809
|
||||
tick 18 close=46.41 rsi=69.3469
|
||||
```
|
||||
|
||||
The first value lands on tick 15 because `Rsi::new(14)?.warmup_period() == 15`
|
||||
(14 diffs to seed Wilder's smoothing, so the 15th input emits the first RSI).
|
||||
The `70.4641` value matches the textbook value pinned by the unit test
|
||||
`classic_wilder_textbook_values` in `crates/wickra-core/src/indicators/rsi.rs`.
|
||||
|
||||
## Composing indicators with `Chain`
|
||||
|
||||
`Chain<A, B>` wires the output of `A` straight into the input of `B`, provided
|
||||
both stages agree on `f64` as the bridging type. The chain itself is an
|
||||
`Indicator`, so you can stack three stages with `.then(c)`, or four with
|
||||
`.then(c).then(d)`.
|
||||
|
||||
```rust
|
||||
use wickra::{Chain, Ema, Indicator, Rsi};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// RSI(7) computed on the output of EMA(14).
|
||||
let mut chain = Chain::new(Ema::new(14)?, Rsi::new(7)?);
|
||||
for i in 1..=22 {
|
||||
if let Some(v) = chain.update(f64::from(i)) {
|
||||
println!("chain emitted at input #{i}: {v}");
|
||||
}
|
||||
}
|
||||
println!("chain.warmup_period() = {}", chain.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
chain emitted at input #21: 100
|
||||
chain emitted at input #22: 100
|
||||
chain.warmup_period() = 22
|
||||
```
|
||||
|
||||
`Ema::new(14)` needs 14 inputs to seed and `Rsi::new(7)` needs 8 more once
|
||||
the EMA starts flowing, so the chain emits its first value at input 21. The
|
||||
`warmup_period()` reported by `Chain` is a conservative `first + second` sum
|
||||
(here `14 + 8 = 22`); see [Indicator Chaining](Indicator-Chaining.md) for the
|
||||
exact contract.
|
||||
|
||||
## A deeper example
|
||||
|
||||
`examples/rust/src/bin/backtest.rs` (in the `wickra-examples` workspace
|
||||
crate) computes a panel of indicators (RSI, EMA, Bollinger, MACD, ATR, ADX,
|
||||
OBV) over an OHLCV CSV by way of `wickra-data`:
|
||||
|
||||
```bash
|
||||
cargo run --release -p wickra-examples --bin backtest -- path/to/ohlcv.csv
|
||||
```
|
||||
|
||||
For live-data work, `wickra-data` ships a streaming CSV reader, a
|
||||
tick-to-candle aggregator, a candle resampler, and a Binance kline WebSocket
|
||||
adapter under the `live-binance` feature. `examples/rust/src/bin/live_binance.rs`
|
||||
is the canonical example for the latter.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Python](Quickstart-Python.md) — same engine, NumPy-flavoured.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — the `batch == repeated update`
|
||||
contract and the benchmark numbers it buys you.
|
||||
- [Indicator Chaining](Indicator-Chaining.md) — three-stage chains and the
|
||||
stacked-warmup rule.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,154 +0,0 @@
|
||||
# Quickstart: WebAssembly
|
||||
|
||||
A five-minute tour of the Wickra WebAssembly binding. The same Rust core that
|
||||
powers the Python, Node, and Rust APIs is compiled to WebAssembly with
|
||||
[wasm-bindgen](https://rustwasm.github.io/wasm-bindgen/), so indicators run
|
||||
entirely client-side — in a browser tab, a bundler build, or Node — with no
|
||||
server round-trips.
|
||||
|
||||
## Install
|
||||
|
||||
The published package is `wickra-wasm` on npm:
|
||||
|
||||
```bash
|
||||
npm install wickra-wasm
|
||||
```
|
||||
|
||||
The npm package is built for the bundler target, so it works directly with
|
||||
Webpack, Vite, Rollup, and similar toolchains.
|
||||
|
||||
## Build from source
|
||||
|
||||
To build the binding yourself you need [`wasm-pack`](https://rustwasm.github.io/wasm-pack/)
|
||||
and the `wasm32-unknown-unknown` target:
|
||||
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install wasm-pack
|
||||
|
||||
# Browser ES-module build (import directly from a <script type="module">):
|
||||
wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
|
||||
# Bundler build (Webpack / Vite / Rollup):
|
||||
wasm-pack build bindings/wasm --target bundler --release --features panic-hook
|
||||
```
|
||||
|
||||
`wasm-pack` writes the generated module, the `.wasm` binary, and TypeScript
|
||||
definitions into `bindings/wasm/pkg/`. The `panic-hook` feature routes Rust
|
||||
panics to `console.error` for readable stack traces during development.
|
||||
|
||||
## A first run (browser)
|
||||
|
||||
With a `--target web` build, import the generated module directly:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import init, { version, SMA } from "./pkg/wickra_wasm.js";
|
||||
|
||||
await init(); // load and instantiate the .wasm binary
|
||||
console.log("wickra-wasm", version());
|
||||
|
||||
const sma = new SMA(3);
|
||||
console.log(sma.batch([2, 4, 6, 8, 10]));
|
||||
// -> Float64Array [ NaN, NaN, 4, 6, 8 ]
|
||||
</script>
|
||||
```
|
||||
|
||||
`init()` must be awaited once before any indicator is constructed — it fetches
|
||||
and instantiates the WebAssembly binary. After that the API mirrors the other
|
||||
bindings.
|
||||
|
||||
## Streaming
|
||||
|
||||
```javascript
|
||||
import init, { RSI } from "wickra-wasm";
|
||||
|
||||
await init();
|
||||
|
||||
const rsi = new RSI(14);
|
||||
for (const price of liveFeed) {
|
||||
const value = rsi.update(price); // number, or null/undefined during warmup
|
||||
if (value != null && value > 70) {
|
||||
console.log("overbought");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Every indicator is an O(1)-per-update state machine: `update` advances the
|
||||
indicator by exactly one input, so a browser charting app pays no cost for
|
||||
recomputing history on each tick.
|
||||
|
||||
## Multi-output indicators
|
||||
|
||||
Several indicators return a structured object from `update` (or `null` during
|
||||
warmup). The full list and their field names:
|
||||
|
||||
| Indicator | `update` return shape |
|
||||
|---------------------|--------------------------------------------------|
|
||||
| `MACD` | `{ macd, signal, histogram }` |
|
||||
| `BollingerBands` | `{ upper, middle, lower, stddev }` |
|
||||
| `Stochastic` | `{ k, d }` |
|
||||
| `ADX` | `{ plusDi, minusDi, adx }` |
|
||||
| `Keltner` | `{ upper, middle, lower }` |
|
||||
| `Donchian` | `{ upper, middle, lower }` |
|
||||
| `Aroon` | `{ up, down }` |
|
||||
| `SuperTrend` | `{ value, direction }` |
|
||||
|
||||
```javascript
|
||||
import init, { MACD } from "wickra-wasm";
|
||||
|
||||
await init();
|
||||
|
||||
const macd = new MACD(12, 26, 9);
|
||||
let last = null;
|
||||
for (let i = 0; i < 40; i++) {
|
||||
last = macd.update(100 + i * 0.5); // null during warmup, else { macd, signal, histogram }
|
||||
}
|
||||
console.log(last);
|
||||
```
|
||||
|
||||
`batch` returns a flat `Float64Array`; multi-output indicators interleave
|
||||
their fields per row (e.g. MACD: `[macd0, signal0, hist0, macd1, ...]`). The
|
||||
exact layout is documented in the generated `pkg/wickra_wasm.d.ts`.
|
||||
|
||||
> Since `wickra-wasm@0.2.1`, every candle-input indicator (ATR, ADX,
|
||||
> WilliamsR, CCI, MFI, PSAR, Keltner, Donchian, VWAP, RollingVWAP,
|
||||
> AwesomeOscillator, Aroon, Stochastic, OBV, and the rest of the
|
||||
> volume / volatility / trailing-stop / price-statistics families) exposes
|
||||
> the same streaming API as `MACD` here — `update`, `batch`, `reset`,
|
||||
> `isReady` and `warmupPeriod`. Earlier releases only shipped `batch` for
|
||||
> twelve of these classes; browser code no longer needs to replay `batch`
|
||||
> on every tick.
|
||||
|
||||
## Errors
|
||||
|
||||
Unlike the Node binding (whose constructors clamp pathological values), the
|
||||
WASM binding's constructors throw a JavaScript error for invalid parameters:
|
||||
|
||||
```javascript
|
||||
try {
|
||||
new MACD(0, 0, 0);
|
||||
} catch (e) {
|
||||
console.error("invalid MACD parameters:", e);
|
||||
}
|
||||
```
|
||||
|
||||
## A complete example
|
||||
|
||||
`examples/wasm/index.html` is a self-contained browser demo: it streams a
|
||||
synthetic price series through six indicators and draws a live chart on a
|
||||
`<canvas>`. Open it after a `--target web` build:
|
||||
|
||||
```bash
|
||||
wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
# then serve the repository root and open examples/wasm/index.html
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Node](Quickstart-Node.md) — the native (non-WASM) Node binding.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — why `update` is the primary
|
||||
entry point.
|
||||
- [Indicators Overview](Indicators-Overview.md) — every indicator and its
|
||||
parameters.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,160 +0,0 @@
|
||||
# Streaming vs Batch
|
||||
|
||||
Wickra has one engine, not two. Every indicator is a state machine driven by
|
||||
a single method, `Indicator::update`, and the batch API is a thin loop over
|
||||
that method. This page is the concept doc for why that matters, and what
|
||||
contracts you can rely on when you mix the two in real code.
|
||||
|
||||
## The `update` contract
|
||||
|
||||
`Indicator::update` is the only state transition. From `crates/wickra-core/src/traits.rs`:
|
||||
|
||||
```rust
|
||||
pub trait Indicator {
|
||||
type Input;
|
||||
type Output;
|
||||
|
||||
/// Feed one new data point into the indicator and return the freshly computed
|
||||
/// output, or `None` if the indicator is still warming up.
|
||||
fn update(&mut self, input: Self::Input) -> Option<Self::Output>;
|
||||
|
||||
fn reset(&mut self);
|
||||
fn warmup_period(&self) -> usize;
|
||||
fn is_ready(&self) -> bool;
|
||||
fn name(&self) -> &'static str;
|
||||
}
|
||||
```
|
||||
|
||||
Three properties hold by contract:
|
||||
|
||||
1. **O(1) in the input length.** `update` may touch some pre-existing
|
||||
buffered state, but it must never recompute over the entire history. The
|
||||
`wickra-core` crate is `#![forbid(unsafe_code)]`, and the standard
|
||||
indicator implementations all carry rolling sums, single recursive
|
||||
accumulators, or fixed-size `VecDeque` windows.
|
||||
2. **`None` during warmup, `Some` thereafter.** An indicator returns `None`
|
||||
while it doesn't yet have enough data to produce a defined value. After
|
||||
the first `Some`, it never goes back to `None` (short of a `reset()`).
|
||||
3. **`reset()` restores construction-time state.** The state-machine is
|
||||
fully encapsulated, so resetting and replaying produces bit-identical
|
||||
results to a fresh instance.
|
||||
|
||||
## The `BatchExt` blanket implementation
|
||||
|
||||
The batch API is a blanket extension on top of every `Indicator`. The whole
|
||||
implementation is six lines:
|
||||
|
||||
```rust
|
||||
pub trait BatchExt: Indicator {
|
||||
fn batch(&mut self, inputs: &[Self::Input]) -> Vec<Option<Self::Output>>
|
||||
where Self::Input: Clone,
|
||||
{
|
||||
let mut out = Vec::with_capacity(inputs.len());
|
||||
for x in inputs {
|
||||
out.push(self.update(x.clone()));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Indicator> BatchExt for T {}
|
||||
```
|
||||
|
||||
Two consequences:
|
||||
|
||||
- **`batch == repeated update`, exactly.** There is no separate "vectorised"
|
||||
code path that might disagree numerically with the streaming one. A unit
|
||||
test pinning this invariant — `batch_equals_streaming` — lives in nearly
|
||||
every `crates/wickra-core/src/indicators/<name>.rs` file. You can rely on
|
||||
the batch results in your backtest matching the streaming results that
|
||||
your live bot will see.
|
||||
- **Implementing one trait is enough.** Adding a new indicator means
|
||||
implementing `Indicator` in Rust; every binding plus every batch helper
|
||||
comes along for free.
|
||||
|
||||
You can verify the equivalence yourself in Python:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
np.random.seed(0)
|
||||
prices = np.cumsum(np.random.randn(100)) + 100.0
|
||||
|
||||
# Batch path.
|
||||
batch_out = ta.RSI(14).batch(prices)
|
||||
|
||||
# Streaming path: same inputs, fresh indicator, fed one at a time.
|
||||
rsi = ta.RSI(14)
|
||||
stream_out = np.array(
|
||||
[np.nan if (v := rsi.update(p)) is None else v for p in prices]
|
||||
)
|
||||
|
||||
b_nan = np.isnan(batch_out)
|
||||
s_nan = np.isnan(stream_out)
|
||||
assert np.array_equal(b_nan, s_nan)
|
||||
assert np.array_equal(batch_out[~b_nan], stream_out[~s_nan])
|
||||
```
|
||||
|
||||
This passes; the last three values of both arrays are
|
||||
`[69.64533252, 70.00767057, 71.18111330]`.
|
||||
|
||||
## Why batch-only libraries fall behind live
|
||||
|
||||
Suppose a strategy looks at RSI(14) on each new minute-bar of a market. A
|
||||
classical batch-only library (TA-Lib, pandas-ta, finta, ...) gives you a
|
||||
single function `rsi(prices)` that recomputes the indicator over the entire
|
||||
input array. To use it inside a streaming loop, you concatenate each new
|
||||
tick onto your history and call `rsi(history)` again. That's
|
||||
`O(n)` work for every new bar, and the gap widens linearly as `n` grows.
|
||||
|
||||
Wickra's `update` is the opposite: each new bar is O(1) because the
|
||||
recursive smoothing state is already inside the indicator. You never carry
|
||||
history just to recompute it.
|
||||
|
||||
The numbers below are reproduced from the project README, where
|
||||
`python -m benchmarks.compare_libraries` is the source script.
|
||||
|
||||
### Batch — single full pass over a 20 000-bar series
|
||||
|
||||
| Indicator | Wickra | finta | talipp |
|
||||
|---------------------|---------------------|-----------------------------|-------------------------------|
|
||||
| SMA(20) | **95.6 µs** | 343.5 µs (3.6× slower) | 7 640.6 µs (79.9× slower) |
|
||||
| EMA(20) | **64.6 µs** | 223.1 µs (3.5× slower) | 12 160.9 µs (188.2× slower) |
|
||||
| RSI(14) | **126.2 µs** | 1 107.1 µs (8.8× slower) | 15 792.2 µs (125.1× slower) |
|
||||
| MACD(12, 26, 9) | **119.0 µs** | 531.8 µs (4.5× slower) | 49 788.1 µs (418.2× slower) |
|
||||
| Bollinger(20, 2.0) | **105.3 µs** | 812.0 µs (7.7× slower) | 130 938.3 µs (1 243.7× slower)|
|
||||
| ATR(14) | **123.5 µs** | 5 144.8 µs (41.7× slower) | 28 816.0 µs (233.4× slower) |
|
||||
|
||||
### Streaming — per-tick latency after seeding with 5 000 historical bars
|
||||
|
||||
| Indicator | Wickra (per tick) | talipp (per tick) |
|
||||
|-----------|---------------------|---------------------------|
|
||||
| RSI(14) | **0.119 µs** | 1.644 µs (13.8× slower) |
|
||||
|
||||
The streaming gap widens linearly with how much history a batch-only library
|
||||
has to recompute on every new tick; the table above is the gap at a 5 000-bar
|
||||
seed followed by 15 000 live updates.
|
||||
|
||||
## Practical consequences
|
||||
|
||||
- **Mix freely.** A common pattern is "warm up the indicator on historical
|
||||
bars in one `batch` call, then drive it tick-by-tick with `update` for
|
||||
live data". This is correct because the two paths share state.
|
||||
- **`is_ready()` is the safe gate.** Don't use a `len(prices) > warmup_period`
|
||||
check; trust the indicator's `is_ready()` method, which is `true` exactly
|
||||
when at least one `Some` value has been emitted.
|
||||
- **Multi-output indicators NaN/None together.** Every column of a MACD or
|
||||
Bollinger batch transitions from `NaN` to a real value on the same row.
|
||||
Use `~np.isnan(out[:, 0])` (Python) or `Number.isFinite(row[0])`
|
||||
(Node) as a single mask across all columns.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: Python](Quickstart-Python.md) — concrete Python usage of both
|
||||
paths.
|
||||
- [Quickstart: Rust](Quickstart-Rust.md) — the `BatchExt` trait and `?`
|
||||
error handling.
|
||||
- [Warmup Periods](Warmup-Periods.md) — the exact `warmup_period()` for
|
||||
every indicator.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,93 +0,0 @@
|
||||
# Migrating from TA-Lib
|
||||
|
||||
A quick lookup table for users porting code from TA-Lib (the C library, or
|
||||
its Python binding `talib`) to Wickra. Replace `talib.X(...)` with the
|
||||
matching Wickra expression and the rest of your code keeps working.
|
||||
|
||||
## Argument-order conventions
|
||||
|
||||
The two libraries take the same numeric arguments but differ in shape:
|
||||
|
||||
- **TA-Lib (Python)** is functional and pass-by-array. `talib.RSI(close, n)`
|
||||
is a *recompute-everything* call: it walks the entire `close` series each
|
||||
time, even when you only want the latest value.
|
||||
- **Wickra** is a state machine. `wickra.RSI(n)` returns an *instance*; you
|
||||
call `.batch(close)` for the full series or `.update(price)` one price at
|
||||
a time. The same instance, fed one price per minute, drives a live
|
||||
trading bot — see [Streaming vs Batch](Streaming-vs-Batch.md).
|
||||
|
||||
Multi-output indicators (MACD, Bollinger Bands, Stochastic, ADX, Aroon,
|
||||
Keltner, Donchian, SuperTrend, …) return a tuple from `update` and a 2-D
|
||||
NumPy array (one column per output) from `batch`.
|
||||
|
||||
## Mapping table
|
||||
|
||||
| TA-Lib | Wickra (Python) |
|
||||
|-----------------------------------------------------|--------------------------------------------------------------------------------------------------|
|
||||
| `talib.SMA(close, n)` | `wickra.SMA(n).batch(close)` |
|
||||
| `talib.EMA(close, n)` | `wickra.EMA(n).batch(close)` |
|
||||
| `talib.WMA(close, n)` | `wickra.WMA(n).batch(close)` |
|
||||
| `talib.DEMA(close, n)` | `wickra.DEMA(n).batch(close)` |
|
||||
| `talib.TEMA(close, n)` | `wickra.TEMA(n).batch(close)` |
|
||||
| `talib.KAMA(close, n)` | `wickra.KAMA(n).batch(close)` |
|
||||
| `talib.T3(close, n, vfactor)` | `wickra.T3(n, vfactor).batch(close)` |
|
||||
| `talib.RSI(close, n)` | `wickra.RSI(n).batch(close)` |
|
||||
| `talib.STOCH(high, low, close, k, smooth, d)` | `wickra.Stochastic(k_period, d_period).batch(high, low, close)` → shape `(n, 2)` |
|
||||
| `talib.STOCHRSI(close, n, k, d)` | `wickra.StochRSI(rsi_period, stoch_period).batch(close)` |
|
||||
| `talib.CCI(high, low, close, n)` | `wickra.CCI(n).batch(high, low, close)` |
|
||||
| `talib.WILLR(high, low, close, n)` | `wickra.WilliamsR(n).batch(high, low, close)` |
|
||||
| `talib.MFI(high, low, close, volume, n)` | `wickra.MFI(n).batch(high, low, close, volume)` |
|
||||
| `talib.ROC(close, n)` | `wickra.ROC(n).batch(close)` |
|
||||
| `talib.MOM(close, n)` | `wickra.MOM(n).batch(close)` |
|
||||
| `talib.CMO(close, n)` | `wickra.CMO(n).batch(close)` |
|
||||
| `talib.MACD(close, fast, slow, signal)` | `wickra.MACD(fast, slow, signal).batch(close)` → shape `(n, 3)` |
|
||||
| `talib.PPO(close, fast, slow)` | `wickra.PPO(fast, slow).batch(close)` |
|
||||
| `talib.APO(close, fast, slow)` | `wickra.PPO(fast, slow).batch(close)` *(PPO is APO scaled to percent)* |
|
||||
| `talib.TRIX(close, n)` | `wickra.TRIX(n).batch(close)` |
|
||||
| `talib.ADX(high, low, close, n)` | `wickra.ADX(n).batch(high, low, close)` → shape `(n, 3)` (`+DI`, `−DI`, `ADX`) |
|
||||
| `talib.AROON(high, low, n)` | `wickra.Aroon(n).batch(high, low, close)` → shape `(n, 2)` |
|
||||
| `talib.AROONOSC(high, low, n)` | `wickra.AroonOscillator(n).batch(high, low, close)` |
|
||||
| `talib.BBANDS(close, n, dev_up, dev_dn)` | `wickra.BollingerBands(n, multiplier).batch(close)` → shape `(n, 4)` (`upper`, `middle`, `lower`, `stddev`) |
|
||||
| `talib.ATR(high, low, close, n)` | `wickra.ATR(n).batch(high, low, close)` |
|
||||
| `talib.NATR(high, low, close, n)` | `wickra.NATR(n).batch(high, low, close)` |
|
||||
| `talib.STDDEV(close, n)` | `wickra.StdDev(n).batch(close)` |
|
||||
| `talib.TRANGE(high, low, close)` | `wickra.TrueRange().batch(high, low, close)` |
|
||||
| `talib.OBV(close, volume)` | `wickra.OBV().batch(close, volume)` |
|
||||
| `talib.AD(high, low, close, volume)` | `wickra.ADL().batch(high, low, close, volume)` |
|
||||
| `talib.ADOSC(high, low, close, volume, fast, slow)` | `wickra.ChaikinOscillator(fast, slow).batch(high, low, close, volume)` |
|
||||
| `talib.SAR(high, low, accel, max)` | `wickra.PSAR(accel_start, accel_step, accel_max).batch(high, low, close)` |
|
||||
| `talib.LINEARREG(close, n)` | `wickra.LinearRegression(n).batch(close)` |
|
||||
| `talib.LINEARREG_SLOPE(close, n)` | `wickra.LinRegSlope(n).batch(close)` |
|
||||
| `talib.LINEARREG_ANGLE(close, n)` | `wickra.LinRegAngle(n).batch(close)` |
|
||||
| `talib.TYPPRICE(high, low, close)` | `wickra.TypicalPrice().batch(high, low, close)` |
|
||||
| `talib.MEDPRICE(high, low)` | `wickra.MedianPrice().batch(high, low, close)` |
|
||||
| `talib.WCLPRICE(high, low, close)` | `wickra.WeightedClose().batch(high, low, close)` |
|
||||
| `talib.ULTOSC(high, low, close, p1, p2, p3)` | `wickra.UltimateOscillator(p1, p2, p3).batch(high, low, close)` |
|
||||
|
||||
## What Wickra has that TA-Lib does not
|
||||
|
||||
- **Trailing stops** — `SuperTrend`, `ChandelierExit`, `ChandeKrollStop`,
|
||||
`AtrTrailingStop` (TA-Lib only has `SAR`).
|
||||
- **Volume oscillators** — `ChaikinMoneyFlow`, `ForceIndex`,
|
||||
`EaseOfMovement`, `VolumePriceTrend`, plus the windowed `RollingVwap`.
|
||||
- **Other modern indicators** — `Choppiness Index`, `Vertical Horizontal
|
||||
Filter`, `Coppock`, `PMO`, `Z-Score`, `Mass Index`, `Vortex`, `TSI`,
|
||||
`Smma`, `Trima`, `Zlema`, `Vwma`, `BollingerBandwidth`, `%B`.
|
||||
|
||||
## What TA-Lib has that Wickra does not (yet)
|
||||
|
||||
- Pattern recognition (`CDL*` candlestick patterns).
|
||||
- Hilbert-transform-based indicators (`HT_DCPERIOD`, `HT_TRENDLINE`, …).
|
||||
- A few trivial transforms (`AVGPRICE`, `MIDPOINT`, `MIDPRICE`).
|
||||
|
||||
If you need one of these,
|
||||
[open an issue](https://github.com/kingchenc/wickra/issues) — most are
|
||||
short additions on top of the existing engine.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicators Overview](Indicators-Overview.md) — every Wickra indicator,
|
||||
organised by family.
|
||||
- [Quickstart: Python](Quickstart-Python.md) — concrete Python usage.
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — why Wickra is fast at
|
||||
per-tick updates while TA-Lib re-computes the whole series.
|
||||
@@ -1,174 +0,0 @@
|
||||
# Warmup Periods
|
||||
|
||||
Every Wickra indicator returns `None` (Rust), `None` (Python), or `null`
|
||||
(Node) for its first few inputs while it gathers enough data to produce a
|
||||
defined value. The number of inputs an indicator needs before it emits its
|
||||
first non-empty value is its **warmup period**, surfaced everywhere as
|
||||
`warmup_period()` / `warmupPeriod()`.
|
||||
|
||||
After the first emission, the indicator never goes back to a "no value yet"
|
||||
state — it has rolled its state forward and will produce a steady value on
|
||||
every subsequent `update()`. Calling `reset()` returns to the warming-up
|
||||
state, equivalent to a freshly constructed instance.
|
||||
|
||||
## How to read the formula column
|
||||
|
||||
The formulas below are taken verbatim from the `warmup_period()` methods in
|
||||
`crates/wickra-core/src/indicators/<name>.rs`. The "Inputs at first
|
||||
emission" column says, in 1-indexed terms, which `update()` call returns the
|
||||
first `Some`/non-`NaN` value. They are the same number; "first emission
|
||||
index" in 0-indexed terms is `warmup_period − 1`.
|
||||
|
||||
## Single-output indicators
|
||||
|
||||
> The rows are keyed by **constructor**, not by indicator name. `Vwap`
|
||||
> appears twice — once for the cumulative `Vwap::new()` and once for the
|
||||
> finite-window `RollingVwap::new(period)` — because the two share the
|
||||
> indicator name `Vwap` (see [Indicators-Overview.md](Indicators-Overview.md))
|
||||
> but have different warmup periods. That is the only such pair; every other
|
||||
> row is one canonical indicator.
|
||||
|
||||
| Indicator | Constructor | Formula | `warmup_period()` for shown args | Inputs at first emission |
|
||||
|-----------------|----------------------------------------------|----------------------------------|----------------------------------|--------------------------|
|
||||
| `Sma` | `Sma::new(14)` | `period` | 14 | 14th |
|
||||
| `Ema` | `Ema::new(14)` | `period` | 14 | 14th |
|
||||
| `Wma` | `Wma::new(14)` | `period` | 14 | 14th |
|
||||
| `Dema` | `Dema::new(14)` | `2 * period - 1` | 27 | 27th |
|
||||
| `Tema` | `Tema::new(14)` | `3 * period - 2` | 40 | 40th |
|
||||
| `Hma` | `Hma::new(14)` | `period + round(sqrt(period)).max(1) - 1` | 17 | 17th |
|
||||
| `Kama` | `Kama::new(10, 2, 30)` | `er_period + 1` | 11 | 11th |
|
||||
| `Rsi` | `Rsi::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `Cci` | `Cci::new(20)` | `period` | 20 | 20th |
|
||||
| `Roc` | `Roc::new(12)` | `period + 1` | 13 | 13th |
|
||||
| `WilliamsR` | `WilliamsR::new(14)` | `period` | 14 | 14th |
|
||||
| `Mfi` | `Mfi::new(14)` | `period` | 14 | 14th |
|
||||
| `Trix` | `Trix::new(15)` | `3 * period - 1` | 44 | 44th |
|
||||
| `AwesomeOscillator` | `AwesomeOscillator::new(5, 34)` | `slow_period` | 34 | 34th |
|
||||
| `Atr` | `Atr::new(14)` | `period` | 14 | 14th |
|
||||
| `Psar` | `Psar::new(0.02, 0.02, 0.20)` | constant `2` | 2 | 2nd |
|
||||
| `Obv` | `Obv::new()` | constant `1` | 1 | 1st |
|
||||
| `Vwap` | `Vwap::new()` | constant `1` | 1 | 1st |
|
||||
| `RollingVwap` | `RollingVwap::new(20)` | `period` | 20 | 20th |
|
||||
| `Smma` | `Smma::new(14)` | `period` | 14 | 14th |
|
||||
| `Trima` | `Trima::new(20)` | `period` | 20 | 20th |
|
||||
| `Zlema` | `Zlema::new(14)` | `lag + period` (`lag = (period − 1) / 2`) | 20 | 20th |
|
||||
| `T3` | `T3::new(5, 0.7)` | `6 * period - 5` | 25 | 25th |
|
||||
| `Vwma` | `Vwma::new(20)` | `period` | 20 | 20th |
|
||||
| `Mom` | `Mom::new(10)` | `period + 1` | 11 | 11th |
|
||||
| `Cmo` | `Cmo::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `Tsi` | `Tsi::new(25, 13)` | `long + short` | 38 | 38th |
|
||||
| `Pmo` | `Pmo::new(35, 20)` | constant `2` | 2 | 2nd |
|
||||
| `StochRsi` | `StochRsi::new(14, 14)` | `rsi_period + stoch_period` | 28 | 28th |
|
||||
| `UltimateOscillator` | `UltimateOscillator::new(7, 14, 28)` | `max(short, mid, long) + 1` | 29 | 29th |
|
||||
| `Ppo` | `Ppo::new(12, 26)` | `slow` | 26 | 26th |
|
||||
| `Dpo` | `Dpo::new(20)` | `max(period, period / 2 + 2)` | 20 | 20th |
|
||||
| `Coppock` | `Coppock::new(14, 11, 10)` | `max(roc_long, roc_short) + wma_period` | 24 | 24th |
|
||||
| `AroonOscillator` | `AroonOscillator::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `MassIndex` | `MassIndex::new(9, 25)` | `2 * ema_period + sum_period - 2` | 41 | 41st |
|
||||
| `Natr` | `Natr::new(14)` | `period` | 14 | 14th |
|
||||
| `StdDev` | `StdDev::new(20)` | `period` | 20 | 20th |
|
||||
| `UlcerIndex` | `UlcerIndex::new(14)` | `2 * period - 1` | 27 | 27th |
|
||||
| `HistoricalVolatility` | `HistoricalVolatility::new(20, 252)` | `period + 1` | 21 | 21st |
|
||||
| `BollingerBandwidth` | `BollingerBandwidth::new(20, 2.0)` | `period` | 20 | 20th |
|
||||
| `PercentB` | `PercentB::new(20, 2.0)` | `period` | 20 | 20th |
|
||||
| `AtrTrailingStop` | `AtrTrailingStop::new(14, 3.0)` | `atr_period` | 14 | 14th |
|
||||
| `Adl` | `Adl::new()` | constant `1` | 1 | 1st |
|
||||
| `VolumePriceTrend` | `VolumePriceTrend::new()` | constant `1` | 1 | 1st |
|
||||
| `ChaikinMoneyFlow` | `ChaikinMoneyFlow::new(20)` | `period` | 20 | 20th |
|
||||
| `ChaikinOscillator` | `ChaikinOscillator::new(3, 10)` | `slow` | 10 | 10th |
|
||||
| `ForceIndex` | `ForceIndex::new(13)` | `period + 1` | 14 | 14th |
|
||||
| `EaseOfMovement` | `EaseOfMovement::new(14)` | `period + 1` | 15 | 15th |
|
||||
| `TypicalPrice` | `TypicalPrice::new()` | constant `1` | 1 | 1st |
|
||||
| `MedianPrice` | `MedianPrice::new()` | constant `1` | 1 | 1st |
|
||||
| `WeightedClose` | `WeightedClose::new()` | constant `1` | 1 | 1st |
|
||||
| `LinearRegression` | `LinearRegression::new(14)` | `period` | 14 | 14th |
|
||||
| `LinRegSlope` | `LinRegSlope::new(14)` | `period` | 14 | 14th |
|
||||
| `AcceleratorOscillator` | `AcceleratorOscillator::classic()` | `ao_slow + signal_period - 1` | 38 | 38th |
|
||||
| `BalanceOfPower` | `BalanceOfPower::new()` | constant `1` | 1 | 1st |
|
||||
| `ChoppinessIndex` | `ChoppinessIndex::new(14)` | `period` | 14 | 14th |
|
||||
| `VerticalHorizontalFilter` | `VerticalHorizontalFilter::new(28)` | `period + 1` | 29 | 29th |
|
||||
| `TrueRange` | `TrueRange::new()` | constant `1` | 1 | 1st |
|
||||
| `ChaikinVolatility` | `ChaikinVolatility::new(10, 10)` | `ema_period + roc_period` | 20 | 20th |
|
||||
| `ZScore` | `ZScore::new(20)` | `period` | 20 | 20th |
|
||||
| `LinRegAngle` | `LinRegAngle::new(14)` | `period` | 14 | 14th |
|
||||
|
||||
## Multi-output indicators
|
||||
|
||||
These indicators emit several values at once (a struct in Rust, a tuple in
|
||||
Python, an object in Node) and every column / field transitions from "not
|
||||
ready" to "ready" together — there are no rows that have a `signal` but no
|
||||
`macd`, for example.
|
||||
|
||||
| Indicator | Constructor | Formula | `warmup_period()` for shown args | Inputs at first emission | Outputs |
|
||||
|-------------------|--------------------------------------|------------------------------------------|----------------------------------|--------------------------|--------------------------------------------------------|
|
||||
| `MacdIndicator` | `MacdIndicator::new(12, 26, 9)` | `slow + signal - 1` | 34 | 34th | `macd`, `signal`, `histogram` |
|
||||
| `BollingerBands` | `BollingerBands::new(20, 2.0)` | `period` | 20 | 20th | `upper`, `middle`, `lower`, `stddev` |
|
||||
| `Stochastic` | `Stochastic::new(14, 3)` | `k_period + d_period - 1` | 16 | 16th | `k`, `d` |
|
||||
| `Adx` | `Adx::new(14)` | `2 * period` | 28 | 28th | `plus_di`, `minus_di`, `adx` |
|
||||
| `Aroon` | `Aroon::new(14)` | `period + 1` | 15 | 15th | `up`, `down` |
|
||||
| `Keltner` | `Keltner::new(20, 10, 2.0)` | `ema_period.max(atr_period)` | 20 | 20th | `upper`, `middle`, `lower` |
|
||||
| `Donchian` | `Donchian::new(20)` | `period` | 20 | 20th | `upper`, `middle`, `lower` |
|
||||
| `Vortex` | `Vortex::new(14)` | `period + 1` | 15 | 15th | `plus`, `minus` |
|
||||
| `SuperTrend` | `SuperTrend::new(10, 3.0)` | `atr_period` | 10 | 10th | `value`, `direction` |
|
||||
| `ChandelierExit` | `ChandelierExit::new(22, 3.0)` | `period` | 22 | 22nd | `long_stop`, `short_stop` |
|
||||
| `ChandeKrollStop` | `ChandeKrollStop::new(10, 1.0, 9)` | `atr_period + stop_period - 1` | 18 | 18th | `stop_long`, `stop_short` |
|
||||
|
||||
## "Off-by-one" cases worth memorising
|
||||
|
||||
A few indicators look like they should warm up at `period` but in fact need
|
||||
`period + 1` inputs. The reason is always the same — they consume *diffs*
|
||||
or *previous-close* differences, not the prices themselves, and the very
|
||||
first input has nothing to diff against.
|
||||
|
||||
- **`Rsi::new(period)` warmup is `period + 1`.** RSI is based on Wilder's
|
||||
smoothing over per-tick gains and losses. With 14 prices you only have 13
|
||||
diffs; you need 15 prices to compute 14 diffs and seed `avg_gain` /
|
||||
`avg_loss`. The Rust unit test that pins this is
|
||||
`warmup_period_is_period_plus_one`:
|
||||
```rust
|
||||
let rsi = Rsi::new(14).unwrap();
|
||||
assert_eq!(rsi.warmup_period(), 15);
|
||||
```
|
||||
- **`Roc::new(period)` warmup is `period + 1`.** ROC compares the current
|
||||
price to the price `period` bars ago; that comparison only makes sense
|
||||
starting at input `period + 1`.
|
||||
- **`Aroon::new(period)` warmup is `period + 1`.** Aroon scans a `period + 1`-bar
|
||||
window to find the bars-since-high and bars-since-low.
|
||||
- **`Kama::new(er_period, ...)` warmup is `er_period + 1`.** Kaufman's
|
||||
efficiency ratio needs `er_period` differences, which costs one extra
|
||||
bar.
|
||||
|
||||
## Cross-checking from your own code
|
||||
|
||||
The cleanest way to verify any of these from your application code is the
|
||||
indicator's own `warmup_period()`:
|
||||
|
||||
```rust
|
||||
use wickra::{Indicator, MacdIndicator};
|
||||
let macd = MacdIndicator::classic(); // (12, 26, 9)
|
||||
assert_eq!(macd.warmup_period(), 34);
|
||||
```
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
assert ta.MACD(12, 26, 9).warmup_period() == 34
|
||||
```
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
const sma = new wickra.SMA(20);
|
||||
console.log(sma.warmupPeriod()); // -> 20
|
||||
```
|
||||
|
||||
(Since `wickra@0.2.1`, `warmupPeriod()` is exposed on every Node and
|
||||
WASM class — single- and multi-output — alongside `update()`, `reset()`
|
||||
and `isReady()`. Consult `bindings/node/index.d.ts` for the
|
||||
authoritative TypeScript surface.)
|
||||
|
||||
## See also
|
||||
|
||||
- [Streaming vs Batch](Streaming-vs-Batch.md) — the `is_ready()` gate, and
|
||||
why a `len(prices) > warmup_period` check is the wrong abstraction.
|
||||
- [Indicator Chaining](Indicator-Chaining.md) — how warmups stack inside a
|
||||
`Chain`.
|
||||
- Source: <https://github.com/kingchenc/wickra>
|
||||
@@ -1,195 +0,0 @@
|
||||
# AwesomeOscillator
|
||||
|
||||
> Bill Williams' Awesome Oscillator — the difference of two simple moving
|
||||
> averages computed on the bar's median price `(high + low) / 2`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (centred on 0; in price-difference units) |
|
||||
| Default parameters | `fast = 5`, `slow = 34` (`AwesomeOscillator::classic()`, Python default) |
|
||||
| Warmup period | `slow_period` (34 for the classic configuration) |
|
||||
| Interpretation | zero-line cross; "saucer" and "twin-peaks" Bill Williams patterns |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new candle, compute the median price:
|
||||
|
||||
```
|
||||
median_t = (high_t + low_t) / 2
|
||||
```
|
||||
|
||||
Then AO is the difference of two SMAs of that series:
|
||||
|
||||
```
|
||||
AO_t = SMA_fast(median)_t − SMA_slow(median)_t
|
||||
```
|
||||
|
||||
There is no smoothing on top — the output is in the same units as the
|
||||
input prices (a number, not a percent).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `fast` | `usize` | `5` | `>= 1` and `< slow` | Fast SMA period over median price. |
|
||||
| `slow` | `usize` | `34` | `>= 1` and `> fast` | Slow SMA period over median price. |
|
||||
|
||||
`AwesomeOscillator::new` returns `Error::PeriodZero` if either period is
|
||||
zero and `Error::InvalidPeriod` if `fast >= slow`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for AwesomeOscillator`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
```
|
||||
|
||||
The `close` and `volume` fields on the input candle are ignored — only
|
||||
`high` and `low` matter, via `Candle::median_price()`.
|
||||
|
||||
Python's `AwesomeOscillator.batch(high, low)` returns a 1-D `float64`
|
||||
`np.ndarray`. Node's `AwesomeOscillator.batch(high, low)` returns a
|
||||
flat `number[]`. Both produce `NaN` during warmup; only Python exposes
|
||||
a streaming `update(candle)` method.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `slow_period`. The slow SMA is the slower of
|
||||
the two SMAs, and because both consume the same median-price stream the
|
||||
first time both have valid output is exactly the `slow_period`-th input.
|
||||
For the classic `(5, 34)` configuration this is `34` — verified above.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant input.** Both SMAs converge to the constant median price,
|
||||
so `AO == 0` (test `constant_series_yields_zero`).
|
||||
- **Reset.** `reset()` resets both SMAs; the next `slow_period` updates
|
||||
return `None`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{AwesomeOscillator, BatchExt, Candle, Indicator};
|
||||
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let m = 100.0 + i as f64;
|
||||
Candle::new(m, m + 1.0, m - 1.0, m, 1.0, 0).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut ao = AwesomeOscillator::classic();
|
||||
let out = ao.batch(&candles);
|
||||
println!("row 33 = {}", out[33].unwrap());
|
||||
println!("row 39 = {}", out[39].unwrap());
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 33 = 14.5
|
||||
row 39 = 14.5
|
||||
```
|
||||
|
||||
(`SMA(5) − SMA(34)` on a unit-slope ramp converges to a constant offset
|
||||
that depends only on the difference between the two windows' centres,
|
||||
which is why both rows print the same number.)
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
n = 40
|
||||
i = np.arange(n, dtype=float)
|
||||
m = 100.0 + i
|
||||
high = m + 1.0
|
||||
low = m - 1.0
|
||||
ao = ta.AwesomeOscillator(5, 34)
|
||||
out = ao.batch(high, low)
|
||||
print('warmup:', ao.warmup_period())
|
||||
print('row 33:', out[33])
|
||||
print('row 39:', out[39])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 34
|
||||
row 33: 14.5
|
||||
row 39: 14.5
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const n = 40;
|
||||
const high = [], low = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const m = 100 + i;
|
||||
high.push(m + 1);
|
||||
low.push(m - 1);
|
||||
}
|
||||
const ao = new wickra.AwesomeOscillator(5, 34);
|
||||
const out = ao.batch(high, low);
|
||||
console.log('row 33:', out[33]);
|
||||
console.log('row 39:', out[39]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 33: 14.5
|
||||
row 39: 14.5
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Zero-line cross.** AO crossing zero from below is a bullish
|
||||
momentum signal — the fast SMA of median price has overtaken the
|
||||
slow SMA. The mirror cross is bearish.
|
||||
- **Saucer.** A short sequence of bars where AO turns from negative to
|
||||
positive momentum without crossing zero (two declining-magnitude
|
||||
bars on the same side of zero followed by a turn) is Bill Williams'
|
||||
"saucer" pattern.
|
||||
- **Twin peaks.** Two AO peaks on the same side of the zero line, with
|
||||
the second peak lower (or shallower) than the first while price
|
||||
pushes further, is Williams' divergence-style "twin peaks" pattern.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Median-price input, not close.** AO ignores `close` entirely. If
|
||||
your data source reports an "average" price or only closes, you must
|
||||
reconstruct `high` and `low` or pick a different oscillator (e.g.
|
||||
MACD on closes).
|
||||
- **Output magnitude depends on the asset.** Because AO is in raw
|
||||
price units, an AO of `14.5` on a price ramp through `100..140`
|
||||
means something completely different than `14.5` on a price stream
|
||||
near `0.00012`. Always interpret AO relative to a per-asset baseline
|
||||
or normalise by ATR.
|
||||
|
||||
## References
|
||||
|
||||
- Bill Williams, *Trading Chaos: Applying Expert Techniques to
|
||||
Maximize Your Profits*, Wiley, 1995 — introduces the Awesome
|
||||
Oscillator alongside the rest of the Profitunity tool set.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — sister
|
||||
oscillator on closes (with an extra signal line on top).
|
||||
- [Indicator: Trix](../trend-directional/Indicator-Trix.md) — momentum oscillator on a
|
||||
triple-smoothed series.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — bare `slow_period`.
|
||||
@@ -1,198 +0,0 @@
|
||||
# CCI
|
||||
|
||||
> Commodity Channel Index — measures how far the current typical price
|
||||
> deviates from its rolling mean, in units of mean absolute deviation
|
||||
> scaled by Lambert's constant.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (typically `[−200, +200]` thanks to the 0.015 factor) |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `period` (20 for `period = 20`) |
|
||||
| Interpretation | `> +100` overbought, `< −100` oversold (Lambert) |
|
||||
|
||||
## Formula
|
||||
|
||||
For each candle, compute the typical price `TP = (high + low + close) / 3`,
|
||||
then over the rolling `period`-bar window:
|
||||
|
||||
```
|
||||
SMA_TP_t = (TP_{t-period+1} + … + TP_t) / period
|
||||
MAD_t = (1 / period) · Σ |TP_i − SMA_TP_t| for i = t-period+1 … t
|
||||
|
||||
CCI_t = (TP_t − SMA_TP_t) / (factor · MAD_t)
|
||||
```
|
||||
|
||||
The default `factor` is Lambert's `0.015`, chosen empirically so that
|
||||
roughly 70–80 % of values fall inside `[−100, +100]`. The implementation
|
||||
exposes the factor through `Cci::with_factor(period, factor)` if you want
|
||||
to retune it for an asset with very different volatility characteristics.
|
||||
|
||||
When `MAD == 0` (a perfectly flat window), the implementation returns `0`
|
||||
rather than dividing by zero.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` | `>= 1` | Rolling window length for both the SMA of typical price and the MAD. |
|
||||
| `factor` | `f64` | `0.015` (`Cci::new`) | `> 0`, finite | Lambert's scaling constant; configurable via `Cci::with_factor`. |
|
||||
|
||||
`Cci::new(0)` returns `Error::PeriodZero`. `Cci::with_factor(_, factor)`
|
||||
returns `Error::NonPositiveMultiplier` when `factor <= 0` or non-finite.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Cci`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
```
|
||||
|
||||
Python's `CCI.batch(high, low, close)` returns a 1-D `float64` `np.ndarray`
|
||||
with `NaN` during warmup. Node's `CCI.batch(high, low, close)` returns a
|
||||
flat `number[]` (also `NaN` during warmup); the Node binding does not
|
||||
expose a streaming `update()` (`bindings/node/index.d.ts` lists only
|
||||
`constructor` and `batch`).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns exactly `period`. CCI does not consume diffs —
|
||||
it only needs `period` typical-price samples to populate its rolling
|
||||
window before it can compute an SMA and MAD. In streaming terms, calls
|
||||
`1..period` return `None`; the `period`-th call returns the first value.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat input.** Every `TP` is the SMA, so `MAD == 0` and the
|
||||
implementation returns `0.0` (test `flat_candles_yield_zero`). This
|
||||
avoids the divide-by-zero that would otherwise produce `NaN` /
|
||||
`±∞`.
|
||||
- **Custom factor.** `Cci::with_factor(period, factor)` lets you replace
|
||||
Lambert's `0.015`. Picking a smaller factor widens the typical range
|
||||
of CCI values; picking a larger one compresses them.
|
||||
- **Reset.** `reset()` clears the rolling window and the running sum,
|
||||
returning the indicator to the freshly-constructed state.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Cci, Indicator};
|
||||
|
||||
let candles: Vec<Candle> = (0..25)
|
||||
.map(|i| {
|
||||
let m = 50.0 + i as f64;
|
||||
Candle::new(m, m + 1.0, m - 1.0, m, 1.0, 0).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut cci = Cci::new(20)?;
|
||||
let out = cci.batch(&candles);
|
||||
println!("row 19 = {}", out[19].unwrap());
|
||||
println!("row 24 = {}", out[24].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 19 = 126.66666666666667
|
||||
row 24 = 126.66666666666667
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
i = np.arange(25, dtype=float)
|
||||
m = 50.0 + i
|
||||
high = m + 1.0
|
||||
low = m - 1.0
|
||||
close = m
|
||||
cci = ta.CCI(20)
|
||||
out = cci.batch(high, low, close)
|
||||
print('row 19:', out[19])
|
||||
print('row 24:', out[24])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 19: 126.66666666666667
|
||||
row 24: 126.66666666666667
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const n = 25;
|
||||
const high = [], low = [], close = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const m = 50 + i;
|
||||
high.push(m + 1);
|
||||
low.push(m - 1);
|
||||
close.push(m);
|
||||
}
|
||||
const cci = new wickra.CCI(20);
|
||||
const out = cci.batch(high, low, close);
|
||||
console.log('row 19:', out[19]);
|
||||
console.log('row 24:', out[24]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 19: 126.66666666666667
|
||||
row 24: 126.66666666666667
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **±100 threshold.** Lambert's published convention is to treat values
|
||||
above `+100` as overbought and below `−100` as oversold. The choice
|
||||
of `0.015` for the divisor is what makes the threshold meaningful;
|
||||
changing the factor changes the threshold.
|
||||
- **Zero-line cross.** `CCI` crossing zero says the typical price has
|
||||
moved through its `period`-bar mean — sometimes used as a
|
||||
trend-direction filter.
|
||||
- **Divergence.** As with RSI/Stochastic, a price making a new high
|
||||
while CCI makes a lower high is a classic bearish divergence.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **CCI is unbounded.** Unlike RSI or Stochastic, CCI can spike well
|
||||
outside `±100` in volatile markets. Threshold-based rules should be
|
||||
paired with a maximum-absolute-value guard, or you will mis-classify
|
||||
legitimate breakouts as "extreme overbought".
|
||||
- **The 0.015 factor is empirical, not derived.** It was chosen by
|
||||
Lambert in 1980 for commodity futures markets. Modern equities and
|
||||
crypto have wider distributions; if your `|CCI|` distribution sits
|
||||
almost entirely outside `±100`, retune via `Cci::with_factor` rather
|
||||
than rewriting downstream thresholds.
|
||||
|
||||
## References
|
||||
|
||||
- Donald Lambert, "Commodity Channel Index: Tools for Trading Cyclical
|
||||
Trends", *Commodities Magazine*, October 1980 — the original
|
||||
publication, including the empirical choice of `0.015`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — bounded sibling for comparison.
|
||||
- [Indicator: WilliamsR](../momentum-oscillators/Indicator-WilliamsR.md) — another candle-input
|
||||
oscillator, range-based rather than deviation-based.
|
||||
- [Indicator: Mfi](../momentum-oscillators/Indicator-Mfi.md) — volume-weighted RSI; useful as a
|
||||
confirmation alongside CCI.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — `period` (no off-by-one).
|
||||
@@ -1,155 +0,0 @@
|
||||
# CMO
|
||||
|
||||
> Chande Momentum Oscillator — a bounded `[−100, 100]` momentum gauge from
|
||||
> the unsmoothed sum of gains versus losses.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−100, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | `+100` pure gains, `−100` pure losses, `0` balanced. |
|
||||
|
||||
## Formula
|
||||
|
||||
Over the last `period` price *changes*, sum the gains and the losses
|
||||
separately:
|
||||
|
||||
```
|
||||
gain_t = max(price_t − price_{t−1}, 0)
|
||||
loss_t = max(price_{t−1} − price_t, 0)
|
||||
CMO = 100 · (Σ gain − Σ loss) / (Σ gain + Σ loss)
|
||||
```
|
||||
|
||||
Unlike RSI — which Wilder-smooths the gain/loss averages — CMO sums them
|
||||
raw, with equal weight on every change in the window. That makes it
|
||||
faster and wider-swinging than RSI at the same period.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` (Python) | `>= 1` | Number of price changes summed. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `14` via `#[pyo3(signature = (period=14))]`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/cmo.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Cmo {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Cmo::new(period).warmup_period() == period + 1`. The first price change
|
||||
needs two inputs, and the gain/loss window must hold `period` changes, so
|
||||
the first non-`None` output lands on input `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure trend.** A window of only gains returns `+100`; only losses,
|
||||
`−100` (`pure_uptrend_saturates_at_plus_100` /
|
||||
`pure_downtrend_saturates_at_minus_100` pin this).
|
||||
- **Constant series.** A flat series has no gains and no losses; the
|
||||
`0 / 0` is guarded and the output is `0.0`
|
||||
(`constant_series_yields_zero` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; state
|
||||
is left untouched.
|
||||
- **Reset.** `cmo.reset()` clears the previous price, the gain/loss window
|
||||
and both running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Cmo};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmo = Cmo::new(3)?;
|
||||
let out: Vec<Option<f64>> = cmo.batch(&[10.0, 11.0, 10.0, 12.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, Some(50.0)]
|
||||
```
|
||||
|
||||
The three changes are `+1, −1, +2`: `Σ gain = 3`, `Σ loss = 1`, so
|
||||
`CMO = 100·(3 − 1)/(3 + 1) = 50`. This matches the `reference_value` test
|
||||
in `crates/wickra-core/src/indicators/cmo.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
cmo = ta.CMO(3)
|
||||
print(cmo.batch(np.array([10.0, 11.0, 10.0, 12.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan nan 50.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const cmo = new ta.CMO(3);
|
||||
console.log(cmo.batch([10, 11, 10, 12]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, 50 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Cmo` is read like other bounded oscillators: readings near `+50` and
|
||||
above flag overbought conditions, near `−50` and below oversold, and the
|
||||
zero line marks the gain/loss balance point. Because it is unsmoothed it
|
||||
reacts a bar or two sooner than RSI but is noisier — pair it with a slower
|
||||
filter, or use it for divergence rather than raw threshold triggers.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting the `[0, 100]` RSI scale.** `Cmo` is centred on zero and
|
||||
spans `[−100, 100]`; an RSI of `30` corresponds to a `Cmo` near `−40`.
|
||||
- **Treating it as a smoothed average.** `Cmo` sums raw changes — it is
|
||||
deliberately not Wilder-smoothed.
|
||||
|
||||
## References
|
||||
|
||||
Tushar Chande, *The New Technical Trader* (1994). The unsmoothed
|
||||
gain/loss sum here matches the original definition and TA-Lib's `CMO`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Rsi.md](../momentum-oscillators/Indicator-Rsi.md) — the Wilder-smoothed relative.
|
||||
- [Indicator-Mom.md](../momentum-oscillators/Indicator-Mom.md) — raw price-difference momentum.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,203 +0,0 @@
|
||||
# MFI
|
||||
|
||||
> Money Flow Index — a volume-weighted RSI built on typical price times
|
||||
> volume.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` (volume needed) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` (14 for `period = 14`) |
|
||||
| Interpretation | overbought above 80, oversold below 20 |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new candle:
|
||||
|
||||
```
|
||||
TP_t = (high_t + low_t + close_t) / 3 (typical price)
|
||||
MF_t = TP_t · volume_t (money flow)
|
||||
|
||||
positive MF = MF_t if TP_t > TP_{t-1}, else 0
|
||||
negative MF = MF_t if TP_t < TP_{t-1}, else 0
|
||||
(both zero when TP_t == TP_{t-1})
|
||||
```
|
||||
|
||||
Maintain rolling sums of positive and negative money flow over the last
|
||||
`period` bars. Then:
|
||||
|
||||
```
|
||||
MR_t = positive_sum / negative_sum
|
||||
MFI_t = 100 − 100 / (1 + MR_t)
|
||||
```
|
||||
|
||||
The implementation guards both special cases: when both rolling sums are
|
||||
zero, MFI returns `50` (neutral); when only `negative_sum == 0`, MFI
|
||||
returns `100`; otherwise the standard formula.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` | `>= 1` | Rolling window length for the positive/negative money-flow sums. |
|
||||
|
||||
`Mfi::new(0)` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Mfi`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
```
|
||||
|
||||
Volume is consumed via `candle.volume` — it is not optional. Calling
|
||||
the indicator with a zero-volume candle is legal (every money flow on
|
||||
that bar is zero), but mass zero-volume bars will dilute the sums.
|
||||
|
||||
Python's `MFI.batch(high, low, close, volume)` returns a 1-D `float64`
|
||||
`np.ndarray` (warmup → `NaN`). Node's `MFI.batch(high, low, close,
|
||||
volume)` returns a flat `number[]` (warmup → `NaN`); only `batch` is
|
||||
exposed on the Node binding.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `period`. The first candle has no previous
|
||||
`TP` to compare against, so its money flow is classified as neither
|
||||
positive nor negative — it sits in the window as a `0 / 0` slot but
|
||||
still counts toward filling the window. The first `Some` is therefore
|
||||
emitted at the `period`-th `update`, exactly when the rolling positive
|
||||
and negative sums first contain `period − 1` real comparisons.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure uptrend.** Every `TP_t > TP_{t-1}`, so `negative_sum == 0` and
|
||||
the implementation returns `100` directly (test
|
||||
`pure_uptrend_yields_high_mfi`). Pure downtrend mirrors at `0` (test
|
||||
`pure_downtrend_yields_low_mfi`).
|
||||
- **Flat input (all `TP` equal).** Both sums stay at zero; the
|
||||
implementation returns `50` (the same neutral convention as RSI on
|
||||
flat input).
|
||||
- **Zero-volume candle.** Money flow on that bar is zero. The window
|
||||
still advances; the indicator just gets one less data point of
|
||||
influence.
|
||||
- **Reset.** `reset()` clears `prev_tp`, both rolling windows, and both
|
||||
sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Mfi};
|
||||
|
||||
let candles: Vec<Candle> = (1..=20)
|
||||
.map(|i| Candle::new(i as f64, i as f64, i as f64, i as f64, 100.0, 0).unwrap())
|
||||
.collect();
|
||||
let mut mfi = Mfi::new(14)?;
|
||||
let out = mfi.batch(&candles);
|
||||
println!("row 13 = {}", out[13].unwrap());
|
||||
println!("row 19 = {}", out[19].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 13 = 100
|
||||
row 19 = 100
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
n = 20
|
||||
i = np.arange(1, n + 1, dtype=float)
|
||||
high = low = close = i
|
||||
volume = np.full(n, 100.0)
|
||||
mfi = ta.MFI(14)
|
||||
out = mfi.batch(high, low, close, volume)
|
||||
print('warmup:', mfi.warmup_period())
|
||||
print('row 13:', out[13])
|
||||
print('row 19:', out[19])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 14
|
||||
row 13: 100.0
|
||||
row 19: 100.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const n = 20;
|
||||
const high = [], low = [], close = [], vol = [];
|
||||
for (let i = 1; i <= n; i++) {
|
||||
high.push(i); low.push(i); close.push(i); vol.push(100);
|
||||
}
|
||||
const m = new wickra.MFI(14);
|
||||
const out = m.batch(high, low, close, vol);
|
||||
console.log('row 13:', out[13]);
|
||||
console.log('row 19:', out[19]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 13: 100
|
||||
row 19: 100
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Overbought / oversold.** The conventional MFI thresholds are
|
||||
`80 / 20` — tighter than RSI's `70 / 30` because the volume weighting
|
||||
amplifies sustained one-way moves.
|
||||
- **Divergence.** MFI divergences are read like RSI divergences: a new
|
||||
price high without a confirming MFI high is bearish, and vice versa.
|
||||
Because volume is in the mix, MFI divergences are often interpreted
|
||||
as "the move is happening on weak participation" — i.e. structurally
|
||||
more meaningful than a pure-price divergence.
|
||||
- **Compare with OBV.** OBV (the unsmoothed cumulative volume) tells
|
||||
you accumulated participation; MFI tells you participation pressure
|
||||
over a fixed horizon. The two often diverge interestingly near
|
||||
trend exhaustion.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **MFI requires volume.** Unlike RSI (close only) or Stochastic
|
||||
(high/low/close), MFI's per-bar money flow is `TP × volume`. Passing
|
||||
a candle stream with `volume == 0` throughout will collapse MFI to
|
||||
`50` regardless of price action. Validate your data source before
|
||||
reaching for MFI.
|
||||
- **Same flat-input convention as RSI.** A perfectly flat window yields
|
||||
`50` (not `NaN`, not "no value"). Treat the value as informational
|
||||
only until the underlying TP series starts moving.
|
||||
|
||||
## References
|
||||
|
||||
- Gene Quong and Avrum Soudack, "Volume-Weighted RSI: Money Flow",
|
||||
*Technical Analysis of Stocks & Commodities*, March 1989 — the
|
||||
original publication of the MFI as a volume-weighted RSI variant.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — the price-only ancestor.
|
||||
- [Indicator: Adx](../trend-directional/Indicator-Adx.md) — directional/trend strength to
|
||||
pair with MFI's overbought/oversold reading.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — bare `period` (no off-by-one).
|
||||
@@ -1,151 +0,0 @@
|
||||
# MOM
|
||||
|
||||
> Momentum — the raw price change over a fixed lookback,
|
||||
> `price_t − price_{t−period}`, in absolute price units.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (price-difference scale) |
|
||||
| Default parameters | `period = 10` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Sign and size of the move over the last `period` bars. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
MOM_t = price_t − price_{t−period}
|
||||
```
|
||||
|
||||
The simplest momentum primitive. Positive output means price is higher
|
||||
than it was `period` bars ago, negative means lower, and the magnitude is
|
||||
the change in raw price units. [`Roc`](../momentum-oscillators/Indicator-Roc.md) is the same idea
|
||||
expressed as a percentage of the old price.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|----------------|-------------|-------------|
|
||||
| `period` | `usize` | `10` (Python) | `>= 1` | Lookback distance in bars. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `10` via `#[pyo3(signature = (period=10))]`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/mom.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Mom {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Mom::new(period).warmup_period() == period + 1`. The output needs both
|
||||
the current price and the price `period` bars back, so the window must
|
||||
hold `period + 1` values — the first non-`None` output lands on input
|
||||
`period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** A flat series yields `0.0` from input `period + 1`
|
||||
onward (`constant_series_yields_zero` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped: the
|
||||
rolling window is not advanced and the previous value is returned. The
|
||||
next finite input still references the correct historical price.
|
||||
- **Reset.** `mom.reset()` clears the window and restarts the warmup.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Mom};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut mom = Mom::new(3)?;
|
||||
let out: Vec<Option<f64>> = mom.batch(&[1.0, 2.0, 3.0, 4.0, 7.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, Some(3.0), Some(5.0)]
|
||||
```
|
||||
|
||||
`MOM(3)` first emits on input 4: `4 − 1 = 3`. The fifth input gives
|
||||
`7 − 2 = 5`. This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/mom.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
mom = ta.MOM(3)
|
||||
print(mom.batch(np.array([1.0, 2.0, 3.0, 4.0, 7.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan nan 3. 5.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const mom = new ta.MOM(3);
|
||||
console.log(mom.batch([1, 2, 3, 4, 7]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, 3, 5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Mom` is a zero-centred oscillator. The textbook reads are the zero-line
|
||||
cross (momentum flipping sign) and divergence (price making a new high
|
||||
while `Mom` makes a lower high — a stalling trend). Because the output is
|
||||
in price units, `Mom` values are not comparable across instruments at
|
||||
different price levels; use [`Roc`](../momentum-oscillators/Indicator-Roc.md) when you need a
|
||||
scale-free percentage instead.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Comparing `Mom` across instruments.** A `Mom` of `5` means very
|
||||
different things on a $10 stock and a $5000 index. Normalise with `Roc`
|
||||
for cross-asset work.
|
||||
- **Forgetting the `+1` warmup.** `warmup_period()` is `period + 1`, not
|
||||
`period`.
|
||||
|
||||
## References
|
||||
|
||||
Momentum is one of the oldest technical studies; the implementation here
|
||||
is the standard `price − price[period]` difference, matching TA-Lib's
|
||||
`MOM`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Roc.md](../momentum-oscillators/Indicator-Roc.md) — the percentage-scaled counterpart.
|
||||
- [Indicator-Cmo.md](../momentum-oscillators/Indicator-Cmo.md) — bounded momentum from summed changes.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,169 +0,0 @@
|
||||
# PMO
|
||||
|
||||
> Price Momentum Oscillator — Carl Swenlin's DecisionPoint PMO line: a
|
||||
> doubly-smoothed rate of change.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `(smoothing1 = 35, smoothing2 = 20)` (Python) |
|
||||
| Warmup period | `2` |
|
||||
| Interpretation | Smoothed momentum; zero-line and signal-line crosses are the signals. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
roc_t = (price_t / price_{t−1} − 1) · 100
|
||||
smoothed_t = customEMA(roc, smoothing1)_t
|
||||
PMO_t = customEMA(10 · smoothed, smoothing2)_t
|
||||
```
|
||||
|
||||
`customEMA` is the DecisionPoint smoothing: an exponential average whose
|
||||
smoothing constant is `2 / period` (not the textbook `2 / (period + 1)`),
|
||||
seeded from its first input. The 1-bar percentage change is smoothed once,
|
||||
scaled by `10`, then smoothed again.
|
||||
|
||||
The classic PMO **signal line** is a 10-period EMA of this PMO line. It is
|
||||
deliberately not bundled in — compose it yourself with
|
||||
[`Chain`](../../Indicator-Chaining.md) and an `Ema(10)`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------------|---------|---------------|-------------|-------------|
|
||||
| `smoothing1` | `usize` | `35` (Python) | `>= 2` | First smoothing period (applied to ROC). `0` errors with `Error::PeriodZero`; `1` with `Error::InvalidPeriod`. |
|
||||
| `smoothing2` | `usize` | `20` (Python) | `>= 2` | Second smoothing period (applied to `10 · smoothed`). Same error rules. |
|
||||
|
||||
`smoothing = 1` is rejected because the smoothing constant `2 / 1 = 2`
|
||||
would exceed `1`. The Python binding defaults the pair to `(35, 20)` via
|
||||
`#[pyo3(signature = (smoothing1=35, smoothing2=20))]`. The `periods`
|
||||
property returns `(smoothing1, smoothing2)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/pmo.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Pmo {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Pmo::new(s1, s2).warmup_period() == 2`. The first ROC needs a previous
|
||||
price, and both `customEMA`s seed from their very first input, so the
|
||||
first non-`None` output lands on the **second** `update()`. Note this is
|
||||
the first *defined* value; the doubly-smoothed series only stabilises
|
||||
after many more bars, so treat early readings as unsettled.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** A flat series gives `roc = 0` on every bar, so both
|
||||
smoothings stay at `0` and PMO is `0.0`
|
||||
(`constant_series_yields_zero` pins this).
|
||||
- **Zero previous price.** A ratio against a `0.0` prior price is
|
||||
undefined; `roc` is treated as `0` for that bar.
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
smoothing chains are not advanced.
|
||||
- **Reset.** `pmo.reset()` clears the previous price and both EMAs.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Indicator, Pmo};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut pmo = Pmo::new(35, 20)?;
|
||||
println!("{:?}", pmo.update(100.0)); // no previous price yet
|
||||
println!("{:?}", pmo.update(101.0)); // first defined PMO
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
None
|
||||
Some(10.0)
|
||||
```
|
||||
|
||||
The first `update` only records the price. The second produces
|
||||
`roc = 1.0%`; each `customEMA` seeds from its first input, so the inner
|
||||
EMA emits `1.0`, the `×10` scaling gives `10.0`, and the outer EMA seeds
|
||||
at `10.0` — hence `PMO = 10.0` on the first defined bar. Early values are
|
||||
seed artefacts: the double smoothing only settles after many more bars.
|
||||
This matches the `first_emission_at_second_update` test in
|
||||
`crates/wickra-core/src/indicators/pmo.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
pmo = ta.PMO() # (smoothing1=35, smoothing2=20)
|
||||
prices = 100.0 * 1.01 ** np.arange(120) # steady uptrend
|
||||
out = pmo.batch(prices)
|
||||
print("last > 0:", out[-1] > 0)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
last > 0: True
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const pmo = new ta.PMO(35, 20);
|
||||
const prices = Array.from({ length: 120 }, (_, i) => 100 * 1.01 ** i);
|
||||
console.log('last:', pmo.batch(prices).at(-1));
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Pmo` is a smoothed momentum line. The DecisionPoint reads are: PMO
|
||||
crossing its zero line (momentum changing sign), PMO crossing its signal
|
||||
line (a 10-EMA of PMO — build it with `Chain`), and PMO turning up/down
|
||||
from an extreme. Because the rate of change is taken in percentage terms,
|
||||
PMO values *are* comparable across instruments — unlike raw
|
||||
[`Mom`](../momentum-oscillators/Indicator-Mom.md).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Trusting the first few values.** `warmup_period()` is `2`, but that is
|
||||
only the first *defined* output — the double smoothing needs many bars
|
||||
to settle. Discard the early ramp.
|
||||
- **Expecting a bundled signal line.** PMO here is the single PMO line;
|
||||
add `Ema(10)` via `Chain` for the signal.
|
||||
|
||||
## References
|
||||
|
||||
Carl Swenlin, DecisionPoint Price Momentum Oscillator. The
|
||||
`2 / period` "custom smoothing", the `×10` scaling and the conventional
|
||||
`(35, 20)` periods follow the published DecisionPoint definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Roc.md](../momentum-oscillators/Indicator-Roc.md) — the raw rate of change PMO smooths.
|
||||
- [Indicator-Tsi.md](../momentum-oscillators/Indicator-Tsi.md) — another double-smoothed momentum
|
||||
oscillator.
|
||||
- [Indicator-Chaining.md](../../Indicator-Chaining.md) — how to add the
|
||||
signal-line EMA.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,172 +0,0 @@
|
||||
# ROC
|
||||
|
||||
> Rate of Change — the percent change between the current close and the
|
||||
> close `period` bars ago.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (centred on 0; expressed as a percent) |
|
||||
| Default parameters | none — `period` is required in every binding |
|
||||
| Warmup period | `period + 1` (13 for `period = 12`) |
|
||||
| Interpretation | sign and magnitude of momentum; zero-line crossover for direction changes |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
ROC_t = (close_t − close_{t − period}) / close_{t − period} · 100
|
||||
```
|
||||
|
||||
When `close_{t − period}` is exactly zero, the implementation returns
|
||||
`0.0` rather than dividing by zero. The unit test `known_value` pins the
|
||||
basic case: with `period = 3`, inputs `[100, 105, 108, 110]` produce
|
||||
ROC `= 10` at index 3 (because `(110 − 100) / 100 · 100 = 10`).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|------|------|---------|-------------|-------------|
|
||||
| `period` | `usize` | required | `>= 1` | Lookback distance for the comparison close. |
|
||||
|
||||
`Roc::new(0)` returns `Error::PeriodZero`. The Python and Node bindings
|
||||
do **not** assign a default for `period`; you must pass it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Roc`:
|
||||
|
||||
```rust
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
fn update(&mut self, input: f64) -> Option<f64>;
|
||||
```
|
||||
|
||||
Python's `ROC.batch(prices)` returns a 1-D `float64` `np.ndarray`. Node's
|
||||
`ROC.batch(prices)` returns a flat `number[]`. Streaming `update(price)`
|
||||
returns a scalar (`float` / `number`) or `None` / `null` during warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `period + 1`. The reason is the same off-by-one
|
||||
as RSI: ROC compares against the close `period` bars ago, so at the
|
||||
`period`-th input we still have nothing to look back at — the `(period +
|
||||
1)`-th input is the first one for which `close_{t − period}` exists.
|
||||
Internally the rolling buffer is sized `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant input.** Every diff is zero, so `ROC == 0` for every emitted
|
||||
value (test `constant_series_yields_zero`).
|
||||
- **Reference close of zero.** Treated as `0.0` rather than producing
|
||||
`NaN`/`±∞` — see the `prev == 0.0` early return in `update`. This
|
||||
matters for assets quoted with zero as a legitimate value (rare for
|
||||
prices, but possible for, e.g., yield spreads).
|
||||
- **Non-finite input.** `update(NaN)` or `update(±∞)` returns `None`
|
||||
without advancing the rolling buffer.
|
||||
- **Reset.** `reset()` clears the rolling buffer; the next `period + 1`
|
||||
updates return `None`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Roc};
|
||||
|
||||
let mut roc = Roc::new(3)?;
|
||||
let out = roc.batch(&[100.0, 105.0, 108.0, 110.0]);
|
||||
println!("ROC(3) at idx 3 = {}", out[3].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
ROC(3) at idx 3 = 10
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
roc = ta.ROC(3)
|
||||
print('warmup:', roc.warmup_period())
|
||||
for p in [100.0, 105.0, 108.0, 110.0]:
|
||||
print(p, '->', roc.update(p))
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 4
|
||||
100.0 -> None
|
||||
105.0 -> None
|
||||
108.0 -> None
|
||||
110.0 -> 10.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const roc = new wickra.ROC(3);
|
||||
console.log('warmup:', roc.warmupPeriod());
|
||||
for (const p of [100, 105, 108, 110]) {
|
||||
console.log(p, '->', roc.update(p));
|
||||
}
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 4
|
||||
100 -> null
|
||||
105 -> null
|
||||
108 -> null
|
||||
110 -> 10
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Sign.** Positive ROC means price is higher than `period` bars ago;
|
||||
negative means lower. The magnitude is the percent move.
|
||||
- **Zero-line crossover.** A move through zero signals a regime change
|
||||
in the `period`-bar horizon. Combined with a longer-period ROC, this
|
||||
gives you a poor-man's trend filter.
|
||||
- **Divergence.** A new price high paired with a lower ROC high is the
|
||||
same bearish-divergence pattern as RSI/Stochastic, with the
|
||||
unbounded-oscillator caveat that "lower high" is unambiguous (no
|
||||
saturation against a `100` ceiling).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **ROC is unbounded.** A 10× price spike over `period` bars produces
|
||||
`ROC = 900`. Don't pipe ROC directly into rule sets designed for
|
||||
bounded oscillators (RSI, %K, %R) without an explicit clamp or a
|
||||
log-return transformation upstream.
|
||||
- **Off-by-one on the warmup.** The first non-`None` value lands at the
|
||||
`(period + 1)`-th input, not the `period`-th. A common bug is sizing
|
||||
an output array as `len(prices) - period` and getting an off-by-one
|
||||
empty row at the end.
|
||||
|
||||
## References
|
||||
|
||||
- Robert Colby, *The Encyclopedia of Technical Market Indicators*,
|
||||
2nd ed., McGraw-Hill, 2002 — Chapter on Rate of Change / Momentum,
|
||||
covering the canonical percent and ratio formulations.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — same `period + 1` warmup, but
|
||||
bounded.
|
||||
- [Indicator: Trix](../trend-directional/Indicator-Trix.md) — also a rate of change, but on
|
||||
a triple-smoothed EMA.
|
||||
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — momentum
|
||||
cousin operating on EMA differences instead of raw close differences.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — the `period + 1` family.
|
||||
@@ -1,213 +0,0 @@
|
||||
# RSI
|
||||
|
||||
> Relative Strength Index — Wilder's bounded momentum oscillator that maps
|
||||
> the ratio of average gains to average losses onto the `[0, 100]` range.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period + 1` (15 for `period = 14`) |
|
||||
| Interpretation | overbought above 70, oversold below 30 (Wilder's thresholds) |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
diff_t = close_t − close_{t-1}
|
||||
gain_t = max(diff_t, 0)
|
||||
loss_t = max(−diff_t, 0)
|
||||
|
||||
Seed (Wilder, at t = period):
|
||||
avg_gain_p = (gain_1 + … + gain_p) / p
|
||||
avg_loss_p = (loss_1 + … + loss_p) / p
|
||||
|
||||
Recursive smoothing (t > period), with α = 1 / period:
|
||||
avg_gain_t = (avg_gain_{t-1} · (period − 1) + gain_t) / period
|
||||
avg_loss_t = (avg_loss_{t-1} · (period − 1) + loss_t) / period
|
||||
|
||||
RS_t = avg_gain_t / avg_loss_t
|
||||
RSI_t = 100 − 100 / (1 + RS_t)
|
||||
```
|
||||
|
||||
When `avg_loss_t == 0` and `avg_gain_t > 0`, RSI is `100` directly; when both
|
||||
are zero (a perfectly flat series) the implementation returns the standard
|
||||
`50` convention.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` | `>= 1` | Wilder smoothing length. `Rsi::new(0)` returns `Error::PeriodZero`. |
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Rsi` in `crates/wickra-core/src/indicators/rsi.rs`:
|
||||
|
||||
```rust
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
fn update(&mut self, input: f64) -> Option<f64>;
|
||||
```
|
||||
|
||||
The output is a scalar in `[0, 100]`. In Python `batch(prices)` returns a
|
||||
1-D `np.ndarray` of `float64`, with `NaN` in the warmup positions. In Node
|
||||
`batch(prices)` returns a flat `number[]`, also `NaN` during warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `period + 1`. The reason is that RSI consumes
|
||||
*diffs*, not prices: with `period` prices you only have `period − 1` diffs,
|
||||
so you need exactly one extra price before Wilder's seed average is well
|
||||
defined. The Rust test `warmup_period_is_period_plus_one` pins this:
|
||||
|
||||
```rust
|
||||
let rsi = Rsi::new(14).unwrap();
|
||||
assert_eq!(rsi.warmup_period(), 15);
|
||||
```
|
||||
|
||||
In streaming terms, the first `period` calls to `update()` return `None`;
|
||||
the `(period + 1)`-th call returns the first `Some(value)`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat input.** When every input price is identical, every `gain` and
|
||||
every `loss` is zero, so `avg_loss == avg_gain == 0`. The implementation
|
||||
returns `50.0` by convention (see `Rsi::rsi_from_avgs`). The unit test
|
||||
`flat_series_yields_rsi_50` pins this behaviour.
|
||||
- **Pure uptrend / pure downtrend.** `avg_loss == 0` with `avg_gain > 0`
|
||||
short-circuits to `100`; the mirror case returns `0`. Tests
|
||||
`pure_uptrend_yields_rsi_100` and `pure_downtrend_yields_rsi_0` cover
|
||||
this.
|
||||
- **Non-finite input.** `update()` returns the previously emitted value
|
||||
(or `None` if no value has been emitted yet) when the input is `NaN` or
|
||||
infinite — the internal state is *not* advanced.
|
||||
- **Reset.** `reset()` returns the indicator to the freshly-constructed
|
||||
state: `prev_close`, both seed buffers, both averages, and `last_value`
|
||||
are cleared.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Rsi};
|
||||
|
||||
let prices = [
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
||||
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
||||
46.03, 46.41, 46.22, 45.64,
|
||||
];
|
||||
let mut rsi = Rsi::new(14)?;
|
||||
let out = rsi.batch(&prices);
|
||||
println!("first = {}", out[14].unwrap());
|
||||
println!("last = {}", out[19].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
first = 70.46413502109705
|
||||
last = 57.91502067008556
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
prices = np.array([
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
||||
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
||||
46.03, 46.41, 46.22, 45.64,
|
||||
], dtype=float)
|
||||
rsi = ta.RSI(14)
|
||||
v = rsi.batch(prices)
|
||||
print("warmup:", rsi.warmup_period())
|
||||
print("first :", float(v[14]))
|
||||
print("last :", float(v[-1]))
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 15
|
||||
first : 70.46413502109705
|
||||
last : 57.91502067008556
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const rsi = new wickra.RSI(14);
|
||||
const prices = [
|
||||
44.34, 44.09, 44.15, 43.61, 44.33, 44.83, 45.10, 45.42,
|
||||
45.84, 46.08, 45.89, 46.03, 45.61, 46.28, 46.28, 46.00,
|
||||
46.03, 46.41, 46.22, 45.64,
|
||||
];
|
||||
const v = rsi.batch(prices);
|
||||
console.log('warmup:', rsi.warmupPeriod());
|
||||
console.log('first :', v[14]);
|
||||
console.log('last :', v[19]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 15
|
||||
first : 70.46413502109705
|
||||
last : 57.91502067008556
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Overbought / oversold zones.** Wilder's classic thresholds are `70`
|
||||
(overbought) and `30` (oversold). Many crypto and FX desks tighten them
|
||||
to `80 / 20` for trending markets and loosen to `60 / 40` for
|
||||
range-bound markets.
|
||||
- **Midline cross.** A move through `50` is sometimes used as a directional
|
||||
signal; above 50 means average gains exceed average losses over the
|
||||
smoothing window.
|
||||
- **Divergence.** A higher price high paired with a lower RSI high (bearish
|
||||
divergence) is a classic Wilder signal; the symmetric pattern at lows is
|
||||
bullish.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **RSI on flat input is `50`, not undefined.** The implementation returns
|
||||
`50.0` when both averages are zero. Do not interpret this as a neutral
|
||||
signal — it is a placeholder that means "the indicator has no opinion
|
||||
yet". Pair RSI with a volatility filter (e.g. ATR) if your strategy is
|
||||
sensitive to ranging markets.
|
||||
- **`period + 1` warmup, not `period`.** A common bug is sizing the result
|
||||
array against `period` and indexing into the warmup region. The first
|
||||
`Some` arrives at the *(period + 1)*-th `update`; in batch form, indices
|
||||
`0..period` are `None`/`NaN`. See [Warmup Periods](../../Warmup-Periods.md).
|
||||
- **Non-finite inputs are absorbed silently.** `update(f64::NAN)` does not
|
||||
advance the state and returns the previous value. If you depend on a 1:1
|
||||
input-to-output mapping, pre-validate your data before feeding it in.
|
||||
|
||||
## References
|
||||
|
||||
- J. Welles Wilder, *New Concepts in Technical Trading Systems*, Trend
|
||||
Research, 1978. The original publication that defines both RSI and the
|
||||
Wilder smoothing scheme used internally.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — also momentum,
|
||||
but trend-following and unbounded.
|
||||
- [Indicator: Stochastic](../momentum-oscillators/Indicator-Stochastic.md) — sibling bounded
|
||||
oscillator, faster and noisier than RSI.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — the canonical `period + 1`
|
||||
off-by-one explained.
|
||||
- [Quickstart: Python](../../Quickstart-Python.md) — full RSI batch / streaming
|
||||
walk-through.
|
||||
@@ -1,164 +0,0 @@
|
||||
# StochRSI
|
||||
|
||||
> Stochastic RSI — the Stochastic Oscillator formula applied to the RSI
|
||||
> series, sharpening RSI's overbought/oversold turns.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 100]` |
|
||||
| Default parameters | `(rsi_period = 14, stoch_period = 14)` (Python) |
|
||||
| Warmup period | `rsi_period + stoch_period` |
|
||||
| Interpretation | Where RSI sits in its own recent range; near `0`/`100` = extremes. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
RSI_t = Rsi(rsi_period) of price
|
||||
StochRSI = 100 · (RSI_t − min(RSI, stoch_period)) / (max(RSI, …) − min(RSI, …))
|
||||
```
|
||||
|
||||
RSI rarely visits its `0`/`100` extremes — it spends most of its life
|
||||
bunched around the middle. StochRSI re-normalises it: it asks where the
|
||||
*current* RSI sits within its own high/low range over the last
|
||||
`stoch_period` bars. The result swings the full `[0, 100]` width far more
|
||||
often than raw RSI, so reversals are easier to spot.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------------|---------|---------------|-------------|-------------|
|
||||
| `rsi_period` | `usize` | `14` (Python) | `>= 1` | Period of the underlying RSI. `0` errors with `Error::PeriodZero`. |
|
||||
| `stoch_period` | `usize` | `14` (Python) | `>= 1` | Lookback for the high/low range of RSI. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults the pair to `(14, 14)` via
|
||||
`#[pyo3(signature = (rsi_period=14, stoch_period=14))]`. Node and WASM
|
||||
take both explicitly. The `periods` property returns
|
||||
`(rsi_period, stoch_period)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/stoch_rsi.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for StochRsi {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`StochRsi::new(rsi_period, stoch_period).warmup_period()
|
||||
== rsi_period + stoch_period`. The inner RSI emits its first value on
|
||||
input `rsi_period + 1`; the stochastic window then needs `stoch_period`
|
||||
RSI values, so the first non-`None` output lands on input
|
||||
`rsi_period + stoch_period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat RSI window.** When every RSI value in the window is equal — for
|
||||
example a constant price (RSI pinned at `50`) or a pure trend (RSI
|
||||
pinned at `100`) — the range is zero and StochRSI reports the neutral
|
||||
`50.0` (`flat_rsi_window_yields_50` and `pure_uptrend_yields_50` pin
|
||||
this).
|
||||
- **Bounds.** The output is always within `[0, 100]`
|
||||
(`output_stays_within_0_100` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
RSI and the window are not advanced.
|
||||
- **Reset.** `stoch_rsi.reset()` clears the inner RSI and the window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, StochRsi};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sr = StochRsi::new(14, 14)?;
|
||||
let prices: Vec<f64> = (1..=60)
|
||||
.map(|i| 100.0 + (f64::from(i) * 0.3).sin() * 10.0)
|
||||
.collect();
|
||||
let out = sr.batch(&prices);
|
||||
println!("warmup_period = {}", sr.warmup_period());
|
||||
println!("ready values: {}", out.iter().flatten().count());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 28
|
||||
ready values: 33
|
||||
```
|
||||
|
||||
The first 27 inputs return `None`; from input 28 onward every output is a
|
||||
defined `[0, 100]` value.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
sr = ta.StochRSI() # (rsi_period=14, stoch_period=14)
|
||||
prices = np.full(40, 100.0) # constant series
|
||||
print(sr.batch(prices)[-1]) # flat RSI window -> neutral 50
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
50.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const sr = new ta.StochRSI(14, 14);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + Math.sin(i * 0.3) * 10);
|
||||
console.log('warmupPeriod:', sr.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`StochRsi` is read like any `[0, 100]` oscillator, but with tighter
|
||||
thresholds because it saturates so readily: above `80` is overbought,
|
||||
below `20` oversold, and the `50` line is the midpoint. Because it is two
|
||||
oscillators deep, it is *fast and noisy* — excellent for spotting
|
||||
short-term turns, poor as a standalone trend filter. Many traders smooth
|
||||
it further (an SMA of StochRSI) and trade the crossover.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Using it as a trend filter.** `StochRsi` whipsaws; confirm with a
|
||||
slower indicator before acting on a raw threshold cross.
|
||||
- **Forgetting the stacked warmup.** Warmup is `rsi_period + stoch_period`
|
||||
— for the default `(14, 14)` that is 28 bars.
|
||||
- **Expecting raw-RSI values.** `StochRsi` is a *position within range*,
|
||||
not RSI itself; the two are not interchangeable.
|
||||
|
||||
## References
|
||||
|
||||
Tushar Chande and Stanley Kroll, *The New Technical Trader* (1994). The
|
||||
implementation is the standard Stochastic-of-RSI; the flat-window
|
||||
convention (`50`) matches this library's [`Stochastic`](../momentum-oscillators/Indicator-Stochastic.md).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Rsi.md](../momentum-oscillators/Indicator-Rsi.md) — the underlying oscillator.
|
||||
- [Indicator-Stochastic.md](../momentum-oscillators/Indicator-Stochastic.md) — the same formula on
|
||||
price instead of RSI.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,219 +0,0 @@
|
||||
# Stochastic
|
||||
|
||||
> The fast Stochastic Oscillator — `%K` measures where the current close
|
||||
> sits inside the high/low range of the last `k_period` bars, and `%D` is
|
||||
> a short SMA on top of `%K`.
|
||||
|
||||
Wickra ships a single **fast** variant (`%K` is the raw oscillator value,
|
||||
`%D` is its SMA). The "slow stochastic" wraps an additional SMA on `%K`;
|
||||
that variant is not built in — if you need it, smooth `%K` yourself via
|
||||
a `Chain` with `Sma::new(slow_period)`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `StochasticOutput { k, d }` |
|
||||
| Output range | `k, d ∈ [0, 100]` |
|
||||
| Default parameters | `k_period = 14`, `d_period = 3` (`Stochastic::classic()`) |
|
||||
| Warmup period | `k_period + d_period − 1` (16 for the classic configuration) |
|
||||
| Interpretation | overbought above 80, oversold below 20; %K / %D crossovers |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new candle at time `t`, let `HH` and `LL` be the highest high
|
||||
and lowest low over the last `k_period` candles:
|
||||
|
||||
```
|
||||
HH_t = max(high_{t-k_period+1}, …, high_t)
|
||||
LL_t = min(low_{t-k_period+1}, …, low_t)
|
||||
|
||||
%K_t = 100 · (close_t − LL_t) / (HH_t − LL_t) when HH ≠ LL
|
||||
%K_t = 50 when HH == LL (flat range)
|
||||
|
||||
%D_t = SMA_{d_period}(%K)_t
|
||||
```
|
||||
|
||||
The implementation maintains `HH` and `LL` with two monotonic deques so
|
||||
each update is amortized O(1).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `k_period` | `usize` | `14` | `>= 1` | Lookback window for the `%K` extrema. |
|
||||
| `d_period` | `usize` | `3` | `>= 1` | SMA period for `%D` over the `%K` stream. |
|
||||
|
||||
Either period being zero returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Stochastic`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = StochasticOutput;
|
||||
fn update(&mut self, candle: Candle) -> Option<StochasticOutput>;
|
||||
```
|
||||
|
||||
`StochasticOutput`:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `k` | Raw `%K` (where `close` sits inside the window's H–L range). |
|
||||
| `d` | `SMA(d_period)` of the `%K` series — the slower "signal" line. |
|
||||
|
||||
Python's `Stochastic.batch(high, low, close)` returns a `(n, 2)` array
|
||||
with columns `[k, d]`; warmup rows are `[NaN, NaN]`.
|
||||
|
||||
Node's `Stochastic.batch(high, low, close)` returns a flat `number[]`
|
||||
of length `n * 2`, interleaved as `[k_0, d_0, k_1, d_1, …]`. There is
|
||||
no streaming `update()` on the Node binding — only `batch` is exposed.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `k_period + d_period − 1`. The `%K` series itself
|
||||
becomes available at input `k_period`; the `%D` SMA then needs `d_period`
|
||||
of those `%K` values to seed, producing its first output at input
|
||||
`k_period + d_period − 1`. For the classic `(14, 3)` configuration this is
|
||||
`16` — verified above.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat range (`HH == LL`).** The implementation returns `%K = 50` by
|
||||
convention (mirroring RSI's flat-input behaviour). The unit test
|
||||
`flat_range_yields_k_50` pins this; with a constant input both `%K` and
|
||||
`%D` collapse to `50`.
|
||||
- **Close at the window high.** `%K = 100` exactly; close at the window
|
||||
low gives `%K = 0` exactly (tests `close_at_high_yields_k_100` and
|
||||
`close_at_low_yields_k_0`).
|
||||
- **Reset.** `reset()` clears the candle buffer, both monotonic deques,
|
||||
the SMA, and `last_k` — the indicator returns to a freshly-constructed
|
||||
state.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Stochastic};
|
||||
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| {
|
||||
let m = 10.0 + (i as f64 * 0.5).sin() * 2.0;
|
||||
Candle::new(m, m + 1.0, m - 1.0, m, 1.0, 0).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut s = Stochastic::new(14, 3)?;
|
||||
let out = s.batch(&candles);
|
||||
let v = out[15].unwrap();
|
||||
println!("row 15 k={} d={}", v.k, v.d);
|
||||
let v = out[19].unwrap();
|
||||
println!("row 19 k={} d={}", v.k, v.d);
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 15 k=81.19360374383255 d=69.94559370965067
|
||||
row 19 k=47.26766986190959 d=62.55762656278284
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
n = 20
|
||||
i = np.arange(n, dtype=float)
|
||||
m = 10.0 + np.sin(i * 0.5) * 2.0
|
||||
high = m + 1.0
|
||||
low = m - 1.0
|
||||
close = m
|
||||
stoch = ta.Stochastic(14, 3)
|
||||
out = stoch.batch(high, low, close)
|
||||
print('shape :', out.shape)
|
||||
print('warmup:', stoch.warmup_period())
|
||||
print('row 15:', out[15])
|
||||
print('row 19:', out[19])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
shape : (20, 2)
|
||||
warmup: 16
|
||||
row 15: [81.19360374 69.94559371]
|
||||
row 19: [47.26766986 62.55762656]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const n = 20;
|
||||
const high = [], low = [], close = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const m = 10.0 + Math.sin(i * 0.5) * 2.0;
|
||||
high.push(m + 1.0);
|
||||
low.push(m - 1.0);
|
||||
close.push(m);
|
||||
}
|
||||
const s = new wickra.Stochastic(14, 3);
|
||||
const out = s.batch(high, low, close);
|
||||
console.log('len :', out.length);
|
||||
console.log('row 15 :', { k: out[15 * 2], d: out[15 * 2 + 1] });
|
||||
console.log('row 19 :', { k: out[19 * 2], d: out[19 * 2 + 1] });
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
len : 40
|
||||
row 15 : { k: 81.19360374383255, d: 69.94559370965067 }
|
||||
row 19 : { k: 47.26766986190959, d: 62.55762656278284 }
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Overbought / oversold zones.** The canonical Lane thresholds are
|
||||
`80` and `20`. Crossings back from outside these bands are typically
|
||||
used as reversal-confirmation signals, not entries on their own.
|
||||
- **`%K` / `%D` crossover.** `%K` crossing above `%D` from below is a
|
||||
short-horizon bullish signal; the mirror cross is bearish.
|
||||
- **Divergence.** A price making a new high but `%K` failing to confirm
|
||||
is a classic bearish divergence — same logic as RSI divergence but on
|
||||
a faster, range-based oscillator.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`%K` on a flat candle window is `50`, not undefined.** During a
|
||||
quiet drift where `HH == LL`, the convention used here is `50.0` and
|
||||
`%D` therefore also converges to `50.0`. Do not interpret a sequence
|
||||
of `50`s as a real oversold/overbought cycle — it is the silent-market
|
||||
fallback path.
|
||||
- **Wickra exposes only the fast variant.** "Slow stochastic" is `%K =
|
||||
SMA(raw_%K, slow_k)` with `%D = SMA(%K, d_period)` on top. The
|
||||
built-in `Stochastic` skips the first SMA; to reproduce the slow
|
||||
variant, drive the raw `%K` (taken from `stoch.update(candle).k`)
|
||||
through your own `Sma`.
|
||||
|
||||
## References
|
||||
|
||||
- George C. Lane, *Investment Educators* seminars and articles
|
||||
(late 1950s, popularised through the 1980s) — the original
|
||||
formulation of `%K` and `%D` as a fast oscillator.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — sister bounded oscillator, slower
|
||||
and smoother than `%K`.
|
||||
- [Indicator: WilliamsR](../momentum-oscillators/Indicator-WilliamsR.md) — the negated mirror of
|
||||
fast `%K`, plotted on `[−100, 0]`.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — `k_period + d_period − 1` rule
|
||||
in context.
|
||||
@@ -1,159 +0,0 @@
|
||||
# TSI
|
||||
|
||||
> True Strength Index — a double-smoothed momentum oscillator that strips
|
||||
> noise while keeping a clean, zero-centred read on trend pressure.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | roughly `[−100, 100]`, centred on zero |
|
||||
| Default parameters | `(long = 25, short = 13)` (Python) |
|
||||
| Warmup period | `long + short` |
|
||||
| Interpretation | Positive = net upward pressure, negative = net downward. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
momentum_t = price_t − price_{t−1}
|
||||
TSI = 100 · EMA_short(EMA_long(momentum)) / EMA_short(EMA_long(|momentum|))
|
||||
```
|
||||
|
||||
The 1-bar momentum and its absolute value are each smoothed twice — first
|
||||
with an EMA of length `long`, then with an EMA of length `short`. The
|
||||
ratio of the two double-smoothed series normalises the result: when every
|
||||
recent move is up, numerator and denominator are equal and TSI saturates
|
||||
at `+100`; when every move is down, at `−100`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|---------|---------|---------------|-------------|-------------|
|
||||
| `long` | `usize` | `25` (Python) | `>= 1` | First (slow) smoothing length. `0` errors with `Error::PeriodZero`. |
|
||||
| `short` | `usize` | `13` (Python) | `>= 1` | Second (fast) smoothing length. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults the pair to `(25, 13)` via
|
||||
`#[pyo3(signature = (long=25, short=13))]`. Node and WASM take both
|
||||
explicitly. The `periods` property returns `(long, short)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/tsi.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Tsi {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Tsi::new(long, short).warmup_period() == long + short`. The momentum
|
||||
series starts on input 2; the SMA-seeded `long` EMA seeds at input
|
||||
`long + 1`, and the `short` EMA stacked on top seeds `short − 1` inputs
|
||||
later, so the first non-`None` output lands on input `long + short`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure trend.** A monotone rising series saturates at `+100`, a falling
|
||||
one at `−100` — `|momentum|` equals `momentum` (or its negative), so the
|
||||
ratio is `±1` (`pure_uptrend_saturates_at_plus_100` /
|
||||
`pure_downtrend_saturates_at_minus_100` pin this).
|
||||
- **Constant series.** Every momentum is `0`; the `0 / 0` is guarded and
|
||||
the output is `0.0` (`constant_series_yields_zero` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
smoothing chains are not advanced.
|
||||
- **Reset.** `tsi.reset()` clears the previous price and all four EMAs.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Tsi};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let prices: Vec<f64> = (1..=40).map(f64::from).collect();
|
||||
let mut tsi = Tsi::new(5, 3)?;
|
||||
let out = tsi.batch(&prices);
|
||||
println!("warmup_period = {}", tsi.warmup_period());
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 8
|
||||
last = Some(100.0)
|
||||
```
|
||||
|
||||
A pure ramp has a constant `+1` momentum, so the double-smoothed ratio is
|
||||
exactly `1` and TSI saturates at `+100`. This matches the
|
||||
`pure_uptrend_saturates_at_plus_100` test in
|
||||
`crates/wickra-core/src/indicators/tsi.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
tsi = ta.TSI() # (long=25, short=13)
|
||||
prices = np.linspace(100.0, 80.0, 60) # steady downtrend
|
||||
out = tsi.batch(prices)
|
||||
print("last =", out[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
last = -100.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const tsi = new ta.TSI(25, 13);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + i);
|
||||
console.log('last:', tsi.batch(prices).at(-1));
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Tsi` is a low-noise momentum oscillator. The standard signals are the
|
||||
zero-line cross (momentum changing sign), overbought/oversold extremes
|
||||
near `±25` for the default settings, and a signal-line cross — many
|
||||
traders overlay an EMA of TSI and trade the crossover. The double
|
||||
smoothing makes divergences unusually clean compared with raw momentum.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading it as a `[0, 100]` oscillator.** TSI is centred on zero and
|
||||
signed; `+25` is "strong up", not "mid-range".
|
||||
- **Under-budgeting warmup.** Warmup is `long + short` — for the default
|
||||
`(25, 13)` that is 38 bars.
|
||||
|
||||
## References
|
||||
|
||||
William Blau, "True Strength Index", *Technical Analysis of Stocks &
|
||||
Commodities* (1991), and *Momentum, Direction, and Divergence* (1995).
|
||||
The double-EMA-of-momentum definition here follows Blau's original.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Mom.md](../momentum-oscillators/Indicator-Mom.md) — the raw momentum TSI smooths.
|
||||
- [Indicator-MacdIndicator.md](../trend-directional/Indicator-MacdIndicator.md) — another
|
||||
EMA-difference momentum oscillator with a signal line.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,178 +0,0 @@
|
||||
# UltimateOscillator
|
||||
|
||||
> Ultimate Oscillator — Larry Williams' momentum oscillator that blends
|
||||
> three lookback periods into one bounded `[0, 100]` reading.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 100]` |
|
||||
| Default parameters | `(short = 7, mid = 14, long = 28)` (Python) |
|
||||
| Warmup period | `max(short, mid, long) + 1` |
|
||||
| Interpretation | Weighted three-timeframe buying pressure; `50` is neutral. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
true_low_t = min(low_t, close_{t−1})
|
||||
BP_t = close_t − true_low_t (buying pressure)
|
||||
TR_t = max(high_t, close_{t−1}) − true_low_t (true range)
|
||||
avg_n = Σ BP over n / Σ TR over n
|
||||
UO = 100 · (4·avg_short + 2·avg_mid + avg_long) / 7
|
||||
```
|
||||
|
||||
A single-timeframe momentum oscillator can show false divergences when
|
||||
its lookback does not match the swing being measured. The Ultimate
|
||||
Oscillator averages buying pressure over *three* windows and weights the
|
||||
fastest (`4×`) above the medium (`2×`) and slow (`1×`), which damps those
|
||||
false signals while keeping the response quick.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|---------|---------|---------------|-------------|-------------|
|
||||
| `short` | `usize` | `7` (Python) | `>= 1` | Fast lookback (weight `4`). `0` errors with `Error::PeriodZero`. |
|
||||
| `mid` | `usize` | `14` (Python) | `>= 1` | Medium lookback (weight `2`). |
|
||||
| `long` | `usize` | `28` (Python) | `>= 1` | Slow lookback (weight `1`). |
|
||||
|
||||
The Python binding defaults the trio to `(7, 14, 28)` via
|
||||
`#[pyo3(signature = (short=7, mid=14, long=28))]`. Node and WASM take all
|
||||
three explicitly. The `periods` property returns `(short, mid, long)`.
|
||||
`UltimateOscillator::classic()` is the conventional `(7, 14, 28)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ultimate_oscillator.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for UltimateOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`UltimateOscillator` is a **candle-input** indicator: it reads `high`,
|
||||
`low` and `close`. In Python the streaming `update` accepts a 6-tuple or
|
||||
a dict; the batch helper takes `high`, `low`, `close` numpy arrays. Node
|
||||
and WASM expose `update(high, low, close)` and `batch(high, low, close)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == max(short, mid, long) + 1`. The first bar has no
|
||||
previous close, so the first `BP`/`TR` pair forms on bar 2; the longest
|
||||
window must then fill, so the first non-`None` output lands on input
|
||||
`max(short, mid, long) + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure uptrend.** Bars that each close higher have `BP == TR`, so every
|
||||
ratio is `1` and UO saturates at `100`
|
||||
(`pure_uptrend_saturates_at_100` pins this).
|
||||
- **Pure downtrend.** Bars that each close lower have `BP == 0`, so UO is
|
||||
`0` (`pure_downtrend_saturates_at_0` pins this).
|
||||
- **Flat market.** Identical bars have zero true range; each window
|
||||
contributes the neutral ratio `0.5`, so UO reads `50`
|
||||
(`flat_market_reads_50` pins this).
|
||||
- **Bounds.** The output is always within `[0, 100]`
|
||||
(`output_stays_within_0_100` pins this).
|
||||
- **Candle validation.** `Candle::new` rejects NaN/infinite fields, so
|
||||
`update` never sees an invalid bar.
|
||||
- **Reset.** `uo.reset()` clears the previous close, the rolling window
|
||||
and all six running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, UltimateOscillator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut uo = UltimateOscillator::classic(); // (7, 14, 28)
|
||||
// 30 flat candles, each closing one tick higher than the last.
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let p = 100.0 + f64::from(i);
|
||||
Candle::new(p, p, p, p, 1.0, i64::from(i)).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let out = uo.batch(&candles);
|
||||
println!("warmup_period = {}", uo.warmup_period());
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 29
|
||||
last = Some(100.0)
|
||||
```
|
||||
|
||||
Every bar closes higher with `BP == TR`, so UO saturates at `100`. This
|
||||
matches the `pure_uptrend_saturates_at_100` test in
|
||||
`crates/wickra-core/src/indicators/ultimate_oscillator.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
uo = ta.UltimateOscillator() # (7, 14, 28)
|
||||
high = np.full(40, 100.0)
|
||||
low = np.full(40, 100.0)
|
||||
close = np.full(40, 100.0) # perfectly flat market
|
||||
print(uo.batch(high, low, close)[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
50.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const uo = new ta.UltimateOscillator(7, 14, 28);
|
||||
const flat = Array.from({ length: 40 }, () => 100);
|
||||
console.log(uo.batch(flat, flat, flat).at(-1)); // 50
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`UltimateOscillator` is read with the usual overbought/oversold lens —
|
||||
above `70` is stretched, below `30` is washed out — but Larry Williams'
|
||||
canonical signal is *divergence with confirmation*: price makes a new
|
||||
extreme while UO does not, then UO breaks the level of the divergence.
|
||||
The three-timeframe blend makes those divergences more reliable than a
|
||||
single-period oscillator.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** It needs `high`/`low`/`close`; it takes a
|
||||
`Candle`, not an `f64`.
|
||||
- **Reordering the periods.** The `4 / 2 / 1` weights assume `short` is
|
||||
the fastest window — keep `short < mid < long`. Any positive periods
|
||||
are accepted, but mis-ordering them inverts the intended weighting.
|
||||
|
||||
## References
|
||||
|
||||
Larry Williams, "The Ultimate Oscillator", *Technical Analysis of Stocks
|
||||
& Commodities* (1985). The buying-pressure / true-range definition and the
|
||||
`4 / 2 / 1` weighting follow Williams' original.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Stochastic.md](../momentum-oscillators/Indicator-Stochastic.md) — single-timeframe
|
||||
bounded oscillator.
|
||||
- [Indicator-Rsi.md](../momentum-oscillators/Indicator-Rsi.md) — the canonical momentum oscillator.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,183 +0,0 @@
|
||||
# WilliamsR
|
||||
|
||||
> Williams %R — Larry Williams' negated mirror of fast Stochastic %K,
|
||||
> plotted on `[−100, 0]` instead of `[0, 100]`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Momentum Oscillators |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−100, 0]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` (14 for `period = 14`) |
|
||||
| Interpretation | overbought above `−20`, oversold below `−80` |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new candle, let `HH` and `LL` be the highest high and lowest
|
||||
low over the last `period` candles:
|
||||
|
||||
```
|
||||
HH_t = max(high_{t-period+1}, …, high_t)
|
||||
LL_t = min(low_{t-period+1}, …, low_t)
|
||||
|
||||
%R_t = −100 · (HH_t − close_t) / (HH_t − LL_t) when HH ≠ LL
|
||||
%R_t = −50 when HH == LL (flat range)
|
||||
```
|
||||
|
||||
This is the negation of fast Stochastic `%K` measured from the *top* of
|
||||
the window: when the close sits at the window high, `%R = 0`; when it
|
||||
sits at the window low, `%R = −100`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` | `>= 1` | Lookback window for the `HH` / `LL` extrema. |
|
||||
|
||||
`WilliamsR::new(0)` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for WilliamsR`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
```
|
||||
|
||||
Python's `WilliamsR.batch(high, low, close)` returns a 1-D `float64`
|
||||
`np.ndarray` (warmup → `NaN`). Node's `WilliamsR.batch(high, low, close)`
|
||||
returns a flat `number[]` (warmup → `NaN`); only `batch` is exposed on
|
||||
the Node binding.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `period`. Williams %R works on a rolling
|
||||
range, not a rolling diff, so once `period` candles have arrived the
|
||||
indicator is ready — there is no off-by-one. The first `period − 1`
|
||||
calls to `update()` return `None`; the `period`-th call returns the
|
||||
first `Some(value)`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Close at the window high.** `%R == 0` exactly. The unit test
|
||||
`close_at_high_yields_zero` pins this case (with H, L = 8, 10, 12 and
|
||||
closes ending at 12, the result is `0`). Note that floating-point
|
||||
zero can print as `-0` when scaled by `-100`; both compare equal to
|
||||
`0`.
|
||||
- **Close at the window low.** `%R == −100` exactly (test
|
||||
`close_at_low_yields_minus_100`).
|
||||
- **Flat range.** When `HH == LL`, the implementation returns `−50` as
|
||||
the neutral convention.
|
||||
- **Reset.** `reset()` clears the candle buffer; the next `period`
|
||||
updates return `None`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, WilliamsR};
|
||||
|
||||
let candles = vec![
|
||||
Candle::new(9.0, 10.0, 8.0, 9.0, 1.0, 0).unwrap(),
|
||||
Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap(),
|
||||
Candle::new(12.0, 12.0, 10.0, 12.0, 1.0, 0).unwrap(), // close == HH
|
||||
];
|
||||
let mut w = WilliamsR::new(3)?;
|
||||
let out = w.batch(&candles);
|
||||
println!("Williams %R(3) at idx 2 = {}", out[2].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
Williams %R(3) at idx 2 = -0
|
||||
```
|
||||
|
||||
(`-0.0` is bit-equal to `0.0` in IEEE-754; the negative sign is just a
|
||||
side effect of multiplying `+0.0` by `-100.0`.)
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
high = np.array([10.0, 11.0, 12.0])
|
||||
low = np.array([8.0, 9.0, 10.0])
|
||||
close = np.array([9.0, 10.0, 12.0])
|
||||
w = ta.WilliamsR(3)
|
||||
out = w.batch(high, low, close)
|
||||
print('warmup:', w.warmup_period())
|
||||
print('row 2 :', out[2])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 3
|
||||
row 2 : -0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const high = [10.0, 11.0, 12.0];
|
||||
const low = [8.0, 9.0, 10.0];
|
||||
const close = [9.0, 10.0, 12.0];
|
||||
const w = new wickra.WilliamsR(3);
|
||||
const out = w.batch(high, low, close);
|
||||
console.log('row 2:', out[2]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 2: -0
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Larry Williams' thresholds.** `%R > −20` is overbought; `%R < −80`
|
||||
is oversold. Because the scale runs from `−100` (oversold) to `0`
|
||||
(overbought), the inequalities feel inverted to anyone used to
|
||||
Stochastic — but the *positions* of the bands are identical.
|
||||
- **Failure swings.** A `%R` value that pokes into overbought, retreats,
|
||||
then fails to reach overbought on the next rally is the classic
|
||||
Williams "failure swing" — interpreted as bearish exhaustion.
|
||||
- **Use alongside trend.** %R is a pure range oscillator; in a strong
|
||||
trend it can stay pinned at `0` or `−100` for many bars. Pair with
|
||||
ADX or a moving-average filter before reading it as a reversal cue.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Sign inversion.** Williams %R lives in `[−100, 0]`, not `[0, 100]`.
|
||||
Code that assumes "higher value = more bullish" will work; code that
|
||||
assumes a positive range will silently mis-classify every value.
|
||||
- **Mirror of fast %K, not slow.** Williams %R has no built-in
|
||||
smoothing; it tracks raw `%K` (with a sign flip and a shift). If you
|
||||
need a smoothed version, drive `%R` through your own `Sma` or `Ema`
|
||||
via a `Chain`.
|
||||
|
||||
## References
|
||||
|
||||
- Larry Williams, *How I Made One Million Dollars … Last Year …
|
||||
Trading Commodities*, Windsor Books, 1973 — the original %R
|
||||
publication.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Stochastic](../momentum-oscillators/Indicator-Stochastic.md) — the positive-axis
|
||||
sibling; `%R` and `%K` are linked by `%R = %K − 100`.
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — slower bounded oscillator,
|
||||
better behaved in trending markets.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — bare `period` (no off-by-one).
|
||||
@@ -1,213 +0,0 @@
|
||||
# DEMA
|
||||
|
||||
> Double Exponential Moving Average — Patrick Mulloy's `2·EMA − EMA(EMA)`,
|
||||
> a single-line trend filter that removes the first-order lag of a plain
|
||||
> EMA.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `2·period − 1` |
|
||||
| Interpretation | EMA-style smoothing with less lag; sits ahead of `Ema` on a sustained trend. |
|
||||
|
||||
## Formula
|
||||
|
||||
Let `EMA1 = EMA(price, period)` and `EMA2 = EMA(EMA1, period)`. Then:
|
||||
|
||||
```
|
||||
DEMA_t = 2 * EMA1_t - EMA2_t
|
||||
```
|
||||
|
||||
Both inner EMAs use the same `period`, hence the same
|
||||
`α = 2 / (period + 1)`. The subtraction is a finite-difference
|
||||
approximation of "remove the lag introduced by single EMA smoothing":
|
||||
if EMA lags the true series by `L`, then EMA(EMA) lags by roughly `2L`,
|
||||
so `2·EMA − EMA(EMA)` cancels most of the first-order error.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Period shared by both internal EMAs. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
(Python class `wickra.DEMA(period)` has no `#[pyo3(signature)]` default;
|
||||
pass `period` explicitly.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/dema.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Dema {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python `update` returns `float | None`, `batch` returns a 1-D
|
||||
`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns
|
||||
`number | null`, `batch` returns `Array<number>` with `NaN` placeholders.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Dema::new(period).warmup_period() == 2 * period - 1`. The comment in
|
||||
the source explains it cleanly:
|
||||
|
||||
> EMA1 seeds at `period`, then EMA2 needs another `period − 1` values to
|
||||
> seed.
|
||||
|
||||
`Ema::new(period)` only starts producing output once it has seen
|
||||
`period` inputs. So `ema1` emits its first value at input `period`. From
|
||||
that point on, `ema2` starts receiving inputs (the outputs of `ema1`)
|
||||
and itself needs `period` of them to seed — first emission at "input
|
||||
`period` of `ema1`" = input `2·period − 1` of `Dema`. For
|
||||
`Dema::new(14)` this gives `27`, matching the table in
|
||||
[Warmup Periods](../../Warmup-Periods.md).
|
||||
|
||||
The implementation uses the `?` operator to short-circuit:
|
||||
`let e1 = self.ema1.update(input)?; let e2 = self.ema2.update(e1)?;`,
|
||||
so `ema2` is only fed once `ema1` actually emits — which is exactly
|
||||
what the warmup arithmetic above models.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[100.0; n]` eventually produces
|
||||
`Some(100.0)`: once both EMAs converge to `100.0`, the output is
|
||||
`2 · 100 − 100 = 100`. The unit test `constant_series_yields_constant_dema`
|
||||
pins this with `Dema::new(5)` over 60 constants.
|
||||
- **NaN / infinity inputs.** Inherited from the inner `Ema`: non-finite
|
||||
inputs are silently dropped and the previously emitted value (if any)
|
||||
is preserved. Inputs that fail to pass `is_finite()` never reach the
|
||||
`2·EMA1 − EMA2` arithmetic.
|
||||
- **Reset.** `dema.reset()` resets both internal EMAs. The next `update`
|
||||
starts a full `2·period − 1` warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Dema, Indicator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut dema = Dema::new(5)?;
|
||||
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
|
||||
let out: Vec<Option<f64>> = dema.batch(&prices);
|
||||
println!("warmup_period = {}", dema.warmup_period());
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 9
|
||||
[None, None, None, None, None, None, None, None, Some(9.0), Some(10.0), Some(11.0), Some(12.0), Some(13.000000000000002), Some(14.000000000000002), Some(15.000000000000002), Some(16.000000000000004), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]
|
||||
```
|
||||
|
||||
The first `Some` arrives at index 8 (the 9th input), exactly as
|
||||
predicted by `2·5 − 1 = 9`. On a linear ramp `1, 2, …, 20`, DEMA tracks
|
||||
the input ramp almost perfectly because the lag has been cancelled to
|
||||
first order — the floating-point tail of `13.000000000000002` is
|
||||
ordinary IEEE-754 drift. The unit test
|
||||
`linear_uptrend_dema_above_ema_eventually` pins the property that
|
||||
`Dema` exceeds `Ema` of the same period on a sustained uptrend.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
dema = ta.DEMA(5)
|
||||
out = dema.batch(np.arange(1.0, 21.0))
|
||||
print("warmup_period =", dema.warmup_period())
|
||||
print(out)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 9
|
||||
[nan nan nan nan nan nan nan nan 9. 10. 11. 12. 13. 14. 15. 16. 17. 18.
|
||||
19. 20.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const dema = new ta.DEMA(5);
|
||||
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
console.log(dema.batch(prices));
|
||||
console.log('warmupPeriod:', dema.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
9, 10,
|
||||
11, 12,
|
||||
13.000000000000002, 14.000000000000002,
|
||||
15.000000000000002, 16.000000000000004,
|
||||
17, 18,
|
||||
19, 20
|
||||
]
|
||||
warmupPeriod: 9
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Dema` is the canonical "I want EMA, but with less lag" answer. On a
|
||||
sustained directional trend the DEMA line sits ahead of an `Ema` of the
|
||||
same period (the unit test pins this). The same signals you use for
|
||||
`Ema` — price-vs-MA crossover, fast-vs-slow MA crossover — apply, and
|
||||
they fire earlier. In return for the lower lag you accept more
|
||||
sensitivity to noise: on choppy data DEMA will whipsaw earlier than EMA
|
||||
of the same period.
|
||||
|
||||
Prefer `Dema` over `Ema` when you want a faster trend filter without
|
||||
moving to a smaller `period` (which would also amplify noise). Prefer
|
||||
`Tema` for *even* less lag at the cost of further noise sensitivity, or
|
||||
`Hma` if you want lag reduction *plus* an inherent smoothing step.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Picking a `period` that's too short for a noisy market.** Because
|
||||
`Dema` removes lag rather than adding smoothing, on choppy series it
|
||||
amplifies high-frequency oscillations. If you reach for `Dema(5)` on
|
||||
a tick-by-tick feed and get a jittery line, the fix is to *raise*
|
||||
`period` — `Dema(20)` is often a better compromise than `Dema(5)`.
|
||||
- **Assuming the first `Dema` value lines up with the first `Ema`
|
||||
value at the same period.** `Ema(14)` first emits at input 14;
|
||||
`Dema(14)` first emits at input 27. If you align a DEMA series to an
|
||||
EMA series in a backtest, account for the offset or use the
|
||||
`~np.isnan(...)` mask (Python) / `is_some()` filter (Rust) to drop the
|
||||
warmup rows.
|
||||
|
||||
## References
|
||||
|
||||
Patrick G. Mulloy, *"Smoothing Data with Faster Moving Averages"*,
|
||||
**Technical Analysis of Stocks & Commodities**, January 1994 (DEMA), and
|
||||
*"Smoothing Data with Less Lag"*, **Technical Analysis of Stocks &
|
||||
Commodities**, February 1994 (TEMA).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the building block.
|
||||
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — three-EMA version, less lag still.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — same lag-reduction goal, built on WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,200 +0,0 @@
|
||||
# EMA
|
||||
|
||||
> Exponential Moving Average with smoothing factor `α = 2 / (period + 1)`,
|
||||
> seeded from the SMA of the first `period` inputs (the TA-Lib convention).
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required; or `Ema::with_alpha(α)` for a custom smoothing factor |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Smoother, less laggy than `Sma` of the same length. |
|
||||
|
||||
## Formula
|
||||
|
||||
For `t >= period` (after warmup):
|
||||
|
||||
```
|
||||
α = 2 / (period + 1)
|
||||
seed = (1 / period) * Σ_{i=0}^{period-1} price_i // SMA of first `period` inputs
|
||||
EMA_t = α * price_t + (1 - α) * EMA_{t-1} // recursive update
|
||||
```
|
||||
|
||||
The first emitted value (at input `period`) is the seed itself, identical
|
||||
to `Sma::new(period)` on the same prefix. From input `period + 1` onward
|
||||
the recursive formula takes over. (`Ema::with_alpha(α)` skips the seed and
|
||||
uses the very first input as the initial state, so `warmup_period() == 1`
|
||||
in that mode — see the `with_alpha` method for details.)
|
||||
|
||||
Wilder's smoothing (used by `Rsi`/`Atr`/`Adx`) uses `α = 1/period`
|
||||
instead; that is a different smoothing constant and a different
|
||||
indicator family.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|----------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Window length used to derive `α`. `period = 0` errors with `Error::PeriodZero`. |
|
||||
| `α` (alternative constructor `Ema::with_alpha`) | `f64` | none | `(0.0, 1.0]` and finite | Custom smoothing factor; bypasses the period-derived α. Reported `period` is 1, `warmup_period() == 1`. Invalid `α` errors with `Error::InvalidPeriod`. |
|
||||
|
||||
(The Python class `wickra.EMA(period)` does not set a `#[pyo3(signature)]`
|
||||
default; the period must be passed explicitly. `with_alpha` is Rust-only.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ema.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Ema {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python streams as `float | None`, batches as a 1-D `numpy.ndarray`
|
||||
(`NaN` for warmup). Node streams as `number | null`, batches as
|
||||
`Array<number>` with `NaN` placeholders.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Ema::new(period).warmup_period() == period`. The first non-empty value
|
||||
is the SMA of the first `period` inputs (the "seed"); from there each
|
||||
new input contributes `α * input + (1 − α) * previous`. The unit test
|
||||
`warmup_returns_none_until_seed` and the test
|
||||
`first_value_equals_sma_seed` pin this contract.
|
||||
|
||||
This is the same warmup count as `Sma::new(period)` because the seed
|
||||
itself is an SMA — `Ema` is "no slower to start emitting than `Sma`, just
|
||||
more reactive afterwards".
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[42.0; n]` returns `Some(42.0)` from input
|
||||
`period` onward; the seed is `42.0`, and `α · 42 + (1 − α) · 42 = 42`.
|
||||
The unit test `constant_series_converges_to_constant` pins this.
|
||||
- **NaN / infinity inputs.** The first line of `update` is
|
||||
`if !input.is_finite() { return self.state; }`. Non-finite inputs are
|
||||
silently dropped: they do not advance warmup, do not corrupt the
|
||||
state, and the previously emitted value (if any) is returned. The unit
|
||||
test `ignores_non_finite_input` pins this.
|
||||
- **Reset.** `ema.reset()` clears both the smoothed state and the warmup
|
||||
buffer; the next `update` starts a new warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Ema, Indicator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ema = Ema::new(3)?;
|
||||
let out: Vec<Option<f64>> = ema.batch(&[1.0, 2.0, 3.0, 10.0]);
|
||||
println!("{:?}", out);
|
||||
println!("alpha = {}", ema.alpha());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(2.0), Some(6.0)]
|
||||
alpha = 0.5
|
||||
```
|
||||
|
||||
`period = 3` gives `α = 2 / 4 = 0.5`. The seed at input 3 is the SMA of
|
||||
`[1, 2, 3] = 2.0`; the next step is `0.5 · 10 + 0.5 · 2 = 6.0`. This
|
||||
matches the `step_after_seed_uses_alpha_formula` unit test.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
ema = ta.EMA(3)
|
||||
for x in [1.0, 2.0, 3.0, 10.0]:
|
||||
print(x, '->', ema.update(x))
|
||||
print('alpha:', ema.alpha)
|
||||
print('warmup_period:', ema.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
1.0 -> None
|
||||
2.0 -> None
|
||||
3.0 -> 2.0
|
||||
10.0 -> 6.0
|
||||
alpha: 0.5
|
||||
warmup_period: 3
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ema = new ta.EMA(3);
|
||||
for (const x of [1, 2, 3, 10]) {
|
||||
console.log(x, '->', ema.update(x));
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
1 -> null
|
||||
2 -> null
|
||||
3 -> 2
|
||||
10 -> 6
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Ema` is the "default" smoothed trend filter for most practitioners. The
|
||||
two main signals are price-vs-EMA and EMA-fast-vs-EMA-slow crossovers
|
||||
(the latter is the basis of `MacdIndicator`). Compared with `Sma` at the
|
||||
same period, `Ema` reacts faster to direction changes at the cost of
|
||||
slightly noisier output — useful when you care about the inflection
|
||||
point, not the long-run level.
|
||||
|
||||
Prefer `Ema` over `Sma` when you want a single-line trend filter with
|
||||
moderate lag. Prefer `Dema` / `Tema` when the EMA lag is too much for
|
||||
your timeframe. Prefer `Hma` when you want lag reduction *and* a built-in
|
||||
noise filter (a triple WMA chain rather than a triple EMA chain).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing `α = 2/(n+1)` with Wilder's `α = 1/n`.** Wickra's `Ema`
|
||||
uses the TA-Lib convention `α = 2/(n+1)`. The same numerical period
|
||||
passed to `Rsi(14)` or `Atr(14)` uses `α = 1/14 ≈ 0.0714`, not
|
||||
`α = 2/15 ≈ 0.1333`. They are different smoothing schemes; comparing
|
||||
an EMA(14) line directly to the RSI/ATR's internal smoothing will not
|
||||
match. If you want a Wilder-style EMA, build it on top of `Ema` with
|
||||
the custom factor: `Ema::with_alpha(1.0 / 14.0)`.
|
||||
- **Assuming the first emitted EMA is "the EMA".** The first value is
|
||||
the SMA seed, not a recursively-smoothed EMA. The series only starts
|
||||
behaving like an EMA from input `period + 1` onward. For short series,
|
||||
this means the first emission tracks `Sma::new(period)` exactly — that
|
||||
is the intended behaviour, not a bug.
|
||||
|
||||
## References
|
||||
|
||||
The TA-Lib seeding convention used here ("EMA is seeded with an SMA")
|
||||
is documented in the TA-Lib source and replicated by virtually every
|
||||
commercial charting platform. The recursive form
|
||||
`EMA_t = α · price + (1 − α) · EMA_{t-1}` is the standard exponential
|
||||
smoothing identity attributed to Robert Brown (1956).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — equal weights, identical seed.
|
||||
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — `2·EMA − EMA(EMA)`.
|
||||
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — `3·EMA − 3·EMA(EMA) + EMA(EMA(EMA))`.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,228 +0,0 @@
|
||||
# HMA
|
||||
|
||||
> Hull Moving Average — Alan Hull's
|
||||
> `WMA(2·WMA(n/2) − WMA(n), √n)`, a near-lag-free trend filter that
|
||||
> combines a fast `Wma(n/2)`, a slow `Wma(n)`, and a final smoothing pass.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period (`warmup_period()`) | `period + round(√period).max(1) − 1` — exact first-emission index |
|
||||
| Interpretation | Near-zero-lag trend line with an inherent smoothing step. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
half = max(period / 2, 1) // integer division
|
||||
smooth = max(round(sqrt(period)), 1) // nearest integer, floor at 1
|
||||
|
||||
raw_t = 2 * WMA(price, half)_t - WMA(price, period)_t
|
||||
HMA_t = WMA(raw, smooth)_t
|
||||
```
|
||||
|
||||
The "magic" is the `2·WMA(n/2) − WMA(n)` step: the fast WMA leads the
|
||||
slow WMA on a trend, so doubling the fast and subtracting the slow
|
||||
produces a series that is *ahead* of the input by roughly the WMA lag.
|
||||
The final `WMA(…, √n)` then smooths the resulting overshoot back down
|
||||
to a clean line. For `period = 9` this gives `half = 4`, `smooth = 3`;
|
||||
for `period = 14`, `half = 7`, `smooth = 4`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Top-level lookback. The inner WMA periods are derived from it. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
(Python class `wickra.HMA(period)` has no `#[pyo3(signature)]` default;
|
||||
pass `period` explicitly.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/hma.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Hma {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python returns `float | None` (streaming) / `numpy.ndarray` (batch,
|
||||
`NaN` for warmup). Node returns `number | null` / `Array<number>` with
|
||||
`NaN`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns:
|
||||
|
||||
```
|
||||
period + round(sqrt(period)).max(1) - 1
|
||||
```
|
||||
|
||||
which gives `11` for `Hma::new(9)`, `17` for `Hma::new(14)`,
|
||||
`19` for `Hma::new(16)`. This figure is **exact**: the first non-`None`
|
||||
output lands on input `warmup_period()` (index `warmup_period() - 1`).
|
||||
|
||||
The number reflects how the three inner WMAs warm up *in parallel*: the
|
||||
slow `WMA(period)` emits at input `period`, then the smoothing
|
||||
`WMA(√period)` needs `√period − 1` more inputs on top.
|
||||
|
||||
```rust
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
// Both raw WMAs are fed unconditionally so neither delays the other.
|
||||
let h = self.half_wma.update(input);
|
||||
let f = self.full_wma.update(input);
|
||||
match (h, f) {
|
||||
(Some(h), Some(f)) => self.smooth_wma.update(2.0 * h - f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`half_wma` and `full_wma` receive every input, so `full_wma` emits at
|
||||
input `period` (not later). The `2·half − full` diff then flows into
|
||||
`smooth_wma`, which needs `round(√period)` of those — giving a first
|
||||
emission at exactly `period + round(√period) − 1`.
|
||||
|
||||
| `period` | `round(√period)` | `warmup_period()` | First emission (input #) |
|
||||
|----------|------------------|-------------------|--------------------------|
|
||||
| 9 | 3 | 11 | 11 |
|
||||
| 14 | 4 | 17 | 17 |
|
||||
| 16 | 4 | 19 | 19 |
|
||||
|
||||
This is pinned by the `first_emission_matches_warmup_period` test in
|
||||
`hma.rs`: the first call that returns `Some` is exactly at
|
||||
`warmup_period() - 1` (0-indexed).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[10.0; n]` produces `Some(10.0)` once
|
||||
the chain is warm. All three WMAs converge to `10`, so
|
||||
`raw = 2·10 − 10 = 10`, then `WMA(10, smooth) = 10`. The unit test
|
||||
`constant_series_yields_constant_hma` pins this with `Hma::new(9)`
|
||||
over 80 constants.
|
||||
- **NaN / infinity inputs.** Inherited from the inner `Wma`: non-finite
|
||||
inputs are silently dropped at the half/full WMA boundary and never
|
||||
reach the `2·h − f` arithmetic.
|
||||
- **Reset.** `hma.reset()` resets all three internal WMAs; the next
|
||||
`update` starts a full warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Hma, Indicator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut hma = Hma::new(9)?;
|
||||
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
|
||||
let out: Vec<Option<f64>> = hma.batch(&prices);
|
||||
println!("warmup_period = {}", hma.warmup_period());
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 11
|
||||
[None, None, None, None, None, None, None, None, None, None, Some(11.0), Some(12.0), Some(13.0), Some(14.0), Some(15.0), Some(16.0), Some(17.0), Some(18.0), Some(19.0), Some(20.0)]
|
||||
```
|
||||
|
||||
The first `Some` lands at index 10 (the 11th input) — exactly
|
||||
`warmup_period() - 1`, as the [Warmup](#warmup) section explains. On the
|
||||
linear ramp `1, 2, …, 20`, HMA tracks price exactly with no visible lag.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
hma = ta.HMA(9)
|
||||
out = hma.batch(np.arange(1.0, 21.0))
|
||||
print("warmup_period =", hma.warmup_period())
|
||||
print(out)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 11
|
||||
[nan nan nan nan nan nan nan nan nan nan 11. 12. 13. 14. 15. 16. 17. 18.
|
||||
19. 20.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const hma = new ta.HMA(9);
|
||||
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
console.log(hma.batch(prices));
|
||||
console.log('warmupPeriod:', hma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN, NaN, NaN, NaN, NaN, NaN,
|
||||
NaN, NaN, NaN, NaN, 11, 12,
|
||||
13, 14, 15, 16, 17, 18,
|
||||
19, 20
|
||||
]
|
||||
warmupPeriod: 11
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Hma` is the lag-reduction trend filter that does *not* require you to
|
||||
choose between responsiveness and noise: the final `WMA(√period)` pass
|
||||
is a built-in smoothing step that prevents the kind of whipsaw a `Tema`
|
||||
of the same period would produce on noisy data. On clean trending data
|
||||
it sits effectively on top of price; on choppy data the smoothing pass
|
||||
keeps the line readable.
|
||||
|
||||
The textbook signal is colour-coded slope: HMA turning up = uptrend,
|
||||
turning down = downtrend. Crossover patterns (`Hma(9)` vs `Hma(20)`)
|
||||
also work and tend to be cleaner than the equivalent EMA pair.
|
||||
|
||||
Prefer `Hma` over `Dema` / `Tema` when your data is noisy enough that
|
||||
the lag-reduction in those would manifest as whipsaws. Prefer `Tema` /
|
||||
`Dema` on cleaner data where you want one fewer smoothing step.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Mis-reading the warmup as a lag.** `warmup_period()` is the exact
|
||||
first-emission index (`Hma::new(9).warmup_period() == 11`, first
|
||||
`Some` at the 11th input), so it can be used directly for `Chain`
|
||||
alignment. The leading `None`/`NaN` values are warmup, not lag — once
|
||||
HMA emits it tracks price with near-zero lag.
|
||||
- **Picking `period = 2` or `3`.** The inner `half = period / 2` is an
|
||||
integer division floored at 1. For `period = 2`, `half = 1`,
|
||||
`smooth = 1`, and you essentially end up with `Wma(2·price − WMA(2))`
|
||||
which is a sharp, noisy line. HMA is designed for `period >= 9` or so;
|
||||
for shorter lookbacks reach for `Ema(period)` or `Wma(period)` instead.
|
||||
|
||||
## References
|
||||
|
||||
Alan Hull, *"How to Reduce Lag in a Moving Average"*, 2005 — the
|
||||
original HMA derivation, hosted on Hull's site at
|
||||
<https://alanhull.com/hull-moving-average>.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — the building block.
|
||||
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — same lag-reduction goal, EMA-based.
|
||||
- [Indicator-Kama.md](../moving-averages/Indicator-Kama.md) — adaptive smoothing instead of fixed.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,250 +0,0 @@
|
||||
# KAMA
|
||||
|
||||
> Kaufman's Adaptive Moving Average — picks its own smoothing constant
|
||||
> on every bar from a fast/slow EMA pair, weighted by an efficiency
|
||||
> ratio that measures how trending the recent price action has been.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | Python: `(er_period=10, fast=2, slow=30)`; Rust: `Kama::classic()` returns the same triple |
|
||||
| Warmup period (`warmup_period()`) | `er_period + 1` — see below; the *first* emission lands at this index, but on a fresh KAMA that emission equals the seed (the input itself) |
|
||||
| Interpretation | Fast in trending markets, slow in choppy markets — by construction. |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new input `price_t` (with `n = er_period`):
|
||||
|
||||
```
|
||||
direction_t = | price_t - price_{t-n} |
|
||||
volatility_t = Σ_{i=1}^{n} | price_{t-i+1} - price_{t-i} |
|
||||
ER_t = direction_t / volatility_t // 0 = pure chop, 1 = pure trend; 0 if volatility = 0
|
||||
|
||||
fast_sc = 2 / (fast + 1) // fast EMA smoothing constant
|
||||
slow_sc = 2 / (slow + 1) // slow EMA smoothing constant
|
||||
SC_t = (ER_t * (fast_sc - slow_sc) + slow_sc) ^ 2
|
||||
|
||||
KAMA_t = KAMA_{t-1} + SC_t * (price_t - KAMA_{t-1})
|
||||
```
|
||||
|
||||
The squared `SC_t` is Kaufman's choice (he found that squaring widens
|
||||
the dynamic range between "act like a fast EMA" and "act like a slow
|
||||
EMA"). On the very first emission `KAMA_{t-1}` is seeded with the
|
||||
oldest price in the window (`window.front()`), which is the convention
|
||||
in the source.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python `KAMA(...)`) | Valid range | Description |
|
||||
|-------------|---------|-------------------------------|-------------|-------------|
|
||||
| `er_period` | `usize` | `10` | `>= 1` | Lookback for the efficiency ratio. Larger → smoother ER, slower adaptation. |
|
||||
| `fast` | `usize` | `2` | `>= 1`, strictly `< slow` | Fast EMA period; sets the lower bound on responsiveness. |
|
||||
| `slow` | `usize` | `30` | `>= 1`, strictly `> fast` | Slow EMA period; sets the upper bound on smoothness. |
|
||||
|
||||
Any of `er_period`, `fast`, `slow` being `0` errors with
|
||||
`Error::PeriodZero`; `fast >= slow` errors with `Error::InvalidPeriod`.
|
||||
The Python defaults come from
|
||||
`#[pyo3(signature = (er_period=10, fast=2, slow=30))]` in
|
||||
`bindings/python/src/lib.rs`; the Rust convenience constructor
|
||||
`Kama::classic()` returns the same triple.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/kama.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Kama {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python returns `float | None` (streaming) / `numpy.ndarray` (batch,
|
||||
`NaN` for warmup). Node returns `number | null` (streaming) /
|
||||
`Array<number>` with `NaN` (batch). `warmup_period()` is exposed in
|
||||
Rust and Python but **not** on the Node `KAMA` class (consult
|
||||
`bindings/node/index.d.ts` for the surface).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Kama::new(er_period, fast, slow).warmup_period() == er_period + 1`.
|
||||
The "off-by-one" is because the efficiency ratio compares `price_t` to
|
||||
`price_{t-er_period}` and sums `er_period` consecutive absolute diffs;
|
||||
that requires `er_period + 1` prices in the window. For
|
||||
`Kama::classic()` (`er_period = 10`) the first emission lands on input
|
||||
11, matching the table in [Warmup Periods](../../Warmup-Periods.md).
|
||||
|
||||
The implementation uses a `VecDeque` of capacity `er_period + 1`. Once
|
||||
full, every subsequent `update` pops the front and pushes the new
|
||||
input — `update` is O(`er_period`) in principle (the volatility sum is
|
||||
re-computed) but O(1) in `period`/`fast`/`slow` since the EMA-style
|
||||
recursion has no window.
|
||||
|
||||
Note: on the *first* emission, `prev = window.front()` (the oldest
|
||||
price), and the output is
|
||||
`prev + SC · (input − prev)`. On a perfectly trending series
|
||||
(`ER ≈ 1`, `SC ≈ fast_sc² ≈ 0.444`) this means the first KAMA value
|
||||
is materially below the latest price; on `[1, 2, …, 20]` for instance,
|
||||
KAMA's first emission at input 11 is `5.444…`, not `11`. See the
|
||||
example output below.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[100.0; n]` produces `Some(100.0)`:
|
||||
both `direction` and `volatility` are zero, the source branches
|
||||
`if volatility == 0.0 { 0.0 } else { ... }` so `ER = 0`,
|
||||
`SC = slow_sc² ≈ 0.00416`, and
|
||||
`100 + 0.00416 · (100 − 100) = 100`. The unit test
|
||||
`constant_series_yields_constant_kama` pins this with
|
||||
`Kama::classic()`.
|
||||
- **NaN / infinity inputs.** The first line of `update` is
|
||||
`if !input.is_finite() { return self.state; }`. Non-finite inputs are
|
||||
silently dropped; the window is not advanced, the previously emitted
|
||||
value is preserved.
|
||||
- **Reset.** `kama.reset()` clears both the window and the smoothed
|
||||
state. The next `update` starts a fresh `er_period + 1` warmup
|
||||
countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Kama};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut kama = Kama::classic(); // (10, 2, 30)
|
||||
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
|
||||
let out: Vec<Option<f64>> = kama.batch(&prices);
|
||||
println!("warmup_period = {}", kama.warmup_period());
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 11
|
||||
[None, None, None, None, None, None, None, None, None, None, Some(5.444444444444443), Some(8.358024691358022), Some(10.421124828532234), Some(12.011736015851241), Some(13.339853342139579), Some(14.522140745633099), Some(15.62341152535172), Some(16.679673069639843), Some(17.710929483133246), Some(18.728294157296247)]
|
||||
```
|
||||
|
||||
`Kama::classic().periods()` returns `(10, 0.6666666666666666, 0.06451612903225806)`
|
||||
— the second and third numbers are `fast_sc = 2/3` and `slow_sc = 2/31`,
|
||||
not the integer `fast`/`slow` periods themselves. On the linear ramp
|
||||
`1, 2, …, 20` the efficiency ratio is `1.0` (every step moves direction
|
||||
the same as volatility), so `SC = fast_sc² ≈ 0.4444`. The first
|
||||
emission `5.444…` is `1 + 0.4444 · (11 − 1)` and each subsequent value
|
||||
follows the same recursion.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
kama = ta.KAMA() # defaults: er_period=10, fast=2, slow=30
|
||||
out = kama.batch(np.arange(1.0, 21.0))
|
||||
print("warmup_period =", kama.warmup_period())
|
||||
print(out)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 11
|
||||
[ nan nan nan nan nan nan
|
||||
nan nan nan nan 5.44444444 8.35802469
|
||||
10.42112483 12.01173602 13.33985334 14.52214075 15.62341153 16.67967307
|
||||
17.71092948 18.72829416]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const kama = new ta.KAMA(10, 2, 30); // no default constructor; pass the triple
|
||||
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
console.log(kama.batch(prices));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
5.444444444444443, 8.358024691358022,
|
||||
10.421124828532234, 12.011736015851241,
|
||||
13.339853342139579, 14.522140745633099,
|
||||
15.62341152535172, 16.679673069639843,
|
||||
17.710929483133246, 18.728294157296247
|
||||
]
|
||||
```
|
||||
|
||||
(The Node `KAMA` class does not expose `warmupPeriod()`; use the Rust
|
||||
or Python binding if you need that getter from your application.)
|
||||
|
||||
## Interpretation
|
||||
|
||||
KAMA's defining property is that it **changes its own behaviour with the
|
||||
market**. In a clean trend the efficiency ratio approaches `1`, `SC`
|
||||
approaches `fast_sc²`, and KAMA behaves like a fast EMA — it tracks
|
||||
price closely. In a choppy sideways market the efficiency ratio
|
||||
collapses toward `0`, `SC` approaches `slow_sc²`, and KAMA effectively
|
||||
freezes — its line goes nearly flat regardless of how violently price
|
||||
oscillates around it. This is by design: Kaufman's argument is that you
|
||||
should not chase noise.
|
||||
|
||||
The two usable signals are slope (positive = uptrend; flat = ranging;
|
||||
negative = downtrend) and price-vs-KAMA crossover. Because KAMA can
|
||||
sit nearly flat for long stretches in a range, "price crossed KAMA"
|
||||
generates fewer false signals than the same test against an EMA of
|
||||
similar period.
|
||||
|
||||
Prefer `Kama` over a static EMA/SMA when the market regime varies
|
||||
materially (trending → ranging → trending). Prefer a fixed `Ema` /
|
||||
`Hma` when you want a predictable smoothing profile that is independent
|
||||
of price action.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Assuming `warmup_period()` is when the line is "good".** The first
|
||||
emission lands at input 11 (for the default `er_period = 10`), but
|
||||
the seed `KAMA_{t-1} = window.front()` is the *oldest* price in the
|
||||
window, so the very first emitted value is biased toward the
|
||||
10-bars-ago price. On a strong trend this means the first 3–5
|
||||
emissions are noticeably below (or above, depending on direction) the
|
||||
current price. If that matters, drop the first `er_period` post-warmup
|
||||
emissions, not just the warmup itself.
|
||||
- **Tuning `fast` and `slow` independently of `er_period`.** Kaufman's
|
||||
derivation assumes `slow >> fast` so that the per-bar SC has room to
|
||||
move. Picking, say, `(10, 5, 6)` gives `fast_sc ≈ 0.333` and
|
||||
`slow_sc ≈ 0.286`, so SC barely changes regardless of the efficiency
|
||||
ratio — KAMA degenerates into "an EMA somewhere around period 6".
|
||||
Keep `slow` at least `5×` `fast` if you want the adaptive behaviour
|
||||
to actually matter.
|
||||
|
||||
## References
|
||||
|
||||
Perry J. Kaufman, *Smarter Trading*, McGraw-Hill, 1995 (book-length
|
||||
introduction); reprinted in Kaufman's *Trading Systems and Methods*
|
||||
across multiple editions, where the squared-SC choice is justified
|
||||
empirically.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the two endpoints (`fast` and
|
||||
`slow`) KAMA interpolates between.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — the other "smart" trend filter in
|
||||
Wickra.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,188 +0,0 @@
|
||||
# SMA
|
||||
|
||||
> Simple Moving Average — the equal-weighted rolling mean of the last
|
||||
> `period` closes, maintained as an O(1) rolling-sum state machine.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Smoothed price level; price-vs-SMA crossings flag direction changes. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
SMA_t = (1 / n) * Σ_{i=0}^{n-1} price_{t-i}
|
||||
```
|
||||
|
||||
where `n = period`. Maintained incrementally as `sum -= window.pop_front();
|
||||
sum += new_price; out = sum / n`, so `update` is O(1) regardless of
|
||||
`period`. To keep f64 rounding error bounded on long-running streams (where
|
||||
catastrophic cancellation between add/subtract pairs could otherwise
|
||||
accumulate), the running `sum` is reseeded from the live window every
|
||||
`16 · period` updates — still amortised O(1) (`O(period)` work amortised
|
||||
over `O(period)` updates), zero observable change on inputs that did not
|
||||
drift to begin with.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|----------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Length of the rolling window. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
|
||||
|
||||
(There is no Python `#[pyo3(signature = …)]` default for `SMA`, so
|
||||
`wickra.SMA(period)` requires the period explicitly.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/sma.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Sma {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. The Python binding maps
|
||||
this to `float | None` (streaming) or a `numpy.ndarray` of dtype
|
||||
`float64` with `NaN` for warmup rows (batch). The Node binding maps it to
|
||||
`number | null` / `Array<number>` with `NaN` for warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Sma::new(period).warmup_period() == period`. The first non-empty value
|
||||
is emitted on the `period`-th `update()` call, because the window needs to
|
||||
hold exactly `period` values before the mean is defined. There is no
|
||||
seeding step beyond filling the window — `Sma` only ever stores its
|
||||
running sum and the `VecDeque` of values, so its readiness condition is
|
||||
literally `window.len() == period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input
|
||||
`period` onward; the running-sum bookkeeping is exact for constants
|
||||
(the unit test `constant_series_yields_constant_sma` pins this).
|
||||
- **NaN / infinity inputs.** The first line of `update` is
|
||||
`if !input.is_finite() { return self.value(); }`. Non-finite inputs are
|
||||
**silently dropped** — they do not advance the window, do not corrupt
|
||||
the sum, and the previous valid value (if any) is returned. The unit
|
||||
test `ignores_non_finite_input_but_keeps_state` pins this behaviour.
|
||||
- **Reset.** `sma.reset()` clears the window and the sum, returning the
|
||||
indicator to a fresh `is_ready() == false` state. The next `update`
|
||||
starts a new warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Sma};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sma = Sma::new(3)?;
|
||||
let out: Vec<Option<f64>> = sma.batch(&[2.0, 4.0, 6.0, 8.0, 10.0]);
|
||||
println!("{:?}", out);
|
||||
println!("warmup_period = {}", sma.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(4.0), Some(6.0), Some(8.0)]
|
||||
warmup_period = 3
|
||||
```
|
||||
|
||||
The first two inputs return `None` while the window fills; the third
|
||||
emits `(2 + 4 + 6) / 3 = 4.0` and every subsequent input slides the
|
||||
window by one. This matches the `known_reference_values` test in
|
||||
`crates/wickra-core/src/indicators/sma.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
sma = ta.SMA(3)
|
||||
print(sma.batch(np.array([2.0, 4.0, 6.0, 8.0, 10.0])))
|
||||
print("warmup_period =", sma.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan 4. 6. 8.]
|
||||
warmup_period = 3
|
||||
```
|
||||
|
||||
Warmup rows come back as `NaN` so the result aligns 1:1 with the input
|
||||
array.
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const sma = new ta.SMA(3);
|
||||
console.log(sma.batch([2, 4, 6, 8, 10]));
|
||||
console.log('warmupPeriod:', sma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 4, 6, 8 ]
|
||||
warmupPeriod: 3
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Sma` is a smoothed price level. The two canonical signals are:
|
||||
|
||||
1. **Price–SMA crossover.** Close above the SMA suggests an uptrend, close
|
||||
below suggests a downtrend. The longer the SMA, the slower (and more
|
||||
trustworthy) the signal.
|
||||
2. **Two-SMA crossover.** A fast SMA crossing above a slow SMA is the
|
||||
classic "golden cross"; below is the "death cross". Either of `Ema`
|
||||
or `Hma` will give earlier (but noisier) signals at the same period.
|
||||
|
||||
Prefer `Sma` when you want the simplest possible reference price — for
|
||||
example, as the middle band of [`BollingerBands`](../../Indicators-Overview.md),
|
||||
which uses an SMA by construction. Prefer `Ema` if you want the same
|
||||
smoothness profile but slightly less lag on direction changes.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Treating `period = 0` as "use a default".** `Sma::new(0)` returns
|
||||
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; there is
|
||||
no implicit default. Pass an explicit period.
|
||||
- **Slicing batch results with `> warmup_period` instead of
|
||||
`~np.isnan(...)`.** In Python the batch output has `NaN` for warmup
|
||||
rows; in Rust it has `None`. Use the warmup-aware mask to filter — see
|
||||
the [Quickstart: Python](../../Quickstart-Python.md#macd-a-multi-column-indicator-and-its-warmup-nans)
|
||||
pattern. Slicing by `prices.size - warmup_period` works for a single
|
||||
indicator but breaks the moment you compose two of them via `Chain`.
|
||||
|
||||
## References
|
||||
|
||||
The simple moving average predates technical analysis as a discipline.
|
||||
The implementation here follows the standard "rolling sum, slide on each
|
||||
update" formulation; the matching reference implementations are TA-Lib
|
||||
and pandas (`rolling(period).mean()`).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — same smoothness budget, less lag.
|
||||
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear weights instead of equal.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — built on three WMAs for near-zero
|
||||
lag.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,170 +0,0 @@
|
||||
# SMMA
|
||||
|
||||
> Smoothed Moving Average — Wilder's running moving average (RMA): an
|
||||
> SMA-seeded exponential average with a slow `1 / period` smoothing factor.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Heavily smoothed price level; the average underlying Wilder's RSI and ATR. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
SMMA_period = SMA(price_1 … price_period) (seed)
|
||||
SMMA_t = (SMMA_{t-1} * (period - 1) + price_t) / period (t > period)
|
||||
```
|
||||
|
||||
This is algebraically an exponential moving average with smoothing factor
|
||||
`alpha = 1 / period` — substantially slower than the `Ema` factor of
|
||||
`2 / (period + 1)` at the same `period`. The recurrence is O(1): each
|
||||
`update` touches only the previous value.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Smoothing length. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
|
||||
|
||||
There is no Python `#[pyo3(signature = …)]` default for `SMMA`, so
|
||||
`wickra.SMMA(period)` requires the period explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/smma.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Smma {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` (streaming) or a `numpy.ndarray` with `NaN` warmup rows
|
||||
(batch); Node maps it to `number | null` / `Array<number>` with `NaN`
|
||||
warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Smma::new(period).warmup_period() == period`. The first `period - 1`
|
||||
inputs are buffered while the seed accumulates; the `period`-th `update()`
|
||||
emits the simple average of those inputs as `SMMA_period`. Every later
|
||||
input applies the `(prev·(n−1)+x)/n` recurrence.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input
|
||||
`period` onward — the recurrence is a fixed point for constants
|
||||
(`constant_series_yields_the_constant` pins this).
|
||||
- **NaN / infinity inputs.** The first line of `update` is
|
||||
`if !input.is_finite() { return self.current; }`. Non-finite inputs are
|
||||
**silently dropped** — they neither advance the seed nor perturb the
|
||||
recurrence, and the previous valid value (if any) is returned.
|
||||
- **Reset.** `smma.reset()` clears the seed buffer and the current value,
|
||||
restarting the warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Smma};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut smma = Smma::new(3)?;
|
||||
let out: Vec<Option<f64>> = smma.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
println!("{:?}", out);
|
||||
println!("warmup_period = {}", smma.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(2.0), Some(2.6666666666666665), Some(3.4444444444444446)]
|
||||
warmup_period = 3
|
||||
```
|
||||
|
||||
The third input emits the seed `(1 + 2 + 3) / 3 = 2.0`; the fourth applies
|
||||
`(2.0·2 + 4) / 3 = 8/3`; the fifth `(8/3·2 + 5) / 3 = 31/9`. This matches
|
||||
the `warmup_then_recurrence` test in
|
||||
`crates/wickra-core/src/indicators/smma.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
smma = ta.SMMA(3)
|
||||
print(smma.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0])))
|
||||
print("warmup_period =", smma.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan 2. 2.6666667 3.4444444]
|
||||
warmup_period = 3
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const smma = new ta.SMMA(3);
|
||||
console.log(smma.batch([1, 2, 3, 4, 5]));
|
||||
console.log('warmupPeriod:', smma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 2, 2.6666666666666665, 3.4444444444444446 ]
|
||||
warmupPeriod: 3
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Smma` is a very smooth, lag-heavy price level. Because its smoothing
|
||||
factor is `1 / period` rather than `2 / (period + 1)`, an `Smma(n)` is
|
||||
roughly as smooth as an `Ema(2n − 1)` — useful when you want maximum
|
||||
noise rejection from a single line. Its main role in this library,
|
||||
however, is structural: it is the exact smoothing kernel inside
|
||||
[`Rsi`](../momentum-oscillators/Indicator-Rsi.md) and [`Atr`](../volatility-bands/Indicator-Atr.md),
|
||||
so reaching for `Smma` directly lets you reproduce Wilder-style averages
|
||||
on any series.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing it with `Ema` at the same period.** `Smma(n)` and `Ema(n)`
|
||||
are *not* interchangeable — `Smma` lags far more. Match `Ema(2n − 1)`
|
||||
if you need comparable smoothness.
|
||||
- **Treating `period = 0` as "use a default".** `Smma::new(0)` returns
|
||||
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; pass an
|
||||
explicit period.
|
||||
|
||||
## References
|
||||
|
||||
The smoothed moving average is J. Welles Wilder Jr.'s running average
|
||||
from *New Concepts in Technical Trading Systems* (1978); it is the
|
||||
averaging step in his RSI, ATR and ADX. The implementation here follows
|
||||
the standard SMA-seeded formulation, matching TA-Lib's `RMA`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — faster exponential average.
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the equal-weighted mean used as
|
||||
the SMMA seed.
|
||||
- [Indicator-Trima.md](../moving-averages/Indicator-Trima.md) — the other F1 average.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,171 +0,0 @@
|
||||
# T3
|
||||
|
||||
> Tillson T3 — a six-fold cascaded EMA recombined with a volume factor `v`
|
||||
> to give a smooth, low-lag trend line.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` required; `v = 0.7` (Python default) |
|
||||
| Warmup period | `6·period − 5` |
|
||||
| Interpretation | Smooth trend line with less lag than a same-period EMA. |
|
||||
|
||||
## Formula
|
||||
|
||||
T3 is the *generalised DEMA* (`GD`) applied three times. Tim Tillson's
|
||||
expansion of `GD(GD(GD(price)))` over six chained EMAs — `e1 … e6`, each
|
||||
of the same `period`, where `e2 = EMA(e1)`, `e3 = EMA(e2)`, … — is:
|
||||
|
||||
```
|
||||
v2 = v², v3 = v³
|
||||
c1 = −v3
|
||||
c2 = 3·v2 + 3·v3
|
||||
c3 = −6·v2 − 3·v − 3·v3
|
||||
c4 = 1 + 3·v + v3 + 3·v2
|
||||
T3 = c1·e6 + c2·e5 + c3·e4 + c4·e3
|
||||
```
|
||||
|
||||
The four coefficients always sum to `1`, so a constant price series maps
|
||||
to itself. The volume factor `v` controls the lag/overshoot trade-off:
|
||||
`v = 0` collapses T3 to the plain triple-cascaded EMA `e3`; the
|
||||
conventional `v = 0.7` adds a corrective hump that sharpens turns.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|----------------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Length of every EMA in the cascade. `period = 0` errors with `Error::PeriodZero`. |
|
||||
| `v` | `f64` | `0.7` (Python) | `[0.0, 1.0]`| Volume factor. Non-finite or out-of-range values error with `Error::InvalidPeriod`. |
|
||||
|
||||
The Python binding defaults `v` to `0.7` via `#[pyo3(signature = (period, v=0.7))]`;
|
||||
`period` is always explicit. The Node and WASM constructors take both
|
||||
arguments explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/t3.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for T3 {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`T3::new(period, v).warmup_period() == 6·period − 5`. Each stage of the
|
||||
SMA-seeded EMA cascade adds `period − 1` bars of delay: `e1` seeds at
|
||||
input `period`, `e2` at `2·period − 1`, …, `e6` at `6·period − 5`. T3
|
||||
emits its first value once `e6` is ready, since the output formula needs
|
||||
`e3` through `e6`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Because `c1 + c2 + c3 + c4 = 1` for any `v`, a flat
|
||||
input series produces a flat output equal to the constant
|
||||
(`coefficients_sum_to_one` and `constant_series_yields_the_constant`
|
||||
pin this).
|
||||
- **`v = 0`.** The coefficients become `c1 = c2 = c3 = 0`, `c4 = 1`, so
|
||||
`T3` is exactly the third stage of the EMA cascade
|
||||
(`zero_volume_factor_collapses_to_triple_cascaded_ema` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped — the
|
||||
cascade is not advanced — and the previous valid value is returned.
|
||||
- **Reset.** `t3.reset()` clears all six EMAs and the cached value.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, T3};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let prices: Vec<f64> = (1..=40).map(f64::from).collect();
|
||||
let mut t3 = T3::new(3, 0.7)?;
|
||||
let out = t3.batch(&prices);
|
||||
println!("warmup_period = {}", t3.warmup_period());
|
||||
println!("first ready index = {:?}", out.iter().position(Option::is_some));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 13
|
||||
first ready index = Some(12)
|
||||
```
|
||||
|
||||
`T3(3, 0.7)` warms up after `6·3 − 5 = 13` inputs, so the first non-`None`
|
||||
output sits at index `12`. On a pure ramp the output then tracks the input
|
||||
trend with a smooth, near-constant offset.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
t3 = ta.T3(5) # v defaults to 0.7
|
||||
prices = np.linspace(100.0, 140.0, 60)
|
||||
out = t3.batch(prices)
|
||||
print("warmup_period =", t3.warmup_period())
|
||||
print("ready values:", np.count_nonzero(~np.isnan(out)))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 25
|
||||
ready values: 36
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const t3 = new ta.T3(5, 0.7);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + i);
|
||||
console.log('warmupPeriod:', t3.warmupPeriod());
|
||||
console.log('last:', t3.batch(prices).at(-1));
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`T3` is a "best of both" trend line — close to `Tema` in lag reduction but
|
||||
visibly smoother, because the six-EMA cascade filters noise the
|
||||
three-EMA `Tema` lets through. Use it as a single trend filter or as the
|
||||
slow leg of a crossover where you want a clean line. Raise `v` toward `1`
|
||||
for sharper turns (more overshoot), lower it toward `0` for maximum
|
||||
smoothness (`v = 0` is just a triple EMA).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Treating `v` as optional outside Python.** Only the Python binding
|
||||
defaults `v` to `0.7`; the Rust, Node and WASM constructors require it.
|
||||
- **Underestimating warmup.** `6·period − 5` grows fast — a `T3(20)` needs
|
||||
`115` bars before its first value.
|
||||
|
||||
## References
|
||||
|
||||
Tim Tillson, "Better Moving Averages", *Technical Analysis of Stocks &
|
||||
Commodities* (1998). The six-EMA expansion and coefficient formulas here
|
||||
match Tillson's published derivation and TA-Lib's `T3`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Tema.md](../moving-averages/Indicator-Tema.md) — the three-EMA relative.
|
||||
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — the two-EMA relative.
|
||||
- [Indicator-Zlema.md](../moving-averages/Indicator-Zlema.md) — low-lag average via de-lagging.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,207 +0,0 @@
|
||||
# TEMA
|
||||
|
||||
> Triple Exponential Moving Average — Mulloy's
|
||||
> `3·EMA1 − 3·EMA2 + EMA3` (where each EMA is fed from the previous one),
|
||||
> the second-order lag-reduction sibling of DEMA.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `3·period − 2` |
|
||||
| Interpretation | Even less lag than `Dema`, at the cost of more noise sensitivity. |
|
||||
|
||||
## Formula
|
||||
|
||||
Let `EMA1 = EMA(price, period)`, `EMA2 = EMA(EMA1, period)`,
|
||||
`EMA3 = EMA(EMA2, period)`. Then:
|
||||
|
||||
```
|
||||
TEMA_t = 3 * EMA1_t - 3 * EMA2_t + EMA3_t
|
||||
```
|
||||
|
||||
All three EMAs share the same `period`, hence the same
|
||||
`α = 2 / (period + 1)`. The coefficients `(3, −3, 1)` are the
|
||||
second-order finite-difference correction that removes both the
|
||||
first-order and second-order EMA lag terms — they come from expanding
|
||||
`(1 − L)^{-3}` where `L` is the lag operator.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Period shared by all three internal EMAs. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
(Python class `wickra.TEMA(period)` has no `#[pyo3(signature)]` default;
|
||||
pass `period` explicitly.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/tema.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Tema {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python `update` returns `float | None`, `batch` returns a 1-D
|
||||
`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns
|
||||
`number | null`, `batch` returns `Array<number>` with `NaN`
|
||||
placeholders.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Tema::new(period).warmup_period() == 3 * period - 2`. Each stacked EMA
|
||||
adds `period − 1` more inputs to the warmup count:
|
||||
|
||||
- `ema1` emits first at input `period`.
|
||||
- `ema2`, fed from `ema1`, emits first at input `period + (period − 1) = 2·period − 1`.
|
||||
- `ema3`, fed from `ema2`, emits first at input `(2·period − 1) + (period − 1) = 3·period − 2`.
|
||||
|
||||
For `Tema::new(14)` this gives `40` (matches the table in
|
||||
[Warmup Periods](../../Warmup-Periods.md)); for `Tema::new(5)` (the example
|
||||
below) it gives `13`. The implementation uses `?` short-circuit on every
|
||||
stage, so each inner EMA is only fed once the previous one emits.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Feeding `[42.0; n]` produces `Some(42.0)` once all
|
||||
three EMAs have converged: `3·42 − 3·42 + 42 = 42`. The unit test
|
||||
`constant_series_yields_constant_tema` pins this with `Tema::new(5)`
|
||||
over 80 constants.
|
||||
- **NaN / infinity inputs.** Inherited from the inner `Ema`: non-finite
|
||||
inputs are silently dropped at the `ema1` boundary and never reach the
|
||||
`3·EMA1 − 3·EMA2 + EMA3` arithmetic.
|
||||
- **Reset.** `tema.reset()` resets all three internal EMAs; the next
|
||||
`update` starts a full `3·period − 2` warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Tema};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut tema = Tema::new(5)?;
|
||||
let prices: Vec<f64> = (1..=20).map(f64::from).collect();
|
||||
let out: Vec<Option<f64>> = tema.batch(&prices);
|
||||
println!("warmup_period = {}", tema.warmup_period());
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 13
|
||||
[None, None, None, None, None, None, None, None, None, None, None, None, Some(13.0), Some(14.0), Some(15.000000000000002), Some(16.000000000000004), Some(17.000000000000007), Some(18.000000000000007), Some(19.000000000000007), Some(20.0)]
|
||||
```
|
||||
|
||||
The first `Some` lands at index 12 (the 13th input), matching
|
||||
`3·5 − 2 = 13`. On the linear ramp `1, 2, …, 20`, TEMA tracks the input
|
||||
ramp essentially exactly because both first- and second-order lag have
|
||||
been cancelled; the floating-point tail
|
||||
(`15.000000000000002`, `16.000000000000004`, …) is ordinary IEEE-754
|
||||
drift from the recursive subtractions.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
tema = ta.TEMA(5)
|
||||
out = tema.batch(np.arange(1.0, 21.0))
|
||||
print("warmup_period =", tema.warmup_period())
|
||||
print(out)
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 13
|
||||
[nan nan nan nan nan nan nan nan nan nan nan nan 13. 14. 15. 16. 17. 18.
|
||||
19. 20.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const tema = new ta.TEMA(5);
|
||||
const prices = Array.from({ length: 20 }, (_, i) => i + 1);
|
||||
console.log(tema.batch(prices));
|
||||
console.log('warmupPeriod:', tema.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
NaN, NaN,
|
||||
13, 14,
|
||||
15.000000000000002, 16.000000000000004,
|
||||
17.000000000000007, 18.000000000000007,
|
||||
19.000000000000007, 20
|
||||
]
|
||||
warmupPeriod: 13
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Tema` removes more lag than `Dema` and noticeably more than `Ema`.
|
||||
On a clean trending series the line stays glued to price; on a noisy
|
||||
or sideways series the same lag-cancellation amplifies the noise — TEMA
|
||||
overshoots and reverses faster than DEMA, and very much faster than EMA.
|
||||
|
||||
The signals are the same crossover patterns: price-vs-TEMA and
|
||||
fast-TEMA-vs-slow-TEMA. The `(3, −3, 1)` coefficient pattern is also
|
||||
what makes `Trix` (also in this family) work — `Trix` is the percentage
|
||||
change of `EMA3`, the triple-smoothed series.
|
||||
|
||||
Prefer `Tema` when `Dema` still feels too laggy and your data is clean
|
||||
enough to tolerate the extra noise sensitivity. Prefer `Hma` if you want
|
||||
a similar lag profile but with a built-in smoothing step (WMA chain
|
||||
instead of EMA chain), which behaves more gracefully on noisy data.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Forgetting the `3·period − 2` warmup.** `Tema::new(50)` will not
|
||||
emit until input 148. That is a significant chunk of any short-term
|
||||
backtest. If you are running a side-by-side panel of indicators with
|
||||
different warmups, filter rows on `~np.isnan(...)` (Python) /
|
||||
`is_some()` (Rust) per indicator rather than picking one global
|
||||
warmup cutoff.
|
||||
- **Using TEMA for noisy intraday data without a smoothing step.** The
|
||||
same lag-cancellation that makes TEMA attractive on clean data turns
|
||||
into whipsaws on tick-by-tick feeds. Either raise `period` materially
|
||||
or switch to `Hma`, which has a final WMA smoothing pass built in.
|
||||
|
||||
## References
|
||||
|
||||
Patrick G. Mulloy, *"Smoothing Data with Less Lag"*, **Technical Analysis
|
||||
of Stocks & Commodities**, February 1994 (TEMA). The coefficient pattern
|
||||
`(3, −3, 1)` for cancelling first- and second-order EMA lag is derived
|
||||
in the same article.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the building block.
|
||||
- [Indicator-Dema.md](../moving-averages/Indicator-Dema.md) — second-order's sibling.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — similar lag profile, built on WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,166 +0,0 @@
|
||||
# TRIMA
|
||||
|
||||
> Triangular Moving Average — a simple moving average applied twice, which
|
||||
> triangular-weights the window so the middle bars carry the most weight.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Very smooth price level; the triangular weighting suppresses edge bars. |
|
||||
|
||||
## Formula
|
||||
|
||||
`TRIMA(n)` is `SMA` stacked on `SMA`. For period `n` the two lengths are:
|
||||
|
||||
```
|
||||
odd n: n1 = n2 = (n + 1) / 2
|
||||
even n: n1 = n / 2, n2 = n / 2 + 1
|
||||
TRIMA_t = SMA_{n2}( SMA_{n1}(price) )_t
|
||||
```
|
||||
|
||||
Composing two equal-weight means convolves two rectangular windows, which
|
||||
yields a triangular weight profile over the original `n` closes — the
|
||||
centre bar gets the largest weight, the two edges the smallest. Both
|
||||
stacked SMAs are O(1), so `update` is O(1) regardless of `period`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Window length. `period = 0` errors with `Error::PeriodZero`. `period = 1` and `period = 2` degenerate to short SMAs. |
|
||||
|
||||
There is no Python `#[pyo3(signature = …)]` default for `TRIMA`, so
|
||||
`wickra.TRIMA(period)` requires the period explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/trima.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Trima {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Trima::new(period).warmup_period() == period`. The inner SMA emits after
|
||||
`n1` inputs; the outer SMA then needs `n2 − 1` more, and `n1 + n2 − 1 = n`
|
||||
for both the odd and even splits. So the first non-`None` output lands on
|
||||
exactly the `period`-th `update()`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** `[42.0; n]` returns `Some(42.0)` from input
|
||||
`period` onward — both SMAs are exact for constants
|
||||
(`constant_series_yields_the_constant` pins this).
|
||||
- **NaN / infinity inputs.** `update` returns `self.outer.value()` for a
|
||||
non-finite input *without* feeding either SMA, so the inner SMA's stale
|
||||
value is never double-counted into the outer SMA. State is left
|
||||
untouched.
|
||||
- **Reset.** `trima.reset()` resets both inner and outer SMAs, restarting
|
||||
the warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Trima};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut trima = Trima::new(5)?;
|
||||
let out: Vec<Option<f64>> = trima.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]);
|
||||
println!("{:?}", out);
|
||||
println!("warmup_period = {}", trima.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, None, Some(3.0), Some(4.0), Some(5.0)]
|
||||
warmup_period = 5
|
||||
```
|
||||
|
||||
`TRIMA(5)` is `SMA(3)` of `SMA(3)`. `SMA(3)` of `1..=7` is
|
||||
`[_, _, 2, 3, 4, 5, 6]`; `SMA(3)` of that is `[_, _, _, _, 3, 4, 5]`. This
|
||||
matches the `odd_period_reference_values` test in
|
||||
`crates/wickra-core/src/indicators/trima.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
trima = ta.TRIMA(5)
|
||||
print(trima.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0])))
|
||||
print("warmup_period =", trima.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan nan nan 3. 4. 5.]
|
||||
warmup_period = 5
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const trima = new ta.TRIMA(5);
|
||||
console.log(trima.batch([1, 2, 3, 4, 5, 6, 7]));
|
||||
console.log('warmupPeriod:', trima.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, NaN, 3, 4, 5 ]
|
||||
warmupPeriod: 5
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Trima` is one of the smoothest single-line averages in the library: the
|
||||
triangular weight profile damps the most recent bar far more than a plain
|
||||
`Sma` does, so whipsaws are rare. The cost is lag — a `Trima(n)` lags
|
||||
roughly like an `Sma(n/2)` doubled. Use it as a slow trend filter where a
|
||||
clean, low-noise line matters more than fast reaction; prefer
|
||||
[`Ema`](../moving-averages/Indicator-Ema.md) or [`Hma`](../moving-averages/Indicator-Hma.md) when responsiveness
|
||||
matters.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting `Sma`-like lag.** Stacking two means roughly doubles the
|
||||
effective lag; size the period accordingly.
|
||||
- **Treating `period = 0` as "use a default".** `Trima::new(0)` returns
|
||||
`Err(Error::PeriodZero)` in Rust and a `ValueError` in Python.
|
||||
|
||||
## References
|
||||
|
||||
The triangular moving average is a standard double-smoothed SMA; the
|
||||
odd/even split used here (`n1`, `n2`) matches TA-Lib's `TRIMA`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the building block applied twice.
|
||||
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear (not triangular) weights.
|
||||
- [Indicator-Smma.md](../moving-averages/Indicator-Smma.md) — the other F1 average.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,175 +0,0 @@
|
||||
# VWMA
|
||||
|
||||
> Volume-Weighted Moving Average — a rolling mean of closes where each bar
|
||||
> is weighted by its own traded volume.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `Candle` (uses `close` and `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Trend line that leans toward high-conviction (high-volume) bars. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
VWMA_t = Σ(close_i · volume_i) / Σ(volume_i) over the last `period` bars
|
||||
```
|
||||
|
||||
A heavy bar pulls the average toward its close; a thin bar barely moves
|
||||
it. Both the numerator (`Σ price·volume`) and denominator (`Σ volume`)
|
||||
are maintained as O(1) rolling sums, so `update` is O(1) regardless of
|
||||
`period`.
|
||||
|
||||
If **every** bar in the window has zero volume the weighted mean is
|
||||
undefined (`0 / 0`). VWMA then falls back to the plain unweighted mean of
|
||||
the `period` closes, so the output is always finite and defined.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Rolling window length in bars. `period = 0` errors with `Error::PeriodZero`. |
|
||||
|
||||
There is no Python `#[pyo3(signature = …)]` default for `VWMA`, so
|
||||
`wickra.VWMA(period)` requires the period explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/vwma.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Vwma {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`VWMA` is a **candle-input** indicator: it reads `close` and `volume` from
|
||||
each `Candle`. In Python the streaming `update` accepts a 6-tuple or a
|
||||
dict; the batch helper takes `close` and `volume` numpy arrays. Node and
|
||||
WASM expose `update(close, volume)` and `batch(close, volume)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Vwma::new(period).warmup_period() == period`. The first `period − 1`
|
||||
candles fill the rolling window; the `period`-th `update()` produces the
|
||||
first weighted mean.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant closes.** Closes all equal to `c` give `VWMA = c` regardless
|
||||
of the volumes (`Σ c·v / Σ v = c`), and the zero-volume fallback also
|
||||
yields `c` (`constant_series_yields_the_constant` pins this).
|
||||
- **Zero-volume window.** If every bar in the window has `volume = 0`,
|
||||
VWMA returns the unweighted mean of the `period` closes
|
||||
(`zero_volume_window_falls_back_to_unweighted_mean` pins this).
|
||||
- **Candle validation.** `Candle::new` already rejects NaN/infinite fields
|
||||
and negative volume, so `update` never sees an invalid bar — there is no
|
||||
separate non-finite guard.
|
||||
- **Reset.** `vwma.reset()` clears the window and all three rolling sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Candle, Indicator, Vwma};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut vwma = Vwma::new(2)?;
|
||||
// (close, volume): (10, 1) then (20, 3).
|
||||
let a = Candle::new(10.0, 10.0, 10.0, 10.0, 1.0, 0)?;
|
||||
let b = Candle::new(20.0, 20.0, 20.0, 20.0, 3.0, 1)?;
|
||||
println!("{:?}", vwma.update(a));
|
||||
println!("{:?}", vwma.update(b));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
None
|
||||
Some(17.5)
|
||||
```
|
||||
|
||||
The window holds two bars: `(10·1 + 20·3) / (1 + 3) = 70 / 4 = 17.5`. The
|
||||
heavier bar at `20` dominates, so the result sits well above the simple
|
||||
mean of `15`. This matches the `reference_value` test in
|
||||
`crates/wickra-core/src/indicators/vwma.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
vwma = ta.VWMA(2)
|
||||
close = np.array([10.0, 20.0, 30.0])
|
||||
volume = np.array([1.0, 3.0, 1.0])
|
||||
print(vwma.batch(close, volume))
|
||||
print("warmup_period =", vwma.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 17.5 22.5]
|
||||
warmup_period = 2
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const vwma = new ta.VWMA(2);
|
||||
console.log(vwma.batch([10, 20, 30], [1, 3, 1]));
|
||||
console.log('warmupPeriod:', vwma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 17.5, 22.5 ]
|
||||
warmupPeriod: 2
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Vwma` is a trend line that respects participation. Compared with an
|
||||
equal-weighted `Sma` of the same period, it reacts faster to moves backed
|
||||
by heavy volume and lags moves on thin volume. The classic read is the
|
||||
`Vwma`-vs-`Sma` relationship: `Vwma` above `Sma` means recent strength was
|
||||
volume-backed (more trustworthy); `Vwma` below `Sma` means the up-moves
|
||||
came on light volume. It is a session-independent cousin of
|
||||
[`Vwap`](../volume/Indicator-Vwap.md) — VWAP weights by volume since the
|
||||
start of the stream, VWMA over a fixed rolling window.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** `VWMA` needs volume; it takes a `Candle`,
|
||||
not an `f64`. Use `Sma`/`Wma` for a pure price series.
|
||||
- **Assuming a zero-volume window is an error.** It is not — VWMA falls
|
||||
back to the unweighted mean. If that fallback matters to you, screen the
|
||||
window's total volume yourself.
|
||||
|
||||
## References
|
||||
|
||||
The volume-weighted moving average is a standard volume-weighted rolling
|
||||
mean; the rolling-sum formulation here matches the common pandas
|
||||
implementation `(close*volume).rolling(n).sum() / volume.rolling(n).sum()`,
|
||||
with an explicit zero-volume fallback added for robustness.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the equal-weighted counterpart.
|
||||
- [Indicator-Vwap.md](../volume/Indicator-Vwap.md) — volume-weighted price
|
||||
since the start of the stream.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,185 +0,0 @@
|
||||
# WMA
|
||||
|
||||
> Weighted Moving Average with linear weights `1, 2, …, period`, so the
|
||||
> most recent bar carries the most weight.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Front-weighted trend filter; faster than `Sma`, smoother than `Ema`. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
weights = [1, 2, ..., n] // n = period
|
||||
W = n * (n + 1) / 2 // sum of weights
|
||||
WMA_t = (1 / W) * Σ_{i=0}^{n-1} (n - i) * price_{t-i}
|
||||
= (1 / W) * (n * price_t + (n-1) * price_{t-1} + ... + 1 * price_{t-n+1})
|
||||
```
|
||||
|
||||
Maintained in O(1) using the identity that, when sliding the window by
|
||||
one, every retained element's weight drops by exactly one and the
|
||||
newcomer enters at weight `n`:
|
||||
|
||||
```
|
||||
new_weight_sum = old_weight_sum - old_value_sum + n * new_input
|
||||
new_value_sum = old_value_sum - oldest_value + new_input
|
||||
```
|
||||
|
||||
This is the bookkeeping in the steady-state branch of `update`; during
|
||||
warmup the full `Σ weight·value` is computed once when the window first
|
||||
fills.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | Length of the rolling window. `period = 0` errors with `Error::PeriodZero`. `period = 1` is a pass-through. |
|
||||
|
||||
(The Python class `wickra.WMA(period)` does not set a `#[pyo3(signature)]`
|
||||
default; pass the period explicitly.)
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/wma.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Wma {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
Python returns `float | None` from `update` and a `numpy.ndarray`
|
||||
(`float64`, `NaN` for warmup) from `batch`. Node returns `number | null`
|
||||
and `Array<number>` (with `NaN` placeholders) respectively.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Wma::new(period).warmup_period() == period`. Like `Sma`, the first
|
||||
emission lands on the `period`-th `update()` call: the window needs
|
||||
exactly `period` values for the weighted sum to be defined. There is no
|
||||
seeding step beyond filling the window.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** For `[c; n]`, every element contributes `c · weight_i`
|
||||
and the result is `c · ΣW / ΣW = c`. The proptest
|
||||
`proptest_matches_naive` exercises this implicitly across many random
|
||||
inputs; the textbook `period = 4` test confirms `WMA(4)` of
|
||||
`[1, 2, 3, 4]` is exactly `(1·1 + 2·2 + 3·3 + 4·4) / 10 = 30 / 10 = 3.0`.
|
||||
- **NaN / infinity inputs.** The first line of `update` is
|
||||
`if !input.is_finite() { return self.value(); }`. Non-finite inputs are
|
||||
silently dropped — they do not advance warmup, do not corrupt the
|
||||
rolling sums, and the previously emitted value (if any) is returned.
|
||||
- **Reset.** `wma.reset()` clears the window and both rolling sums; the
|
||||
next `update` starts a new warmup countdown.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Wma};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut wma = Wma::new(4)?;
|
||||
let out: Vec<Option<f64>> = wma.batch(&[1.0, 2.0, 3.0, 4.0]);
|
||||
println!("{:?}", out);
|
||||
println!("warmup_period = {}", wma.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, Some(3.0)]
|
||||
warmup_period = 4
|
||||
```
|
||||
|
||||
The fourth input emits `(1·1 + 2·2 + 3·3 + 4·4) / (1+2+3+4) = 30 / 10 = 3.0`.
|
||||
This matches the `known_values_period_4` unit test in
|
||||
`crates/wickra-core/src/indicators/wma.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
wma = ta.WMA(4)
|
||||
print(wma.batch(np.array([1.0, 2.0, 3.0, 4.0])))
|
||||
print("warmup_period =", wma.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan nan 3.]
|
||||
warmup_period = 4
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const wma = new ta.WMA(4);
|
||||
console.log(wma.batch([1, 2, 3, 4]));
|
||||
console.log('warmupPeriod:', wma.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, 3 ]
|
||||
warmupPeriod: 4
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Wma` sits between `Sma` and `Ema` on the lag/responsiveness spectrum:
|
||||
because the most recent bar carries weight `n` (vs `1` for the oldest),
|
||||
direction changes propagate faster than in `Sma`, but the smooth linear
|
||||
decay produces less of the "exponential tail" overshoot you sometimes
|
||||
see with `Ema`. The same two crossover signals (price-vs-WMA and
|
||||
fast-WMA-vs-slow-WMA) apply.
|
||||
|
||||
The most important downstream use of `Wma` inside Wickra is `Hma`:
|
||||
`Hma` is built entirely from three `Wma` instances (see
|
||||
[Indicator-Hma.md](../moving-averages/Indicator-Hma.md)).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Mistaking linear weights for exponential ones.** A `Wma(20)` is *not*
|
||||
an `Ema(20)`; the weights decay linearly `(20, 19, 18, …, 1)` rather
|
||||
than geometrically, so very old bars still contribute (weight 1) where
|
||||
in an EMA they would have decayed to near zero. If you want the
|
||||
exponential decay, use `Ema`.
|
||||
- **Comparing `Wma(period)` to a "WMA" from a different library and
|
||||
finding the seed off.** Wickra's `Wma` has no separate seeding step —
|
||||
it simply returns `None` until the window is full and then returns the
|
||||
exact weighted mean from input `period` onward. Some libraries
|
||||
pre-seed with a partial-window value; that is a different convention
|
||||
and will produce different first-few-bar values.
|
||||
|
||||
## References
|
||||
|
||||
The linearly-weighted moving average is older than most named indicators
|
||||
and has no single canonical citation; TA-Lib's `WMA` is the standard
|
||||
reference implementation and matches Wickra's output bit-for-bit.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — equal weights instead of linear.
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — exponential decay instead of linear.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — Hull MA, built from three WMAs.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,166 +0,0 @@
|
||||
# ZLEMA
|
||||
|
||||
> Zero-Lag Exponential Moving Average — an EMA fed a de-lagged price series
|
||||
> so it tracks turns with almost no group delay.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Moving Averages |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; tracks the input price scale |
|
||||
| Default parameters | `period` is required (no default in either binding) |
|
||||
| Warmup period | `lag + period` where `lag = (period − 1) / 2` |
|
||||
| Interpretation | Low-lag trend line; crossings of price react far sooner than a plain EMA. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
lag = (period − 1) / 2 (integer division)
|
||||
de_lagged_t = 2·price_t − price_{t−lag}
|
||||
ZLEMA_t = EMA_period(de_lagged)_t
|
||||
```
|
||||
|
||||
The trick (Ehlers & Way, 2010): `price_t − price_{t−lag}` is a momentum
|
||||
term. Adding it to the current price *over-shoots* in the direction of the
|
||||
recent move by exactly enough to cancel the EMA's lag. The inner EMA then
|
||||
smooths that de-lagged series with the usual `α = 2 / (period + 1)`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------|-------------|-------------|
|
||||
| `period` | `usize` | none | `>= 1` | EMA length. `period = 0` errors with `Error::PeriodZero`. The lag offset is derived as `(period − 1) / 2`. |
|
||||
|
||||
There is no Python `#[pyo3(signature = …)]` default for `ZLEMA`, so
|
||||
`wickra.ZLEMA(period)` requires the period explicitly. The derived `lag`
|
||||
is exposed as a read-only property.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/zlema.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Zlema {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Zlema::new(period).warmup_period() == lag + period`. The de-lagged series
|
||||
is undefined until `lag` prior inputs exist, so it produces its first
|
||||
value on input `lag + 1`; the inner EMA then needs `period` de-lagged
|
||||
values to seed. The first non-`None` output therefore lands on input
|
||||
`lag + period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** De-lagging a constant gives the same constant
|
||||
(`2c − c = c`), so `ZLEMA` of a flat series is flat
|
||||
(`constant_series_yields_the_constant` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped: the
|
||||
rolling lag buffer is not advanced and the inner EMA is not fed, so the
|
||||
previous valid value (if any) is returned.
|
||||
- **`period = 1`.** `lag = 0`, the de-lagged series equals the raw price,
|
||||
and `ZLEMA(1)` degenerates to a pass-through.
|
||||
- **Reset.** `zlema.reset()` clears the lag buffer and the inner EMA.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Zlema};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut zlema = Zlema::new(3)?;
|
||||
let out: Vec<Option<f64>> = zlema.batch(&[1.0, 2.0, 3.0, 4.0, 5.0]);
|
||||
println!("{:?}", out);
|
||||
println!("lag = {}, warmup_period = {}", zlema.lag(), zlema.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, Some(4.0), Some(5.0)]
|
||||
lag = 1, warmup_period = 4
|
||||
```
|
||||
|
||||
`ZLEMA(3)` has `lag = 1`. The de-lagged series of `[1,2,3,4,5]` is
|
||||
`[_, 3, 4, 5, 6]`; `EMA(3)` of that seeds at `mean(3,4,5) = 4.0`, then
|
||||
`0.5·6 + 0.5·4 = 5.0`. This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/zlema.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
zlema = ta.ZLEMA(3)
|
||||
print(zlema.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0])))
|
||||
print("lag =", zlema.lag, "warmup_period =", zlema.warmup_period())
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan nan 4. 5.]
|
||||
lag = 1 warmup_period = 4
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const zlema = new ta.ZLEMA(3);
|
||||
console.log(zlema.batch([1, 2, 3, 4, 5]));
|
||||
console.log('warmupPeriod:', zlema.warmupPeriod());
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, 4, 5 ]
|
||||
warmupPeriod: 4
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Zlema` is a low-lag trend line. Use it where an `Ema` would lag too much
|
||||
into a reversal — for example as the fast leg of a crossover system, or
|
||||
as a trailing reference that should react quickly. The momentum injection
|
||||
that removes the lag also makes `Zlema` overshoot on sharp spikes, so it
|
||||
is noisier than the `Ema` it is built on; pair it with a slower filter if
|
||||
whipsaws are a concern.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting `Ema`-identical values.** `Zlema` is deliberately *not* an
|
||||
`Ema` — it leads price. The two only coincide for `period = 1`.
|
||||
- **Forgetting the extra warmup.** Warmup is `lag + period`, not `period`;
|
||||
budget `(period − 1) / 2` extra bars before the first output.
|
||||
|
||||
## References
|
||||
|
||||
John Ehlers and Ric Way, "Zero Lag (Well, Almost)", *Technical Analysis
|
||||
of Stocks & Commodities* (2010). The implementation here uses the standard
|
||||
`lag = (period − 1) / 2` and an SMA-seeded inner EMA.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the inner average ZLEMA de-lags.
|
||||
- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — another low-lag average, via WMAs.
|
||||
- [Indicator-T3.md](../moving-averages/Indicator-T3.md) — low-lag average via a six-EMA cascade.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,144 +0,0 @@
|
||||
# AcceleratorOscillator
|
||||
|
||||
> Accelerator Oscillator (AC) — Bill Williams' measure of how fast
|
||||
> momentum itself is changing.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Oscillators |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `ao_fast = 5`, `ao_slow = 34`, `signal_period = 5` (Python) |
|
||||
| Warmup period | `ao_slow + signal_period − 1` |
|
||||
| Interpretation | Acceleration of momentum; zero-line crossings lead the Awesome Oscillator. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
AO = SMA(median, ao_fast) − SMA(median, ao_slow) (the Awesome Oscillator)
|
||||
AC = AO − SMA(AO, signal_period)
|
||||
```
|
||||
|
||||
Where the [`AwesomeOscillator`](../momentum-oscillators/Indicator-AwesomeOscillator.md) measures
|
||||
momentum, the Accelerator measures the *change* in momentum — it is the AO
|
||||
minus a short moving average of itself. Because acceleration leads speed, the
|
||||
`AC` tends to turn before the `AO` does. Bill Williams' classic configuration
|
||||
is the `(5, 34)` AO with a `5`-period signal average.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `ao_fast`, `ao_slow` — the underlying Awesome Oscillator periods (`5`, `34`).
|
||||
- `signal_period` — the moving average of the AO subtracted from it (`5`).
|
||||
|
||||
`AcceleratorOscillator::classic()` returns the `(5, 34, 5)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/accelerator_oscillator.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for AcceleratorOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
It is a **candle-input** indicator — the inner Awesome Oscillator reads the
|
||||
median price `(high + low) / 2`. Python's streaming `update` accepts a 6-tuple
|
||||
or a dict; the batch helper takes `high`, `low` numpy arrays. Node and WASM
|
||||
expose `update(high, low)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`AcceleratorOscillator::classic().warmup_period() == 38`. The AO first emits at
|
||||
candle `ao_slow`; the signal average then needs `signal_period` AO values.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A flat series gives `AO = 0`, so `AC = 0` throughout.
|
||||
- **`ao_fast >= ao_slow`.** Rejected at construction.
|
||||
- **Reset.** `ac.reset()` clears the AO and the signal average.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, AcceleratorOscillator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ac = AcceleratorOscillator::classic();
|
||||
let candles: Vec<Candle> = (0..60)
|
||||
.map(|i| Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, i).unwrap())
|
||||
.collect();
|
||||
println!("{:?}", ac.batch(&candles).last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.0)
|
||||
```
|
||||
|
||||
A flat market produces a flat AO and therefore a zero Accelerator.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ac = ta.AcceleratorOscillator(5, 34, 5)
|
||||
n = 60
|
||||
print(ac.batch(np.full(n, 11.0), np.full(n, 9.0))[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ac = new ta.AcceleratorOscillator(5, 34, 5);
|
||||
const out = ac.batch(Array(60).fill(11), Array(60).fill(9));
|
||||
console.log(out[out.length - 1]);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Trade the Accelerator like a momentum-acceleration gauge: bars rising above
|
||||
the zero line mean momentum is building, bars falling below mean it is fading.
|
||||
Because it leads the Awesome Oscillator, a colour change in the AC is an early
|
||||
warning that the AO — and price momentum — is about to turn.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading the level.** Only the sign and the slope matter; the magnitude
|
||||
scales with the instrument.
|
||||
- **Feeding it scalar prices.** It needs the `high`/`low` bar.
|
||||
|
||||
## References
|
||||
|
||||
Bill Williams' Accelerator Oscillator, from *Trading Chaos*.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-AwesomeOscillator.md](../momentum-oscillators/Indicator-AwesomeOscillator.md) — the
|
||||
momentum oscillator the Accelerator is built on.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,138 +0,0 @@
|
||||
# BalanceOfPower
|
||||
|
||||
> Balance of Power (BOP) — where the bar closed within its range relative
|
||||
> to where it opened.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Oscillators |
|
||||
| Input type | `Candle` (uses `open`, `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−1, +1]` |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | Intrabar buyer/seller control; `+1` buyers, `−1` sellers. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
BOP = (close − open) / (high − low)
|
||||
```
|
||||
|
||||
Balance of Power asks a single question per bar: did buyers or sellers win it?
|
||||
A bar that opened on its low and closed on its high scores `+1` (buyers in
|
||||
total control); the mirror image scores `−1`. It is a stateless per-bar
|
||||
reading. A zero-range bar carries no information and yields `0`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`BalanceOfPower` takes **no parameters** — `BalanceOfPower::new()` in Rust,
|
||||
`wickra.BalanceOfPower()` in Python, `new ta.BalanceOfPower()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/balance_of_power.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for BalanceOfPower {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`BalanceOfPower` is a **candle-input** indicator that reads all four of
|
||||
`open`, `high`, `low`, `close`. Python's streaming `update` accepts a 6-tuple
|
||||
or a dict; the batch helper takes `open`, `high`, `low`, `close` numpy arrays.
|
||||
Node and WASM expose `update(open, high, low, close)` and the matching
|
||||
`batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`BalanceOfPower::new().warmup_period() == 1`. It is a stateless per-bar
|
||||
transform — it emits a value from the very first candle.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero-range bar.** `high == low` yields `0` instead of dividing by zero.
|
||||
- **Close on high, open on low.** Scores exactly `+1`.
|
||||
- **Reset.** `bop.reset()` only clears the `is_ready` flag.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Candle, Indicator, BalanceOfPower};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut bop = BalanceOfPower::new();
|
||||
// open 10, high 14, low 10, close 12 -> (12 - 10) / (14 - 10) = 0.5.
|
||||
let v = bop.update(Candle::new(10.0, 14.0, 10.0, 12.0, 1.0, 0)?);
|
||||
println!("{:?}", v);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.5)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
bop = ta.BalanceOfPower()
|
||||
print(bop.batch(
|
||||
np.array([10.0]), np.array([14.0]), np.array([10.0]), np.array([12.0])
|
||||
))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[0.5]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const bop = new ta.BalanceOfPower();
|
||||
console.log(bop.batch([10], [14], [10], [12]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 0.5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
A BOP holding above zero says buyers are consistently winning the bars — a
|
||||
healthy uptrend; below zero is the seller's mirror. Because the raw per-bar
|
||||
value is noisy, it is commonly smoothed with a short moving average before
|
||||
trading the zero-line crossings, or read for divergence against price.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Using the raw value as a trend signal.** Per-bar BOP whipsaws; smooth it.
|
||||
- **Feeding it scalar prices.** It needs the full OHLC bar — including `open`.
|
||||
|
||||
## References
|
||||
|
||||
Balance of Power, popularised by Igor Livshin; the `(close − open) /
|
||||
(high − low)` definition is the standard one.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-AwesomeOscillator.md](../momentum-oscillators/Indicator-AwesomeOscillator.md) — another
|
||||
Bill Williams-era price oscillator.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,153 +0,0 @@
|
||||
# Coppock
|
||||
|
||||
> Coppock Curve — a long-horizon momentum indicator: a weighted moving
|
||||
> average of two rates of change, designed to flag major bottoms.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `(roc_long = 14, roc_short = 11, wma_period = 10)` (Python) |
|
||||
| Warmup period | `max(roc_long, roc_short) + wma_period` |
|
||||
| Interpretation | Long-term momentum; an upturn from below zero is the buy signal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
Coppock = WMA( ROC(roc_long) + ROC(roc_short), wma_period )
|
||||
```
|
||||
|
||||
Edwin Coppock built this in 1962 as a long-horizon buy signal for stock
|
||||
indices. The two rates of change blend a slightly longer and a slightly
|
||||
shorter momentum horizon; the [`Wma`](../moving-averages/Indicator-Wma.md) smooths
|
||||
their sum. On a **monthly** chart with the conventional
|
||||
`(14, 11, 10)` settings, the curve turning *up from below zero* has
|
||||
historically marked the start of a new bull phase.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------------|---------|---------------|-------------|-------------|
|
||||
| `roc_long` | `usize` | `14` (Python) | `>= 1` | Longer ROC period. `0` errors with `Error::PeriodZero`. |
|
||||
| `roc_short` | `usize` | `11` (Python) | `>= 1` | Shorter ROC period. |
|
||||
| `wma_period` | `usize` | `10` (Python) | `>= 1` | WMA smoothing length. |
|
||||
|
||||
The Python binding defaults the trio to `(14, 11, 10)`. The `periods`
|
||||
property returns `(roc_long, roc_short, wma_period)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/coppock.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Coppock {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == max(roc_long, roc_short) + wma_period`. Each ROC emits
|
||||
its first value at input `roc_period + 1`; the longer ROC is the last to
|
||||
become ready, and the WMA then needs `wma_period` of the summed ROC
|
||||
values — so the first non-`None` output lands on input
|
||||
`max(roc_long, roc_short) + wma_period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Both ROCs are `0` on a flat series, so the WMA of
|
||||
zeros — and the curve — is `0` (`constant_series_yields_zero` pins
|
||||
this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; no
|
||||
component is advanced.
|
||||
- **Reset.** `coppock.reset()` clears both ROCs and the WMA.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Coppock};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut coppock = Coppock::new(14, 11, 10)?;
|
||||
let prices: Vec<f64> = (1..=120).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
|
||||
let out = coppock.batch(&prices);
|
||||
println!("warmup_period = {}", coppock.warmup_period());
|
||||
println!("last > 0: {}", out.last().unwrap().unwrap() > 0.0);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 24
|
||||
last > 0: true
|
||||
```
|
||||
|
||||
A steady uptrend keeps both ROCs positive, so the Coppock Curve stays
|
||||
above zero.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
coppock = ta.Coppock() # (roc_long=14, roc_short=11, wma_period=10)
|
||||
prices = np.full(60, 100.0) # flat series
|
||||
print(coppock.batch(prices)[-1]) # ROCs are 0 -> 0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const coppock = new ta.Coppock(14, 11, 10);
|
||||
const prices = Array.from({ length: 120 }, (_, i) => 100 * 1.01 ** i);
|
||||
console.log('warmupPeriod:', coppock.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Coppock` is a long-horizon signal, traditionally read on **monthly**
|
||||
data. The canonical rule is a single one: when the curve has been below
|
||||
zero and turns up, that is a long-term buy. It was not designed to give
|
||||
sell signals — Coppock left exits to other tools. On faster timeframes it
|
||||
behaves as a smoothed momentum oscillator, but its statistical edge is
|
||||
specifically the monthly bottom call.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Using it for sell signals.** The Coppock Curve is a buy-only
|
||||
indicator by design; pair it with a separate exit rule.
|
||||
- **Applying it intraday and expecting the historical edge.** The
|
||||
documented behaviour is for monthly index charts.
|
||||
|
||||
## References
|
||||
|
||||
E. S. Coppock, "Practical Relative Strength Charting", *Barron's* (1962).
|
||||
The `WMA(ROC(14) + ROC(11), 10)` construction here is Coppock's original.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Roc.md](../momentum-oscillators/Indicator-Roc.md) — the rate-of-change building block.
|
||||
- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — the smoothing average.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,161 +0,0 @@
|
||||
# DPO
|
||||
|
||||
> Detrended Price Oscillator — removes the trend from price by comparing a
|
||||
> shifted past price to the moving average, exposing the underlying cycle.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (price-difference scale) |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `max(period, period / 2 + 2)` |
|
||||
| Interpretation | Detrended price; peak-to-peak spacing reveals the cycle length. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
shift = period / 2 + 1
|
||||
DPO_t = price_{t − shift} − SMA(period)_t
|
||||
```
|
||||
|
||||
A normal oscillator compares price to a *current* average and therefore
|
||||
still carries the trend. DPO instead subtracts the average from a price
|
||||
taken `period / 2 + 1` bars **back** — roughly half a cycle. The dominant
|
||||
trend cancels, and what is left swings around zero with the same period
|
||||
as the price's shorter cycles, so the distance between DPO peaks reads off
|
||||
the cycle length directly.
|
||||
|
||||
DPO is **not** a momentum or signal indicator: by construction it is
|
||||
shifted into the past and is not meant to track the latest bar.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` (Python) | `>= 1` | SMA length; also sets the look-back `shift = period / 2 + 1`. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `20`. The derived `shift` is
|
||||
exposed as a read-only property.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/dpo.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Dpo {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == max(period, period / 2 + 2)`. The output needs both a
|
||||
full `period`-bar SMA window and a price `shift` bars back; the indicator
|
||||
becomes ready once the rolling window holds enough bars for both. For the
|
||||
usual `period >= 4` this simplifies to `period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** On a flat series the shifted price equals the SMA,
|
||||
so DPO is `0` (`constant_series_yields_zero` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
window is not advanced.
|
||||
- **Reset.** `dpo.reset()` clears the window and the rolling sum.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Dpo};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut dpo = Dpo::new(4)?;
|
||||
let out: Vec<Option<f64>> = dpo.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
println!("{:?}", out);
|
||||
println!("shift = {}, warmup_period = {}", dpo.shift(), dpo.warmup_period());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, Some(-1.5), Some(-1.5), Some(-1.5)]
|
||||
shift = 3, warmup_period = 4
|
||||
```
|
||||
|
||||
`DPO(4)` has `shift = 3`. At input 4 the SMA of `[1,2,3,4]` is `2.5` and
|
||||
the price 3 bars back is `1`, giving `1 − 2.5 = −1.5`. On a pure ramp the
|
||||
detrended value is constant. This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/dpo.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
dpo = ta.DPO(4)
|
||||
print(dpo.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan nan -1.5 -1.5 -1.5]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const dpo = new ta.DPO(4);
|
||||
console.log(dpo.batch([1, 2, 3, 4, 5, 6]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, -1.5, -1.5, -1.5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Dpo` is a cycle-measurement tool, not a trading trigger. Read it for the
|
||||
*spacing* of its peaks and troughs: regular spacing reveals the dominant
|
||||
cycle length, which you can then feed back into the periods of other
|
||||
indicators. Crossing zero is not a signal — because the series is shifted
|
||||
into the past, the latest DPO value does not correspond to the latest bar.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Trading the zero cross.** DPO is detrended *and* time-shifted; its
|
||||
latest value is historical. Use it to size cycles, not to time entries.
|
||||
- **Reading it as momentum.** It is a detrended price, not a rate of
|
||||
change — see [`Roc`](../momentum-oscillators/Indicator-Roc.md) or [`Mom`](../momentum-oscillators/Indicator-Mom.md) for
|
||||
momentum.
|
||||
|
||||
## References
|
||||
|
||||
The Detrended Price Oscillator is a standard cycle-analysis study; the
|
||||
`period / 2 + 1` look-back shift used here matches the common definition
|
||||
(StockCharts, TA-Lib-compatible implementations).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the moving average DPO
|
||||
detrends against.
|
||||
- [Indicator-Roc.md](../momentum-oscillators/Indicator-Roc.md) — momentum, the indicator DPO is
|
||||
often confused with.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,154 +0,0 @@
|
||||
# PPO
|
||||
|
||||
> Percentage Price Oscillator — MACD expressed as a percentage of the slow
|
||||
> EMA, so readings are comparable across instruments.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Oscillators |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (percent) |
|
||||
| Default parameters | `(fast = 12, slow = 26)` (Python) |
|
||||
| Warmup period | `slow` |
|
||||
| Interpretation | Percentage gap between a fast and slow EMA; zero-line crosses are signals. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
PPO = 100 · (EMA_fast − EMA_slow) / EMA_slow
|
||||
```
|
||||
|
||||
PPO is [`MacdIndicator`](../trend-directional/Indicator-MacdIndicator.md) divided by the slow
|
||||
EMA. That single change makes it **scale-free**: a `PPO` of `1.5` always
|
||||
means "the fast EMA is 1.5 % above the slow EMA", whether the instrument
|
||||
trades at $5 or $5000 — so PPO values can be compared across assets and
|
||||
across time, which raw MACD values cannot. The classic PPO **signal
|
||||
line** is a 9-period EMA of this PPO line; compose it with
|
||||
[`Chain`](../../Indicator-Chaining.md) and an `Ema(9)`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------|---------|---------------|------------------|-------------|
|
||||
| `fast` | `usize` | `12` (Python) | `>= 1`, `< slow` | Fast EMA period. |
|
||||
| `slow` | `usize` | `26` (Python) | `> fast` | Slow EMA period. |
|
||||
|
||||
`fast` must be strictly less than `slow` — otherwise `new` returns
|
||||
`Error::InvalidPeriod`. A zero period returns `Error::PeriodZero`. The
|
||||
Python binding defaults the pair to `(12, 26)`; the `periods` property
|
||||
returns `(fast, slow)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ppo.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Ppo {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`Ppo::new(fast, slow).warmup_period() == slow`. Both EMAs are SMA-seeded;
|
||||
the slow EMA is the last to seed, at input `slow`, which is also when PPO
|
||||
emits its first value.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Both EMAs converge to the constant, so their gap —
|
||||
and PPO — is `0` (`constant_series_yields_zero` pins this).
|
||||
- **Zero slow EMA.** A `0.0` slow EMA would divide by zero; PPO reports
|
||||
`0.0` for that bar instead.
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
EMAs are not advanced.
|
||||
- **Reset.** `ppo.reset()` clears both EMAs and the cached value.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Ppo};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ppo = Ppo::new(12, 26)?;
|
||||
let prices: Vec<f64> = (1..=80).map(f64::from).collect();
|
||||
let out = ppo.batch(&prices);
|
||||
println!("warmup_period = {}", ppo.warmup_period());
|
||||
println!("last > 0: {}", out.last().unwrap().unwrap() > 0.0);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 26
|
||||
last > 0: true
|
||||
```
|
||||
|
||||
In a rising series the fast EMA leads the slow EMA, so PPO is positive.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ppo = ta.PPO() # (fast=12, slow=26)
|
||||
prices = np.full(60, 100.0) # flat series
|
||||
print(ppo.batch(prices)[-1]) # both EMAs equal -> 0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ppo = new ta.PPO(12, 26);
|
||||
const prices = Array.from({ length: 80 }, (_, i) => 100 + i);
|
||||
console.log('warmupPeriod:', ppo.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Ppo` is read exactly like MACD: the zero-line cross (fast EMA crossing
|
||||
the slow EMA), the signal-line cross (PPO crossing its own 9-EMA), and
|
||||
histogram-style divergence. Its advantage over MACD is comparability — a
|
||||
PPO scan across a watchlist ranks instruments by *relative* trend
|
||||
strength, which a MACD scan cannot do because MACD is in each
|
||||
instrument's own price units.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting a bundled signal line.** `Ppo` here is the single PPO line;
|
||||
add `Ema(9)` via `Chain` for the signal line and histogram.
|
||||
- **`fast >= slow`.** The constructor rejects it — the fast EMA must be
|
||||
the faster one.
|
||||
|
||||
## References
|
||||
|
||||
Gerald Appel's MACD, re-expressed as a percentage. The implementation
|
||||
follows the standard PPO definition and matches TA-Lib's `PPO`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-MacdIndicator.md](../trend-directional/Indicator-MacdIndicator.md) — the price-unit
|
||||
original, with a bundled signal line and histogram.
|
||||
- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the underlying average.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,143 +0,0 @@
|
||||
# LinRegAngle
|
||||
|
||||
> Linear Regression Angle — the slope of the rolling least-squares fit,
|
||||
> expressed as an angle in degrees.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `f64` (price) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `(−90°, +90°)` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Steepness of the trend; sign is direction, magnitude is pitch. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
LinRegAngle = atan(LinRegSlope) · 180 / π
|
||||
```
|
||||
|
||||
The angle carries exactly the same information as
|
||||
[`LinRegSlope`](../price-statistics/Indicator-LinRegSlope.md) — positive while price trends up,
|
||||
negative while it trends down — but maps the unbounded slope through `atan`
|
||||
onto `(−90°, +90°)`. That bounded, price-unit-free scale makes "how steep is
|
||||
the trend" comparable at a glance and across instruments. This is TA-Lib's
|
||||
`LINEARREG_ANGLE`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the regression window. Must be at least `2` (a line needs two
|
||||
points). The Python binding defaults it to `14`; the Rust and Node
|
||||
constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/linreg_angle.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for LinRegAngle {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`LinRegAngle` is a **scalar** indicator: it consumes one `f64` price per step.
|
||||
Because `Input = f64` it can sit inside a [`Chain`](../../Indicator-Chaining.md).
|
||||
|
||||
## Warmup
|
||||
|
||||
`LinRegAngle::new(14).warmup_period() == 14`. The first value lands once the
|
||||
window holds a full `period` prices.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **`period < 2`.** Rejected at construction — a regression line is undefined
|
||||
for fewer than two points.
|
||||
- **Unit slope.** A series rising by exactly `1` per step has slope `1`, and
|
||||
`atan(1) = 45°`.
|
||||
- **Flat series.** A constant input has slope `0` and therefore angle `0`.
|
||||
- **Reset.** `angle.reset()` clears the rolling regression window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, LinRegAngle};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut angle = LinRegAngle::new(5)?;
|
||||
// Closes rising by 1 per step -> slope 1 -> atan(1) = 45 degrees.
|
||||
let out = angle.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, None, Some(45.0), Some(45.0)]
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
angle = ta.LinRegAngle(5)
|
||||
print(angle.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan nan nan 45. 45.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const angle = new ta.LinRegAngle(5);
|
||||
console.log(angle.batch([1, 2, 3, 4, 5, 6]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, NaN, 45, 45 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The angle is read like a slope: sign gives trend direction, magnitude gives
|
||||
how steeply price is pitched. Because it is bounded to `±90°` it is convenient
|
||||
for thresholds — e.g. "only trade with the trend while the angle exceeds
|
||||
`30°`" — and for comparing trend pitch across instruments with different price
|
||||
scales, which the raw [`LinRegSlope`](../price-statistics/Indicator-LinRegSlope.md) cannot do.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading degrees as a price quantity.** The angle depends on the chart's
|
||||
implicit scaling; treat it as a relative steepness gauge, not an absolute.
|
||||
- **Tiny periods.** `period = 2` reduces the fit to the last difference.
|
||||
|
||||
## References
|
||||
|
||||
The angle of an ordinary least-squares fit to a rolling price window; matches
|
||||
TA-Lib's `LINEARREG_ANGLE`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-LinRegSlope.md](../price-statistics/Indicator-LinRegSlope.md) — the same fit's slope,
|
||||
in raw price-per-bar units.
|
||||
- [Indicator-LinearRegression.md](../price-statistics/Indicator-LinearRegression.md) — the
|
||||
endpoint of the same rolling fit.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,149 +0,0 @@
|
||||
# LinRegSlope
|
||||
|
||||
> Linear Regression Slope — the slope of a rolling ordinary-least-squares
|
||||
> fit over the last `period` prices.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `f64` (price) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (price units per bar) |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | How steeply price trends; positive up, negative down, zero flat. |
|
||||
|
||||
## Formula
|
||||
|
||||
Over the last `period` inputs, indexed `x = 0, 1, …, period − 1`:
|
||||
|
||||
```
|
||||
b = (n·Σxy − Σx·Σy) / (n·Σxx − (Σx)²)
|
||||
```
|
||||
|
||||
`LinRegSlope` fits a straight line to the window by ordinary least squares —
|
||||
the same fit as [`LinearRegression`](../price-statistics/Indicator-LinearRegression.md) — but
|
||||
reports the *slope* `b` instead of the endpoint. The slope is in price units
|
||||
per bar: positive while price trends up, negative while it trends down, near
|
||||
zero when it is ranging. This is TA-Lib's `LINEARREG_SLOPE`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the regression window. Must be at least `2` (a line needs two
|
||||
points). The Python binding defaults it to `14`; the Rust and Node
|
||||
constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/linreg_slope.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for LinRegSlope {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`LinRegSlope` is a **scalar** indicator: it consumes one `f64` price per step.
|
||||
Because `Input = f64` it can sit inside a [`Chain`](../../Indicator-Chaining.md).
|
||||
|
||||
## Warmup
|
||||
|
||||
`LinRegSlope::new(14).warmup_period() == 14`. The first value lands once the
|
||||
window holds a full `period` prices — on input index `period − 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **`period < 2`.** Rejected at construction — a regression line is undefined
|
||||
for fewer than two points.
|
||||
- **Perfect line.** Fed a series rising by a fixed step, the slope is exactly
|
||||
that step (`perfect_line_returns_its_step` pins this).
|
||||
- **Constant series.** A flat input returns a slope of `0`.
|
||||
- **Falling series.** A descending input returns a negative slope.
|
||||
- **Reset.** `ls.reset()` clears the rolling window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, LinRegSlope};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ls = LinRegSlope::new(3)?;
|
||||
// Fit over [1, 2, 9]: the least-squares line is y = 4x, slope 4.
|
||||
let out = ls.batch(&[1.0, 2.0, 9.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(4.0)]
|
||||
```
|
||||
|
||||
This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/linreg_slope.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ls = ta.LinRegSlope(3)
|
||||
print(ls.batch(np.array([1.0, 2.0, 9.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan 4.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ls = new ta.LinRegSlope(3);
|
||||
console.log(ls.batch([1, 2, 9]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 4 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`LinRegSlope` is a momentum gauge: its sign is the trend direction and its
|
||||
magnitude is the trend's steepness in price-per-bar. A slope crossing zero
|
||||
marks a trend change; a slope that flattens while price still rises warns the
|
||||
trend is losing pace. Unlike a difference-based oscillator it uses every bar
|
||||
in the window, so it is less jumpy.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Comparing slopes across instruments.** The slope is in the instrument's
|
||||
own price units per bar — normalise (e.g. divide by price) to compare.
|
||||
- **Tiny periods.** `period = 2` reduces the slope to the last simple
|
||||
difference; use a meaningful window.
|
||||
|
||||
## References
|
||||
|
||||
The slope of an ordinary least-squares fit to a rolling price window; matches
|
||||
TA-Lib's `LINEARREG_SLOPE`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-LinearRegression.md](../price-statistics/Indicator-LinearRegression.md) — the
|
||||
endpoint of the same rolling fit.
|
||||
- [Indicator-Mom.md](../momentum-oscillators/Indicator-Mom.md) — raw price-difference
|
||||
momentum, the unsmoothed cousin.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,162 +0,0 @@
|
||||
# LinearRegression
|
||||
|
||||
> Linear Regression — the endpoint of a rolling ordinary-least-squares fit
|
||||
> over the last `period` prices.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `f64` (price) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | A low-lag smoothed price — the trend line extrapolated to now. |
|
||||
|
||||
## Formula
|
||||
|
||||
Over the last `period` inputs, indexed `x = 0, 1, …, period − 1`:
|
||||
|
||||
```
|
||||
b (slope) = (n·Σxy − Σx·Σy) / (n·Σxx − (Σx)²)
|
||||
a (intercept) = (Σy − b·Σx) / n
|
||||
LinearReg = a + b·(period − 1)
|
||||
```
|
||||
|
||||
The indicator fits a straight line to the window by ordinary least squares,
|
||||
then reports that line's value at the most recent bar. Because it
|
||||
extrapolates the *local trend* forward rather than averaging it away, it lags
|
||||
a same-period [`Sma`](../moving-averages/Indicator-Sma.md) noticeably less. This is
|
||||
TA-Lib's `LINEARREG`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the regression window. Must be at least `2` (a line needs two
|
||||
points). The Python binding defaults it to `14`; the Rust and Node
|
||||
constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/linreg.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for LinearRegression {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`LinearRegression` is a **scalar** indicator: it consumes one `f64` price per
|
||||
step. Because `Input = f64` it can sit inside a [`Chain`](../../Indicator-Chaining.md).
|
||||
|
||||
## Warmup
|
||||
|
||||
`LinearRegression::new(14).warmup_period() == 14`. The first value lands once
|
||||
the window holds a full `period` prices — on input index `period − 1`.
|
||||
|
||||
## Complexity
|
||||
|
||||
Each `update` is **O(1)**: the `Σx` and `Σxx` terms depend only on `period`
|
||||
and are precomputed once at construction, and `Σy` / `Σxy` are maintained
|
||||
incrementally as the window slides via the closed-form identity
|
||||
`new_Σxy = old_Σxy − old_Σy + popped_y₀` (then `Σxy += (n − 1) · new_value`
|
||||
and `Σy += new_value`). The same applies to
|
||||
[`LinRegSlope`](Indicator-LinRegSlope.md) and
|
||||
[`LinRegAngle`](Indicator-LinRegAngle.md).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **`period < 2`.** Rejected at construction — a regression line is undefined
|
||||
for fewer than two points.
|
||||
- **Perfect line.** Fed a perfectly linear series, the fit *is* that line, so
|
||||
the endpoint equals the current value (`perfect_line_returns_current_value`
|
||||
pins this).
|
||||
- **Constant series.** A flat input returns that constant.
|
||||
- **Reset.** `lr.reset()` clears the rolling window and the running `Σy` /
|
||||
`Σxy` accumulators.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, LinearRegression};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut lr = LinearRegression::new(3)?;
|
||||
// Fit over [1, 2, 9]: the least-squares line is y = 4x, endpoint 4·2 = 8.
|
||||
let out = lr.batch(&[1.0, 2.0, 9.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(8.0)]
|
||||
```
|
||||
|
||||
This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/linreg.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
lr = ta.LinearRegression(3)
|
||||
print(lr.batch(np.array([1.0, 2.0, 9.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan 8.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const lr = new ta.LinearRegression(3);
|
||||
console.log(lr.batch([1, 2, 9]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 8 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Read `LinearRegression` as a low-lag moving average: it tracks price more
|
||||
closely than an SMA of the same period because it projects the window's trend
|
||||
to the current bar instead of centring on the window. A shorter `period`
|
||||
hugs price; a longer one is a smoother trend line. Pair it with
|
||||
[`LinRegSlope`](../price-statistics/Indicator-LinRegSlope.md) to read the same fit's steepness.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing it with an SMA.** It is a *projected* fit, not a centred
|
||||
average, so it leads an SMA of the same period.
|
||||
- **Tiny periods.** `period = 2` is allowed but the "fit" just passes through
|
||||
the last two points; use a meaningful window.
|
||||
|
||||
## References
|
||||
|
||||
Ordinary least-squares linear regression applied to a rolling price window;
|
||||
the endpoint formulation matches TA-Lib's `LINEARREG`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-LinRegSlope.md](../price-statistics/Indicator-LinRegSlope.md) — the slope of the same
|
||||
rolling fit.
|
||||
- [Indicator-Sma.md](../moving-averages/Indicator-Sma.md) — the centred average it is
|
||||
often compared against.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,135 +0,0 @@
|
||||
# MedianPrice
|
||||
|
||||
> Median Price — the bar's `(high + low) / 2`, the midpoint of its range.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | The midpoint of the bar's range, ignoring open and close. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
MedianPrice = (high + low) / 2
|
||||
```
|
||||
|
||||
The median price is the centre of the bar's range — it discards where the bar
|
||||
opened and closed entirely. It is the price series Bill Williams'
|
||||
[`AwesomeOscillator`](../momentum-oscillators/Indicator-AwesomeOscillator.md) is built on,
|
||||
and a useful close substitute when the close is noisy relative to the range.
|
||||
|
||||
## Parameters
|
||||
|
||||
`MedianPrice` takes **no parameters** — `MedianPrice::new()` in Rust,
|
||||
`wickra.MedianPrice()` in Python, `new ta.MedianPrice()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/median_price.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for MedianPrice {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`MedianPrice` is a **candle-input** indicator that reads `high` and `low`. In
|
||||
Python the streaming `update` accepts a 6-tuple or a dict; the batch helper
|
||||
takes `high`, `low` numpy arrays. Node and WASM expose `update(high, low)` and
|
||||
the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`MedianPrice::new().warmup_period() == 1`. It is a stateless per-bar transform
|
||||
— it emits a value from the very first candle.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **No warmup.** Every candle produces a value immediately.
|
||||
- **Reset.** `mp.reset()` only clears the `is_ready` flag; there is no
|
||||
rolling state to discard.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Candle, Indicator, MedianPrice};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut mp = MedianPrice::new();
|
||||
let v = mp.update(Candle::new(10.0, 12.0, 8.0, 11.0, 1.0, 0)?);
|
||||
println!("{:?}", v);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(10.0)
|
||||
```
|
||||
|
||||
`(12 + 8) / 2 = 10`. This matches the `reference_value` test in
|
||||
`crates/wickra-core/src/indicators/median_price.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
mp = ta.MedianPrice()
|
||||
print(mp.batch(np.array([12.0]), np.array([8.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[10.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const mp = new ta.MedianPrice();
|
||||
console.log(mp.batch([12], [8]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 10 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The median price is the most range-centric of the three transforms — it is
|
||||
blind to the close. Use it when the question is "where did this bar trade?"
|
||||
rather than "where did it settle?", or as the input to a Bill Williams setup.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting the close to matter.** It does not — by definition the median
|
||||
price ignores both the open and the close.
|
||||
|
||||
## References
|
||||
|
||||
The Median Price; the `(H + L) / 2` definition is standard (TA-Lib's
|
||||
`MEDPRICE`).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-TypicalPrice.md](../price-statistics/Indicator-TypicalPrice.md) — `(H + L + C) / 3`.
|
||||
- [Indicator-WeightedClose.md](../price-statistics/Indicator-WeightedClose.md) — `(H + L + 2C) / 4`.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,136 +0,0 @@
|
||||
# TypicalPrice
|
||||
|
||||
> Typical Price — the bar's `(high + low + close) / 3`, a single
|
||||
> representative price per candle.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | A representative per-bar price; a smoother stand-in for the close. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
TypicalPrice = (high + low + close) / 3
|
||||
```
|
||||
|
||||
The typical price collapses a full OHLC bar to one number, giving the close
|
||||
no more weight than the two extremes. It is the price series that
|
||||
[`Cci`](../momentum-oscillators/Indicator-Cci.md) and [`Mfi`](../momentum-oscillators/Indicator-Mfi.md)
|
||||
are defined on, and a common input to feed any close-driven indicator when you
|
||||
want the bar's range reflected in the value.
|
||||
|
||||
## Parameters
|
||||
|
||||
`TypicalPrice` takes **no parameters** — `TypicalPrice::new()` in Rust,
|
||||
`wickra.TypicalPrice()` in Python, `new ta.TypicalPrice()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/typical_price.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for TypicalPrice {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`TypicalPrice` is a **candle-input** indicator that reads `high`, `low` and
|
||||
`close`. In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high`, `low`, `close` numpy arrays. Node and WASM expose
|
||||
`update(high, low, close)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`TypicalPrice::new().warmup_period() == 1`. It is a stateless per-bar
|
||||
transform — it emits a value from the very first candle.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **No warmup.** Every candle produces a value immediately.
|
||||
- **Reset.** `tp.reset()` only clears the `is_ready` flag; there is no
|
||||
rolling state to discard.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Candle, Indicator, TypicalPrice};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut tp = TypicalPrice::new();
|
||||
let v = tp.update(Candle::new(9.0, 12.0, 6.0, 9.0, 1.0, 0)?);
|
||||
println!("{:?}", v);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(9.0)
|
||||
```
|
||||
|
||||
`(12 + 6 + 9) / 3 = 9`. This matches the `reference_value` test in
|
||||
`crates/wickra-core/src/indicators/typical_price.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
tp = ta.TypicalPrice()
|
||||
print(tp.batch(np.array([12.0]), np.array([6.0]), np.array([9.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[9.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const tp = new ta.TypicalPrice();
|
||||
console.log(tp.batch([12], [6], [9]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 9 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Use it wherever you would use the close but want the bar's range to count —
|
||||
feeding a moving average, an oscillator, or a band. It is marginally smoother
|
||||
than the raw close because a wild close is pulled back toward the bar's mid.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** It needs the full `high`/`low`/`close` bar.
|
||||
|
||||
## References
|
||||
|
||||
The Typical Price (also "pivot price"); the `(H + L + C) / 3` definition is
|
||||
standard (StockCharts, TA-Lib's `TYPPRICE`).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-MedianPrice.md](../price-statistics/Indicator-MedianPrice.md) — `(H + L) / 2`.
|
||||
- [Indicator-WeightedClose.md](../price-statistics/Indicator-WeightedClose.md) — `(H + L + 2C) / 4`.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,136 +0,0 @@
|
||||
# WeightedClose
|
||||
|
||||
> Weighted Close — the bar's `(high + low + 2·close) / 4`, a per-bar price
|
||||
> that gives the close double weight.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | A representative per-bar price that leans on the close. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
WeightedClose = (high + low + 2·close) / 4
|
||||
```
|
||||
|
||||
Like the [`TypicalPrice`](../price-statistics/Indicator-TypicalPrice.md), the weighted close
|
||||
collapses an OHLC bar to one number — but it counts the close twice, so the
|
||||
result sits closer to where the bar settled than to its range. Reach for it
|
||||
when the closing print carries more signal than the extremes.
|
||||
|
||||
## Parameters
|
||||
|
||||
`WeightedClose` takes **no parameters** — `WeightedClose::new()` in Rust,
|
||||
`wickra.WeightedClose()` in Python, `new ta.WeightedClose()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/weighted_close.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for WeightedClose {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`WeightedClose` is a **candle-input** indicator that reads `high`, `low` and
|
||||
`close`. In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high`, `low`, `close` numpy arrays. Node and WASM expose
|
||||
`update(high, low, close)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`WeightedClose::new().warmup_period() == 1`. It is a stateless per-bar
|
||||
transform — it emits a value from the very first candle.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **No warmup.** Every candle produces a value immediately.
|
||||
- **Reset.** `wc.reset()` only clears the `is_ready` flag; there is no
|
||||
rolling state to discard.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Candle, Indicator, WeightedClose};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut wc = WeightedClose::new();
|
||||
let v = wc.update(Candle::new(10.0, 12.0, 8.0, 11.0, 1.0, 0)?);
|
||||
println!("{:?}", v);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(10.5)
|
||||
```
|
||||
|
||||
`(12 + 8 + 2·11) / 4 = 42 / 4 = 10.5`. This matches the `reference_value`
|
||||
test in `crates/wickra-core/src/indicators/weighted_close.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
wc = ta.WeightedClose()
|
||||
print(wc.batch(np.array([12.0]), np.array([8.0]), np.array([11.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[10.5]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const wc = new ta.WeightedClose();
|
||||
console.log(wc.batch([12], [8], [11]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 10.5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The weighted close sits on the spectrum between the raw close and the
|
||||
[`TypicalPrice`](../price-statistics/Indicator-TypicalPrice.md): closer to the close, but still
|
||||
nudged by the bar's range. Use it as a drop-in close replacement when you want
|
||||
the settlement to dominate without ignoring the extremes entirely.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** It needs the full `high`/`low`/`close` bar.
|
||||
|
||||
## References
|
||||
|
||||
The Weighted Close; the `(H + L + 2C) / 4` definition is standard (TA-Lib's
|
||||
`WCLPRICE`).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-TypicalPrice.md](../price-statistics/Indicator-TypicalPrice.md) — `(H + L + C) / 3`.
|
||||
- [Indicator-MedianPrice.md](../price-statistics/Indicator-MedianPrice.md) — `(H + L) / 2`.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,139 +0,0 @@
|
||||
# ZScore
|
||||
|
||||
> Z-Score — how many standard deviations the latest price sits from its
|
||||
> rolling mean.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Price Statistics |
|
||||
| Input type | `f64` (price) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (standard deviations) |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Large magnitude = stretched; a return toward `0` = reversion. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
ZScore = (price − SMA(price, n)) / population_stddev(price, n)
|
||||
```
|
||||
|
||||
The Z-Score normalises price against its own recent behaviour: it subtracts
|
||||
the rolling mean and divides by the rolling population standard deviation. A
|
||||
reading of `+2` means price is two standard deviations above its `n`-bar
|
||||
average — statistically stretched to the upside; `−2` is the mirror. It is the
|
||||
standard input to mean-reversion strategies.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the rolling window for the mean and standard deviation. The Python
|
||||
binding defaults it to `20`; the Rust and Node constructors require it.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/z_score.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ZScore {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ZScore` is a **scalar** indicator: it consumes one `f64` price per step.
|
||||
Because `Input = f64` it can sit inside a [`Chain`](../../Indicator-Chaining.md).
|
||||
|
||||
## Warmup
|
||||
|
||||
`ZScore::new(20).warmup_period() == 20`. The first value lands once the window
|
||||
holds a full `period` prices.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero dispersion.** A flat window has a zero standard deviation; `ZScore`
|
||||
is defined as `0` rather than dividing by zero.
|
||||
- **Rising series.** A monotonically rising price always scores above its
|
||||
trailing mean (positive).
|
||||
- **Reset.** `z.reset()` clears the rolling window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, ZScore};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut z = ZScore::new(2)?;
|
||||
// Window [1, 3]: mean 2, population stddev 1; latest 3 -> (3 - 2) / 1.
|
||||
println!("{:?}", z.batch(&[1.0, 3.0]));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(1.0)]
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
z = ta.ZScore(2)
|
||||
print(z.batch(np.array([1.0, 3.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan 1.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const z = new ta.ZScore(2);
|
||||
console.log(z.batch([1, 3]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 1 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Z-Score is the workhorse of mean-reversion: a common rule enters against the
|
||||
move when `|ZScore| > 2` and exits as it crosses back through `0`. Read
|
||||
together with a trend filter — a high Z-Score in a strong trend is often
|
||||
continuation, not exhaustion, so the reversion edge is best in ranging
|
||||
regimes.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Trading extremes blindly.** A trending market can hold a high Z-Score for
|
||||
a long time; pair it with a regime filter.
|
||||
- **Tiny periods.** A short window makes the mean and stddev jumpy.
|
||||
|
||||
## References
|
||||
|
||||
The standard statistical Z-Score (standard score) applied to a rolling price
|
||||
window.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-StdDev.md](../volatility-bands/Indicator-StdDev.md) — the rolling
|
||||
standard deviation in the denominator.
|
||||
- [Indicator-LinearRegression.md](../price-statistics/Indicator-LinearRegression.md) — another
|
||||
rolling statistical fit.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,169 +0,0 @@
|
||||
# AtrTrailingStop
|
||||
|
||||
> ATR Trailing Stop — a single stop level that trails price by a fixed ATR
|
||||
> multiple, ratcheting toward the trend and flipping on a close through it.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trailing Stops |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | `atr_period = 14`, `multiplier = 3.0` (Python) |
|
||||
| Warmup period | `atr_period` |
|
||||
| Interpretation | One trailing stop line; price closing through it flips the trade. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
loss = multiplier · ATR
|
||||
|
||||
stop_t = max(stop_{t−1}, close − loss) while price holds above the stop
|
||||
= min(stop_{t−1}, close + loss) while price holds below the stop
|
||||
= close − loss on a fresh break above the stop
|
||||
= close + loss on a fresh break below the stop
|
||||
```
|
||||
|
||||
This is the trailing stop popularised by the "UT Bot": a single line that sits
|
||||
`multiplier · ATR` away from the close. While price holds on one side of the
|
||||
stop the level only ratchets *toward* price — up in an uptrend, down in a
|
||||
downtrend — and never away from it. When a close crosses the stop the level
|
||||
snaps to the opposite side of the new close, flipping the trade. Unlike the
|
||||
[`ChandelierExit`](../trailing-stops/Indicator-ChandelierExit.md), it hangs off the close
|
||||
itself, not the window's extreme, and reports one line rather than two.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `atr_period` — the ATR lookback (Python default `14`).
|
||||
- `multiplier` — the ATR multiple the stop trails by (Python default `3.0`).
|
||||
|
||||
`AtrTrailingStop::classic()` returns the `(14, 3.0)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/atr_trailing_stop.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for AtrTrailingStop {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`AtrTrailingStop` is a **candle-input** indicator (it reads `high`, `low`,
|
||||
`close`). In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high`, `low`, `close` numpy arrays. Node and WASM expose
|
||||
`update(high, low, close)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`AtrTrailingStop::classic().warmup_period() == 14`. The first value lands once
|
||||
the inner ATR is ready, on input index `atr_period − 1`. That first bar seeds
|
||||
the stop below price (a long).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Seed bar.** The first emitted stop is `close − loss` — the indicator
|
||||
starts on the long side.
|
||||
- **Ratchet.** While price holds above the stop it never moves down, and
|
||||
while price holds below it never moves up
|
||||
(`uptrend_stop_ratchets_up_and_stays_below_price` pins this).
|
||||
- **Flat market.** Constant candles hold the stop at a fixed `close − loss`.
|
||||
- **Reset.** `ts.reset()` clears the ATR and the carried stop / close.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, AtrTrailingStop};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ts = AtrTrailingStop::new(5, 3.0)?;
|
||||
// Flat market: ATR = 2, loss = 3·2 = 6, stop = 10 - 6 = 4.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, i).unwrap())
|
||||
.collect();
|
||||
let out = ts.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(4.0)
|
||||
```
|
||||
|
||||
On a flat market the seeded long stop holds at `close − loss = 10 − 6 = 4`.
|
||||
This matches the `reference_values_flat_market` test in
|
||||
`crates/wickra-core/src/indicators/atr_trailing_stop.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ts = ta.AtrTrailingStop(5, 3.0)
|
||||
n = 20
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
print(ts.batch(high, low, close)[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
4.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ts = new ta.AtrTrailingStop(5, 3.0);
|
||||
const n = 20;
|
||||
const high = Array(n).fill(11), low = Array(n).fill(9), close = Array(n).fill(10);
|
||||
const out = ts.batch(high, low, close);
|
||||
console.log(out[out.length - 1]);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
4
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Read it as a stop-and-reverse line: while the stop sits below the close you
|
||||
are long and it trails your profit up; the bar a close prints below the stop,
|
||||
it flips above the new close and you are short. A larger `multiplier` gives
|
||||
the trade more room — fewer flips, wider risk; a smaller one flips sooner.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting it off the window high.** It trails the *close*, so it can sit
|
||||
closer to price than a [`ChandelierExit`](../trailing-stops/Indicator-ChandelierExit.md).
|
||||
- **Feeding it scalar prices.** It needs the full `high`/`low`/`close` bar to
|
||||
drive the ATR.
|
||||
|
||||
## References
|
||||
|
||||
The ATR Trailing Stop used by the well-known "UT Bot"; the four-branch ratchet
|
||||
here matches the common Sylvain Vervoort formulation.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-SuperTrend.md](../trailing-stops/Indicator-SuperTrend.md) — an ATR trailing stop
|
||||
with band ratcheting and an explicit direction flag.
|
||||
- [Indicator-ChandelierExit.md](../trailing-stops/Indicator-ChandelierExit.md) — an ATR stop hung
|
||||
off the window's extreme instead of the close.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the volatility measure underneath.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,176 +0,0 @@
|
||||
# ChandeKrollStop
|
||||
|
||||
> Chande Kroll Stop — a two-stage ATR stop: an ATR stop off the recent
|
||||
> extreme, then smoothed by taking the most extreme such stop over a
|
||||
> shorter window.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trailing Stops |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `(stop_long, stop_short)` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | `atr_period = 10`, `atr_multiplier = 1.0`, `stop_period = 9` (Python) |
|
||||
| Warmup period | `atr_period + stop_period − 1` |
|
||||
| Interpretation | Smoothed long/short stop levels, less prone to single-bar whipsaw. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
preliminary (window p = atr_period, x = atr_multiplier):
|
||||
high_stop = highest_high(p) − x · ATR(p)
|
||||
low_stop = lowest_low(p) + x · ATR(p)
|
||||
|
||||
final (window q = stop_period):
|
||||
stop_short = highest(high_stop, q)
|
||||
stop_long = lowest(low_stop, q)
|
||||
```
|
||||
|
||||
Tushar Chande and Stanley Kroll's stop runs in two stages. The first builds a
|
||||
preliminary ATR stop off the recent extreme — the same idea as a
|
||||
[`ChandelierExit`](../trailing-stops/Indicator-ChandelierExit.md). The second smooths it: rather
|
||||
than use that preliminary stop directly, it takes the *most extreme*
|
||||
preliminary stop seen over a shorter window `q`. That second pass keeps a
|
||||
single unusually wide bar from yanking the stop around. The classic
|
||||
configuration from *The New Technical Trader* is `ATR(10)`, multiplier `1.0`,
|
||||
smoothing window `9`.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `atr_period` — window for the preliminary ATR and the highest high / lowest
|
||||
low (Python default `10`).
|
||||
- `atr_multiplier` — how many ATRs the preliminary stop sits off the extreme
|
||||
(default `1.0`).
|
||||
- `stop_period` — the smoothing window `q` (default `9`).
|
||||
|
||||
`ChandeKrollStop::classic()` returns the `(10, 1.0, 9)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/chande_kroll_stop.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChandeKrollStop {
|
||||
type Input = Candle;
|
||||
type Output = ChandeKrollStopOutput; // { stop_long: f64, stop_short: f64 }
|
||||
// update(&mut self, input: Candle) -> Option<ChandeKrollStopOutput>
|
||||
}
|
||||
```
|
||||
|
||||
`ChandeKrollStop` is a **candle-input** indicator (it reads `high`, `low`,
|
||||
`close`). Python's streaming `update` returns a `(stop_long, stop_short)`
|
||||
tuple; the batch helper returns an `(n, 2)` array with columns
|
||||
`[stop_long, stop_short]`. Node's `update` returns `{ stopLong, stopShort }`
|
||||
and `batch` a flat `[l0, s0, l1, s1, …]` array; WASM matches Node.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChandeKrollStop::classic().warmup_period() == 18` (`atr_period + stop_period −
|
||||
1`). The preliminary stop first appears on candle `atr_period`; the smoothing
|
||||
window then needs `stop_period` of them.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Two-stage warmup.** Nothing is emitted until both the preliminary window
|
||||
and the smoothing window have filled.
|
||||
- **Flat market.** Constant candles collapse both stages to fixed levels.
|
||||
- **Reset.** `cks.reset()` clears the ATR and all four windows.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChandeKrollStop};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cks = ChandeKrollStop::new(5, 1.0, 3)?;
|
||||
// Flat market: ATR = 2, HH = 11, LL = 9.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, i).unwrap())
|
||||
.collect();
|
||||
let out = cks.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(ChandeKrollStopOutput { stop_long: 11.0, stop_short: 9.0 })
|
||||
```
|
||||
|
||||
`high_stop = 11 − 1·2 = 9`, `low_stop = 9 + 1·2 = 11`; the smoothing pass over
|
||||
constant values leaves `stop_short = 9` and `stop_long = 11`. This matches the
|
||||
`reference_values_flat_market` test in
|
||||
`crates/wickra-core/src/indicators/chande_kroll_stop.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
cks = ta.ChandeKrollStop(5, 1.0, 3)
|
||||
n = 20
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
print(cks.batch(high, low, close)[-1]) # [stop_long, stop_short]
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[11. 9.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const cks = new ta.ChandeKrollStop(5, 1.0, 3);
|
||||
const n = 20;
|
||||
const high = Array(n).fill(11), low = Array(n).fill(9), close = Array(n).fill(10);
|
||||
const out = cks.batch(high, low, close);
|
||||
console.log(out.slice(-2)); // [stop_long, stop_short] of the last bar
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 11, 9 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Use `stop_long` to trail a long position and `stop_short` to trail a short.
|
||||
Compared with a one-stage [`ChandelierExit`](../trailing-stops/Indicator-ChandelierExit.md), the
|
||||
extra smoothing window makes the Chande Kroll Stop steadier — it will not lurch
|
||||
on a single wide-range bar — at the cost of reacting a little slower to a
|
||||
genuine trend change.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Forgetting the longer warmup.** Two stacked windows mean `atr_period +
|
||||
stop_period − 1` bars before the first value.
|
||||
- **Confusing the labels.** `stop_short` is generally the lower level and
|
||||
`stop_long` the higher — they bracket recent price, but each only applies to
|
||||
its own side.
|
||||
|
||||
## References
|
||||
|
||||
Tushar Chande and Stanley Kroll's stop, from *The New Technical Trader* (1994);
|
||||
the two-stage formulation here matches the common TradingView implementation.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-ChandelierExit.md](../trailing-stops/Indicator-ChandelierExit.md) — the one-stage
|
||||
ATR stop this smooths.
|
||||
- [Indicator-SuperTrend.md](../trailing-stops/Indicator-SuperTrend.md) — an ATR trailing stop
|
||||
with explicit flip logic.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the volatility measure underneath.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,165 +0,0 @@
|
||||
# ChandelierExit
|
||||
|
||||
> Chandelier Exit — an ATR trailing stop hung a fixed number of ATRs off
|
||||
> the highest high (for longs) or the lowest low (for shorts) of a window.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trailing Stops |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `(long_stop, short_stop)` |
|
||||
| Output range | unbounded (price scale) |
|
||||
| Default parameters | `period = 22`, `multiplier = 3.0` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Long/short trailing-stop levels; a close past one exits the trade. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
long_stop = highest_high(period) − multiplier · ATR(period)
|
||||
short_stop = lowest_low(period) + multiplier · ATR(period)
|
||||
```
|
||||
|
||||
Chuck LeBeau's Chandelier Exit hangs the stop off the extreme of the lookback
|
||||
window — like a chandelier off a ceiling — a fixed `multiplier · ATR` below the
|
||||
highest high (for a long) or above the lowest low (for a short). Because the
|
||||
extreme only moves favourably while a trend runs, the stop trails price up
|
||||
(or down) and never loosens. A long is exited when price closes below
|
||||
`long_stop`; a short when it closes above `short_stop`. The classic
|
||||
configuration is a `22`-bar window with a `3.0` multiplier.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `period` — the window for both the highest high / lowest low and the ATR
|
||||
(Python default `22`).
|
||||
- `multiplier` — how many ATRs the stop hangs off the extreme (default `3.0`).
|
||||
|
||||
`ChandelierExit::classic()` returns the `(22, 3.0)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/chandelier_exit.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChandelierExit {
|
||||
type Input = Candle;
|
||||
type Output = ChandelierExitOutput; // { long_stop: f64, short_stop: f64 }
|
||||
// update(&mut self, input: Candle) -> Option<ChandelierExitOutput>
|
||||
}
|
||||
```
|
||||
|
||||
`ChandelierExit` is a **candle-input** indicator (it reads `high`, `low`,
|
||||
`close`). Python's streaming `update` returns a `(long_stop, short_stop)`
|
||||
tuple; the batch helper returns an `(n, 2)` array with columns
|
||||
`[long_stop, short_stop]`. Node's `update` returns `{ longStop, shortStop }`
|
||||
and `batch` a flat `[l0, s0, l1, s1, …]` array; WASM matches Node.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChandelierExit::classic().warmup_period() == 22`. The highest-high / lowest-low
|
||||
window and the inner ATR become ready on the same candle — input index
|
||||
`period − 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Window bound.** `long_stop` never exceeds the window's highest high, and
|
||||
`short_stop` never drops below its lowest low
|
||||
(`long_stop_below_highest_short_stop_above_lowest` pins this).
|
||||
- **Flat market.** Constant candles give constant `ATR` and equal extremes, so
|
||||
both stops sit a fixed `multiplier · ATR` from the price.
|
||||
- **Reset.** `ce.reset()` clears the ATR and both extreme windows.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChandelierExit};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ce = ChandelierExit::new(5, 3.0)?;
|
||||
// Flat market: ATR = 2, HH = 11, LL = 9.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, i).unwrap())
|
||||
.collect();
|
||||
let out = ce.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(ChandelierExitOutput { long_stop: 5.0, short_stop: 15.0 })
|
||||
```
|
||||
|
||||
`long_stop = 11 − 3·2 = 5`, `short_stop = 9 + 3·2 = 15`. This matches the
|
||||
`reference_values_flat_market` test in
|
||||
`crates/wickra-core/src/indicators/chandelier_exit.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ce = ta.ChandelierExit(5, 3.0)
|
||||
n = 20
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
print(ce.batch(high, low, close)[-1]) # [long_stop, short_stop]
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 5. 15.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ce = new ta.ChandelierExit(5, 3.0);
|
||||
const n = 20;
|
||||
const high = Array(n).fill(11), low = Array(n).fill(9), close = Array(n).fill(10);
|
||||
const out = ce.batch(high, low, close);
|
||||
console.log(out.slice(-2)); // [long_stop, short_stop] of the last bar
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 5, 15 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
While long, watch `long_stop`: it climbs as new highs print and never falls,
|
||||
so a close beneath it is a disciplined exit. While short, `short_stop` is the
|
||||
mirror. The `3.0` multiplier is wide enough to ride a trend through normal
|
||||
pullbacks; tightening it exits sooner at the cost of more whipsaws.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Using the wrong stop for the position.** `long_stop` only applies to
|
||||
longs, `short_stop` only to shorts — they are not a channel.
|
||||
- **Feeding it scalar prices.** It needs the full `high`/`low`/`close` bar.
|
||||
|
||||
## References
|
||||
|
||||
Chuck LeBeau's Chandelier Exit; the highest-high-minus-ATR formulation here
|
||||
matches the standard definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-SuperTrend.md](../trailing-stops/Indicator-SuperTrend.md) — an ATR trailing stop
|
||||
with explicit flip logic and a single line.
|
||||
- [Indicator-ChandeKrollStop.md](../trailing-stops/Indicator-ChandeKrollStop.md) — a two-stage
|
||||
ATR stop that smooths the preliminary level.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the volatility measure underneath.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,257 +0,0 @@
|
||||
# PSAR (Parabolic SAR)
|
||||
|
||||
> Wilder's parabolic Stop-And-Reverse: a state-machine trailing stop that
|
||||
> accelerates toward price as a trend extends and flips sides on a
|
||||
> penetration of the SAR line.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Item | Value |
|
||||
|---------------------|------------------------------------------------------------------------------------|
|
||||
| Family | Trailing Stops |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded; bracketed by the prior two highs/lows |
|
||||
| Default parameters | `af_start = 0.02`, `af_step = 0.02`, `af_max = 0.20` (Wilder) |
|
||||
| Warmup period | `2` (state machine seeds on the 2nd candle) |
|
||||
| Interpretation | trailing stop that "flips" sides on penetration; never tied to a fixed bar count |
|
||||
|
||||
## Formula
|
||||
|
||||
PSAR is a two-state machine — `Up` (long bias) and `Down` (short bias).
|
||||
Each bar updates three pieces of state:
|
||||
|
||||
```
|
||||
EP_t = extreme price reached so far in the current trend (max high in Up,
|
||||
min low in Down)
|
||||
AF_t = acceleration factor, bumped by af_step each time EP makes a new
|
||||
extreme, capped at af_max
|
||||
SAR_t = stop-and-reverse level
|
||||
```
|
||||
|
||||
The transition is:
|
||||
|
||||
```
|
||||
SAR_t = SAR_{t-1} + AF_{t-1} * (EP_{t-1} - SAR_{t-1})
|
||||
|
||||
# Wilder rule: SAR cannot penetrate today's or yesterday's range
|
||||
if Up: SAR_t = min(SAR_t, low_{t-1}, low_t)
|
||||
if Down: SAR_t = max(SAR_t, high_{t-1}, high_t)
|
||||
|
||||
# Reversal test
|
||||
if Up and low_t <= SAR_t: flip to Down, SAR_t = EP_{t-1}, reset AF
|
||||
if Down and high_t >= SAR_t: flip to Up, SAR_t = EP_{t-1}, reset AF
|
||||
```
|
||||
|
||||
The exact step-by-step is `crates/wickra-core/src/indicators/psar.rs:75-141`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Constraint | Source |
|
||||
|------------|-------|---------|-------------------------------------------|---------------------------------------|
|
||||
| `af_start` | `f64` | `0.02` | finite, `> 0`, `≤ af_max` | `Psar::new` (`psar.rs:39-50`) |
|
||||
| `af_step` | `f64` | `0.02` | finite, `> 0` | `Psar::new` (`psar.rs:39-50`) |
|
||||
| `af_max` | `f64` | `0.20` | finite, `> 0` | `Psar::new` (`psar.rs:39-50`) |
|
||||
|
||||
Python defaults from
|
||||
`#[pyo3(signature = (af_start=0.02, af_step=0.02, af_max=0.20))]` in
|
||||
`bindings/python/src/lib.rs`. `Psar::classic()` returns the same triple.
|
||||
|
||||
Validation errors:
|
||||
- non-finite or non-positive AF parameter → `Error::NonPositiveMultiplier`
|
||||
- `af_start > af_max` → `Error::InvalidPeriod { message: "af_start must be <= af_max" }`
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
```rust
|
||||
impl Indicator for Psar {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
fn warmup_period(&self) -> usize { 2 }
|
||||
}
|
||||
```
|
||||
|
||||
- **Python streaming.** `psar.update(candle)` returns `float | None`.
|
||||
- **Python batch.** `PSAR.batch(high, low, close)` returns a 1-D
|
||||
`np.ndarray`; the first row is `NaN` (warmup) and every subsequent
|
||||
row holds the SAR level for that bar.
|
||||
- **Node streaming.** `psar.update(high, low, close)` returns `number | null`.
|
||||
- **Node batch.** `psar.batch(high, low, close)` returns
|
||||
`Array<number>` with `NaN` for the first row.
|
||||
- **WASM streaming.** `psar.update(high, low, close)` returns
|
||||
`number | null` once warm.
|
||||
- **WASM batch.** `psar.batch(high, low, close)` returns a
|
||||
`Float64Array` with `NaN` for the first row.
|
||||
- **`isReady` convention.** `psar.is_ready()` flips to `true` only once the
|
||||
first non-`None` SAR has been produced (i.e. from the second candle
|
||||
onwards). The first (seed) candle returns `None` and `is_ready()` stays
|
||||
`false`, matching every other indicator in the library. Previous releases
|
||||
flipped the flag after the seed candle even though it produced no value —
|
||||
consumers that wrote `if psar.is_ready() { use(psar.update(c)?) }` would
|
||||
hit an unexpected `None` on the first post-seed update; that's now fixed.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == 2`. The very first candle seeds internal state
|
||||
(`prev_high`, `prev_low`, `sar = low`, `ep = high`, `trend = Up`,
|
||||
`af = af_start`) and returns `None`. The second candle produces the
|
||||
first SAR value.
|
||||
|
||||
The seed trend is **always** `Up` (`psar.rs:83`); the indicator will
|
||||
reverse to `Down` on the first qualifying penetration. There is no
|
||||
look-ahead at the second candle's close — the seed is purely structural.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **First bar.** Always returns `None`; downstream code must tolerate
|
||||
the first row being absent without crashing.
|
||||
- **Pure uptrend.** With monotonically rising highs and lows, the SAR
|
||||
remains below the lows and accelerates toward price as the EP makes
|
||||
successive new highs. The pinned test `pure_uptrend_sar_below_lows`
|
||||
asserts `SAR ≤ low` on every emitted bar of a 40-bar ramp.
|
||||
- **Pure downtrend.** Symmetrically, with monotonically falling highs,
|
||||
the SAR sits above the highs after the trend establishes.
|
||||
`pure_downtrend_sar_above_highs` covers this.
|
||||
- **Reversal mechanics.** When the trend flips, `SAR` is set to the
|
||||
previous EP (not the calculated parabola value), AF is reset to
|
||||
`af_start`, and the new EP is the current bar's high (Down→Up) or
|
||||
low (Up→Down).
|
||||
- **Choppy regime.** Frequent reversals cause many AF resets; SAR
|
||||
becomes a poor stop in mean-reverting regimes and whipsaws.
|
||||
- **NaN / infinity.** `Candle::new` rejects non-finite OHLC values.
|
||||
`Psar::new` rejects non-finite AF parameters.
|
||||
- **Reset.** `reset()` clears the initialised flag and resets `af` to
|
||||
`af_start`, `sar` to `0.0`, `ep` to `0.0`; the next `update` re-seeds.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Psar};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let candles: Vec<Candle> = (0..8)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
Candle::new(base, base + 0.5, base - 0.5, base + 0.25, 1.0, 0).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut p = Psar::classic(); // (0.02, 0.02, 0.20)
|
||||
for (i, v) in p.batch(&candles).into_iter().enumerate() {
|
||||
println!("i={i} -> {:?}", v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
i=0 -> None
|
||||
i=1 -> Some(99.5)
|
||||
i=2 -> Some(99.58)
|
||||
i=3 -> Some(99.7552)
|
||||
i=4 -> Some(100.054784)
|
||||
i=5 -> Some(100.4993056)
|
||||
i=6 -> Some(101.099388928)
|
||||
i=7 -> Some(101.85547447808)
|
||||
```
|
||||
|
||||
The SAR starts at `99.5` (the first candle's low) and accelerates
|
||||
upward toward price as the EP makes new highs on every bar.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
p = ta.PSAR() # defaults (0.02, 0.02, 0.20)
|
||||
h = np.array([100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5])
|
||||
l = np.array([ 99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5])
|
||||
cl = np.array([100.25, 101.25, 102.25, 103.25, 104.25, 105.25, 106.25, 107.25])
|
||||
print(p.batch(h, l, cl))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 99.5 99.58 99.7552 100.054784
|
||||
100.4993056 101.09938893 101.85547448]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```js
|
||||
const w = require('wickra');
|
||||
|
||||
const p = new w.PSAR(0.02, 0.02, 0.20);
|
||||
console.log(p.batch(
|
||||
[100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5, 107.5],
|
||||
[ 99.5, 100.5, 101.5, 102.5, 103.5, 104.5, 105.5, 106.5],
|
||||
[100.25, 101.25, 102.25, 103.25, 104.25, 105.25, 106.25, 107.25],
|
||||
));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[
|
||||
NaN,
|
||||
99.5,
|
||||
99.58,
|
||||
99.7552,
|
||||
100.054784,
|
||||
100.4993056,
|
||||
101.099388928,
|
||||
101.85547447808
|
||||
]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Stop & reverse.** PSAR is a *trailing stop*, not a signal generator
|
||||
in isolation: a long is exited (and a short is initiated) the bar
|
||||
that price penetrates the SAR line.
|
||||
- **Acceleration.** The further a trend extends without making new
|
||||
extremes, the slower the SAR rises (or falls). When EP makes a new
|
||||
extreme, AF bumps by `af_step` and the SAR closes the distance to
|
||||
price more aggressively.
|
||||
- **Whipsaw risk.** In sideways markets PSAR flips repeatedly; pair it
|
||||
with a trend filter (ADX, slope of EMA) to skip trades when the
|
||||
underlying isn't actually trending.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **The first bar always returns `None`.** Code that pre-allocates a
|
||||
vector and does `out[i] = psar.update(c).unwrap()` will panic on
|
||||
the very first input. Use `if let Some(...)` or skip the first
|
||||
row explicitly.
|
||||
- **Initial trend is hard-coded to `Up`.** The seed bar always sets
|
||||
`trend = Up`, regardless of whether the data is in a downtrend.
|
||||
Expect a near-immediate reversal to `Down` if you feed PSAR a
|
||||
decisively bearish series — the first emitted SAR may look
|
||||
"wrong" because it is the prior EP from the artificial `Up`
|
||||
seed, not from a real bullish run.
|
||||
- **Acceleration cap matters.** `af_max = 0.20` is Wilder's choice;
|
||||
raising it produces an extremely tight stop near tops/bottoms but
|
||||
exits good trends prematurely. Lowering it produces a forgiving
|
||||
stop that gives back more open profit. Always re-validate strategy
|
||||
PnL when you change `af_max`.
|
||||
|
||||
## References
|
||||
|
||||
- J. Welles Wilder Jr., *New Concepts in Technical Trading Systems*,
|
||||
Trend Research, 1978. Chapter on the Parabolic SAR introduces the
|
||||
state-machine recursion and the default `(0.02, 0.02, 0.20)`
|
||||
parameters.
|
||||
|
||||
## See also
|
||||
|
||||
- [ATR](../volatility-bands/Indicator-Atr.md) — sister indicator from the same Wilder text.
|
||||
- [Donchian Channels](../volatility-bands/Indicator-Donchian.md) — alternative breakout-style
|
||||
trailing stop based on rolling extrema.
|
||||
- [Keltner Channels](../volatility-bands/Indicator-Keltner.md) — envelope you can use as a
|
||||
smoother stop boundary than PSAR in choppy regimes.
|
||||
@@ -1,173 +0,0 @@
|
||||
# SuperTrend
|
||||
|
||||
> SuperTrend — an ATR-banded trailing stop that flips sides when price
|
||||
> closes through the band, reporting both the stop level and the trend
|
||||
> direction.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trailing Stops |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `(value, direction)` |
|
||||
| Output range | `value`: unbounded (price scale); `direction`: `−1.0` or `+1.0` |
|
||||
| Default parameters | `atr_period = 10`, `multiplier = 3.0` (Python) |
|
||||
| Warmup period | `atr_period` |
|
||||
| Interpretation | Trend-following stop; a direction flip marks a trend change. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
hl2 = (high + low) / 2
|
||||
basic_upper = hl2 + multiplier · ATR
|
||||
basic_lower = hl2 − multiplier · ATR
|
||||
|
||||
final_upper = basic_upper if basic_upper < prev_final_upper or prev_close > prev_final_upper
|
||||
else prev_final_upper
|
||||
final_lower = basic_lower if basic_lower > prev_final_lower or prev_close < prev_final_lower
|
||||
else prev_final_lower
|
||||
|
||||
downtrend: stay down while close <= final_upper, else flip up
|
||||
uptrend: stay up while close >= final_lower, else flip down
|
||||
SuperTrend = final_lower in an uptrend, final_upper in a downtrend
|
||||
```
|
||||
|
||||
The two final bands ratchet — the upper band only moves down, the lower band
|
||||
only moves up — until price closes through the active one. That close flips
|
||||
the trend and hands the trailing-stop role to the opposite band. The result is
|
||||
a single line that sits below price in an uptrend and above it in a downtrend,
|
||||
plus a `direction` flag (`+1.0` / `-1.0`) that names which regime you are in.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `atr_period` — the ATR lookback (Python default `10`).
|
||||
- `multiplier` — how many ATRs wide the bands sit (Python default `3.0`).
|
||||
|
||||
`SuperTrend::classic()` returns Wilder's `(10, 3.0)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/super_trend.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for SuperTrend {
|
||||
type Input = Candle;
|
||||
type Output = SuperTrendOutput; // { value: f64, direction: f64 }
|
||||
// update(&mut self, input: Candle) -> Option<SuperTrendOutput>
|
||||
}
|
||||
```
|
||||
|
||||
`SuperTrend` is a **candle-input** indicator (it reads `high`, `low`, `close`).
|
||||
Python's streaming `update` returns a `(value, direction)` tuple; the batch
|
||||
helper returns an `(n, 2)` array with columns `[value, direction]`. Node's
|
||||
`update` returns `{ value, direction }` and `batch` a flat `[v0, d0, v1, d1, …]`
|
||||
array; WASM matches Node.
|
||||
|
||||
## Warmup
|
||||
|
||||
`SuperTrend::classic().warmup_period() == 10`. The first value lands once the
|
||||
inner ATR is ready, on input index `atr_period − 1`. The first ATR-ready bar
|
||||
seeds the trend as up; the flip logic corrects it within a few bars if the
|
||||
market is actually falling.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Seed direction.** The first emitted bar is always `direction = +1.0`; a
|
||||
genuine downtrend flips it within a handful of bars.
|
||||
- **Flat market.** Constant candles give a constant ATR, so both bands and the
|
||||
line are flat and the trend never flips.
|
||||
- **Reset.** `st.reset()` clears the ATR and the carried band state.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, SuperTrend};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut st = SuperTrend::new(5, 3.0)?;
|
||||
// Flat market: ATR = 2, hl2 = 10, lower band = 10 - 3·2 = 4.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, i).unwrap())
|
||||
.collect();
|
||||
let out = st.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(SuperTrendOutput { value: 4.0, direction: 1.0 })
|
||||
```
|
||||
|
||||
On a flat market the seeded uptrend never flips and the line holds at the
|
||||
lower band, `4.0`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
st = ta.SuperTrend(5, 3.0)
|
||||
n = 20
|
||||
high = np.full(n, 11.0)
|
||||
low = np.full(n, 9.0)
|
||||
close = np.full(n, 10.0)
|
||||
print(st.batch(high, low, close)[-1]) # [value, direction]
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[4. 1.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const st = new ta.SuperTrend(5, 3.0);
|
||||
const n = 20;
|
||||
const high = Array(n).fill(11), low = Array(n).fill(9), close = Array(n).fill(10);
|
||||
const out = st.batch(high, low, close);
|
||||
console.log(out.slice(-2)); // [value, direction] of the last bar
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 4, 1 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`SuperTrend` is used as a stop-and-reverse system: stay long while
|
||||
`direction == +1` and the line trails below price, flip to short the bar the
|
||||
`direction` turns `-1` and the line jumps above price. A larger `multiplier`
|
||||
widens the bands — fewer whipsaws, later flips; a smaller one flips sooner.
|
||||
The line itself doubles as a concrete stop-loss level.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting an exact flip bar.** The seed bar is always an uptrend; on
|
||||
genuinely falling data the flip lands a few bars in.
|
||||
- **Reading `value` without `direction`.** The line means "support" in an
|
||||
uptrend and "resistance" in a downtrend — `direction` tells you which.
|
||||
|
||||
## References
|
||||
|
||||
The SuperTrend trailing stop; the final-band ratchet formulation here matches
|
||||
the widely used TradingView / Olivier Seban definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Psar.md](../trailing-stops/Indicator-Psar.md) — Wilder's parabolic stop-and-reverse.
|
||||
- [Indicator-AtrTrailingStop.md](../trailing-stops/Indicator-AtrTrailingStop.md) — a plain
|
||||
ATR trailing stop without the band ratchet.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the volatility measure underneath.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,244 +0,0 @@
|
||||
# ADX
|
||||
|
||||
> Wilder's Average Directional Index — the smoothed strength of a trend,
|
||||
> plus the two directional components (`+DI`, `−DI`) that say which
|
||||
> direction the trend is going.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `AdxOutput { plus_di, minus_di, adx }` |
|
||||
| Output range | each field in `[0, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `2 · period` (28 for `period = 14`) |
|
||||
| Interpretation | `adx > 25` means a meaningful trend; the dominant DI gives its direction |
|
||||
|
||||
## Formula
|
||||
|
||||
For each new candle at time `t` (with previous candle `t-1`):
|
||||
|
||||
```
|
||||
+DM_t = high_t − high_{t-1} if (high_t − high_{t-1}) > (low_{t-1} − low_t)
|
||||
and (high_t − high_{t-1}) > 0
|
||||
= 0 otherwise
|
||||
|
||||
−DM_t = low_{t-1} − low_t if (low_{t-1} − low_t) > (high_t − high_{t-1})
|
||||
and (low_{t-1} − low_t) > 0
|
||||
= 0 otherwise
|
||||
|
||||
TR_t = max(high_t − low_t,
|
||||
|high_t − close_{t-1}|,
|
||||
|low_t − close_{t-1}|)
|
||||
```
|
||||
|
||||
Wilder's smoothing is applied to all three series. Seeding is a simple
|
||||
sum over the first `period` post-prev candles; after seeding the update
|
||||
rule for any of these is
|
||||
|
||||
```
|
||||
S_t = S_{t-1} − S_{t-1} / period + X_t
|
||||
```
|
||||
|
||||
where `X_t` is `TR_t`, `+DM_t`, or `−DM_t`. The directional indicators
|
||||
and DX then are
|
||||
|
||||
```
|
||||
+DI_t = 100 · (+DM smoothed)_t / (TR smoothed)_t
|
||||
−DI_t = 100 · (−DM smoothed)_t / (TR smoothed)_t
|
||||
DX_t = 100 · |+DI_t − −DI_t| / (+DI_t + −DI_t)
|
||||
```
|
||||
|
||||
`ADX_t` is itself a Wilder-smoothed `DX` series, seeded as the mean of
|
||||
the first `period` `DX` values, and then updated with `α = 1/period`:
|
||||
|
||||
```
|
||||
ADX_t = (ADX_{t-1} · (period − 1) + DX_t) / period
|
||||
```
|
||||
|
||||
When `+DI + −DI == 0`, `DX` is `0`; when `TR == 0`, both DI lines are
|
||||
`0`. These are the divide-by-zero guards in `Adx::update`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` | `>= 1` | Wilder smoothing length shared by `+DM`, `−DM`, `TR`, and `ADX`. |
|
||||
|
||||
`Adx::new(0)` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Adx`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = AdxOutput;
|
||||
fn update(&mut self, candle: Candle) -> Option<AdxOutput>;
|
||||
```
|
||||
|
||||
`AdxOutput`:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `plus_di` | Plus Directional Indicator (`+DI`) — strength of upward movement. |
|
||||
| `minus_di` | Minus Directional Indicator (`−DI`) — strength of downward movement. |
|
||||
| `adx` | Average Directional Index — smoothed `|DX|`, a directionless trend-strength measure. |
|
||||
|
||||
Python's `ADX.batch(high, low, close)` returns a `(n, 3)` `float64` array
|
||||
with columns `[plus_di, minus_di, adx]`; warmup rows are entirely `NaN`.
|
||||
The streaming `update(candle)` returns a `(plus_di, minus_di, adx)`
|
||||
tuple or `None`.
|
||||
|
||||
Node's `ADX.batch(high, low, close)` returns a flat `number[]` of length
|
||||
`n * 3`, interleaved `[plus_di_0, minus_di_0, adx_0, plus_di_1, …]`.
|
||||
Only `batch` is exposed on the Node binding — no `update`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `2 · period`. The first candle just provides a
|
||||
"previous" reference (no DM/TR can be computed yet); the next `period`
|
||||
candles seed the smoothed `+DM`, `−DM`, and `TR` sums; the next `period`
|
||||
candles after that produce `DX` values that seed `ADX`. For `period =
|
||||
14` that's `1 + 14 + 13 = 28` candles before the first full
|
||||
`AdxOutput`, which matches `2 · 14 = 28`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Strong unidirectional trend.** If every candle is strictly higher
|
||||
than the last (with `+DM` always positive, `−DM` always zero), `+DI`
|
||||
saturates at `100`, `−DI` at `0`, and `ADX` climbs toward `100`. The
|
||||
example below produces exactly that.
|
||||
- **Flat market (no high/low movement).** Every `TR`, `+DM`, `−DM` is
|
||||
zero, so the divide-by-zero guards return `+DI = −DI = 0` and `DX =
|
||||
0`; `ADX` then sits at `0` indefinitely.
|
||||
- **Reset.** `reset()` clears `prev`, all seed sums and counts, all
|
||||
smoothed values, the DX buffer, and `adx_value`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Adx, BatchExt, Candle, Indicator};
|
||||
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + i as f64 * 2.0;
|
||||
Candle::new(base + 0.5, base + 1.0, base - 0.5, base + 0.5, 1.0, 0).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let mut adx = Adx::new(14)?;
|
||||
let out = adx.batch(&candles);
|
||||
let v = out[27].unwrap();
|
||||
println!("row 27 +DI={} -DI={} ADX={}", v.plus_di, v.minus_di, v.adx);
|
||||
let v = out[39].unwrap();
|
||||
println!("row 39 +DI={} -DI={} ADX={}", v.plus_di, v.minus_di, v.adx);
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 27 +DI=80 -DI=0 ADX=100
|
||||
row 39 +DI=80 -DI=0 ADX=100
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
n = 40
|
||||
i = np.arange(n, dtype=float)
|
||||
base = 100.0 + i * 2.0
|
||||
high = base + 1.0
|
||||
low = base - 0.5
|
||||
close = base + 0.5
|
||||
adx = ta.ADX(14)
|
||||
out = adx.batch(high, low, close)
|
||||
print('warmup:', adx.warmup_period())
|
||||
print('shape :', out.shape)
|
||||
print('row 27:', out[27])
|
||||
print('row 39:', out[39])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 28
|
||||
shape : (40, 3)
|
||||
row 27: [ 80. 0. 100.]
|
||||
row 39: [ 80. 0. 100.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const n = 40;
|
||||
const high = [], low = [], close = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
const b = 100 + i * 2;
|
||||
high.push(b + 1);
|
||||
low.push(b - 0.5);
|
||||
close.push(b + 0.5);
|
||||
}
|
||||
const adx = new wickra.ADX(14);
|
||||
const out = adx.batch(high, low, close);
|
||||
console.log('len :', out.length);
|
||||
console.log('row 27:', { plusDi: out[27 * 3], minusDi: out[27 * 3 + 1], adx: out[27 * 3 + 2] });
|
||||
console.log('row 39:', { plusDi: out[39 * 3], minusDi: out[39 * 3 + 1], adx: out[39 * 3 + 2] });
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
len : 120
|
||||
row 27: { plusDi: 80, minusDi: 0, adx: 100 }
|
||||
row 39: { plusDi: 80, minusDi: 0, adx: 100 }
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Trend-strength bands.** `ADX < 20` is typically read as a ranging
|
||||
market; `ADX > 25` as a "real" trend; `ADX > 40` as a strong trend.
|
||||
ADX itself is direction-agnostic — you need `+DI` vs `−DI` to know
|
||||
which way the trend points.
|
||||
- **DI crossover.** `+DI` crossing above `−DI` is a bullish directional
|
||||
signal; the mirror is bearish. Many traders only act on a crossover
|
||||
when `ADX > 25` to filter out crossovers in a ranging market.
|
||||
- **ADX peaks.** A rising ADX confirms trend continuation; a falling
|
||||
ADX from a high level suggests the current trend is exhausting (even
|
||||
if `+DI` still dominates `−DI`).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Long warmup.** ADX needs `2 · period` candles before the first
|
||||
emission — twice as many as most other Wilder indicators. A common
|
||||
bug is reusing an "RSI fits in `period + 1` bars" mental model and
|
||||
reading garbage during the ADX warmup; check `is_ready()` or test
|
||||
for `NaN` on the `adx` column.
|
||||
- **Plotted on the same axis as DI.** `+DI`, `−DI`, and `ADX` all live
|
||||
in `[0, 100]` and are typically overlaid. The crossover signal is
|
||||
between `+DI` and `−DI` only — `ADX` does not cross either of them
|
||||
for any directional meaning.
|
||||
|
||||
## References
|
||||
|
||||
- J. Welles Wilder, *New Concepts in Technical Trading Systems*, Trend
|
||||
Research, 1978 — the original publication of `+DI`, `−DI`, `DX`,
|
||||
`ADX`, and the smoothing scheme they share with RSI and ATR.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — shares Wilder smoothing.
|
||||
- [Indicator: Aroon](../trend-directional/Indicator-Aroon.md) — alternative trend-strength
|
||||
measure, range-based.
|
||||
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — trend-following
|
||||
momentum, useful as a confirmation against `+DI` / `−DI`.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — the `2 · period` ADX entry.
|
||||
@@ -1,205 +0,0 @@
|
||||
# Aroon
|
||||
|
||||
> Tushar Chande's Aroon indicator — tracks the bars-since-highest-high
|
||||
> and bars-since-lowest-low inside a `period + 1`-bar window, reported
|
||||
> as percentages.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` |
|
||||
| Output type | `AroonOutput { up, down }` |
|
||||
| Output range | `up, down ∈ [0, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period + 1` (15 for `period = 14`) |
|
||||
| Interpretation | `up > 70 && down < 30` strong uptrend (mirror for downtrend); crossovers as turn signals |
|
||||
|
||||
## Formula
|
||||
|
||||
Scan the rolling `period + 1`-bar window for the position of the highest
|
||||
high and the position of the lowest low (with `0 = oldest`,
|
||||
`period = newest`):
|
||||
|
||||
```
|
||||
hh_idx_t = argmax_{i in 0..period} high_{t-period+i}
|
||||
ll_idx_t = argmin_{i in 0..period} low_{t-period+i}
|
||||
|
||||
up_t = 100 · hh_idx_t / period
|
||||
down_t = 100 · ll_idx_t / period
|
||||
```
|
||||
|
||||
When the highest high lands on the most-recent bar (`hh_idx == period`),
|
||||
`up == 100`; when it lands on the oldest bar in the window, `up == 0`.
|
||||
The same holds for `down`.
|
||||
|
||||
In Wickra's implementation the scan uses `>=` / `<=`, so ties go to the
|
||||
*latest* matching bar — which is why a perfectly flat window produces
|
||||
`up = down = 100` rather than `0` (the latest bar is always tied with
|
||||
the oldest).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` | `>= 1` | Lookback length. The internal window holds `period + 1` candles. |
|
||||
|
||||
`Aroon::new(0)` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Aroon`:
|
||||
|
||||
```rust
|
||||
type Input = Candle;
|
||||
type Output = AroonOutput;
|
||||
fn update(&mut self, candle: Candle) -> Option<AroonOutput>;
|
||||
```
|
||||
|
||||
`AroonOutput`:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `up` | `100 · bars_since_oldest_HH / period`, in `[0, 100]`. High = recent new high. |
|
||||
| `down` | `100 · bars_since_oldest_LL / period`, in `[0, 100]`. High = recent new low. |
|
||||
|
||||
Python's `Aroon.batch(high, low)` returns a `(n, 2)` `float64` array
|
||||
with columns `[up, down]`; warmup rows are `[NaN, NaN]`. Streaming
|
||||
`update(candle)` returns a `(up, down)` tuple or `None`.
|
||||
|
||||
Node's `Aroon.batch(high, low)` returns a flat `number[]` of length
|
||||
`n * 2`, interleaved `[up_0, down_0, up_1, down_1, …]`. Only `batch`
|
||||
is exposed on the Node binding.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `period + 1`. Aroon scans `period + 1` bars
|
||||
to find "bars since highest high" (which ranges over `0..period`), so
|
||||
the indicator is not ready until exactly `period + 1` candles have
|
||||
arrived. This is the same off-by-one as RSI and ROC, but for a
|
||||
window-position reason rather than a diff reason.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure uptrend.** Every new candle is a new high — `hh_idx` is always
|
||||
the latest position, `up == 100`. The lowest low is the oldest
|
||||
candle in the window, `down == 0`. Tests `pure_uptrend_aroon_up_100`
|
||||
pin this.
|
||||
- **Constant input.** Every candle's high is equal to every other
|
||||
candle's high. The `>=` tiebreak in the scan means the most-recent
|
||||
candle always wins both the HH and LL positions — both `up` and
|
||||
`down` end up at `100`. (Be careful: this is *not* a neutral
|
||||
reading; it is an artefact of the tiebreak rule.)
|
||||
- **Reset.** `reset()` clears the candle buffer; the next `period + 1`
|
||||
updates return `None`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Aroon, BatchExt, Candle, Indicator};
|
||||
|
||||
let candles: Vec<Candle> = (1..=15)
|
||||
.map(|i| Candle::new(i as f64, i as f64 + 1.0, i as f64 - 1.0, i as f64, 1.0, 0).unwrap())
|
||||
.collect();
|
||||
let mut aroon = Aroon::new(14)?;
|
||||
let out = aroon.batch(&candles);
|
||||
let v = out[14].unwrap();
|
||||
println!("uptrend row 14 up={} down={}", v.up, v.down);
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
uptrend row 14 up=100 down=0
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
i = np.arange(1, 16, dtype=float)
|
||||
high = i + 1.0
|
||||
low = i - 1.0
|
||||
aroon = ta.Aroon(14)
|
||||
out = aroon.batch(high, low)
|
||||
print('warmup:', aroon.warmup_period())
|
||||
print('shape :', out.shape)
|
||||
print('row 14:', out[14])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 15
|
||||
shape : (15, 2)
|
||||
row 14: [100. 0.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const high = [], low = [];
|
||||
for (let i = 1; i <= 15; i++) {
|
||||
high.push(i + 1);
|
||||
low.push(i - 1);
|
||||
}
|
||||
const a = new wickra.Aroon(14);
|
||||
const out = a.batch(high, low);
|
||||
console.log('len :', out.length);
|
||||
console.log('row 14:', { up: out[14 * 2], down: out[14 * 2 + 1] });
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
len : 30
|
||||
row 14: { up: 100, down: 0 }
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Strong trend bands.** `up > 70` with `down < 30` indicates a strong
|
||||
uptrend (new highs are recent, new lows are old); mirror for a
|
||||
downtrend.
|
||||
- **Crossover.** `up` crossing above `down` is a bullish trend-shift
|
||||
signal; the mirror is bearish. Crossovers near `50/50` are weak
|
||||
(the window has no clear leader); crossovers from `0`/`100` extremes
|
||||
are strong.
|
||||
- **Consolidation.** Both lines wandering near `50` means neither
|
||||
recent highs nor recent lows are dominating — typical of a
|
||||
range-bound market.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Constant input gives `up == down == 100`, not `0` or `50`.** The
|
||||
`>=` / `<=` tiebreak in the scan rewards the most-recent candle.
|
||||
Treat constant or near-constant windows as a degenerate case; a
|
||||
reading of `(100, 100)` is *not* a strong trend in both directions
|
||||
— it is "no information".
|
||||
- **`period + 1` warmup, not `period`.** Same off-by-one trap as RSI:
|
||||
the indicator looks at a window of size `period + 1` so that
|
||||
`bars_since_high` can range from `0` to `period`. Indexing your
|
||||
output array as if it were ready at the `period`-th input gives you
|
||||
one `NaN` / `None` row at the start you didn't expect.
|
||||
|
||||
## References
|
||||
|
||||
- Tushar Chande, "A New Tool for Technical Traders: The Aroon
|
||||
Indicator", *Technical Analysis of Stocks & Commodities*, September
|
||||
1995 — the original publication.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Adx](../trend-directional/Indicator-Adx.md) — alternative trend-strength
|
||||
measure with explicit `+DI` / `−DI` direction.
|
||||
- [Indicator: Stochastic](../momentum-oscillators/Indicator-Stochastic.md) — also range-window
|
||||
based, but reports close position rather than extremum age.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — the `period + 1` family.
|
||||
@@ -1,158 +0,0 @@
|
||||
# AroonOscillator
|
||||
|
||||
> Aroon Oscillator — the single-line difference `AroonUp − AroonDown`,
|
||||
> condensing the two Aroon lines into one trend gauge.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−100, 100]` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Positive = up-trend, negative = down-trend, near zero = range. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
AroonOscillator = AroonUp − AroonDown
|
||||
```
|
||||
|
||||
where [`Aroon`](../trend-directional/Indicator-Aroon.md) reports two `[0, 100]` lines measuring
|
||||
how recently the window's highest high and lowest low occurred. Their
|
||||
difference lives in `[−100, 100]`: strongly positive means the most recent
|
||||
high is much fresher than the most recent low (an up-trend); strongly
|
||||
negative is the mirror image; near zero means neither extreme is recent.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` (Python) | `>= 1` | Aroon lookback window. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `14`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/aroon_oscillator.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for AroonOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`AroonOscillator` is a **candle-input** indicator: it reads `high` and
|
||||
`low`. In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high` and `low` numpy arrays. Node and WASM expose
|
||||
`update(high, low)` and `batch(high, low)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`AroonOscillator::new(period).warmup_period() == period + 1` — identical
|
||||
to the underlying `Aroon`, which needs a `period + 1`-bar window before
|
||||
the first reading.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure trend.** A series of fresh highs gives `AroonUp = 100`,
|
||||
`AroonDown = 0`, so the oscillator is `+100`; a series of fresh lows is
|
||||
`−100` (`pure_uptrend_yields_plus_100` /
|
||||
`pure_downtrend_yields_minus_100` pin this).
|
||||
- **Bounds.** The output is always within `[−100, 100]`
|
||||
(`output_stays_within_minus_100_and_100` pins this).
|
||||
- **Candle validation.** `Candle::new` rejects invalid bars before
|
||||
`update` ever sees them.
|
||||
- **Reset.** `osc.reset()` clears the underlying Aroon window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, AroonOscillator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut osc = AroonOscillator::new(5)?;
|
||||
// 30 bars, each a fresh high.
|
||||
let candles: Vec<Candle> = (0..30)
|
||||
.map(|i| {
|
||||
let p = 100.0 + f64::from(i);
|
||||
Candle::new(p, p + 1.0, p - 1.0, p, 1.0, i64::from(i)).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let out = osc.batch(&candles);
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
last = Some(100.0)
|
||||
```
|
||||
|
||||
Every bar is a fresh high and never a fresh low, so the oscillator pins at
|
||||
`+100`. This matches the `pure_uptrend_yields_plus_100` test in
|
||||
`crates/wickra-core/src/indicators/aroon_oscillator.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
osc = ta.AroonOscillator(14)
|
||||
high = np.arange(100.0, 140.0)
|
||||
low = high - 2.0
|
||||
print(osc.batch(high, low)[-1]) # steady uptrend -> 100
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
100.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const osc = new ta.AroonOscillator(14);
|
||||
const high = Array.from({ length: 40 }, (_, i) => 100 + i);
|
||||
const low = high.map((h) => h - 2);
|
||||
console.log(osc.batch(high, low).at(-1)); // 100
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`AroonOscillator` is a compact trend gauge. The two canonical reads are
|
||||
the zero-line cross (`AroonUp` overtaking `AroonDown` or vice versa — a
|
||||
trend change) and the magnitude (values pinned near `±100` confirm a
|
||||
strong, uninterrupted trend; values oscillating near zero confirm a
|
||||
range). Use it where the two-line `Aroon` is more detail than you need.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** It needs `high`/`low`; it takes a
|
||||
`Candle`, not an `f64`.
|
||||
- **Expecting the `[0, 100]` Aroon scale.** The oscillator is signed and
|
||||
spans `[−100, 100]`.
|
||||
|
||||
## References
|
||||
|
||||
Tushar Chande's Aroon system (1995); the oscillator is the standard
|
||||
`AroonUp − AroonDown` difference.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Aroon.md](../trend-directional/Indicator-Aroon.md) — the two-line indicator this
|
||||
collapses.
|
||||
- [Indicator-Adx.md](../trend-directional/Indicator-Adx.md) — another trend-strength gauge.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,146 +0,0 @@
|
||||
# ChoppinessIndex
|
||||
|
||||
> Choppiness Index — is the market trending or just chopping sideways?
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 100]` (typical) |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | High = choppy/ranging, low = trending; `61.8` / `38.2` thresholds. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
CI = 100 · log10( Σ(TR, n) / (highest_high(n) − lowest_low(n)) ) / log10(n)
|
||||
```
|
||||
|
||||
The ratio compares the distance price *actually travelled* (the summed true
|
||||
range) with the *net ground it covered* (the high-low span of the window). A
|
||||
clean trend travels almost exactly its span, so the ratio is near `1` and `CI`
|
||||
near `0`; a choppy market criss-crosses far more than its span, so the ratio
|
||||
is large and `CI` climbs toward `100`. The conventional reading is `CI > 61.8`
|
||||
ranging, `CI < 38.2` trending.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the lookback window. Must be at least `2` (the `log10(period)`
|
||||
denominator is zero for `period == 1`). The Python binding defaults it to `14`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/choppiness_index.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChoppinessIndex {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ChoppinessIndex` is a **candle-input** indicator that reads `high`, `low` and
|
||||
`close` (the close drives the true range across bar gaps). Python's streaming
|
||||
`update` accepts a 6-tuple or a dict; the batch helper takes `high`, `low`,
|
||||
`close` numpy arrays. Node and WASM expose `update(high, low, close)` and the
|
||||
matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChoppinessIndex::new(14).warmup_period() == 14`. The first value lands once
|
||||
the window holds a full `period` bars.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat window.** A window with `high == low` everywhere has a zero span;
|
||||
`CI` is defined as `100` (maximal choppiness).
|
||||
- **Steady trend.** A one-directional march reads well below `50`.
|
||||
- **`period < 2`.** Rejected at construction.
|
||||
- **Reset.** `ci.reset()` clears the true-range and high/low windows.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChoppinessIndex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ci = ChoppinessIndex::new(2)?;
|
||||
// Two H=11 L=9 C=10 bars: ΣTR = 4, span = 2 -> CI = 100·log10(2)/log10(2).
|
||||
let out = ci.batch(&[
|
||||
Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0)?,
|
||||
Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 1)?,
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(100.0)]
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ci = ta.ChoppinessIndex(2)
|
||||
high = np.array([11.0, 11.0])
|
||||
low = np.array([9.0, 9.0])
|
||||
close = np.array([10.0, 10.0])
|
||||
print(ci.batch(high, low, close))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 100.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ci = new ta.ChoppinessIndex(2);
|
||||
console.log(ci.batch([11, 11], [9, 9], [10, 10]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 100 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
The Choppiness Index is not directional — it does not say *which way* price is
|
||||
going, only *whether* it is going anywhere. Use it as a regime filter: above
|
||||
`61.8` favour mean-reversion / range tactics; below `38.2` favour
|
||||
trend-following. It pairs naturally with a directional indicator that picks
|
||||
the side once a trend is confirmed.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting a direction.** It has none — combine it with a trend indicator.
|
||||
- **Tiny periods.** `period = 2` is allowed but noisy; `14` is conventional.
|
||||
|
||||
## References
|
||||
|
||||
E. W. Dreiss' Choppiness Index; the summed-true-range formulation here is the
|
||||
standard one.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-VerticalHorizontalFilter.md](../trend-directional/Indicator-VerticalHorizontalFilter.md)
|
||||
— the same trending-vs-ranging question on an inverted scale.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,215 +0,0 @@
|
||||
# MacdIndicator
|
||||
|
||||
> Moving Average Convergence Divergence — the difference of two EMAs, with
|
||||
> a third EMA on top as the signal line.
|
||||
|
||||
The Rust struct is `MacdIndicator` (since `Macd` would collide with the
|
||||
output struct on case-insensitive file systems and several existing trait
|
||||
imports). The Python and Node bindings expose the same engine under the
|
||||
shorter, conventional name `MACD`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `f64` (close) |
|
||||
| Output type | `MacdOutput { macd, signal, histogram }` |
|
||||
| Output range | unbounded (centred on 0) |
|
||||
| Default parameters | `fast = 12`, `slow = 26`, `signal = 9` (`MacdIndicator::classic()`) |
|
||||
| Warmup period | `slow + signal − 1` (34 for the classic configuration) |
|
||||
| Interpretation | crossovers of `macd` and `signal`; zero-line crosses; histogram momentum |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
EMA_n(x) = exponential moving average of x over n periods
|
||||
(Wickra's EMA seeds from a simple average of the first n inputs)
|
||||
|
||||
macd_t = EMA_fast(close)_t − EMA_slow(close)_t
|
||||
signal_t = EMA_signal(macd)_t
|
||||
hist_t = macd_t − signal_t
|
||||
```
|
||||
|
||||
The signal EMA does not start consuming inputs until `macd_t` becomes
|
||||
defined (i.e. until both the fast and slow EMAs have seeded), which is
|
||||
why the overall warmup is `slow + signal − 1` rather than
|
||||
`max(slow, signal)`.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default (Python) | Valid range | Description |
|
||||
|------|------|------------------|-------------|-------------|
|
||||
| `fast` | `usize` | `12` | `>= 1` and `< slow` | Fast EMA period. |
|
||||
| `slow` | `usize` | `26` | `>= 1` and `> fast` | Slow EMA period. |
|
||||
| `signal` | `usize` | `9` | `>= 1` | EMA period applied to the raw MACD line. |
|
||||
|
||||
`MacdIndicator::new` returns `Error::PeriodZero` if any period is zero and
|
||||
`Error::InvalidPeriod` if `fast >= slow`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for MacdIndicator`:
|
||||
|
||||
```rust
|
||||
type Input = f64;
|
||||
type Output = MacdOutput;
|
||||
fn update(&mut self, input: f64) -> Option<MacdOutput>;
|
||||
```
|
||||
|
||||
`MacdOutput` carries three fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| `macd` | `EMA(fast) − EMA(slow)` of the input series. |
|
||||
| `signal` | `EMA(signal)` of `macd`. |
|
||||
| `histogram` | `macd − signal`. |
|
||||
|
||||
Python's `MACD.batch(prices)` returns a `(n, 3)` `float64` array with
|
||||
columns `[macd, signal, histogram]`; warmup rows are entirely `NaN`.
|
||||
|
||||
Node's `MACD.batch(prices)` returns a flat `number[]` of length `n * 3`
|
||||
in the same interleaved order: index `i*3 + 0` is `macd`, `i*3 + 1` is
|
||||
`signal`, `i*3 + 2` is `histogram`. The streaming `update(value)` returns
|
||||
a `{ macd, signal, histogram }` object (or `null` during warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `slow + signal − 1`. The slow EMA seeds at
|
||||
input `slow`; from that point onward the signal EMA starts receiving
|
||||
`macd` values, and needs `signal − 1` further inputs to seed itself.
|
||||
For the classic `(12, 26, 9)` configuration this gives `26 + 9 − 1 = 34`
|
||||
inputs before the first complete `MacdOutput` is emitted, as pinned by
|
||||
the unit test `first_emission_matches_warmup_period`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant input.** Both EMAs converge to the constant value, so `macd`
|
||||
approaches `0`; with no movement in `macd`, the signal EMA also
|
||||
approaches `0`, and so does the histogram. The Rust test
|
||||
`constant_series_yields_zero_macd_eventually` pins this.
|
||||
- **Non-finite input.** `update(NaN)` or `update(±∞)` returns the
|
||||
previously emitted `MacdOutput` without advancing any internal EMA.
|
||||
- **Reset.** `reset()` resets all three EMAs and clears `last`. The next
|
||||
`warmup_period()` calls return `None` again.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, MacdIndicator};
|
||||
|
||||
let prices: Vec<f64> = (0..40).map(|i| 100.0 + i as f64 * (20.0 / 39.0)).collect();
|
||||
let mut macd = MacdIndicator::classic();
|
||||
let out = macd.batch(&prices);
|
||||
let v = out[33].unwrap();
|
||||
println!("row 33 macd={} signal={} hist={}", v.macd, v.signal, v.histogram);
|
||||
let v = out[39].unwrap();
|
||||
println!("row 39 macd={} signal={} hist={}", v.macd, v.signal, v.histogram);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 33 macd=3.589743589743577 signal=3.5897435897435788 hist=-0.0000000000000017763568394002505
|
||||
row 39 macd=3.589743589743591 signal=3.589743589743585 hist=0.000000000000006217248937900877
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
prices = np.linspace(100.0, 120.0, 40)
|
||||
macd = ta.MACD(12, 26, 9)
|
||||
out = macd.batch(prices)
|
||||
print('shape :', out.shape)
|
||||
print('warmup:', macd.warmup_period())
|
||||
print('row 33:', out[33])
|
||||
print('row 39:', out[39])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
shape : (40, 3)
|
||||
warmup: 34
|
||||
row 33: [ 3.58974359e+00 3.58974359e+00 -1.77635684e-15]
|
||||
row 39: [3.58974359e+00 3.58974359e+00 6.21724894e-15]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const macd = new wickra.MACD(12, 26, 9);
|
||||
const prices = Array.from({ length: 40 }, (_, i) => 100 + i * 20 / 39);
|
||||
const flat = macd.batch(prices);
|
||||
console.log('flat length:', flat.length);
|
||||
console.log('row 33 macd :', flat[33 * 3]);
|
||||
console.log('row 33 signal:', flat[33 * 3 + 1]);
|
||||
console.log('row 33 hist :', flat[33 * 3 + 2]);
|
||||
console.log('row 39 macd :', flat[39 * 3]);
|
||||
console.log('row 39 signal:', flat[39 * 3 + 1]);
|
||||
console.log('row 39 hist :', flat[39 * 3 + 2]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
flat length: 120
|
||||
row 33 macd : 3.589743589743577
|
||||
row 33 signal: 3.5897435897435788
|
||||
row 33 hist : -1.7763568394002505e-15
|
||||
row 39 macd : 3.589743589743591
|
||||
row 39 signal: 3.589743589743585
|
||||
row 39 hist : 6.217248937900877e-15
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Signal-line crossover.** `macd` crossing above `signal` is the canonical
|
||||
bullish signal; the symmetric crossover below is bearish. The
|
||||
`histogram` makes this explicit — it crosses zero on the same bar.
|
||||
- **Zero-line crossover.** `macd` crossing above zero says the fast EMA
|
||||
has overtaken the slow EMA; a longer-term trend confirmation, weaker
|
||||
than the signal-line cross.
|
||||
- **Histogram momentum.** Rising histogram bars (even while negative)
|
||||
indicate that bearish momentum is fading, and vice versa. Traders use
|
||||
this to anticipate signal-line crosses.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **The signal line lags the MACD line by `signal_period` bars.** A
|
||||
crossover signal therefore arrives one full EMA-cycle after the
|
||||
underlying momentum turn, which is why MACD is a *confirmation*
|
||||
indicator, not a leading one.
|
||||
- **`fast >= slow` is rejected.** A common bug when reading
|
||||
parameters from a config file is swapping the two — the constructor
|
||||
returns `Error::InvalidPeriod` rather than silently producing an
|
||||
inverted MACD line.
|
||||
- **Don't slice a single column out of a warmup row.** During the first
|
||||
`slow + signal − 1` inputs every field is `NaN` (Python) or absent
|
||||
(`None` in Rust / `null` in Node). Filter by checking `macd` for
|
||||
finiteness before reading `signal` or `histogram`.
|
||||
|
||||
## References
|
||||
|
||||
- Gerald Appel, *Technical Analysis: Power Tools for Active Investors*,
|
||||
Financial Times Prentice Hall, 2005 — the canonical modern treatment
|
||||
of the MACD line/signal-line/histogram trio Appel popularised in the
|
||||
late 1970s.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: Rsi](../momentum-oscillators/Indicator-Rsi.md) — bounded sibling oscillator, useful
|
||||
as a confirmation filter on top of MACD signals.
|
||||
- [Indicator: Trix](../trend-directional/Indicator-Trix.md) — another EMA-based momentum
|
||||
oscillator (triple-smoothed rate of change).
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — table including the `slow +
|
||||
signal − 1` rule.
|
||||
- [Quickstart: Python](../../Quickstart-Python.md) — MACD multi-column NaN
|
||||
pattern explained.
|
||||
@@ -1,172 +0,0 @@
|
||||
# MassIndex
|
||||
|
||||
> Mass Index — Donald Dorsey's range-expansion indicator: it watches the
|
||||
> high–low range widen and contract to anticipate reversals.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `> 0`, oscillates around `sum_period` |
|
||||
| Default parameters | `(ema_period = 9, sum_period = 25)` (Python) |
|
||||
| Warmup period | `2·ema_period + sum_period − 2` |
|
||||
| Interpretation | A rise above `27` then fall below `26.5` flags a reversal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
range_t = high_t − low_t
|
||||
single_t = EMA(range, ema_period)_t
|
||||
double_t = EMA(single, ema_period)_t
|
||||
ratio_t = single_t / double_t
|
||||
MassIndex = Σ ratio over sum_period
|
||||
```
|
||||
|
||||
The Mass Index ignores direction entirely — it tracks **volatility shape**.
|
||||
When the high–low range widens, the single EMA pulls ahead of the double
|
||||
EMA, the ratio climbs above `1`, and the windowed sum rises. Dorsey's
|
||||
"reversal bulge" is the classic pattern: the Mass Index rising above `27`
|
||||
and then falling back below `26.5` warns that a range expansion is about
|
||||
to resolve — often into a trend reversal.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------------|---------|---------------|-------------|-------------|
|
||||
| `ema_period` | `usize` | `9` (Python) | `>= 1` | Period of both EMAs in the cascade. `0` errors with `Error::PeriodZero`. |
|
||||
| `sum_period` | `usize` | `25` (Python) | `>= 1` | Length of the summation window. |
|
||||
|
||||
The Python binding defaults the pair to `(9, 25)`. The `periods` property
|
||||
returns `(ema_period, sum_period)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/mass_index.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for MassIndex {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`MassIndex` is a **candle-input** indicator: it reads `high` and `low`. In
|
||||
Python the streaming `update` accepts a 6-tuple or a dict; the batch
|
||||
helper takes `high` and `low` numpy arrays. Node and WASM expose
|
||||
`update(high, low)` and `batch(high, low)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == 2·ema_period + sum_period − 2`. The first EMA seeds at
|
||||
input `ema_period`; the second EMA, stacked on it, seeds at
|
||||
`2·ema_period − 1`; the summation window then needs `sum_period` ratios.
|
||||
For the default `(9, 25)` that is `41` bars.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant range.** When every bar has the same high–low range, both
|
||||
EMAs converge to the same value, every ratio is `1`, and the Mass Index
|
||||
equals `sum_period` (`constant_range_sums_to_sum_period` pins this).
|
||||
- **Zero-range market.** A flat market (`high == low`) drives both EMAs to
|
||||
`0`; the `0 / 0` is guarded with the neutral ratio `1`, so the Mass
|
||||
Index again equals `sum_period`
|
||||
(`zero_range_market_sums_to_sum_period` pins this).
|
||||
- **Candle validation.** `Candle::new` rejects invalid bars upstream.
|
||||
- **Reset.** `mi.reset()` clears both EMAs, the window and the sum.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, MassIndex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut mi = MassIndex::new(3, 4)?;
|
||||
// Constant high-low range of 2.0; the Mass Index settles at sum_period.
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let mid = 100.0 + f64::from(i);
|
||||
Candle::new(mid, mid + 1.0, mid - 1.0, mid, 1.0, i64::from(i)).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let out = mi.batch(&candles);
|
||||
println!("warmup_period = {}", mi.warmup_period());
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 8
|
||||
last = Some(4.0)
|
||||
```
|
||||
|
||||
A constant range makes every ratio `1`, so the sum equals `sum_period`
|
||||
(`4`). This matches the `constant_range_sums_to_sum_period` test in
|
||||
`crates/wickra-core/src/indicators/mass_index.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
mi = ta.MassIndex() # (ema_period=9, sum_period=25)
|
||||
mid = np.arange(100.0, 160.0)
|
||||
high = mid + 1.0
|
||||
low = mid - 1.0
|
||||
print(mi.batch(high, low)[-1]) # constant range -> 25
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
25.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const mi = new ta.MassIndex(9, 25);
|
||||
const mid = Array.from({ length: 60 }, (_, i) => 100 + i);
|
||||
const high = mid.map((m) => m + 1);
|
||||
const low = mid.map((m) => m - 1);
|
||||
console.log(mi.batch(high, low).at(-1)); // 25
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`MassIndex` is a *reversal-warning* tool, not a direction tool — it never
|
||||
tells you which way price will go, only that a turn is likely. The textbook
|
||||
use is the "reversal bulge" on the default `(9, 25)` settings: watch for
|
||||
the index to push above `27`, then act when it drops back under `26.5`,
|
||||
using a directional indicator (a moving average, ADX) to pick the side.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting a direction.** The Mass Index is direction-blind; always
|
||||
pair it with a trend indicator.
|
||||
- **Feeding it scalar prices.** It needs `high`/`low`; it takes a
|
||||
`Candle`, not an `f64`.
|
||||
|
||||
## References
|
||||
|
||||
Donald Dorsey, "The Mass Index", *Technical Analysis of Stocks &
|
||||
Commodities* (1992). The double-EMA-of-range construction and the `(9,
|
||||
25)` defaults follow Dorsey's original.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — directional-free
|
||||
volatility in price units.
|
||||
- [Indicator-BollingerBands.md](../volatility-bands/Indicator-BollingerBands.md)
|
||||
— another range-expansion lens.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,186 +0,0 @@
|
||||
# TRIX
|
||||
|
||||
> Triple-EMA percent rate of change — applies three EMAs in sequence to
|
||||
> smooth out short-term noise, then reports the one-bar percent change
|
||||
> of the resulting series.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `f64` (close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (typically a few percent, centred on 0) |
|
||||
| Default parameters | none — `period` is required in every binding |
|
||||
| Warmup period | `3 · period − 1` (44 for `period = 15`) |
|
||||
| Interpretation | zero-line crossings as trend-change cues; magnitude as momentum |
|
||||
|
||||
## Formula
|
||||
|
||||
Let `EMA_n(·)` denote Wickra's EMA over `n` periods (seeded from the
|
||||
simple mean of the first `n` inputs, then recursive with `α = 2/(n+1)`).
|
||||
For each input close, build a triple-smoothed series:
|
||||
|
||||
```
|
||||
TR_t = EMA_period( EMA_period( EMA_period( close ) ) )_t
|
||||
```
|
||||
|
||||
Then TRIX is the one-bar percent rate of change of `TR`:
|
||||
|
||||
```
|
||||
TRIX_t = 100 · (TR_t − TR_{t-1}) / TR_{t-1}
|
||||
```
|
||||
|
||||
When `TR_{t-1} == 0` exactly, the implementation returns `0.0` rather
|
||||
than dividing by zero.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|------|------|---------|-------------|-------------|
|
||||
| `period` | `usize` | required | `>= 1` | Period shared by all three EMAs. |
|
||||
|
||||
`Trix::new(0)` returns `Error::PeriodZero` (via the inner `Ema::new`).
|
||||
The Python and Node bindings expose no default for `period`; you must
|
||||
pass it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `impl Indicator for Trix`:
|
||||
|
||||
```rust
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
fn update(&mut self, input: f64) -> Option<f64>;
|
||||
```
|
||||
|
||||
Python's `TRIX.batch(prices)` returns a 1-D `float64` `np.ndarray`
|
||||
(warmup → `NaN`). Node's `TRIX.batch(prices)` returns a flat
|
||||
`number[]` (warmup → `NaN`). Both also expose streaming `update(price)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` returns `3 · period − 1`. Three stacked EMAs of the
|
||||
same period seed at input `3 · period − 2`; once `TR` exists, TRIX
|
||||
itself needs one more input to form the `TR_t − TR_{t-1}` difference,
|
||||
which lands at input `3 · period − 1`. For `period = 15` this is
|
||||
`3 · 15 − 1 = 44`, verified above.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant input.** All three EMAs converge to the constant value, so
|
||||
`TR_t − TR_{t-1} == 0` and TRIX returns `0` (test
|
||||
`constant_series_yields_zero_trix`).
|
||||
- **`TR_{t-1} == 0`.** The implementation returns `0` rather than
|
||||
producing `NaN` / `±∞`. This is the `Some(_)` branch with `prev !=
|
||||
0.0`-failed in `Trix::update`.
|
||||
- **Reset.** `reset()` resets all three EMAs and clears `prev_tr`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, Trix};
|
||||
|
||||
let prices: Vec<f64> = (1..=50).map(|i| i as f64).collect();
|
||||
let mut trix = Trix::new(15)?;
|
||||
let out = trix.batch(&prices);
|
||||
println!("row 43 = {}", out[43].unwrap());
|
||||
println!("row 49 = {}", out[49].unwrap());
|
||||
# Ok::<(), wickra::Error>(())
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
row 43 = 4.545454545454546
|
||||
row 49 = 3.5714285714285716
|
||||
```
|
||||
|
||||
(The series decays toward zero as a ramp gets longer because the
|
||||
percent change of an arithmetic ramp shrinks as the level grows.)
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import wickra as ta
|
||||
|
||||
trix = ta.TRIX(15)
|
||||
print('warmup:', trix.warmup_period())
|
||||
vals = []
|
||||
for i in range(1, 51):
|
||||
vals.append(trix.update(float(i)))
|
||||
print('vals[43]:', vals[43])
|
||||
print('vals[49]:', vals[49])
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 44
|
||||
vals[43]: 4.545454545454546
|
||||
vals[49]: 3.5714285714285716
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const wickra = require('wickra');
|
||||
|
||||
const trix = new wickra.TRIX(15);
|
||||
console.log('warmup:', trix.warmupPeriod());
|
||||
const vals = [];
|
||||
for (let i = 1; i <= 50; i++) vals.push(trix.update(i));
|
||||
console.log('vals[43]:', vals[43]);
|
||||
console.log('vals[49]:', vals[49]);
|
||||
```
|
||||
|
||||
Verified output:
|
||||
|
||||
```
|
||||
warmup: 44
|
||||
vals[43]: 4.545454545454546
|
||||
vals[49]: 3.5714285714285716
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Zero-line cross.** TRIX crossing above zero suggests the
|
||||
triple-smoothed trend is turning up; crossing below, turning down.
|
||||
Because of the triple smoothing, these crosses are deliberately
|
||||
late and deliberately stable.
|
||||
- **Magnitude.** A larger absolute TRIX value means the smoothed series
|
||||
is changing faster per bar. There is no canonical "overbought" band
|
||||
— TRIX is interpreted by its sign and slope, not by threshold.
|
||||
- **Compare to MACD.** Both are EMA-based momentum oscillators on a
|
||||
zero-centred scale. MACD reacts faster (two EMAs, one diff); TRIX
|
||||
reacts slower (three EMAs, one rate of change), making it a
|
||||
cleaner long-horizon trend filter.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Long warmup.** `3 · period − 1` is one of the largest warmups in
|
||||
the library (44 for the canonical `period = 15`). Sizing your input
|
||||
buffer to `period` and expecting values immediately will hand you
|
||||
`None` / `NaN` for a full 44 bars.
|
||||
- **Triple smoothing kills small wiggles.** TRIX deliberately ignores
|
||||
short-term noise. Do not use it for entry-timing inside a fast
|
||||
oscillator strategy; use it as a long-term trend filter on top of a
|
||||
faster signal.
|
||||
|
||||
## References
|
||||
|
||||
- Jack Hutson, "Good TRIX", *Technical Analysis of Stocks &
|
||||
Commodities*, July 1983 — the original publication popularising the
|
||||
triple-EMA rate-of-change oscillator.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator: MacdIndicator](../trend-directional/Indicator-MacdIndicator.md) — faster
|
||||
EMA-based momentum oscillator, useful as a confirmation against
|
||||
TRIX zero-line crosses.
|
||||
- [Indicator: Roc](../momentum-oscillators/Indicator-Roc.md) — the raw, one-stage rate of
|
||||
change TRIX is built on top of.
|
||||
- [Warmup Periods](../../Warmup-Periods.md) — `3 · period − 1` entry.
|
||||
@@ -1,140 +0,0 @@
|
||||
# VerticalHorizontalFilter
|
||||
|
||||
> Vertical Horizontal Filter (VHF) — net distance covered divided by total
|
||||
> distance walked; a trend-versus-range gauge.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `f64` (close price) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, 1]` |
|
||||
| Default parameters | `period = 28` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Near `1` = trending, near `0` = choppy. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
VHF = (highest_close(n) − lowest_close(n)) / Σ|close − close_prev|(n)
|
||||
```
|
||||
|
||||
The numerator is the *net* distance price covered over the window; the
|
||||
denominator is the *total* distance it walked. Their ratio lives in `[0, 1]`:
|
||||
a clean trend walks almost only in its net direction, so `VHF` approaches `1`;
|
||||
a choppy market doubles back constantly, inflating the denominator and pushing
|
||||
`VHF` toward `0`. It answers the same question as the
|
||||
[`ChoppinessIndex`](../trend-directional/Indicator-ChoppinessIndex.md) on an inverted scale.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the lookback window. The Python binding defaults it to `28`; the
|
||||
Rust and Node constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/vertical_horizontal_filter.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for VerticalHorizontalFilter {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`VerticalHorizontalFilter` is a **scalar** indicator: it consumes one `f64`
|
||||
close per step. Because `Input = f64` it can sit inside a
|
||||
[`Chain`](../../Indicator-Chaining.md).
|
||||
|
||||
## Warmup
|
||||
|
||||
`VerticalHorizontalFilter::new(28).warmup_period() == 29`. The high/low window
|
||||
fills at `period` closes, but the `period`-th difference needs one extra input
|
||||
because the first close has nothing to diff against.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat series.** A window that walked nowhere has a zero denominator; `VHF`
|
||||
is defined as `0`.
|
||||
- **Pure trend.** A series rising by a fixed step reads `(period − 1) / period`.
|
||||
- **Choppy series.** An oscillating series reads near `0`.
|
||||
- **Reset.** `vhf.reset()` clears the close and difference windows.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, VerticalHorizontalFilter};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut vhf = VerticalHorizontalFilter::new(5)?;
|
||||
// Closes 1..6: each diff is 1 (Σ = 5), the 5-close span is 4 -> 4/5.
|
||||
let out = vhf.batch(&[1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, None, None, None, Some(0.8)]
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
vhf = ta.VerticalHorizontalFilter(5)
|
||||
print(vhf.batch(np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan nan nan nan 0.8]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const vhf = new ta.VerticalHorizontalFilter(5);
|
||||
console.log(vhf.batch([1, 2, 3, 4, 5, 6]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, NaN, NaN, NaN, 0.8 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Use the VHF as a regime filter: a high, rising VHF says a trend is in force —
|
||||
favour trend-following entries; a low VHF says price is ranging — favour
|
||||
mean-reversion. A VHF turning down from a high level is an early hint the
|
||||
trend is losing its grip.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting a direction.** Like the Choppiness Index it is non-directional —
|
||||
pair it with a trend indicator.
|
||||
- **Reading a single bar.** It is a regime gauge; read its level and slope.
|
||||
|
||||
## References
|
||||
|
||||
Adam White's Vertical Horizontal Filter; the net-over-total formulation here
|
||||
is the standard one.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-ChoppinessIndex.md](../trend-directional/Indicator-ChoppinessIndex.md) — the same
|
||||
trending-vs-ranging question on an inverted `[0, 100]` scale.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,159 +0,0 @@
|
||||
# Vortex
|
||||
|
||||
> Vortex Indicator — a pair of oscillators (`VI+`, `VI−`) whose crossings
|
||||
> identify the start of a new trend.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Trend & Directional |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `VortexOutput { plus, minus }` |
|
||||
| Output range | each line `>= 0`, typically around `1.0` |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | `VI+` above `VI−` = up-trend; the cross marks the turn. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
VM+_t = |high_t − low_{t−1}| (positive vortex movement)
|
||||
VM−_t = |low_t − high_{t−1}| (negative vortex movement)
|
||||
TR_t = true range
|
||||
VI+ = Σ VM+ over period / Σ TR over period
|
||||
VI− = Σ VM− over period / Σ TR over period
|
||||
```
|
||||
|
||||
Each vortex movement measures how far this bar reached against the
|
||||
*opposite* extreme of the previous bar; dividing the running sums by the
|
||||
running true range normalises both lines to a comparable scale around
|
||||
`1.0`. `VI+` crossing above `VI−` signals a new up-trend; the reverse, a
|
||||
down-trend.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` (Python) | `>= 1` | Summation window. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `14`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/vortex.rs`:
|
||||
|
||||
```rust
|
||||
pub struct VortexOutput { pub plus: f64, pub minus: f64 }
|
||||
|
||||
impl Indicator for Vortex {
|
||||
type Input = Candle;
|
||||
type Output = VortexOutput;
|
||||
}
|
||||
```
|
||||
|
||||
`Vortex` is a **candle-input** indicator reading `high`, `low` and
|
||||
`close`. The streaming `update` returns `VortexOutput` (Rust),
|
||||
`(plus, minus)` (Python), or `{ plus, minus }` (Node/WASM). The batch
|
||||
helper returns one row per input — a `(n, 2)` numpy array in Python, a
|
||||
flat `[plus, minus, …]` array of length `2·n` in Node/WASM, with `NaN`
|
||||
during warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Vortex::new(period).warmup_period() == period + 1`. The first VM/TR
|
||||
triple needs a previous bar, so it forms on bar 2; the summation window
|
||||
then needs `period` triples — the first output lands on input
|
||||
`period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A window with zero total true range cannot be
|
||||
normalised; both lines are reported as `0.0`
|
||||
(`perfectly_flat_market_yields_zero` pins this).
|
||||
- **Non-negative.** Both `VI+` and `VI−` are sums of absolute values over
|
||||
a non-negative range, so neither is ever negative
|
||||
(`outputs_are_non_negative` pins this).
|
||||
- **Candle validation.** `Candle::new` rejects invalid bars upstream.
|
||||
- **Reset.** `vortex.reset()` clears the previous bar, the window and the
|
||||
three running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Vortex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let candles = [
|
||||
Candle::new(9.0, 10.0, 8.0, 9.0, 1.0, 0)?,
|
||||
Candle::new(10.0, 12.0, 9.0, 11.0, 1.0, 1)?,
|
||||
Candle::new(12.0, 13.0, 11.0, 12.0, 1.0, 2)?,
|
||||
];
|
||||
let mut v = Vortex::new(2)?;
|
||||
let out = v.batch(&candles);
|
||||
println!("{:?}", out[2]);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(VortexOutput { plus: 1.6, minus: 0.4 })
|
||||
```
|
||||
|
||||
Over the two formed bars `Σ VM+ = 8`, `Σ VM− = 2`, `Σ TR = 5`, giving
|
||||
`VI+ = 1.6` and `VI− = 0.4`. This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/vortex.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
v = ta.Vortex(14)
|
||||
high = np.array([10.0, 12.0, 13.0])
|
||||
low = np.array([8.0, 9.0, 11.0])
|
||||
close = np.array([9.0, 11.0, 12.0])
|
||||
# v.batch(high, low, close) -> (3, 2) array of [plus, minus], NaN during warmup
|
||||
print(v.update((9.0, 10.0, 8.0, 9.0, 1.0, 0)))
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const v = new ta.Vortex(14);
|
||||
console.log(v.update(12, 9, 11)); // { plus, minus } or null during warmup
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Vortex` is a trend-onset detector. The signal is the **crossing**: when
|
||||
`VI+` rises above `VI−`, a new up-trend is starting; when `VI−` rises
|
||||
above `VI+`, a down-trend. The gap between the lines measures conviction —
|
||||
a wide, widening gap is a strong trend, converging lines warn of a stall.
|
||||
Unlike a lagging moving-average cross, the vortex movements react to the
|
||||
*reach* of each bar, so the cross tends to fire early.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading the lines in isolation.** A `VI+` of `1.1` means nothing on
|
||||
its own — what matters is its position relative to `VI−`.
|
||||
- **Feeding it scalar prices.** It needs `high`/`low`/`close`.
|
||||
|
||||
## References
|
||||
|
||||
Etienne Botes and Douglas Siepman, "The Vortex Indicator", *Technical
|
||||
Analysis of Stocks & Commodities* (2010). The `VM±` / true-range
|
||||
definition here follows their original.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Adx.md](../trend-directional/Indicator-Adx.md) — Wilder's directional system.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the true range
|
||||
Vortex normalises against.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,225 +0,0 @@
|
||||
# ATR (Average True Range)
|
||||
|
||||
> Wilder's volatility benchmark: an exponentially-smoothed average of the
|
||||
> per-bar true range that absorbs overnight gaps and is dimensioned in price
|
||||
> units.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Item | Value |
|
||||
|---------------------|--------------------------------------------------------------------------------------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded `≥ 0` |
|
||||
| Default parameters | `period = 14` (Wilder) |
|
||||
| Warmup period | `period` (14 for defaults) |
|
||||
| Interpretation | dollar-denominated volatility scale; rises in chop and expansion |
|
||||
|
||||
## Formula
|
||||
|
||||
For each candle, the **true range** is
|
||||
`TR_t = max(H_t - L_t, |H_t - C_{t-1}|, |L_t - C_{t-1}|)` when a previous
|
||||
close is available, otherwise `TR_t = H_t - L_t` (see `Candle::true_range`
|
||||
in `crates/wickra-core/src/ohlcv.rs`).
|
||||
|
||||
ATR is then Wilder-smoothed:
|
||||
|
||||
```
|
||||
seed_ATR_period = (TR_1 + TR_2 + … + TR_period) / period
|
||||
ATR_t = ((period - 1) * ATR_{t-1} + TR_t) / period for t > period
|
||||
```
|
||||
|
||||
This is mathematically the same recursion as an EMA with `alpha = 1/period`
|
||||
(Wilder smoothing), seeded with a simple mean of the first `period` true
|
||||
ranges (`crates/wickra-core/src/indicators/atr.rs:58-69`).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Constraint | Source |
|
||||
|----------|---------|---------|------------|-------------------------------------|
|
||||
| `period` | `usize` | `14` | `> 0` | `Atr::new` (`atr.rs:26`) |
|
||||
|
||||
Python default from `#[pyo3(signature = (period=14))]` in
|
||||
`bindings/python/src/lib.rs`. `period == 0` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
```rust
|
||||
impl Indicator for Atr {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
fn update(&mut self, candle: Candle) -> Option<f64>;
|
||||
fn warmup_period(&self) -> usize { self.period }
|
||||
}
|
||||
```
|
||||
|
||||
- **Rust input.** A full `Candle` struct; only `high`, `low`, and `close`
|
||||
are read (`prev_close` is cached internally between calls).
|
||||
- **Python streaming.** Accepts either a 6-tuple
|
||||
`(open, high, low, close, volume, timestamp)` or a dict with keys
|
||||
`open`, `high`, `low`, `close`, `volume`, and optional `timestamp`.
|
||||
- **Python batch.** `ATR.batch(high, low, close)` takes three equal-length
|
||||
`numpy.ndarray` columns and returns a 1-D `np.ndarray` with `NaN` for
|
||||
every warmup row.
|
||||
- **Node streaming.** `atr.update(high, low, close)` returns `number | null`.
|
||||
- **Node batch.** `atr.batch(high, low, close)` returns `Array<number>` of
|
||||
the same length, `NaN` during warmup.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == period`. The first `period - 1` candles return `None`
|
||||
(or `NaN`/`null` in batch); the `period`-th candle returns the seed value
|
||||
`(TR_1 + … + TR_period) / period`. Each subsequent candle applies the
|
||||
Wilder recursion.
|
||||
|
||||
Verified for `period = 3`: the first non-`None` output is at index `2`
|
||||
(the 3rd candle).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **First candle.** `Candle::true_range(None)` falls back to `high - low`
|
||||
because there is no previous close yet. The first TR is the bar range.
|
||||
- **Gaps.** With a previous close at `5.0` and a candle of `H=10, L=9`,
|
||||
`TR = max(1, 5, 4) = 5` — i.e. `|H - prev_close|` dominates. The
|
||||
pinned test `gap_up_uses_high_minus_prev_close` covers exactly this.
|
||||
- **Constant input.** A series of identical candles (no gaps, fixed range)
|
||||
yields a constant ATR equal to the bar range, even before the seed is
|
||||
complete — the smoothing has nothing to smooth.
|
||||
- **Non-negativity.** ATR is always `≥ 0`. The Rust test `never_negative`
|
||||
pins this property across a sinusoidal price series.
|
||||
- **NaN / infinity.** `Candle::new` rejects non-finite `open`/`high`/
|
||||
`low`/`close`/`volume`; constructing the candle returns
|
||||
`Error::InvalidCandle` before it can ever reach ATR.
|
||||
- **Reset.** `reset()` clears `prev_close`, the seed buffer, and the
|
||||
running average; the next call behaves as if the indicator were
|
||||
freshly constructed.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{Atr, BatchExt, Candle, Indicator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let candles = vec![
|
||||
Candle::new(10.0, 11.0, 9.0, 10.5, 1.0, 0)?,
|
||||
Candle::new(10.5, 12.0, 10.0, 11.5, 1.0, 0)?,
|
||||
Candle::new(11.5, 13.0, 11.0, 12.5, 1.0, 0)?,
|
||||
Candle::new(12.5, 14.0, 12.0, 13.5, 1.0, 0)?,
|
||||
Candle::new(13.5, 15.0, 13.0, 14.5, 1.0, 0)?,
|
||||
];
|
||||
let mut atr = Atr::new(3)?;
|
||||
println!("{:?}", atr.batch(&candles));
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(2.0), Some(2.0), Some(2.0)]
|
||||
```
|
||||
|
||||
Every bar has range `2.0` and no gap-driven TR component, so both the
|
||||
seed `(2 + 2 + 2) / 3 = 2.0` and every subsequent Wilder update stay at
|
||||
`2.0`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
atr = ta.ATR(3)
|
||||
high = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
|
||||
low = np.array([ 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
close = np.array([10.5, 11.5, 12.5, 13.5, 14.5])
|
||||
print(atr.batch(high, low, close))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan nan 2. 2. 2.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```js
|
||||
const w = require('wickra');
|
||||
|
||||
const atr = new w.ATR(3);
|
||||
console.log(atr.batch(
|
||||
[11, 12, 13, 14, 15],
|
||||
[ 9, 10, 11, 12, 13],
|
||||
[10.5, 11.5, 12.5, 13.5, 14.5],
|
||||
));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 2, 2, 2 ]
|
||||
```
|
||||
|
||||
Streaming form (`atr.update(high, low, close)`):
|
||||
|
||||
```js
|
||||
const w = require('wickra');
|
||||
|
||||
const atr = new w.ATR(3);
|
||||
console.log(atr.update(11, 9, 10.5));
|
||||
console.log(atr.update(12, 10, 11.5));
|
||||
console.log(atr.update(13, 11, 12.5));
|
||||
console.log(atr.update(14, 12, 13.5));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
null
|
||||
null
|
||||
2
|
||||
2
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Stop sizing.** A common pattern is "place a stop `k * ATR` away from
|
||||
entry," with `k` typically in `[1.5, 3.0]` depending on the timeframe.
|
||||
ATR's units are price, so the stop distance is directly tradable.
|
||||
- **Position sizing.** `risk_per_trade / ATR` gives a quantity that
|
||||
normalises risk across assets of very different price levels.
|
||||
- **Regime detection.** Persistently rising ATR signals an expansion
|
||||
regime; persistently low ATR signals consolidation, often preceding
|
||||
expansion (the volatility-of-volatility argument).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Wilder smoothing vs EMA.** Wilder's smoothing factor is `1/period`,
|
||||
not the EMA's `2/(period+1)`. They look similar but produce different
|
||||
numbers; a 14-period Wilder ATR is **not** the same as a 14-period
|
||||
EMA of true range. Wickra uses the Wilder recursion explicitly.
|
||||
- **Off-by-one seeding.** ATR(14) emits its first value on the 14th
|
||||
candle, not the 15th — unlike RSI(14) which needs 15 candles for 14
|
||||
diffs. The difference is that ATR's seed uses `period` true ranges
|
||||
directly (and `TR_1` is well-defined even without a previous close),
|
||||
while RSI(14) needs 14 *differences* between consecutive closes.
|
||||
|
||||
## References
|
||||
|
||||
- J. Welles Wilder Jr., *New Concepts in Technical Trading Systems*,
|
||||
Trend Research, 1978. Chapter on the Average True Range and the
|
||||
Wilder smoothing constant.
|
||||
|
||||
## See also
|
||||
|
||||
- [Bollinger Bands](../volatility-bands/Indicator-BollingerBands.md) — stddev-based volatility
|
||||
envelope around an SMA.
|
||||
- [Keltner Channels](../volatility-bands/Indicator-Keltner.md) — directly composes EMA + ATR.
|
||||
- [Donchian Channels](../volatility-bands/Indicator-Donchian.md) — rolling high/low without
|
||||
any smoothing.
|
||||
- [PSAR](../trailing-stops/Indicator-Psar.md) — uses ATR-like volatility tracking implicitly
|
||||
through its acceleration factor.
|
||||
@@ -1,261 +0,0 @@
|
||||
# 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`). On
|
||||
long-running streams the running `Σ x` and `Σ x²` are reseeded from the live
|
||||
window every `16 · period` updates — amortised O(1), bounds the cancellation
|
||||
drift to roughly `16 · period · ULP · max(|x|²)` (sub-picodollar on
|
||||
real-world price scales).
|
||||
|
||||
## 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.
|
||||
@@ -1,155 +0,0 @@
|
||||
# BollingerBandwidth
|
||||
|
||||
> Bollinger Bandwidth — the width of the Bollinger Bands relative to the
|
||||
> middle band: a normalised volatility reading.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` |
|
||||
| Default parameters | `(period = 20, multiplier = 2.0)` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Band width as a fraction of price; lows flag a "squeeze". |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
Bandwidth = (upper − lower) / middle
|
||||
```
|
||||
|
||||
where `upper`, `middle` and `lower` come from
|
||||
[`BollingerBands`](../volatility-bands/Indicator-BollingerBands.md). Since the bands are
|
||||
`middle ± multiplier · stddev`, the bandwidth simplifies to
|
||||
`2 · multiplier · stddev / middle` — volatility normalised by price level.
|
||||
Its extremes name two classic patterns: the **squeeze** (bandwidth at a
|
||||
multi-month low — a coiled, quiet market that often precedes a sharp
|
||||
move) and the **bulge** (bandwidth at an extreme high — an exhausted,
|
||||
over-extended move).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------------|---------|----------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` (Python) | `>= 1` | Bollinger Bands period. `0` errors with `Error::PeriodZero`. |
|
||||
| `multiplier` | `f64` | `2.0` (Python) | `> 0` | Band standard-deviation multiplier. `<= 0` errors with `Error::NonPositiveMultiplier`. |
|
||||
|
||||
The Python binding defaults the pair to `(20, 2.0)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/bollinger_bandwidth.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for BollingerBandwidth {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == period` — identical to the underlying `BollingerBands`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Flat prices collapse the bands onto the middle, so
|
||||
the width — and bandwidth — is `0.0` (`constant_series_yields_zero`
|
||||
pins this).
|
||||
- **Zero middle band.** Bandwidth is undefined against a `0.0` middle
|
||||
band; the indicator reports `0.0` for that bar.
|
||||
- **Non-negative.** Bandwidth is `(upper − lower) / middle` with
|
||||
`upper >= lower` and a positive middle band, so it is never negative
|
||||
(`output_is_non_negative` pins this).
|
||||
- **Reset.** `bbw.reset()` clears the underlying bands.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, BollingerBandwidth};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut bbw = BollingerBandwidth::new(20, 2.0)?;
|
||||
// A flat stretch then a volatile stretch: bandwidth rises.
|
||||
let mut prices: Vec<f64> = vec![100.0; 30];
|
||||
prices.extend((0..30).map(|i| 100.0 + (f64::from(i)).sin() * 10.0));
|
||||
let out = bbw.batch(&prices);
|
||||
println!("flat-window bandwidth: {:?}", out[25]);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
flat-window bandwidth: Some(0.0)
|
||||
```
|
||||
|
||||
While prices are flat the bands sit on top of each other, so bandwidth is
|
||||
`0`; once volatility arrives it climbs.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
bbw = ta.BollingerBandwidth(20, 2.0)
|
||||
prices = np.full(40, 100.0) # flat series
|
||||
print(bbw.batch(prices)[-1]) # 0.0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const bbw = new ta.BollingerBandwidth(20, 2.0);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + Math.sin(i * 0.3) * 6);
|
||||
console.log('warmupPeriod:', bbw.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`BollingerBandwidth` is the standard way to quantify the Bollinger
|
||||
"squeeze". Volatility is mean-reverting and cyclical: extended periods of
|
||||
low bandwidth tend to be followed by expansion, and vice versa. Traders
|
||||
watch for bandwidth dropping to a multi-month low (the squeeze) as a
|
||||
heads-up that a directional move is loading — then take the direction
|
||||
from price breaking the band, or from a separate trend indicator.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Treating the squeeze as directional.** Low bandwidth says a move is
|
||||
*coming*, not which way. Confirm direction separately.
|
||||
- **Comparing raw bandwidth across instruments without context.** It is
|
||||
normalised by price, which helps, but "low" is relative to each
|
||||
instrument's own history — compare against its own range.
|
||||
|
||||
## References
|
||||
|
||||
John Bollinger, *Bollinger on Bollinger Bands* (2001). Bandwidth is one
|
||||
of Bollinger's two derived indicators (with %b).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-BollingerBands.md](../volatility-bands/Indicator-BollingerBands.md) — the bands
|
||||
this measures.
|
||||
- [Indicator-PercentB.md](../volatility-bands/Indicator-PercentB.md) — the companion derived
|
||||
indicator: price *position* within the bands.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,154 +0,0 @@
|
||||
# ChaikinVolatility
|
||||
|
||||
> Chaikin Volatility — the rate of change of a smoothed high-low spread;
|
||||
> is the trading range widening or narrowing?
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high`, `low`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (percent) |
|
||||
| Default parameters | `ema_period = 10`, `roc_period = 10` (Python) |
|
||||
| Warmup period | `ema_period + roc_period` |
|
||||
| Interpretation | Positive = ranges expanding, negative = ranges contracting. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
spread_t = high_t − low_t
|
||||
smoothed_t = EMA(spread, ema_period)_t
|
||||
ChaikinVol = 100 · (smoothed_t − smoothed_{t−roc_period}) / smoothed_{t−roc_period}
|
||||
```
|
||||
|
||||
Marc Chaikin's volatility measure tracks not the *level* of the trading range
|
||||
but how fast it is *widening or narrowing*. The bar's high-low spread is
|
||||
EMA-smoothed, then run through a rate-of-change: a rising value means ranges
|
||||
are expanding (often near a market top, as fear spikes), a falling value means
|
||||
they are contracting (a quiet, complacent market). The classic configuration
|
||||
smooths the spread with a `10`-period EMA and takes its `10`-period rate of
|
||||
change.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `ema_period` — the EMA that smooths the high-low spread (`10`).
|
||||
- `roc_period` — the rate-of-change lookback over the smoothed spread (`10`).
|
||||
|
||||
`ChaikinVolatility::classic()` returns the `(10, 10)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/chaikin_volatility.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChaikinVolatility {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ChaikinVolatility` is a **candle-input** indicator that reads `high` and
|
||||
`low`. Python's streaming `update` accepts a 6-tuple or a dict; the batch
|
||||
helper takes `high`, `low` numpy arrays. Node and WASM expose
|
||||
`update(high, low)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChaikinVolatility::classic().warmup_period() == 20`. The EMA emits at candle
|
||||
`ema_period`; the rate-of-change then needs `roc_period` more smoothed values.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant range.** A constant high-low spread smooths to a constant EMA,
|
||||
whose rate of change is `0`.
|
||||
- **Expanding range.** A monotonically widening range reads positive.
|
||||
- **Reset.** `cv.reset()` clears the inner EMA and ROC.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChaikinVolatility};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cv = ChaikinVolatility::new(10, 10)?;
|
||||
// A constant 2-wide range -> constant EMA -> zero rate of change.
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let base = 100.0 + f64::from(i);
|
||||
Candle::new(base, base + 1.0, base - 1.0, base, 1.0, i).unwrap()
|
||||
})
|
||||
.collect();
|
||||
println!("{:?}", cv.batch(&candles).last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.0)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
cv = ta.ChaikinVolatility(10, 10)
|
||||
n = 40
|
||||
base = np.arange(n, dtype=float) + 100.0
|
||||
print(cv.batch(base + 1.0, base - 1.0)[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const cv = new ta.ChaikinVolatility(10, 10);
|
||||
const base = Array.from({ length: 40 }, (_, i) => 100 + i);
|
||||
const out = cv.batch(base.map((b) => b + 1), base.map((b) => b - 1));
|
||||
console.log(out[out.length - 1]);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
A rising Chaikin Volatility warns that ranges are expanding fast — Chaikin
|
||||
associated sharp rises with market tops, where panic widens bars. A low or
|
||||
falling reading is the calm, range-contracting market that often precedes a
|
||||
move. It complements [`Atr`](../volatility-bands/Indicator-Atr.md): ATR gives the level of
|
||||
volatility, Chaikin Volatility gives its momentum.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading it as a volatility level.** It is a *rate of change* — zero means
|
||||
steady ranges, not zero volatility.
|
||||
- **Feeding it scalar prices.** It needs the `high`/`low` bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Chaikin Volatility; the EMA-of-spread rate-of-change definition
|
||||
here is the standard one.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the level of per-bar volatility.
|
||||
- [Indicator-TrueRange.md](../volatility-bands/Indicator-TrueRange.md) — raw single-bar range.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,213 +0,0 @@
|
||||
# Donchian Channels
|
||||
|
||||
> The unsmoothed price-extreme envelope: highest high and lowest low over a
|
||||
> rolling window, with the mid-band defined as their average. Breakouts of
|
||||
> the Donchian channel are the foundation of the Turtle trading rules.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Item | Value |
|
||||
|---------------------|--------------------------------------------------------------------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high` and `low`) |
|
||||
| Output type | `DonchianOutput { upper: f64, middle: f64, lower: f64 }` |
|
||||
| Output range | unbounded; `lower ≤ middle ≤ upper` |
|
||||
| Default parameters | `period = 20` |
|
||||
| Warmup period | `period` (20 for defaults) |
|
||||
| Interpretation | breakout boundary; channel touches are tradable events |
|
||||
|
||||
## Formula
|
||||
|
||||
For a lookback of `period` candles:
|
||||
|
||||
```
|
||||
upper_t = max( high_t, high_{t-1}, …, high_{t-period+1} )
|
||||
lower_t = min( low_t, low_{t-1}, …, low_{t-period+1} )
|
||||
middle_t = (upper_t + lower_t) / 2
|
||||
```
|
||||
|
||||
`crates/wickra-core/src/indicators/donchian.rs:58-72` computes both
|
||||
extrema by folding over the in-window candles each tick; this is O(n)
|
||||
per update in the period size and O(1) in the data length.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Constraint | Source |
|
||||
|----------|---------|---------|------------|-----------------------------------------|
|
||||
| `period` | `usize` | `20` | `> 0` | `Donchian::new` (`donchian.rs:30`) |
|
||||
|
||||
Python default from `#[pyo3(signature = (period=20))]` in
|
||||
`bindings/python/src/lib.rs`. `period == 0` returns `Error::PeriodZero`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
```rust
|
||||
impl Indicator for Donchian {
|
||||
type Input = Candle;
|
||||
type Output = DonchianOutput;
|
||||
fn update(&mut self, candle: Candle) -> Option<DonchianOutput>;
|
||||
}
|
||||
|
||||
pub struct DonchianOutput { pub upper: f64, pub middle: f64, pub lower: f64 }
|
||||
```
|
||||
|
||||
- **Python streaming.** Returns `(upper, middle, lower)` tuple or `None`.
|
||||
- **Python batch.** `Donchian.batch(high, low)` returns a 2-D
|
||||
`np.ndarray` of shape `(n, 3)` with columns `[upper, middle, lower]`;
|
||||
warmup rows are `NaN` across all three columns. (`close` is not
|
||||
required.)
|
||||
- **Node streaming.** Not exposed — the Node binding ships only the
|
||||
`batch` form for `Donchian`.
|
||||
- **Node batch.** `donchian.batch(high, low)` returns a flat
|
||||
`Array<number>` of length `n * 3` interleaved per row:
|
||||
`[u0, m0, l0, u1, m1, l1, …]`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == period`. The first `period - 1` candles return
|
||||
`None`; the `period`-th candle emits the first envelope. Verified for
|
||||
`period = 3`: the first non-`None` output is at index `2` (the 3rd
|
||||
candle).
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market (HH == LL).** When every candle in the window has
|
||||
identical highs and identical lows, `upper == lower` (and therefore
|
||||
`middle == upper == lower`). The pinned test
|
||||
`flat_market_yields_equal_bands` covers this.
|
||||
- **Single extreme candle.** A lone wick at the edge of the window sets
|
||||
the boundary until it scrolls out. Donchian therefore reacts in
|
||||
step-functions, not smoothly — a new all-time high inside the window
|
||||
immediately moves the upper band; a single bar later, that high
|
||||
remains the boundary unless an even higher print occurs.
|
||||
- **NaN / infinity.** `Candle::new` rejects non-finite OHLC values
|
||||
before they can reach Donchian.
|
||||
- **Reset.** `reset()` clears the candle window; the configured
|
||||
`period` is preserved.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Donchian, Indicator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let candles = vec![
|
||||
Candle::new(10.0, 11.0, 9.0, 10.5, 1.0, 0)?,
|
||||
Candle::new(10.5, 12.0, 10.0, 11.5, 1.0, 0)?,
|
||||
Candle::new(11.5, 13.0, 11.0, 12.5, 1.0, 0)?,
|
||||
Candle::new(12.5, 14.0, 12.0, 13.5, 1.0, 0)?,
|
||||
Candle::new(13.5, 15.0, 13.0, 14.5, 1.0, 0)?,
|
||||
];
|
||||
let mut d = Donchian::new(3)?;
|
||||
for (i, v) in d.batch(&candles).into_iter().enumerate() {
|
||||
println!("i={i} -> {:?}", v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
i=0 -> None
|
||||
i=1 -> None
|
||||
i=2 -> Some(DonchianOutput { upper: 13.0, middle: 11.0, lower: 9.0 })
|
||||
i=3 -> Some(DonchianOutput { upper: 14.0, middle: 12.0, lower: 10.0 })
|
||||
i=4 -> Some(DonchianOutput { upper: 15.0, middle: 13.0, lower: 11.0 })
|
||||
```
|
||||
|
||||
At `i = 2` the window contains highs `[11, 12, 13]` and lows `[9, 10, 11]`,
|
||||
so `upper = 13`, `lower = 9`, `middle = 11`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
d = ta.Donchian(3)
|
||||
h = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
|
||||
l = np.array([ 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
print(d.batch(h, l))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[[nan nan nan]
|
||||
[nan nan nan]
|
||||
[13. 11. 9.]
|
||||
[14. 12. 10.]
|
||||
[15. 13. 11.]]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```js
|
||||
const w = require('wickra');
|
||||
|
||||
const d = new w.Donchian(3);
|
||||
const flat = d.batch(
|
||||
[11, 12, 13, 14, 15],
|
||||
[ 9, 10, 11, 12, 13],
|
||||
);
|
||||
console.log('length:', flat.length);
|
||||
console.log('row 2 [upper, middle, lower]:', flat.slice(6, 9));
|
||||
console.log('row 4 [upper, middle, lower]:', flat.slice(12, 15));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
length: 15
|
||||
row 2 [upper, middle, lower]: [ 13, 11, 9 ]
|
||||
row 4 [upper, middle, lower]: [ 15, 13, 11 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Breakouts.** The original Turtle Trading rules (Dennis / Eckhardt,
|
||||
early 1980s) buy on a 20-day Donchian upper-band breach and sell on
|
||||
a 10-day lower-band breach. The modern descendant is the "channel
|
||||
breakout" family of trend-following systems.
|
||||
- **Mean reversion.** A small minority of systems take the bands as
|
||||
fade levels; this works on range-bound assets and fails dramatically
|
||||
in trends — the inverse of breakout systems.
|
||||
- **Volatility proxy.** Channel width `upper - lower` is a simple
|
||||
volatility proxy that requires no smoothing and no parameter tuning
|
||||
beyond the lookback length.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Stale extreme.** A single shock high from `period` candles ago
|
||||
keeps the upper band elevated even when current prices have fallen
|
||||
back to normal. Watch for the "channel drop" event when that high
|
||||
scrolls out of the window — the upper band will step down sharply
|
||||
in a single bar.
|
||||
- **No close required.** Donchian only uses high/low. Feeding it a
|
||||
close-only series (with high = low = close) collapses it into an
|
||||
envelope of close extremes, which is a much noisier signal than
|
||||
the canonical high/low form. The Python `batch` accepts only
|
||||
`(high, low)` for exactly this reason.
|
||||
- **Flat range collapse.** On a truly flat instrument the channel
|
||||
collapses to a line (`upper == middle == lower`); downstream code
|
||||
that divides by `upper - lower` (e.g. computing channel position)
|
||||
must handle this division-by-zero case explicitly.
|
||||
|
||||
## References
|
||||
|
||||
- Richard Donchian published the 4-week channel rule in the early
|
||||
1960s as part of his broader trend-following work.
|
||||
- Curtis Faith, *Way of the Turtle*, McGraw-Hill, 2007, documents
|
||||
the 20/10-day Donchian variant that defined the Turtle program.
|
||||
|
||||
## See also
|
||||
|
||||
- [Bollinger Bands](../volatility-bands/Indicator-BollingerBands.md) — envelope shaped by
|
||||
stddev rather than rolling extrema.
|
||||
- [Keltner Channels](../volatility-bands/Indicator-Keltner.md) — envelope shaped by ATR
|
||||
around an EMA centerline.
|
||||
- [PSAR](../trailing-stops/Indicator-Psar.md) — alternative trailing-stop construction
|
||||
for breakout systems.
|
||||
@@ -1,170 +0,0 @@
|
||||
# HistoricalVolatility
|
||||
|
||||
> Historical Volatility — the annualised standard deviation of log returns,
|
||||
> the realised volatility used to price options and size risk.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` (annualised percent) |
|
||||
| Default parameters | `(period = 20, trading_periods = 252)` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Annualised volatility of returns, in percent. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
r_t = ln(price_t / price_{t−1})
|
||||
HV = stddev_sample(r over period) · √trading_periods · 100
|
||||
```
|
||||
|
||||
The log returns over the window are measured with the **sample** standard
|
||||
deviation (divisor `n − 1`, Bessel's correction — the unbiased volatility
|
||||
estimator), then annualised by `√trading_periods` and expressed as a
|
||||
percentage. `trading_periods` is the number of bars in a year for the
|
||||
data's frequency: `252` for daily bars, `52` for weekly, `12` for
|
||||
monthly.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|-------------------|---------|----------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` (Python) | `>= 2` | Number of log returns in the window. `0` errors with `Error::PeriodZero`; `1` with `Error::InvalidPeriod` (the sample stddev needs two returns). |
|
||||
| `trading_periods` | `usize` | `252` (Python) | `>= 1` | Annualisation factor. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults the pair to `(20, 252)`. The `periods`
|
||||
property returns `(period, trading_periods)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/historical_volatility.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for HistoricalVolatility {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == period + 1`. The first log return needs a previous
|
||||
price, and the window must then hold `period` returns — so the first
|
||||
non-`None` output lands on input `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** A flat price series has all log returns equal to
|
||||
`0`, so volatility is `0.0` (`constant_series_yields_zero` pins this).
|
||||
- **Geometric series.** A constant growth factor produces a *constant*
|
||||
log return; its standard deviation — and so HV — is `0`
|
||||
(`geometric_series_yields_zero` pins this).
|
||||
- **Non-positive prices.** A log return is undefined when either price is
|
||||
`<= 0`. Such ticks are **skipped**: the previous valid value is returned,
|
||||
the indicator's state (previous price, window, sums) is left untouched, and
|
||||
the next real positive tick re-anchors against the previous *valid* price.
|
||||
Previous releases silently treated bad ticks as a `0.0` log-return, which
|
||||
underreported realised volatility on broken data feeds — that behaviour has
|
||||
changed.
|
||||
- **Non-negative.** Volatility is a standard deviation and is never
|
||||
negative (`output_is_non_negative` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped.
|
||||
- **Reset.** `hv.reset()` clears the previous price, the window and the
|
||||
running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, HistoricalVolatility};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// 20-bar window, 252 trading days per year.
|
||||
let mut hv = HistoricalVolatility::new(20, 252)?;
|
||||
let prices: Vec<f64> = (0..40).map(|i| 100.0 * 1.01_f64.powi(i)).collect();
|
||||
let out = hv.batch(&prices);
|
||||
println!("warmup_period = {}", hv.warmup_period());
|
||||
// A perfectly geometric series has constant returns -> zero volatility.
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
warmup_period = 21
|
||||
last = Some(0.0)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
hv = ta.HistoricalVolatility() # (period=20, trading_periods=252)
|
||||
prices = np.full(40, 100.0) # flat series
|
||||
print(hv.batch(prices)[-1]) # no return variation -> 0
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
// 52 trading periods per year for weekly bars.
|
||||
const hv = new ta.HistoricalVolatility(20, 52);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + Math.sin(i * 0.3) * 5);
|
||||
console.log('warmupPeriod:', hv.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`HistoricalVolatility` is the realised-volatility number quoted in
|
||||
options and risk work — "this stock has been running at 30 % annualised
|
||||
vol". Compare it against an option's *implied* volatility to judge whether
|
||||
options are cheap or rich, feed it into position-sizing (smaller size as
|
||||
HV rises), or track its own trend: volatility clusters, so a rising HV
|
||||
tends to keep rising.
|
||||
|
||||
Always match `trading_periods` to your bar frequency — annualising daily
|
||||
bars with `252`, weekly with `52`, monthly with `12`. Using the wrong
|
||||
factor rescales every reading.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Mismatched `trading_periods`.** Annualising weekly data with `252`
|
||||
inflates HV by `√(252/52) ≈ 2.2×`.
|
||||
- **Confusing it with `StdDev`.** `StdDev` is the population dispersion of
|
||||
*prices*; `HistoricalVolatility` is the sample (`n − 1`) dispersion of
|
||||
*log returns*, annualised.
|
||||
|
||||
## References
|
||||
|
||||
Historical (realised) volatility is the standard `√252`-annualised
|
||||
standard deviation of log returns; the unbiased `n − 1` estimator is the
|
||||
conventional choice for volatility estimation.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-StdDev.md](../volatility-bands/Indicator-StdDev.md) — population dispersion of
|
||||
raw prices.
|
||||
- [Indicator-Natr.md](../volatility-bands/Indicator-Natr.md) — range-based volatility as a
|
||||
percentage.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,214 +0,0 @@
|
||||
# Keltner Channels
|
||||
|
||||
> A pure composition of [EMA](../moving-averages/Indicator-Ema.md) on typical price plus
|
||||
> ATR-scaled envelopes. The middle line is the trend filter, the bands are
|
||||
> the volatility cone.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Item | Value |
|
||||
|---------------------|------------------------------------------------------------------------------------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `KeltnerOutput { upper: f64, middle: f64, lower: f64 }` |
|
||||
| Output range | unbounded; `lower ≤ middle ≤ upper` |
|
||||
| Default parameters | `ema_period = 20`, `atr_period = 10`, `multiplier = 2.0` |
|
||||
| Warmup period | `max(ema_period, atr_period)` (`20` for defaults) — exact first-emission index |
|
||||
| Interpretation | trend-following envelope; tags signal momentum, not exhaustion |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
middle_t = EMA_{ema_period}( typical_price_t ) // tp = (H+L+C)/3
|
||||
upper_t = middle_t + multiplier * ATR_{atr_period}_t
|
||||
lower_t = middle_t - multiplier * ATR_{atr_period}_t
|
||||
```
|
||||
|
||||
The middle line is an EMA of **typical price**, not of close
|
||||
(`crates/wickra-core/src/indicators/keltner.rs:62`,
|
||||
`candle.typical_price()`).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Constraint | Source |
|
||||
|--------------|---------|---------|-------------------------|----------------------------------------------|
|
||||
| `ema_period` | `usize` | `20` | `> 0` | `Keltner::new` (`keltner.rs:33`) |
|
||||
| `atr_period` | `usize` | `10` | `> 0` | `Keltner::new` (`keltner.rs:33`) |
|
||||
| `multiplier` | `f64` | `2.0` | finite and `> 0.0` | `Keltner::new` (`keltner.rs:34-36`) |
|
||||
|
||||
Python defaults from
|
||||
`#[pyo3(signature = (ema_period=20, atr_period=10, multiplier=2.0))]` in
|
||||
`bindings/python/src/lib.rs`. `Keltner::classic()` returns the same
|
||||
configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
```rust
|
||||
impl Indicator for Keltner {
|
||||
type Input = Candle;
|
||||
type Output = KeltnerOutput;
|
||||
fn update(&mut self, candle: Candle) -> Option<KeltnerOutput>;
|
||||
}
|
||||
|
||||
pub struct KeltnerOutput { pub upper: f64, pub middle: f64, pub lower: f64 }
|
||||
```
|
||||
|
||||
- **Python streaming.** Returns `(upper, middle, lower)` tuple or `None`.
|
||||
- **Python batch.** `Keltner.batch(high, low, close)` returns a 2-D
|
||||
`np.ndarray` of shape `(n, 3)` with columns `[upper, middle, lower]`;
|
||||
warmup rows are `NaN` across all three columns.
|
||||
- **Node streaming.** Returns a `{ upper, middle, lower }` object or
|
||||
`null`.
|
||||
- **Node batch.** `keltner.batch(high, low, close)` returns a flat
|
||||
`Array<number>` of length `n * 3` interleaved per row:
|
||||
`[u0, m0, l0, u1, m1, l1, …]`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period()` reports `max(ema_period, atr_period)` — for the
|
||||
default `(20, 10, 2.0)` that is `20` — and that figure is **exact**: the
|
||||
first non-`None` output lands on candle `warmup_period()` (index
|
||||
`warmup_period() - 1`).
|
||||
|
||||
`Keltner::update` feeds the EMA and ATR sub-indicators *unconditionally*
|
||||
on every candle, then emits once both are ready. The two sub-indicators
|
||||
warm up in parallel over the same candle window, so the slower of the
|
||||
two (`max(ema_period, atr_period)`) governs the first emission. With the
|
||||
classic `(20, 10, 2.0)` configuration the first valid `KeltnerOutput` is
|
||||
the 20th candle (index `19`). This is pinned by the
|
||||
`first_emission_matches_warmup_period` test in `keltner.rs`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A constant-OHLC series produces `upper == middle == lower`
|
||||
because ATR collapses to `0`. The pinned test
|
||||
`flat_market_collapses_bands` covers this.
|
||||
- **Trending market.** When ATR rises, both bands widen symmetrically
|
||||
around the EMA centerline.
|
||||
- **Reset.** `reset()` resets both the underlying EMA and ATR; the
|
||||
configured periods/multiplier are preserved.
|
||||
- **NaN / infinity.** `Candle::new` rejects non-finite OHLC values up
|
||||
front; the indicator never receives them.
|
||||
- **Invalid params.** `ema_period == 0`, `atr_period == 0`, or non-positive
|
||||
`multiplier` returns an error from `Keltner::new`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Keltner};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let candles = vec![
|
||||
Candle::new(10.0, 11.0, 9.0, 10.5, 1.0, 0)?,
|
||||
Candle::new(10.5, 12.0, 10.0, 11.5, 1.0, 0)?,
|
||||
Candle::new(11.5, 13.0, 11.0, 12.5, 1.0, 0)?,
|
||||
Candle::new(12.5, 14.0, 12.0, 13.5, 1.0, 0)?,
|
||||
Candle::new(13.5, 15.0, 13.0, 14.5, 1.0, 0)?,
|
||||
];
|
||||
let mut k = Keltner::new(3, 3, 2.0)?;
|
||||
for (i, v) in k.batch(&candles).into_iter().enumerate() {
|
||||
println!("i={i} -> {:?}", v);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
i=0 -> None
|
||||
i=1 -> None
|
||||
i=2 -> Some(KeltnerOutput { upper: 15.166666666666666, middle: 11.166666666666666, lower: 7.166666666666666 })
|
||||
i=3 -> Some(KeltnerOutput { upper: 16.166666666666664, middle: 12.166666666666666, lower: 8.166666666666666 })
|
||||
i=4 -> Some(KeltnerOutput { upper: 17.166666666666664, middle: 13.166666666666666, lower: 9.166666666666666 })
|
||||
```
|
||||
|
||||
The first emission is at `i = 2` (the 3rd candle), exactly
|
||||
`max(ema=3, atr=3) = 3` — the value `warmup_period()` reports. The EMA
|
||||
and ATR sub-indicators are fed in parallel, so neither delays the
|
||||
other.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
k = ta.Keltner(3, 3, 2.0)
|
||||
h = np.array([11.0, 12.0, 13.0, 14.0, 15.0])
|
||||
l = np.array([ 9.0, 10.0, 11.0, 12.0, 13.0])
|
||||
c = np.array([10.5, 11.5, 12.5, 13.5, 14.5])
|
||||
print(k.batch(h, l, c))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[[ nan nan nan]
|
||||
[ nan nan nan]
|
||||
[ nan nan nan]
|
||||
[ nan nan nan]
|
||||
[17.16666667 13.16666667 9.16666667]]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```js
|
||||
const w = require('wickra');
|
||||
|
||||
const k = new w.Keltner(3, 3, 2.0);
|
||||
const flat = k.batch(
|
||||
[11, 12, 13, 14, 15],
|
||||
[ 9, 10, 11, 12, 13],
|
||||
[10.5, 11.5, 12.5, 13.5, 14.5],
|
||||
);
|
||||
console.log('length:', flat.length);
|
||||
console.log('row 4 [upper, middle, lower]:', flat.slice(12, 15));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
length: 15
|
||||
row 4 [upper, middle, lower]: [ 17.166666666666664, 13.166666666666666, 9.166666666666666 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
- **Trend filter.** Persistent closes above the upper band signal
|
||||
trend continuation, much like Bollinger's "walking the band" pattern;
|
||||
Keltner is generally tighter than Bollinger on noisy series because
|
||||
ATR responds more smoothly than a rolling stddev.
|
||||
- **Squeeze cross-over.** A common "squeeze" setup compares Bollinger
|
||||
bandwidth to Keltner channel width: when Bollinger fits *inside*
|
||||
Keltner, a volatility expansion is statistically more likely.
|
||||
- **Pullback entries.** In a defined uptrend, pullbacks to the middle
|
||||
EMA line are a classic continuation entry; the lower band acts as
|
||||
the disaster stop.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Typical price ≠ close.** The middle EMA runs on
|
||||
`(H + L + C) / 3`, not on close. A pre-computed "EMA of close"
|
||||
panel will not equal the Keltner middle line and trying to align
|
||||
them at floating-point precision will fail.
|
||||
|
||||
## References
|
||||
|
||||
- Chester W. Keltner, *How to Make Money in Commodities*, 1960. The
|
||||
original construction used a 10-day SMA of typical price with an
|
||||
envelope sized by the 10-day average range. The modern variant
|
||||
(EMA centerline + ATR envelope) is the form Wickra implements.
|
||||
- Linda Bradford Raschke popularised the EMA + ATR rephrasing in the
|
||||
1990s; this is the version most TA libraries ship today.
|
||||
|
||||
## See also
|
||||
|
||||
- [EMA](../moving-averages/Indicator-Ema.md) — the centerline component.
|
||||
- [ATR](../volatility-bands/Indicator-Atr.md) — the envelope width component.
|
||||
- [Bollinger Bands](../volatility-bands/Indicator-BollingerBands.md) — envelope using stddev
|
||||
rather than ATR; useful side-by-side comparison.
|
||||
- [Donchian Channels](../volatility-bands/Indicator-Donchian.md) — envelope using rolling
|
||||
extrema with no smoothing.
|
||||
@@ -1,143 +0,0 @@
|
||||
# NATR
|
||||
|
||||
> Normalized Average True Range — ATR expressed as a percentage of price, so
|
||||
> volatility is comparable across instruments.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` (percent) |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Average true range as a percent of the close. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
NATR = 100 · ATR(period) / close
|
||||
```
|
||||
|
||||
[`Atr`](../volatility-bands/Indicator-Atr.md) measures volatility in raw price units — a `2.0`
|
||||
ATR is large on a $10 stock and tiny on a $5000 index. Dividing by the
|
||||
current close converts it to a percentage, so a NATR of `2.0` always
|
||||
means "the average true range is 2 % of price". That makes NATR readings
|
||||
comparable across a portfolio, and stop or position-size rules expressed
|
||||
as a NATR multiple behave consistently regardless of price level.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` (Python) | `>= 1` | Wilder smoothing period of the underlying ATR. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `14`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/natr.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Natr {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`NATR` is a **candle-input** indicator: it reads `high`, `low` and
|
||||
`close`. In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high`, `low`, `close` numpy arrays. Node and WASM
|
||||
expose `update(high, low, close)` and `batch(high, low, close)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Natr::new(period).warmup_period() == period` — identical to the
|
||||
underlying `Atr`, which is Wilder-seeded over `period` true ranges.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A market with no range has `ATR = 0`, so `NATR = 0`
|
||||
(`flat_market_yields_zero` pins this).
|
||||
- **Zero close.** NATR is undefined against a `0.0` close; the indicator
|
||||
reports `0.0` for that bar.
|
||||
- **Identity.** NATR equals `100 · ATR / close` bar for bar
|
||||
(`natr_is_atr_over_close_as_percent` pins this).
|
||||
- **Reset.** `natr.reset()` clears the underlying ATR.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Natr};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut natr = Natr::new(14)?;
|
||||
let candles: Vec<Candle> = (0..40)
|
||||
.map(|i| {
|
||||
let p = 100.0 + f64::from(i);
|
||||
Candle::new(p, p + 2.0, p - 2.0, p, 10.0, i64::from(i)).unwrap()
|
||||
})
|
||||
.collect();
|
||||
let out = natr.batch(&candles);
|
||||
println!("warmup_period = {}", natr.warmup_period());
|
||||
println!("last = {:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
natr = ta.NATR(14)
|
||||
high = np.arange(102.0, 142.0)
|
||||
low = high - 4.0
|
||||
close = high - 2.0
|
||||
print(natr.batch(high, low, close)[-1])
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const natr = new ta.NATR(14);
|
||||
const high = Array.from({ length: 40 }, (_, i) => 102 + i);
|
||||
const low = high.map((h) => h - 4);
|
||||
const close = high.map((h) => h - 2);
|
||||
console.log(natr.batch(high, low, close).at(-1));
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Natr` is the tool of choice whenever an ATR-based rule must work across
|
||||
instruments or across long stretches of time where the price level
|
||||
drifts. A volatility filter like "skip entries when NATR > 5" or a stop
|
||||
at "entry − 3 × NATR %" stays meaningful on any symbol. Use raw
|
||||
[`Atr`](../volatility-bands/Indicator-Atr.md) only when you specifically want the answer in
|
||||
price units (e.g. to place a stop a fixed number of points away).
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Feeding it scalar prices.** It needs `high`/`low`/`close`.
|
||||
- **Confusing it with ATR.** NATR is a percentage; an ATR-multiple stop
|
||||
and a NATR-multiple stop are different distances.
|
||||
|
||||
## References
|
||||
|
||||
NATR is the percentage-normalised ATR as implemented by TA-Lib (`NATR`);
|
||||
the underlying ATR is Wilder's from *New Concepts in Technical Trading
|
||||
Systems* (1978).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the price-unit original.
|
||||
- [Indicator-HistoricalVolatility.md](../volatility-bands/Indicator-HistoricalVolatility.md) —
|
||||
return-based annualised volatility.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,147 +0,0 @@
|
||||
# PercentB
|
||||
|
||||
> Bollinger %b — where price sits within the Bollinger Bands, scaled so
|
||||
> `0` is the lower band and `1` is the upper band.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (`0` = lower band, `1` = upper band) |
|
||||
| Default parameters | `(period = 20, multiplier = 2.0)` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Price position in the band; `> 1` / `< 0` = band overshoot. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
%b = (price − lower) / (upper − lower)
|
||||
```
|
||||
|
||||
where `upper` and `lower` come from
|
||||
[`BollingerBands`](../volatility-bands/Indicator-BollingerBands.md). `%b = 1` is price exactly
|
||||
on the upper band, `%b = 0` on the lower band, `%b = 0.5` on the middle
|
||||
band. The value is **deliberately not clamped**: a close above the upper
|
||||
band gives `%b > 1`, a close below the lower band gives `%b < 0` — so %b
|
||||
shows band overshoots directly.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|--------------|---------|----------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` (Python) | `>= 1` | Bollinger Bands period. `0` errors with `Error::PeriodZero`. |
|
||||
| `multiplier` | `f64` | `2.0` (Python) | `> 0` | Band standard-deviation multiplier. `<= 0` errors with `Error::NonPositiveMultiplier`. |
|
||||
|
||||
The Python binding defaults the pair to `(20, 2.0)`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/percent_b.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for PercentB {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == period` — identical to the underlying `BollingerBands`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** Flat prices collapse the bands onto the middle;
|
||||
with zero band width the price is exactly mid-band and %b is reported
|
||||
as `0.5` (`constant_series_yields_midpoint` pins this).
|
||||
- **Band overshoot.** %b is not clamped — values outside `[0, 1]` are
|
||||
expected and meaningful.
|
||||
- **NaN / infinity inputs.** Passed straight to the underlying
|
||||
`BollingerBands`, which drops them.
|
||||
- **Reset.** `pb.reset()` clears the underlying bands.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, PercentB};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut pb = PercentB::new(5, 2.0)?;
|
||||
// A flat series: price is exactly mid-band, so %b is 0.5.
|
||||
let out = pb.batch(&[100.0; 20]);
|
||||
println!("{:?}", out[10]);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.5)
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
pb = ta.PercentB(20, 2.0)
|
||||
prices = np.full(40, 100.0) # flat series -> mid-band
|
||||
print(pb.batch(prices)[-1]) # 0.5
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.5
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const pb = new ta.PercentB(20, 2.0);
|
||||
const prices = Array.from({ length: 60 }, (_, i) => 100 + Math.sin(i * 0.3) * 6);
|
||||
console.log('warmupPeriod:', pb.warmupPeriod());
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`PercentB` turns "is price near a band?" into a single number. The
|
||||
canonical reads: `%b > 1` is a close above the upper band (strong, often
|
||||
overbought); `%b < 0` is a close below the lower band (weak, often
|
||||
oversold); `%b` crossing `0.5` is price crossing the middle SMA. Because
|
||||
it is normalised, %b is the right input when you want to *compare* band
|
||||
position across instruments, or feed band position into another rule —
|
||||
for example "buy when %b crosses back above 0 from below".
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting `[0, 1]` bounds.** %b is intentionally unclamped; values
|
||||
outside `[0, 1]` are the band-overshoot signal, not an error.
|
||||
- **Confusing it with bandwidth.** %b is price *position*;
|
||||
[`BollingerBandwidth`](../volatility-bands/Indicator-BollingerBandwidth.md) is band *width*.
|
||||
|
||||
## References
|
||||
|
||||
John Bollinger, *Bollinger on Bollinger Bands* (2001). %b is one of
|
||||
Bollinger's two derived indicators (with bandwidth).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-BollingerBands.md](../volatility-bands/Indicator-BollingerBands.md) — the bands
|
||||
this locates price within.
|
||||
- [Indicator-BollingerBandwidth.md](../volatility-bands/Indicator-BollingerBandwidth.md) — the
|
||||
companion derived indicator: band *width*.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,155 +0,0 @@
|
||||
# StdDev
|
||||
|
||||
> Rolling population standard deviation — the dispersion of the last
|
||||
> `period` prices around their mean.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` (price-difference scale) |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Spread of recent prices; the raw volatility behind Bollinger Bands. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
mean = (1/n) · Σ price
|
||||
variance = (1/n) · Σ price² − mean²
|
||||
StdDev = √variance
|
||||
```
|
||||
|
||||
This is the **population** standard deviation (divisor `n`, not `n − 1`)
|
||||
— the exact dispersion measure that drives the band width of
|
||||
[`BollingerBands`](../volatility-bands/Indicator-BollingerBands.md). It is maintained as an
|
||||
O(1) state machine: a running sum and a running sum-of-squares, each
|
||||
updated by one add and one subtract per bar. Floating-point cancellation
|
||||
can leave the computed variance very slightly negative; it is clamped to
|
||||
zero before the square root.
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `20` (Python) | `>= 1` | Rolling window length. `0` errors with `Error::PeriodZero`. `period = 1` always yields `0`. |
|
||||
|
||||
The Python binding defaults `period` to `20`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/std_dev.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for StdDev {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`StdDev::new(period).warmup_period() == period`. The first non-`None`
|
||||
value is emitted once the window holds `period` prices.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant series.** A flat series has zero dispersion, so the output
|
||||
is `0.0` (`constant_series_yields_zero` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped; the
|
||||
window and the running sums are left untouched.
|
||||
- **Reset.** `sd.reset()` clears the window and both running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, StdDev};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut sd = StdDev::new(3)?;
|
||||
let out: Vec<Option<f64>> = sd.batch(&[2.0, 4.0, 6.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(1.6329931618554525)]
|
||||
```
|
||||
|
||||
The window `[2, 4, 6]` has mean `4` and variance `(4 + 0 + 4) / 3 = 8/3`,
|
||||
so the standard deviation is `√(8/3) ≈ 1.633`. This matches the
|
||||
`reference_value` test in `crates/wickra-core/src/indicators/std_dev.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
sd = ta.StdDev(3)
|
||||
print(sd.batch(np.array([2.0, 4.0, 6.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan 1.6329932]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const sd = new ta.StdDev(3);
|
||||
console.log(sd.batch([2, 4, 6]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 1.6329931618554525 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`StdDev` is the most direct volatility measure in the library: large
|
||||
values mean prices are scattered widely around their mean, small values
|
||||
mean a tight, quiet market. Use it on its own as a volatility filter, or
|
||||
recognise it as the engine inside `BollingerBands` — multiplying `StdDev`
|
||||
by the band multiplier and adding it to an `Sma` reproduces the bands
|
||||
exactly.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Expecting the sample standard deviation.** `StdDev` divides by `n`,
|
||||
not `n − 1`. For the unbiased return-based estimator use
|
||||
[`HistoricalVolatility`](../volatility-bands/Indicator-HistoricalVolatility.md).
|
||||
- **Comparing across instruments.** The output is in price units; a
|
||||
`StdDev` of `5` is not comparable between a $10 and a $1000 asset.
|
||||
|
||||
## References
|
||||
|
||||
The population standard deviation is standard statistics; this
|
||||
implementation matches the dispersion term of John Bollinger's Bollinger
|
||||
Bands and pandas' `rolling(period).std(ddof=0)`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-BollingerBands.md](../volatility-bands/Indicator-BollingerBands.md) — bands built
|
||||
from this dispersion measure.
|
||||
- [Indicator-HistoricalVolatility.md](../volatility-bands/Indicator-HistoricalVolatility.md) —
|
||||
annualised volatility of log returns.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,147 +0,0 @@
|
||||
# TrueRange
|
||||
|
||||
> True Range — the single-bar volatility measure that ATR is the average
|
||||
> of, exposed raw.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` (price scale) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | Per-bar volatility including overnight gaps. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
TR = max( high − low, |high − close_prev|, |low − close_prev| )
|
||||
```
|
||||
|
||||
True Range is the greatest of the bar's own range and the two gaps to the
|
||||
previous close, so it captures volatility that opens *between* bars — an
|
||||
overnight gap — not only the range printed within a bar. The first bar has no
|
||||
previous close and falls back to `high − low`. Where [`Atr`](../volatility-bands/Indicator-Atr.md)
|
||||
is the Wilder-smoothed average of this series, `TrueRange` exposes it raw, one
|
||||
value per bar.
|
||||
|
||||
## Parameters
|
||||
|
||||
`TrueRange` takes **no parameters** — `TrueRange::new()` in Rust,
|
||||
`wickra.TrueRange()` in Python, `new ta.TrueRange()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/true_range.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for TrueRange {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`TrueRange` is a **candle-input** indicator that reads `high`, `low` and
|
||||
`close` (the close drives the gap terms). Python's streaming `update` accepts
|
||||
a 6-tuple or a dict; the batch helper takes `high`, `low`, `close` numpy
|
||||
arrays. Node and WASM expose `update(high, low, close)` and the matching
|
||||
`batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`TrueRange::new().warmup_period() == 1`. It emits a value from the very first
|
||||
candle — that bar simply has no previous close and uses `high − low`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **First bar.** No previous close: `TR = high − low`.
|
||||
- **Gap.** A bar that opens far from the prior close has a `TR` larger than
|
||||
its own `high − low`.
|
||||
- **Non-negative.** `TR` is always `>= 0`.
|
||||
- **Reset.** `tr.reset()` drops the previous close; the next bar restarts.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, TrueRange};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut tr = TrueRange::new();
|
||||
let out = tr.batch(&[
|
||||
Candle::new(11.0, 12.0, 8.0, 11.0, 1.0, 0)?, // no prev close -> 12 - 8
|
||||
Candle::new(9.5, 10.0, 9.0, 9.5, 1.0, 1)?, // prev close 11 -> max(1, 1, 2)
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[Some(4.0), Some(2.0)]
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
tr = ta.TrueRange()
|
||||
print(tr.batch(
|
||||
np.array([12.0, 10.0]), np.array([8.0, 9.0]), np.array([11.0, 9.5])
|
||||
))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[4. 2.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const tr = new ta.TrueRange();
|
||||
console.log(tr.batch([12, 10], [8, 9], [11, 9.5]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 4, 2 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Read `TrueRange` as raw per-bar volatility. It spikes on wide-range or gapping
|
||||
bars and shrinks in quiet stretches. Smoothing it with a moving average gives
|
||||
[`Atr`](../volatility-bands/Indicator-Atr.md); using it directly is useful for volatility-scaled
|
||||
position sizing or for spotting single outlier bars an average would hide.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing it with `high − low`.** On a gap bar the True Range is larger —
|
||||
that is the whole point.
|
||||
- **Feeding it scalar prices.** It needs the full `high`/`low`/`close` bar.
|
||||
|
||||
## References
|
||||
|
||||
J. Welles Wilder Jr.'s True Range, from *New Concepts in Technical Trading
|
||||
Systems* (1978).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — the Wilder-smoothed average of the
|
||||
True Range.
|
||||
- [Indicator-ChaikinVolatility.md](../volatility-bands/Indicator-ChaikinVolatility.md) — a
|
||||
rate-of-change volatility measure.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,160 +0,0 @@
|
||||
# UlcerIndex
|
||||
|
||||
> Ulcer Index — Peter Martin's downside-only risk measure: the
|
||||
> root-mean-square of recent drawdowns.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volatility & Bands |
|
||||
| Input type | `f64` (single close) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[0, ∞)` (percent) |
|
||||
| Default parameters | `period = 14` (Python) |
|
||||
| Warmup period | `2·period − 1` |
|
||||
| Interpretation | Depth and duration of drawdowns; `0` means no drawdown at all. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
max_t = highest price over the trailing `period` bars
|
||||
drawdown_t = 100 · (price_t − max_t) / max_t
|
||||
UlcerIndex = √( mean( drawdown² over period ) )
|
||||
```
|
||||
|
||||
Standard deviation treats an up-move and a down-move as equally
|
||||
"volatile". The Ulcer Index measures only the **pain of being underwater**:
|
||||
for each bar it takes the percentage drop from the trailing high, squares
|
||||
it, and reports the root-mean-square. A market that only rises has no
|
||||
drawdown and an Ulcer Index of `0`; the deeper and longer the drawdowns,
|
||||
the higher the reading. It is the volatility term in the Martin ratio
|
||||
(Ulcer Performance Index).
|
||||
|
||||
## Parameters
|
||||
|
||||
| Name | Type | Default | Valid range | Description |
|
||||
|----------|---------|---------------|-------------|-------------|
|
||||
| `period` | `usize` | `14` (Python) | `>= 1` | Look-back for both the trailing high and the RMS window. `0` errors with `Error::PeriodZero`. |
|
||||
|
||||
The Python binding defaults `period` to `14`.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ulcer_index.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for UlcerIndex {
|
||||
type Input = f64;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: f64) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
A single `f64` close in, an `Option<f64>` out. Python maps this to
|
||||
`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` /
|
||||
`Array<number>` (NaN warmup).
|
||||
|
||||
## Warmup
|
||||
|
||||
`UlcerIndex::new(period).warmup_period() == 2·period − 1`. The first
|
||||
`period` prices fill the trailing-maximum window; the per-bar squared
|
||||
drawdown then needs another `period − 1` bars to fill the RMS window.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Pure up-trend.** Price never trades below its own running high, so
|
||||
every drawdown — and the Ulcer Index — is `0`
|
||||
(`pure_uptrend_yields_zero` pins this).
|
||||
- **Constant series.** A flat series has no drawdown; the output is `0.0`
|
||||
(`constant_series_yields_zero` pins this).
|
||||
- **Non-negative.** The Ulcer Index is an RMS of real numbers and is
|
||||
never negative (`output_is_non_negative` pins this).
|
||||
- **NaN / infinity inputs.** Non-finite inputs are silently dropped.
|
||||
- **Reset.** `ui.reset()` clears both rolling windows and the sum.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Indicator, UlcerIndex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut ui = UlcerIndex::new(2)?;
|
||||
let out: Vec<Option<f64>> = ui.batch(&[10.0, 8.0, 12.0, 9.0]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, None, Some(14.142135623730951), Some(17.67766952966369)]
|
||||
```
|
||||
|
||||
`UlcerIndex(2)` warms up after `3` bars. At bar 3 the squared drawdowns in
|
||||
the window are `[400, 0]`, so the index is `√(400/2) = √200`. At bar 4
|
||||
they are `[0, 625]`, giving `√(625/2) = √312.5`. This matches the
|
||||
`reference_values` test in
|
||||
`crates/wickra-core/src/indicators/ulcer_index.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
ui = ta.UlcerIndex(2)
|
||||
print(ui.batch(np.array([10.0, 8.0, 12.0, 9.0])))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan nan 14.1421356 17.6776695]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const ui = new ta.UlcerIndex(2);
|
||||
console.log(ui.batch([10, 8, 12, 9]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, NaN, 14.142135623730951, 17.67766952966369 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`UlcerIndex` answers "how uncomfortable has holding this been?" — a high
|
||||
reading means deep or prolonged drawdowns, a low reading means a smooth
|
||||
ride up. It is most useful for *comparing* instruments or strategies on a
|
||||
downside-risk basis, and as the denominator of the Ulcer Performance
|
||||
Index (`(return − risk-free) / UlcerIndex`), a Sharpe-ratio analogue that
|
||||
penalises only downside volatility.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading it as two-sided volatility.** The Ulcer Index ignores upside
|
||||
entirely — a wildly choppy *up*-trend can still score near `0`. Use
|
||||
[`StdDev`](../volatility-bands/Indicator-StdDev.md) for two-sided dispersion.
|
||||
- **Forgetting the doubled warmup.** Warmup is `2·period − 1`, not
|
||||
`period`.
|
||||
|
||||
## References
|
||||
|
||||
Peter Martin and Byron McCann, *The Investor's Guide to Fidelity Funds*
|
||||
(1989); the index is also documented at StockCharts. The trailing-high
|
||||
drawdown RMS here follows that definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-StdDev.md](../volatility-bands/Indicator-StdDev.md) — two-sided dispersion.
|
||||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — per-bar range volatility.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,160 +0,0 @@
|
||||
# ADL
|
||||
|
||||
> Accumulation/Distribution Line — a cumulative volume-flow line that
|
||||
> weights each bar's volume by where its close fell within the range.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (drifts with cumulative volume) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | Running buying/selling pressure; slope and divergence matter. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
MFM_t = ((close − low) − (high − close)) / (high − low) (money-flow multiplier, −1..+1)
|
||||
MFV_t = MFM_t · volume_t (money-flow volume)
|
||||
ADL_t = ADL_{t−1} + MFV_t
|
||||
```
|
||||
|
||||
The money-flow multiplier asks *where in the bar's range did price
|
||||
close?* A close on the high gives `+1` (full accumulation), on the low
|
||||
`−1` (full distribution), in the middle `0`. Scaling by volume and
|
||||
running the cumulative total gives a line whose **slope** reflects
|
||||
sustained buying or selling pressure. A bar with `high == low` carries no
|
||||
positional information and contributes `0`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`ADL` takes **no parameters** — `Adl::new()` in Rust, `wickra.ADL()` in
|
||||
Python, `new ta.ADL()` in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/adl.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for Adl {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ADL` is a **candle-input** indicator: it reads `high`, `low`, `close` and
|
||||
`volume`. In Python the streaming `update` accepts a 6-tuple or a dict;
|
||||
the batch helper takes `high`, `low`, `close`, `volume` numpy arrays. Node
|
||||
and WASM expose `update(high, low, close, volume)` and the matching
|
||||
`batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`Adl::new().warmup_period() == 1`. ADL is cumulative — it emits a value
|
||||
from the very first candle.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero-range bar.** A bar with `high == low` contributes `0` to the line
|
||||
(`zero_range_bar_contributes_nothing` pins this).
|
||||
- **Close at the high.** Every bar closing on its high has `MFM = +1`, so
|
||||
ADL grows by exactly `volume` each bar
|
||||
(`close_at_high_accumulates_full_volume` pins this).
|
||||
- **Candle validation.** `Candle::new` rejects invalid bars upstream.
|
||||
- **Reset.** `adl.reset()` returns the running total to `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, Adl};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut adl = Adl::new();
|
||||
let out = adl.batch(&[
|
||||
Candle::new(8.0, 10.0, 8.0, 10.0, 100.0, 0)?, // close at high
|
||||
Candle::new(10.0, 12.0, 8.0, 9.0, 200.0, 1)?,
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[Some(100.0), Some(0.0)]
|
||||
```
|
||||
|
||||
Bar 1 closes at its high (`MFM = +1`), adding `+100`. Bar 2 has
|
||||
`MFM = ((9−8)−(12−9))/4 = −0.5`, adding `−100`, so the line returns to
|
||||
`0`. This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/adl.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
adl = ta.ADL()
|
||||
high = np.array([10.0, 12.0])
|
||||
low = np.array([8.0, 8.0])
|
||||
close = np.array([10.0, 9.0])
|
||||
volume = np.array([100.0, 200.0])
|
||||
print(adl.batch(high, low, close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[100. 0.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const adl = new ta.ADL();
|
||||
console.log(adl.batch([10, 12], [8, 8], [10, 9], [100, 200]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 100, 0 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`Adl` is read by slope and by divergence, never by absolute level (the
|
||||
total drifts arbitrarily with cumulative volume). A rising ADL confirms
|
||||
that an up-move is backed by accumulation; a *falling* ADL while price
|
||||
rises is a bearish divergence — the rally is not being bought into.
|
||||
[`ChaikinOscillator`](../volume/Indicator-ChaikinOscillator.md) is the standard way
|
||||
to turn the ADL into a bounded, tradeable oscillator.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading the absolute value.** Only the slope and divergences are
|
||||
meaningful; the level depends on where you started the stream.
|
||||
- **Feeding it scalar prices.** It needs the full OHLCV bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Accumulation/Distribution Line; the money-flow-multiplier
|
||||
formulation here matches the standard definition (StockCharts, TA-Lib's
|
||||
`AD`).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Obv.md](../volume/Indicator-Obv.md) — cumulative *signed* volume.
|
||||
- [Indicator-ChaikinOscillator.md](../volume/Indicator-ChaikinOscillator.md) — an
|
||||
oscillator built on the ADL.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,158 +0,0 @@
|
||||
# ChaikinMoneyFlow
|
||||
|
||||
> Chaikin Money Flow (CMF) — the ratio of money-flow volume to total
|
||||
> volume over a rolling window, bounded to `[−1, +1]`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−1, +1]` |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Window accumulation/distribution balance; sign and magnitude both matter. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
MFM_t = ((close − low) − (high − close)) / (high − low) (money-flow multiplier, −1..+1)
|
||||
MFV_t = MFM_t · volume_t (money-flow volume)
|
||||
CMF_t = Σ(MFV, period) / Σ(volume, period)
|
||||
```
|
||||
|
||||
CMF is the [`Adl`](../volume/Indicator-Adl.md) increment averaged the way RSI averages
|
||||
gains: rather than a running total, it divides the *summed* money-flow volume
|
||||
of the last `period` bars by the *summed* volume of those bars. The result is
|
||||
volume-normalised, so it lives in `[−1, +1]` regardless of how heavily the
|
||||
instrument trades. A bar with `high == low` carries no positional information
|
||||
and contributes a money-flow volume of `0`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the lookback window. The Python binding defaults it to `20`; the
|
||||
Rust and Node constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/cmf.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChaikinMoneyFlow {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ChaikinMoneyFlow` is a **candle-input** indicator: it reads `high`, `low`,
|
||||
`close` and `volume`. In Python the streaming `update` accepts a 6-tuple or a
|
||||
dict; the batch helper takes `high`, `low`, `close`, `volume` numpy arrays.
|
||||
Node and WASM expose `update(high, low, close, volume)` and the matching
|
||||
`batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChaikinMoneyFlow::new(20).warmup_period() == 20`. The first value lands once
|
||||
the window holds a full `period` bars — on input index `period − 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero-range bar.** A bar with `high == low` contributes `MFV = 0`.
|
||||
- **Empty-volume window.** If the whole window traded zero volume, the
|
||||
`0/0` ratio is defined as `0.0` (`zero_volume_window_yields_zero` pins this).
|
||||
- **Saturated flow.** Every bar closing on its high gives `MFM = +1`, so CMF
|
||||
saturates at `+1` (`closes_at_high_yield_cmf_one` pins this).
|
||||
- **Reset.** `cmf.reset()` clears the window and both running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChaikinMoneyFlow};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmf = ChaikinMoneyFlow::new(2)?;
|
||||
let out = cmf.batch(&[
|
||||
Candle::new(8.0, 10.0, 8.0, 10.0, 100.0, 0)?, // close at high -> MFV +100
|
||||
Candle::new(10.0, 12.0, 8.0, 10.0, 100.0, 1)?, // close mid-range -> MFV 0
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(0.5)]
|
||||
```
|
||||
|
||||
Bar 1 closes at its high (`MFM = +1`, `MFV = +100`); bar 2 closes mid-range
|
||||
(`MFM = 0`, `MFV = 0`). `CMF(2) = (100 + 0) / (100 + 100) = 0.5`. This matches
|
||||
the `reference_values` test in `crates/wickra-core/src/indicators/cmf.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
cmf = ta.ChaikinMoneyFlow(2)
|
||||
high = np.array([10.0, 12.0])
|
||||
low = np.array([8.0, 8.0])
|
||||
close = np.array([10.0, 10.0])
|
||||
volume = np.array([100.0, 100.0])
|
||||
print(cmf.batch(high, low, close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan 0.5]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const cmf = new ta.ChaikinMoneyFlow(2);
|
||||
console.log(cmf.batch([10, 12], [8, 8], [10, 10], [100, 100]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 0.5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
CMF reads as a balance: sustained positive values mean closes are clustering
|
||||
near bar highs on real volume (accumulation), sustained negative values mean
|
||||
the opposite (distribution). Crosses of the zero line are the textbook signal;
|
||||
the `±0.05` band is often treated as a neutral zone. Because CMF is
|
||||
volume-normalised it is comparable across instruments — unlike the raw
|
||||
[`Adl`](../volume/Indicator-Adl.md), whose level is arbitrary.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing it with the ADL.** CMF is a *bounded ratio*; the ADL is an
|
||||
*unbounded running total*. They share the money-flow multiplier and nothing
|
||||
else.
|
||||
- **Feeding it scalar prices.** It needs the full OHLCV bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Chaikin Money Flow; the money-flow-multiplier formulation here
|
||||
matches the standard definition (StockCharts).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Adl.md](../volume/Indicator-Adl.md) — the cumulative line CMF is built on.
|
||||
- [Indicator-ChaikinOscillator.md](../volume/Indicator-ChaikinOscillator.md) — the
|
||||
EMA-difference oscillator on the ADL.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,160 +0,0 @@
|
||||
# ChaikinOscillator
|
||||
|
||||
> Chaikin Oscillator — the MACD of the Accumulation/Distribution Line:
|
||||
> a fast EMA of the ADL minus a slow EMA of the ADL.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `fast = 3`, `slow = 10` (Python) |
|
||||
| Warmup period | `slow` |
|
||||
| Interpretation | Momentum of accumulation/distribution; zero-line crossings are the signal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
ChaikinOsc_t = EMA(ADL, fast)_t − EMA(ADL, slow)_t
|
||||
```
|
||||
|
||||
The [`Adl`](../volume/Indicator-Adl.md) is an unbounded line that drifts with cumulative
|
||||
volume — useful for its slope but awkward to trade directly. The Chaikin
|
||||
Oscillator applies the MACD construction to it: difference a fast and a slow
|
||||
EMA of the ADL to get a zero-centred momentum reading. Positive values mean
|
||||
short-term accumulation is outrunning the longer trend; negative values mean
|
||||
distribution leads.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `fast` — period of the fast EMA on the ADL (classic `3`).
|
||||
- `slow` — period of the slow EMA on the ADL (classic `10`).
|
||||
|
||||
`fast` must be strictly less than `slow`. `ChaikinOscillator::classic()`
|
||||
returns the `(3, 10)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/chaikin_oscillator.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChaikinOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
It is a **candle-input** indicator (the ADL inside it needs `high`, `low`,
|
||||
`close`, `volume`). Python's streaming `update` accepts a 6-tuple or a dict;
|
||||
the batch helper takes `high`, `low`, `close`, `volume` numpy arrays. Node and
|
||||
WASM expose `update(high, low, close, volume)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChaikinOscillator::classic().warmup_period() == 10`. The ADL emits a value
|
||||
from the very first candle, so both EMAs are fed every bar and the slow EMA
|
||||
gates the first output — the warmup is exactly `slow`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A flat candle has zero money-flow volume, so the ADL never
|
||||
moves and both EMAs of the constant-zero series stay at zero — the
|
||||
oscillator sits at `0.0` (`flat_market_yields_zero` pins this).
|
||||
- **`fast >= slow`.** Rejected at construction with an error.
|
||||
- **Reset.** `osc.reset()` clears the ADL and both EMAs.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChaikinOscillator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut osc = ChaikinOscillator::classic(); // EMA(ADL, 3) − EMA(ADL, 10)
|
||||
// A flat market: the ADL never moves, so the oscillator sits at zero.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, i).unwrap())
|
||||
.collect();
|
||||
let out = osc.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.0)
|
||||
```
|
||||
|
||||
A flat series produces a flat ADL and therefore a zero oscillator. This
|
||||
matches the `flat_market_yields_zero` test in
|
||||
`crates/wickra-core/src/indicators/chaikin_oscillator.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
osc = ta.ChaikinOscillator(3, 10)
|
||||
n = 20
|
||||
flat = np.full(n, 10.0)
|
||||
print(osc.batch(flat, flat, flat, np.full(n, 100.0))[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const osc = new ta.ChaikinOscillator(3, 10);
|
||||
const flat = Array(20).fill(10);
|
||||
const vol = Array(20).fill(100);
|
||||
const out = osc.batch(flat, flat, flat, vol);
|
||||
console.log(out[out.length - 1]);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Trade the Chaikin Oscillator like any MACD-style line: a cross above zero is a
|
||||
bullish accumulation signal, a cross below is bearish. Divergence between the
|
||||
oscillator and price is the higher-conviction setup — for example, price
|
||||
making a new high while the oscillator does not is the same warning the raw
|
||||
ADL gives, but packaged as a bounded, zero-centred series.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Treating the level as meaningful.** Only the sign and the slope carry
|
||||
information; the magnitude scales with the instrument's volume.
|
||||
- **Feeding it scalar prices.** It needs the full OHLCV bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Chaikin Oscillator — the MACD construction applied to his
|
||||
Accumulation/Distribution Line (StockCharts).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Adl.md](../volume/Indicator-Adl.md) — the cumulative line this oscillates.
|
||||
- [Indicator-ChaikinMoneyFlow.md](../volume/Indicator-ChaikinMoneyFlow.md) — a bounded
|
||||
ratio built from the same money-flow volume.
|
||||
- [Indicator-MacdIndicator.md](../trend-directional/Indicator-MacdIndicator.md) — the
|
||||
same fast/slow EMA-difference construction on price.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,160 +0,0 @@
|
||||
# EaseOfMovement
|
||||
|
||||
> Ease of Movement (EOM) — Richard Arms' measure of how far price travels
|
||||
> per unit of volume, averaged over a window.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `high`, `low`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero (scaled by `divisor`) |
|
||||
| Default parameters | `period = 14`, `divisor = 1e8` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Light-volume moves push it away from zero; sign tracks direction. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
distance_t = (high_t + low_t)/2 − (high_{t−1} + low_{t−1})/2
|
||||
EMV_t = distance_t · (high_t − low_t) · divisor / volume_t
|
||||
EOM_t = SMA(EMV, period)_t
|
||||
```
|
||||
|
||||
Arms' question is *how easily did price move?* A bar whose midpoint jumped a
|
||||
long way on a wide range but light volume gets a large `EMV`; a bar that
|
||||
needed heavy volume to budge gets a small one. The `divisor` is a pure
|
||||
output-scaling constant — the conventional `1e8` keeps `EMV` readable for
|
||||
typical share volumes; smaller markets want a smaller divisor. The window SMA
|
||||
smooths the noisy per-bar `EMV` into a tradeable line.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `period` — the SMA averaging window (Python default `14`).
|
||||
- `divisor` — the volume-scaling constant (Python default `1e8`). Rust exposes
|
||||
`EaseOfMovement::new(period)` for the `1e8` default and
|
||||
`EaseOfMovement::with_divisor(period, divisor)` for an explicit value.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ease_of_movement.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for EaseOfMovement {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`EaseOfMovement` is a **candle-input** indicator that reads `high`, `low` and
|
||||
`volume`. In Python the streaming `update` accepts a 6-tuple or a dict; the
|
||||
batch helper takes `high`, `low`, `volume` numpy arrays. Node and WASM expose
|
||||
`update(high, low, volume)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`EaseOfMovement::new(14).warmup_period() == 15`. The first candle only seeds
|
||||
the previous midpoint, so the first `EMV` appears on candle 2 and the first
|
||||
averaged value on candle `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero-volume bar.** Contributes `EMV = 0` instead of dividing by zero
|
||||
(`zero_volume_contributes_zero` pins this).
|
||||
- **Zero-range bar.** `high == low` makes the `(high − low)` factor zero, so
|
||||
`EMV = 0`.
|
||||
- **Constant series.** Unchanging midpoints give zero distance, so EOM stays
|
||||
at `0.0` (`constant_series_yields_zero` pins this).
|
||||
- **Reset.** `eom.reset()` clears the previous midpoint and the SMA window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, EaseOfMovement};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// EOM(period = 1, divisor = 1): one EMV value is its own average.
|
||||
let mut eom = EaseOfMovement::with_divisor(1, 1.0)?;
|
||||
let out = eom.batch(&[
|
||||
Candle::new(9.0, 10.0, 8.0, 9.0, 50.0, 0)?, // seeds the previous midpoint (9)
|
||||
Candle::new(12.0, 14.0, 10.0, 12.0, 100.0, 1)?, // mid 12, distance 3, range 4
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(0.12)]
|
||||
```
|
||||
|
||||
Bar 2: `EMV = distance · range · divisor / volume = 3 · 4 · 1 / 100 = 0.12`.
|
||||
This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/ease_of_movement.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
eom = ta.EaseOfMovement(1, 1.0)
|
||||
high = np.array([10.0, 14.0])
|
||||
low = np.array([8.0, 10.0])
|
||||
volume = np.array([50.0, 100.0])
|
||||
print(eom.batch(high, low, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 0.12]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const eom = new ta.EaseOfMovement(1, 1.0);
|
||||
console.log(eom.batch([10, 14], [8, 10], [50, 100]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 0.12 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
EOM crossing above zero says price is drifting up *without* needing much
|
||||
volume — an easy, low-resistance advance; below zero is the same for a
|
||||
decline. A reading hovering near zero means volume is heavy relative to the
|
||||
distance covered, i.e. price is grinding. The sign tracks direction; the
|
||||
distance from zero tracks how freely the move is happening.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading the raw magnitude.** It depends entirely on the `divisor` you
|
||||
chose — only the sign and relative size are portable.
|
||||
- **Feeding it scalar prices.** It needs `high`, `low` *and* `volume`.
|
||||
|
||||
## References
|
||||
|
||||
Richard W. Arms Jr.'s Ease of Movement; the box-ratio formulation here matches
|
||||
the standard definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-ForceIndex.md](../volume/Indicator-ForceIndex.md) — a different
|
||||
price-change-vs-volume gauge.
|
||||
- [Indicator-ChaikinMoneyFlow.md](../volume/Indicator-ChaikinMoneyFlow.md) — bounded
|
||||
money-flow balance.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,154 +0,0 @@
|
||||
# ForceIndex
|
||||
|
||||
> Force Index — Alexander Elder's price change scaled by volume, then
|
||||
> smoothed with an EMA.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `period = 13` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Conviction behind a move; sign and zero-crossings are the signal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
raw_t = (close_t − close_{t−1}) · volume_t
|
||||
Force_t = EMA(raw, period)_t
|
||||
```
|
||||
|
||||
The raw force is positive on an up-close and negative on a down-close, with a
|
||||
magnitude that grows with the volume backing the move — a large move on heavy
|
||||
volume registers a large force, a large move on thin volume does not.
|
||||
Smoothing the raw series with an EMA turns the noisy per-bar reading into a
|
||||
tradeable line; Elder's classic period is `13`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the EMA smoothing period. The Python binding defaults it to `13`;
|
||||
the Rust and Node constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/force_index.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ForceIndex {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ForceIndex` is a **candle-input** indicator that reads `close` and `volume`.
|
||||
In Python the streaming `update` accepts a 6-tuple or a dict; the batch helper
|
||||
takes `close`, `volume` numpy arrays. Node and WASM expose
|
||||
`update(close, volume)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ForceIndex::new(13).warmup_period() == 14`. The first candle only establishes
|
||||
the previous close, so the first raw force appears on candle 2 and the first
|
||||
smoothed value on candle `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **First candle.** Establishes the previous close only; emits `None`.
|
||||
- **Up- vs down-trend.** A strictly rising series gives a positive force, a
|
||||
strictly falling series a negative one (`pure_uptrend_is_positive` and
|
||||
`pure_downtrend_is_negative` pin this).
|
||||
- **`period = 1`.** `EMA(1)` has `alpha = 1`, so the Force Index passes the
|
||||
raw force through unsmoothed.
|
||||
- **Reset.** `fi.reset()` clears the previous close and the EMA.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ForceIndex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// ForceIndex(1): EMA(1) passes the raw force through.
|
||||
let mut fi = ForceIndex::new(1)?;
|
||||
let out = fi.batch(&[
|
||||
Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?, // seeds the previous close
|
||||
Candle::new(12.0, 12.0, 12.0, 12.0, 100.0, 1)?, // raw = (12-10)·100
|
||||
Candle::new(11.0, 11.0, 11.0, 11.0, 200.0, 2)?, // raw = (11-12)·200
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(200.0), Some(-200.0)]
|
||||
```
|
||||
|
||||
This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/force_index.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
fi = ta.ForceIndex(1)
|
||||
close = np.array([10.0, 12.0, 11.0])
|
||||
volume = np.array([100.0, 100.0, 200.0])
|
||||
print(fi.batch(close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 200. -200.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const fi = new ta.ForceIndex(1);
|
||||
console.log(fi.batch([10, 12, 11], [100, 100, 200]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 200, -200 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Elder reads the Force Index on two horizons. A short period (the classic `2`)
|
||||
is a sensitive entry timer — it crosses zero often. A longer period (`13`)
|
||||
tracks the conviction behind the prevailing trend: it staying above zero
|
||||
confirms buyers are in control. Divergence between a `13`-period Force Index
|
||||
and price flags an exhausting move.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Comparing levels across instruments.** The force scales with raw volume,
|
||||
so a value of `200` means nothing without knowing the instrument.
|
||||
- **Feeding it scalar prices.** It needs `close` *and* `volume`.
|
||||
|
||||
## References
|
||||
|
||||
Alexander Elder's Force Index, introduced in *Trading for a Living* (1993).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Obv.md](../volume/Indicator-Obv.md) — cumulative signed volume, a coarser
|
||||
volume-conviction gauge.
|
||||
- [Indicator-VolumePriceTrend.md](../volume/Indicator-VolumePriceTrend.md) — cumulative
|
||||
volume scaled by percentage move.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,189 +0,0 @@
|
||||
# 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 |
|
||||
| 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](../volume/Indicator-Vwap.md) — volume-weighted price benchmark; OBV and
|
||||
VWAP are the two canonical volume-aware indicators in the panel.
|
||||
- [MFI](../momentum-oscillators/Indicator-Mfi.md) — money-flow index, an oscillator blending
|
||||
typical price with volume.
|
||||
- [SMA](../moving-averages/Indicator-Sma.md) / [EMA](../moving-averages/Indicator-Ema.md) — the smoothers
|
||||
most commonly layered on top of OBV to define trade triggers.
|
||||
@@ -1,160 +0,0 @@
|
||||
# VolumePriceTrend
|
||||
|
||||
> Volume-Price Trend (VPT) — a cumulative volume line where each bar's
|
||||
> contribution is scaled by its percentage price change.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Input type | `Candle` (uses `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded (drifts with cumulative volume) |
|
||||
| Default parameters | none (no parameters) |
|
||||
| Warmup period | `1` |
|
||||
| Interpretation | Running volume flow; slope and divergence matter. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
VPT_t = VPT_{t−1} + volume_t · (close_t − close_{t−1}) / close_{t−1}
|
||||
```
|
||||
|
||||
VPT is a close relative of [`Obv`](../volume/Indicator-Obv.md). Where OBV adds the
|
||||
*entire* bar volume on any up-close, VPT adds volume scaled by the **size**
|
||||
of the move: a 2 % gain on a given volume moves the line twice as far as a
|
||||
1 % gain on the same volume. That makes VPT more sensitive to the
|
||||
conviction behind a move. The first bar establishes the baseline at `0`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`VolumePriceTrend` takes **no parameters** — `VolumePriceTrend::new()` in
|
||||
Rust, `wickra.VolumePriceTrend()` in Python, `new ta.VolumePriceTrend()`
|
||||
in Node.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/vpt.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for VolumePriceTrend {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`VolumePriceTrend` is a **candle-input** indicator: it reads `close` and
|
||||
`volume`. In Python the streaming `update` accepts a 6-tuple or a dict;
|
||||
the batch helper takes `close` and `volume` numpy arrays. Node and WASM
|
||||
expose `update(close, volume)` and `batch(close, volume)`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`warmup_period() == 1`. VPT is cumulative — it emits the baseline `0` from
|
||||
the first candle, then accumulates from the second onward.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Constant close.** With no price change every bar contributes `0`, so
|
||||
the line stays flat regardless of volume
|
||||
(`constant_close_keeps_line_flat` pins this).
|
||||
- **First bar.** The first candle has no previous close; VPT emits the
|
||||
baseline `0.0` (`emits_from_first_candle_at_zero` pins this).
|
||||
- **Zero previous close.** A percentage change against a `0.0` prior
|
||||
close is undefined and is treated as `0`.
|
||||
- **Candle validation.** `Candle::new` rejects invalid bars upstream.
|
||||
- **Reset.** `vpt.reset()` returns the running total to `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, VolumePriceTrend};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut vpt = VolumePriceTrend::new();
|
||||
// closes 10 -> 11 -> 9, volumes 100, 200, 300.
|
||||
let out = vpt.batch(&[
|
||||
Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?,
|
||||
Candle::new(11.0, 11.0, 11.0, 11.0, 200.0, 1)?,
|
||||
Candle::new(9.0, 9.0, 9.0, 9.0, 300.0, 2)?,
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[Some(0.0), Some(20.0), Some(-34.54545454545455)]
|
||||
```
|
||||
|
||||
Bar 1 is the baseline `0`. Bar 2 adds `200 · (11−10)/10 = 20`. Bar 3 adds
|
||||
`300 · (9−11)/11 = −600/11`, leaving `20 − 600/11 ≈ −34.545`. This matches
|
||||
the `reference_values` test in `crates/wickra-core/src/indicators/vpt.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
vpt = ta.VolumePriceTrend()
|
||||
close = np.array([10.0, 11.0, 9.0])
|
||||
volume = np.array([100.0, 200.0, 300.0])
|
||||
print(vpt.batch(close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 0. 20. -34.54545455]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const vpt = new ta.VolumePriceTrend();
|
||||
console.log(vpt.batch([10, 11, 9], [100, 200, 300]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ 0, 20, -34.54545454545455 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
`VolumePriceTrend` is read like OBV — by **slope** and by **divergence**,
|
||||
never by absolute level. A VPT rising in step with price confirms the
|
||||
trend is volume-supported; VPT flattening or falling while price climbs
|
||||
is a bearish divergence warning that the move lacks participation. Versus
|
||||
OBV, VPT gives proportionally more weight to large moves and less to a
|
||||
string of tiny up-closes, so it tracks the *magnitude* of conviction, not
|
||||
just its direction.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Reading the absolute value.** Only slope and divergences carry
|
||||
meaning; the level depends on the stream's start point.
|
||||
- **Expecting OBV-identical behaviour.** VPT scales by percentage change,
|
||||
so the two lines diverge — especially across large single-bar moves.
|
||||
|
||||
## References
|
||||
|
||||
The Volume-Price Trend (also "Price-Volume Trend") is a standard
|
||||
cumulative volume study; the `volume · ROC` accumulation here matches the
|
||||
common definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Obv.md](../volume/Indicator-Obv.md) — cumulative signed volume, the
|
||||
closest relative.
|
||||
- [Indicator-Adl.md](../volume/Indicator-Adl.md) — cumulative range-weighted volume.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -1,340 +0,0 @@
|
||||
# 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`.
|
||||
|
||||
This page documents two distinct public types — jump straight to
|
||||
[`Vwap` (cumulative)](#vwap-cumulative) or
|
||||
[`RollingVwap` (finite window)](#rollingvwap-finite-window).
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Item | Value |
|
||||
|---------------------|----------------------------------------------------------------|
|
||||
| Family | Volume |
|
||||
| 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
|
||||
**all four bindings**: as `RollingVwap` in Rust and `RollingVWAP` in
|
||||
Python, Node and WASM. The plain `VWAP` class in each binding remains the
|
||||
cumulative form; `RollingVWAP` is the finite-window variant.
|
||||
|
||||
### 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`.
|
||||
|
||||
#### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
high = np.array([10.0, 20.0, 30.0, 40.0])
|
||||
low = np.array([10.0, 20.0, 30.0, 40.0])
|
||||
close = np.array([10.0, 20.0, 30.0, 40.0])
|
||||
volume = np.array([ 1.0, 3.0, 1.0, 2.0])
|
||||
|
||||
rv = ta.RollingVWAP(3)
|
||||
print(rv.batch(high, low, close, volume))
|
||||
# [nan, nan, 20.0, 28.333333333333332]
|
||||
```
|
||||
|
||||
#### Node
|
||||
|
||||
```js
|
||||
const { RollingVWAP } = require('wickra');
|
||||
|
||||
const high = [10, 20, 30, 40];
|
||||
const low = [10, 20, 30, 40];
|
||||
const close = [10, 20, 30, 40];
|
||||
const volume = [ 1, 3, 1, 2];
|
||||
|
||||
const rv = new RollingVWAP(3);
|
||||
console.log(rv.batch(high, low, close, volume));
|
||||
// [ NaN, NaN, 20, 28.333333333333332 ]
|
||||
```
|
||||
|
||||
#### WASM
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import init, { RollingVWAP } from './pkg/wickra_wasm.js';
|
||||
await init();
|
||||
const rv = new RollingVWAP(3);
|
||||
const out = rv.batch(
|
||||
new Float64Array([10, 20, 30, 40]),
|
||||
new Float64Array([10, 20, 30, 40]),
|
||||
new Float64Array([10, 20, 30, 40]),
|
||||
new Float64Array([ 1, 3, 1, 2]),
|
||||
);
|
||||
console.log(Array.from(out));
|
||||
// [ NaN, NaN, 20, 28.333333333333332 ]
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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](../volume/Indicator-Obv.md) — cumulative signed-volume measure that pairs
|
||||
well with VWAP as a divergence flag.
|
||||
- [MFI](../momentum-oscillators/Indicator-Mfi.md) — money-flow oscillator that also blends
|
||||
typical price with volume.
|
||||
- [Bollinger Bands](../volatility-bands/Indicator-BollingerBands.md) — non-volume volatility
|
||||
envelope, often layered alongside VWAP on intraday charts.
|
||||
@@ -45,7 +45,7 @@ Then open the demo you want at `http://localhost:8000/examples/wasm/<file>`.
|
||||
|
||||
## See also
|
||||
|
||||
- [Quickstart: WASM](../../docs/wiki/Quickstart-WASM.md) — module-load
|
||||
- [Quickstart: WASM](https://github.com/kingchenc/wickra/wiki/Quickstart-WASM.md) — module-load
|
||||
flow, `wasm-pack` targets, and the streaming API.
|
||||
- [examples/README.md](../README.md) — cross-language index, including
|
||||
the Rust, Python and Node siblings of every demo above.
|
||||
|
||||
Reference in New Issue
Block a user