From 4aec5d544c100e2a3e8254ccffd8018797298297 Mon Sep 17 00:00:00 2001 From: kingchenc Date: Sat, 23 May 2026 22:48:48 +0200 Subject: [PATCH] docs(wiki): migrate documentation out of repo into GitHub Wiki MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/PULL_REQUEST_TEMPLATE.md | 5 +- CONTRIBUTING.md | 8 +- docs/README.md | 28 ++ docs/wiki/Cookbook.md | 185 ---------- docs/wiki/Data-Layer.md | 194 ---------- docs/wiki/FAQ.md | 123 ------- docs/wiki/Home.md | 183 ---------- docs/wiki/Indicator-Chaining.md | 179 --------- docs/wiki/Indicators-Overview.md | 212 ----------- docs/wiki/Quickstart-Node.md | 174 --------- docs/wiki/Quickstart-Python.md | 181 ---------- docs/wiki/Quickstart-Rust.md | 142 -------- docs/wiki/Quickstart-WASM.md | 154 -------- docs/wiki/Streaming-vs-Batch.md | 160 --------- docs/wiki/TA-Lib-Migration.md | 93 ----- docs/wiki/Warmup-Periods.md | 174 --------- .../Indicator-AwesomeOscillator.md | 195 ---------- .../momentum-oscillators/Indicator-Cci.md | 198 ---------- .../momentum-oscillators/Indicator-Cmo.md | 155 -------- .../momentum-oscillators/Indicator-Mfi.md | 203 ----------- .../momentum-oscillators/Indicator-Mom.md | 151 -------- .../momentum-oscillators/Indicator-Pmo.md | 169 --------- .../momentum-oscillators/Indicator-Roc.md | 172 --------- .../momentum-oscillators/Indicator-Rsi.md | 213 ----------- .../Indicator-StochRsi.md | 164 --------- .../Indicator-Stochastic.md | 219 ----------- .../momentum-oscillators/Indicator-Tsi.md | 159 -------- .../Indicator-UltimateOscillator.md | 178 --------- .../Indicator-WilliamsR.md | 183 ---------- .../moving-averages/Indicator-Dema.md | 213 ----------- .../moving-averages/Indicator-Ema.md | 200 ----------- .../moving-averages/Indicator-Hma.md | 228 ------------ .../moving-averages/Indicator-Kama.md | 250 ------------- .../moving-averages/Indicator-Sma.md | 188 ---------- .../moving-averages/Indicator-Smma.md | 170 --------- .../moving-averages/Indicator-T3.md | 171 --------- .../moving-averages/Indicator-Tema.md | 207 ----------- .../moving-averages/Indicator-Trima.md | 166 --------- .../moving-averages/Indicator-Vwma.md | 175 --------- .../moving-averages/Indicator-Wma.md | 185 ---------- .../moving-averages/Indicator-Zlema.md | 166 --------- .../Indicator-AcceleratorOscillator.md | 144 -------- .../Indicator-BalanceOfPower.md | 138 ------- .../price-oscillators/Indicator-Coppock.md | 153 -------- .../price-oscillators/Indicator-Dpo.md | 161 --------- .../price-oscillators/Indicator-Ppo.md | 154 -------- .../price-statistics/Indicator-LinRegAngle.md | 143 -------- .../price-statistics/Indicator-LinRegSlope.md | 149 -------- .../Indicator-LinearRegression.md | 162 --------- .../price-statistics/Indicator-MedianPrice.md | 135 ------- .../Indicator-TypicalPrice.md | 136 ------- .../Indicator-WeightedClose.md | 136 ------- .../price-statistics/Indicator-ZScore.md | 139 ------- .../Indicator-AtrTrailingStop.md | 169 --------- .../Indicator-ChandeKrollStop.md | 176 --------- .../Indicator-ChandelierExit.md | 165 --------- .../trailing-stops/Indicator-Psar.md | 257 ------------- .../trailing-stops/Indicator-SuperTrend.md | 173 --------- .../trend-directional/Indicator-Adx.md | 244 ------------- .../trend-directional/Indicator-Aroon.md | 205 ----------- .../Indicator-AroonOscillator.md | 158 -------- .../Indicator-ChoppinessIndex.md | 146 -------- .../Indicator-MacdIndicator.md | 215 ----------- .../trend-directional/Indicator-MassIndex.md | 172 --------- .../trend-directional/Indicator-Trix.md | 186 ---------- .../Indicator-VerticalHorizontalFilter.md | 140 -------- .../trend-directional/Indicator-Vortex.md | 159 -------- .../volatility-bands/Indicator-Atr.md | 225 ------------ .../Indicator-BollingerBands.md | 261 -------------- .../Indicator-BollingerBandwidth.md | 155 -------- .../Indicator-ChaikinVolatility.md | 154 -------- .../volatility-bands/Indicator-Donchian.md | 213 ----------- .../Indicator-HistoricalVolatility.md | 170 --------- .../volatility-bands/Indicator-Keltner.md | 214 ----------- .../volatility-bands/Indicator-Natr.md | 143 -------- .../volatility-bands/Indicator-PercentB.md | 147 -------- .../volatility-bands/Indicator-StdDev.md | 155 -------- .../volatility-bands/Indicator-TrueRange.md | 147 -------- .../volatility-bands/Indicator-UlcerIndex.md | 160 --------- docs/wiki/indicators/volume/Indicator-Adl.md | 160 --------- .../volume/Indicator-ChaikinMoneyFlow.md | 158 -------- .../volume/Indicator-ChaikinOscillator.md | 160 --------- .../volume/Indicator-EaseOfMovement.md | 160 --------- .../indicators/volume/Indicator-ForceIndex.md | 154 -------- docs/wiki/indicators/volume/Indicator-Obv.md | 189 ---------- .../volume/Indicator-VolumePriceTrend.md | 160 --------- docs/wiki/indicators/volume/Indicator-Vwap.md | 340 ------------------ examples/wasm/README.md | 2 +- 88 files changed, 37 insertions(+), 14778 deletions(-) create mode 100644 docs/README.md delete mode 100644 docs/wiki/Cookbook.md delete mode 100644 docs/wiki/Data-Layer.md delete mode 100644 docs/wiki/FAQ.md delete mode 100644 docs/wiki/Home.md delete mode 100644 docs/wiki/Indicator-Chaining.md delete mode 100644 docs/wiki/Indicators-Overview.md delete mode 100644 docs/wiki/Quickstart-Node.md delete mode 100644 docs/wiki/Quickstart-Python.md delete mode 100644 docs/wiki/Quickstart-Rust.md delete mode 100644 docs/wiki/Quickstart-WASM.md delete mode 100644 docs/wiki/Streaming-vs-Batch.md delete mode 100644 docs/wiki/TA-Lib-Migration.md delete mode 100644 docs/wiki/Warmup-Periods.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-AwesomeOscillator.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Cci.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Cmo.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Mfi.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Mom.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Pmo.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Roc.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Rsi.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-StochRsi.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Stochastic.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-Tsi.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-UltimateOscillator.md delete mode 100644 docs/wiki/indicators/momentum-oscillators/Indicator-WilliamsR.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Dema.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Ema.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Hma.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Kama.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Sma.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Smma.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-T3.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Tema.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Trima.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Vwma.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Wma.md delete mode 100644 docs/wiki/indicators/moving-averages/Indicator-Zlema.md delete mode 100644 docs/wiki/indicators/price-oscillators/Indicator-AcceleratorOscillator.md delete mode 100644 docs/wiki/indicators/price-oscillators/Indicator-BalanceOfPower.md delete mode 100644 docs/wiki/indicators/price-oscillators/Indicator-Coppock.md delete mode 100644 docs/wiki/indicators/price-oscillators/Indicator-Dpo.md delete mode 100644 docs/wiki/indicators/price-oscillators/Indicator-Ppo.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-LinRegAngle.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-LinRegSlope.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-LinearRegression.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-MedianPrice.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-TypicalPrice.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-WeightedClose.md delete mode 100644 docs/wiki/indicators/price-statistics/Indicator-ZScore.md delete mode 100644 docs/wiki/indicators/trailing-stops/Indicator-AtrTrailingStop.md delete mode 100644 docs/wiki/indicators/trailing-stops/Indicator-ChandeKrollStop.md delete mode 100644 docs/wiki/indicators/trailing-stops/Indicator-ChandelierExit.md delete mode 100644 docs/wiki/indicators/trailing-stops/Indicator-Psar.md delete mode 100644 docs/wiki/indicators/trailing-stops/Indicator-SuperTrend.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-Adx.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-Aroon.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-AroonOscillator.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-ChoppinessIndex.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-MacdIndicator.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-MassIndex.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-Trix.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-VerticalHorizontalFilter.md delete mode 100644 docs/wiki/indicators/trend-directional/Indicator-Vortex.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-Atr.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-BollingerBands.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-BollingerBandwidth.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-ChaikinVolatility.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-Donchian.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-HistoricalVolatility.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-Keltner.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-Natr.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-PercentB.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-StdDev.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-TrueRange.md delete mode 100644 docs/wiki/indicators/volatility-bands/Indicator-UlcerIndex.md delete mode 100644 docs/wiki/indicators/volume/Indicator-Adl.md delete mode 100644 docs/wiki/indicators/volume/Indicator-ChaikinMoneyFlow.md delete mode 100644 docs/wiki/indicators/volume/Indicator-ChaikinOscillator.md delete mode 100644 docs/wiki/indicators/volume/Indicator-EaseOfMovement.md delete mode 100644 docs/wiki/indicators/volume/Indicator-ForceIndex.md delete mode 100644 docs/wiki/indicators/volume/Indicator-Obv.md delete mode 100644 docs/wiki/indicators/volume/Indicator-VolumePriceTrend.md delete mode 100644 docs/wiki/indicators/volume/Indicator-Vwap.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0f72007e..408d8dca 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 390d1113..7a391e5b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..c8ca53ae --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/wiki/Cookbook.md b/docs/wiki/Cookbook.md deleted file mode 100644 index 5792033c..00000000 --- a/docs/wiki/Cookbook.md +++ /dev/null @@ -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 = 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> = 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. diff --git a/docs/wiki/Data-Layer.md b/docs/wiki/Data-Layer.md deleted file mode 100644 index f84bc754..00000000 --- a/docs/wiki/Data-Layer.md +++ /dev/null @@ -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 - -// 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: diff --git a/docs/wiki/FAQ.md b/docs/wiki/FAQ.md deleted file mode 100644 index b9c72ff4..00000000 --- a/docs/wiki/FAQ.md +++ /dev/null @@ -1,123 +0,0 @@ -# FAQ - -Frequently asked questions about Wickra. If yours is not here, check the -[issue tracker](https://github.com/kingchenc/wickra/issues) or open a new -issue. - -## Will batch and streaming produce the same result? - -Yes — bit-identical, by construction. `batch(prices)` is a one-line wrapper -that calls `update(p)` for every `p` in the input. The same unit test — -`batch_equals_streaming` — pins this for every indicator. See -[Streaming vs Batch](Streaming-vs-Batch.md) for the full contract. - -## What does `warmup_period()` mean? - -It's the number of inputs an indicator needs before it emits its first -non-`None` value. For RSI(14) that's 15 (14 diffs plus the seed); for -SMA(20) it's 20; for MACD(12, 26, 9) it's 34 (`slow + signal − 1`). After -warmup the indicator never goes back to `None`. The complete table lives -at [Warmup Periods](Warmup-Periods.md). - -## Why am I getting `None` / `NaN` for the first N values? - -That's the warmup. Use `is_ready()` (or the corresponding `isReady()` in -Node, `is_ready()` in Python) to gate your code on "do I have a real -value yet?" rather than counting inputs yourself: - -```python -import wickra as ta -rsi = ta.RSI(14) -for price in feed: - rsi.update(price) - if rsi.is_ready(): - ... -``` - -## Which indicator should I use for X? - -A short cheat-sheet (full version at the bottom of -[Indicators Overview](Indicators-Overview.md)): - -- **trend direction** → MA family (`SMA`, `EMA`, `HMA`, `T3`, `KAMA`) -- **trend strength** → `ADX`, `ChoppinessIndex`, `VerticalHorizontalFilter` -- **overbought / oversold** → `RSI`, `Stochastic`, `Williams %R`, `MFI` -- **volatility** → `ATR`, `TrueRange`, `ChaikinVolatility`, `StdDev` -- **breakout level** → `Donchian`, `BollingerBands` -- **trailing stop** → `PSAR`, `SuperTrend`, `ChandelierExit`, - `AtrTrailingStop` -- **volume confirmation** → `OBV`, `ChaikinMoneyFlow`, `VWAP` - -## Is a single indicator instance thread-safe? - -No. `update` mutates state, so a single instance must not be shared across -threads. Each thread should own its own indicator. For multi-asset -parallelism, the Rust crate provides `BatchExt::batch_parallel`, which -fans out over many series each with its own fresh instance behind the -default `parallel` feature (rayon). Node's `worker_threads` gives the -same shape from JavaScript — see `examples/node/parallel_assets.js`. - -## Does Wickra need a system compiler to install? - -No. Every published wheel (PyPI), npm package, and crate ships pre-built -artefacts. `pip install wickra` and `npm install wickra` are -no-prerequisite installs on Linux, macOS, and Windows x64 / arm64. The -only time you need a toolchain is when you are building Wickra from -source. - -## How do I handle non-finite inputs (NaN / Inf)? - -The scalar indicators (`SMA`, `EMA`, `WMA`, `RSI`, `ROC`, …) return the -most recent valid value when fed a non-finite input, leaving their state -untouched. That lets a missing price in your feed pass through without -poisoning the rest of the series. `ATR` and the volume-aware indicators -reject non-finite volume at the `Candle::new` boundary, so an aggregator -that overflows surfaces an error instead of producing a corrupted candle -(see [Data Layer](Data-Layer.md)). - -## How fast is Wickra? - -The streaming path is O(1) per `update` — the per-tick cost does not grow -with how much history you have already seen. The README has a benchmark -table comparing Wickra against `finta` and `talipp`; the gap is roughly -10–30× on batch workloads and ~17× per tick on a streaming RSI seeded -with 2 000 historical bars. - -## How do I add a custom indicator? - -Implement the `Indicator` trait in -`crates/wickra-core/src/indicators/.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. diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md deleted file mode 100644 index 820053a1..00000000 --- a/docs/wiki/Home.md +++ /dev/null @@ -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: -. - -## 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//` 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: -- Releases: -- Issue tracker: diff --git a/docs/wiki/Indicator-Chaining.md b/docs/wiki/Indicator-Chaining.md deleted file mode 100644 index ebad4556..00000000 --- a/docs/wiki/Indicator-Chaining.md +++ /dev/null @@ -1,179 +0,0 @@ -# Indicator Chaining - -`Chain` 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, Rsi>`. You can keep chaining -indefinitely; the `Chain` produced at each step also implements -`Indicator`, 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` 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> = chain.batch(&prices); -``` - -## Stages from different families - -The bridging type is `f64`, so anything `Indicator` -can serve as the first stage and anything `Indicator` 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: diff --git a/docs/wiki/Indicators-Overview.md b/docs/wiki/Indicators-Overview.md deleted file mode 100644 index be5e840b..00000000 --- a/docs/wiki/Indicators-Overview.md +++ /dev/null @@ -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//` directory layout. - -Every indicator is an O(1) state machine that consumes one input at a time -and produces either `Option` (Rust), `float | None` (Python), or -`number | null` (Node). Inputs are either a `f64` close price or an OHLCV -`Candle` (Rust) / dict-or-tuple (Python) / column arrays (Node). The full -trait surface and warmup-period semantics are covered in -[Quickstart: Rust](Quickstart-Rust.md) and [Warmup Periods](Warmup-Periods.md). - -The "Output range" column is the value bounds an indicator emits once warm; -"unbounded" means it tracks the price scale of the input. The "Warmup" column -quotes `warmup_period()` as the indicator reports it — the **exact** -first-emission index: the first non-`None` output lands on input -`warmup_period()` (0-indexed `warmup_period() - 1`). - -The eight families: - -| # | Family | Count | What it answers | -|---|--------|-------|-----------------| -| 1 | [Moving Averages](#moving-averages) | 12 | Where is the smoothed trend line? | -| 2 | [Momentum Oscillators](#momentum-oscillators) | 13 | How fast is price changing; is it overbought? | -| 3 | [Trend & Directional](#trend--directional) | 9 | Is there a trend, and which way? | -| 4 | [Price Oscillators](#price-oscillators) | 5 | Difference-of-averages momentum around zero. | -| 5 | [Volatility & Bands](#volatility--bands) | 12 | How wide is the range; where are the envelopes? | -| 6 | [Trailing Stops](#trailing-stops) | 5 | Where is the stop-loss for this trend? | -| 7 | [Volume](#volume) | 8 | Is volume confirming the move? | -| 8 | [Price Statistics](#price-statistics) | 7 | Per-bar price transforms and rolling regressions. | - -## Moving Averages - -Smooth the price series to surface direction. All are single-input, -single-output (`f64 → f64`) except `Vwma`, which weights by volume. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Sma` | Equal-weighted rolling mean over `period` closes. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Sma.md](indicators/moving-averages/Indicator-Sma.md) | -| `Ema` | EMA with `α = 2 / (period + 1)`, SMA-seeded. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Ema.md](indicators/moving-averages/Indicator-Ema.md) | -| `Wma` | Linear weights `1, 2, …, period`; newest bar matters most. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Wma.md](indicators/moving-averages/Indicator-Wma.md) | -| `Dema` | Mulloy's `2·EMA − EMA(EMA)`; removes first-order EMA lag. | `f64` | `f64` | unbounded (price scale) | `period` | `2·period − 1` | [Indicator-Dema.md](indicators/moving-averages/Indicator-Dema.md) | -| `Tema` | Mulloy's `3·EMA − 3·EMA(EMA) + EMA(EMA(EMA))`. | `f64` | `f64` | unbounded (price scale) | `period` | `3·period − 2` | [Indicator-Tema.md](indicators/moving-averages/Indicator-Tema.md) | -| `Hma` | Hull's near-zero-lag `WMA(2·WMA(n/2) − WMA(n), √n)`. | `f64` | `f64` | unbounded (price scale) | `period` | `period + round(√period) − 1` | [Indicator-Hma.md](indicators/moving-averages/Indicator-Hma.md) | -| `Kama` | Kaufman's adaptive average; efficiency ratio picks α per bar. | `f64` | `f64` | unbounded (price scale) | `(er_period=10, fast=2, slow=30)` | `er_period + 1` | [Indicator-Kama.md](indicators/moving-averages/Indicator-Kama.md) | -| `Smma` | Wilder's RMA: SMA-seeded exponential average, `1/period` factor. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Smma.md](indicators/moving-averages/Indicator-Smma.md) | -| `Trima` | A `period`-window SMA applied twice; triangular weights. | `f64` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Trima.md](indicators/moving-averages/Indicator-Trima.md) | -| `Zlema` | EMA of the de-lagged series `2·price − price[lag]`. | `f64` | `f64` | unbounded (price scale) | `period` | `lag + period` | [Indicator-Zlema.md](indicators/moving-averages/Indicator-Zlema.md) | -| `T3` | Tillson's six-EMA cascade recombined with a volume factor `v`. | `f64` | `f64` | unbounded (price scale) | `(period, v=0.7)` (Python) | `6·period − 5` | [Indicator-T3.md](indicators/moving-averages/Indicator-T3.md) | -| `Vwma` | Rolling mean of closes weighted by each bar's volume. | `Candle` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Vwma.md](indicators/moving-averages/Indicator-Vwma.md) | - -## Momentum Oscillators - -Measure the *rate* of price change. Several are bounded by construction -(0–100 / ±100 oscillators), the rest are difference-driven. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Rsi` | Wilder's RSI; smoothed `gain / (gain + loss) × 100`. | `f64` | `f64` | `[0, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Rsi.md](indicators/momentum-oscillators/Indicator-Rsi.md) | -| `Stochastic` | `%K = (close − low_n)/(high_n − low_n) × 100`, smoothed into `%D`. | `Candle` | `(k, d)` | each in `[0, 100]` | `(k_period=14, d_period=3)` (Python) | `k_period + d_period − 1` | [Indicator-Stochastic.md](indicators/momentum-oscillators/Indicator-Stochastic.md) | -| `Cci` | `(typical − SMA(typical)) / (0.015 · mean_dev)`. | `Candle` | `f64` | unbounded (typically `±100`–`±200`) | `period = 20` (Python) | `period` | [Indicator-Cci.md](indicators/momentum-oscillators/Indicator-Cci.md) | -| `Roc` | `(price − price_n) / price_n × 100`; raw percentage change. | `f64` | `f64` | unbounded around zero | `period` | `period + 1` | [Indicator-Roc.md](indicators/momentum-oscillators/Indicator-Roc.md) | -| `WilliamsR` | `−100 × (high_n − close) / (high_n − low_n)`. | `Candle` | `f64` | `[−100, 0]` | `period = 14` (Python) | `period` | [Indicator-WilliamsR.md](indicators/momentum-oscillators/Indicator-WilliamsR.md) | -| `Mfi` | "Volume-weighted RSI": Wilder smoothing of money-flow ratios. | `Candle` | `f64` | `[0, 100]` | `period = 14` (Python) | `period` | [Indicator-Mfi.md](indicators/momentum-oscillators/Indicator-Mfi.md) | -| `AwesomeOscillator` | `SMA(median, fast) − SMA(median, slow)`; zero-line crossover. | `Candle` | `f64` | unbounded around zero | `(fast=5, slow=34)` (Python) | `slow_period` | [Indicator-AwesomeOscillator.md](indicators/momentum-oscillators/Indicator-AwesomeOscillator.md) | -| `Mom` | `price − price[period]`; raw price-difference momentum. | `f64` | `f64` | unbounded around zero | `period = 10` (Python) | `period + 1` | [Indicator-Mom.md](indicators/momentum-oscillators/Indicator-Mom.md) | -| `Cmo` | Chande Momentum Oscillator; `100·(Σgain − Σloss)/(Σgain + Σloss)`. | `f64` | `f64` | `[−100, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Cmo.md](indicators/momentum-oscillators/Indicator-Cmo.md) | -| `Tsi` | True Strength Index; double-EMA-smoothed momentum ratio. | `f64` | `f64` | ≈ `[−100, 100]` | `(long=25, short=13)` (Python) | `long + short` | [Indicator-Tsi.md](indicators/momentum-oscillators/Indicator-Tsi.md) | -| `Pmo` | DecisionPoint Price Momentum Oscillator; doubly-smoothed ROC. | `f64` | `f64` | unbounded around zero | `(smoothing1=35, smoothing2=20)` (Python) | `2` | [Indicator-Pmo.md](indicators/momentum-oscillators/Indicator-Pmo.md) | -| `StochRsi` | Stochastic Oscillator applied to the RSI series. | `f64` | `f64` | `[0, 100]` | `(rsi_period=14, stoch_period=14)` (Python) | `rsi_period + stoch_period` | [Indicator-StochRsi.md](indicators/momentum-oscillators/Indicator-StochRsi.md) | -| `UltimateOscillator` | Larry Williams' weighted three-timeframe buying-pressure oscillator. | `Candle` | `f64` | `[0, 100]` | `(short=7, mid=14, long=28)` (Python) | `max(short,mid,long) + 1` | [Indicator-UltimateOscillator.md](indicators/momentum-oscillators/Indicator-UltimateOscillator.md) | - -## Trend & Directional - -Answer whether a trend exists and which way it points — directional systems, -crossover packages and trend-versus-range filters. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `MacdIndicator` | `EMA(fast) − EMA(slow)` plus a signal EMA and the histogram. | `f64` | `(macd, signal, histogram)` | unbounded around zero | `(fast=12, slow=26, signal=9)` (Python) | `slow + signal − 1` | [Indicator-MacdIndicator.md](indicators/trend-directional/Indicator-MacdIndicator.md) | -| `Adx` | Wilder's directional system: `+DI`, `−DI` and the `ADX` strength index. | `Candle` | `(plus_di, minus_di, adx)` | each in `[0, 100]` | `period = 14` (Python) | `2·period` | [Indicator-Adx.md](indicators/trend-directional/Indicator-Adx.md) | -| `Aroon` | Bars-since-high and bars-since-low scaled to `[0, 100]`. | `Candle` | `(up, down)` | each in `[0, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-Aroon.md](indicators/trend-directional/Indicator-Aroon.md) | -| `Trix` | Rate of change of a triple-smoothed EMA, `× 10000`. | `f64` | `f64` | unbounded around zero | `period = 15` (Python) | `3·period − 1` | [Indicator-Trix.md](indicators/trend-directional/Indicator-Trix.md) | -| `AroonOscillator` | `AroonUp − AroonDown`; the two Aroon lines as one gauge. | `Candle` | `f64` | `[−100, 100]` | `period = 14` (Python) | `period + 1` | [Indicator-AroonOscillator.md](indicators/trend-directional/Indicator-AroonOscillator.md) | -| `Vortex` | Vortex Indicator `VI+` / `VI−`; crossings mark trend onset. | `Candle` | `(plus, minus)` | each `>= 0` | `period = 14` (Python) | `period + 1` | [Indicator-Vortex.md](indicators/trend-directional/Indicator-Vortex.md) | -| `MassIndex` | Dorsey's range-expansion sum of the EMA-of-range ratio. | `Candle` | `f64` | `> 0` | `(ema_period=9, sum_period=25)` (Python) | `2·ema_period + sum_period − 2` | [Indicator-MassIndex.md](indicators/trend-directional/Indicator-MassIndex.md) | -| `ChoppinessIndex` | Summed true range over the high-low span, log-scaled. | `Candle` | `f64` | `[0, 100]` | `period = 14` (Python) | `period` | [Indicator-ChoppinessIndex.md](indicators/trend-directional/Indicator-ChoppinessIndex.md) | -| `VerticalHorizontalFilter` | Net price move divided by total move over `period`. | `f64` | `f64` | `[0, 1]` | `period = 28` (Python) | `period + 1` | [Indicator-VerticalHorizontalFilter.md](indicators/trend-directional/Indicator-VerticalHorizontalFilter.md) | - -## Price Oscillators - -Difference-of-averages and intrabar oscillators that swing around a zero line. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Ppo` | Percentage Price Oscillator; `100·(EMA_fast − EMA_slow)/EMA_slow`. | `f64` | `f64` | unbounded around zero (percent) | `(fast=12, slow=26)` (Python) | `slow` | [Indicator-Ppo.md](indicators/price-oscillators/Indicator-Ppo.md) | -| `Dpo` | Detrended Price Oscillator; `price[t − period/2 − 1] − SMA(period)`. | `f64` | `f64` | unbounded around zero | `period = 20` (Python) | `max(period, period/2 + 2)` | [Indicator-Dpo.md](indicators/price-oscillators/Indicator-Dpo.md) | -| `Coppock` | Coppock Curve; `WMA(ROC(long) + ROC(short), wma_period)`. | `f64` | `f64` | unbounded around zero | `(roc_long=14, roc_short=11, wma_period=10)` (Python) | `max(roc_long, roc_short) + wma_period` | [Indicator-Coppock.md](indicators/price-oscillators/Indicator-Coppock.md) | -| `AcceleratorOscillator` | `AO − SMA(AO, signal)`; the acceleration of momentum. | `Candle` | `f64` | unbounded around zero | `(ao_fast=5, ao_slow=34, signal_period=5)` (Python) | `ao_slow + signal_period − 1` | [Indicator-AcceleratorOscillator.md](indicators/price-oscillators/Indicator-AcceleratorOscillator.md) | -| `BalanceOfPower` | `(close − open) / (high − low)`; intrabar buyer/seller control. | `Candle` | `f64` | `[−1, +1]` | (no parameters) | `1` | [Indicator-BalanceOfPower.md](indicators/price-oscillators/Indicator-BalanceOfPower.md) | - -## Volatility & Bands - -Indicators that measure dispersion / range and those that draw an envelope -around price. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Atr` | Wilder-smoothed True Range; per-bar absolute volatility. | `Candle` | `f64` | `[0, ∞)` (price scale) | `period = 14` (Python) | `period` | [Indicator-Atr.md](indicators/volatility-bands/Indicator-Atr.md) | -| `BollingerBands` | SMA middle band with `±multiplier × population_stddev` bands. | `f64` | `(upper, middle, lower, stddev)` | unbounded (price scale) | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-BollingerBands.md](indicators/volatility-bands/Indicator-BollingerBands.md) | -| `Keltner` | EMA middle band with `±multiplier × ATR` bands. | `Candle` | `(upper, middle, lower)` | unbounded (price scale) | `(ema_period=20, atr_period=10, multiplier=2.0)` (Python) | `max(ema_period, atr_period)` | [Indicator-Keltner.md](indicators/volatility-bands/Indicator-Keltner.md) | -| `Donchian` | Highest high and lowest low over `period` bars. | `Candle` | `(upper, middle, lower)` | unbounded (price scale) | `period = 20` (Python) | `period` | [Indicator-Donchian.md](indicators/volatility-bands/Indicator-Donchian.md) | -| `Natr` | `100·ATR/close`; ATR as a percentage. | `Candle` | `f64` | `[0, ∞)` (percent) | `period = 14` (Python) | `period` | [Indicator-Natr.md](indicators/volatility-bands/Indicator-Natr.md) | -| `StdDev` | Rolling population standard deviation of price. | `f64` | `f64` | `[0, ∞)` (price scale) | `period = 20` (Python) | `period` | [Indicator-StdDev.md](indicators/volatility-bands/Indicator-StdDev.md) | -| `UlcerIndex` | RMS of trailing-high drawdowns; downside-only risk. | `f64` | `f64` | `[0, ∞)` (percent) | `period = 14` (Python) | `2·period − 1` | [Indicator-UlcerIndex.md](indicators/volatility-bands/Indicator-UlcerIndex.md) | -| `HistoricalVolatility` | Annualised sample stddev of log returns. | `f64` | `f64` | `[0, ∞)` (annualised percent) | `(period=20, trading_periods=252)` (Python) | `period + 1` | [Indicator-HistoricalVolatility.md](indicators/volatility-bands/Indicator-HistoricalVolatility.md) | -| `BollingerBandwidth` | `(upper − lower) / middle` of the Bollinger Bands. | `f64` | `f64` | `[0, ∞)` | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-BollingerBandwidth.md](indicators/volatility-bands/Indicator-BollingerBandwidth.md) | -| `PercentB` | `(price − lower) / (upper − lower)`; price position in the bands. | `f64` | `f64` | unbounded (`0`–`1` inside) | `(period=20, multiplier=2.0)` (Python) | `period` | [Indicator-PercentB.md](indicators/volatility-bands/Indicator-PercentB.md) | -| `TrueRange` | `max(H−L, |H−prevC|, |L−prevC|)`; raw single-bar volatility. | `Candle` | `f64` | `[0, ∞)` (price scale) | (no parameters) | `1` | [Indicator-TrueRange.md](indicators/volatility-bands/Indicator-TrueRange.md) | -| `ChaikinVolatility` | Rate of change of an EMA-smoothed high-low spread. | `Candle` | `f64` | unbounded around zero (percent) | `(ema_period=10, roc_period=10)` (Python) | `ema_period + roc_period` | [Indicator-ChaikinVolatility.md](indicators/volatility-bands/Indicator-ChaikinVolatility.md) | - -## Trailing Stops - -ATR-driven stop-loss trackers: per-bar levels that follow a trend and flip -when price closes through them. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Psar` | Wilder's Parabolic Stop-and-Reverse; flips sides on a crossing. | `Candle` | `f64` | unbounded (price scale) | `(af_start=0.02, af_step=0.02, af_max=0.20)` (Python) | `2` | [Indicator-Psar.md](indicators/trailing-stops/Indicator-Psar.md) | -| `SuperTrend` | ATR-banded trailing stop with explicit flip logic. | `Candle` | `(value, direction)` | `value` price scale; `direction` `±1` | `(atr_period=10, multiplier=3.0)` (Python) | `atr_period` | [Indicator-SuperTrend.md](indicators/trailing-stops/Indicator-SuperTrend.md) | -| `ChandelierExit` | `highest_high − k·ATR` (long) and `lowest_low + k·ATR` (short). | `Candle` | `(long_stop, short_stop)` | unbounded (price scale) | `(period=22, multiplier=3.0)` (Python) | `period` | [Indicator-ChandelierExit.md](indicators/trailing-stops/Indicator-ChandelierExit.md) | -| `ChandeKrollStop` | Two-stage ATR stop: extreme-based, then smoothed. | `Candle` | `(stop_long, stop_short)` | unbounded (price scale) | `(atr_period=10, atr_multiplier=1.0, stop_period=9)` (Python) | `atr_period + stop_period − 1` | [Indicator-ChandeKrollStop.md](indicators/trailing-stops/Indicator-ChandeKrollStop.md) | -| `AtrTrailingStop` | A single line trailing the close by `k·ATR`, ratcheting. | `Candle` | `f64` | unbounded (price scale) | `(atr_period=14, multiplier=3.0)` (Python) | `atr_period` | [Indicator-AtrTrailingStop.md](indicators/trailing-stops/Indicator-AtrTrailingStop.md) | - -## Volume - -Price moves weighted or confirmed by traded volume. All take `Candle` input. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `Obv` | On-Balance Volume: cumulative signed volume. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-Obv.md](indicators/volume/Indicator-Obv.md) | -| `Vwap` | Cumulative volume-weighted average price from the stream start; a sibling `RollingVwap(period)` is exposed for a finite window. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` (cumulative); `period` (rolling) | [Indicator-Vwap.md](indicators/volume/Indicator-Vwap.md) (cumulative + [rolling](indicators/volume/Indicator-Vwap.md#rollingvwap-finite-window)) | -| `Adl` | Accumulation/Distribution Line; cumulative range-weighted volume. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-Adl.md](indicators/volume/Indicator-Adl.md) | -| `VolumePriceTrend` | Cumulative `volume · ROC`; volume weighted by percentage move. | `Candle` | `f64` | unbounded (drifts with volume) | (no parameters) | `1` | [Indicator-VolumePriceTrend.md](indicators/volume/Indicator-VolumePriceTrend.md) | -| `ChaikinMoneyFlow` | Summed money-flow volume over summed volume across `period` bars. | `Candle` | `f64` | `[−1, +1]` | `period = 20` (Python) | `period` | [Indicator-ChaikinMoneyFlow.md](indicators/volume/Indicator-ChaikinMoneyFlow.md) | -| `ChaikinOscillator` | `EMA(ADL, fast) − EMA(ADL, slow)`; the MACD of the ADL. | `Candle` | `f64` | unbounded around zero | `(fast=3, slow=10)` (Python) | `slow` | [Indicator-ChaikinOscillator.md](indicators/volume/Indicator-ChaikinOscillator.md) | -| `ForceIndex` | `EMA((close − prev_close) · volume, period)`. | `Candle` | `f64` | unbounded around zero | `period = 13` (Python) | `period + 1` | [Indicator-ForceIndex.md](indicators/volume/Indicator-ForceIndex.md) | -| `EaseOfMovement` | `SMA` of distance travelled per unit of volume. | `Candle` | `f64` | unbounded around zero | `(period=14, divisor=1e8)` (Python) | `period + 1` | [Indicator-EaseOfMovement.md](indicators/volume/Indicator-EaseOfMovement.md) | - -## Price Statistics - -Per-bar price transforms and rolling least-squares regressions. - -| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive | -|-----------|-----------|-------|--------|-------|----------|--------|-----------| -| `TypicalPrice` | `(high + low + close) / 3`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-TypicalPrice.md](indicators/price-statistics/Indicator-TypicalPrice.md) | -| `MedianPrice` | `(high + low) / 2`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-MedianPrice.md](indicators/price-statistics/Indicator-MedianPrice.md) | -| `WeightedClose` | `(high + low + 2·close) / 4`. | `Candle` | `f64` | unbounded (price scale) | (no parameters) | `1` | [Indicator-WeightedClose.md](indicators/price-statistics/Indicator-WeightedClose.md) | -| `LinearRegression` | Endpoint of the rolling least-squares line. | `f64` | `f64` | unbounded (price scale) | `period = 14` (Python) | `period` | [Indicator-LinearRegression.md](indicators/price-statistics/Indicator-LinearRegression.md) | -| `LinRegSlope` | Slope of the rolling least-squares line. | `f64` | `f64` | unbounded around zero | `period = 14` (Python) | `period` | [Indicator-LinRegSlope.md](indicators/price-statistics/Indicator-LinRegSlope.md) | -| `ZScore` | `(price − SMA(n)) / population_stddev(n)`. | `f64` | `f64` | unbounded around zero | `period = 20` (Python) | `period` | [Indicator-ZScore.md](indicators/price-statistics/Indicator-ZScore.md) | -| `LinRegAngle` | The rolling regression slope as a degree angle. | `f64` | `f64` | `(−90°, +90°)` | `period = 14` (Python) | `period` | [Indicator-LinRegAngle.md](indicators/price-statistics/Indicator-LinRegAngle.md) | - -## Pick the right indicator for… - -A short cheat-sheet of "I want X, which indicator?" answers, grounded in -what each indicator actually computes. - -- **Fast trend filter, minimal lag.** `Hma` for smoothness + responsiveness, - `Tema` for further lag reduction at the cost of noise, `Kama` for - adaptiveness instead of fixed lag. -- **Slow trend filter.** `Sma` is the simplest; `Ema` responds slightly - faster with the same smoothness budget. -- **Trend-following crossovers.** Two-line crossovers are the textbook entry; - `MacdIndicator` packages the idea with a signal line and histogram. -- **Trend strength — is there a trend at all?** `Adx` (`> 25` trending, - `< 20` ranging); `ChoppinessIndex` / `VerticalHorizontalFilter` answer the - same question without a direction. -- **Overbought / oversold.** `Rsi` is the default; `Stochastic` for faster - signals; `WilliamsR` for an inverted scale; `Mfi` for a volume-aware RSI. -- **Volatility level vs. momentum.** `Atr` / `TrueRange` for the level; - `ChaikinVolatility` for whether ranges are expanding or contracting. -- **Breakout level.** `Donchian` upper/lower bands are the Turtle-style - trigger. -- **Trailing stop.** `Psar`, `SuperTrend`, `ChandelierExit`, - `ChandeKrollStop` and `AtrTrailingStop` are a whole family of them. -- **Volume confirmation.** `Obv` is the simplest; `ChaikinMoneyFlow` is a - bounded balance; `Vwap` / `RollingVwap` give a volume-weighted reference. -- **Mean reversion.** `ZScore` flags statistically stretched prices; - `BollingerBandwidth` / `PercentB` locate price within the bands. - -## Source-of-truth files - -Every claim above can be checked against the source in -[`crates/wickra-core/src/indicators/`](https://github.com/kingchenc/wickra/tree/main/crates/wickra-core/src/indicators) -— one file per indicator. The Rust unit tests inside each module are the -ground truth for sample values. Python defaults (the `period = 14` etc.) come -from the `#[pyo3(signature = …)]` attributes in -[`bindings/python/src/lib.rs`](https://github.com/kingchenc/wickra/blob/main/bindings/python/src/lib.rs); -indicators without a Python default require an explicit argument. - -## See also - -- [Warmup Periods](Warmup-Periods.md) — verified table of every indicator's - `warmup_period()`. -- [Indicator Chaining](Indicator-Chaining.md) — combining indicators with - `Chain` and the stacked-warmup rule. -- [Quickstart: Rust](Quickstart-Rust.md), [Quickstart: Python](Quickstart-Python.md), - [Quickstart: Node](Quickstart-Node.md) — language-specific API surfaces. -- Source: diff --git a/docs/wiki/Quickstart-Node.md b/docs/wiki/Quickstart-Node.md deleted file mode 100644 index d8bf015f..00000000 --- a/docs/wiki/Quickstart-Node.md +++ /dev/null @@ -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`. 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` 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` with `NaN` warmup.
Multi-output: flat interleaved `Array`. | -| `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: diff --git a/docs/wiki/Quickstart-Python.md b/docs/wiki/Quickstart-Python.md deleted file mode 100644 index 70a58fae..00000000 --- a/docs/wiki/Quickstart-Python.md +++ /dev/null @@ -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: diff --git a/docs/wiki/Quickstart-Rust.md b/docs/wiki/Quickstart-Rust.md deleted file mode 100644 index a264ba6a..00000000 --- a/docs/wiki/Quickstart-Rust.md +++ /dev/null @@ -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; - 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> { - // 1. Batch: SMA(3) over five prices. - let mut sma = Sma::new(3)?; - let out: Vec> = 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` 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> { - // 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: diff --git a/docs/wiki/Quickstart-WASM.md b/docs/wiki/Quickstart-WASM.md deleted file mode 100644 index b13c981e..00000000 --- a/docs/wiki/Quickstart-WASM.md +++ /dev/null @@ -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 -``` - -`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 -``. 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: diff --git a/docs/wiki/Streaming-vs-Batch.md b/docs/wiki/Streaming-vs-Batch.md deleted file mode 100644 index 88cae89e..00000000 --- a/docs/wiki/Streaming-vs-Batch.md +++ /dev/null @@ -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; - - 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> - where Self::Input: Clone, - { - let mut out = Vec::with_capacity(inputs.len()); - for x in inputs { - out.push(self.update(x.clone())); - } - out - } -} - -impl 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/.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: diff --git a/docs/wiki/TA-Lib-Migration.md b/docs/wiki/TA-Lib-Migration.md deleted file mode 100644 index da581032..00000000 --- a/docs/wiki/TA-Lib-Migration.md +++ /dev/null @@ -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. diff --git a/docs/wiki/Warmup-Periods.md b/docs/wiki/Warmup-Periods.md deleted file mode 100644 index 468a68d1..00000000 --- a/docs/wiki/Warmup-Periods.md +++ /dev/null @@ -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/.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: diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-AwesomeOscillator.md b/docs/wiki/indicators/momentum-oscillators/Indicator-AwesomeOscillator.md deleted file mode 100644 index 9f0ab5be..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-AwesomeOscillator.md +++ /dev/null @@ -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; -``` - -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 = (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`. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Cci.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Cci.md deleted file mode 100644 index cb2bce31..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Cci.md +++ /dev/null @@ -1,198 +0,0 @@ -# CCI - -> Commodity Channel Index — measures how far the current typical price -> deviates from its rolling mean, in units of mean absolute deviation -> scaled by Lambert's constant. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `Candle` | -| Output type | `f64` | -| Output range | unbounded (typically `[−200, +200]` thanks to the 0.015 factor) | -| Default parameters | `period = 20` (Python) | -| Warmup period | `period` (20 for `period = 20`) | -| Interpretation | `> +100` overbought, `< −100` oversold (Lambert) | - -## Formula - -For each candle, compute the typical price `TP = (high + low + close) / 3`, -then over the rolling `period`-bar window: - -``` -SMA_TP_t = (TP_{t-period+1} + … + TP_t) / period -MAD_t = (1 / period) · Σ |TP_i − SMA_TP_t| for i = t-period+1 … t - -CCI_t = (TP_t − SMA_TP_t) / (factor · MAD_t) -``` - -The default `factor` is Lambert's `0.015`, chosen empirically so that -roughly 70–80 % of values fall inside `[−100, +100]`. The implementation -exposes the factor through `Cci::with_factor(period, factor)` if you want -to retune it for an asset with very different volatility characteristics. - -When `MAD == 0` (a perfectly flat window), the implementation returns `0` -rather than dividing by zero. - -## Parameters - -| Name | Type | Default (Python) | Valid range | Description | -|------|------|------------------|-------------|-------------| -| `period` | `usize` | `20` | `>= 1` | Rolling window length for both the SMA of typical price and the MAD. | -| `factor` | `f64` | `0.015` (`Cci::new`) | `> 0`, finite | Lambert's scaling constant; configurable via `Cci::with_factor`. | - -`Cci::new(0)` returns `Error::PeriodZero`. `Cci::with_factor(_, factor)` -returns `Error::NonPositiveMultiplier` when `factor <= 0` or non-finite. - -## Inputs / Outputs - -From `impl Indicator for Cci`: - -```rust -type Input = Candle; -type Output = f64; -fn update(&mut self, candle: Candle) -> Option; -``` - -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 = (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). diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Cmo.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Cmo.md deleted file mode 100644 index 88727278..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Cmo.md +++ /dev/null @@ -1,155 +0,0 @@ -# CMO - -> Chande Momentum Oscillator — a bounded `[−100, 100]` momentum gauge from -> the unsmoothed sum of gains versus losses. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | `[−100, 100]` | -| Default parameters | `period = 14` (Python) | -| Warmup period | `period + 1` | -| Interpretation | `+100` pure gains, `−100` pure losses, `0` balanced. | - -## Formula - -Over the last `period` price *changes*, sum the gains and the losses -separately: - -``` -gain_t = max(price_t − price_{t−1}, 0) -loss_t = max(price_{t−1} − price_t, 0) -CMO = 100 · (Σ gain − Σ loss) / (Σ gain + Σ loss) -``` - -Unlike RSI — which Wilder-smooths the gain/loss averages — CMO sums them -raw, with equal weight on every change in the window. That makes it -faster and wider-swinging than RSI at the same period. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|----------|---------|---------------|-------------|-------------| -| `period` | `usize` | `14` (Python) | `>= 1` | Number of price changes summed. `period = 0` errors with `Error::PeriodZero`. | - -The Python binding defaults `period` to `14` via `#[pyo3(signature = (period=14))]`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/cmo.rs`: - -```rust -impl Indicator for Cmo { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut cmo = Cmo::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Mfi.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Mfi.md deleted file mode 100644 index cab0c73f..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Mfi.md +++ /dev/null @@ -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; -``` - -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 = (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). diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Mom.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Mom.md deleted file mode 100644 index d5e55f61..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Mom.md +++ /dev/null @@ -1,151 +0,0 @@ -# MOM - -> Momentum — the raw price change over a fixed lookback, -> `price_t − price_{t−period}`, in absolute price units. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | unbounded around zero (price-difference scale) | -| Default parameters | `period = 10` (Python) | -| Warmup period | `period + 1` | -| Interpretation | Sign and size of the move over the last `period` bars. | - -## Formula - -``` -MOM_t = price_t − price_{t−period} -``` - -The simplest momentum primitive. Positive output means price is higher -than it was `period` bars ago, negative means lower, and the magnitude is -the change in raw price units. [`Roc`](../momentum-oscillators/Indicator-Roc.md) is the same idea -expressed as a percentage of the old price. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|----------|---------|----------------|-------------|-------------| -| `period` | `usize` | `10` (Python) | `>= 1` | Lookback distance in bars. `period = 0` errors with `Error::PeriodZero`. | - -The Python binding defaults `period` to `10` via `#[pyo3(signature = (period=10))]`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/mom.rs`: - -```rust -impl Indicator for Mom { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut mom = Mom::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Pmo.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Pmo.md deleted file mode 100644 index 5b0f109c..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Pmo.md +++ /dev/null @@ -1,169 +0,0 @@ -# PMO - -> Price Momentum Oscillator — Carl Swenlin's DecisionPoint PMO line: a -> doubly-smoothed rate of change. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | unbounded around zero | -| Default parameters | `(smoothing1 = 35, smoothing2 = 20)` (Python) | -| Warmup period | `2` | -| Interpretation | Smoothed momentum; zero-line and signal-line crosses are the signals. | - -## Formula - -``` -roc_t = (price_t / price_{t−1} − 1) · 100 -smoothed_t = customEMA(roc, smoothing1)_t -PMO_t = customEMA(10 · smoothed, smoothing2)_t -``` - -`customEMA` is the DecisionPoint smoothing: an exponential average whose -smoothing constant is `2 / period` (not the textbook `2 / (period + 1)`), -seeded from its first input. The 1-bar percentage change is smoothed once, -scaled by `10`, then smoothed again. - -The classic PMO **signal line** is a 10-period EMA of this PMO line. It is -deliberately not bundled in — compose it yourself with -[`Chain`](../../Indicator-Chaining.md) and an `Ema(10)`. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|--------------|---------|---------------|-------------|-------------| -| `smoothing1` | `usize` | `35` (Python) | `>= 2` | First smoothing period (applied to ROC). `0` errors with `Error::PeriodZero`; `1` with `Error::InvalidPeriod`. | -| `smoothing2` | `usize` | `20` (Python) | `>= 2` | Second smoothing period (applied to `10 · smoothed`). Same error rules. | - -`smoothing = 1` is rejected because the smoothing constant `2 / 1 = 2` -would exceed `1`. The Python binding defaults the pair to `(35, 20)` via -`#[pyo3(signature = (smoothing1=35, smoothing2=20))]`. The `periods` -property returns `(smoothing1, smoothing2)`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/pmo.rs`: - -```rust -impl Indicator for Pmo { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - 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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Roc.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Roc.md deleted file mode 100644 index 097de963..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Roc.md +++ /dev/null @@ -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; -``` - -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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Rsi.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Rsi.md deleted file mode 100644 index 95fe58f2..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Rsi.md +++ /dev/null @@ -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; -``` - -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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-StochRsi.md b/docs/wiki/indicators/momentum-oscillators/Indicator-StochRsi.md deleted file mode 100644 index 9ba1ab54..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-StochRsi.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut sr = StochRsi::new(14, 14)?; - let prices: Vec = (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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Stochastic.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Stochastic.md deleted file mode 100644 index 85c2af12..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Stochastic.md +++ /dev/null @@ -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`: - -| Field | Description | -|-------|-------------| -| `k` | Raw `%K` (where `close` sits inside the window's H–L range). | -| `d` | `SMA(d_period)` of the `%K` series — the slower "signal" line. | - -Python's `Stochastic.batch(high, low, close)` returns a `(n, 2)` array -with columns `[k, d]`; warmup rows are `[NaN, NaN]`. - -Node's `Stochastic.batch(high, low, close)` returns a flat `number[]` -of length `n * 2`, interleaved as `[k_0, d_0, k_1, d_1, …]`. There is -no streaming `update()` on the Node binding — only `batch` is exposed. - -## Warmup - -`warmup_period()` returns `k_period + d_period − 1`. The `%K` series itself -becomes available at input `k_period`; the `%D` SMA then needs `d_period` -of those `%K` values to seed, producing its first output at input -`k_period + d_period − 1`. For the classic `(14, 3)` configuration this is -`16` — verified above. - -## Edge cases - -- **Flat range (`HH == LL`).** The implementation returns `%K = 50` by - convention (mirroring RSI's flat-input behaviour). The unit test - `flat_range_yields_k_50` pins this; with a constant input both `%K` and - `%D` collapse to `50`. -- **Close at the window high.** `%K = 100` exactly; close at the window - low gives `%K = 0` exactly (tests `close_at_high_yields_k_100` and - `close_at_low_yields_k_0`). -- **Reset.** `reset()` clears the candle buffer, both monotonic deques, - the SMA, and `last_k` — the indicator returns to a freshly-constructed - state. - -## Examples - -### Rust - -```rust -use wickra::{BatchExt, Candle, Indicator, Stochastic}; - -let candles: Vec = (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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-Tsi.md b/docs/wiki/indicators/momentum-oscillators/Indicator-Tsi.md deleted file mode 100644 index c2a1172c..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-Tsi.md +++ /dev/null @@ -1,159 +0,0 @@ -# TSI - -> True Strength Index — a double-smoothed momentum oscillator that strips -> noise while keeping a clean, zero-centred read on trend pressure. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | roughly `[−100, 100]`, centred on zero | -| Default parameters | `(long = 25, short = 13)` (Python) | -| Warmup period | `long + short` | -| Interpretation | Positive = net upward pressure, negative = net downward. | - -## Formula - -``` -momentum_t = price_t − price_{t−1} -TSI = 100 · EMA_short(EMA_long(momentum)) / EMA_short(EMA_long(|momentum|)) -``` - -The 1-bar momentum and its absolute value are each smoothed twice — first -with an EMA of length `long`, then with an EMA of length `short`. The -ratio of the two double-smoothed series normalises the result: when every -recent move is up, numerator and denominator are equal and TSI saturates -at `+100`; when every move is down, at `−100`. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|---------|---------|---------------|-------------|-------------| -| `long` | `usize` | `25` (Python) | `>= 1` | First (slow) smoothing length. `0` errors with `Error::PeriodZero`. | -| `short` | `usize` | `13` (Python) | `>= 1` | Second (fast) smoothing length. `0` errors with `Error::PeriodZero`. | - -The Python binding defaults the pair to `(25, 13)` via -`#[pyo3(signature = (long=25, short=13))]`. Node and WASM take both -explicitly. The `periods` property returns `(long, short)`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/tsi.rs`: - -```rust -impl Indicator for Tsi { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let prices: Vec = (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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-UltimateOscillator.md b/docs/wiki/indicators/momentum-oscillators/Indicator-UltimateOscillator.md deleted file mode 100644 index afaa5bae..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-UltimateOscillator.md +++ /dev/null @@ -1,178 +0,0 @@ -# UltimateOscillator - -> Ultimate Oscillator — Larry Williams' momentum oscillator that blends -> three lookback periods into one bounded `[0, 100]` reading. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Momentum Oscillators | -| Input type | `Candle` (uses `high`, `low`, `close`) | -| Output type | `f64` | -| Output range | `[0, 100]` | -| Default parameters | `(short = 7, mid = 14, long = 28)` (Python) | -| Warmup period | `max(short, mid, long) + 1` | -| Interpretation | Weighted three-timeframe buying pressure; `50` is neutral. | - -## Formula - -``` -true_low_t = min(low_t, close_{t−1}) -BP_t = close_t − true_low_t (buying pressure) -TR_t = max(high_t, close_{t−1}) − true_low_t (true range) -avg_n = Σ BP over n / Σ TR over n -UO = 100 · (4·avg_short + 2·avg_mid + avg_long) / 7 -``` - -A single-timeframe momentum oscillator can show false divergences when -its lookback does not match the swing being measured. The Ultimate -Oscillator averages buying pressure over *three* windows and weights the -fastest (`4×`) above the medium (`2×`) and slow (`1×`), which damps those -false signals while keeping the response quick. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|---------|---------|---------------|-------------|-------------| -| `short` | `usize` | `7` (Python) | `>= 1` | Fast lookback (weight `4`). `0` errors with `Error::PeriodZero`. | -| `mid` | `usize` | `14` (Python) | `>= 1` | Medium lookback (weight `2`). | -| `long` | `usize` | `28` (Python) | `>= 1` | Slow lookback (weight `1`). | - -The Python binding defaults the trio to `(7, 14, 28)` via -`#[pyo3(signature = (short=7, mid=14, long=28))]`. Node and WASM take all -three explicitly. The `periods` property returns `(short, mid, long)`. -`UltimateOscillator::classic()` is the conventional `(7, 14, 28)`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/ultimate_oscillator.rs`: - -```rust -impl Indicator for UltimateOscillator { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - let mut uo = UltimateOscillator::classic(); // (7, 14, 28) - // 30 flat candles, each closing one tick higher than the last. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/momentum-oscillators/Indicator-WilliamsR.md b/docs/wiki/indicators/momentum-oscillators/Indicator-WilliamsR.md deleted file mode 100644 index d1235878..00000000 --- a/docs/wiki/indicators/momentum-oscillators/Indicator-WilliamsR.md +++ /dev/null @@ -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; -``` - -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). diff --git a/docs/wiki/indicators/moving-averages/Indicator-Dema.md b/docs/wiki/indicators/moving-averages/Indicator-Dema.md deleted file mode 100644 index b1c7bf18..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Dema.md +++ /dev/null @@ -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 -} -``` - -Python `update` returns `float | None`, `batch` returns a 1-D -`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns -`number | null`, `batch` returns `Array` 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> { - let mut dema = Dema::new(5)?; - let prices: Vec = (1..=20).map(f64::from).collect(); - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Ema.md b/docs/wiki/indicators/moving-averages/Indicator-Ema.md deleted file mode 100644 index be778cc3..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Ema.md +++ /dev/null @@ -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 -} -``` - -Python streams as `float | None`, batches as a 1-D `numpy.ndarray` -(`NaN` for warmup). Node streams as `number | null`, batches as -`Array` 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> { - let mut ema = Ema::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Hma.md b/docs/wiki/indicators/moving-averages/Indicator-Hma.md deleted file mode 100644 index 8cfc03ff..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Hma.md +++ /dev/null @@ -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 -} -``` - -Python returns `float | None` (streaming) / `numpy.ndarray` (batch, -`NaN` for warmup). Node returns `number | null` / `Array` 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 { - // 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> { - let mut hma = Hma::new(9)?; - let prices: Vec = (1..=20).map(f64::from).collect(); - let out: Vec> = 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 -. - -## 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Kama.md b/docs/wiki/indicators/moving-averages/Indicator-Kama.md deleted file mode 100644 index f4ef9377..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Kama.md +++ /dev/null @@ -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 -} -``` - -Python returns `float | None` (streaming) / `numpy.ndarray` (batch, -`NaN` for warmup). Node returns `number | null` (streaming) / -`Array` 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> { - let mut kama = Kama::classic(); // (10, 2, 30) - let prices: Vec = (1..=20).map(f64::from).collect(); - let out: Vec> = kama.batch(&prices); - println!("warmup_period = {}", kama.warmup_period()); - println!("{:?}", out); - Ok(()) -} -``` - -Output: - -``` -warmup_period = 11 -[None, None, None, None, None, None, None, None, None, None, Some(5.444444444444443), Some(8.358024691358022), Some(10.421124828532234), Some(12.011736015851241), Some(13.339853342139579), Some(14.522140745633099), Some(15.62341152535172), Some(16.679673069639843), Some(17.710929483133246), Some(18.728294157296247)] -``` - -`Kama::classic().periods()` returns `(10, 0.6666666666666666, 0.06451612903225806)` -— the second and third numbers are `fast_sc = 2/3` and `slow_sc = 2/31`, -not the integer `fast`/`slow` periods themselves. On the linear ramp -`1, 2, …, 20` the efficiency ratio is `1.0` (every step moves direction -the same as volatility), so `SC = fast_sc² ≈ 0.4444`. The first -emission `5.444…` is `1 + 0.4444 · (11 − 1)` and each subsequent value -follows the same recursion. - -### Python - -```python -import numpy as np -import wickra as ta - -kama = ta.KAMA() # defaults: er_period=10, fast=2, slow=30 -out = kama.batch(np.arange(1.0, 21.0)) -print("warmup_period =", kama.warmup_period()) -print(out) -``` - -Output: - -``` -warmup_period = 11 -[ nan nan nan nan nan nan - nan nan nan nan 5.44444444 8.35802469 - 10.42112483 12.01173602 13.33985334 14.52214075 15.62341153 16.67967307 - 17.71092948 18.72829416] -``` - -### Node - -```javascript -const ta = require('wickra'); -const kama = new ta.KAMA(10, 2, 30); // no default constructor; pass the triple -const prices = Array.from({ length: 20 }, (_, i) => i + 1); -console.log(kama.batch(prices)); -``` - -Output: - -``` -[ - NaN, NaN, - NaN, NaN, - NaN, NaN, - NaN, NaN, - NaN, NaN, - 5.444444444444443, 8.358024691358022, - 10.421124828532234, 12.011736015851241, - 13.339853342139579, 14.522140745633099, - 15.62341152535172, 16.679673069639843, - 17.710929483133246, 18.728294157296247 -] -``` - -(The Node `KAMA` class does not expose `warmupPeriod()`; use the Rust -or Python binding if you need that getter from your application.) - -## Interpretation - -KAMA's defining property is that it **changes its own behaviour with the -market**. In a clean trend the efficiency ratio approaches `1`, `SC` -approaches `fast_sc²`, and KAMA behaves like a fast EMA — it tracks -price closely. In a choppy sideways market the efficiency ratio -collapses toward `0`, `SC` approaches `slow_sc²`, and KAMA effectively -freezes — its line goes nearly flat regardless of how violently price -oscillates around it. This is by design: Kaufman's argument is that you -should not chase noise. - -The two usable signals are slope (positive = uptrend; flat = ranging; -negative = downtrend) and price-vs-KAMA crossover. Because KAMA can -sit nearly flat for long stretches in a range, "price crossed KAMA" -generates fewer false signals than the same test against an EMA of -similar period. - -Prefer `Kama` over a static EMA/SMA when the market regime varies -materially (trending → ranging → trending). Prefer a fixed `Ema` / -`Hma` when you want a predictable smoothing profile that is independent -of price action. - -## Common pitfalls - -- **Assuming `warmup_period()` is when the line is "good".** The first - emission lands at input 11 (for the default `er_period = 10`), but - the seed `KAMA_{t-1} = window.front()` is the *oldest* price in the - window, so the very first emitted value is biased toward the - 10-bars-ago price. On a strong trend this means the first 3–5 - emissions are noticeably below (or above, depending on direction) the - current price. If that matters, drop the first `er_period` post-warmup - emissions, not just the warmup itself. -- **Tuning `fast` and `slow` independently of `er_period`.** Kaufman's - derivation assumes `slow >> fast` so that the per-bar SC has room to - move. Picking, say, `(10, 5, 6)` gives `fast_sc ≈ 0.333` and - `slow_sc ≈ 0.286`, so SC barely changes regardless of the efficiency - ratio — KAMA degenerates into "an EMA somewhere around period 6". - Keep `slow` at least `5×` `fast` if you want the adaptive behaviour - to actually matter. - -## References - -Perry J. Kaufman, *Smarter Trading*, McGraw-Hill, 1995 (book-length -introduction); reprinted in Kaufman's *Trading Systems and Methods* -across multiple editions, where the squared-SC choice is justified -empirically. - -## See also - -- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — the two endpoints (`fast` and - `slow`) KAMA interpolates between. -- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — the other "smart" trend filter in - Wickra. -- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Sma.md b/docs/wiki/indicators/moving-averages/Indicator-Sma.md deleted file mode 100644 index f828820a..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Sma.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` 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` 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> { - let mut sma = Sma::new(3)?; - let out: Vec> = sma.batch(&[2.0, 4.0, 6.0, 8.0, 10.0]); - println!("{:?}", out); - println!("warmup_period = {}", sma.warmup_period()); - Ok(()) -} -``` - -Output: - -``` -[None, None, Some(4.0), Some(6.0), Some(8.0)] -warmup_period = 3 -``` - -The first two inputs return `None` while the window fills; the third -emits `(2 + 4 + 6) / 3 = 4.0` and every subsequent input slides the -window by one. This matches the `known_reference_values` test in -`crates/wickra-core/src/indicators/sma.rs`. - -### Python - -```python -import numpy as np -import wickra as ta - -sma = ta.SMA(3) -print(sma.batch(np.array([2.0, 4.0, 6.0, 8.0, 10.0]))) -print("warmup_period =", sma.warmup_period()) -``` - -Output: - -``` -[nan nan 4. 6. 8.] -warmup_period = 3 -``` - -Warmup rows come back as `NaN` so the result aligns 1:1 with the input -array. - -### Node - -```javascript -const ta = require('wickra'); -const sma = new ta.SMA(3); -console.log(sma.batch([2, 4, 6, 8, 10])); -console.log('warmupPeriod:', sma.warmupPeriod()); -``` - -Output: - -``` -[ NaN, NaN, 4, 6, 8 ] -warmupPeriod: 3 -``` - -## Interpretation - -`Sma` is a smoothed price level. The two canonical signals are: - -1. **Price–SMA crossover.** Close above the SMA suggests an uptrend, close - below suggests a downtrend. The longer the SMA, the slower (and more - trustworthy) the signal. -2. **Two-SMA crossover.** A fast SMA crossing above a slow SMA is the - classic "golden cross"; below is the "death cross". Either of `Ema` - or `Hma` will give earlier (but noisier) signals at the same period. - -Prefer `Sma` when you want the simplest possible reference price — for -example, as the middle band of [`BollingerBands`](../../Indicators-Overview.md), -which uses an SMA by construction. Prefer `Ema` if you want the same -smoothness profile but slightly less lag on direction changes. - -## Common pitfalls - -- **Treating `period = 0` as "use a default".** `Sma::new(0)` returns - `Err(Error::PeriodZero)` in Rust and a `ValueError` in Python; there is - no implicit default. Pass an explicit period. -- **Slicing batch results with `> warmup_period` instead of - `~np.isnan(...)`.** In Python the batch output has `NaN` for warmup - rows; in Rust it has `None`. Use the warmup-aware mask to filter — see - the [Quickstart: Python](../../Quickstart-Python.md#macd-a-multi-column-indicator-and-its-warmup-nans) - pattern. Slicing by `prices.size - warmup_period` works for a single - indicator but breaks the moment you compose two of them via `Chain`. - -## References - -The simple moving average predates technical analysis as a discipline. -The implementation here follows the standard "rolling sum, slide on each -update" formulation; the matching reference implementations are TA-Lib -and pandas (`rolling(period).mean()`). - -## See also - -- [Indicator-Ema.md](../moving-averages/Indicator-Ema.md) — same smoothness budget, less lag. -- [Indicator-Wma.md](../moving-averages/Indicator-Wma.md) — linear weights instead of equal. -- [Indicator-Hma.md](../moving-averages/Indicator-Hma.md) — built on three WMAs for near-zero - lag. -- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Smma.md b/docs/wiki/indicators/moving-averages/Indicator-Smma.md deleted file mode 100644 index 5efaa55a..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Smma.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` (streaming) or a `numpy.ndarray` with `NaN` warmup rows -(batch); Node maps it to `number | null` / `Array` with `NaN` -warmup. - -## Warmup - -`Smma::new(period).warmup_period() == period`. The first `period - 1` -inputs are buffered while the seed accumulates; the `period`-th `update()` -emits the simple average of those inputs as `SMMA_period`. Every later -input applies the `(prev·(n−1)+x)/n` recurrence. - -## Edge cases - -- **Constant series.** Feeding `[7.0; n]` returns `Some(7.0)` from input - `period` onward — the recurrence is a fixed point for constants - (`constant_series_yields_the_constant` pins this). -- **NaN / infinity inputs.** The first line of `update` is - `if !input.is_finite() { return self.current; }`. Non-finite inputs are - **silently dropped** — they neither advance the seed nor perturb the - recurrence, and the previous valid value (if any) is returned. -- **Reset.** `smma.reset()` clears the seed buffer and the current value, - restarting the warmup countdown. - -## Examples - -### Rust - -```rust -use wickra::{BatchExt, Indicator, Smma}; - -fn main() -> Result<(), Box> { - let mut smma = Smma::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-T3.md b/docs/wiki/indicators/moving-averages/Indicator-T3.md deleted file mode 100644 index 38f7693b..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-T3.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let prices: Vec = (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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Tema.md b/docs/wiki/indicators/moving-averages/Indicator-Tema.md deleted file mode 100644 index 7868bdba..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Tema.md +++ /dev/null @@ -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 -} -``` - -Python `update` returns `float | None`, `batch` returns a 1-D -`numpy.ndarray` (`float64`, `NaN` for warmup). Node `update` returns -`number | null`, `batch` returns `Array` 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> { - let mut tema = Tema::new(5)?; - let prices: Vec = (1..=20).map(f64::from).collect(); - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Trima.md b/docs/wiki/indicators/moving-averages/Indicator-Trima.md deleted file mode 100644 index 9d796f79..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Trima.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut trima = Trima::new(5)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Vwma.md b/docs/wiki/indicators/moving-averages/Indicator-Vwma.md deleted file mode 100644 index f9a52f92..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Vwma.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Wma.md b/docs/wiki/indicators/moving-averages/Indicator-Wma.md deleted file mode 100644 index 2ce8312c..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Wma.md +++ /dev/null @@ -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 -} -``` - -Python returns `float | None` from `update` and a `numpy.ndarray` -(`float64`, `NaN` for warmup) from `batch`. Node returns `number | null` -and `Array` (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> { - let mut wma = Wma::new(4)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/moving-averages/Indicator-Zlema.md b/docs/wiki/indicators/moving-averages/Indicator-Zlema.md deleted file mode 100644 index ea0d66b0..00000000 --- a/docs/wiki/indicators/moving-averages/Indicator-Zlema.md +++ /dev/null @@ -1,166 +0,0 @@ -# ZLEMA - -> Zero-Lag Exponential Moving Average — an EMA fed a de-lagged price series -> so it tracks turns with almost no group delay. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Moving Averages | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | unbounded; tracks the input price scale | -| Default parameters | `period` is required (no default in either binding) | -| Warmup period | `lag + period` where `lag = (period − 1) / 2` | -| Interpretation | Low-lag trend line; crossings of price react far sooner than a plain EMA. | - -## Formula - -``` -lag = (period − 1) / 2 (integer division) -de_lagged_t = 2·price_t − price_{t−lag} -ZLEMA_t = EMA_period(de_lagged)_t -``` - -The trick (Ehlers & Way, 2010): `price_t − price_{t−lag}` is a momentum -term. Adding it to the current price *over-shoots* in the direction of the -recent move by exactly enough to cancel the EMA's lag. The inner EMA then -smooths that de-lagged series with the usual `α = 2 / (period + 1)`. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|----------|---------|---------|-------------|-------------| -| `period` | `usize` | none | `>= 1` | EMA length. `period = 0` errors with `Error::PeriodZero`. The lag offset is derived as `(period − 1) / 2`. | - -There is no Python `#[pyo3(signature = …)]` default for `ZLEMA`, so -`wickra.ZLEMA(period)` requires the period explicitly. The derived `lag` -is exposed as a read-only property. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/zlema.rs`: - -```rust -impl Indicator for Zlema { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut zlema = Zlema::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/price-oscillators/Indicator-AcceleratorOscillator.md b/docs/wiki/indicators/price-oscillators/Indicator-AcceleratorOscillator.md deleted file mode 100644 index 56de3e67..00000000 --- a/docs/wiki/indicators/price-oscillators/Indicator-AcceleratorOscillator.md +++ /dev/null @@ -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 -} -``` - -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> { - let mut ac = AcceleratorOscillator::classic(); - let candles: Vec = (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. diff --git a/docs/wiki/indicators/price-oscillators/Indicator-BalanceOfPower.md b/docs/wiki/indicators/price-oscillators/Indicator-BalanceOfPower.md deleted file mode 100644 index 7b45dd15..00000000 --- a/docs/wiki/indicators/price-oscillators/Indicator-BalanceOfPower.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-oscillators/Indicator-Coppock.md b/docs/wiki/indicators/price-oscillators/Indicator-Coppock.md deleted file mode 100644 index 387f944d..00000000 --- a/docs/wiki/indicators/price-oscillators/Indicator-Coppock.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut coppock = Coppock::new(14, 11, 10)?; - let prices: Vec = (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. diff --git a/docs/wiki/indicators/price-oscillators/Indicator-Dpo.md b/docs/wiki/indicators/price-oscillators/Indicator-Dpo.md deleted file mode 100644 index 77bf1397..00000000 --- a/docs/wiki/indicators/price-oscillators/Indicator-Dpo.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut dpo = Dpo::new(4)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/price-oscillators/Indicator-Ppo.md b/docs/wiki/indicators/price-oscillators/Indicator-Ppo.md deleted file mode 100644 index 1a92b6e2..00000000 --- a/docs/wiki/indicators/price-oscillators/Indicator-Ppo.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut ppo = Ppo::new(12, 26)?; - let prices: Vec = (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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-LinRegAngle.md b/docs/wiki/indicators/price-statistics/Indicator-LinRegAngle.md deleted file mode 100644 index 0578365e..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-LinRegAngle.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-LinRegSlope.md b/docs/wiki/indicators/price-statistics/Indicator-LinRegSlope.md deleted file mode 100644 index 8df8bbce..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-LinRegSlope.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-LinearRegression.md b/docs/wiki/indicators/price-statistics/Indicator-LinearRegression.md deleted file mode 100644 index a5d18044..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-LinearRegression.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-MedianPrice.md b/docs/wiki/indicators/price-statistics/Indicator-MedianPrice.md deleted file mode 100644 index 40c9a391..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-MedianPrice.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-TypicalPrice.md b/docs/wiki/indicators/price-statistics/Indicator-TypicalPrice.md deleted file mode 100644 index a6b653c5..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-TypicalPrice.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-WeightedClose.md b/docs/wiki/indicators/price-statistics/Indicator-WeightedClose.md deleted file mode 100644 index f3aba929..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-WeightedClose.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/price-statistics/Indicator-ZScore.md b/docs/wiki/indicators/price-statistics/Indicator-ZScore.md deleted file mode 100644 index aa3981ff..00000000 --- a/docs/wiki/indicators/price-statistics/Indicator-ZScore.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/trailing-stops/Indicator-AtrTrailingStop.md b/docs/wiki/indicators/trailing-stops/Indicator-AtrTrailingStop.md deleted file mode 100644 index 175fa1c6..00000000 --- a/docs/wiki/indicators/trailing-stops/Indicator-AtrTrailingStop.md +++ /dev/null @@ -1,169 +0,0 @@ -# AtrTrailingStop - -> ATR Trailing Stop — a single stop level that trails price by a fixed ATR -> multiple, ratcheting toward the trend and flipping on a close through it. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Trailing Stops | -| Input type | `Candle` (uses `high`, `low`, `close`) | -| Output type | `f64` | -| Output range | unbounded (price scale) | -| Default parameters | `atr_period = 14`, `multiplier = 3.0` (Python) | -| Warmup period | `atr_period` | -| Interpretation | One trailing stop line; price closing through it flips the trade. | - -## Formula - -``` -loss = multiplier · ATR - -stop_t = max(stop_{t−1}, close − loss) while price holds above the stop - = min(stop_{t−1}, close + loss) while price holds below the stop - = close − loss on a fresh break above the stop - = close + loss on a fresh break below the stop -``` - -This is the trailing stop popularised by the "UT Bot": a single line that sits -`multiplier · ATR` away from the close. While price holds on one side of the -stop the level only ratchets *toward* price — up in an uptrend, down in a -downtrend — and never away from it. When a close crosses the stop the level -snaps to the opposite side of the new close, flipping the trade. Unlike the -[`ChandelierExit`](../trailing-stops/Indicator-ChandelierExit.md), it hangs off the close -itself, not the window's extreme, and reports one line rather than two. - -## Parameters - -- `atr_period` — the ATR lookback (Python default `14`). -- `multiplier` — the ATR multiple the stop trails by (Python default `3.0`). - -`AtrTrailingStop::classic()` returns the `(14, 3.0)` configuration. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/atr_trailing_stop.rs`: - -```rust -impl Indicator for AtrTrailingStop { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - let mut ts = AtrTrailingStop::new(5, 3.0)?; - // Flat market: ATR = 2, loss = 3·2 = 6, stop = 10 - 6 = 4. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trailing-stops/Indicator-ChandeKrollStop.md b/docs/wiki/indicators/trailing-stops/Indicator-ChandeKrollStop.md deleted file mode 100644 index 328b3c32..00000000 --- a/docs/wiki/indicators/trailing-stops/Indicator-ChandeKrollStop.md +++ /dev/null @@ -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 -} -``` - -`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> { - let mut cks = ChandeKrollStop::new(5, 1.0, 3)?; - // Flat market: ATR = 2, HH = 11, LL = 9. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trailing-stops/Indicator-ChandelierExit.md b/docs/wiki/indicators/trailing-stops/Indicator-ChandelierExit.md deleted file mode 100644 index a92b4cc4..00000000 --- a/docs/wiki/indicators/trailing-stops/Indicator-ChandelierExit.md +++ /dev/null @@ -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 -} -``` - -`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> { - let mut ce = ChandelierExit::new(5, 3.0)?; - // Flat market: ATR = 2, HH = 11, LL = 9. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trailing-stops/Indicator-Psar.md b/docs/wiki/indicators/trailing-stops/Indicator-Psar.md deleted file mode 100644 index 922196f4..00000000 --- a/docs/wiki/indicators/trailing-stops/Indicator-Psar.md +++ /dev/null @@ -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; - 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` 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> { - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trailing-stops/Indicator-SuperTrend.md b/docs/wiki/indicators/trailing-stops/Indicator-SuperTrend.md deleted file mode 100644 index 59dca1fa..00000000 --- a/docs/wiki/indicators/trailing-stops/Indicator-SuperTrend.md +++ /dev/null @@ -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 -} -``` - -`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> { - let mut st = SuperTrend::new(5, 3.0)?; - // Flat market: ATR = 2, hl2 = 10, lower band = 10 - 3·2 = 4. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-Adx.md b/docs/wiki/indicators/trend-directional/Indicator-Adx.md deleted file mode 100644 index b3cd9801..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-Adx.md +++ /dev/null @@ -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`: - -| 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 = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-Aroon.md b/docs/wiki/indicators/trend-directional/Indicator-Aroon.md deleted file mode 100644 index 538cd62f..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-Aroon.md +++ /dev/null @@ -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`: - -| 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 = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-AroonOscillator.md b/docs/wiki/indicators/trend-directional/Indicator-AroonOscillator.md deleted file mode 100644 index ca0c8ecf..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-AroonOscillator.md +++ /dev/null @@ -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 -} -``` - -`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> { - let mut osc = AroonOscillator::new(5)?; - // 30 bars, each a fresh high. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-ChoppinessIndex.md b/docs/wiki/indicators/trend-directional/Indicator-ChoppinessIndex.md deleted file mode 100644 index f4dfb322..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-ChoppinessIndex.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-MacdIndicator.md b/docs/wiki/indicators/trend-directional/Indicator-MacdIndicator.md deleted file mode 100644 index bb899870..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-MacdIndicator.md +++ /dev/null @@ -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` 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 = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-MassIndex.md b/docs/wiki/indicators/trend-directional/Indicator-MassIndex.md deleted file mode 100644 index 7ef7466c..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-MassIndex.md +++ /dev/null @@ -1,172 +0,0 @@ -# MassIndex - -> Mass Index — Donald Dorsey's range-expansion indicator: it watches the -> high–low range widen and contract to anticipate reversals. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Trend & Directional | -| Input type | `Candle` (uses `high`, `low`) | -| Output type | `f64` | -| Output range | `> 0`, oscillates around `sum_period` | -| Default parameters | `(ema_period = 9, sum_period = 25)` (Python) | -| Warmup period | `2·ema_period + sum_period − 2` | -| Interpretation | A rise above `27` then fall below `26.5` flags a reversal. | - -## Formula - -``` -range_t = high_t − low_t -single_t = EMA(range, ema_period)_t -double_t = EMA(single, ema_period)_t -ratio_t = single_t / double_t -MassIndex = Σ ratio over sum_period -``` - -The Mass Index ignores direction entirely — it tracks **volatility shape**. -When the high–low range widens, the single EMA pulls ahead of the double -EMA, the ratio climbs above `1`, and the windowed sum rises. Dorsey's -"reversal bulge" is the classic pattern: the Mass Index rising above `27` -and then falling back below `26.5` warns that a range expansion is about -to resolve — often into a trend reversal. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|--------------|---------|---------------|-------------|-------------| -| `ema_period` | `usize` | `9` (Python) | `>= 1` | Period of both EMAs in the cascade. `0` errors with `Error::PeriodZero`. | -| `sum_period` | `usize` | `25` (Python) | `>= 1` | Length of the summation window. | - -The Python binding defaults the pair to `(9, 25)`. The `periods` property -returns `(ema_period, sum_period)`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/mass_index.rs`: - -```rust -impl Indicator for MassIndex { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`MassIndex` is a **candle-input** indicator: it reads `high` and `low`. In -Python the streaming `update` accepts a 6-tuple or a dict; the batch -helper takes `high` and `low` numpy arrays. Node and WASM expose -`update(high, low)` and `batch(high, low)`. - -## Warmup - -`warmup_period() == 2·ema_period + sum_period − 2`. The first EMA seeds at -input `ema_period`; the second EMA, stacked on it, seeds at -`2·ema_period − 1`; the summation window then needs `sum_period` ratios. -For the default `(9, 25)` that is `41` bars. - -## Edge cases - -- **Constant range.** When every bar has the same high–low range, both - EMAs converge to the same value, every ratio is `1`, and the Mass Index - equals `sum_period` (`constant_range_sums_to_sum_period` pins this). -- **Zero-range market.** A flat market (`high == low`) drives both EMAs to - `0`; the `0 / 0` is guarded with the neutral ratio `1`, so the Mass - Index again equals `sum_period` - (`zero_range_market_sums_to_sum_period` pins this). -- **Candle validation.** `Candle::new` rejects invalid bars upstream. -- **Reset.** `mi.reset()` clears both EMAs, the window and the sum. - -## Examples - -### Rust - -```rust -use wickra::{BatchExt, Candle, Indicator, MassIndex}; - -fn main() -> Result<(), Box> { - let mut mi = MassIndex::new(3, 4)?; - // Constant high-low range of 2.0; the Mass Index settles at sum_period. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-Trix.md b/docs/wiki/indicators/trend-directional/Indicator-Trix.md deleted file mode 100644 index 7c4b2af6..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-Trix.md +++ /dev/null @@ -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; -``` - -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 = (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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-VerticalHorizontalFilter.md b/docs/wiki/indicators/trend-directional/Indicator-VerticalHorizontalFilter.md deleted file mode 100644 index 5fece0ec..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-VerticalHorizontalFilter.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/trend-directional/Indicator-Vortex.md b/docs/wiki/indicators/trend-directional/Indicator-Vortex.md deleted file mode 100644 index 07f99226..00000000 --- a/docs/wiki/indicators/trend-directional/Indicator-Vortex.md +++ /dev/null @@ -1,159 +0,0 @@ -# Vortex - -> Vortex Indicator — a pair of oscillators (`VI+`, `VI−`) whose crossings -> identify the start of a new trend. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Trend & Directional | -| Input type | `Candle` (uses `high`, `low`, `close`) | -| Output type | `VortexOutput { plus, minus }` | -| Output range | each line `>= 0`, typically around `1.0` | -| Default parameters | `period = 14` (Python) | -| Warmup period | `period + 1` | -| Interpretation | `VI+` above `VI−` = up-trend; the cross marks the turn. | - -## Formula - -``` -VM+_t = |high_t − low_{t−1}| (positive vortex movement) -VM−_t = |low_t − high_{t−1}| (negative vortex movement) -TR_t = true range -VI+ = Σ VM+ over period / Σ TR over period -VI− = Σ VM− over period / Σ TR over period -``` - -Each vortex movement measures how far this bar reached against the -*opposite* extreme of the previous bar; dividing the running sums by the -running true range normalises both lines to a comparable scale around -`1.0`. `VI+` crossing above `VI−` signals a new up-trend; the reverse, a -down-trend. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|----------|---------|---------------|-------------|-------------| -| `period` | `usize` | `14` (Python) | `>= 1` | Summation window. `0` errors with `Error::PeriodZero`. | - -The Python binding defaults `period` to `14`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/vortex.rs`: - -```rust -pub struct VortexOutput { pub plus: f64, pub minus: f64 } - -impl Indicator for Vortex { - type Input = Candle; - type Output = VortexOutput; -} -``` - -`Vortex` is a **candle-input** indicator reading `high`, `low` and -`close`. The streaming `update` returns `VortexOutput` (Rust), -`(plus, minus)` (Python), or `{ plus, minus }` (Node/WASM). The batch -helper returns one row per input — a `(n, 2)` numpy array in Python, a -flat `[plus, minus, …]` array of length `2·n` in Node/WASM, with `NaN` -during warmup. - -## Warmup - -`Vortex::new(period).warmup_period() == period + 1`. The first VM/TR -triple needs a previous bar, so it forms on bar 2; the summation window -then needs `period` triples — the first output lands on input -`period + 1`. - -## Edge cases - -- **Flat market.** A window with zero total true range cannot be - normalised; both lines are reported as `0.0` - (`perfectly_flat_market_yields_zero` pins this). -- **Non-negative.** Both `VI+` and `VI−` are sums of absolute values over - a non-negative range, so neither is ever negative - (`outputs_are_non_negative` pins this). -- **Candle validation.** `Candle::new` rejects invalid bars upstream. -- **Reset.** `vortex.reset()` clears the previous bar, the window and the - three running sums. - -## Examples - -### Rust - -```rust -use wickra::{BatchExt, Candle, Indicator, Vortex}; - -fn main() -> Result<(), Box> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-Atr.md b/docs/wiki/indicators/volatility-bands/Indicator-Atr.md deleted file mode 100644 index 0d7ee513..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-Atr.md +++ /dev/null @@ -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; - 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` 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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-BollingerBands.md b/docs/wiki/indicators/volatility-bands/Indicator-BollingerBands.md deleted file mode 100644 index bb163454..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-BollingerBands.md +++ /dev/null @@ -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; - 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` 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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-BollingerBandwidth.md b/docs/wiki/indicators/volatility-bands/Indicator-BollingerBandwidth.md deleted file mode 100644 index 2d7be5f5..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-BollingerBandwidth.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut bbw = BollingerBandwidth::new(20, 2.0)?; - // A flat stretch then a volatile stretch: bandwidth rises. - let mut prices: Vec = 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-ChaikinVolatility.md b/docs/wiki/indicators/volatility-bands/Indicator-ChaikinVolatility.md deleted file mode 100644 index 93b96716..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-ChaikinVolatility.md +++ /dev/null @@ -1,154 +0,0 @@ -# ChaikinVolatility - -> Chaikin Volatility — the rate of change of a smoothed high-low spread; -> is the trading range widening or narrowing? - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volatility & Bands | -| Input type | `Candle` (uses `high`, `low`) | -| Output type | `f64` | -| Output range | unbounded around zero (percent) | -| Default parameters | `ema_period = 10`, `roc_period = 10` (Python) | -| Warmup period | `ema_period + roc_period` | -| Interpretation | Positive = ranges expanding, negative = ranges contracting. | - -## Formula - -``` -spread_t = high_t − low_t -smoothed_t = EMA(spread, ema_period)_t -ChaikinVol = 100 · (smoothed_t − smoothed_{t−roc_period}) / smoothed_{t−roc_period} -``` - -Marc Chaikin's volatility measure tracks not the *level* of the trading range -but how fast it is *widening or narrowing*. The bar's high-low spread is -EMA-smoothed, then run through a rate-of-change: a rising value means ranges -are expanding (often near a market top, as fear spikes), a falling value means -they are contracting (a quiet, complacent market). The classic configuration -smooths the spread with a `10`-period EMA and takes its `10`-period rate of -change. - -## Parameters - -- `ema_period` — the EMA that smooths the high-low spread (`10`). -- `roc_period` — the rate-of-change lookback over the smoothed spread (`10`). - -`ChaikinVolatility::classic()` returns the `(10, 10)` configuration. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/chaikin_volatility.rs`: - -```rust -impl Indicator for ChaikinVolatility { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - let mut cv = ChaikinVolatility::new(10, 10)?; - // A constant 2-wide range -> constant EMA -> zero rate of change. - let candles: Vec = (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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-Donchian.md b/docs/wiki/indicators/volatility-bands/Indicator-Donchian.md deleted file mode 100644 index 2d1fd483..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-Donchian.md +++ /dev/null @@ -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; -} - -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` 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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-HistoricalVolatility.md b/docs/wiki/indicators/volatility-bands/Indicator-HistoricalVolatility.md deleted file mode 100644 index e8f6517d..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-HistoricalVolatility.md +++ /dev/null @@ -1,170 +0,0 @@ -# HistoricalVolatility - -> Historical Volatility — the annualised standard deviation of log returns, -> the realised volatility used to price options and size risk. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volatility & Bands | -| Input type | `f64` (single close) | -| Output type | `f64` | -| Output range | `[0, ∞)` (annualised percent) | -| Default parameters | `(period = 20, trading_periods = 252)` (Python) | -| Warmup period | `period + 1` | -| Interpretation | Annualised volatility of returns, in percent. | - -## Formula - -``` -r_t = ln(price_t / price_{t−1}) -HV = stddev_sample(r over period) · √trading_periods · 100 -``` - -The log returns over the window are measured with the **sample** standard -deviation (divisor `n − 1`, Bessel's correction — the unbiased volatility -estimator), then annualised by `√trading_periods` and expressed as a -percentage. `trading_periods` is the number of bars in a year for the -data's frequency: `252` for daily bars, `52` for weekly, `12` for -monthly. - -## Parameters - -| Name | Type | Default | Valid range | Description | -|-------------------|---------|----------------|-------------|-------------| -| `period` | `usize` | `20` (Python) | `>= 2` | Number of log returns in the window. `0` errors with `Error::PeriodZero`; `1` with `Error::InvalidPeriod` (the sample stddev needs two returns). | -| `trading_periods` | `usize` | `252` (Python) | `>= 1` | Annualisation factor. `0` errors with `Error::PeriodZero`. | - -The Python binding defaults the pair to `(20, 252)`. The `periods` -property returns `(period, trading_periods)`. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/historical_volatility.rs`: - -```rust -impl Indicator for HistoricalVolatility { - type Input = f64; - type Output = f64; - // update(&mut self, input: f64) -> Option -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - // 20-bar window, 252 trading days per year. - let mut hv = HistoricalVolatility::new(20, 252)?; - let prices: Vec = (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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-Keltner.md b/docs/wiki/indicators/volatility-bands/Indicator-Keltner.md deleted file mode 100644 index 9db300de..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-Keltner.md +++ /dev/null @@ -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; -} - -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` 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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-Natr.md b/docs/wiki/indicators/volatility-bands/Indicator-Natr.md deleted file mode 100644 index 3b4a1397..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-Natr.md +++ /dev/null @@ -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 -} -``` - -`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> { - let mut natr = Natr::new(14)?; - let candles: Vec = (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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-PercentB.md b/docs/wiki/indicators/volatility-bands/Indicator-PercentB.md deleted file mode 100644 index 670c20ae..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-PercentB.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-StdDev.md b/docs/wiki/indicators/volatility-bands/Indicator-StdDev.md deleted file mode 100644 index eefd14ec..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-StdDev.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut sd = StdDev::new(3)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-TrueRange.md b/docs/wiki/indicators/volatility-bands/Indicator-TrueRange.md deleted file mode 100644 index 4b730f8a..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-TrueRange.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/volatility-bands/Indicator-UlcerIndex.md b/docs/wiki/indicators/volatility-bands/Indicator-UlcerIndex.md deleted file mode 100644 index dc670e02..00000000 --- a/docs/wiki/indicators/volatility-bands/Indicator-UlcerIndex.md +++ /dev/null @@ -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 -} -``` - -A single `f64` close in, an `Option` out. Python maps this to -`float | None` / `numpy.ndarray` (NaN warmup); Node to `number | null` / -`Array` (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> { - let mut ui = UlcerIndex::new(2)?; - let out: Vec> = 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. diff --git a/docs/wiki/indicators/volume/Indicator-Adl.md b/docs/wiki/indicators/volume/Indicator-Adl.md deleted file mode 100644 index 60fb751c..00000000 --- a/docs/wiki/indicators/volume/Indicator-Adl.md +++ /dev/null @@ -1,160 +0,0 @@ -# ADL - -> Accumulation/Distribution Line — a cumulative volume-flow line that -> weights each bar's volume by where its close fell within the range. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volume | -| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) | -| Output type | `f64` | -| Output range | unbounded (drifts with cumulative volume) | -| Default parameters | none (no parameters) | -| Warmup period | `1` | -| Interpretation | Running buying/selling pressure; slope and divergence matter. | - -## Formula - -``` -MFM_t = ((close − low) − (high − close)) / (high − low) (money-flow multiplier, −1..+1) -MFV_t = MFM_t · volume_t (money-flow volume) -ADL_t = ADL_{t−1} + MFV_t -``` - -The money-flow multiplier asks *where in the bar's range did price -close?* A close on the high gives `+1` (full accumulation), on the low -`−1` (full distribution), in the middle `0`. Scaling by volume and -running the cumulative total gives a line whose **slope** reflects -sustained buying or selling pressure. A bar with `high == low` carries no -positional information and contributes `0`. - -## Parameters - -`ADL` takes **no parameters** — `Adl::new()` in Rust, `wickra.ADL()` in -Python, `new ta.ADL()` in Node. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/adl.rs`: - -```rust -impl Indicator for Adl { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - let mut adl = Adl::new(); - let out = adl.batch(&[ - Candle::new(8.0, 10.0, 8.0, 10.0, 100.0, 0)?, // close at high - Candle::new(10.0, 12.0, 8.0, 9.0, 200.0, 1)?, - ]); - println!("{:?}", out); - Ok(()) -} -``` - -Output: - -``` -[Some(100.0), Some(0.0)] -``` - -Bar 1 closes at its high (`MFM = +1`), adding `+100`. Bar 2 has -`MFM = ((9−8)−(12−9))/4 = −0.5`, adding `−100`, so the line returns to -`0`. This matches the `reference_values` test in -`crates/wickra-core/src/indicators/adl.rs`. - -### Python - -```python -import numpy as np -import wickra as ta - -adl = ta.ADL() -high = np.array([10.0, 12.0]) -low = np.array([8.0, 8.0]) -close = np.array([10.0, 9.0]) -volume = np.array([100.0, 200.0]) -print(adl.batch(high, low, close, volume)) -``` - -Output: - -``` -[100. 0.] -``` - -### Node - -```javascript -const ta = require('wickra'); -const adl = new ta.ADL(); -console.log(adl.batch([10, 12], [8, 8], [10, 9], [100, 200])); -``` - -Output: - -``` -[ 100, 0 ] -``` - -## Interpretation - -`Adl` is read by slope and by divergence, never by absolute level (the -total drifts arbitrarily with cumulative volume). A rising ADL confirms -that an up-move is backed by accumulation; a *falling* ADL while price -rises is a bearish divergence — the rally is not being bought into. -[`ChaikinOscillator`](../volume/Indicator-ChaikinOscillator.md) is the standard way -to turn the ADL into a bounded, tradeable oscillator. - -## Common pitfalls - -- **Reading the absolute value.** Only the slope and divergences are - meaningful; the level depends on where you started the stream. -- **Feeding it scalar prices.** It needs the full OHLCV bar. - -## References - -Marc Chaikin's Accumulation/Distribution Line; the money-flow-multiplier -formulation here matches the standard definition (StockCharts, TA-Lib's -`AD`). - -## See also - -- [Indicator-Obv.md](../volume/Indicator-Obv.md) — cumulative *signed* volume. -- [Indicator-ChaikinOscillator.md](../volume/Indicator-ChaikinOscillator.md) — an - oscillator built on the ADL. -- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy. diff --git a/docs/wiki/indicators/volume/Indicator-ChaikinMoneyFlow.md b/docs/wiki/indicators/volume/Indicator-ChaikinMoneyFlow.md deleted file mode 100644 index f5797b79..00000000 --- a/docs/wiki/indicators/volume/Indicator-ChaikinMoneyFlow.md +++ /dev/null @@ -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 -} -``` - -`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> { - 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. diff --git a/docs/wiki/indicators/volume/Indicator-ChaikinOscillator.md b/docs/wiki/indicators/volume/Indicator-ChaikinOscillator.md deleted file mode 100644 index ef521ed2..00000000 --- a/docs/wiki/indicators/volume/Indicator-ChaikinOscillator.md +++ /dev/null @@ -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 -} -``` - -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> { - 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 = (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. diff --git a/docs/wiki/indicators/volume/Indicator-EaseOfMovement.md b/docs/wiki/indicators/volume/Indicator-EaseOfMovement.md deleted file mode 100644 index 67b7b947..00000000 --- a/docs/wiki/indicators/volume/Indicator-EaseOfMovement.md +++ /dev/null @@ -1,160 +0,0 @@ -# EaseOfMovement - -> Ease of Movement (EOM) — Richard Arms' measure of how far price travels -> per unit of volume, averaged over a window. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volume | -| Input type | `Candle` (uses `high`, `low`, `volume`) | -| Output type | `f64` | -| Output range | unbounded around zero (scaled by `divisor`) | -| Default parameters | `period = 14`, `divisor = 1e8` (Python) | -| Warmup period | `period + 1` | -| Interpretation | Light-volume moves push it away from zero; sign tracks direction. | - -## Formula - -``` -distance_t = (high_t + low_t)/2 − (high_{t−1} + low_{t−1})/2 -EMV_t = distance_t · (high_t − low_t) · divisor / volume_t -EOM_t = SMA(EMV, period)_t -``` - -Arms' question is *how easily did price move?* A bar whose midpoint jumped a -long way on a wide range but light volume gets a large `EMV`; a bar that -needed heavy volume to budge gets a small one. The `divisor` is a pure -output-scaling constant — the conventional `1e8` keeps `EMV` readable for -typical share volumes; smaller markets want a smaller divisor. The window SMA -smooths the noisy per-bar `EMV` into a tradeable line. - -## Parameters - -- `period` — the SMA averaging window (Python default `14`). -- `divisor` — the volume-scaling constant (Python default `1e8`). Rust exposes - `EaseOfMovement::new(period)` for the `1e8` default and - `EaseOfMovement::with_divisor(period, divisor)` for an explicit value. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/ease_of_movement.rs`: - -```rust -impl Indicator for EaseOfMovement { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - // 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. diff --git a/docs/wiki/indicators/volume/Indicator-ForceIndex.md b/docs/wiki/indicators/volume/Indicator-ForceIndex.md deleted file mode 100644 index 9df9b765..00000000 --- a/docs/wiki/indicators/volume/Indicator-ForceIndex.md +++ /dev/null @@ -1,154 +0,0 @@ -# ForceIndex - -> Force Index — Alexander Elder's price change scaled by volume, then -> smoothed with an EMA. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volume | -| Input type | `Candle` (uses `close`, `volume`) | -| Output type | `f64` | -| Output range | unbounded around zero | -| Default parameters | `period = 13` (Python) | -| Warmup period | `period + 1` | -| Interpretation | Conviction behind a move; sign and zero-crossings are the signal. | - -## Formula - -``` -raw_t = (close_t − close_{t−1}) · volume_t -Force_t = EMA(raw, period)_t -``` - -The raw force is positive on an up-close and negative on a down-close, with a -magnitude that grows with the volume backing the move — a large move on heavy -volume registers a large force, a large move on thin volume does not. -Smoothing the raw series with an EMA turns the noisy per-bar reading into a -tradeable line; Elder's classic period is `13`. - -## Parameters - -`period` — the EMA smoothing period. The Python binding defaults it to `13`; -the Rust and Node constructors require it explicitly. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/force_index.rs`: - -```rust -impl Indicator for ForceIndex { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - // 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. diff --git a/docs/wiki/indicators/volume/Indicator-Obv.md b/docs/wiki/indicators/volume/Indicator-Obv.md deleted file mode 100644 index 9db9cff7..00000000 --- a/docs/wiki/indicators/volume/Indicator-Obv.md +++ /dev/null @@ -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; - 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` - 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> { - 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. diff --git a/docs/wiki/indicators/volume/Indicator-VolumePriceTrend.md b/docs/wiki/indicators/volume/Indicator-VolumePriceTrend.md deleted file mode 100644 index a411bf42..00000000 --- a/docs/wiki/indicators/volume/Indicator-VolumePriceTrend.md +++ /dev/null @@ -1,160 +0,0 @@ -# VolumePriceTrend - -> Volume-Price Trend (VPT) — a cumulative volume line where each bar's -> contribution is scaled by its percentage price change. - -## Quick reference - -| Field | Value | -|-------|-------| -| Family | Volume | -| Input type | `Candle` (uses `close`, `volume`) | -| Output type | `f64` | -| Output range | unbounded (drifts with cumulative volume) | -| Default parameters | none (no parameters) | -| Warmup period | `1` | -| Interpretation | Running volume flow; slope and divergence matter. | - -## Formula - -``` -VPT_t = VPT_{t−1} + volume_t · (close_t − close_{t−1}) / close_{t−1} -``` - -VPT is a close relative of [`Obv`](../volume/Indicator-Obv.md). Where OBV adds the -*entire* bar volume on any up-close, VPT adds volume scaled by the **size** -of the move: a 2 % gain on a given volume moves the line twice as far as a -1 % gain on the same volume. That makes VPT more sensitive to the -conviction behind a move. The first bar establishes the baseline at `0`. - -## Parameters - -`VolumePriceTrend` takes **no parameters** — `VolumePriceTrend::new()` in -Rust, `wickra.VolumePriceTrend()` in Python, `new ta.VolumePriceTrend()` -in Node. - -## Inputs / Outputs - -From `crates/wickra-core/src/indicators/vpt.rs`: - -```rust -impl Indicator for VolumePriceTrend { - type Input = Candle; - type Output = f64; - // update(&mut self, input: Candle) -> Option -} -``` - -`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> { - let mut vpt = VolumePriceTrend::new(); - // closes 10 -> 11 -> 9, volumes 100, 200, 300. - let out = vpt.batch(&[ - Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?, - Candle::new(11.0, 11.0, 11.0, 11.0, 200.0, 1)?, - Candle::new(9.0, 9.0, 9.0, 9.0, 300.0, 2)?, - ]); - println!("{:?}", out); - Ok(()) -} -``` - -Output: - -``` -[Some(0.0), Some(20.0), Some(-34.54545454545455)] -``` - -Bar 1 is the baseline `0`. Bar 2 adds `200 · (11−10)/10 = 20`. Bar 3 adds -`300 · (9−11)/11 = −600/11`, leaving `20 − 600/11 ≈ −34.545`. This matches -the `reference_values` test in `crates/wickra-core/src/indicators/vpt.rs`. - -### Python - -```python -import numpy as np -import wickra as ta - -vpt = ta.VolumePriceTrend() -close = np.array([10.0, 11.0, 9.0]) -volume = np.array([100.0, 200.0, 300.0]) -print(vpt.batch(close, volume)) -``` - -Output: - -``` -[ 0. 20. -34.54545455] -``` - -### Node - -```javascript -const ta = require('wickra'); -const vpt = new ta.VolumePriceTrend(); -console.log(vpt.batch([10, 11, 9], [100, 200, 300])); -``` - -Output: - -``` -[ 0, 20, -34.54545454545455 ] -``` - -## Interpretation - -`VolumePriceTrend` is read like OBV — by **slope** and by **divergence**, -never by absolute level. A VPT rising in step with price confirms the -trend is volume-supported; VPT flattening or falling while price climbs -is a bearish divergence warning that the move lacks participation. Versus -OBV, VPT gives proportionally more weight to large moves and less to a -string of tiny up-closes, so it tracks the *magnitude* of conviction, not -just its direction. - -## Common pitfalls - -- **Reading the absolute value.** Only slope and divergences carry - meaning; the level depends on the stream's start point. -- **Expecting OBV-identical behaviour.** VPT scales by percentage change, - so the two lines diverge — especially across large single-bar moves. - -## References - -The Volume-Price Trend (also "Price-Volume Trend") is a standard -cumulative volume study; the `volume · ROC` accumulation here matches the -common definition. - -## See also - -- [Indicator-Obv.md](../volume/Indicator-Obv.md) — cumulative signed volume, the - closest relative. -- [Indicator-Adl.md](../volume/Indicator-Adl.md) — cumulative range-weighted volume. -- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy. diff --git a/docs/wiki/indicators/volume/Indicator-Vwap.md b/docs/wiki/indicators/volume/Indicator-Vwap.md deleted file mode 100644 index 734ae15d..00000000 --- a/docs/wiki/indicators/volume/Indicator-Vwap.md +++ /dev/null @@ -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; - 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` 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> { - 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; - 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> { - 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 - -``` - -## 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. diff --git a/examples/wasm/README.md b/examples/wasm/README.md index 8c0b5906..a02a9191 100644 --- a/examples/wasm/README.md +++ b/examples/wasm/README.md @@ -45,7 +45,7 @@ Then open the demo you want at `http://localhost:8000/examples/wasm/`. ## 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.