Compare commits

..

10 Commits

Author SHA1 Message Date
kingchenc dc415a77fd release: bump 0.6.9 -> 0.7.0 (#213)
Version bump 0.6.9 -> 0.7.0 for the B15 Microstructure batch (488 indicators).
2026-06-08 03:10:16 +02:00
kingchenc e385734275 feat(microstructure): trade-sign autocorrelation, PIN, Hasbrouck information share (B15) (#212)
## B15 Microstructure — three new indicators (485 → 488)

| Indicator | Input | Output | Notes |
|-----------|-------|--------|-------|
| `TradeSignAutocorrelation` | `Trade` | `f64` ∈ [-1,1] | lag-1 autocorrelation of the signed aggressor (order-flow persistence) |
| `Pin` | `Trade` | `f64` ∈ [0,1] | probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator); `name()` = `"PIN"` |
| `HasbrouckInformationShare` | `(f64, f64)` | `f64` ∈ [0,1] | variance-ratio proxy for each venue's share of price discovery |

### Wiring
- Core structs + full unit tests (every branch).
- Hand-written Python/Node/WASM bindings for the two `Trade`-input indicators (precedent `TradeImbalance`); `node_pair_indicator!` / `wasm_pair_indicator!` macro bindings + hand Python pyclass for the pairwise Hasbrouck (precedent `RollingCorrelation`).
- Fuzz drives added to `indicator_update_trade.rs` and `indicator_update_pair.rs`.
- Dedicated Python + Node streaming-vs-batch and reference tests; Hasbrouck in the `PAIR` registry.
- README counter (3 spots) + `docs/README.md` + `FAMILIES` assert bumped to 488.

### Verify (all green, local)
- `cargo test -p wickra-core --lib`: 3991 passed
- `cargo test -p wickra-core --doc`: 438 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean
- node: 561 passed · pytest: 926 passed
2026-06-08 03:07:50 +02:00
kingchenc 3a46b210bb release: bump 0.6.8 -> 0.6.9 (#211)
Release 0.6.9 — ships the B14 Candlestick Patterns indicators (485 total). Version-string bump only.
2026-06-08 02:30:46 +02:00
kingchenc 943825d6a0 feat: add Candlestick Patterns deepening (B14, 6 indicators) (#209)
B14 of the family-deepening roadmap — six candlestick patterns (479 -> 485), all in the **Candlestick Patterns** family.

**Fixed-lookback (candle-pattern macro bindings, neutral 0.0 during warmup):**
- **Tristar** — three-doji star reversal.
- **Harami Cross** — Harami whose second candle is a contained doji.
- **Tower Top/Bottom** — tall bar, small pause, tall opposite bar.

**Windowed / parameterized (hand-bound, `candle -> f64`):**
- **Frying Pan Bottom** — rounded U-shaped accumulation base, recovery-confirmed.
- **Dumpling Top** — rounded dome-shaped distribution top, breakdown-confirmed.
- **New Price Lines** — run of N consecutive new closing highs (+1) / lows (-1).

Window/Gap (Rising-Falling) dropped (SKIP — existing gap coverage). Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs counter (485) and CHANGELOG. Verified: core 3966 + doc 435, clippy clean, node 560, python 922.
2026-06-08 02:29:24 +02:00
kingchenc d16df1e224 ci: warm the cargo registry with backoff so a DNS blip can't fail clippy (#210)
## Problem

A macOS runner on #206 failed the **Rust** job's clippy step with:

```
Updating crates.io index
error: failed to get `rayon` as a dependency ...
  download of config.json failed
  [6] Couldn't resolve host name (Could not resolve host: index.crates.io)
```

A pure transient DNS blip — unrelated to the change (a docs-only PR). `CARGO_NET_RETRY=10` only does fast in-process retries; a longer DNS outage outlasts them, so the very first cargo step's crates.io index fetch fails the whole job.

## Fix

Add a **Warm cargo registry** step right after the cache restore in the `rust`, `clippy-bindings` and `msrv` jobs. It runs `cargo fetch` in a 5-attempt loop with real backoff (20/40/60/80s sleeps) so the dependency graph is pulled once, patiently, riding out a multi-second DNS outage that cargo's rapid retries can't. The later clippy/build/test steps then resolve from the warmed local cache.

Mirrors the existing inline retry pattern already used for setup-node/setup-python CDN flakes. Can be extended to the coverage/python/wasm/node jobs if they ever hit the same blip.
2026-06-08 02:22:26 +02:00
kingchenc 34c097aee2 docs: refresh Python benchmark figures from a fresh measured run (#206)
The published Python benchmark tables (README/BENCHMARKS.md) were a stale, incoherent run. Re-measured locally with the current build (wickra 0.6.5, post batch fast-paths) via `compare_libraries.py` on the same 9950X.

- **Streaming vs talipp:** 11-56x (was 9-58x).
- **Batch:** real per-indicator numbers; MACD and ATR were notably off in the old table.
- **Prose:** Wickra beats TA-Lib on RSI and ATR (no longer MACD, which now trails 130 vs 111 us).

Rust tables unchanged. Numbers are a single coherent run; absolute us still depend on machine state (caveat already in the doc).
2026-06-08 02:13:13 +02:00
kingchenc acd7e8dc52 release: bump 0.6.7 -> 0.6.8 (#208)
Release 0.6.8 — ships the B13 Ichimoku & Charts indicators (479 total). Version-string bump only.
2026-06-08 01:50:32 +02:00
kingchenc ceaeb90a22 feat: add Ichimoku & Charts deepening (B13, 5 indicators) (#207)
B13 of the family-deepening roadmap — five alternative-chart indicators (474 -> 479), all in the **Ichimoku & Charts** family.

- **Smoothed Heikin-Ashi** (`candle -> struct {open, high, low, close}`) — a Heikin-Ashi candle computed from EMA-smoothed OHLC.
- **Heikin-Ashi Oscillator** (`candle -> f64`) — the HA body (`ha_close - ha_open`), optionally EMA-smoothed, as a zero-line oscillator.
- **Three Line Break** (`candle -> f64`) — line-break ("kakushi") chart trend direction; reverses only when the close breaks the extreme of the last N lines. Distinct from the candlestick `ThreeLineStrike`.
- **Equivolume** (`candle -> struct {height, width}`) — a box whose height is the bar range and width is volume-relative.
- **CandleVolume** (`candle -> struct {body, width}`) — a candle whose body is close-minus-open and width is volume-relative.

All bindings hand-written (3 struct-output + 2 candle-input-with-open / non-period-ctor). Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs counter (479) and CHANGELOG. Verified: core 3915 + doc 432, clippy clean, node 554, python 913.
2026-06-08 01:49:03 +02:00
kingchenc 57e26fb22f release: bump 0.6.6 -> 0.6.7 (#205)
Release 0.6.7 — ships the B12 DeMark indicators (474 total).

Version-string bump only: Cargo workspace + wickra-core dep, Python pyproject, Node package.json (+6 platform packages), both package-lock files, Cargo.lock, and CHANGELOG.
2026-06-08 01:14:23 +02:00
kingchenc 8431b1400c feat: add DeMark deepening (B12, 7 indicators) (#204)
B12 of the family-deepening roadmap — seven Tom DeMark indicators (467 -> 474).

**Candle -> +1/0 qualifier patterns (candlestick macro bindings):**
- **TD Camouflage** — hidden intrabar strength/weakness against the prior close.
- **TD Clop** — two-bar open/close engulfing reversal.
- **TD Clopwin** — the inside-body cousin of TD Clop (compression bar).
- **TD Propulsion** — continuation thrust closing beyond the prior extreme.
- **TD Trap** — inside ("trap") bar followed by a range breakout.

**Hand-bound:**
- **TD D-Wave** — streaming Elliott-style 1-5 / A-C swing-wave counter (candle -> f64, `strength` param).
- **TD Moving Averages** — ST1/ST2 median-price trend ribbon (candle -> struct {st1, st2}).

All seven join the existing **DeMark** family. Patterns follow the house-style
+1/0 candle-pattern convention (neutral 0.0 during warmup). Public binding names
use the family-consistent `TD...` casing.

Wiring complete across core, Python, Node, WASM, fuzz, tests, README + docs
counter (474) and CHANGELOG. Verified: core 3874 + doc 427, clippy clean,
node 549, python 903.
2026-06-08 01:12:46 +02:00
51 changed files with 7484 additions and 133 deletions
+42
View File
@@ -53,6 +53,22 @@ jobs:
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Warm cargo registry (retry transient DNS/registry flakes)
shell: bash
# CARGO_NET_RETRY rides out short blips, but a longer runner DNS outage
# outlasts cargo's rapid in-process retries: the crates.io index fetch
# the first cargo step does ("Could not resolve host: index.crates.io")
# then fails the whole job. Pre-fetch the dependency graph here with real
# backoff so clippy/build/test resolve from the warmed local cache.
run: |
for attempt in 1 2 3 4 5; do
if cargo fetch; then exit 0; fi
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
sleep $((attempt * 20))
done
echo "::error::cargo fetch still failing after 5 attempts"
exit 1
- name: Format check
run: cargo fmt --all -- --check
@@ -232,6 +248,19 @@ jobs:
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Warm cargo registry (retry transient DNS/registry flakes)
shell: bash
# See the rust job: pre-fetch with backoff so the clippy index update
# can't fail the job on a transient "Could not resolve host" DNS blip.
run: |
for attempt in 1 2 3 4 5; do
if cargo fetch; then exit 0; fi
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
sleep $((attempt * 20))
done
echo "::error::cargo fetch still failing after 5 attempts"
exit 1
- name: Clippy (bindings, all targets)
run: cargo clippy -p wickra-node -p wickra-python --all-targets -- -D warnings
@@ -272,6 +301,19 @@ jobs:
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Warm cargo registry (retry transient DNS/registry flakes)
shell: bash
# See the rust job: pre-fetch with backoff so the first cargo step can't
# fail the job on a transient "Could not resolve host" DNS blip.
run: |
for attempt in 1 2 3 4 5; do
if cargo fetch; then exit 0; fi
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
sleep $((attempt * 20))
done
echo "::error::cargo fetch still failing after 5 attempts"
exit 1
- name: Build on MSRV
run: cargo build ${{ matrix.packages }} --verbose
+18 -18
View File
@@ -26,15 +26,15 @@ was built to expose.
| Indicator | **★ Wickra** | talipp | TA-Lib (recompute) |
|------------------|------------------:|------------------|-----------------------|
| SMA(20) | **0.063 µs ★** | 0.59 µs (9×) | 204 µs (3 300×) |
| EMA(20) | **0.060 µs ★** | 0.72 µs (12×) | 212 µs (3 500×) |
| RSI(14) | **0.065 µs ★** | 1.06 µs (16×) | 230 µs (3 600×) |
| MACD(12, 26, 9) | **0.078 µs ★** | 4.22 µs (54×) | 245 µs (3 100×) |
| Bollinger(20, 2) | **0.088 µs ★** | 5.15 µs (58×) | 229 µs (2 600×) |
| SMA(20) | **0.089 µs ★** | 0.96 µs (11×) | 422 µs (4 700×) |
| EMA(20) | **0.111 µs ★** | 1.19 µs (11×) | 430 µs (3 900×) |
| RSI(14) | **0.061 µs ★** | 0.95 µs (16×) | 298 µs (4 900×) |
| MACD(12, 26, 9) | **0.079 µs ★** | 3.30 µs (42×) | 327 µs (4 100×) |
| Bollinger(20, 2) | **0.089 µs ★** | 4.97 µs (56×) | 296 µs (3 300×) |
Against the only other incremental Python peer Wickra is **958× faster**;
against the recompute-on-every-tick libraries it is **2 60014 000× faster**
(`finta` RSI hits 14 000×). tulipy / pandas-ta land in the same recompute band
Against the only other incremental Python peer Wickra is **1156× faster**;
against the recompute-on-every-tick libraries it is **2 80019 000× faster**
(`finta` RSI hits 19 000×). tulipy / pandas-ta land in the same recompute band
as TA-Lib.
**Rust — per-tick latency** (whole 50 000-bar series, lower = faster):
@@ -63,17 +63,17 @@ field everywhere.
**Python** (20 000-bar pass, µs/op, lower = faster):
| Indicator | Wickra | TA-Lib | tulipy | pandas-ta |
|------------------|---------:|-------:|-------:|----------:|
| SMA(20) | 22.7 | **15.4** | 15.9 | 33.7 |
| EMA(20) | 30.8 | **30.3** | 31.1 | 48.8 |
| RSI(14) | 58.9 | 72.5 | **38.5** | 94.8 |
| MACD(12, 26, 9) | 71.7 | 99.1 | **33.5** | 207.6 |
| Bollinger(20, 2) | 84.9 | 65.7 | **32.3** | 336.4 |
| ATR(14) | 52.0 | 79.4 | **31.9** | — |
| Indicator | Wickra | TA-Lib | tulipy | pandas-ta | finta |
|------------------|---------:|---------:|---------:|----------:|---------:|
| SMA(20) | 22.2 | **15.6** | 15.9 | 32.7 | 290.1 |
| EMA(20) | 30.5 | **30.4** | 30.9 | 46.7 | 198.5 |
| RSI(14) | 52.3 | 72.0 | **34.2** | 88.8 | 812.3 |
| MACD(12, 26, 9) | 129.8 | 111.1 | **38.4** | 286.8 | 716.7 |
| Bollinger(20, 2) | 87.2 | 74.6 | **37.9** | 474.3 | 1255.5 |
| ATR(14) | 74.7 | 87.3 | **35.5** | — | 3496.4 |
Wickra beats TA-Lib on RSI, MACD and ATR and the whole Python field on every
row; tulipy's SIMD C stays ahead on the heavier indicators.
Wickra beats pandas-ta and finta on every row and TA-Lib on RSI and ATR;
tulipy's SIMD C (and TA-Lib on SMA/EMA) lead the remaining rows.
**Rust** (50 000-bar pass, µs, lower = faster). Only Wickra and `kand` expose a
batch API; `ta-rs` and `yata` are streaming-only:
+34 -1
View File
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.0] - 2026-06-08
- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
- **PIN** — probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator) (`Pin`).
- **Trade-Sign Autocorrelation** — lag-1 autocorrelation of the signed trade aggressor (order-flow persistence) (`TradeSignAutocorrelation`).
## [0.6.9] - 2026-06-08
- **Tristar** — a three-doji star reversal: three consecutive dojis with the middle gapped above (bearish) or below (bullish) its neighbours (`Tristar`).
- **Harami Cross** — a Harami whose second candle is a contained doji, a stronger reversal than a plain Harami (`HaramiCross`).
- **Tower Top/Bottom** — a tall bar, a small pause bar, then a tall opposite bar marking a reversal (`TowerTopBottom`).
- **Frying Pan Bottom** — a rounded (U-shaped) accumulation base over the lookback window, confirmed when price recovers above the rim (`FryPanBottom`).
- **Dumpling Top** — a rounded (dome-shaped) distribution top over the lookback window, confirmed when price breaks below the start (`DumplingTop`).
- **New Price Lines** — flags a run of N consecutive new closing highs (+1) or lows (-1), the eight/ten-new-price-lines exhaustion gauge (`NewPriceLines`).
## [0.6.8] - 2026-06-08
- **Smoothed Heikin-Ashi** — a Heikin-Ashi candle computed from EMA-smoothed OHLC, damping noise into a cleaner trend candle (`SmoothedHeikinAshi`).
- **Heikin-Ashi Oscillator** — the Heikin-Ashi candle body (`ha_close ha_open`), optionally EMA-smoothed, as a zero-line oscillator (`HeikinAshiOscillator`).
- **Three Line Break** — the trend direction of a line-break chart, reversing only when the close breaks the extreme of the last N lines (`ThreeLineBreak`).
- **Equivolume** — a chart box whose height is the bar range and whose width is volume-relative, fusing price range with activity (`Equivolume`).
- **CandleVolume** — a candle whose body is close-minus-open and whose width is volume-relative, a volume-weighted candle chart (`CandleVolume`).
## [0.6.7] - 2026-06-08
- **TD Camouflage** — a DeMark qualifier flagging hidden intrabar strength or weakness against the prior close (`TDCamouflage`).
- **TD Clop** — a DeMark two-bar open/close engulfing reversal where the bar opens beyond and closes back across the prior body (`TDClop`).
- **TD Clopwin** — the inside-body cousin of TD Clop, marking a compression bar whose direction hints at the next move (`TDClopwin`).
- **TD Propulsion** — a DeMark continuation thrust that opens on the trend side and closes beyond the prior bar's extreme (`TDPropulsion`).
- **TD Trap** — an inside ("trap") bar followed by a close beyond its range, triggering a directional breakout signal (`TDTrap`).
- **TD D-Wave** — a streaming Elliott-style swing-wave counter labelling the market's 15 impulse / AC correction sequence (`TDDWave`).
- **TD Moving Averages** — the DeMark ST1 (fast) and ST2 (slow) median-price trend ribbon whose crossover frames the trend (`TDMovingAverage`).
## [0.6.6] - 2026-06-08
- **Pivot Reversal** — a breakout signal when price closes through the most recently confirmed swing pivot (`PIVOT_REVERSAL`).
- **Volume-Weighted Support/Resistance** — a band whose edges are the volume-weighted average of recent highs and lows (`VOLUME_WEIGHTED_SR`).
@@ -1353,7 +1382,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.6.6...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.0...HEAD
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
[0.6.9]: https://github.com/wickra-lib/wickra/compare/v0.6.8...v0.6.9
[0.6.8]: https://github.com/wickra-lib/wickra/compare/v0.6.7...v0.6.8
[0.6.7]: https://github.com/wickra-lib/wickra/compare/v0.6.6...v0.6.7
[0.6.6]: https://github.com/wickra-lib/wickra/compare/v0.6.5...v0.6.6
[0.6.5]: https://github.com/wickra-lib/wickra/compare/v0.6.4...v0.6.5
[0.6.4]: https://github.com/wickra-lib/wickra/compare/v0.6.3...v0.6.4
Generated
+8 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"criterion",
"kand",
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "wickra-core"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"approx",
"proptest",
@@ -1977,7 +1977,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"approx",
"csv",
@@ -1994,7 +1994,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"serde_json",
"tokio",
@@ -2004,7 +2004,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"napi",
"napi-build",
@@ -2014,7 +2014,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"numpy",
"pyo3",
@@ -2023,7 +2023,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.6.6"
version = "0.7.0"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -13,7 +13,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.6.6"
version = "0.7.0"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -25,7 +25,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.6.6" }
wickra-core = { path = "crates/wickra-core", version = "0.7.0" }
thiserror = "2"
rayon = "1.10"
+13 -13
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=467" alt="Wickra — streaming-first technical indicators" width="100%"></a>
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=488" alt="Wickra — streaming-first technical indicators" width="100%"></a>
</p>
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
@@ -48,7 +48,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM).
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
every one of the 467 indicators; start at the
every one of the 488 indicators; start at the
[indicators overview](https://docs.wickra.org/Indicators-Overview).
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
@@ -66,7 +66,7 @@ an afterthought — **live, tick-by-tick data** — without giving up the breadt
a full batch library, and without making you reimplement your indicators four
times to get there.
- **The biggest streaming-native catalogue, period.** 467 indicators across 24
- **The biggest streaming-native catalogue, period.** 488 indicators across 24
families — candlesticks, harmonic & chart patterns, market profile, market
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
@@ -77,8 +77,8 @@ times to get there.
- **Correct by construction, not by hope.** Every `update` validates its input,
runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
for all 467 indicators**.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **958×**
for all 488 indicators**.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **1156×**
faster than the only other incremental peer and **thousands of times** faster
than recompute-on-every-tick libraries. On batch it wins several rows outright
and trades the simple recurrences (SMA, EMA, MACD) for its guarantees — and
@@ -95,7 +95,7 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **467** | **yes** |
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **488** | **yes** |
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes |
@@ -118,7 +118,7 @@ useful version of that itch is the one other people can build on too.
## Benchmarks
Wickra updates every indicator in **O(1)** per tick. In **streaming** — the
workload it is built for — it is **958× faster** than the only other incremental
workload it is built for — it is **1156× faster** than the only other incremental
peer and **thousands of times** faster than recompute-on-every-tick libraries.
**Batch** is competitive: it wins several rows outright and trades a few µs
elsewhere for `None`-warmup, NaN-safety and bit-exact `batch == streaming`.
@@ -128,7 +128,7 @@ Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
## Indicators
467 streaming-first indicators across twenty-four families. Every one passes the
488 streaming-first indicators across twenty-four families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
@@ -146,14 +146,14 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient, Jarque-Bera, Rolling Min-Max Scaler, Shannon Entropy, Sample Entropy, Kendall Tau |
| Ehlers / Cycle (DSP) | MAMA, FAMA, Fisher Transform, Inverse Fisher Transform, SuperSmoother, Hilbert Dominant Cycle, Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode, Sine Wave, Decycler, Decycler Oscillator, Roofing Filter, Center of Gravity, Cybernetic Cycle, Adaptive Cycle, Empirical Mode Decomposition, Ehlers Stochastic, Instantaneous Trendline, Highpass Filter, Reflex, Trendflex, Correlation Trend Indicator, Adaptive RSI, Universal Oscillator, Adaptive CCI, Bandpass Filter, Even Better Sinewave, Autocorrelation Periodogram |
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal |
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages |
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure |
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share |
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
@@ -237,7 +237,7 @@ A Python live-trading example using the public `websockets` package lives at
```
wickra/
├── crates/
│ ├── wickra-core/ core engine + all 467 indicators
│ ├── wickra-core/ core engine + all 488 indicators
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
+30 -1
View File
@@ -378,6 +378,20 @@ const candleScalar = {
BetterVolume: { make: () => new wickra.BetterVolume(14), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
ADAPTIVECCI: { make: () => new wickra.ADAPTIVECCI(20), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
PivotReversal: { make: () => new wickra.PivotReversal(1, 1), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
TDCamouflage: { make: () => new wickra.TDCamouflage(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TDClop: { make: () => new wickra.TDClop(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TDClopwin: { make: () => new wickra.TDClopwin(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TDPropulsion: { make: () => new wickra.TDPropulsion(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TDTrap: { make: () => new wickra.TDTrap(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TDDWave: { make: () => new wickra.TDDWave(2), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
HeikinAshiOscillator: { make: () => new wickra.HeikinAshiOscillator(5), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
ThreeLineBreak: { make: () => new wickra.ThreeLineBreak(3), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
Tristar: { make: () => new wickra.Tristar(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
HaramiCross: { make: () => new wickra.HaramiCross(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
TowerTopBottom: { make: () => new wickra.TowerTopBottom(), step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
DumplingTop: { make: () => new wickra.DumplingTop(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
NewPriceLines: { make: () => new wickra.NewPriceLines(5), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
FryPanBottom: { make: () => new wickra.FryPanBottom(9), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
};
for (const [name, d] of Object.entries(candleScalar)) {
@@ -479,6 +493,10 @@ const multi = {
MurreyMathLines: { make: () => new wickra.MurreyMathLines(4), fields: ['mm8_8', 'mm7_8', 'mm6_8', 'mm5_8', 'mm4_8', 'mm3_8', 'mm2_8', 'mm1_8', 'mm0_8'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
AndrewsPitchfork: { make: () => new wickra.AndrewsPitchfork(2), fields: ['median', 'upper', 'lower'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
VolumeWeightedSr: { make: () => new wickra.VolumeWeightedSr(3), fields: ['support', 'resistance'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
TDMovingAverage: { make: () => new wickra.TDMovingAverage(5, 13), fields: ['st1', 'st2'], step: (ind, i) => ind.update(high[i], low[i]), batch: (ind) => ind.batch(high, low) },
SmoothedHeikinAshi: { make: () => new wickra.SmoothedHeikinAshi(5), fields: ['open', 'high', 'low', 'close'], step: (ind, i) => ind.update(open[i], high[i], low[i], close[i]), batch: (ind) => ind.batch(open, high, low, close) },
Equivolume: { make: () => new wickra.Equivolume(20), fields: ['height', 'width'], step: (ind, i) => ind.update(high[i], low[i], volume[i]), batch: (ind) => ind.batch(high, low, volume) },
CandleVolume: { make: () => new wickra.CandleVolume(20), fields: ['body', 'width'], step: (ind, i) => ind.update(open[i], close[i], volume[i]), batch: (ind) => ind.batch(open, close, volume) },
};
for (const [name, d] of Object.entries(multi)) {
@@ -649,6 +667,7 @@ const pairFactories = {
GrangerCausality: () => new wickra.GrangerCausality(60, 1),
SpreadAr1Coefficient: () => new wickra.SpreadAr1Coefficient(40),
KendallTau: () => new wickra.KendallTau(20),
HasbrouckInformationShare: () => new wickra.HasbrouckInformationShare(2),
};
for (const [name, make] of Object.entries(pairFactories)) {
@@ -1252,7 +1271,7 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
const price = Array.from({ length: n }, (_, i) => 100 + Math.sin(i * 0.25) * 4);
const size = Array.from({ length: n }, (_, i) => 1 + (i % 5));
const isBuy = Array.from({ length: n }, (_, i) => i % 2 === 0);
for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14)]) {
for (const make of [() => new wickra.Vpin(8, 5), () => new wickra.AmihudIlliquidity(14), () => new wickra.RollMeasure(14), () => new wickra.TradeSignAutocorrelation(10), () => new wickra.Pin(10)]) {
const batch = make().batch(price, size, isBuy);
const streamer = make();
assert.equal(batch.length, n);
@@ -1261,6 +1280,16 @@ test('vpin / amihud / roll reference + streaming matches batch', () => {
assert.ok((Number.isNaN(batch[i]) && s === null) || Math.abs(s - batch[i]) < 1e-9, `mismatch at ${i}`);
}
}
// Trade-sign autocorrelation: alternating signs -> -1, all buys -> +1.
let tsac = null;
const tsacInd = new wickra.TradeSignAutocorrelation(10);
for (let i = 0; i < 20; i++) tsac = tsacInd.update(100, 1, i % 2 === 0);
assert.ok(Math.abs(tsac - -1.0) < 1e-12);
// PIN: one-sided flow -> 1, balanced flow -> 0.
let pin = null;
const pinInd = new wickra.Pin(10);
for (let i = 0; i < 20; i++) pin = pinInd.update(100, 1, true);
assert.ok(Math.abs(pin - 1.0) < 1e-12);
});
test('price-impact indicators reference values', () => {
+211
View File
@@ -342,6 +342,10 @@ export interface TdSequentialValue {
countdown: number
direction: number
}
export interface TdMovingAverageValue {
st1: number
st2: number
}
/** TD Lines output pair: latest TDST resistance / support (NaN if unset). */
export interface TdLinesValue {
resistance: number
@@ -381,6 +385,20 @@ export interface HeikinAshiValue {
low: number
close: number
}
export interface SmoothedHeikinAshiValue {
open: number
high: number
low: number
close: number
}
export interface EquivolumeValue {
height: number
width: number
}
export interface CandleVolumeValue {
body: number
width: number
}
export interface ValueAreaValue {
poc: number
vah: number
@@ -1431,6 +1449,19 @@ export declare class BetaNeutralSpread {
isReady(): boolean
warmupPeriod(): number
}
export type HasbrouckInformationShareNode = HasbrouckInformationShare
export declare class HasbrouckInformationShare {
constructor(period: number)
update(x: number, y: number): number | null
/**
* Batch over two equally-sized arrays. Returns a length-`n` array
* with `NaN` for warmup positions.
*/
batch(x: Array<number>, y: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type PairSpreadZScoreNode = PairSpreadZScore
/**
* Pair spread z-score: two ctor params (`betaPeriod`, `zPeriod`), one `(a, b)`
@@ -3159,6 +3190,24 @@ export declare class TDCombo {
isReady(): boolean
warmupPeriod(): number
}
export type TdDWaveNode = TDDWave
export declare class TDDWave {
constructor(strength: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdMovingAverageNode = TDMovingAverage
export declare class TDMovingAverage {
constructor(periodSt1: number, periodSt2: number)
update(high: number, low: number): TdMovingAverageValue | null
batch(high: Array<number>, low: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdCountdownNode = TDCountdown
export declare class TDCountdown {
constructor(setupLookback: number, setupTarget: number, countdownLookback: number, countdownTarget: number)
@@ -3340,6 +3389,78 @@ export declare class HeikinAshi {
isReady(): boolean
warmupPeriod(): number
}
export type HeikinAshiOscillatorNode = HeikinAshiOscillator
export declare class HeikinAshiOscillator {
constructor(period: number)
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ThreeLineBreakNode = ThreeLineBreak
export declare class ThreeLineBreak {
constructor(lines: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type SmoothedHeikinAshiNode = SmoothedHeikinAshi
export declare class SmoothedHeikinAshi {
constructor(period: number)
update(open: number, high: number, low: number, close: number): SmoothedHeikinAshiValue | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type EquivolumeNode = Equivolume
export declare class Equivolume {
constructor(period: number)
update(high: number, low: number, volume: number): EquivolumeValue | null
batch(high: Array<number>, low: Array<number>, volume: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type CandleVolumeNode = CandleVolume
export declare class CandleVolume {
constructor(period: number)
update(open: number, close: number, volume: number): CandleVolumeValue | null
batch(open: Array<number>, close: Array<number>, volume: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type FryPanBottomNode = FryPanBottom
export declare class FryPanBottom {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type DumplingTopNode = DumplingTop
export declare class DumplingTop {
constructor(period: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type NewPriceLinesNode = NewPriceLines
export declare class NewPriceLines {
constructor(count: number)
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type ValueAreaNode = ValueArea
export declare class ValueArea {
constructor(period: number, binCount: number, valueAreaPct: number)
@@ -4072,6 +4193,78 @@ export declare class ThreeDrives {
isReady(): boolean
warmupPeriod(): number
}
export type TdCamouflageNode = TDCamouflage
export declare class TDCamouflage {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdClopNode = TDClop
export declare class TDClop {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdClopwinNode = TDClopwin
export declare class TDClopwin {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdPropulsionNode = TDPropulsion
export declare class TDPropulsion {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TdTrapNode = TDTrap
export declare class TDTrap {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TristarNode = Tristar
export declare class Tristar {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type HaramiCrossNode = HaramiCross
export declare class HaramiCross {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type TowerTopBottomNode = TowerTopBottom
export declare class TowerTopBottom {
constructor()
update(open: number, high: number, low: number, close: number): number | null
batch(open: Array<number>, high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OrderBookImbalanceTop1Node = OrderBookImbalanceTop1
export declare class OrderBookImbalanceTop1 {
constructor()
@@ -4153,6 +4346,24 @@ export declare class TradeImbalance {
isReady(): boolean
warmupPeriod(): number
}
export type TradeSignAutocorrelationNode = TradeSignAutocorrelation
export declare class TradeSignAutocorrelation {
constructor(period: number)
update(price: number, size: number, isBuy: boolean): number | null
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type PinNode = Pin
export declare class Pin {
constructor(window: number)
update(price: number, size: number, isBuy: boolean): number | null
batch(price: Array<number>, size: Array<number>, isBuy: Array<boolean>): Array<number>
reset(): void
isReady(): boolean
warmupPeriod(): number
}
export type OrderFlowImbalanceNode = OrderFlowImbalance
export declare class OrderFlowImbalance {
constructor(period: number)
+22 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-arm64-gnu",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-x64-gnu.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-arm64-msvc",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-arm64-msvc.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-x64-msvc",
"version": "0.6.6",
"version": "0.7.0",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.6.6",
"version": "0.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.6.6",
"version": "0.7.0",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.6.6",
"wickra-darwin-x64": "0.6.6",
"wickra-linux-arm64-gnu": "0.6.6",
"wickra-linux-x64-gnu": "0.6.6",
"wickra-win32-arm64-msvc": "0.6.6",
"wickra-win32-x64-msvc": "0.6.6"
"wickra-darwin-arm64": "0.7.0",
"wickra-darwin-x64": "0.7.0",
"wickra-linux-arm64-gnu": "0.7.0",
"wickra-linux-x64-gnu": "0.7.0",
"wickra-win32-arm64-msvc": "0.7.0",
"wickra-win32-x64-msvc": "0.7.0"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.0.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.0.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.0.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.0.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.0.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.6.6",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.6.6.tgz",
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.0.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.6.6",
"version": "0.7.0",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>",
"main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.6.6",
"wickra-linux-arm64-gnu": "0.6.6",
"wickra-darwin-x64": "0.6.6",
"wickra-darwin-arm64": "0.6.6",
"wickra-win32-x64-msvc": "0.6.6",
"wickra-win32-arm64-msvc": "0.6.6"
"wickra-linux-x64-gnu": "0.7.0",
"wickra-linux-arm64-gnu": "0.7.0",
"wickra-darwin-x64": "0.7.0",
"wickra-darwin-arm64": "0.7.0",
"wickra-win32-x64-msvc": "0.7.0",
"wickra-win32-arm64-msvc": "0.7.0"
},
"scripts": {
"build": "napi build --platform --release",
+739
View File
@@ -884,6 +884,11 @@ node_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
node_pair_indicator!(
HasbrouckInformationShareNode,
"HasbrouckInformationShare",
wc::HasbrouckInformationShare
);
// ============================== PairSpreadZScore ==============================
@@ -11100,6 +11105,124 @@ impl TdComboNode {
}
}
// ============================== TD D-Wave ==============================
#[napi(js_name = "TDDWave")]
pub struct TdDWaveNode {
inner: wc::TdDWave,
}
#[napi]
impl TdDWaveNode {
#[napi(constructor)]
pub fn new(strength: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TdDWave::new(strength as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== TD Moving Averages ==============================
#[napi(object)]
pub struct TdMovingAverageValue {
pub st1: f64,
pub st2: f64,
}
#[napi(js_name = "TDMovingAverage")]
pub struct TdMovingAverageNode {
inner: wc::TdMovingAverage,
}
#[napi]
impl TdMovingAverageNode {
#[napi(constructor)]
pub fn new(period_st1: u32, period_st2: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TdMovingAverage::new(period_st1 as usize, period_st2 as usize)
.map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64) -> napi::Result<Option<TdMovingAverageValue>> {
Ok(self
.inner
.update(cnd(high, low, low, 0.0)?)
.map(|o| TdMovingAverageValue {
st1: o.st1,
st2: o.st2,
}))
}
#[napi]
pub fn batch(&mut self, high: Vec<f64>, low: Vec<f64>) -> napi::Result<Vec<f64>> {
if high.len() != low.len() {
return Err(NapiError::from_reason(
"high, low must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
if let Some(o) = self.inner.update(cnd(high[i], low[i], low[i], 0.0)?) {
out[i * 2] = o.st1;
out[i * 2 + 1] = o.st2;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== TD Countdown ==============================
#[napi(js_name = "TDCountdown")]
@@ -12159,6 +12282,512 @@ impl HeikinAshiNode {
}
}
// ============================== Heikin-Ashi Oscillator ==============================
#[napi(js_name = "HeikinAshiOscillator")]
pub struct HeikinAshiOscillatorNode {
inner: wc::HeikinAshiOscillator,
}
#[napi]
impl HeikinAshiOscillatorNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::HeikinAshiOscillator::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<f64>> {
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(c))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Three Line Break ==============================
#[napi(js_name = "ThreeLineBreak")]
pub struct ThreeLineBreakNode {
inner: wc::ThreeLineBreak,
}
#[napi]
impl ThreeLineBreakNode {
#[napi(constructor)]
pub fn new(lines: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::ThreeLineBreak::new(lines as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Smoothed Heikin-Ashi ==============================
#[napi(object)]
pub struct SmoothedHeikinAshiValue {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
}
#[napi(js_name = "SmoothedHeikinAshi")]
pub struct SmoothedHeikinAshiNode {
inner: wc::SmoothedHeikinAshi,
}
#[napi]
impl SmoothedHeikinAshiNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::SmoothedHeikinAshi::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> napi::Result<Option<SmoothedHeikinAshiValue>> {
let c = wc::Candle::new(open, high, low, close, 0.0, 0).map_err(map_err)?;
Ok(self.inner.update(c).map(|o| SmoothedHeikinAshiValue {
open: o.open,
high: o.high,
low: o.low,
close: o.close,
}))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"open, high, low, close must be equal length".to_string(),
));
}
let n = open.len();
let mut out = vec![f64::NAN; n * 4];
for i in 0..n {
let c = wc::Candle::new(open[i], high[i], low[i], close[i], 0.0, 0).map_err(map_err)?;
if let Some(o) = self.inner.update(c) {
out[i * 4] = o.open;
out[i * 4 + 1] = o.high;
out[i * 4 + 2] = o.low;
out[i * 4 + 3] = o.close;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Equivolume ==============================
#[napi(object)]
pub struct EquivolumeValue {
pub height: f64,
pub width: f64,
}
#[napi(js_name = "Equivolume")]
pub struct EquivolumeNode {
inner: wc::Equivolume,
}
#[napi]
impl EquivolumeNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Equivolume::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
high: f64,
low: f64,
volume: f64,
) -> napi::Result<Option<EquivolumeValue>> {
let c = wc::Candle::new(low, high, low, low, volume, 0).map_err(map_err)?;
Ok(self.inner.update(c).map(|o| EquivolumeValue {
height: o.height,
width: o.width,
}))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
volume: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != volume.len() {
return Err(NapiError::from_reason(
"high, low, volume must be equal length".to_string(),
));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let c =
wc::Candle::new(low[i], high[i], low[i], low[i], volume[i], 0).map_err(map_err)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.height;
out[i * 2 + 1] = o.width;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== CandleVolume ==============================
#[napi(object)]
pub struct CandleVolumeValue {
pub body: f64,
pub width: f64,
}
#[napi(js_name = "CandleVolume")]
pub struct CandleVolumeNode {
inner: wc::CandleVolume,
}
#[napi]
impl CandleVolumeNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::CandleVolume::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(
&mut self,
open: f64,
close: f64,
volume: f64,
) -> napi::Result<Option<CandleVolumeValue>> {
let high = open.max(close);
let low = open.min(close);
let c = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
Ok(self.inner.update(c).map(|o| CandleVolumeValue {
body: o.body,
width: o.width,
}))
}
#[napi]
pub fn batch(
&mut self,
open: Vec<f64>,
close: Vec<f64>,
volume: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if open.len() != close.len() || close.len() != volume.len() {
return Err(NapiError::from_reason(
"open, close, volume must be equal length".to_string(),
));
}
let n = open.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let high = open[i].max(close[i]);
let low = open[i].min(close[i]);
let c = wc::Candle::new(open[i], high, low, close[i], volume[i], 0).map_err(map_err)?;
if let Some(o) = self.inner.update(c) {
out[i * 2] = o.body;
out[i * 2 + 1] = o.width;
}
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Frying Pan Bottom ==============================
#[napi(js_name = "FryPanBottom")]
pub struct FryPanBottomNode {
inner: wc::FryPanBottom,
}
#[napi]
impl FryPanBottomNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::FryPanBottom::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== Dumpling Top ==============================
#[napi(js_name = "DumplingTop")]
pub struct DumplingTopNode {
inner: wc::DumplingTop,
}
#[napi]
impl DumplingTopNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::DumplingTop::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== New Price Lines ==============================
#[napi(js_name = "NewPriceLines")]
pub struct NewPriceLinesNode {
inner: wc::NewPriceLines,
}
#[napi]
impl NewPriceLinesNode {
#[napi(constructor)]
pub fn new(count: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::NewPriceLines::new(count as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, high: f64, low: f64, close: f64) -> napi::Result<Option<f64>> {
Ok(self.inner.update(cnd(high, low, close, 0.0)?))
}
#[napi]
pub fn batch(
&mut self,
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
) -> napi::Result<Vec<f64>> {
if high.len() != low.len() || low.len() != close.len() {
return Err(NapiError::from_reason(
"high, low, close must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
out.push(
self.inner
.update(cnd(high[i], low[i], close[i], 0.0)?)
.unwrap_or(f64::NAN),
);
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// ============================== ValueArea ==============================
#[napi(object)]
@@ -12807,6 +13436,14 @@ node_candle_pattern!(CrabNode, wc::Crab, "Crab");
node_candle_pattern!(SharkNode, wc::Shark, "Shark");
node_candle_pattern!(CypherNode, wc::Cypher, "Cypher");
node_candle_pattern!(ThreeDrivesNode, wc::ThreeDrives, "ThreeDrives");
node_candle_pattern!(TdCamouflageNode, wc::TdCamouflage, "TDCamouflage");
node_candle_pattern!(TdClopNode, wc::TdClop, "TDClop");
node_candle_pattern!(TdClopwinNode, wc::TdClopwin, "TDClopwin");
node_candle_pattern!(TdPropulsionNode, wc::TdPropulsion, "TDPropulsion");
node_candle_pattern!(TdTrapNode, wc::TdTrap, "TDTrap");
node_candle_pattern!(TristarNode, wc::Tristar, "Tristar");
node_candle_pattern!(HaramiCrossNode, wc::HaramiCross, "HaramiCross");
node_candle_pattern!(TowerTopBottomNode, wc::TowerTopBottom, "TowerTopBottom");
// ============================== Microstructure: Order Book ==============================
//
@@ -13107,6 +13744,108 @@ impl TradeImbalanceNode {
}
}
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
#[napi(js_name = "TradeSignAutocorrelation")]
pub struct TradeSignAutocorrelationNode {
inner: wc::TradeSignAutocorrelation,
}
#[napi]
impl TradeSignAutocorrelationNode {
#[napi(constructor)]
pub fn new(period: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::TradeSignAutocorrelation::new(period as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
#[napi]
pub fn batch(
&mut self,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> napi::Result<Vec<f64>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(NapiError::from_reason(
"price, size, is_buy must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// PIN carries a `window` parameter, so it is hand-written.
#[napi(js_name = "Pin")]
pub struct PinNode {
inner: wc::Pin,
}
#[napi]
impl PinNode {
#[napi(constructor)]
pub fn new(window: u32) -> napi::Result<Self> {
Ok(Self {
inner: wc::Pin::new(window as usize).map_err(map_err)?,
})
}
#[napi]
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> napi::Result<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
#[napi]
pub fn batch(
&mut self,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> napi::Result<Vec<f64>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(NapiError::from_reason(
"price, size, is_buy must be equal length".to_string(),
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
}
Ok(out)
}
#[napi]
pub fn reset(&mut self) {
self.inner.reset();
}
#[napi(js_name = "isReady")]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[napi(js_name = "warmupPeriod")]
pub fn warmup_period(&self) -> u32 {
self.inner.warmup_period() as u32
}
}
// Order Flow Imbalance: order-book input with a `period` parameter.
#[napi(js_name = "OrderFlowImbalance")]
pub struct OrderFlowImbalanceNode {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.6.6"
version = "0.7.0"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+42
View File
@@ -322,6 +322,13 @@ from ._wickra import (
WilliamsFractals,
ZigZag,
# DeMark
TDMovingAverage,
TDDWave,
TDTrap,
TDPropulsion,
TDClopwin,
TDClop,
TDCamouflage,
TDSetup,
TDSequential,
TDDeMarker,
@@ -337,6 +344,11 @@ from ._wickra import (
# Ichimoku & alternative charts
Ichimoku,
HeikinAshi,
SmoothedHeikinAshi,
HeikinAshiOscillator,
ThreeLineBreak,
Equivolume,
CandleVolume,
# Market Profile
ValueArea,
VolumeProfile,
@@ -348,6 +360,12 @@ from ._wickra import (
KagiBars,
PointAndFigureBars,
# Candlestick patterns
TowerTopBottom,
HaramiCross,
Tristar,
FryPanBottom,
DumplingTop,
NewPriceLines,
Doji,
Hammer,
InvertedHammer,
@@ -446,6 +464,8 @@ from ._wickra import (
QuotedSpread,
DepthSlope,
# Microstructure: trade flow
Pin,
TradeSignAutocorrelation,
RollMeasure,
AmihudIlliquidity,
Vpin,
@@ -453,6 +473,7 @@ from ._wickra import (
CumulativeVolumeDelta,
TradeImbalance,
# Microstructure: price impact
HasbrouckInformationShare,
EffectiveSpread,
RealizedSpread,
KylesLambda,
@@ -819,6 +840,13 @@ __all__ = [
"WilliamsFractals",
"ZigZag",
# DeMark
"TDMovingAverage",
"TDDWave",
"TDTrap",
"TDPropulsion",
"TDClopwin",
"TDClop",
"TDCamouflage",
"TDSetup",
"TDSequential",
"TDDeMarker",
@@ -834,6 +862,11 @@ __all__ = [
# Ichimoku & alternative charts
"Ichimoku",
"HeikinAshi",
"SmoothedHeikinAshi",
"HeikinAshiOscillator",
"ThreeLineBreak",
"Equivolume",
"CandleVolume",
# Market Profile
"ValueArea",
"VolumeProfile",
@@ -845,6 +878,12 @@ __all__ = [
"KagiBars",
"PointAndFigureBars",
# Candlestick patterns
"TowerTopBottom",
"HaramiCross",
"Tristar",
"FryPanBottom",
"DumplingTop",
"NewPriceLines",
"Doji",
"Hammer",
"InvertedHammer",
@@ -943,6 +982,8 @@ __all__ = [
"QuotedSpread",
"DepthSlope",
# Microstructure: trade flow
"Pin",
"TradeSignAutocorrelation",
"RollMeasure",
"AmihudIlliquidity",
"Vpin",
@@ -950,6 +991,7 @@ __all__ = [
"CumulativeVolumeDelta",
"TradeImbalance",
# Microstructure: price impact
"HasbrouckInformationShare",
"EffectiveSpread",
"RealizedSpread",
"KylesLambda",
+852
View File
@@ -14086,6 +14086,131 @@ impl PyTdCombo {
}
}
// ============================== TD D-Wave ==============================
#[pyclass(name = "TDDWave", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyTdDWave {
inner: wc::TdDWave,
}
#[pymethods]
impl PyTdDWave {
#[new]
#[pyo3(signature = (strength=2))]
fn new(strength: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TdDWave::new(strength).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== TD Moving Averages ==============================
#[pyclass(
name = "TDMovingAverage",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyTdMovingAverage {
inner: wc::TdMovingAverage,
}
#[pymethods]
impl PyTdMovingAverage {
#[new]
#[pyo3(signature = (period_st1=5, period_st2=13))]
fn new(period_st1: usize, period_st2: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TdMovingAverage::new(period_st1, period_st2).map_err(map_err)?,
})
}
/// Returns `(st1, st2)`.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.st1, o.st2)))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() {
return Err(PyValueError::new_err("high, low must be equal length"));
}
let n = h.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], 0.0, 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.st1;
out[i * 2 + 1] = o.st2;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== TD Countdown ==============================
#[pyclass(name = "TDCountdown", module = "wickra._wickra", skip_from_py_object)]
@@ -15232,6 +15357,534 @@ impl PyVariance {
}
}
// ============================== Heikin-Ashi Oscillator ==============================
#[pyclass(
name = "HeikinAshiOscillator",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyHeikinAshiOscillator {
inner: wc::HeikinAshiOscillator,
}
#[pymethods]
impl PyHeikinAshiOscillator {
#[new]
#[pyo3(signature = (period=5))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::HeikinAshiOscillator::new(period).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let o = open
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"open, high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(o.len());
for i in 0..o.len() {
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== Three Line Break ==============================
#[pyclass(
name = "ThreeLineBreak",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyThreeLineBreak {
inner: wc::ThreeLineBreak,
}
#[pymethods]
impl PyThreeLineBreak {
#[new]
#[pyo3(signature = (lines=3))]
fn new(lines: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::ThreeLineBreak::new(lines).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== Smoothed Heikin-Ashi ==============================
#[pyclass(
name = "SmoothedHeikinAshi",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PySmoothedHeikinAshi {
inner: wc::SmoothedHeikinAshi,
}
#[pymethods]
impl PySmoothedHeikinAshi {
#[new]
#[pyo3(signature = (period=5))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::SmoothedHeikinAshi::new(period).map_err(map_err)?,
})
}
/// Returns `(open, high, low, close)`.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64, f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self
.inner
.update(c)
.map(|o| (o.open, o.high, o.low, o.close)))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let o = open
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if o.len() != h.len() || h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"open, high, low, close must be equal length",
));
}
let n = o.len();
let mut out = vec![f64::NAN; n * 4];
for i in 0..n {
let candle = wc::Candle::new(o[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
if let Some(v) = self.inner.update(candle) {
out[i * 4] = v.open;
out[i * 4 + 1] = v.high;
out[i * 4 + 2] = v.low;
out[i * 4 + 3] = v.close;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 4), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== Equivolume ==============================
#[pyclass(name = "Equivolume", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyEquivolume {
inner: wc::Equivolume,
}
#[pymethods]
impl PyEquivolume {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Equivolume::new(period).map_err(map_err)?,
})
}
/// Returns `(height, width)`.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.height, o.width)))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let vol = volume
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != vol.len() {
return Err(PyValueError::new_err(
"high, low, volume must be equal length",
));
}
let n = h.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle = wc::Candle::new(l[i], h[i], l[i], l[i], vol[i], 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.height;
out[i * 2 + 1] = o.width;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== CandleVolume ==============================
#[pyclass(name = "CandleVolume", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyCandleVolume {
inner: wc::CandleVolume,
}
#[pymethods]
impl PyCandleVolume {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::CandleVolume::new(period).map_err(map_err)?,
})
}
/// Returns `(body, width)`.
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<(f64, f64)>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c).map(|o| (o.body, o.width)))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
open: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
volume: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray2<f64>>> {
let o = open
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let vol = volume
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if o.len() != c.len() || c.len() != vol.len() {
return Err(PyValueError::new_err(
"open, close, volume must be equal length",
));
}
let n = o.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let high = o[i].max(c[i]);
let low = o[i].min(c[i]);
let candle = wc::Candle::new(o[i], high, low, c[i], vol[i], 0).map_err(map_err)?;
if let Some(v) = self.inner.update(candle) {
out[i * 2] = v.body;
out[i * 2 + 1] = v.width;
}
}
Ok(numpy::ndarray::Array2::from_shape_vec((n, 2), out)
.expect("shape consistent")
.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== Frying Pan Bottom ==============================
#[pyclass(name = "FryPanBottom", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyFryPanBottom {
inner: wc::FryPanBottom,
}
#[pymethods]
impl PyFryPanBottom {
#[new]
#[pyo3(signature = (period=9))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::FryPanBottom::new(period).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== Dumpling Top ==============================
#[pyclass(name = "DumplingTop", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyDumplingTop {
inner: wc::DumplingTop,
}
#[pymethods]
impl PyDumplingTop {
#[new]
#[pyo3(signature = (period=9))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::DumplingTop::new(period).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== New Price Lines ==============================
#[pyclass(name = "NewPriceLines", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyNewPriceLines {
inner: wc::NewPriceLines,
}
#[pymethods]
impl PyNewPriceLines {
#[new]
#[pyo3(signature = (count=5))]
fn new(count: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::NewPriceLines::new(count).map_err(map_err)?,
})
}
fn update(&mut self, candle: &Bound<'_, PyAny>) -> PyResult<Option<f64>> {
let c = extract_candle(candle)?;
Ok(self.inner.update(c))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
high: PyReadonlyArray1<'py, f64>,
low: PyReadonlyArray1<'py, f64>,
close: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let h = high
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let l = low
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let c = close
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if h.len() != l.len() || l.len() != c.len() {
return Err(PyValueError::new_err(
"high, low, close must be equal length",
));
}
let mut out = Vec::with_capacity(h.len());
for i in 0..h.len() {
let candle = wc::Candle::new(c[i], h[i], l[i], c[i], 0.0, 0).map_err(map_err)?;
out.push(self.inner.update(candle).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ============================== CoefficientOfVariation ==============================
#[pyclass(
@@ -16327,6 +16980,70 @@ impl PyRollingCorrelation {
}
}
// ========================= HasbrouckInformationShare =========================
#[pyclass(
name = "HasbrouckInformationShare",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyHasbrouckInformationShare {
inner: wc::HasbrouckInformationShare,
}
#[pymethods]
impl PyHasbrouckInformationShare {
#[new]
#[pyo3(signature = (period=20))]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::HasbrouckInformationShare::new(period).map_err(map_err)?,
})
}
fn update(&mut self, a: f64, b: f64) -> Option<f64> {
self.inner.update((a, b))
}
/// Batch over two equally-sized numpy arrays: `a` and `b`.
fn batch<'py>(
&mut self,
py: Python<'py>,
a: PyReadonlyArray1<'py, f64>,
b: PyReadonlyArray1<'py, f64>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
let xs = a
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
let ys = b
.as_slice()
.map_err(|_| PyValueError::new_err(NON_CONTIGUOUS))?;
if xs.len() != ys.len() {
return Err(PyValueError::new_err("a and b must be equal length"));
}
let mut out = Vec::with_capacity(xs.len());
for i in 0..xs.len() {
out.push(self.inner.update((xs[i], ys[i])).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
#[getter]
fn period(&self) -> usize {
self.inner.period()
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("HasbrouckInformationShare(period={})", self.inner.period())
}
}
// ============================== RollingCovariance ==============================
#[pyclass(
@@ -17703,6 +18420,14 @@ candle_pattern_no_param!(PyCrab, wc::Crab, "Crab");
candle_pattern_no_param!(PyShark, wc::Shark, "Shark");
candle_pattern_no_param!(PyCypher, wc::Cypher, "Cypher");
candle_pattern_no_param!(PyThreeDrives, wc::ThreeDrives, "ThreeDrives");
candle_pattern_no_param!(PyTdCamouflage, wc::TdCamouflage, "TDCamouflage");
candle_pattern_no_param!(PyTdClop, wc::TdClop, "TDClop");
candle_pattern_no_param!(PyTdClopwin, wc::TdClopwin, "TDClopwin");
candle_pattern_no_param!(PyTdPropulsion, wc::TdPropulsion, "TDPropulsion");
candle_pattern_no_param!(PyTdTrap, wc::TdTrap, "TDTrap");
candle_pattern_no_param!(PyTristar, wc::Tristar, "Tristar");
candle_pattern_no_param!(PyHaramiCross, wc::HaramiCross, "HaramiCross");
candle_pattern_no_param!(PyTowerTopBottom, wc::TowerTopBottom, "TowerTopBottom");
// ============================== Microstructure: Order Book ==============================
//
// Order-book indicators consume a depth snapshot rather than OHLCV. Streaming
@@ -17991,6 +18716,112 @@ impl PyTradeImbalance {
}
}
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
#[pyclass(
name = "TradeSignAutocorrelation",
module = "wickra._wickra",
skip_from_py_object
)]
#[derive(Clone)]
struct PyTradeSignAutocorrelation {
inner: wc::TradeSignAutocorrelation,
}
#[pymethods]
impl PyTradeSignAutocorrelation {
#[new]
fn new(period: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
})
}
fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(PyValueError::new_err(
"price, size, is_buy must be equal length",
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("TradeSignAutocorrelation(period={})", self.inner.period())
}
}
// PIN carries a `window` parameter, so it is hand-written.
#[pyclass(name = "Pin", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyPin {
inner: wc::Pin,
}
#[pymethods]
impl PyPin {
#[new]
fn new(window: usize) -> PyResult<Self> {
Ok(Self {
inner: wc::Pin::new(window).map_err(map_err)?,
})
}
fn update(&mut self, price: f64, size: f64, is_buy: bool) -> PyResult<Option<f64>> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
fn batch<'py>(
&mut self,
py: Python<'py>,
price: Vec<f64>,
size: Vec<f64>,
is_buy: Vec<bool>,
) -> PyResult<Bound<'py, PyArray1<f64>>> {
if price.len() != size.len() || size.len() != is_buy.len() {
return Err(PyValueError::new_err(
"price, size, is_buy must be equal length",
));
}
let mut out = Vec::with_capacity(price.len());
for i in 0..price.len() {
let trade = build_trade(price[i], size[i], is_buy[i])?;
out.push(self.inner.update(trade).unwrap_or(f64::NAN));
}
Ok(out.into_pyarray(py))
}
fn reset(&mut self) {
self.inner.reset();
}
fn is_ready(&self) -> bool {
self.inner.is_ready()
}
fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
format!("Pin(window={})", self.inner.window())
}
}
// Order Flow Imbalance carries a `period` parameter and an order-book input,
// so it is hand-written.
#[pyclass(
@@ -24028,6 +24859,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyTdRei>()?;
m.add_class::<PyTdPressure>()?;
m.add_class::<PyTdCombo>()?;
m.add_class::<PyTdDWave>()?;
m.add_class::<PyTdMovingAverage>()?;
m.add_class::<PyTdCountdown>()?;
m.add_class::<PyTdLines>()?;
m.add_class::<PyTdRangeProjection>()?;
@@ -24056,6 +24889,11 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
// Family 13 — Ichimoku & alternative charts
m.add_class::<PyIchimoku>()?;
m.add_class::<PyHeikinAshi>()?;
m.add_class::<PySmoothedHeikinAshi>()?;
m.add_class::<PyHeikinAshiOscillator>()?;
m.add_class::<PyThreeLineBreak>()?;
m.add_class::<PyEquivolume>()?;
m.add_class::<PyCandleVolume>()?;
m.add_class::<PyVariance>()?;
m.add_class::<PyCoefficientOfVariation>()?;
m.add_class::<PySkewness>()?;
@@ -24075,6 +24913,7 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyCointegration>()?;
m.add_class::<PyRelativeStrengthAB>()?;
m.add_class::<PyRollingCorrelation>()?;
m.add_class::<PyHasbrouckInformationShare>()?;
m.add_class::<PyRollingCovariance>()?;
m.add_class::<PyOuHalfLife>()?;
m.add_class::<PySpreadHurst>()?;
@@ -24165,6 +25004,8 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PySignedVolume>()?;
m.add_class::<PyCumulativeVolumeDelta>()?;
m.add_class::<PyTradeImbalance>()?;
m.add_class::<PyTradeSignAutocorrelation>()?;
m.add_class::<PyPin>()?;
m.add_class::<PyOrderFlowImbalance>()?;
m.add_class::<PyVpin>()?;
m.add_class::<PyAmihudIlliquidity>()?;
@@ -24336,5 +25177,16 @@ fn _wickra(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyAdaptiveRsi>()?;
m.add_class::<PyUniversalOscillator>()?;
m.add_class::<PyAdaptiveCci>()?;
m.add_class::<PyTdCamouflage>()?;
m.add_class::<PyTdClop>()?;
m.add_class::<PyTdClopwin>()?;
m.add_class::<PyTdPropulsion>()?;
m.add_class::<PyTdTrap>()?;
m.add_class::<PyTristar>()?;
m.add_class::<PyHaramiCross>()?;
m.add_class::<PyTowerTopBottom>()?;
m.add_class::<PyFryPanBottom>()?;
m.add_class::<PyDumplingTop>()?;
m.add_class::<PyNewPriceLines>()?;
Ok(())
}
@@ -217,6 +217,7 @@ def test_scalar_streaming_matches_batch(cls, args, sine_prices):
# --- Two-series (asset, benchmark) indicators -----------------------------
PAIR = [
(ta.HasbrouckInformationShare, (2,)),
(ta.KendallTau, (20,)),
(ta.SpreadAr1Coefficient, (40,)),
(ta.GrangerCausality, (60, 1)),
@@ -382,6 +383,62 @@ def test_relative_strength_streaming_matches_batch():
# 6-tuple candle; the batch helper takes only the columns it needs.
CANDLE_SCALAR = {
"FryPanBottom": (
lambda: ta.FryPanBottom(9),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"NewPriceLines": (
lambda: ta.NewPriceLines(5),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"DumplingTop": (
lambda: ta.DumplingTop(9),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"TowerTopBottom": (
lambda: ta.TowerTopBottom(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"HaramiCross": (
lambda: ta.HaramiCross(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"Tristar": (
lambda: ta.Tristar(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"ThreeLineBreak": (
lambda: ta.ThreeLineBreak(3),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"HeikinAshiOscillator": (
lambda: ta.HeikinAshiOscillator(5),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TDDWave": (
lambda: ta.TDDWave(2),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"TDTrap": (
lambda: ta.TDTrap(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TDPropulsion": (
lambda: ta.TDPropulsion(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TDClopwin": (
lambda: ta.TDClopwin(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TDClop": (
lambda: ta.TDClop(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"TDCamouflage": (
lambda: ta.TDCamouflage(),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
),
"PivotReversal": (
lambda: ta.PivotReversal(1, 1),
lambda ind, h, l, c, v: ind.batch(h, l, c),
@@ -952,6 +1009,26 @@ def test_candle_scalar_streaming_matches_batch(name, ohlcv):
# --- Candle-input, multi-output indicators --------------------------------
MULTI = {
"CandleVolume": (
lambda: ta.CandleVolume(20),
lambda ind, h, l, c, v: ind.batch(c, c, v),
2,
),
"Equivolume": (
lambda: ta.Equivolume(20),
lambda ind, h, l, c, v: ind.batch(h, l, v),
2,
),
"SmoothedHeikinAshi": (
lambda: ta.SmoothedHeikinAshi(5),
lambda ind, h, l, c, v: ind.batch(c, h, l, c),
4,
),
"TDMovingAverage": (
lambda: ta.TDMovingAverage(5, 13),
lambda ind, h, l, c, v: ind.batch(h, l),
2,
),
"VolumeWeightedSr": (
lambda: ta.VolumeWeightedSr(3),
lambda ind, h, l, c, v: ind.batch(h, l, v),
@@ -3174,6 +3251,86 @@ def test_pivot_reversal_reference():
assert t.update((13.0, 14.0, 12.5, 13.0, 1.0, 4)) == pytest.approx(1.0)
def test_td_camouflage_reference():
t = ta.TDCamouflage()
assert t.update((10.0, 11.0, 8.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((9.0, 10.0, 7.0, 9.5, 1.0, 1)) == pytest.approx(1.0)
def test_td_clop_reference():
t = ta.TDClop()
assert t.update((10.0, 12.0, 9.0, 11.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((9.0, 13.0, 8.0, 12.0, 1.0, 1)) == pytest.approx(1.0)
def test_td_clopwin_reference():
t = ta.TDClopwin()
assert t.update((10.0, 15.0, 9.0, 14.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((11.0, 14.0, 10.0, 13.0, 1.0, 1)) == pytest.approx(1.0)
def test_td_propulsion_reference():
t = ta.TDPropulsion()
assert t.update((9.5, 11.0, 9.0, 10.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((10.5, 12.0, 10.0, 11.5, 1.0, 1)) == pytest.approx(1.0)
def test_td_trap_reference():
t = ta.TDTrap()
assert t.update((100.0, 110.0, 90.0, 100.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((101.5, 108.0, 95.0, 102.0, 1.0, 1)) == pytest.approx(0.0)
assert t.update((106.0, 112.0, 100.0, 109.0, 1.0, 2)) == pytest.approx(1.0)
def test_heikin_ashi_oscillator_reference():
t = ta.HeikinAshiOscillator(5)
def test_three_line_break_reference():
t = ta.ThreeLineBreak(3)
def test_smoothed_heikin_ashi_reference():
t = ta.SmoothedHeikinAshi(5)
def test_equivolume_reference():
t = ta.Equivolume(20)
def test_candle_volume_reference():
t = ta.CandleVolume(20)
def test_tristar_reference():
t = ta.Tristar()
assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 0)) == pytest.approx(0.0)
assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(0.0)
assert t.update((100.0, 101.0, 99.0, 100.02, 1.0, 2)) == pytest.approx(-1.0)
def test_harami_cross_reference():
t = ta.HaramiCross()
assert t.update((110.0, 110.2, 99.8, 100.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((105.0, 106.0, 104.0, 105.02, 1.0, 1)) == pytest.approx(1.0)
def test_tower_top_bottom_reference():
t = ta.TowerTopBottom()
assert t.update((100.0, 110.1, 99.9, 110.0, 1.0, 0)) == pytest.approx(0.0)
assert t.update((105.0, 107.0, 103.0, 105.1, 1.0, 1)) == pytest.approx(0.0)
assert t.update((110.0, 110.1, 99.9, 100.0, 1.0, 2)) == pytest.approx(-1.0)
def test_hasbrouck_information_share_reference():
t = ta.HasbrouckInformationShare(2)
assert t.update(7.0, 9.0) is None
assert t.update(7.0, 9.0) is None
assert t.update(7.0, 9.0) == pytest.approx(0.5)
# --- Lifecycle ------------------------------------------------------------
@@ -3514,6 +3671,8 @@ def test_tradeflow_indicators_streaming_equals_batch():
lambda: ta.Vpin(8.0, 5),
lambda: ta.AmihudIlliquidity(14),
lambda: ta.RollMeasure(14),
lambda: ta.TradeSignAutocorrelation(10),
lambda: ta.Pin(10),
):
batch = make().batch(price, size, is_buy)
streamer = make()
@@ -3525,6 +3684,34 @@ def test_tradeflow_indicators_streaming_equals_batch():
assert _eq_nan(batch, streamed)
def test_trade_sign_autocorrelation_reference():
# Perfectly alternating aggressor signs -> lag-1 autocorrelation -1.
t = ta.TradeSignAutocorrelation(10)
last = None
for i in range(20):
last = t.update(100.0, 1.0, i % 2 == 0)
assert last == pytest.approx(-1.0)
# All buys -> perfectly persistent flow -> +1.
t2 = ta.TradeSignAutocorrelation(10)
for _ in range(20):
last2 = t2.update(100.0, 1.0, True)
assert last2 == pytest.approx(1.0)
def test_pin_reference():
# One-sided flow (all buys) -> maximally informed -> PIN 1.
p = ta.Pin(10)
last = None
for _ in range(20):
last = p.update(100.0, 1.0, True)
assert last == pytest.approx(1.0)
# Balanced flow -> uninformed -> PIN 0.
p2 = ta.Pin(10)
for i in range(20):
last2 = p2.update(100.0, 1.0, i % 2 == 0)
assert last2 == pytest.approx(0.0)
def test_price_impact_indicators_streaming_equals_batch():
n = 40
mid = np.array([100.0 + 0.5 * math.sin(i * 0.4) for i in range(n)], dtype=np.float64)
+620
View File
@@ -563,6 +563,11 @@ wasm_pair_indicator!(
"BetaNeutralSpread",
wc::BetaNeutralSpread
);
wasm_pair_indicator!(
WasmHasbrouckInformationShare,
"HasbrouckInformationShare",
wc::HasbrouckInformationShare
);
// ---------- PairSpreadZScore (two params) ----------
@@ -7817,6 +7822,109 @@ impl WasmTdCombo {
}
}
// ---------- TD D-Wave ----------
#[wasm_bindgen(js_name = TDDWave)]
pub struct WasmTdDWave {
inner: wc::TdDWave,
}
#[wasm_bindgen(js_class = TDDWave)]
impl WasmTdDWave {
#[wasm_bindgen(constructor)]
pub fn new(strength: usize) -> Result<WasmTdDWave, JsError> {
Ok(Self {
inner: wc::TdDWave::new(strength).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- TD Moving Averages ----------
#[wasm_bindgen(js_name = TDMovingAverage)]
pub struct WasmTdMovingAverage {
inner: wc::TdMovingAverage,
}
#[wasm_bindgen(js_class = TDMovingAverage)]
impl WasmTdMovingAverage {
#[wasm_bindgen(constructor)]
pub fn new(period_st1: usize, period_st2: usize) -> Result<WasmTdMovingAverage, JsError> {
Ok(Self {
inner: wc::TdMovingAverage::new(period_st1, period_st2).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64) -> Result<JsValue, JsError> {
let candle = make_candle(high, low, low, 0.0)?;
match self.inner.update(candle) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"st1".into(), &o.st1.into()).ok();
Reflect::set(&obj, &"st2".into(), &o.st2.into()).ok();
Ok(obj.into())
}
None => Ok(JsValue::NULL),
}
}
pub fn batch(&mut self, high: &[f64], low: &[f64]) -> Result<Float64Array, JsError> {
if high.len() != low.len() {
return Err(JsError::new("high, low must be equal length"));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle = make_candle(high[i], low[i], low[i], 0.0)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.st1;
out[i * 2 + 1] = o.st2;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- TD Countdown ----------
#[wasm_bindgen(js_name = TDCountdown)]
@@ -8942,6 +9050,14 @@ wasm_candle_pattern!(WasmCrab, wc::Crab, Crab);
wasm_candle_pattern!(WasmShark, wc::Shark, Shark);
wasm_candle_pattern!(WasmCypher, wc::Cypher, Cypher);
wasm_candle_pattern!(WasmThreeDrives, wc::ThreeDrives, ThreeDrives);
wasm_candle_pattern!(WasmTdCamouflage, wc::TdCamouflage, TDCamouflage);
wasm_candle_pattern!(WasmTdClop, wc::TdClop, TDClop);
wasm_candle_pattern!(WasmTdClopwin, wc::TdClopwin, TDClopwin);
wasm_candle_pattern!(WasmTdPropulsion, wc::TdPropulsion, TDPropulsion);
wasm_candle_pattern!(WasmTdTrap, wc::TdTrap, TDTrap);
wasm_candle_pattern!(WasmTristar, wc::Tristar, Tristar);
wasm_candle_pattern!(WasmHaramiCross, wc::HaramiCross, HaramiCross);
wasm_candle_pattern!(WasmTowerTopBottom, wc::TowerTopBottom, TowerTopBottom);
// ============================== Microstructure: Order Book ==============================
//
@@ -9168,6 +9284,66 @@ impl WasmTradeImbalance {
}
}
// Trade-sign autocorrelation carries a `period` parameter, so it is hand-written.
#[wasm_bindgen(js_name = TradeSignAutocorrelation)]
pub struct WasmTradeSignAutocorrelation {
inner: wc::TradeSignAutocorrelation,
}
#[wasm_bindgen(js_class = TradeSignAutocorrelation)]
impl WasmTradeSignAutocorrelation {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmTradeSignAutocorrelation, JsError> {
Ok(Self {
inner: wc::TradeSignAutocorrelation::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result<Option<f64>, JsError> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// PIN carries a `window` parameter, so it is hand-written.
#[wasm_bindgen(js_name = Pin)]
pub struct WasmPin {
inner: wc::Pin,
}
#[wasm_bindgen(js_class = Pin)]
impl WasmPin {
#[wasm_bindgen(constructor)]
pub fn new(window: usize) -> Result<WasmPin, JsError> {
Ok(Self {
inner: wc::Pin::new(window).map_err(map_err)?,
})
}
pub fn update(&mut self, price: f64, size: f64, is_buy: bool) -> Result<Option<f64>, JsError> {
Ok(self.inner.update(build_trade(price, size, is_buy)?))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// Order Flow Imbalance: order-book input with a `period` parameter.
#[wasm_bindgen(js_name = OrderFlowImbalance)]
pub struct WasmOrderFlowImbalance {
@@ -10080,6 +10256,450 @@ impl WasmCalendarSpread {
}
}
// ---------- Heikin-Ashi Oscillator ----------
#[wasm_bindgen(js_name = HeikinAshiOscillator)]
pub struct WasmHeikinAshiOscillator {
inner: wc::HeikinAshiOscillator,
}
#[wasm_bindgen(js_class = HeikinAshiOscillator)]
impl WasmHeikinAshiOscillator {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmHeikinAshiOscillator, JsError> {
Ok(Self {
inner: wc::HeikinAshiOscillator::new(period).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> Result<Option<f64>, JsError> {
let c = make_candle_ohlc(open, high, low, close)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("open, high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(open.len());
for i in 0..open.len() {
let c = make_candle_ohlc(open[i], high[i], low[i], close[i])?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Three Line Break ----------
#[wasm_bindgen(js_name = ThreeLineBreak)]
pub struct WasmThreeLineBreak {
inner: wc::ThreeLineBreak,
}
#[wasm_bindgen(js_class = ThreeLineBreak)]
impl WasmThreeLineBreak {
#[wasm_bindgen(constructor)]
pub fn new(lines: usize) -> Result<WasmThreeLineBreak, JsError> {
Ok(Self {
inner: wc::ThreeLineBreak::new(lines).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Smoothed Heikin-Ashi ----------
#[wasm_bindgen(js_name = SmoothedHeikinAshi)]
pub struct WasmSmoothedHeikinAshi {
inner: wc::SmoothedHeikinAshi,
}
#[wasm_bindgen(js_class = SmoothedHeikinAshi)]
impl WasmSmoothedHeikinAshi {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmSmoothedHeikinAshi, JsError> {
Ok(Self {
inner: wc::SmoothedHeikinAshi::new(period).map_err(map_err)?,
})
}
pub fn update(
&mut self,
open: f64,
high: f64,
low: f64,
close: f64,
) -> Result<JsValue, JsError> {
let candle = make_candle_ohlc(open, high, low, close)?;
match self.inner.update(candle) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"open".into(), &o.open.into()).ok();
Reflect::set(&obj, &"high".into(), &o.high.into()).ok();
Reflect::set(&obj, &"low".into(), &o.low.into()).ok();
Reflect::set(&obj, &"close".into(), &o.close.into()).ok();
Ok(obj.into())
}
None => Ok(JsValue::NULL),
}
}
pub fn batch(
&mut self,
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if open.len() != high.len() || high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("open, high, low, close must be equal length"));
}
let n = open.len();
let mut out = vec![f64::NAN; n * 4];
for i in 0..n {
let candle = make_candle_ohlc(open[i], high[i], low[i], close[i])?;
if let Some(o) = self.inner.update(candle) {
out[i * 4] = o.open;
out[i * 4 + 1] = o.high;
out[i * 4 + 2] = o.low;
out[i * 4 + 3] = o.close;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Equivolume ----------
#[wasm_bindgen(js_name = Equivolume)]
pub struct WasmEquivolume {
inner: wc::Equivolume,
}
#[wasm_bindgen(js_class = Equivolume)]
impl WasmEquivolume {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmEquivolume, JsError> {
Ok(Self {
inner: wc::Equivolume::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, volume: f64) -> Result<JsValue, JsError> {
let candle = wc::Candle::new(low, high, low, low, volume, 0).map_err(map_err)?;
match self.inner.update(candle) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"height".into(), &o.height.into()).ok();
Reflect::set(&obj, &"width".into(), &o.width.into()).ok();
Ok(obj.into())
}
None => Ok(JsValue::NULL),
}
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
volume: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != volume.len() {
return Err(JsError::new("high, low, volume must be equal length"));
}
let n = high.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let candle =
wc::Candle::new(low[i], high[i], low[i], low[i], volume[i], 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.height;
out[i * 2 + 1] = o.width;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- CandleVolume ----------
#[wasm_bindgen(js_name = CandleVolume)]
pub struct WasmCandleVolume {
inner: wc::CandleVolume,
}
#[wasm_bindgen(js_class = CandleVolume)]
impl WasmCandleVolume {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmCandleVolume, JsError> {
Ok(Self {
inner: wc::CandleVolume::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, open: f64, close: f64, volume: f64) -> Result<JsValue, JsError> {
let high = open.max(close);
let low = open.min(close);
let candle = wc::Candle::new(open, high, low, close, volume, 0).map_err(map_err)?;
match self.inner.update(candle) {
Some(o) => {
let obj = Object::new();
Reflect::set(&obj, &"body".into(), &o.body.into()).ok();
Reflect::set(&obj, &"width".into(), &o.width.into()).ok();
Ok(obj.into())
}
None => Ok(JsValue::NULL),
}
}
pub fn batch(
&mut self,
open: &[f64],
close: &[f64],
volume: &[f64],
) -> Result<Float64Array, JsError> {
if open.len() != close.len() || close.len() != volume.len() {
return Err(JsError::new("open, close, volume must be equal length"));
}
let n = open.len();
let mut out = vec![f64::NAN; n * 2];
for i in 0..n {
let high = open[i].max(close[i]);
let low = open[i].min(close[i]);
let candle =
wc::Candle::new(open[i], high, low, close[i], volume[i], 0).map_err(map_err)?;
if let Some(o) = self.inner.update(candle) {
out[i * 2] = o.body;
out[i * 2 + 1] = o.width;
}
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Frying Pan Bottom ----------
#[wasm_bindgen(js_name = FryPanBottom)]
pub struct WasmFryPanBottom {
inner: wc::FryPanBottom,
}
#[wasm_bindgen(js_class = FryPanBottom)]
impl WasmFryPanBottom {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmFryPanBottom, JsError> {
Ok(Self {
inner: wc::FryPanBottom::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Dumpling Top ----------
#[wasm_bindgen(js_name = DumplingTop)]
pub struct WasmDumplingTop {
inner: wc::DumplingTop,
}
#[wasm_bindgen(js_class = DumplingTop)]
impl WasmDumplingTop {
#[wasm_bindgen(constructor)]
pub fn new(period: usize) -> Result<WasmDumplingTop, JsError> {
Ok(Self {
inner: wc::DumplingTop::new(period).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- New Price Lines ----------
#[wasm_bindgen(js_name = NewPriceLines)]
pub struct WasmNewPriceLines {
inner: wc::NewPriceLines,
}
#[wasm_bindgen(js_class = NewPriceLines)]
impl WasmNewPriceLines {
#[wasm_bindgen(constructor)]
pub fn new(count: usize) -> Result<WasmNewPriceLines, JsError> {
Ok(Self {
inner: wc::NewPriceLines::new(count).map_err(map_err)?,
})
}
pub fn update(&mut self, high: f64, low: f64, close: f64) -> Result<Option<f64>, JsError> {
let c = make_candle(high, low, close, 0.0)?;
Ok(self.inner.update(c))
}
pub fn batch(
&mut self,
high: &[f64],
low: &[f64],
close: &[f64],
) -> Result<Float64Array, JsError> {
if high.len() != low.len() || low.len() != close.len() {
return Err(JsError::new("high, low, close must be equal length"));
}
let mut out = Vec::with_capacity(high.len());
for i in 0..high.len() {
let c = make_candle(high[i], low[i], close[i], 0.0)?;
out.push(self.inner.update(c).unwrap_or(f64::NAN));
}
Ok(Float64Array::from(out.as_slice()))
}
pub fn reset(&mut self) {
self.inner.reset();
}
#[wasm_bindgen(js_name = isReady)]
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
#[wasm_bindgen(js_name = warmupPeriod)]
pub fn warmup_period(&self) -> usize {
self.inner.warmup_period()
}
}
// ---------- Market Breadth (CrossSection input) ----------
//
// A breadth tick is the per-symbol state of the whole universe, passed as four
@@ -0,0 +1,231 @@
#![allow(clippy::doc_markdown)]
//! CandleVolume — candlestick body with a volume-scaled width.
use crate::error::{Error, Result};
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Output of [`CandleVolume`]: the signed candle body and its volume-relative width.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CandleVolumeOutput {
/// Signed body `close open` (positive = bullish candle).
pub body: f64,
/// Box width — volume relative to its `period` average (`1.0` = average).
pub width: f64,
}
/// CandleVolume — the candlestick analogue of [`Equivolume`](crate::Equivolume):
/// each bar's **body** (`close open`) paired with a **width** proportional to its
/// volume relative to the recent average.
///
/// ```text
/// body = close open (signed; + bullish, bearish)
/// width = volume / SMA(volume, period) (1.0 = average volume)
/// ```
///
/// Where Equivolume uses the high-low *range* for the box height, CandleVolume uses
/// the candlestick *body*, preserving direction: a wide bullish body (long up
/// candle on heavy volume) is strong demand, a wide bearish body strong supply, and
/// a narrow body on heavy volume (wide but short) is churn. The signed body plus
/// the normalised width capture both the move's direction and the participation
/// behind it.
///
/// The first value lands after `period` inputs (to seed the volume average). Each
/// `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, CandleVolume};
///
/// let mut indicator = CandleVolume::new(14).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let c = Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1_000.0 + f64::from(i), 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct CandleVolume {
period: usize,
vol_sma: Sma,
last: Option<CandleVolumeOutput>,
}
impl CandleVolume {
/// Construct a CandleVolume with the given volume-averaging `period`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
vol_sma: Sma::new(period)?,
last: None,
})
}
/// Configured volume-averaging period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<CandleVolumeOutput> {
self.last
}
}
impl Indicator for CandleVolume {
type Input = Candle;
type Output = CandleVolumeOutput;
fn update(&mut self, candle: Candle) -> Option<CandleVolumeOutput> {
let avg_vol = self.vol_sma.update(candle.volume)?;
let body = candle.close - candle.open;
let width = if avg_vol > 0.0 {
candle.volume / avg_vol
} else {
0.0
};
let out = CandleVolumeOutput { body, width };
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.vol_sma.reset();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"CandleVolume"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(open: f64, close: f64, volume: f64) -> Candle {
let high = open.max(close) + 1.0;
let low = open.min(close) - 1.0;
Candle::new_unchecked(open, high, low, close, volume, 0)
}
#[test]
fn rejects_zero_period() {
assert!(matches!(CandleVolume::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let cv = CandleVolume::new(14).unwrap();
assert_eq!(cv.period(), 14);
assert_eq!(cv.warmup_period(), 14);
assert_eq!(cv.name(), "CandleVolume");
assert!(!cv.is_ready());
assert_eq!(cv.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut cv = CandleVolume::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|_| c(100.0, 101.0, 1_000.0)).collect();
let out = cv.batch(&candles);
for v in out.iter().take(2) {
assert!(v.is_none());
}
assert!(out[2].is_some());
}
#[test]
fn bullish_body_positive() {
let mut cv = CandleVolume::new(2).unwrap();
let out = cv
.batch(&[c(100.0, 103.0, 1_000.0), c(100.0, 103.0, 1_000.0)])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(out.body, 3.0, epsilon = 1e-9);
}
#[test]
fn bearish_body_negative() {
let mut cv = CandleVolume::new(2).unwrap();
let out = cv
.batch(&[c(103.0, 100.0, 1_000.0), c(103.0, 100.0, 1_000.0)])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(out.body, -3.0, epsilon = 1e-9);
}
#[test]
fn heavy_bar_is_wide() {
let mut cv = CandleVolume::new(3).unwrap();
let candles = [
c(100.0, 101.0, 1_000.0),
c(100.0, 101.0, 1_000.0),
c(100.0, 101.0, 4_000.0),
];
let out = cv.batch(&candles).into_iter().flatten().last().unwrap();
assert!(out.width > 1.0);
}
#[test]
fn reset_clears_state() {
let mut cv = CandleVolume::new(3).unwrap();
cv.batch(&[c(100.0, 101.0, 1_000.0); 6]);
assert!(cv.is_ready());
cv.reset();
assert!(!cv.is_ready());
assert_eq!(cv.value(), None);
assert_eq!(cv.update(c(100.0, 101.0, 1_000.0)), None);
}
#[test]
fn zero_volume_gives_zero_width() {
let mut cv = CandleVolume::new(2).unwrap();
let out = cv
.batch(&[c(10.0, 11.0, 0.0), c(11.0, 12.0, 0.0), c(12.0, 13.0, 0.0)])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(out.width, 0.0);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.25).sin() * 5.0;
c(b, b + 0.5, 1_000.0 + f64::from(i))
})
.collect();
let batch = CandleVolume::new(14).unwrap().batch(&candles);
let mut b = CandleVolume::new(14).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,215 @@
//! Dumpling Top — a rounded top (dome) confirmed by a breakdown.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Dumpling Top — the bearish mirror of the [`FryPanBottom`](crate::FryPanBottom):
/// a gently rounded **top** (dome) across the window, confirmed by a close back
/// below where it started.
///
/// ```text
/// over the last `period` closes:
/// the maximum close sits in the middle third of the window (the "dome")
/// the latest close is below the first close (the breakdown)
/// signal = 1 when both hold, else 0
/// ```
///
/// The dumpling top is a distribution pattern: price rounds over at the top as
/// buying fades, then rolls down through the level it rose from. Detection requires
/// a *central* high (a symmetric dome, not a one-sided spike) and a close below the
/// window's opening level. The output is `1.0` (pattern) or `0.0`.
///
/// The first value lands after `period` inputs; each `update` scans the window in
/// O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, DumplingTop};
///
/// let mut indicator = DumplingTop::new(9).unwrap();
/// let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
/// let mut last = None;
/// for &cl in &closes {
/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert_eq!(last, Some(-1.0));
/// ```
#[derive(Debug, Clone)]
pub struct DumplingTop {
period: usize,
closes: VecDeque<f64>,
last: Option<f64>,
}
impl DumplingTop {
/// Construct a Dumpling Top over `period` bars.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 5`.
pub fn new(period: usize) -> Result<Self> {
if period < 5 {
return Err(Error::InvalidPeriod {
message: "dumpling top needs period >= 5",
});
}
Ok(Self {
period,
closes: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for DumplingTop {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
if self.closes.len() == self.period {
self.closes.pop_front();
}
self.closes.push_back(candle.close);
if self.closes.len() < self.period {
return None;
}
let first = *self.closes.front().expect("non-empty");
let last = *self.closes.back().expect("non-empty");
let mut max_idx = 0;
let mut max_val = f64::NEG_INFINITY;
for (i, &v) in self.closes.iter().enumerate() {
if v > max_val {
max_val = v;
max_idx = i;
}
}
let lo = self.period / 4;
let hi = self.period - self.period / 4;
let dome = max_idx >= lo && max_idx < hi;
let broke_down = last < first && last < max_val;
let v = if dome && broke_down { -1.0 } else { 0.0 };
self.last = Some(v);
Some(v)
}
fn reset(&mut self) {
self.closes.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"DumplingTop"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(close: f64) -> Candle {
Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
}
#[test]
fn rejects_small_period() {
assert!(matches!(
DumplingTop::new(4),
Err(Error::InvalidPeriod { .. })
));
assert!(DumplingTop::new(5).is_ok());
}
#[test]
fn accessors_and_metadata() {
let d = DumplingTop::new(9).unwrap();
assert_eq!(d.period(), 9);
assert_eq!(d.warmup_period(), 9);
assert_eq!(d.name(), "DumplingTop");
assert!(!d.is_ready());
assert_eq!(d.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut d = DumplingTop::new(5).unwrap();
let out = d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0), c(98.0)]);
for v in out.iter().take(4) {
assert!(v.is_none());
}
assert!(out[4].is_some());
}
#[test]
fn rounded_top_then_breakdown_signals() {
let mut d = DumplingTop::new(9).unwrap();
let closes = [100.0, 102.0, 104.0, 105.0, 104.0, 102.0, 99.0, 97.0, 95.0];
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, -1.0);
}
#[test]
fn one_sided_rise_is_zero() {
let mut d = DumplingTop::new(9).unwrap();
let candles: Vec<Candle> = (0..9).map(|i| c(100.0 + f64::from(i))).collect();
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 0.0);
}
#[test]
fn no_breakdown_is_zero() {
let mut d = DumplingTop::new(9).unwrap();
let closes = [
100.0, 102.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5,
];
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
let last = d.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 0.0);
}
#[test]
fn reset_clears_state() {
let mut d = DumplingTop::new(5).unwrap();
d.batch(&[c(100.0), c(101.0), c(102.0), c(101.0), c(99.0)]);
assert!(d.is_ready());
d.reset();
assert!(!d.is_ready());
assert_eq!(d.value(), None);
assert_eq!(d.update(c(100.0)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..60)
.map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
.collect();
let batch = DumplingTop::new(9).unwrap().batch(&candles);
let mut b = DumplingTop::new(9).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,235 @@
//! Equivolume — the price box height and its volume-scaled width.
use crate::error::{Error, Result};
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Output of [`Equivolume`]: the box's price height and its volume-relative width.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EquivolumeOutput {
/// Box height — the bar's price range `high low`.
pub height: f64,
/// Box width — volume relative to its `period` average (`1.0` = average).
pub width: f64,
}
/// Equivolume — Richard Arms' charting style rendered as numbers: each bar is a
/// "box" whose **height** is its price range and whose **width** is its volume
/// relative to the recent average.
///
/// ```text
/// height = high low
/// width = volume / SMA(volume, period) (1.0 = average volume)
/// ```
///
/// Equivolume discards time and substitutes volume for the horizontal axis: a tall
/// narrow box is an easy move (big range on light volume), while a short wide box
/// is churn (small range on heavy volume) that often marks support/resistance.
/// Reporting the two dimensions lets you reconstruct that shape programmatically:
/// the height/width relationship is Arms' "ease of movement" read. The width is
/// normalised by the volume SMA so it self-scales across instruments.
///
/// The first value lands after `period` inputs (to seed the volume average). Each
/// `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, Equivolume};
///
/// let mut indicator = Equivolume::new(14).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let c = Candle::new(base, base + 2.0, base - 2.0, base, 1_000.0 + f64::from(i), 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct Equivolume {
period: usize,
vol_sma: Sma,
last: Option<EquivolumeOutput>,
}
impl Equivolume {
/// Construct an Equivolume with the given volume-averaging `period`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
vol_sma: Sma::new(period)?,
last: None,
})
}
/// Configured volume-averaging period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<EquivolumeOutput> {
self.last
}
}
impl Indicator for Equivolume {
type Input = Candle;
type Output = EquivolumeOutput;
fn update(&mut self, candle: Candle) -> Option<EquivolumeOutput> {
let avg_vol = self.vol_sma.update(candle.volume)?;
let height = candle.high - candle.low;
let width = if avg_vol > 0.0 {
candle.volume / avg_vol
} else {
0.0
};
let out = EquivolumeOutput { height, width };
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.vol_sma.reset();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"Equivolume"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(high: f64, low: f64, volume: f64) -> Candle {
Candle::new_unchecked(low, high, low, f64::midpoint(high, low), volume, 0)
}
#[test]
fn rejects_zero_period() {
assert!(matches!(Equivolume::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let e = Equivolume::new(14).unwrap();
assert_eq!(e.period(), 14);
assert_eq!(e.warmup_period(), 14);
assert_eq!(e.name(), "Equivolume");
assert!(!e.is_ready());
assert_eq!(e.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut e = Equivolume::new(3).unwrap();
let candles: Vec<Candle> = (0..6).map(|_| c(102.0, 98.0, 1_000.0)).collect();
let out = e.batch(&candles);
for v in out.iter().take(2) {
assert!(v.is_none());
}
assert!(out[2].is_some());
}
#[test]
fn height_is_range() {
let mut e = Equivolume::new(2).unwrap();
let out = e
.batch(&[c(105.0, 100.0, 1_000.0), c(105.0, 100.0, 1_000.0)])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(out.height, 5.0, epsilon = 1e-9);
}
#[test]
fn average_volume_width_is_one() {
let mut e = Equivolume::new(3).unwrap();
let out = e
.batch(&[c(102.0, 98.0, 1_000.0); 6])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(out.width, 1.0, epsilon = 1e-9);
}
#[test]
fn heavy_bar_is_wide() {
let mut e = Equivolume::new(3).unwrap();
let candles = [
c(102.0, 98.0, 1_000.0),
c(102.0, 98.0, 1_000.0),
c(102.0, 98.0, 4_000.0),
];
let out = e.batch(&candles).into_iter().flatten().last().unwrap();
assert!(
out.width > 1.0,
"a heavy bar should be wider than average, got {}",
out.width
);
}
#[test]
fn reset_clears_state() {
let mut e = Equivolume::new(3).unwrap();
e.batch(&[c(102.0, 98.0, 1_000.0); 6]);
assert!(e.is_ready());
e.reset();
assert!(!e.is_ready());
assert_eq!(e.value(), None);
assert_eq!(e.update(c(102.0, 98.0, 1_000.0)), None);
}
#[test]
fn zero_volume_gives_zero_width() {
let mut e = Equivolume::new(2).unwrap();
let out = e
.batch(&[c(11.0, 9.0, 0.0), c(12.0, 10.0, 0.0), c(13.0, 11.0, 0.0)])
.into_iter()
.flatten()
.last()
.unwrap();
assert_eq!(out.width, 0.0);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| {
c(
110.0 + (f64::from(i) * 0.25).sin() * 5.0,
90.0,
1_000.0 + f64::from(i),
)
})
.collect();
let batch = Equivolume::new(14).unwrap().batch(&candles);
let mut b = Equivolume::new(14).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,218 @@
//! Frying Pan Bottom — a rounded bottom (U) confirmed by recovery.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Frying Pan Bottom — a gently rounded bottom across the lookback window: prices
/// decline, flatten near the centre, then recover above where they started.
///
/// ```text
/// over the last `period` closes:
/// the minimum close sits in the middle third of the window (the "bowl")
/// the latest close is above the first close (the rim is recovered)
/// signal = +1 when both hold, else 0
/// ```
///
/// The frying pan is a bullish accumulation pattern: a saucer-shaped base where
/// selling dries up, the curve flattens, and price lifts off the rim. Detecting it
/// requires the low point to be central (a symmetric bowl, not a one-sided drop)
/// and the close to have climbed back above the window's opening level, confirming
/// the breakout from the base. The output is `+1.0` (pattern) or `0.0`.
///
/// The first value lands after `period` inputs; each `update` scans the window in
/// O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, FryPanBottom};
///
/// let mut indicator = FryPanBottom::new(9).unwrap();
/// // A U-shaped base then recovery.
/// let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
/// let mut last = None;
/// for &cl in &closes {
/// let c = Candle::new(cl, cl + 0.5, cl - 0.5, cl, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert_eq!(last, Some(1.0));
/// ```
#[derive(Debug, Clone)]
pub struct FryPanBottom {
period: usize,
closes: VecDeque<f64>,
last: Option<f64>,
}
impl FryPanBottom {
/// Construct a Frying Pan Bottom over `period` bars.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 5` (a bowl needs room for a
/// central low between recovering sides).
pub fn new(period: usize) -> Result<Self> {
if period < 5 {
return Err(Error::InvalidPeriod {
message: "frying pan bottom needs period >= 5",
});
}
Ok(Self {
period,
closes: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for FryPanBottom {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
if self.closes.len() == self.period {
self.closes.pop_front();
}
self.closes.push_back(candle.close);
if self.closes.len() < self.period {
return None;
}
let first = *self.closes.front().expect("non-empty");
let last = *self.closes.back().expect("non-empty");
// Index of the minimum close.
let mut min_idx = 0;
let mut min_val = f64::INFINITY;
for (i, &v) in self.closes.iter().enumerate() {
if v < min_val {
min_val = v;
min_idx = i;
}
}
let lo = self.period / 4;
let hi = self.period - self.period / 4;
let bowl = min_idx >= lo && min_idx < hi;
let recovered = last > first && last > min_val;
let v = if bowl && recovered { 1.0 } else { 0.0 };
self.last = Some(v);
Some(v)
}
fn reset(&mut self) {
self.closes.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"FryPanBottom"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(close: f64) -> Candle {
Candle::new_unchecked(close, close + 0.5, close - 0.5, close, 1_000.0, 0)
}
#[test]
fn rejects_small_period() {
assert!(matches!(
FryPanBottom::new(4),
Err(Error::InvalidPeriod { .. })
));
assert!(FryPanBottom::new(5).is_ok());
}
#[test]
fn accessors_and_metadata() {
let f = FryPanBottom::new(9).unwrap();
assert_eq!(f.period(), 9);
assert_eq!(f.warmup_period(), 9);
assert_eq!(f.name(), "FryPanBottom");
assert!(!f.is_ready());
assert_eq!(f.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut f = FryPanBottom::new(5).unwrap();
let out = f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0), c(102.0)]);
for v in out.iter().take(4) {
assert!(v.is_none());
}
assert!(out[4].is_some());
}
#[test]
fn rounded_bottom_then_recovery_signals() {
let mut f = FryPanBottom::new(9).unwrap();
let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 98.0, 101.0, 103.0, 105.0];
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 1.0);
}
#[test]
fn one_sided_drop_is_zero() {
// A straight decline (min at the end) is not a bowl.
let mut f = FryPanBottom::new(9).unwrap();
let candles: Vec<Candle> = (0..9).map(|i| c(100.0 - f64::from(i))).collect();
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 0.0);
}
#[test]
fn no_recovery_is_zero() {
// Bowl shape but the last close never climbs above the first.
let mut f = FryPanBottom::new(9).unwrap();
let closes = [100.0, 98.0, 96.0, 95.0, 96.0, 97.0, 98.0, 99.0, 99.5];
let candles: Vec<Candle> = closes.iter().map(|&x| c(x)).collect();
let last = f.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 0.0);
}
#[test]
fn reset_clears_state() {
let mut f = FryPanBottom::new(5).unwrap();
f.batch(&[c(100.0), c(99.0), c(98.0), c(99.0), c(101.0)]);
assert!(f.is_ready());
f.reset();
assert!(!f.is_ready());
assert_eq!(f.value(), None);
assert_eq!(f.update(c(100.0)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..60)
.map(|i| c(100.0 + (f64::from(i) * 0.3).sin() * 5.0))
.collect();
let batch = FryPanBottom::new(9).unwrap().batch(&candles);
let mut b = FryPanBottom::new(9).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,193 @@
#![allow(clippy::doc_markdown)]
//! Harami Cross — a Harami whose second candle is a Doji.
//!
//! A Harami Cross is a stronger Harami: a large real body followed by a Doji whose
//! body sits *within* the prior body. The Doji's total indecision after a strong
//! move makes the reversal signal more potent than a plain Harami.
//!
//! - **Bullish** (`+1.0`): the prior candle is a large **bearish** body
//! (`close < open`) and the current candle is a Doji whose open and close lie
//! within the prior body.
//! - **Bearish** (`-1.0`): the prior candle is a large **bullish** body and the
//! current is a contained Doji.
//! - Otherwise the output is `0.0`.
//!
//! A doji is a candle whose body is `<= 0.1 * range`. The two-bar lookback means
//! the first value lands on the second candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
fn is_doji(candle: Candle) -> bool {
let body = (candle.close - candle.open).abs();
let range = candle.high - candle.low;
range > 0.0 && body <= 0.1 * range
}
/// Harami Cross — large-body-then-contained-doji reversal detector.
#[derive(Debug, Clone, Default)]
pub struct HaramiCross {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl HaramiCross {
/// Construct a new `HaramiCross`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for HaramiCross {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let prev_body_low = prev.open.min(prev.close);
let prev_body_high = prev.open.max(prev.close);
let prev_is_solid = !is_doji(prev);
let curr_is_doji = is_doji(candle);
let contained = candle.open >= prev_body_low
&& candle.open <= prev_body_high
&& candle.close >= prev_body_low
&& candle.close <= prev_body_high;
let v = if prev_is_solid && curr_is_doji && contained {
if prev.close < prev.open {
1.0
} else {
-1.0
}
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"HaramiCross"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn solid(open: f64, close: f64) -> Candle {
Candle::new_unchecked(
open,
open.max(close) + 0.2,
open.min(close) - 0.2,
close,
0.0,
0,
)
}
fn doji(mid: f64) -> Candle {
Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let h = HaramiCross::new();
assert_eq!(h.warmup_period(), 2);
assert_eq!(h.name(), "HaramiCross");
assert!(!h.is_ready());
assert_eq!(h.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut h = HaramiCross::new();
assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
assert!(h.update(doji(105.0)).is_some());
}
#[test]
fn bullish_harami_cross() {
// prior big bearish body [100, 110]; doji centred at 105 inside it -> +1.
let mut h = HaramiCross::new();
h.update(solid(110.0, 100.0));
assert_eq!(h.update(doji(105.0)), Some(1.0));
}
#[test]
fn bearish_harami_cross() {
// prior big bullish body [100, 110]; doji inside -> -1.
let mut h = HaramiCross::new();
h.update(solid(100.0, 110.0));
assert_eq!(h.update(doji(105.0)), Some(-1.0));
}
#[test]
fn doji_outside_body_is_zero() {
let mut h = HaramiCross::new();
h.update(solid(110.0, 100.0));
// doji centred at 120, outside the prior body -> 0.
assert_eq!(h.update(doji(120.0)), Some(0.0));
}
#[test]
fn non_doji_second_is_zero() {
let mut h = HaramiCross::new();
h.update(solid(110.0, 100.0));
assert_eq!(h.update(solid(104.0, 106.0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut h = HaramiCross::new();
h.update(solid(110.0, 100.0));
h.update(doji(105.0));
assert!(h.is_ready());
h.reset();
assert!(!h.is_ready());
assert_eq!(h.update(solid(110.0, 100.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
if i % 2 == 0 {
solid(110.0, 100.0)
} else {
doji(105.0)
}
})
.collect();
let batch = HaramiCross::new().batch(&candles);
let mut b = HaramiCross::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,251 @@
//! Hasbrouck Information Share — each venue's contribution to price discovery.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::traits::Indicator;
/// Hasbrouck Information Share — the share of price-discovery attributable to the
/// **first** of two synchronised price series (e.g. the same asset on two venues).
///
/// ```text
/// rx_t = x_t x_{t1}, ry_t = y_t y_{t1} (one-step price changes)
/// IS_x = var(rx) / ( var(rx) + var(ry) ) over the window, ∈ [0, 1]
/// ```
///
/// When the same instrument trades on several venues, Joel Hasbrouck's information
/// share measures how much each venue contributes to the common efficient price.
/// The venue whose innovations carry more of the variance leads price discovery.
/// This streaming form uses the **variance-ratio proxy**: the fraction of total
/// return variance contributed by series `x`. A reading above `0.5` means venue
/// `x` is the price leader; below `0.5`, the follower. (The full Hasbrouck measure
/// estimates a vector error-correction model and reports an upper/lower bound from
/// the Cholesky ordering; this proxy captures the leading idea without the VECM.)
///
/// The output is in `[0, 1]`; if both series are flat it reports the neutral `0.5`.
/// The first value lands after `period + 1` inputs. Each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, HasbrouckInformationShare};
///
/// let mut indicator = HasbrouckInformationShare::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// // Venue x moves a lot, venue y barely moves -> x leads.
/// let x = (f64::from(i) * 0.5).sin() * 10.0;
/// let y = (f64::from(i) * 0.5).sin() * 1.0;
/// last = indicator.update((x, y));
/// }
/// assert!(last.unwrap() > 0.8);
/// ```
#[derive(Debug, Clone)]
pub struct HasbrouckInformationShare {
period: usize,
prev: Option<(f64, f64)>,
window: VecDeque<(f64, f64)>,
sum_x: f64,
sum_y: f64,
sum_xx: f64,
sum_yy: f64,
}
impl HasbrouckInformationShare {
/// Construct a Hasbrouck information share over `period` return pairs.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2` (variance needs two
/// returns).
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "information share needs period >= 2",
});
}
Ok(Self {
period,
prev: None,
window: VecDeque::with_capacity(period),
sum_x: 0.0,
sum_y: 0.0,
sum_xx: 0.0,
sum_yy: 0.0,
})
}
/// Configured window of return pairs.
pub const fn period(&self) -> usize {
self.period
}
}
impl Indicator for HasbrouckInformationShare {
type Input = (f64, f64);
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (x, y) = input;
let Some((px, py)) = self.prev else {
self.prev = Some((x, y));
return None;
};
self.prev = Some((x, y));
let (rx, ry) = (x - px, y - py);
if self.window.len() == self.period {
let (ox, oy) = self.window.pop_front().expect("non-empty");
self.sum_x -= ox;
self.sum_y -= oy;
self.sum_xx -= ox * ox;
self.sum_yy -= oy * oy;
}
self.window.push_back((rx, ry));
self.sum_x += rx;
self.sum_y += ry;
self.sum_xx += rx * rx;
self.sum_yy += ry * ry;
if self.window.len() < self.period {
return None;
}
let n = self.period as f64;
let var_x = (self.sum_xx / n - (self.sum_x / n).powi(2)).max(0.0);
let var_y = (self.sum_yy / n - (self.sum_y / n).powi(2)).max(0.0);
let total = var_x + var_y;
Some(if total > 0.0 { var_x / total } else { 0.5 })
}
fn reset(&mut self) {
self.prev = None;
self.window.clear();
self.sum_x = 0.0;
self.sum_y = 0.0;
self.sum_xx = 0.0;
self.sum_yy = 0.0;
}
fn warmup_period(&self) -> usize {
self.period + 1
}
fn is_ready(&self) -> bool {
self.window.len() == self.period
}
fn name(&self) -> &'static str {
"HasbrouckInformationShare"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
#[test]
fn rejects_period_below_two() {
assert!(matches!(
HasbrouckInformationShare::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(HasbrouckInformationShare::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let h = HasbrouckInformationShare::new(20).unwrap();
assert_eq!(h.period(), 20);
assert_eq!(h.warmup_period(), 21);
assert_eq!(h.name(), "HasbrouckInformationShare");
assert!(!h.is_ready());
}
#[test]
fn warmup_needs_period_plus_one() {
let mut h = HasbrouckInformationShare::new(3).unwrap();
assert_eq!(h.update((1.0, 1.0)), None);
assert_eq!(h.update((2.0, 2.0)), None);
assert_eq!(h.update((3.0, 2.5)), None);
assert!(h.update((4.0, 3.0)).is_some());
}
#[test]
fn loud_venue_leads() {
// x is far more volatile than y -> x holds nearly all the share.
let pairs: Vec<(f64, f64)> = (0..40)
.map(|i| {
(
(f64::from(i) * 0.5).sin() * 10.0,
(f64::from(i) * 0.5).sin() * 1.0,
)
})
.collect();
let last = HasbrouckInformationShare::new(20)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
.last()
.unwrap();
assert!(last > 0.8, "the loud venue should lead, got {last}");
}
#[test]
fn equal_venues_split_evenly() {
// Independent but equal-variance moves -> share near 0.5.
let pairs: Vec<(f64, f64)> = (0..200)
.map(|i| {
(
(f64::from(i) * 0.5).sin() * 5.0,
(f64::from(i) * 0.5).cos() * 5.0,
)
})
.collect();
for v in HasbrouckInformationShare::new(40)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
{
assert!((0.0..=1.0).contains(&v));
}
}
#[test]
fn flat_series_is_half() {
let pairs: Vec<(f64, f64)> = (0..20).map(|_| (7.0, 9.0)).collect();
let last = HasbrouckInformationShare::new(5)
.unwrap()
.batch(&pairs)
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.5, epsilon = 1e-12);
}
#[test]
fn reset_clears_state() {
let mut h = HasbrouckInformationShare::new(4).unwrap();
h.batch(&[(1.0, 1.0), (2.0, 2.0), (3.0, 3.0), (4.0, 4.0), (5.0, 5.0)]);
assert!(h.is_ready());
h.reset();
assert!(!h.is_ready());
assert_eq!(h.update((1.0, 1.0)), None);
}
#[test]
fn batch_equals_streaming() {
let pairs: Vec<(f64, f64)> = (0..120)
.map(|i| {
let t = f64::from(i);
(t.sin() * 5.0, (t * 0.5).cos() * 3.0)
})
.collect();
let batch = HasbrouckInformationShare::new(20).unwrap().batch(&pairs);
let mut h = HasbrouckInformationShare::new(20).unwrap();
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,231 @@
//! Heikin-Ashi Oscillator — the (smoothed) Heikin-Ashi candle body as a zero-line oscillator.
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::indicators::heikin_ashi::HeikinAshi;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Heikin-Ashi Oscillator — the body of the [`HeikinAshi`](crate::HeikinAshi)
/// candle (`ha_close ha_open`), optionally EMA-smoothed, as an oscillator around
/// zero.
///
/// ```text
/// body = ha_close ha_open
/// HAO = EMA(body, period)
/// ```
///
/// A Heikin-Ashi candle is bullish when its close is above its open and bearish
/// when below; the size of that body measures conviction. Plotting the body as an
/// oscillator turns the visual HA colour/strength into a number: positive =
/// bullish HA candles, negative = bearish, and the magnitude is trend strength.
/// Smoothing the body with an EMA (`period`) damps single-bar noise so zero-line
/// crosses mark cleaner trend changes. With `period == 1` the oscillator is the raw
/// HA body.
///
/// The output is centred on zero (price units). The first value lands after
/// `period` inputs (the HA transform itself needs only one). Each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, HeikinAshiOscillator};
///
/// let mut indicator = HeikinAshiOscillator::new(5).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let c = Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct HeikinAshiOscillator {
period: usize,
ha: HeikinAshi,
ema: Ema,
last: Option<f64>,
}
impl HeikinAshiOscillator {
/// Construct a Heikin-Ashi Oscillator with the given EMA smoothing `period`
/// (use `1` for the raw body).
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
ha: HeikinAshi::new(),
ema: Ema::new(period)?,
last: None,
})
}
/// Configured smoothing period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for HeikinAshiOscillator {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let ha = self.ha.update(candle).expect("HeikinAshi emits every bar");
let body = ha.close - ha.open;
let v = self.ema.update(body)?;
self.last = Some(v);
Some(v)
}
fn reset(&mut self) {
self.ha.reset();
self.ema.reset();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"HeikinAshiOscillator"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
Candle::new_unchecked(open, high, low, close, 1_000.0, 0)
}
#[test]
fn rejects_zero_period() {
assert!(matches!(
HeikinAshiOscillator::new(0),
Err(Error::PeriodZero)
));
}
#[test]
fn accessors_and_metadata() {
let h = HeikinAshiOscillator::new(5).unwrap();
assert_eq!(h.period(), 5);
assert_eq!(h.warmup_period(), 5);
assert_eq!(h.name(), "HeikinAshiOscillator");
assert!(!h.is_ready());
assert_eq!(h.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut h = HeikinAshiOscillator::new(3).unwrap();
let candles: Vec<Candle> = (0..6)
.map(|i| {
let b = 100.0 + f64::from(i);
c(b, b + 1.0, b - 1.0, b + 0.5)
})
.collect();
let out = h.batch(&candles);
for v in out.iter().take(2) {
assert!(v.is_none());
}
assert!(out[2].is_some());
}
#[test]
fn uptrend_is_positive() {
let mut h = HeikinAshiOscillator::new(3).unwrap();
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + 2.0 * f64::from(i);
c(b, b + 1.0, b - 1.0, b + 1.5)
})
.collect();
let last = h.batch(&candles).into_iter().flatten().last().unwrap();
assert!(
last > 0.0,
"uptrend should give a positive HA body, got {last}"
);
}
#[test]
fn downtrend_is_negative() {
let mut h = HeikinAshiOscillator::new(3).unwrap();
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 200.0 - 2.0 * f64::from(i);
c(b, b + 1.0, b - 1.0, b - 1.5)
})
.collect();
let last = h.batch(&candles).into_iter().flatten().last().unwrap();
assert!(
last < 0.0,
"downtrend should give a negative HA body, got {last}"
);
}
#[test]
fn flat_market_near_zero() {
let mut h = HeikinAshiOscillator::new(3).unwrap();
let last = h
.batch(&[c(100.0, 100.5, 99.5, 100.0); 30])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-9);
}
#[test]
fn reset_clears_state() {
let mut h = HeikinAshiOscillator::new(3).unwrap();
h.batch(
&(0..10)
.map(|i| {
let b = 100.0 + f64::from(i);
c(b, b + 1.0, b - 1.0, b)
})
.collect::<Vec<_>>(),
);
assert!(h.is_ready());
h.reset();
assert!(!h.is_ready());
assert_eq!(h.value(), None);
assert_eq!(h.update(c(100.0, 101.0, 99.0, 100.0)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
c(b, b + 1.0, b - 1.0, b + 0.3)
})
.collect();
let batch = HeikinAshiOscillator::new(5).unwrap().batch(&candles);
let mut b = HeikinAshiOscillator::new(5).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
+71 -2
View File
@@ -67,6 +67,7 @@ mod butterfly;
mod calendar_spread;
mod calmar_ratio;
mod camarilla_pivots;
mod candle_volume;
mod cci;
mod center_of_gravity;
mod central_pivot_range;
@@ -116,6 +117,7 @@ mod downside_gap_three_methods;
mod dpo;
mod dragonfly_doji;
mod drawdown_duration;
mod dumpling_top;
mod dx;
mod dynamic_momentum_index;
mod ease_of_movement;
@@ -128,6 +130,7 @@ mod elder_safezone;
mod ema;
mod empirical_mode_decomposition;
mod engulfing;
mod equivolume;
mod even_better_sinewave;
mod evening_doji_star;
mod evwma;
@@ -151,6 +154,7 @@ mod footprint;
mod force_index;
mod fractal_chaos_bands;
mod frama;
mod fry_pan_bottom;
mod funding_basis;
mod funding_rate;
mod funding_rate_mean;
@@ -169,8 +173,11 @@ mod gravestone_doji;
mod hammer;
mod hanging_man;
mod harami;
mod harami_cross;
mod hasbrouck_information_share;
mod head_and_shoulders;
mod heikin_ashi;
mod heikin_ashi_oscillator;
mod high_low_index;
mod high_low_range;
mod high_wave;
@@ -262,6 +269,7 @@ mod morning_evening_star;
mod murrey_math_lines;
mod natr;
mod new_highs_new_lows;
mod new_price_lines;
mod nrtr;
mod nvi;
mod ob_imbalance_full;
@@ -289,6 +297,7 @@ mod percent_b;
mod percentage_trailing_stop;
mod pgo;
mod piercing_dark_cloud;
mod pin;
mod pivot_reversal;
mod plus_di;
mod plus_dm;
@@ -356,6 +365,7 @@ mod skewness;
mod sma;
mod smi;
mod smma;
mod smoothed_heikin_ashi;
mod sortino_ratio;
mod spearman_correlation;
mod spinning_top;
@@ -379,22 +389,30 @@ mod t3;
mod taker_buy_sell_ratio;
mod takuri;
mod tasuki_gap;
mod td_camouflage;
mod td_clop;
mod td_clopwin;
mod td_combo;
mod td_countdown;
mod td_demarker;
mod td_differential;
mod td_dwave;
mod td_lines;
mod td_moving_average;
mod td_open;
mod td_pressure;
mod td_propulsion;
mod td_range_projection;
mod td_rei;
mod td_risk_level;
mod td_sequential;
mod td_setup;
mod td_trap;
mod tema;
mod term_structure_basis;
mod three_drives;
mod three_inside;
mod three_line_break;
mod three_line_strike;
mod three_outside;
mod three_soldiers_or_crows;
@@ -404,8 +422,10 @@ mod tick_index;
mod tii;
mod time_based_stop;
mod time_of_day_return_profile;
mod tower_top_bottom;
mod tpo_profile;
mod trade_imbalance;
mod trade_sign_autocorrelation;
mod trade_volume_index;
mod trend_label;
mod trend_strength_index;
@@ -415,6 +435,7 @@ mod triangle;
mod trima;
mod trin;
mod triple_top_bottom;
mod tristar;
mod trix;
mod true_range;
mod tsf;
@@ -534,6 +555,7 @@ pub use butterfly::Butterfly;
pub use calendar_spread::CalendarSpread;
pub use calmar_ratio::CalmarRatio;
pub use camarilla_pivots::{Camarilla, CamarillaPivotsOutput};
pub use candle_volume::{CandleVolume, CandleVolumeOutput};
pub use cci::Cci;
pub use center_of_gravity::CenterOfGravity;
pub use central_pivot_range::{CentralPivotRange, CentralPivotRangeOutput};
@@ -583,6 +605,7 @@ pub use downside_gap_three_methods::DownsideGapThreeMethods;
pub use dpo::Dpo;
pub use dragonfly_doji::DragonflyDoji;
pub use drawdown_duration::DrawdownDuration;
pub use dumpling_top::DumplingTop;
pub use dx::Dx;
pub use dynamic_momentum_index::DynamicMomentumIndex;
pub use ease_of_movement::EaseOfMovement;
@@ -595,6 +618,7 @@ pub use elder_safezone::{ElderSafeZone, ElderSafeZoneOutput};
pub use ema::Ema;
pub use empirical_mode_decomposition::EmpiricalModeDecomposition;
pub use engulfing::Engulfing;
pub use equivolume::{Equivolume, EquivolumeOutput};
pub use even_better_sinewave::EvenBetterSinewave;
pub use evening_doji_star::EveningDojiStar;
pub use evwma::Evwma;
@@ -618,6 +642,7 @@ pub use footprint::{Footprint, FootprintLevel, FootprintOutput};
pub use force_index::ForceIndex;
pub use fractal_chaos_bands::{FractalChaosBands, FractalChaosBandsOutput};
pub use frama::Frama;
pub use fry_pan_bottom::FryPanBottom;
pub use funding_basis::FundingBasis;
pub use funding_rate::FundingRate;
pub use funding_rate_mean::FundingRateMean;
@@ -636,8 +661,11 @@ pub use gravestone_doji::GravestoneDoji;
pub use hammer::Hammer;
pub use hanging_man::HangingMan;
pub use harami::Harami;
pub use harami_cross::HaramiCross;
pub use hasbrouck_information_share::HasbrouckInformationShare;
pub use head_and_shoulders::HeadAndShoulders;
pub use heikin_ashi::{HeikinAshi, HeikinAshiOutput};
pub use heikin_ashi_oscillator::HeikinAshiOscillator;
pub use high_low_index::HighLowIndex;
pub use high_low_range::HighLowRange;
pub use high_wave::HighWave;
@@ -729,6 +757,7 @@ pub use morning_evening_star::MorningEveningStar;
pub use murrey_math_lines::{MurreyMathLines, MurreyMathLinesOutput};
pub use natr::Natr;
pub use new_highs_new_lows::NewHighsNewLows;
pub use new_price_lines::NewPriceLines;
pub use nrtr::{Nrtr, NrtrOutput};
pub use nvi::Nvi;
pub use ob_imbalance_full::OrderBookImbalanceFull;
@@ -756,6 +785,7 @@ pub use percent_b::PercentB;
pub use percentage_trailing_stop::PercentageTrailingStop;
pub use pgo::Pgo;
pub use piercing_dark_cloud::PiercingDarkCloud;
pub use pin::Pin;
pub use pivot_reversal::PivotReversal;
pub use plus_di::PlusDi;
pub use plus_dm::PlusDm;
@@ -823,6 +853,7 @@ pub use skewness::Skewness;
pub use sma::Sma;
pub use smi::Smi;
pub use smma::Smma;
pub use smoothed_heikin_ashi::{SmoothedHeikinAshi, SmoothedHeikinAshiOutput};
pub use sortino_ratio::SortinoRatio;
pub use spearman_correlation::SpearmanCorrelation;
pub use spinning_top::SpinningTop;
@@ -846,22 +877,30 @@ pub use t3::T3;
pub use taker_buy_sell_ratio::TakerBuySellRatio;
pub use takuri::Takuri;
pub use tasuki_gap::TasukiGap;
pub use td_camouflage::TdCamouflage;
pub use td_clop::TdClop;
pub use td_clopwin::TdClopwin;
pub use td_combo::TdCombo;
pub use td_countdown::TdCountdown;
pub use td_demarker::TdDeMarker;
pub use td_differential::TdDifferential;
pub use td_dwave::TdDWave;
pub use td_lines::{TdLines, TdLinesOutput};
pub use td_moving_average::{TdMovingAverage, TdMovingAverageOutput};
pub use td_open::TdOpen;
pub use td_pressure::TdPressure;
pub use td_propulsion::TdPropulsion;
pub use td_range_projection::{TdRangeProjection, TdRangeProjectionOutput};
pub use td_rei::TdRei;
pub use td_risk_level::{TdRiskLevel, TdRiskLevelOutput};
pub use td_sequential::{TdSequential, TdSequentialOutput};
pub use td_setup::TdSetup;
pub use td_trap::TdTrap;
pub use tema::Tema;
pub use term_structure_basis::TermStructureBasis;
pub use three_drives::ThreeDrives;
pub use three_inside::ThreeInside;
pub use three_line_break::ThreeLineBreak;
pub use three_line_strike::ThreeLineStrike;
pub use three_outside::ThreeOutside;
pub use three_soldiers_or_crows::ThreeSoldiersOrCrows;
@@ -871,8 +910,10 @@ pub use tick_index::TickIndex;
pub use tii::Tii;
pub use time_based_stop::TimeBasedStop;
pub use time_of_day_return_profile::{TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput};
pub use tower_top_bottom::TowerTopBottom;
pub use tpo_profile::{TpoProfile, TpoProfileOutput};
pub use trade_imbalance::TradeImbalance;
pub use trade_sign_autocorrelation::TradeSignAutocorrelation;
pub use trade_volume_index::TradeVolumeIndex;
pub use trend_label::TrendLabel;
pub use trend_strength_index::TrendStrengthIndex;
@@ -882,6 +923,7 @@ pub use triangle::Triangle;
pub use trima::Trima;
pub use trin::Trin;
pub use triple_top_bottom::TripleTopBottom;
pub use tristar::Tristar;
pub use trix::Trix;
pub use true_range::TrueRange;
pub use tsf::Tsf;
@@ -1304,9 +1346,27 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"TdDifferential",
"TdOpen",
"TdRiskLevel",
"TdCamouflage",
"TdClop",
"TdClopwin",
"TdPropulsion",
"TdTrap",
"TdDWave",
"TdMovingAverage",
],
),
(
"Ichimoku & Charts",
&[
"Ichimoku",
"HeikinAshi",
"HeikinAshiOscillator",
"ThreeLineBreak",
"SmoothedHeikinAshi",
"Equivolume",
"CandleVolume",
],
),
("Ichimoku & Charts", &["Ichimoku", "HeikinAshi"]),
(
"Candlestick Patterns",
&[
@@ -1370,6 +1430,12 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"TasukiGap",
"UniqueThreeRiver",
"ConcealingBabySwallow",
"Tristar",
"HaramiCross",
"TowerTopBottom",
"FryPanBottom",
"DumplingTop",
"NewPriceLines",
],
),
(
@@ -1392,6 +1458,9 @@ pub const FAMILIES: &[(&str, &[&str])] = &[
"Vpin",
"AmihudIlliquidity",
"RollMeasure",
"TradeSignAutocorrelation",
"Pin",
"HasbrouckInformationShare",
],
),
(
@@ -1555,6 +1624,6 @@ mod family_tests {
// the actual indicator count is the early-warning signal that an
// indicator was added without being assigned a family.
let total: usize = FAMILIES.iter().map(|(_, ns)| ns.len()).sum();
assert_eq!(total, 467, "FAMILIES total drifted from indicator count");
assert_eq!(total, 488, "FAMILIES total drifted from indicator count");
}
}
@@ -0,0 +1,234 @@
//! New Price Lines — the "eight/ten new price lines" exhaustion count.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// New Price Lines — the Japanese "shinne" (new-price) exhaustion count: when the
/// close has made `count` consecutive new highs (or lows), the trend is considered
/// stretched and ripe for a pause or reversal.
///
/// ```text
/// consecutive higher closes form "new price lines" up
/// consecutive lower closes form "new price lines" down
/// signal = 1 once `count` consecutive higher closes (overbought / sell warning)
/// signal = +1 once `count` consecutive lower closes (oversold / buy warning)
/// signal = 0 otherwise
/// ```
///
/// Traditional Japanese practice flags **eight** new price lines (and a stronger
/// **ten** or twelve) as the point where a directional run becomes exhausted —
/// the market has gone up (or down) so many bars in a row that a corrective pause
/// is statistically due. The signal stays active for every bar the streak remains
/// at or above `count`, and clears the moment a close breaks the streak.
///
/// The first value lands on the second bar (one prior close is needed). The
/// output is `+1` / `0` / `1`. Each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, NewPriceLines};
///
/// let mut indicator = NewPriceLines::new(8).unwrap();
/// let mut last = None;
/// for i in 0..12 {
/// let close = 100.0 + f64::from(i); // 11 consecutive higher closes
/// let c = Candle::new(close, close, close, close, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert_eq!(last, Some(-1.0));
/// ```
#[derive(Debug, Clone)]
pub struct NewPriceLines {
count: usize,
prev_close: Option<f64>,
consec_up: usize,
consec_down: usize,
last: Option<f64>,
}
impl NewPriceLines {
/// Construct a New Price Lines counter that fires at `count` consecutive new
/// closes (classic `8`, stronger `10`/`12`).
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `count < 2`.
pub fn new(count: usize) -> Result<Self> {
if count < 2 {
return Err(Error::InvalidPeriod {
message: "new price lines count must be >= 2",
});
}
Ok(Self {
count,
prev_close: None,
consec_up: 0,
consec_down: 0,
last: None,
})
}
/// Configured count threshold.
pub const fn count(&self) -> usize {
self.count
}
/// Current consecutive streak `(up, down)`.
pub const fn streak(&self) -> (usize, usize) {
(self.consec_up, self.consec_down)
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for NewPriceLines {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let close = candle.close;
let Some(prev) = self.prev_close else {
self.prev_close = Some(close);
return None;
};
if close > prev {
self.consec_up += 1;
self.consec_down = 0;
} else if close < prev {
self.consec_down += 1;
self.consec_up = 0;
} else {
self.consec_up = 0;
self.consec_down = 0;
}
self.prev_close = Some(close);
let v = if self.consec_up >= self.count {
-1.0
} else if self.consec_down >= self.count {
1.0
} else {
0.0
};
self.last = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev_close = None;
self.consec_up = 0;
self.consec_down = 0;
self.last = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"NewPriceLines"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(close: f64) -> Candle {
Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
}
#[test]
fn rejects_small_count() {
assert!(matches!(
NewPriceLines::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(NewPriceLines::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let n = NewPriceLines::new(8).unwrap();
assert_eq!(n.count(), 8);
assert_eq!(n.streak(), (0, 0));
assert_eq!(n.warmup_period(), 2);
assert_eq!(n.name(), "NewPriceLines");
assert!(!n.is_ready());
assert_eq!(n.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut n = NewPriceLines::new(3).unwrap();
assert_eq!(n.update(c(100.0)), None);
assert!(n.update(c(101.0)).is_some());
}
#[test]
fn eight_higher_closes_signal_sell() {
let mut n = NewPriceLines::new(8).unwrap();
// 11 consecutive higher closes -> by the 9th the count reaches 8 -> -1.
let candles: Vec<Candle> = (0..12).map(|i| c(100.0 + f64::from(i))).collect();
let last = n.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, -1.0);
}
#[test]
fn eight_lower_closes_signal_buy() {
let mut n = NewPriceLines::new(8).unwrap();
let candles: Vec<Candle> = (0..12).map(|i| c(200.0 - f64::from(i))).collect();
let last = n.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, 1.0);
}
#[test]
fn break_in_streak_clears_signal() {
let mut n = NewPriceLines::new(3).unwrap();
n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // streak 3 -> -1
assert_eq!(n.value(), Some(-1.0));
// A lower close breaks the up streak.
assert_eq!(n.update(c(102.0)), Some(0.0));
assert_eq!(n.streak(), (0, 1));
}
#[test]
fn unchanged_close_resets_streak() {
let mut n = NewPriceLines::new(3).unwrap();
n.batch(&[c(100.0), c(101.0), c(102.0)]);
assert_eq!(n.update(c(102.0)), Some(0.0)); // equal -> reset
assert_eq!(n.streak(), (0, 0));
}
#[test]
fn reset_clears_state() {
let mut n = NewPriceLines::new(3).unwrap();
n.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]);
assert!(n.is_ready());
n.reset();
assert!(!n.is_ready());
assert_eq!(n.value(), None);
assert_eq!(n.streak(), (0, 0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
.collect();
let batch = NewPriceLines::new(8).unwrap().batch(&candles);
let mut b = NewPriceLines::new(8).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
+223
View File
@@ -0,0 +1,223 @@
//! PIN — Probability of Informed Trading (single-window EKOP estimate).
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::microstructure::Trade;
use crate::traits::Indicator;
/// PIN — the **Probability of Informed Trading**, estimated from the buy/sell order
/// imbalance over a rolling window of trades.
///
/// ```text
/// over the last `window` trades: B = buys, S = sells (B + S = window)
/// PIN ≈ |B S| / (B + S) ∈ [0, 1]
/// ```
///
/// The Easley-Kiefer-O'Hara-Paperman (EKOP) model splits order flow into an
/// uninformed component (balanced buys and sells, rate `ε` per side) and an
/// informed component that trades one-directionally when private information
/// arrives (rate `μ`, probability `α`). The probability that any given trade is
/// information-motivated is `PIN = αμ / (αμ + 2ε)`. Estimated over a single window,
/// the informed flow shows up as the **net imbalance** `|B S|` and the uninformed
/// flow as the balanced remainder, giving the moment estimator above. A high PIN
/// flags a one-sided, likely-informed market; a low PIN flags balanced, uninformed
/// flow.
///
/// This is distinct from [`Vpin`](crate::Vpin), the volume-synchronised variant
/// that buckets by volume and uses bulk-volume classification; here trades are
/// counted in event time and classified by their tagged aggressor side. The full
/// PIN is fit by maximum likelihood over many periods — this single-window
/// estimator is the streaming moment approximation. The output is in `[0, 1]`; the
/// first value lands after `window` trades.
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, Pin, Side, Trade};
///
/// let mut indicator = Pin::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// // All buys -> maximally one-sided -> PIN 1.
/// last = indicator.update(Trade::new(100.0, 1.0, Side::Buy, i).unwrap());
/// }
/// assert!((last.unwrap() - 1.0).abs() < 1e-9);
/// ```
#[derive(Debug, Clone)]
pub struct Pin {
window: usize,
sides: VecDeque<f64>,
buy_count: usize,
last: Option<f64>,
}
impl Pin {
/// Construct a PIN estimator over `window` trades.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `window == 0`.
pub fn new(window: usize) -> Result<Self> {
if window == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
window,
sides: VecDeque::with_capacity(window),
buy_count: 0,
last: None,
})
}
/// Configured window of trades.
pub const fn window(&self) -> usize {
self.window
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for Pin {
type Input = Trade;
type Output = f64;
fn update(&mut self, trade: Trade) -> Option<f64> {
let is_buy = trade.side.sign() > 0.0;
if self.sides.len() == self.window {
let old = self.sides.pop_front().expect("non-empty");
if old > 0.0 {
self.buy_count -= 1;
}
}
self.sides.push_back(if is_buy { 1.0 } else { 0.0 });
if is_buy {
self.buy_count += 1;
}
if self.sides.len() < self.window {
return None;
}
// The window is full and `window >= 1` (zero is rejected at
// construction), so the trade count is always positive — `|B - S| / N`
// needs no zero guard.
let buys = self.buy_count as f64;
let sells = self.window as f64 - buys;
let total = self.window as f64;
let pin = (buys - sells).abs() / total;
self.last = Some(pin);
Some(pin)
}
fn reset(&mut self) {
self.sides.clear();
self.buy_count = 0;
self.last = None;
}
fn warmup_period(&self) -> usize {
self.window
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"PIN"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::microstructure::Side;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn buy() -> Trade {
Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
}
fn sell() -> Trade {
Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
}
#[test]
fn rejects_zero_window() {
assert!(matches!(Pin::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let p = Pin::new(20).unwrap();
assert_eq!(p.window(), 20);
assert_eq!(p.warmup_period(), 20);
assert_eq!(p.name(), "PIN");
assert!(!p.is_ready());
assert_eq!(p.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut p = Pin::new(4).unwrap();
let out = p.batch(&[buy(), buy(), buy(), buy(), buy()]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn one_sided_flow_is_one() {
let mut p = Pin::new(10).unwrap();
let trades: Vec<Trade> = (0..20).map(|_| buy()).collect();
let last = p.batch(&trades).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 1.0, epsilon = 1e-12);
}
#[test]
fn balanced_flow_is_zero() {
let mut p = Pin::new(10).unwrap();
let trades: Vec<Trade> = (0..20)
.map(|i| if i % 2 == 0 { buy() } else { sell() })
.collect();
let last = p.batch(&trades).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 0.0, epsilon = 1e-12);
}
#[test]
fn output_in_range() {
let mut p = Pin::new(16).unwrap();
let trades: Vec<Trade> = (0..200)
.map(|i| if (i * 5 % 13) < 8 { buy() } else { sell() })
.collect();
for v in p.batch(&trades).into_iter().flatten() {
assert!((0.0..=1.0).contains(&v));
}
}
#[test]
fn reset_clears_state() {
let mut p = Pin::new(4).unwrap();
p.batch(&[buy(), buy(), sell(), buy()]);
assert!(p.is_ready());
p.reset();
assert!(!p.is_ready());
assert_eq!(p.value(), None);
assert_eq!(p.update(buy()), None);
}
#[test]
fn batch_equals_streaming() {
let trades: Vec<Trade> = (0..120)
.map(|i| if i % 3 == 0 { sell() } else { buy() })
.collect();
let batch = Pin::new(16).unwrap().batch(&trades);
let mut b = Pin::new(16).unwrap();
let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,252 @@
//! Smoothed Heikin-Ashi — Heikin-Ashi computed on EMA-smoothed OHLC.
use crate::error::{Error, Result};
use crate::indicators::ema::Ema;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// One smoothed Heikin-Ashi candle.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SmoothedHeikinAshiOutput {
/// Smoothed Heikin-Ashi open.
pub open: f64,
/// Smoothed Heikin-Ashi high.
pub high: f64,
/// Smoothed Heikin-Ashi low.
pub low: f64,
/// Smoothed Heikin-Ashi close.
pub close: f64,
}
/// Smoothed Heikin-Ashi — the [`HeikinAshi`](crate::HeikinAshi) transform applied
/// to **EMA-smoothed** OHLC, for an even cleaner trend view.
///
/// ```text
/// eo, eh, el, ec = EMA(open|high|low|close, period)
/// ha_close = (eo + eh + el + ec) / 4
/// ha_open = (prev_ha_open + prev_ha_close) / 2 (seeded with (eo + ec)/2)
/// ha_high = max(eh, ha_open, ha_close)
/// ha_low = min(el, ha_open, ha_close)
/// ```
///
/// Standard Heikin-Ashi already averages the OHLC; smoothing each input series
/// with an EMA *before* the transform removes still more noise, producing long,
/// uninterrupted runs of same-colour candles in a trend and crisp colour flips at
/// turns. The trade-off is added lag proportional to `period`. The output uses the
/// same OHLC field layout as a candle so it can be charted directly.
///
/// The first value lands once the EMAs are seeded (`period` inputs). Each `update`
/// is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, SmoothedHeikinAshi};
///
/// let mut indicator = SmoothedHeikinAshi::new(10).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let c = Candle::new(base, base + 1.0, base - 1.0, base + 0.5, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct SmoothedHeikinAshi {
period: usize,
ema_open: Ema,
ema_high: Ema,
ema_low: Ema,
ema_close: Ema,
prev: Option<SmoothedHeikinAshiOutput>,
last: Option<SmoothedHeikinAshiOutput>,
}
impl SmoothedHeikinAshi {
/// Construct a smoothed Heikin-Ashi with the given EMA `period`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `period == 0`.
pub fn new(period: usize) -> Result<Self> {
if period == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
period,
ema_open: Ema::new(period)?,
ema_high: Ema::new(period)?,
ema_low: Ema::new(period)?,
ema_close: Ema::new(period)?,
prev: None,
last: None,
})
}
/// Configured smoothing period.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<SmoothedHeikinAshiOutput> {
self.last
}
}
impl Indicator for SmoothedHeikinAshi {
type Input = Candle;
type Output = SmoothedHeikinAshiOutput;
fn update(&mut self, candle: Candle) -> Option<SmoothedHeikinAshiOutput> {
let eo = self.ema_open.update(candle.open);
let eh = self.ema_high.update(candle.high);
let el = self.ema_low.update(candle.low);
let ec = self.ema_close.update(candle.close);
let (Some(eo), Some(eh), Some(el), Some(ec)) = (eo, eh, el, ec) else {
return None;
};
let ha_close = (eo + eh + el + ec) / 4.0;
let ha_open = match self.prev {
Some(p) => f64::midpoint(p.open, p.close),
None => f64::midpoint(eo, ec),
};
let ha_high = eh.max(ha_open).max(ha_close);
let ha_low = el.min(ha_open).min(ha_close);
let out = SmoothedHeikinAshiOutput {
open: ha_open,
high: ha_high,
low: ha_low,
close: ha_close,
};
self.prev = Some(out);
self.last = Some(out);
Some(out)
}
fn reset(&mut self) {
self.ema_open.reset();
self.ema_high.reset();
self.ema_low.reset();
self.ema_close.reset();
self.prev = None;
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"SmoothedHeikinAshi"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
Candle::new_unchecked(open, high, low, close, 1_000.0, 0)
}
#[test]
fn rejects_zero_period() {
assert!(matches!(SmoothedHeikinAshi::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let s = SmoothedHeikinAshi::new(10).unwrap();
assert_eq!(s.period(), 10);
assert_eq!(s.warmup_period(), 10);
assert_eq!(s.name(), "SmoothedHeikinAshi");
assert!(!s.is_ready());
assert_eq!(s.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut s = SmoothedHeikinAshi::new(3).unwrap();
let candles: Vec<Candle> = (0..6)
.map(|i| {
let b = 100.0 + f64::from(i);
c(b, b + 1.0, b - 1.0, b + 0.5)
})
.collect();
let out = s.batch(&candles);
for v in out.iter().take(2) {
assert!(v.is_none());
}
assert!(out[2].is_some());
}
#[test]
fn high_brackets_open_close() {
let mut s = SmoothedHeikinAshi::new(3).unwrap();
let candles: Vec<Candle> = (0..30)
.map(|i| {
let b = 100.0 + f64::from(i);
c(b, b + 2.0, b - 2.0, b + 0.5)
})
.collect();
for o in s.batch(&candles).into_iter().flatten() {
assert!(o.high >= o.open && o.high >= o.close);
assert!(o.low <= o.open && o.low <= o.close);
}
}
#[test]
fn uptrend_close_above_open() {
let mut s = SmoothedHeikinAshi::new(3).unwrap();
let candles: Vec<Candle> = (0..30)
.map(|i| {
let b = 100.0 + 2.0 * f64::from(i);
c(b, b + 1.0, b - 1.0, b + 0.5)
})
.collect();
let o = s.batch(&candles).into_iter().flatten().last().unwrap();
assert!(
o.close > o.open,
"an uptrend should print a bullish smoothed HA candle"
);
}
#[test]
fn reset_clears_state() {
let mut s = SmoothedHeikinAshi::new(3).unwrap();
s.batch(
&(0..10)
.map(|i| {
let b = 100.0 + f64::from(i);
c(b, b + 1.0, b - 1.0, b)
})
.collect::<Vec<_>>(),
);
assert!(s.is_ready());
s.reset();
assert!(!s.is_ready());
assert_eq!(s.value(), None);
assert_eq!(s.update(c(100.0, 101.0, 99.0, 100.0)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.25).sin() * 9.0;
c(b, b + 1.0, b - 1.0, b + 0.3)
})
.collect();
let batch = SmoothedHeikinAshi::new(10).unwrap().batch(&candles);
let mut b = SmoothedHeikinAshi::new(10).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,156 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Camouflage — a hidden-strength/weakness 1-bar reversal pattern.
//!
//! TD Camouflage spots a bar that *looks* weak (or strong) on its close-to-close
//! comparison but reveals the opposite intrabar, "camouflaging" a reversal.
//!
//! - **Buy signal** (`+1.0`): `close < close[-1]` (a lower close, looks bearish),
//! yet `close > open` (it actually closed up on the bar) and `low < low[-1]`
//! (it dipped to a new low and was bought back) — hidden accumulation.
//! - **Sell signal** (`-1.0`): `close > close[-1]`, `close < open`, and
//! `high > high[-1]` — hidden distribution.
//! - Otherwise the output is `0.0`.
//!
//! The one-bar lookback means the first value lands on the second candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TD Camouflage — 1-bar hidden-strength/weakness reversal detector.
#[derive(Debug, Clone, Default)]
pub struct TdCamouflage {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl TdCamouflage {
/// Construct a new `TdCamouflage`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdCamouflage {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let v = if candle.close < prev.close && candle.close > candle.open && candle.low < prev.low
{
1.0
} else if candle.close > prev.close && candle.close < candle.open && candle.high > prev.high
{
-1.0
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDCamouflage"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
Candle::new_unchecked(open, high, low, close, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let td = TdCamouflage::new();
assert_eq!(td.warmup_period(), 2);
assert_eq!(td.name(), "TDCamouflage");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut td = TdCamouflage::new();
assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
assert!(td.update(c(10.0, 11.0, 8.0, 9.5)).is_some());
}
#[test]
fn bullish_camouflage_buy() {
// prev close 10. Current: close 9.5 < 10 (lower close), close 9.5 > open 9.0,
// low 7.0 < prev low 8.0 -> buy.
let mut td = TdCamouflage::new();
td.update(c(10.0, 11.0, 8.0, 10.0));
assert_eq!(td.update(c(9.0, 10.0, 7.0, 9.5)), Some(1.0));
}
#[test]
fn bearish_camouflage_sell() {
// prev close 10. Current: close 10.5 > 10, close 10.5 < open 11.0,
// high 12.0 > prev high 11.0 -> sell.
let mut td = TdCamouflage::new();
td.update(c(10.0, 11.0, 8.0, 10.0));
assert_eq!(td.update(c(11.0, 12.0, 10.0, 10.5)), Some(-1.0));
}
#[test]
fn no_pattern_is_zero() {
let mut td = TdCamouflage::new();
td.update(c(10.0, 11.0, 9.0, 10.0));
assert_eq!(td.update(c(10.0, 11.5, 9.5, 11.0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut td = TdCamouflage::new();
td.update(c(10.0, 11.0, 9.0, 10.0));
td.update(c(9.0, 10.0, 7.0, 9.5));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
c(b, b + 1.0, b - 1.0, b + 0.2)
})
.collect();
let batch = TdCamouflage::new().batch(&candles);
let mut b = TdCamouflage::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,159 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Clop — a 2-bar open/close engulfing reversal.
//!
//! TD Clop ("CLose/OPen") fires when the current bar's open opens beyond **both**
//! the prior bar's open and close, and its close finishes back beyond both — an
//! open-gap that fully reverses, signalling a turn.
//!
//! - **Buy signal** (`+1.0`): `open < open[-1]` AND `open < close[-1]`
//! (opens below the whole prior body) AND `close > open[-1]` AND
//! `close > close[-1]` (closes above it).
//! - **Sell signal** (`-1.0`): `open > open[-1]` AND `open > close[-1]` AND
//! `close < open[-1]` AND `close < close[-1]`.
//! - Otherwise the output is `0.0`.
//!
//! The one-bar lookback means the first value lands on the second candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TD Clop — 2-bar open/close engulfing reversal detector.
#[derive(Debug, Clone, Default)]
pub struct TdClop {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl TdClop {
/// Construct a new `TdClop`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdClop {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let below_body = candle.open < prev.open && candle.open < prev.close;
let above_body = candle.close > prev.open && candle.close > prev.close;
let over_body = candle.open > prev.open && candle.open > prev.close;
let under_body = candle.close < prev.open && candle.close < prev.close;
let v = if below_body && above_body {
1.0
} else if over_body && under_body {
-1.0
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDClop"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, close: f64) -> Candle {
let high = open.max(close) + 1.0;
let low = open.min(close) - 1.0;
Candle::new_unchecked(open, high, low, close, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let td = TdClop::new();
assert_eq!(td.warmup_period(), 2);
assert_eq!(td.name(), "TDClop");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut td = TdClop::new();
assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
assert!(td.update(c(9.0, 12.0)).is_some());
}
#[test]
fn bullish_clop_buy() {
// prev body [10, 11]. Current open 9 < both, close 12 > both -> buy.
let mut td = TdClop::new();
td.update(c(10.0, 11.0));
assert_eq!(td.update(c(9.0, 12.0)), Some(1.0));
}
#[test]
fn bearish_clop_sell() {
// prev body [10, 11]. Current open 12 > both, close 9 < both -> sell.
let mut td = TdClop::new();
td.update(c(10.0, 11.0));
assert_eq!(td.update(c(12.0, 9.0)), Some(-1.0));
}
#[test]
fn no_pattern_is_zero() {
let mut td = TdClop::new();
td.update(c(10.0, 11.0));
assert_eq!(td.update(c(10.5, 11.5)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut td = TdClop::new();
td.update(c(10.0, 11.0));
td.update(c(9.0, 12.0));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(10.0, 11.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
c(b, b + 0.5)
})
.collect();
let batch = TdClop::new().batch(&candles);
let mut b = TdClop::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,162 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Clopwin — a 2-bar "close/open within" inside-body pattern.
//!
//! TD Clopwin ("CLose/OPen WInthIN") is the inside-body cousin of TD Clop: the
//! current bar's open **and** close both sit within the prior bar's real body,
//! marking a compression bar whose direction hints at the next move.
//!
//! - **Buy signal** (`+1.0`): current `open` and `close` are both inside the prior
//! bar's body `[min(open,close)[-1], max(open,close)[-1]]` AND `close >= open`
//! (a bullish inside bar).
//! - **Sell signal** (`-1.0`): both inside the prior body AND `close < open`
//! (a bearish inside bar).
//! - Otherwise the output is `0.0`.
//!
//! The one-bar lookback means the first value lands on the second candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TD Clopwin — 2-bar inside-body compression pattern detector.
#[derive(Debug, Clone, Default)]
pub struct TdClopwin {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl TdClopwin {
/// Construct a new `TdClopwin`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdClopwin {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let body_low = prev.open.min(prev.close);
let body_high = prev.open.max(prev.close);
let open_in = candle.open >= body_low && candle.open <= body_high;
let close_in = candle.close >= body_low && candle.close <= body_high;
let v = if open_in && close_in {
if candle.close >= candle.open {
1.0
} else {
-1.0
}
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDClopwin"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, close: f64) -> Candle {
let high = open.max(close) + 1.0;
let low = open.min(close) - 1.0;
Candle::new_unchecked(open, high, low, close, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let td = TdClopwin::new();
assert_eq!(td.warmup_period(), 2);
assert_eq!(td.name(), "TDClopwin");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut td = TdClopwin::new();
assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
assert!(td.update(c(11.0, 13.0)).is_some());
}
#[test]
fn bullish_inside_body_buy() {
// prev body [10, 14]. Current open 11, close 13 both inside, close>open -> +1.
let mut td = TdClopwin::new();
td.update(c(10.0, 14.0));
assert_eq!(td.update(c(11.0, 13.0)), Some(1.0));
}
#[test]
fn bearish_inside_body_sell() {
// prev body [10, 14]. Current open 13, close 11 inside, close<open -> -1.
let mut td = TdClopwin::new();
td.update(c(10.0, 14.0));
assert_eq!(td.update(c(13.0, 11.0)), Some(-1.0));
}
#[test]
fn outside_body_is_zero() {
let mut td = TdClopwin::new();
td.update(c(10.0, 14.0));
// close 16 outside the prior body -> 0.
assert_eq!(td.update(c(11.0, 16.0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut td = TdClopwin::new();
td.update(c(10.0, 14.0));
td.update(c(11.0, 13.0));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(10.0, 14.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
c(b, b + 0.3)
})
.collect();
let batch = TdClopwin::new().batch(&candles);
let mut b = TdClopwin::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,295 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD D-Wave — a simplified Elliott-style swing-wave counter.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Tom DeMark **TD D-Wave** — a streaming wave counter that labels the market's
/// swing sequence with an Elliott-style `15` impulse / `AC` correction count.
///
/// TD D-Wave is DeMark's objective alternative to discretionary Elliott Wave
/// counting. This streaming implementation detects alternating swing pivots with a
/// symmetric fractal of half-width `strength`, and advances a counter through the
/// eight-leg cycle each time a new swing leg is confirmed:
///
/// ```text
/// legs: 1 → 2 → 3 → 4 → 5 → A(6) → B(7) → C(8) → 1 …
/// output = current wave number, 1.0..8.0 (6/7/8 = corrective A/B/C)
/// ```
///
/// The number tells you which wave of the cycle price is currently working on — a
/// running map of impulse versus correction that updates as each swing confirms.
/// This is a **simplified** swing-leg count (it does not enforce Elliott's price
/// ratio and overlap rules); treat it as a structural guide, not a strict wave
/// label.
///
/// Readiness is data-dependent: the first value appears once the first swing pivot
/// confirms (`strength` bars after it forms). `warmup_period` returns the minimum
/// bars to confirm one pivot. Each `update` is O(`strength`).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, TdDWave};
///
/// let mut indicator = TdDWave::new(2).unwrap();
/// let mut last = None;
/// for i in 0..120 {
/// let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0;
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// let _ = last;
/// ```
#[derive(Debug, Clone)]
pub struct TdDWave {
strength: usize,
window: VecDeque<Candle>,
last_is_high: Option<bool>,
last_extreme: f64,
wave: usize,
last_value: Option<f64>,
}
impl TdDWave {
/// Construct a TD D-Wave with the given fractal `strength`.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `strength == 0`.
pub fn new(strength: usize) -> Result<Self> {
if strength == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
strength,
window: VecDeque::with_capacity(2 * strength + 1),
last_is_high: None,
last_extreme: 0.0,
wave: 0,
last_value: None,
})
}
/// Configured fractal strength.
pub const fn strength(&self) -> usize {
self.strength
}
/// Current wave number if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
fn advance(&mut self, is_high: bool, price: f64) {
match self.last_is_high {
Some(prev) if prev == is_high => {
// Same-direction extreme: extend the current leg if more extreme.
let extends = if is_high {
price > self.last_extreme
} else {
price < self.last_extreme
};
if extends {
self.last_extreme = price;
}
}
_ => {
// A new alternating leg: advance the wave counter (1..8 cycle).
self.wave = self.wave % 8 + 1;
self.last_is_high = Some(is_high);
self.last_extreme = price;
self.last_value = Some(self.wave as f64);
}
}
}
}
impl Indicator for TdDWave {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let span = 2 * self.strength + 1;
if self.window.len() == span {
self.window.pop_front();
}
self.window.push_back(candle);
if self.window.len() == span {
let center = self.window[self.strength];
let is_high = self
.window
.iter()
.enumerate()
.all(|(i, c)| i == self.strength || c.high < center.high);
let is_low = self
.window
.iter()
.enumerate()
.all(|(i, c)| i == self.strength || c.low > center.low);
if is_high && !is_low {
self.advance(true, center.high);
} else if is_low && !is_high {
self.advance(false, center.low);
}
}
self.last_value
}
fn reset(&mut self) {
self.window.clear();
self.last_is_high = None;
self.last_extreme = 0.0;
self.wave = 0;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2 * self.strength + 1
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDDWave"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(high: f64, low: f64) -> Candle {
Candle::new_unchecked(
f64::midpoint(high, low),
high,
low,
f64::midpoint(high, low),
1_000.0,
0,
)
}
fn zigzag() -> Vec<Candle> {
(0..200)
.map(|i| {
let base = 100.0 + (f64::from(i) * 0.5).sin() * 10.0;
c(base + 1.0, base - 1.0)
})
.collect()
}
#[test]
fn rejects_zero_strength() {
assert!(matches!(TdDWave::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let td = TdDWave::new(2).unwrap();
assert_eq!(td.strength(), 2);
assert_eq!(td.warmup_period(), 5);
assert_eq!(td.name(), "TDDWave");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn counts_waves_on_swings() {
let mut td = TdDWave::new(2).unwrap();
let out = td.batch(&zigzag());
assert!(out.iter().any(Option::is_some));
assert!(td.is_ready());
}
#[test]
fn same_direction_pivots_extend_one_leg() {
// Strictly decreasing lows mean no bar is ever a low pivot, so the
// confirmed pivots are all highs. Consecutive same-direction highs
// exercise the `extends` branch (true at 30 > 20, false at 25 < 30)
// without ever advancing the wave past leg 1.
let mut td = TdDWave::new(1).unwrap();
let bars = [
(10.0, 100.0),
(20.0, 99.0),
(12.0, 98.0),
(30.0, 97.0),
(15.0, 96.0),
(25.0, 95.0),
(14.0, 94.0),
(14.0, 93.0),
];
let vals: Vec<f64> = bars
.iter()
.filter_map(|&(high, low)| td.update(c(high, low)))
.collect();
assert!(!vals.is_empty());
assert!(vals.iter().all(|&v| v == 1.0));
}
#[test]
fn same_direction_low_pivots_extend_one_leg() {
// Mirror of the high-pivot case: strictly increasing highs mean no bar
// is ever a high pivot, so the confirmed pivots are all lows. The
// `extends` else-branch fires (true at 2 < 5, false at 4 > 2).
let mut td = TdDWave::new(1).unwrap();
let bars = [
(100.0, 10.0),
(101.0, 5.0),
(102.0, 8.0),
(103.0, 2.0),
(104.0, 6.0),
(105.0, 4.0),
(106.0, 7.0),
(107.0, 7.0),
];
let vals: Vec<f64> = bars
.iter()
.filter_map(|&(high, low)| td.update(c(high, low)))
.collect();
assert!(!vals.is_empty());
assert!(vals.iter().all(|&v| v == 1.0));
}
#[test]
fn wave_stays_in_one_to_eight() {
let mut td = TdDWave::new(2).unwrap();
for v in td.batch(&zigzag()).into_iter().flatten() {
assert!((1.0..=8.0).contains(&v), "wave out of range: {v}");
}
}
#[test]
fn flat_input_never_counts() {
// A perfectly flat series has no distinct swing highs/lows.
let mut td = TdDWave::new(2).unwrap();
let candles: Vec<Candle> = (0..40).map(|_| c(100.0, 100.0)).collect();
assert!(td.batch(&candles).iter().all(Option::is_none));
}
#[test]
fn reset_clears_state() {
let mut td = TdDWave::new(2).unwrap();
td.batch(&zigzag());
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn batch_equals_streaming() {
let candles = zigzag();
let batch = TdDWave::new(2).unwrap().batch(&candles);
let mut b = TdDWave::new(2).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,229 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Moving Averages — the ST1 (fast) and ST2 (slow) trend ribbon.
use crate::error::{Error, Result};
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Output of [`TdMovingAverage`]: the fast (`st1`) and slow (`st2`) moving-average
/// lines.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TdMovingAverageOutput {
/// ST1 — the fast (short) moving average.
pub st1: f64,
/// ST2 — the slow (long) moving average.
pub st2: f64,
}
/// Tom DeMark **TD Moving Averages** — a two-line trend ribbon (ST1 fast, ST2
/// slow) computed on the median price, whose relationship defines the trend.
///
/// ```text
/// price = (high + low) / 2 (median price)
/// st1 = SMA(price, period_st1) (fast / "Sequential Trend 1")
/// st2 = SMA(price, period_st2) (slow / "Sequential Trend 2")
/// ```
///
/// DeMark's moving-average pair frames the trend objectively: when `st1` is above
/// `st2` the trend is up, below it down, and the cross marks the change. Using the
/// **median price** rather than the close de-emphasises closing noise. This is a
/// streaming dual-SMA implementation of the ST1/ST2 ribbon; read the lines and
/// their crossover exactly as a fast/slow moving-average system.
///
/// `period_st1` must be strictly smaller than `period_st2`. The first value lands
/// once the slow average is seeded (`period_st2` inputs). Each `update` is O(1).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, TdMovingAverage};
///
/// let mut indicator = TdMovingAverage::new(5, 13).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let base = 100.0 + f64::from(i);
/// let c = Candle::new(base, base + 1.0, base - 1.0, base, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone)]
pub struct TdMovingAverage {
st1: Sma,
st2: Sma,
period_st1: usize,
period_st2: usize,
last: Option<TdMovingAverageOutput>,
}
impl TdMovingAverage {
/// Construct TD Moving Averages with the given fast and slow periods.
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if either period is `0`, and
/// [`Error::InvalidPeriod`] if `period_st1 >= period_st2`.
pub fn new(period_st1: usize, period_st2: usize) -> Result<Self> {
if period_st1 == 0 || period_st2 == 0 {
return Err(Error::PeriodZero);
}
if period_st1 >= period_st2 {
return Err(Error::InvalidPeriod {
message: "TD moving average ST1 period must be strictly less than ST2",
});
}
Ok(Self {
st1: Sma::new(period_st1)?,
st2: Sma::new(period_st2)?,
period_st1,
period_st2,
last: None,
})
}
/// Configured `(period_st1, period_st2)`.
pub const fn periods(&self) -> (usize, usize) {
(self.period_st1, self.period_st2)
}
/// Current value if available.
pub const fn value(&self) -> Option<TdMovingAverageOutput> {
self.last
}
}
impl Indicator for TdMovingAverage {
type Input = Candle;
type Output = TdMovingAverageOutput;
fn update(&mut self, candle: Candle) -> Option<TdMovingAverageOutput> {
let price = candle.median_price();
let fast = self.st1.update(price);
let slow = self.st2.update(price);
if let (Some(st1), Some(st2)) = (fast, slow) {
let out = TdMovingAverageOutput { st1, st2 };
self.last = Some(out);
return Some(out);
}
None
}
fn reset(&mut self) {
self.st1.reset();
self.st2.reset();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period_st2
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"TDMovingAverage"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn c(median: f64) -> Candle {
Candle::new_unchecked(median, median + 1.0, median - 1.0, median, 1_000.0, 0)
}
#[test]
fn rejects_invalid_periods() {
assert!(matches!(
TdMovingAverage::new(0, 13),
Err(Error::PeriodZero)
));
assert!(matches!(
TdMovingAverage::new(13, 5),
Err(Error::InvalidPeriod { .. })
));
assert!(matches!(
TdMovingAverage::new(5, 5),
Err(Error::InvalidPeriod { .. })
));
}
#[test]
fn accessors_and_metadata() {
let td = TdMovingAverage::new(5, 13).unwrap();
assert_eq!(td.periods(), (5, 13));
assert_eq!(td.warmup_period(), 13);
assert_eq!(td.name(), "TDMovingAverage");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut td = TdMovingAverage::new(2, 4).unwrap();
let candles: Vec<Candle> = (0..8).map(|i| c(100.0 + f64::from(i))).collect();
let out = td.batch(&candles);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn fast_leads_slow_in_uptrend() {
let mut td = TdMovingAverage::new(3, 7).unwrap();
let candles: Vec<Candle> = (0..40).map(|i| c(100.0 + f64::from(i))).collect();
let out = td.batch(&candles).into_iter().flatten().last().unwrap();
assert!(out.st1 > out.st2, "fast MA should lead in an uptrend");
}
#[test]
fn fast_below_slow_in_downtrend() {
let mut td = TdMovingAverage::new(3, 7).unwrap();
let candles: Vec<Candle> = (0..40).map(|i| c(200.0 - f64::from(i))).collect();
let out = td.batch(&candles).into_iter().flatten().last().unwrap();
assert!(out.st1 < out.st2, "fast MA should trail in a downtrend");
}
#[test]
fn flat_series_equal_lines() {
let mut td = TdMovingAverage::new(2, 4).unwrap();
let out = td
.batch(&[c(50.0); 10])
.into_iter()
.flatten()
.last()
.unwrap();
assert_relative_eq!(out.st1, 50.0, epsilon = 1e-9);
assert_relative_eq!(out.st2, 50.0, epsilon = 1e-9);
}
#[test]
fn reset_clears_state() {
let mut td = TdMovingAverage::new(2, 4).unwrap();
td.batch(&(0..10).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.value(), None);
assert_eq!(td.update(c(100.0)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
.collect();
let batch = TdMovingAverage::new(5, 13).unwrap().batch(&candles);
let mut b = TdMovingAverage::new(5, 13).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,152 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Propulsion — a 2-bar trend-continuation thrust signal.
//!
//! TD Propulsion qualifies a continuation thrust: the bar opens on the trend side
//! of the prior close and then closes beyond the prior bar's extreme, "propelling"
//! the move forward.
//!
//! - **Propulsion up** (`+1.0`): `open >= close[-1]` (opens at or above the prior
//! close) AND `close > high[-1]` (closes above the prior high).
//! - **Propulsion down** (`-1.0`): `open <= close[-1]` AND `close < low[-1]`.
//! - Otherwise the output is `0.0`.
//!
//! The one-bar lookback means the first value lands on the second candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TD Propulsion — 2-bar trend-continuation thrust detector.
#[derive(Debug, Clone, Default)]
pub struct TdPropulsion {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl TdPropulsion {
/// Construct a new `TdPropulsion`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdPropulsion {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let v = if candle.open >= prev.close && candle.close > prev.high {
1.0
} else if candle.open <= prev.close && candle.close < prev.low {
-1.0
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDPropulsion"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64) -> Candle {
Candle::new_unchecked(open, high, low, close, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let td = TdPropulsion::new();
assert_eq!(td.warmup_period(), 2);
assert_eq!(td.name(), "TDPropulsion");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_bar_seeds_without_signal() {
let mut td = TdPropulsion::new();
assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0)), Some(0.0));
assert!(td.update(c(10.5, 12.0, 10.0, 11.5)).is_some());
}
#[test]
fn propulsion_up() {
// prev close 10, high 11. Current open 10.5 >= 10, close 11.5 > 11 -> +1.
let mut td = TdPropulsion::new();
td.update(c(9.5, 11.0, 9.0, 10.0));
assert_eq!(td.update(c(10.5, 12.0, 10.0, 11.5)), Some(1.0));
}
#[test]
fn propulsion_down() {
// prev close 10, low 9. Current open 9.5 <= 10, close 8.5 < 9 -> -1.
let mut td = TdPropulsion::new();
td.update(c(10.5, 11.0, 9.0, 10.0));
assert_eq!(td.update(c(9.5, 10.0, 8.0, 8.5)), Some(-1.0));
}
#[test]
fn no_thrust_is_zero() {
let mut td = TdPropulsion::new();
td.update(c(9.5, 11.0, 9.0, 10.0));
// close 10.5 not above prior high 11 -> 0.
assert_eq!(td.update(c(10.5, 10.8, 10.0, 10.5)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut td = TdPropulsion::new();
td.update(c(9.5, 11.0, 9.0, 10.0));
td.update(c(10.5, 12.0, 10.0, 11.5));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(9.5, 11.0, 9.0, 10.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.4).sin() * 5.0;
c(b, b + 1.0, b - 1.0, b + 0.3)
})
.collect();
let batch = TdPropulsion::new().batch(&candles);
let mut b = TdPropulsion::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,171 @@
#![allow(clippy::doc_markdown)]
//! Tom DeMark TD Trap — an inside-bar ("trap") followed by a range breakout.
//!
//! A TD Trap forms when one bar is an **inside bar** (its high below and low above
//! the prior bar's), coiling the market; the next bar that closes beyond the trap
//! bar's high or low triggers the directional signal.
//!
//! - **Buy signal** (`+1.0`): the prior bar was an inside bar and the current
//! `close` is above that inside bar's `high`.
//! - **Sell signal** (`-1.0`): the prior bar was an inside bar and the current
//! `close` is below that inside bar's `low`.
//! - Otherwise the output is `0.0`.
//!
//! The two-bar lookback (one to set the inside bar, one before it) means the first
//! value lands on the third candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// TD Trap — inside-bar breakout signal detector.
#[derive(Debug, Clone, Default)]
pub struct TdTrap {
prev1: Option<Candle>,
prev2: Option<Candle>,
last_value: Option<f64>,
}
impl TdTrap {
/// Construct a new `TdTrap`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdTrap {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let (Some(trap), Some(before)) = (self.prev1, self.prev2) else {
// Not enough history yet: emit a neutral 0.0 while seeding.
self.prev2 = self.prev1;
self.prev1 = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let is_inside = trap.high < before.high && trap.low > before.low;
let v = if is_inside && candle.close > trap.high {
1.0
} else if is_inside && candle.close < trap.low {
-1.0
} else {
0.0
};
self.prev2 = self.prev1;
self.prev1 = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev1 = None;
self.prev2 = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDTrap"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(high: f64, low: f64, close: f64) -> Candle {
Candle::new_unchecked(f64::midpoint(high, low), high, low, close, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let td = TdTrap::new();
assert_eq!(td.warmup_period(), 3);
assert_eq!(td.name(), "TDTrap");
assert!(!td.is_ready());
assert_eq!(td.value(), None);
}
#[test]
fn first_two_bars_seed_without_signal() {
let mut td = TdTrap::new();
assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0));
assert_eq!(td.update(c(108.0, 95.0, 102.0)), Some(0.0));
assert!(td.update(c(112.0, 100.0, 110.0)).is_some());
}
#[test]
fn inside_then_breakout_up_buys() {
// bar0 wide [90,110]; bar1 inside [95,108]; bar2 close 109 > 108 -> +1.
let mut td = TdTrap::new();
td.update(c(110.0, 90.0, 100.0));
td.update(c(108.0, 95.0, 102.0)); // inside bar (high<110, low>90)
assert_eq!(td.update(c(112.0, 100.0, 109.0)), Some(1.0));
}
#[test]
fn inside_then_breakdown_sells() {
let mut td = TdTrap::new();
td.update(c(110.0, 90.0, 100.0));
td.update(c(108.0, 95.0, 102.0)); // inside bar
assert_eq!(td.update(c(100.0, 92.0, 94.0)), Some(-1.0)); // close 94 < 95
}
#[test]
fn no_inside_bar_is_zero() {
let mut td = TdTrap::new();
td.update(c(110.0, 90.0, 100.0));
td.update(c(115.0, 85.0, 100.0)); // outside bar, not inside
assert_eq!(td.update(c(120.0, 110.0, 118.0)), Some(0.0));
}
#[test]
fn inside_but_no_breakout_is_zero() {
let mut td = TdTrap::new();
td.update(c(110.0, 90.0, 100.0));
td.update(c(108.0, 95.0, 102.0)); // inside bar
assert_eq!(td.update(c(107.0, 96.0, 103.0)), Some(0.0)); // close 103 within [95,108]
}
#[test]
fn reset_clears_state() {
let mut td = TdTrap::new();
td.update(c(110.0, 90.0, 100.0));
td.update(c(108.0, 95.0, 102.0));
td.update(c(112.0, 100.0, 109.0));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(110.0, 90.0, 100.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let b = 100.0 + (f64::from(i) * 0.4).sin() * 6.0;
c(b + 2.0, b - 2.0, b)
})
.collect();
let batch = TdTrap::new().batch(&candles);
let mut b = TdTrap::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,241 @@
//! Three Line Break — the close-driven line-break chart trend, as a direction.
use crate::error::{Error, Result};
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Three Line Break — the trend direction of a line-break ("kakushi") chart, where
/// a reversal requires the close to break the extreme of the last `lines` lines.
///
/// ```text
/// continue the trend when close exceeds the prior line's end
/// reverse the trend when close breaks beyond the extreme of the last `lines` lines
/// output = current line direction: +1 (up), 1 (down)
/// ```
///
/// A line-break chart ignores time and small moves entirely: it draws a new line
/// only when the close makes a new extreme in the trend, and flips direction only
/// when the close reverses past the high (or low) of the last `lines` lines —
/// classically **three**. This filters out minor pullbacks, so the emitted
/// direction stays in a trend until a genuinely significant reversal. Distinct from
/// the candlestick [`ThreeLineStrike`](crate::ThreeLineStrike) (a fixed four-bar
/// pattern); this is the line-break *chart type* reduced to its trend state. See
/// also the alt-chart "Three-Line-Break Bars" builder.
///
/// The output is `+1.0` / `1.0`. The first bar seeds the reference price; the
/// direction is emitted once the first line is drawn (data-dependent;
/// `warmup_period` returns the minimum `2`). Each `update` is O(`lines`).
///
/// # Example
///
/// ```
/// use wickra_core::{Candle, Indicator, ThreeLineBreak};
///
/// let mut indicator = ThreeLineBreak::new(3).unwrap();
/// let mut last = None;
/// for i in 0..20 {
/// let close = 100.0 + f64::from(i);
/// let c = Candle::new(close, close, close, close, 1_000.0, 0).unwrap();
/// last = indicator.update(c);
/// }
/// assert_eq!(last, Some(1.0));
/// ```
#[derive(Debug, Clone)]
pub struct ThreeLineBreak {
lines: usize,
line_values: Vec<f64>,
dir: i8,
last: Option<f64>,
}
impl ThreeLineBreak {
/// Construct a Three Line Break requiring `lines` lines to reverse (classic 3).
///
/// # Errors
///
/// Returns [`Error::PeriodZero`] if `lines == 0`.
pub fn new(lines: usize) -> Result<Self> {
if lines == 0 {
return Err(Error::PeriodZero);
}
Ok(Self {
lines,
line_values: Vec::with_capacity(lines + 1),
dir: 0,
last: None,
})
}
/// Configured number of lines required to reverse.
pub const fn lines(&self) -> usize {
self.lines
}
/// Current direction if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
fn push_line(&mut self, close: f64, dir: i8) {
self.dir = dir;
self.line_values.push(close);
if self.line_values.len() > self.lines {
self.line_values.remove(0);
}
}
}
impl Indicator for ThreeLineBreak {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let close = candle.close;
let Some(&prior) = self.line_values.last() else {
// Seed the reference price; no line yet.
self.line_values.push(close);
return None;
};
if self.dir >= 0 {
if close > prior {
self.push_line(close, 1);
} else {
let low = self
.line_values
.iter()
.copied()
.fold(f64::INFINITY, f64::min);
if close < low {
self.push_line(close, -1);
}
}
} else if close < prior {
self.push_line(close, -1);
} else {
let high = self
.line_values
.iter()
.copied()
.fold(f64::NEG_INFINITY, f64::max);
if close > high {
self.push_line(close, 1);
}
}
if self.dir == 0 {
return None;
}
let v = f64::from(self.dir);
self.last = Some(v);
Some(v)
}
fn reset(&mut self) {
self.line_values.clear();
self.dir = 0;
self.last = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"ThreeLineBreak"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(close: f64) -> Candle {
Candle::new_unchecked(close, close, close, close, 1_000.0, 0)
}
#[test]
fn rejects_zero_lines() {
assert!(matches!(ThreeLineBreak::new(0), Err(Error::PeriodZero)));
}
#[test]
fn accessors_and_metadata() {
let t = ThreeLineBreak::new(3).unwrap();
assert_eq!(t.lines(), 3);
assert_eq!(t.warmup_period(), 2);
assert_eq!(t.name(), "ThreeLineBreak");
assert!(!t.is_ready());
assert_eq!(t.value(), None);
}
#[test]
fn uptrend_is_plus_one() {
let mut t = ThreeLineBreak::new(3).unwrap();
let candles: Vec<Candle> = (0..20).map(|i| c(100.0 + f64::from(i))).collect();
let out = t.batch(&candles);
assert!(out[0].is_none());
assert_eq!(out[1], Some(1.0));
assert_eq!(out.last().unwrap(), &Some(1.0));
}
#[test]
fn downtrend_is_minus_one() {
let mut t = ThreeLineBreak::new(3).unwrap();
let candles: Vec<Candle> = (0..20).map(|i| c(100.0 - f64::from(i))).collect();
let last = t.batch(&candles).into_iter().flatten().last().unwrap();
assert_eq!(last, -1.0);
}
#[test]
fn small_pullback_does_not_reverse() {
// Rise to build 3 up-lines, then a small dip that does not break the
// 3-line low keeps the direction up.
let mut t = ThreeLineBreak::new(3).unwrap();
t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // up-lines at 101,102,103
// close 102.5 is below the prior line (103) but above the 3-line low (101) -> no reversal.
assert_eq!(t.update(c(102.5)), Some(1.0));
}
#[test]
fn break_of_three_line_extreme_reverses() {
let mut t = ThreeLineBreak::new(3).unwrap();
t.batch(&[c(100.0), c(101.0), c(102.0), c(103.0)]); // lines 101,102,103, dir up
// close 100.5 breaks below the 3-line low (101) -> reverse to down.
assert_eq!(t.update(c(100.5)), Some(-1.0));
}
#[test]
fn reset_clears_state() {
let mut t = ThreeLineBreak::new(3).unwrap();
t.batch(&(0..10).map(|i| c(100.0 + f64::from(i))).collect::<Vec<_>>());
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.value(), None);
assert_eq!(t.update(c(100.0)), None);
}
#[test]
fn flat_close_emits_none_until_a_line_forms() {
let mut t = ThreeLineBreak::new(3).unwrap();
assert_eq!(t.update(c(100.0)), None);
// An identical close draws no line, so the direction stays unset.
assert_eq!(t.update(c(100.0)), None);
assert!(!t.is_ready());
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80)
.map(|i| c(100.0 + (f64::from(i) * 0.25).sin() * 9.0))
.collect();
let batch = ThreeLineBreak::new(3).unwrap().batch(&candles);
let mut b = ThreeLineBreak::new(3).unwrap();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,224 @@
#![allow(clippy::doc_markdown)]
//! Tower Top / Tower Bottom — a tall bar, a pause, then a tall opposite bar.
//!
//! A Tower is a reversal where a strong directional bar is followed by a small
//! "pause" bar and then a strong bar in the *opposite* direction, like two towers
//! flanking a low wall. This is the compact three-bar form of the classic
//! multi-bar Tower pattern.
//!
//! - **Tower Bottom** (`+1.0`): a tall **bearish** bar, a small-bodied bar, then a
//! tall **bullish** bar.
//! - **Tower Top** (`-1.0`): a tall **bullish** bar, a small-bodied bar, then a
//! tall **bearish** bar.
//! - Otherwise the output is `0.0`.
//!
//! "Tall" = body `>= 0.5 * range`; "small" = body `<= 0.3 * range`. The three-bar
//! lookback means the first value lands on the third candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
fn body_fraction(candle: Candle) -> f64 {
let range = candle.high - candle.low;
if range > 0.0 {
(candle.close - candle.open).abs() / range
} else {
0.0
}
}
fn is_tall(candle: Candle) -> bool {
body_fraction(candle) >= 0.5
}
fn is_small(candle: Candle) -> bool {
body_fraction(candle) <= 0.3
}
/// Tower Top / Bottom — three-bar reversal detector.
#[derive(Debug, Clone, Default)]
pub struct TowerTopBottom {
c1: Option<Candle>,
c2: Option<Candle>,
last_value: Option<f64>,
}
impl TowerTopBottom {
/// Construct a new `TowerTopBottom`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TowerTopBottom {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let (Some(first), Some(middle)) = (self.c1, self.c2) else {
self.c1 = self.c2;
self.c2 = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let pause = is_small(middle);
let first_tall = is_tall(first);
let last_tall = is_tall(candle);
let v = if pause && first_tall && last_tall {
let first_up = first.close > first.open;
let last_up = candle.close > candle.open;
if !first_up && last_up {
1.0
} else if first_up && !last_up {
-1.0
} else {
0.0
}
} else {
0.0
};
self.c1 = self.c2;
self.c2 = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TowerTopBottom"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
/// A tall candle from `open` to `close` (body fills most of the range).
fn tall(open: f64, close: f64) -> Candle {
Candle::new_unchecked(
open,
open.max(close) + 0.1,
open.min(close) - 0.1,
close,
0.0,
0,
)
}
/// A small-bodied candle (long shadows, tiny body).
fn small(mid: f64) -> Candle {
Candle::new_unchecked(mid, mid + 2.0, mid - 2.0, mid + 0.1, 0.0, 0)
}
#[test]
fn accessors_and_metadata() {
let t = TowerTopBottom::new();
assert_eq!(t.warmup_period(), 3);
assert_eq!(t.name(), "TowerTopBottom");
assert!(!t.is_ready());
assert_eq!(t.value(), None);
}
#[test]
fn first_two_bars_seed_without_signal() {
let mut t = TowerTopBottom::new();
assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
assert_eq!(t.update(small(105.0)), Some(0.0));
assert!(t.update(tall(110.0, 100.0)).is_some());
}
#[test]
fn tower_top() {
// tall bullish, small pause, tall bearish -> top -> -1.
let mut t = TowerTopBottom::new();
t.update(tall(100.0, 110.0));
t.update(small(110.0));
assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
}
#[test]
fn tower_bottom() {
let mut t = TowerTopBottom::new();
t.update(tall(110.0, 100.0));
t.update(small(100.0));
assert_eq!(t.update(tall(100.0, 110.0)), Some(1.0));
}
#[test]
fn same_direction_is_zero() {
let mut t = TowerTopBottom::new();
t.update(tall(100.0, 110.0));
t.update(small(110.0));
// last bar also bullish -> not a tower -> 0.
assert_eq!(t.update(tall(110.0, 120.0)), Some(0.0));
}
#[test]
fn no_pause_is_zero() {
let mut t = TowerTopBottom::new();
t.update(tall(100.0, 110.0));
t.update(tall(110.0, 120.0)); // middle is tall, not a pause
assert_eq!(t.update(tall(120.0, 110.0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut t = TowerTopBottom::new();
t.update(tall(100.0, 110.0));
t.update(small(110.0));
t.update(tall(110.0, 100.0));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(tall(100.0, 110.0)), Some(0.0));
}
#[test]
fn zero_range_bar_has_zero_body_fraction() {
// A flat bar (high == low) exercises the zero-range body-fraction branch;
// it counts as a small "pause" bar, so tall-flat-tall still reverses.
fn flat(mid: f64) -> Candle {
Candle::new_unchecked(mid, mid, mid, mid, 0.0, 0)
}
let mut t = TowerTopBottom::new();
t.update(tall(100.0, 110.0));
t.update(flat(110.0));
assert_eq!(t.update(tall(110.0, 100.0)), Some(-1.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..30)
.map(|i| match i % 3 {
0 => tall(100.0, 110.0),
1 => small(110.0),
_ => tall(110.0, 100.0),
})
.collect();
let batch = TowerTopBottom::new().batch(&candles);
let mut b = TowerTopBottom::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,217 @@
//! Trade-Sign Autocorrelation — lag-1 persistence of the trade-aggressor side.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::microstructure::Trade;
use crate::traits::Indicator;
/// Trade-Sign Autocorrelation — the lag-1 autocorrelation of the **trade sign**
/// (`+1` buy, `1` sell), measuring how strongly signed order flow persists.
///
/// ```text
/// s_t = +1 if the trade is a buy, 1 if a sell
/// ρ1 = mean over the window of ( s_t · s_{t1} ) ∈ [1, +1]
/// ```
///
/// In real markets trade signs are strongly **positively** autocorrelated: a buy
/// tends to be followed by another buy (and a sell by a sell), because large
/// parent orders are split into many child trades and because of order-splitting
/// and herding. A high reading therefore indicates persistent directional pressure
/// — a footprint of informed or algorithmic execution — while a reading near zero
/// signals balanced, uninformed flow and a negative reading signals alternating
/// (bid-ask bounce) flow.
///
/// The output is the mean product of consecutive signs, bounded in `[1, +1]`. The
/// first value lands after `period` trades. Each `update` is O(`period`).
///
/// # Example
///
/// ```
/// use wickra_core::{Indicator, Side, Trade, TradeSignAutocorrelation};
///
/// let mut indicator = TradeSignAutocorrelation::new(20).unwrap();
/// let mut last = None;
/// for i in 0..40 {
/// let side = if i % 2 == 0 { Side::Buy } else { Side::Sell };
/// last = indicator.update(Trade::new(100.0, 1.0, side, i).unwrap());
/// }
/// // Perfectly alternating signs -> autocorrelation -1.
/// assert!((last.unwrap() + 1.0).abs() < 1e-9);
/// ```
#[derive(Debug, Clone)]
pub struct TradeSignAutocorrelation {
period: usize,
signs: VecDeque<f64>,
last: Option<f64>,
}
impl TradeSignAutocorrelation {
/// Construct a trade-sign autocorrelation over `period` trades.
///
/// # Errors
///
/// Returns [`Error::InvalidPeriod`] if `period < 2` (a lag-1 product needs two
/// trades).
pub fn new(period: usize) -> Result<Self> {
if period < 2 {
return Err(Error::InvalidPeriod {
message: "trade-sign autocorrelation needs period >= 2",
});
}
Ok(Self {
period,
signs: VecDeque::with_capacity(period),
last: None,
})
}
/// Configured window of trades.
pub const fn period(&self) -> usize {
self.period
}
/// Current value if available.
pub const fn value(&self) -> Option<f64> {
self.last
}
}
impl Indicator for TradeSignAutocorrelation {
type Input = Trade;
type Output = f64;
fn update(&mut self, trade: Trade) -> Option<f64> {
if self.signs.len() == self.period {
self.signs.pop_front();
}
self.signs.push_back(trade.side.sign());
if self.signs.len() < self.period {
return None;
}
let mut product_sum = 0.0;
let mut prev: Option<f64> = None;
for &s in &self.signs {
if let Some(p) = prev {
product_sum += s * p;
}
prev = Some(s);
}
let rho = product_sum / (self.period as f64 - 1.0);
self.last = Some(rho);
Some(rho)
}
fn reset(&mut self) {
self.signs.clear();
self.last = None;
}
fn warmup_period(&self) -> usize {
self.period
}
fn is_ready(&self) -> bool {
self.last.is_some()
}
fn name(&self) -> &'static str {
"TradeSignAutocorrelation"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::microstructure::Side;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
fn buy() -> Trade {
Trade::new_unchecked(100.0, 1.0, Side::Buy, 0)
}
fn sell() -> Trade {
Trade::new_unchecked(100.0, 1.0, Side::Sell, 0)
}
#[test]
fn rejects_period_below_two() {
assert!(matches!(
TradeSignAutocorrelation::new(1),
Err(Error::InvalidPeriod { .. })
));
assert!(TradeSignAutocorrelation::new(2).is_ok());
}
#[test]
fn accessors_and_metadata() {
let t = TradeSignAutocorrelation::new(20).unwrap();
assert_eq!(t.period(), 20);
assert_eq!(t.warmup_period(), 20);
assert_eq!(t.name(), "TradeSignAutocorrelation");
assert!(!t.is_ready());
assert_eq!(t.value(), None);
}
#[test]
fn first_emission_at_warmup_period() {
let mut t = TradeSignAutocorrelation::new(4).unwrap();
let out = t.batch(&[buy(), buy(), buy(), buy(), buy()]);
for v in out.iter().take(3) {
assert!(v.is_none());
}
assert!(out[3].is_some());
}
#[test]
fn persistent_flow_is_one() {
let mut t = TradeSignAutocorrelation::new(10).unwrap();
let trades: Vec<Trade> = (0..20).map(|_| buy()).collect();
let last = t.batch(&trades).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, 1.0, epsilon = 1e-12);
}
#[test]
fn alternating_flow_is_minus_one() {
let mut t = TradeSignAutocorrelation::new(10).unwrap();
let trades: Vec<Trade> = (0..20)
.map(|i| if i % 2 == 0 { buy() } else { sell() })
.collect();
let last = t.batch(&trades).into_iter().flatten().last().unwrap();
assert_relative_eq!(last, -1.0, epsilon = 1e-12);
}
#[test]
fn output_in_range() {
let mut t = TradeSignAutocorrelation::new(16).unwrap();
let trades: Vec<Trade> = (0..200)
.map(|i| if (i * 7 % 13) < 6 { buy() } else { sell() })
.collect();
for v in t.batch(&trades).into_iter().flatten() {
assert!((-1.0..=1.0).contains(&v));
}
}
#[test]
fn reset_clears_state() {
let mut t = TradeSignAutocorrelation::new(4).unwrap();
t.batch(&[buy(), buy(), buy(), buy()]);
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.value(), None);
assert_eq!(t.update(buy()), None);
}
#[test]
fn batch_equals_streaming() {
let trades: Vec<Trade> = (0..120)
.map(|i| if i % 3 == 0 { sell() } else { buy() })
.collect();
let batch = TradeSignAutocorrelation::new(16).unwrap().batch(&trades);
let mut b = TradeSignAutocorrelation::new(16).unwrap();
let streamed: Vec<_> = trades.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -0,0 +1,189 @@
#![allow(clippy::doc_markdown)]
//! Tristar — a three-doji reversal pattern.
//!
//! A Tristar is three consecutive Doji candles where the middle one gaps away
//! from its neighbours, forming a star. A bearish Tristar (top) has the middle
//! doji sitting above the other two; a bullish Tristar (bottom) has it below.
//!
//! - **Bullish** (`+1.0`): three dojis, the middle doji's body centre below both
//! neighbours' body centres.
//! - **Bearish** (`-1.0`): three dojis, the middle above both neighbours.
//! - Otherwise the output is `0.0`.
//!
//! A doji is a candle whose body is `<= 0.1 * range`. The three-bar lookback means
//! the first value lands on the third candle.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Body-centre of a candle.
fn body_mid(candle: Candle) -> f64 {
f64::midpoint(candle.open, candle.close)
}
/// Whether a candle is a doji (body small relative to range).
fn is_doji(candle: Candle) -> bool {
let body = (candle.close - candle.open).abs();
let range = candle.high - candle.low;
range > 0.0 && body <= 0.1 * range
}
/// Tristar — three-doji star reversal detector.
#[derive(Debug, Clone, Default)]
pub struct Tristar {
c1: Option<Candle>,
c2: Option<Candle>,
last_value: Option<f64>,
}
impl Tristar {
/// Construct a new `Tristar`.
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Latest emitted signal if available.
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for Tristar {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let (Some(first), Some(middle)) = (self.c1, self.c2) else {
self.c1 = self.c2;
self.c2 = Some(candle);
self.last_value = Some(0.0);
return Some(0.0);
};
let v = if is_doji(first) && is_doji(middle) && is_doji(candle) {
let mid = body_mid(middle);
let n1 = body_mid(first);
let n3 = body_mid(candle);
if mid > n1 && mid > n3 {
-1.0
} else if mid < n1 && mid < n3 {
1.0
} else {
0.0
}
} else {
0.0
};
self.c1 = self.c2;
self.c2 = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.c1 = None;
self.c2 = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
3
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"Tristar"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
/// A doji centred at `mid` (tiny body, symmetric shadows).
fn doji(mid: f64) -> Candle {
Candle::new_unchecked(mid, mid + 1.0, mid - 1.0, mid + 0.02, 0.0, 0)
}
/// A non-doji (big body).
fn solid(open: f64, close: f64) -> Candle {
Candle::new_unchecked(
open,
open.max(close) + 0.1,
open.min(close) - 0.1,
close,
0.0,
0,
)
}
#[test]
fn accessors_and_metadata() {
let t = Tristar::new();
assert_eq!(t.warmup_period(), 3);
assert_eq!(t.name(), "Tristar");
assert!(!t.is_ready());
assert_eq!(t.value(), None);
}
#[test]
fn first_two_bars_seed_without_signal() {
let mut t = Tristar::new();
assert_eq!(t.update(doji(100.0)), Some(0.0));
assert_eq!(t.update(doji(100.0)), Some(0.0));
assert!(t.update(doji(100.0)).is_some());
}
#[test]
fn bearish_tristar_top() {
// middle doji centred above the two neighbours -> top -> -1.
let mut t = Tristar::new();
t.update(doji(100.0));
t.update(doji(105.0)); // middle, highest
assert_eq!(t.update(doji(100.0)), Some(-1.0));
}
#[test]
fn bullish_tristar_bottom() {
let mut t = Tristar::new();
t.update(doji(100.0));
t.update(doji(95.0)); // middle, lowest
assert_eq!(t.update(doji(100.0)), Some(1.0));
}
#[test]
fn non_doji_is_zero() {
let mut t = Tristar::new();
t.update(doji(100.0));
t.update(solid(100.0, 110.0)); // not a doji
assert_eq!(t.update(doji(100.0)), Some(0.0));
}
#[test]
fn reset_clears_state() {
let mut t = Tristar::new();
t.update(doji(100.0));
t.update(doji(105.0));
t.update(doji(100.0));
assert!(t.is_ready());
t.reset();
assert!(!t.is_ready());
assert_eq!(t.update(doji(100.0)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| doji(100.0 + (f64::from(i) * 0.4).sin() * 5.0))
.collect();
let batch = Tristar::new().batch(&candles);
let mut b = Tristar::new();
let streamed: Vec<_> = candles.iter().map(|x| b.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
+47 -42
View File
@@ -67,32 +67,34 @@ pub use indicators::{
BandpassFilter, Bat, BeltHold, Beta, BetaNeutralSpread, BetterVolume, BipowerVariation,
BodySizePct, BollingerBands, BollingerBandwidth, BollingerOutput, BomarBands, BomarBandsOutput,
BreadthThrust, Breakaway, BullishPercentIndex, Butterfly, CalendarSpread, CalmarRatio,
Camarilla, CamarillaPivotsOutput, Cci, CenterOfGravity, CentralPivotRange,
CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility,
ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit, ChandelierExitOutput, ChoppinessIndex,
ClassicPivots, ClassicPivotsOutput, CloseVsOpen, ClosingMarubozu, Cmo, CoefficientOfVariation,
Cointegration, CointegrationOutput, ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi,
Coppock, CorrelationTrendIndicator, Counterattack, Crab, CumulativeVolumeDelta,
CumulativeVolumeIndex, CupAndHandle, CyberneticCycle, Cypher, DayOfWeekProfile,
DayOfWeekProfileOutput, Decycler, DecyclerOscillator, Dema, DemandIndex, DemarkPivots,
DemarkPivotsOutput, DepthSlope, DerivativeOscillator, DetrendedStdDev, DisparityIndex,
DistanceSsd, Doji, DojiStar, Donchian, DonchianOutput, DonchianStop, DonchianStopOutput,
DoubleBollinger, DoubleBollingerOutput, DoubleTopBottom, DownsideGapThreeMethods, Dpo,
DragonflyDoji, DrawdownDuration, Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread,
EhlersStochastic, Ehma, ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone,
ElderSafeZoneOutput, Ema, EmpiricalModeDecomposition, Engulfing, EvenBetterSinewave,
Camarilla, CamarillaPivotsOutput, CandleVolume, CandleVolumeOutput, Cci, CenterOfGravity,
CentralPivotRange, CentralPivotRangeOutput, Cfo, ChaikinMoneyFlow, ChaikinOscillator,
ChaikinVolatility, ChandeKrollStop, ChandeKrollStopOutput, ChandelierExit,
ChandelierExitOutput, ChoppinessIndex, ClassicPivots, ClassicPivotsOutput, CloseVsOpen,
ClosingMarubozu, Cmo, CoefficientOfVariation, Cointegration, CointegrationOutput,
ConcealingBabySwallow, ConditionalValueAtRisk, ConnorsRsi, Coppock, CorrelationTrendIndicator,
Counterattack, Crab, CumulativeVolumeDelta, CumulativeVolumeIndex, CupAndHandle,
CyberneticCycle, Cypher, DayOfWeekProfile, DayOfWeekProfileOutput, Decycler,
DecyclerOscillator, Dema, DemandIndex, DemarkPivots, DemarkPivotsOutput, DepthSlope,
DerivativeOscillator, DetrendedStdDev, DisparityIndex, DistanceSsd, Doji, DojiStar, Donchian,
DonchianOutput, DonchianStop, DonchianStopOutput, DoubleBollinger, DoubleBollingerOutput,
DoubleTopBottom, DownsideGapThreeMethods, Dpo, DragonflyDoji, DrawdownDuration, DumplingTop,
Dx, DynamicMomentumIndex, EaseOfMovement, EffectiveSpread, EhlersStochastic, Ehma,
ElderImpulse, ElderRay, ElderRayOutput, ElderSafeZone, ElderSafeZoneOutput, Ema,
EmpiricalModeDecomposition, Engulfing, Equivolume, EquivolumeOutput, EvenBetterSinewave,
EveningDojiStar, Evwma, EwmaVolatility, Expectancy, FallingThreeMethods, Fama, FibArcs,
FibArcsOutput, FibChannel, FibChannelOutput, FibConfluence, FibConfluenceOutput, FibExtension,
FibExtensionOutput, FibFan, FibFanOutput, FibProjection, FibProjectionOutput, FibRetracement,
FibRetracementOutput, FibTimeZones, FibTimeZonesOutput, FibonacciPivots, FibonacciPivotsOutput,
FisherRsi, FisherTransform, FlagPennant, Footprint, FootprintOutput, ForceIndex,
FractalChaosBands, FractalChaosBandsOutput, Frama, FundingBasis, FundingRate, FundingRateMean,
FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11, GarmanKlassVolatility, Gartley,
GatorOscillator, GatorOscillatorOutput, GeneralizedDema, GeometricMa, GoldenPocket,
GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer, HangingMan, Harami,
HeadAndShoulders, HeikinAshi, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange,
HighWave, HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility,
Hma, HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
FractalChaosBands, FractalChaosBandsOutput, Frama, FryPanBottom, FundingBasis, FundingRate,
FundingRateMean, FundingRateZScore, GainLossRatio, GapSideBySideWhite, Garch11,
GarmanKlassVolatility, Gartley, GatorOscillator, GatorOscillatorOutput, GeneralizedDema,
GeometricMa, GoldenPocket, GoldenPocketOutput, GrangerCausality, GravestoneDoji, Hammer,
HangingMan, Harami, HaramiCross, HasbrouckInformationShare, HeadAndShoulders, HeikinAshi,
HeikinAshiOscillator, HeikinAshiOutput, HiLoActivator, HighLowIndex, HighLowRange, HighWave,
HighpassFilter, Hikkake, HikkakeModified, HilbertDominantCycle, HistoricalVolatility, Hma,
HoltWinters, HomingPigeon, HtDcPhase, HtPhasor, HtPhasorOutput, HtTrendMode, HurstChannel,
HurstChannelOutput, HurstExponent, Ichimoku, IchimokuOutput, IdenticalThreeCrows, InNeck,
Inertia, InformationRatio, InitialBalance, InitialBalanceOutput, InstantaneousTrendline,
IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile,
@@ -109,13 +111,13 @@ pub use indicators::{
McGinleyDynamic, MedianAbsoluteDeviation, MedianChannel, MedianChannelOutput, MedianMa,
MedianPrice, Mfi, Microprice, MidPoint, MidPrice, MinusDi, MinusDm, ModifiedMaStop,
ModifiedMaStopOutput, Mom, MorningDojiStar, MorningEveningStar, MurreyMathLines,
MurreyMathLinesOutput, Natr, NewHighsNewLows, Nrtr, NrtrOutput, Nvi, OIPriceDivergence,
OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu, OpeningRange,
OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1, OrderBookImbalanceTopN,
OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
MurreyMathLinesOutput, Natr, NewHighsNewLows, NewPriceLines, Nrtr, NrtrOutput, Nvi,
OIPriceDivergence, OIWeighted, Obv, OmegaRatio, OnNeck, OpenInterestDelta, OpeningMarubozu,
OpeningRange, OpeningRangeOutput, OrderBookImbalanceFull, OrderBookImbalanceTop1,
OrderBookImbalanceTopN, OrderFlowImbalance, OuHalfLife, OvernightGap, OvernightIntradayReturn,
OvernightIntradayReturnOutput, PainIndex, PairSpreadZScore, PairwiseBeta, ParkinsonVolatility,
PearsonCorrelation, PercentAboveMa, PercentB, PercentageTrailingStop, Pgo, PiercingDarkCloud,
PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
Pin, PivotReversal, PlusDi, PlusDm, Pmo, PointAndFigureBars, PolarizedFractalEfficiency, Ppo,
PpoHistogram, ProfitFactor, ProjectionBands, ProjectionBandsOutput, ProjectionOscillator, Psar,
Pvi, Qqe, QqeOutput, Qstick, QuartileBands, QuartileBandsOutput, QuotedSpread, RSquared,
RealizedSpread, RealizedVolatility, RecoveryFactor, RectangleRange, Reflex, RegimeLabel,
@@ -126,23 +128,26 @@ pub use indicators::{
SampleEntropy, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionHighLowOutput,
SessionRange, SessionRangeOutput, SessionVwap, ShannonEntropy, Shark, SharpeRatio,
ShootingStar, ShortLine, SignedVolume, SineWave, SineWeightedMa, Skewness, Sma, Smi, Smma,
SortinoRatio, SpearmanCorrelation, SpinningTop, SpreadAr1Coefficient, SpreadBollingerBands,
SpreadBollingerBandsOutput, SpreadHurst, StalledPattern, StandardError, StandardErrorBands,
StandardErrorBandsOutput, StarcBands, StarcBandsOutput, Stc, StdDev, StepTrailingStop,
StickSandwich, StochRsi, Stochastic, StochasticCci, StochasticOutput, SuperSmoother,
SuperTrend, SuperTrendOutput, TakerBuySellRatio, Takuri, TasukiGap, TdCombo, TdCountdown,
TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdOpen, TdPressure, TdRangeProjection,
SmoothedHeikinAshi, SmoothedHeikinAshiOutput, SortinoRatio, SpearmanCorrelation, SpinningTop,
SpreadAr1Coefficient, SpreadBollingerBands, SpreadBollingerBandsOutput, SpreadHurst,
StalledPattern, StandardError, StandardErrorBands, StandardErrorBandsOutput, StarcBands,
StarcBandsOutput, Stc, StdDev, StepTrailingStop, StickSandwich, StochRsi, Stochastic,
StochasticCci, StochasticOutput, SuperSmoother, SuperTrend, SuperTrendOutput,
TakerBuySellRatio, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown,
TdDWave, TdDeMarker, TdDifferential, TdLines, TdLinesOutput, TdMovingAverage,
TdMovingAverageOutput, TdOpen, TdPressure, TdPropulsion, TdRangeProjection,
TdRangeProjectionOutput, TdRei, TdRiskLevel, TdRiskLevelOutput, TdSequential,
TdSequentialOutput, TdSetup, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TickIndex,
Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput, TpoProfile,
TpoProfileOutput, TradeImbalance, TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex,
TreynorRatio, Triangle, Trima, Trin, TripleTopBottom, Trix, TrueRange, Tsf, TsfOscillator, Tsi,
Tsv, TtmSqueeze, TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows,
TypicalPrice, UlcerIndex, UltimateOscillator, UniqueThreeRiver, UniversalOscillator,
UpDownVolumeRatio, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput,
ValueAtRisk, Variance, VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone,
VolatilityConeOutput, VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
TdSequentialOutput, TdSetup, TdTrap, Tema, TermStructureBasis, ThreeDrives, ThreeInside,
ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth,
Thrusting, TickIndex, Tii, TimeBasedStop, TimeOfDayReturnProfile, TimeOfDayReturnProfileOutput,
TowerTopBottom, TpoProfile, TpoProfileOutput, TradeImbalance, TradeSignAutocorrelation,
TradeVolumeIndex, TrendLabel, TrendStrengthIndex, Trendflex, TreynorRatio, Triangle, Trima,
Trin, TripleTopBottom, Tristar, Trix, TrueRange, Tsf, TsfOscillator, Tsi, Tsv, TtmSqueeze,
TtmSqueezeOutput, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice,
UlcerIndex, UltimateOscillator, UniqueThreeRiver, UniversalOscillator, UpDownVolumeRatio,
UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, ValueAreaOutput, ValueAtRisk, Variance,
VarianceRatio, VerticalHorizontalFilter, Vidya, VolatilityCone, VolatilityConeOutput,
VolatilityOfVolatility, VolatilityRatio, VoltyStop, VolumeByTimeProfile,
VolumeByTimeProfileOutput, VolumeOscillator, VolumePriceTrend, VolumeProfile,
VolumeProfileOutput, VolumeRsi, VolumeWeightedMacd, VolumeWeightedMacdOutput, VolumeWeightedSr,
VolumeWeightedSrOutput, Vortex, VortexOutput, Vpin, Vwap, VwapStdDevBands,
+1 -1
View File
@@ -8,7 +8,7 @@ That includes:
[Python](https://docs.wickra.org/Quickstart-Python),
[Node](https://docs.wickra.org/Quickstart-Node), and
[WASM](https://docs.wickra.org/Quickstart-WASM).
- A per-indicator deep dive for every one of the **467 indicators** across
- A per-indicator deep dive for every one of the **488 indicators** across
the sixteen families (Moving Averages, Momentum Oscillators, Trend &
Directional, Price Oscillators, Volatility & Bands, Bands & Channels,
Trailing Stops, Volume, Price Statistics, Ehlers / Cycle DSP, Pivots &
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.6.6",
"version": "0.7.0",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.6.6",
"wickra-darwin-x64": "0.6.6",
"wickra-linux-arm64-gnu": "0.6.6",
"wickra-linux-x64-gnu": "0.6.6",
"wickra-win32-arm64-msvc": "0.6.6",
"wickra-win32-x64-msvc": "0.6.6"
"wickra-darwin-arm64": "0.7.0",
"wickra-darwin-x64": "0.7.0",
"wickra-linux-arm64-gnu": "0.7.0",
"wickra-linux-x64-gnu": "0.7.0",
"wickra-win32-arm64-msvc": "0.7.0",
"wickra-win32-x64-msvc": "0.7.0"
}
},
"node_modules/wickra": {
+21 -1
View File
@@ -22,7 +22,7 @@
//! WeightedClose.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HeadAndShoulders, HeikinAshi, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCombo, TdCountdown, TdDeMarker, TdDifferential, TdLines, TdOpen, TdPressure, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, ThreeDrives, ThreeInside, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
use wickra_core::{AbandonedBaby, Abcd, AccelerationBands, AcceleratorOscillator, AdOscillator, AdaptiveCci, Adl, AdvanceBlock, Adx, Adxr, Alligator, AnchoredVwap, AndrewsPitchfork, Aroon, AroonOscillator, Atr, AtrBands, AtrRatchet, AtrTrailingStop, AutoFib, AverageDailyRange, AwesomeOscillator, AwesomeOscillatorHistogram, BalanceOfPower, Bat, BatchExt, BeltHold, BetterVolume, BodySizePct, Breakaway, Butterfly, Camarilla, Candle, CandleVolume, Cci, CentralPivotRange, ChaikinMoneyFlow, ChaikinOscillator, ChaikinVolatility, ChandeKrollStop, ChandelierExit, ChoppinessIndex, ClassicPivots, CloseVsOpen, ClosingMarubozu, ConcealingBabySwallow, Counterattack, Crab, CupAndHandle, Cypher, DayOfWeekProfile, DemandIndex, DemarkPivots, Doji, DojiStar, Donchian, DonchianStop, DoubleTopBottom, DownsideGapThreeMethods, DragonflyDoji, DumplingTop, Dx, EaseOfMovement, ElderRay, ElderSafeZone, Engulfing, Equivolume, EveningDojiStar, Evwma, FallingThreeMethods, FibArcs, FibChannel, FibConfluence, FibExtension, FibFan, FibProjection, FibRetracement, FibTimeZones, FibonacciPivots, FlagPennant, ForceIndex, FractalChaosBands, FryPanBottom, GapSideBySideWhite, GarmanKlassVolatility, Gartley, GatorOscillator, GoldenPocket, GravestoneDoji, Hammer, HangingMan, Harami, HaramiCross, HeadAndShoulders, HeikinAshi, HeikinAshiOscillator, HiLoActivator, HighLowRange, HighWave, Hikkake, HikkakeModified, HomingPigeon, HurstChannel, Ichimoku, IdenticalThreeCrows, InNeck, Indicator, Inertia, InitialBalance, IntradayIntensity, IntradayMomentumIndex, IntradayVolatilityProfile, InvertedHammer, KaseDevStop, KasePermissionStochastic, Keltner, Kicking, KickingByLength, Kvo, LadderBottom, LongLeggedDoji, LongLine, MarketFacilitationIndex, Marubozu, MassIndex, MatHold, MatchingLow, AvgPrice, MedianPrice, Mfi, MidPrice, MinusDi, MinusDm, ModifiedMaStop, MorningDojiStar, MorningEveningStar, MurreyMathLines, Natr, NewPriceLines, Nrtr, Nvi, Obv, OnNeck, OpeningMarubozu, OpeningRange, OvernightGap, OvernightIntradayReturn, ParkinsonVolatility, Pgo, PiercingDarkCloud, PivotReversal, PlusDi, PlusDm, ProjectionBands, ProjectionOscillator, Psar, Pvi, Qstick, RectangleRange, RickshawMan, RisingThreeMethods, RogersSatchellVolatility, RollingVwap, Rvi, Rwi, SarExt, SeasonalZScore, SeparatingLines, SessionHighLow, SessionRange, SessionVwap, Shark, ShootingStar, ShortLine, Smi, SmoothedHeikinAshi, SpinningTop, StalledPattern, StarcBands, StickSandwich, Stochastic, StochasticCci, SuperTrend, Takuri, TasukiGap, TdCamouflage, TdClop, TdClopwin, TdCombo, TdCountdown, TdDWave, TdDeMarker, TdDifferential, TdLines, TdMovingAverage, TdOpen, TdPressure, TdPropulsion, TdRangeProjection, TdRei, TdRiskLevel, TdSequential, TdSetup, TdTrap, ThreeDrives, ThreeInside, ThreeLineBreak, ThreeLineStrike, ThreeOutside, ThreeSoldiersOrCrows, ThreeStarsInSouth, Thrusting, TimeBasedStop, TimeOfDayReturnProfile, TowerTopBottom, TpoProfile, TradeVolumeIndex, Triangle, TripleTopBottom, Tristar, TrueRange, Tsv, TtmSqueeze, TtmTrend, TurnOfMonth, Tweezer, TwiggsMoneyFlow, TwoCrows, TypicalPrice, UltimateOscillator, UniqueThreeRiver, UpsideGapThreeMethods, UpsideGapTwoCrows, ValueArea, VolatilityCone, VolatilityRatio, VoltyStop, VolumeByTimeProfile, VolumeOscillator, VolumePriceTrend, VolumeProfile, VolumeRsi, VolumeWeightedMacd, VolumeWeightedSr, Vortex, Vwap, VwapStdDevBands, Vwma, Vzo, WaveTrend, Wedge, WeightedClose, WickRatio, Wad, WilliamsFractals, WilliamsR, WoodiePivots, YangZhangVolatility, YoyoExit, ZigZag};
/// Convert a flat `f64` stream into a `Vec<Candle>` by chunking it into
/// `[open, high, low, close, volume]` groups. Tuples that fail OHLCV
@@ -315,6 +315,26 @@ fuzz_target!(|data: Vec<f64>| {
}
// --- Candlestick Patterns (family 14) ---
drive(TowerTopBottom::new, &candles);
drive(HaramiCross::new, &candles);
drive(Tristar::new, &candles);
drive(|| FryPanBottom::new(9).unwrap(), &candles);
drive(|| DumplingTop::new(9).unwrap(), &candles);
drive(|| NewPriceLines::new(5).unwrap(), &candles);
drive(TdTrap::new, &candles);
drive(TdPropulsion::new, &candles);
drive(TdClopwin::new, &candles);
drive(TdClop::new, &candles);
drive(TdCamouflage::new, &candles);
drive(|| TdDWave::new(2).unwrap(), &candles);
drive(|| TdMovingAverage::new(5, 13).unwrap(), &candles);
// --- Ichimoku & Charts ---
drive(|| HeikinAshiOscillator::new(5).unwrap(), &candles);
drive(|| ThreeLineBreak::new(3).unwrap(), &candles);
drive(|| SmoothedHeikinAshi::new(5).unwrap(), &candles);
drive(|| Equivolume::new(20).unwrap(), &candles);
drive(|| CandleVolume::new(20).unwrap(), &candles);
drive(ConcealingBabySwallow::new, &candles);
drive(UniqueThreeRiver::new, &candles);
drive(TasukiGap::new, &candles);
+2 -1
View File
@@ -8,7 +8,7 @@
//! panic.
use libfuzzer_sys::fuzz_target;
use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
use wickra_core::{Alpha, BatchExt, BetaNeutralSpread, Cointegration, DistanceSsd, GrangerCausality, HasbrouckInformationShare, Indicator, InformationRatio, KalmanHedgeRatio, KendallTau, LeadLagCrossCorrelation, OuHalfLife, PairSpreadZScore, PairwiseBeta, RelativeStrengthAB, RollingCorrelation, RollingCovariance, SpreadAr1Coefficient, SpreadBollingerBands, SpreadHurst, TreynorRatio, VarianceRatio};
#[inline(never)]
fn drive<I>(make: impl Fn() -> I, data: &[(f64, f64)])
@@ -48,6 +48,7 @@ fuzz_target!(|data: &[u8]| {
drive(|| GrangerCausality::new(60, 1).unwrap(), &pairs);
drive(|| SpreadAr1Coefficient::new(40).unwrap(), &pairs);
drive(|| KendallTau::new(20).unwrap(), &pairs);
drive(|| HasbrouckInformationShare::new(2).unwrap(), &pairs);
// Struct-output pair indicator: drive update + batch directly (the generic
// `drive` above only covers `Output = f64`).
+3 -1
View File
@@ -10,7 +10,7 @@
//! would reject — the indicators must never panic, streaming or batched.
use libfuzzer_sys::fuzz_target;
use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, Vpin};
use wickra_core::{AmihudIlliquidity, BatchExt, CumulativeVolumeDelta, Footprint, Indicator, Pin, RollMeasure, Side, SignedVolume, Trade, TradeImbalance, TradeSignAutocorrelation, Vpin};
#[inline(never)]
fn drive<I>(make: impl Fn() -> I, trades: &[Trade])
@@ -43,6 +43,8 @@ fuzz_target!(|data: &[u8]| {
drive(|| Vpin::new(8.0, 5).unwrap(), &trades);
drive(|| AmihudIlliquidity::new(20).unwrap(), &trades);
drive(|| RollMeasure::new(20).unwrap(), &trades);
drive(|| TradeSignAutocorrelation::new(20).unwrap(), &trades);
drive(|| Pin::new(20).unwrap(), &trades);
// Footprint emits a variable-length `FootprintOutput` rather than an `f64`,
// so it is driven directly rather than through the scalar-output helper.