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:
kingchenc
2026-05-23 22:48:48 +02:00
parent e6375746d3
commit 4aec5d544c
88 changed files with 37 additions and 14778 deletions
+3 -2
View File
@@ -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
View File
@@ -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
+28
View File
@@ -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.
-185
View File
@@ -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.
-194
View File
@@ -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>
-123
View File
@@ -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
1030× 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.
-183
View File
@@ -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>
-179
View File
@@ -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>
-212
View File
@@ -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
(0100 / ±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(HL, |HprevC|, |LprevC|)`; 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>
-174
View File
@@ -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>
-181
View File
@@ -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>
-142
View File
@@ -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>
-154
View File
@@ -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>
-160
View File
@@ -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>
-93
View File
@@ -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.
-174
View File
@@ -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 7080 % 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_{t1}, 0)
loss_t = max(price_{t1} 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_{tperiod}`, 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_{tperiod}
```
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_{t1} 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 HL 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_{t1}
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_{t1})
BP_t = close_t true_low_t (buying pressure)
TR_t = max(high_t, close_{t1}) 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 35
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. **PriceSMA 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·(n1)+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_{tlag}
ZLEMA_t = EMA_period(de_lagged)_t
```
The trick (Ehlers & Way, 2010): `price_t price_{tlag}` 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_{t1}, close loss) while price holds above the stop
= min(stop_{t1}, 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
> highlow 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 highlow 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 highlow 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_{t1}| (positive vortex movement)
VM_t = |low_t high_{t1}| (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_{troc_period}) / smoothed_{troc_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_{t1})
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_{t1} + 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 = ((98)(129))/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_{t1} + low_{t1})/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_{t1}) · 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_{t1} + volume_t · (close_t close_{t1}) / close_{t1}
```
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 · (1110)/10 = 20`. Bar 3 adds
`300 · (911)/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.
+1 -1
View File
@@ -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.