Version bump **0.7.3 → 0.7.4** for the B19 Alt-Chart Bars batch (7 new bar builders, 507 → 514).
Bumps `Cargo.toml` (+ `wickra-core` dep), `Cargo.lock`, `pyproject.toml`, the Node `package.json` and its six platform packages, both `package-lock.json` files, and the CHANGELOG (`[Unreleased]` → `[0.7.4]` with compare URLs).
Version bump **0.7.2 → 0.7.3** for the B18 Risk / Performance batch (9 new indicators, 498 → 507).
Bumps `Cargo.toml` (+ `wickra-core` dep), `Cargo.lock`, `pyproject.toml`, the Node `package.json` and its six platform packages, both `package-lock.json` files, and the CHANGELOG (`[Unreleased]` → `[0.7.3]` with compare URLs).
## 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.
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).
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.
## Summary
- Dedicated batch fast paths for **EMA, RSI, Bollinger, MACD and ATR** (used by the Python bindings): one allocation filled in a single pass, warmup encoded as `NaN`, no per-element `Option` or input re-validation. Each is **bit-for-bit equal** to replaying `update` — SMA/Bollinger keep the drift-reseed cadence, the EMA-family keep the seed division and `mul_add` recurrences. Adds the `BatchNanExt` extension trait.
- **Cross-library benchmark refresh**: `compare_libraries.py` reports the median across timing rounds (`--rounds` / `--streaming-rounds`), gains `--skip-batch` / `--skip-streaming`, and runs every peer through the streaming arena (recompute for batch-only libraries). `wickra-bench` drives the batch fast paths against `kand`.
- **README** benchmark section reordered streaming-first (the order-of-magnitude result), with measured TA-Lib/tulipy/pandas-ta numbers in place of the CI-only placeholders.
## Impact
- Python batch ~2× faster on EMA/RSI/MACD/ATR; streaming path unchanged.
- The `batch == streaming` equivalence stays bit-exact.
## Verification
- `cargo fmt` · `cargo clippy --workspace --all-targets --all-features -- -D warnings` (clean)
- `cargo test --workspace --all-features` — 3782 unit + 420 doc tests pass
- Python `pytest` — streaming-vs-batch, known-values, input-validation, smoke pass
## Notes
- Node/WASM bindings keep their existing batch; the fast paths are Python-only for now.
Adds five support/resistance and pivot indicators, growing the catalog 462 -> 467.
## Indicators
- **CentralPivotRange** (Candle -> struct) — the classic pivot `(H+L+C)/3` flanked by two central levels (TC/BC); range width gauges trending vs balanced days.
- **MurreyMathLines** (Candle -> struct) — T. H. Murrey's eighths grid over a rolling high-low frame; nine levels (0/8 .. 8/8) acting as support/resistance.
- **AndrewsPitchfork** (Candle -> struct) — median line and two parallels projected forward from the last three auto-detected swing pivots (symmetric fractal of half-width `strength`).
- **VolumeWeightedSr** (Candle -> struct) — a band whose edges are the volume-weighted average of recent highs (resistance) and lows (support); falls back to equal weighting when window volume is zero.
- **PivotReversal** (Candle -> f64) — a `+1`/`-1` breakout signal fired on the bar where price closes through the most recently confirmed swing pivot.
## Wiring
Core structs with branch-complete unit tests, Python/Node/WASM bindings, fuzz drives, reference + streaming-vs-batch tests, README + docs counter sync (FAMILIES "Pivots & S/R"), and CHANGELOG entries.
Verified locally: `cargo fmt`, `cargo test -p wickra-core` (3798 lib + 425 doc), `cargo clippy --workspace --all-targets --all-features -D warnings`, `npm run build && npm test` (542), `maturin develop` + `pytest` (891).
Version bump for the **v0.6.5** release shipping the **B10 Ehlers / Cycle** family (#199): 452 -> 462 indicators. Bumps workspace + Python/Node/WASM package versions, lockfiles and CHANGELOG. No code changes.
Version bump for the **v0.6.4** release shipping the **B9 Price Statistics** family (#197): 447 -> 452 indicators. Bumps workspace + Python/Node/WASM package versions, lockfiles and CHANGELOG. No code changes.
Deepens the **Price Statistics** family (B9) with five rolling-statistics indicators (447 -> 452):
- **ShannonEntropy** — Shannon entropy of a binned rolling value distribution.
- **SampleEntropy** — Richman-Moorman sample entropy (regularity/complexity of a window).
- **KendallTau** — Kendall rank correlation (tau-b) over paired observations (pairwise; distinct from Pearson/Spearman).
- **JarqueBera** — Jarque-Bera normality test statistic over a rolling window.
- **RollingMinMaxScaler** — maps the latest value to 0..1 over a rolling window.
All scalar f64 input except KendallTau (pairwise). Multi-arg scalars (Shannon/Sample entropy) use hand-written Python/Node bindings + the variadic wasm macro; KendallTau uses the pair macros. Verified locally: 3668 core lib + 410 doc tests, clippy clean, 527 node tests, 871 pytest, counter 452.
Version bump for the **v0.6.3** release shipping the **B8 Volume** family (#195): 440 -> 447 indicators. Bumps workspace + Python/Node/WASM package versions, lockfiles and CHANGELOG. No code changes.
Deepens the **Volume** family (B8) with seven indicators (440 -> 447):
- **VolumeRsi** — Wilder RSI computed on signed volume flow.
- **WilliamsAd** — Williams Accumulation/Distribution cumulative line (distinct from Chaikin A/D).
- **TwiggsMoneyFlow** — true-range volume accumulation with Wilder smoothing (distinct from CMF).
- **TradeVolumeIndex** — tick-direction volume accumulation past a min-tick threshold (distinct from TSV).
- **IntradayIntensity** — volume weighted by close position within the bar range.
- **BetterVolume** — VSA volume-vs-spread effort/result classifier.
- **VolumeWeightedMacd** — MACD computed on VWMA with signal line and histogram (struct output).
("Up/Down Volume Ratio" already ships from A2.) All Candle input; the six scalar stops emit f64, VolumeWeightedMacd a {macd, signal, histogram} struct. Hand-written Python/Node/WASM bindings for the volume signature. Verified locally: 3620 core lib + 405 doc tests, clippy clean, 522 node tests, 865 pytest, counter 447.
Version bump for the **v0.6.2** release shipping the **B7 Trailing Stops** family (#193): 434 -> 440 indicators.
Bumps workspace + Python/Node/WASM package versions, lockfiles and CHANGELOG (cuts the `[0.6.2]` section). No code changes.
Adds the **Trailing Stops** family deepening (B7), six new indicators (434 -> 440):
- **KaseDevStop** — Cynthia Kase's volatility stop on the standard deviation of the two-bar true range.
- **ElderSafeZone** — Alexander Elder's stop offset by a multiple of average market noise.
- **AtrRatchet** — Kaufman ATR ratchet that tightens its multiple by a per-bar increment.
- **Nrtr** — Nick Rypock Trailing Reverse (percentage band).
- **TimeBasedStop** — exits after a fixed number of bars (scalar fraction of elapsed life).
- **ModifiedMaStop** — moving-average based trailing stop.
("Wilder Volatility System" is intentionally skipped — it overlaps the existing VoltyStop/Psar/SarExt.)
Each takes Candle input; the five band/structure stops emit a {value, direction} struct, TimeBasedStop a scalar. Wired across core, Python/Node/WASM bindings, fuzz target and tests. Verified locally: 3560 core lib + 398 doc tests, clippy clean, 515 node tests, 852 pytest, counter 440.
Version bump for the **v0.6.0** release (ships the B5 Volatility & Bands batch, #189 — 423 -> 429 indicators).
Bumps version strings across Cargo workspace, pyproject, node package.json + 6 platform packages, both package-lock.json files, and Cargo.lock; CHANGELOG `[Unreleased]` -> `[0.6.0]`. No code changes.
Versioning note: patch never reaches two digits — `0.5.9` rolls to the next minor `0.6.0` (not 0.5.10).
Adds six **Volatility & Bands** indicators (Part B5 of the expansion roadmap), 423 → 429.
| Indicator | Input → Output | Summary |
|-----------|----------------|---------|
| `EwmaVolatility` | `f64` → `f64` | RiskMetrics exponentially-weighted volatility (λ decay) |
| `Garch11` | `f64` → `f64` | GARCH(1,1) conditional volatility with a long-run-variance anchor |
| `BipowerVariation` | `f64` → `f64` | jump-robust realized bipower variation (π/2 · Σ\|rₜ\|\|rₜ₋₁\|) |
| `VolatilityRatio` | `Candle` → `f64` | Schwager's true range over the EMA of prior true ranges (>2 = wide-ranging day) |
| `VolatilityCone` | `Candle` → `VolatilityConeOutput` | current realized volatility within its min/median/max envelope + percentile |
| `VolatilityOfVolatility` | `f64` → `f64` | sample stddev of a rolling realized-volatility series |
### Notes
- Two B5 roadmap items were dropped as duplicates/by-construction: `RealizedVolatility` already ships (v0.5.4); `Downside Semi-Deviation` is internal to Sortino. `Bipower Variation` confirmed distinct from `JumpIndicator` (a ±1 flag, not a variance measure).
- `VolatilityRatio` implements the widely-charted EMA-of-true-range convention (denominator excludes the current bar so the 2.0 threshold means "twice typical"), distinct from the existing pairwise `variance_ratio`.
- `Garch11` mean-reverts to `ω/(1−β)` on a flat series (does not decay to 0 like EWMA) — pinned by a dedicated test.
### Coverage / verification
- Full core + Python/Node/WASM bindings, fuzz drivers (scalar + candle), registries, CHANGELOG, README + docs counter sync.
- 100% unit-test coverage per indicator (every branch).
- Green locally: `cargo clippy --workspace --all-targets --all-features -D warnings`, core lib (3479) + doc (387), node (504), python (830).
Deep-dive docs for all six are staged for `wickra-docs` and pushed after release (gated).
Add a dark-mode star-history chart under the README footer thank-you line (all existing badges kept), and make sync-about also patch the indicator count into wickra-docs .vitepress/config.ts.
Patch release: streaming/batch perf (SMA, Bollinger, RSI, EMA, ATR; outputs unchanged), cross-library benchmark harness, honest tiered README. No new indicators, no API changes.
## Summary
An honest, tiered cross-library benchmark — and the optimization pass it triggered.
### Performance (wickra-core, outputs unchanged)
Profiling against the other Rust TA crates exposed real inefficiencies. Each
benchmarked indicator is now **5–79% faster** in both streaming and batch:
- **SMA, Bollinger**: flat `Box<[f64]>` ring buffers replace `VecDeque` (−69…79%).
- **RSI**: `100·ag/(ag+al)` collapses three divisions into one; Wilder smoothing
hoists `1/period` out of the hot path (−46%).
- **ATR**: reciprocal hoisted (−42%).
- **EMA/RSI/ATR**: per-tick `Option<f64>` hot state → bare `f64` + ready flag.
Net result vs `kand`: Wickra now wins **RSI, Bollinger and ATR** (streaming), and
ties `ta-rs` on SMA — up from losing every indicator 1.5–6× before.
### Benchmark harness
New `crates/wickra-bench` (publish=false): a Criterion benchmark comparing Wickra
against `kand`, `ta-rs` and `yata` on an identical BTCUSDT candle series, in
streaming and batch modes. Peer APIs were verified against their source, not
guessed. Wired into the nightly `cross-library-bench` workflow as a separate job.
### Honest README
The benchmark section is rewritten into three layered tables (Rust core vs Rust
crates; Python vs the Python ecosystem) that **show the losses as well as the
wins**. The "only library that combines…" claim is gone; the new framing is
breadth + multi-language reach + the deliberate safety trade-off that costs raw
speed. Added an origin/why-slower rationale and a star CTA.
### Python benchmark
Added `tulipy` runners and expanded per-tick streaming coverage to SMA/EMA/RSI/
MACD/Bollinger. `bench.in`/`bench.txt` now lock `TA-Lib` + `tulipy` (hash-pinned);
`pandas-ta` stays out (it requires Python ≥ 3.12, the bench runs on 3.11).
### Notes
- TA-Lib/tulipy numbers in the README Python table are marked ⧗ — they are
produced by the CI Linux job (C extensions don't build cleanly on every
desktop), not measured locally.
- The matching `wickra-docs` prose update is committed separately and will be
pushed with the release, per the docs-don't-lead-the-registries rule.
Verified locally: `cargo fmt`, `cargo test --workspace --all-features` (3413 core
+ bindings), `cargo clippy --workspace --all-targets --all-features -D warnings`,
Node build + 498 tests, and pytest all green.
Adds three **Price Oscillators** family indicators (420 → 423).
## Indicators
- **TsfOscillator** — `100·(close − TSF)/close`, the percentage gap of the close to the **one-bar-ahead** time-series forecast. Close-relative companion to `Cfo`, which measures the same gap against the regression value at the *current* bar; the two differ by exactly the slope term `100·b/close`.
- **MacdHistogram** — the standalone `macd − signal` bar of MACD exposed as a plain `f64` series.
- **PpoHistogram** — the Percentage Price Oscillator with its 9-period signal EMA and the resulting scale-free, zero-centered histogram (PPO itself only emits the line).
All three are scalar `f64` indicators wrapping existing, already-tested building blocks (`MacdIndicator`, `Ppo` + `Ema`, `Tsf`).
## Scope notes (VORAB-CHECK)
The B4 roadmap listed six items; three were dropped to avoid duplicates:
- *Forecast Oscillator* already ships as `Cfo`.
- *Derivative Oscillator* already ships (`DerivativeOscillator`, B2).
- *Detrended Synthetic Price* deferred — no citable formula distinct from the existing `Apo`/`Dpo`.
## Touchpoints
Core (`tsf_oscillator.rs`, `macd_histogram.rs`, `ppo_histogram.rs`) with full per-branch unit tests, `mod.rs`/`lib.rs`, python/node/wasm bindings (wasm via typed-arg macro, python/node hand-written for the multi-arg histograms), fuzz drivers, python reference + streaming-vs-batch tests, node factories, README family row + counter, CHANGELOG.
Local verify: `cargo test --workspace` green, `clippy -D warnings` clean, node 498 tests, full python suite green.
The two market-profile struct-output indicators from the B3 batch (`GatorOscillator`, `KasePermissionStochastic`) exposed their public output structs from their own modules but did not re-export them from `indicators` / the crate root — unlike every other struct-output indicator (`ElderRayOutput`, `AlligatorOutput`, `QqeOutput`, …).
That left `wickra::GatorOscillatorOutput` / `wickra::KasePermissionStochasticOutput` un-nameable, so Rust callers could not annotate or store the `update` result by type. Surfaced by the wickra-docs Rust-snippet-compile check on the B3 deep-dives.
Re-export both alongside their structs. Both names end in `Output`, so the indicator counter strips them — catalog count stays **420**.
Version bump 0.5.6 → 0.5.7 for the **B3 — Trend & Directional** batch (#181):
seven new indicators (`Qstick`, `TtmTrend`, `TrendStrengthIndex`,
`PolarizedFractalEfficiency`, `WavePm`, `GatorOscillator`,
`KasePermissionStochastic`), catalog 413 → 420.
Bumps: workspace `Cargo.toml` + `Cargo.lock`, `bindings/python/pyproject.toml`,
`bindings/node/package.json` + the six `npm/*/package.json` platform manifests,
both `package-lock.json` files, and the `CHANGELOG.md` `[Unreleased]` → `[0.5.7]`
roll with compare URLs.
Release bump `0.5.5 → 0.5.6` for the Momentum Oscillators family deepening
(#179): ten new indicators (DisparityIndex, FisherRsi, Rmi, DerivativeOscillator,
Rsx, DynamicMomentumIndex, IntradayMomentumIndex, StochasticCci, ElderRay, Qqe),
counter now 413.
Version strings only across all manifests + lockfiles; CHANGELOG `[Unreleased]`
rolled to `[0.5.6] - 2026-06-04` with the new compare links.
Release bump `0.5.4 → 0.5.5` for the Moving Averages family deepening
(#177): seven new indicators (`SineWeightedMa`, `GeometricMa`, `Ehma`,
`MedianMa`, `AdaptiveLaguerreFilter`, `GeneralizedDema`, `HoltWinters`),
counter now 403.
Version strings only across all manifests + lockfiles; CHANGELOG `[Unreleased]`
rolled to `[0.5.5] - 2026-06-04` with the new compare links.
Deepens the **Moving Averages** family with seven widely-used variants
(396 → 403 indicators), the first batch of Part B (family deepening).
All are scalar `f64 → f64`:
| Indicator | Binding | Notes |
|-----------|---------|-------|
| `SineWeightedMa` | `SWMA` | symmetric half-cycle sine-weighted window |
| `GeometricMa` | `GMA` | rolling geometric mean (log-space average) |
| `Ehma` | `EHMA` | exponential Hull MA (Hull construction over EMAs) |
| `MedianMa` | `MedianMA` | rolling median, robust to single outliers |
| `AdaptiveLaguerreFilter` | `AdaptiveLaguerre` | Ehlers' adaptive Laguerre filter (median-of-normalised-error γ) |
| `GeneralizedDema` | `GD` | Tillson's volume-factor double EMA; `v=1` is DEMA, `v=0` is EMA |
| `HoltWinters` | `HoltWinters` | Holt's linear double exponential smoothing (level + trend) |
LSMA was dropped from the planned set: it already ships as `LinearRegression`
(TA-Lib `LINEARREG`, the rolling least-squares endpoint).
The five single-period filters use the generated scalar macro bindings;
`GeneralizedDema` (period, v) and `HoltWinters` (alpha, beta) use hand-written
node/python bindings with the typed wasm macro (precedent `T3` / `Alma`).
Full coverage: core modules with per-branch unit tests (100% intent), mod/lib
catalogue, FAMILIES group + assert, README + docs counters, CHANGELOG, all three
bindings (regenerated `index.d.ts` / `index.js`), fuzz drivers, and the
python/node test registries.
Local verification: `cargo test -p wickra-core` (lib 3255 + doc 361),
`cargo clippy --workspace --all-targets --all-features -D warnings` clean,
node `npm run build && npm test` (478), python `pytest` (791).
Adds 19 streaming indicators so an external trading-bot feature extractor can replace its hand-built features with native, batch/streaming-equivalent ones. Each is a real gap (verified against the existing catalogue), production-only, with full Python/Node/WASM bindings, fuzz drivers, and tests. Five commits, one per family group; counter 377 -> 396.
## What's added
**Price Statistics (6)** — `LogReturn`, `RealizedVolatility` (raw quadratic variation, the un-annualised counterpart to `HistoricalVolatility`), `RollingQuantile`, `RollingIqr`, `RollingPercentileRank`, `SpreadAr1Coefficient` (pairwise AR(1) rho of the spread; complements `OuHalfLife`).
**Price Action (4)** — `CloseVsOpen`, `BodySizePct`, `WickRatio`, `HighLowRange` (stateless per-bar OHLC transforms).
**Regime / Trend / Jump labels (3)** — `TrendLabel` (sign of the rolling OLS slope), `JumpIndicator` (return outliers vs trailing volatility, measured as deviation from the trailing mean so steady drift is not flagged), `RegimeLabel` (volatility-quantile regime split).
**Risk / Performance (2)** — `WinRate`, `Expectancy` (R-multiple).
**Microstructure (4)** — `OrderFlowImbalance` (Cont-Kukanov-Stoikov OFI), `Vpin`, `AmihudIlliquidity`, `RollMeasure`. These reuse the existing `OrderBook` / `Trade` inputs (no new input type).
## Intentionally NOT added (already present, would be duplicates)
- **Population skew / kurtosis** — `skewness.rs` / `kurtosis.rs` are already population moments (divisor n).
- **Hurst R/S** — `hurst_exponent.rs` already uses rescaled-range (R/S) analysis.
- **Queue Imbalance** — exactly `OrderBookImbalanceTop1` ((bidSize - askSize) / (bidSize + askSize)).
## Verification
`cargo test -p wickra-core` (lib 3187 + doc 354), `cargo clippy --workspace --all-targets --all-features -D warnings` clean, node `npm run build && npm test` (471), python `pytest` (784). Counter consistent across `mod.rs`, lib block, README, and docs/README at 396.
Completes the **Fibonacci** family with the four geometric/time tools (catalogue 373 -> 377). All extend the internal `pattern_swing` ZigZag tracker with a per-pivot bar index and a current-bar counter (additive — the chart/harmonic detectors are unaffected), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings.
| Tool | Output |
|------|--------|
| `FibFan` | three trendlines fanning from a swing start through its 38.2/50/61.8% retracement levels, extended to the current bar |
| `FibArcs` | semicircular retracement levels centred on the swing end, normalised by the leg's bar-width (chart-scale-free) |
| `FibChannel` | a sloped base trendline plus parallel lines at Fibonacci multiples of the channel width |
| `FibTimeZones` | markers at Fibonacci bar-distances (1/2/3/5/8/...) from the latest swing pivot |
The geometric tools are novel as streaming indicators; each normalises its geometry to the swing leg's bar-width so the output is chart-scale-free. Formulas are documented in each module and deep-dive.
Fully wired: core (100% unit-tested branches incl. the new `pattern_swing` bar tracking), Python/Node/WASM struct bindings, fuzz, reference + streaming-vs-batch tests.
Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 454 tests, python 768 tests.
Add a Continuity and succession section to GOVERNANCE.md (trusted-contact emergency access to credentials enabling continuity within a week). Closes OpenSSF Silver access_continuity.
Adds the six price-level Fibonacci tools as a new **Fibonacci** family (catalogue 367 -> 373, twenty-four families). All build on the internal `pattern_swing` ZigZag tracker, are parameter-free (baked 5% swing threshold), and emit `Candle -> struct` outputs via custom Python/Node/WASM bindings.
| Tool | Output |
|------|--------|
| `FibRetracement` | seven levels (0/23.6/38.2/50/61.8/78.6/100%) of the last swing leg |
| `FibExtension` | five extension ratios (127.2/141.4/161.8/200/261.8%) projected beyond the leg |
| `FibProjection` | A-B-C measured-move target zone (61.8/100/161.8/261.8%) |
| `AutoFib` | retracement anchored on the dominant (largest-magnitude) recent leg |
| `GoldenPocket` | the 0.618-0.65 optimal-trade-entry band (low/mid/high) |
| `FibConfluence` | densest cluster of retracement levels across recent legs (price + strength) |
Fully wired: core (100% unit-tested branches), Python/Node/WASM struct bindings, fuzz driver, reference + streaming-vs-batch tests, README/docs counter. The four geometric/time tools (Fan, Arcs, Channel, Time Zones) follow in A5b.
Verification: `cargo test --workspace` green, clippy `-D warnings` clean, node 450 tests, python 760 tests.
## Summary
Adds a new **Harmonic Patterns** indicator family (counter 359 → 367, families 22 → 23) — the second half of the A4 roadmap item, following the Chart Patterns family in #166.
Eight Fibonacci-ratio detectors built on the shared swing-pivot tracker (`indicators::pattern_swing`) plus two new helpers there — `xabcd` (reads the last five pivots as X-A-B-C-D) and `ratios_in` (checks a list of `(value, low, high)` Fibonacci windows in one expression, no multi-line `&&` coverage gaps). Each consumes candles and emits the uniform pattern sign convention — `+1.0` bullish (terminal point D a swing low), `-1.0` bearish (D a swing high), `0.0` otherwise, never `None`. Parameter-free, with the Fibonacci windows documented as constants per detector.
## Detectors
| Indicator | Defining ratio |
|-----------|----------------|
| `Abcd` | four-point AB=CD (BC retraces AB, CD ≈ AB) |
| `Gartley` | AD/XA ≈ 0.786 |
| `Butterfly` | AD/XA ∈ 1.27–1.618 (extended D) |
| `Bat` | AD/XA ≈ 0.886, shallow B |
| `Crab` | AD/XA ≈ 1.618 (deepest D) |
| `Shark` | expansion AB, AD/XA 0.886–1.13 |
| `Cypher` | BC on XA, CD/XC ≈ 0.786 |
| `ThreeDrives` | two symmetric extension drives |
## Touchpoints
Core modules + `FAMILIES` group/assert, crate root re-exports, Python/Node/WASM bindings via the candle-pattern macros (Node `index.d.ts`/`index.js` regenerated), the candle fuzz target (`// --- Harmonic Patterns ---` section), Python reference + `CANDLE_SCALAR` registry tests and the Node candle-scalar factory, README catalogue counter + banner cache-buster + family table row + family-count word, `docs/README.md` counter, and the changelog.
## Verification
- `cargo test -p wickra-core --lib` — 2966 passed
- `cargo test -p wickra-core --doc` — 335 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` — clean
- Node `npm run build && npm test` — 444 passed
- Python `maturin develop --release` + `pytest` — 748 passed
Every detector branch is unit-tested, including a bullish and a bearish match per pattern to cover both output arms, plus an out-of-ratio non-match. Fibonacci windows use standard harmonic-trading ranges with documented tolerance bands.
Pass a read-only fine-grained PAT (SCORECARD_TOKEN) as repo_token so the OpenSSF Scorecard Branch-Protection check can read classic branch-protection rules instead of failing with an internal error.
Add a public ROADMAP.md describing project direction and pointing to the issue tracker as the authoritative view. Closes the OpenSSF Silver documentation_roadmap gap.
Add GOVERNANCE.md, MAINTAINERS.md, SUPPORT.md, DCO; add a DCO sign-off requirement to CONTRIBUTING.md and a security assurance case to SECURITY.md. Closes OpenSSF Silver / OSPS Baseline documentation gaps. Additive only.
Adds the OpenSSF Best Practices passing badge next to the OpenSSF Scorecard badge in the README header.
The project earned a passing badge: https://www.bestpractices.dev/projects/13094
Adds a `LICENSES/` directory with SPDX-named copies of the existing license texts (`MIT.txt`, `Apache-2.0.txt`) per the [REUSE Specification](https://reuse.software/spec/).
## Why
Automated license scanners (the OpenSSF Best Practices BadgeApp, GitHub's license API, REUSE tooling) look for a top-level `LICENSE`/`COPYING` file or a `LICENSES/` directory with SPDX-named files. Our files are named `LICENSE-MIT` / `LICENSE-APACHE` (Rust convention), which these scanners do not recognize — so the BadgeApp's `license_location` check keeps auto-flipping to "Unmet".
## What
- New `LICENSES/MIT.txt` — byte-identical copy of `LICENSE-MIT`
- New `LICENSES/Apache-2.0.txt` — byte-identical copy of `LICENSE-APACHE`
- Existing `LICENSE-MIT` and `LICENSE-APACHE` are **unchanged**
The project remains dual-licensed under **MIT OR Apache-2.0**. This change is additive only.
Version bump for the 0.5.0 release, which ships the relicense to MIT OR Apache-2.0.
Stacked on #158 (base branch `chore/relicense-mit-apache`) so this PR's diff is the bump only. After #158 merges to main, GitHub retargets this PR to main; merge it, then tag `v0.5.0` to publish.
## Changes
- Bump 0.4.7 -> 0.5.0 across the Cargo workspace, Python `pyproject.toml`, Node `package.json` + 6 platform manifests + 2 lockfiles, and `Cargo.lock`.
- CHANGELOG: cut the [0.5.0] section (the relicense) and add compare URLs.
- SECURITY.md: supported versions 0.4.x -> 0.5.x.
Minor (not patch) bump: a relicense is a significant change. No code changes.
NOTE: do not tag/release until you give the go (irreversible publish to crates.io/PyPI/npm). Suggested merge order: #158 -> this -> tag `v0.5.0` -> then the downstream PRs.
Relicenses Wickra from PolyForm Noncommercial 1.0.0 to the dual, OSI-approved **MIT OR Apache-2.0** (the de-facto Rust convention). Wickra becomes permissive, commercial-use-permitted open source; users may choose either license.
## Changes
- Replace `LICENSE` (PolyForm) with `LICENSE-MIT` + `LICENSE-APACHE` (full texts).
- Cargo: workspace `license = "MIT OR Apache-2.0"` (SPDX) + all 7 sub-crates switched from `license-file.workspace` to `license.workspace`.
- `deny.toml`: drop PolyForm from the allowlist.
- Python: `pyproject.toml` PEP 639 SPDX expression; remove the non-commercial classifier (verified: sdist metadata emits `License-Expression: MIT OR Apache-2.0`).
- Node: `package.json`, the 6 platform manifests and both lockfiles.
- README + Python/Node/WASM binding READMEs, CONTRIBUTING, CITATION.cff, PR template, and the WASM `pkg.license` step in `release.yml`.
- SECURITY.md: refresh supported versions 0.1.x -> 0.4.x.
- CHANGELOG: note the relicense under [Unreleased].
## Notes
- No code changes; metadata/text only. `cargo build` and `cargo deny check licenses` pass locally.
- GitHub will auto-detect "MIT, Apache-2.0" once this lands (currently NOASSERTION).
- Matching downstream changes (org `.github` profile, webpage, docs) are in separate PRs; merge those together with the relicense release so the live sites and org profile do not claim MIT before the packages do.
Completes expansion-roadmap block **A2 — Market Breadth**: the 14 indicators that remained after the `AdvanceDecline` bootstrap, all built on the existing `CrossSection` input.
## Indicators (all scalar `Indicator<Input = CrossSection, Output = f64>`)
| Indicator | Reading |
|-----------|---------|
| `AdvanceDeclineRatio` | advancers / decliners |
| `AdVolumeLine` | cumulative net advancing volume |
| `McClellanOscillator` | 19/39 EMAs of ratio-adjusted net advances |
| `McClellanSummationIndex` | running total of the oscillator |
| `Trin` (Arms Index) | A/D ratio over up/down volume ratio |
| `BreadthThrust` (Zweig) | SMA of the advancing-issues share |
| `NewHighsNewLows` | new highs − new lows |
| `HighLowIndex` | SMA of the record-high percent |
| `PercentAboveMa` | % of the universe above its MA |
| `UpDownVolumeRatio` | advancing / declining volume |
| `BullishPercentIndex` | % on a point-and-figure buy signal |
| `CumulativeVolumeIndex` | volume-normalised cumulative net advancing volume |
| `AbsoluteBreadthIndex` | \|advancers − decliners\| |
| `TickIndex` | instantaneous net advancers − decliners |
## Input model
`AdVolumeLine` and `CumulativeVolumeIndex` are kept distinct (the latter normalises each tick's net advancing volume by total volume, so it stays comparable across volume regimes). `PercentAboveMa` and `BullishPercentIndex` need a per-symbol state signal that `Member` did not carry, so `Member` gains two additive flags (`above_ma`, `on_buy_signal`) via a new `Member::with_signals` constructor; the 4-arg `Member::new` leaves both cleared, so every existing caller and binding is unchanged. `CrossSection` gains volume / new-extreme / state aggregation helpers.
## Wiring
Fully wired across the Rust core, the python/node/wasm bindings, the cross-section fuzz target, the README + docs indicator counters (325 → 339), and dedicated python/node streaming-vs-batch tests. `fmt` / `test --workspace --all-features` / `clippy --workspace -D warnings` / node build+test / pytest all green locally.
## Problem
On every indicator PR the `sync-about` workflow found `docs/README.md` lagging `lib.rs` (the wiring only bumped `README.md`) and pushed a `wickra-bot` *"sync indicator count"* commit onto the PR head. That push uses `GITHUB_TOKEN`, which **triggers no workflows**, so it moved the PR head onto a commit with no CI run — and the **Codecov patch status** (keyed to the PR head sha) stopped surfacing on the PR.
## Fix
- The indicator wiring (`ScriptHelpers/_common.py` `wire_readme_counter`) now bumps **both** `README.md` and `docs/README.md` in the author's code commit, so the counter is already correct when CI runs.
- This workflow's PR flow is reduced to a **read-only check** that fails loud (fork and same-repo PRs alike) if either counter is stale, and **never pushes**.
- The `GITHUB_TOKEN` job permission drops from `contents: write` back to `read` (OpenSSF Scorecard: Token-Permissions). The removed `ctx` step + push steps are gone.
- The `main`/tag outward syncs (About description, docs/webpage/wiki/org) are **unchanged** — they use the `ABOUT_SYNC_TOKEN` PAT, not `GITHUB_TOKEN`.
## Effect
Indicator PRs keep their head on the code commit → the Codecov patch status surfaces again. No functional change to merged-main state (the counts still land, now inside the squash-merged code commit).
## What
Adds a new indicator input type and family for **market-breadth** analysis — indicators that aggregate the state of an entire universe of symbols at each tick, rather than a single instrument's price. This is the last open input-type on the expansion roadmap (S10) and unblocks the remaining breadth indicators (McClellan, TRIN, High-Low Index, ...).
## Core
- **`CrossSection` input type** (`crates/wickra-core/src/cross_section.rs`) — one tick carrying the per-symbol state of the whole universe as a `Vec<Member>` + `timestamp`. Each `Member` precomputes a signed `change` (sign classifies advancing / declining / unchanged), a `volume`, and `new_high` / `new_low` extreme flags, so the breadth indicators stay stateless per tick. Both `Member` and `CrossSection` are `#[non_exhaustive]` for additive field growth. `CrossSection::new` validates the universe (non-empty, finite changes, finite non-negative volumes); `new_unchecked` skips validation for hot paths. `advancers()` / `decliners()` count by sign.
- **`Error::InvalidCrossSection`** variant for the validation failures.
- **`AdvanceDecline`** (`advance_decline.rs`) — the Advance/Decline Line: the running cumulative sum of net advancing-minus-declining issues. `Input = CrossSection`, `Output = f64`, ready after the first tick.
- New **"Market Breadth"** `FAMILIES` group; indicator count **314 → 315**, family count nineteen → twenty.
## Bindings
All custom (CrossSection is non-scalar, so no macros apply). The universe crosses each boundary as parallel arrays (`change`, `volume`, `new_high`, `new_low`):
- **Python / Node** expose `update` + `batch` (one array group per tick). Node satisfies the completeness contract (`update`/`batch`/`reset`/`isReady`/`warmupPeriod`).
- **WASM** exposes only `update` (the universe is ragged across ticks, matching the other multi-input wasm indicators) with numeric high/low flags.
- Python `map_err` gains the new error arm; `__init__.py` gets a `# Market Breadth` section in both the import and `__all__` blocks. `index.d.ts` / `index.js` regenerated.
## Tests / Fuzz
- Dedicated **streaming-vs-batch + reference-value + ragged-rejection** tests in Python (`test_new_indicators.py`) and Node (`indicators.test.js`) — kept out of the scalar/candle parametrize lists.
- Rust unit tests cover every reject branch (empty / non-finite change / negative & non-finite volume) and every indicator branch.
- New fuzz target `indicator_update_crosssection` drives `AdvanceDecline` over bounded ragged universes built with `new_unchecked`.
## Verify
- `cargo fmt --all` clean
- `cargo test -p wickra-core --lib` → 2593 passed; `--doc` → 298 passed
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` clean
- `cd bindings/node && npm run build && npm test` → 398 passed
- `maturin develop --release` + `pytest bindings/python/tests` → all passed
- counter check: mod-count 315 == lib-block 315
## What
The `FAMILIES` table in `crates/wickra-core/src/indicators/mod.rs` had drifted from the indicator count: `mod`-count was **314** but the FAMILIES total asserted **309**.
The five pairwise indicators `Cointegration`, `LeadLagCrossCorrelation`, `PairSpreadZScore`, `PairwiseBeta` and `RelativeStrengthAB` were exported via `pub use` but never assigned to a `FAMILIES` group — even though the README and docs already list them under **Price Statistics**. The "−5 offset" was therefore unclassified drift, not an intentional cross-asset offset.
## Change
- Add the five indicators to the `Price Statistics` group, next to the existing pairwise cluster (`PearsonCorrelation` / `Beta` / `SpearmanCorrelation`).
- Bump the drift assert `309 → 314` so the FAMILIES total now equals the `mod`-count exactly (offset 0).
No new indicators, no binding or doc changes — purely re-classification. The `mod`-count stays 314, so no counter bump.
## Verify
- `cargo fmt --all` clean
- `cargo test -p wickra-core --lib` → 2578 passed (incl. the FAMILIES drift test)
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` clean
PR #148 merged at **99.67%** patch coverage — `codecov/patch` flagged seven by-construction-rare lines in three of the new indicators that no test exercised. This brings the batch back to 100%.
**`ht_dcphase` / `ht_trendmode`** (6 lines) — the dominant-cycle phase recovery guards against a near-zero imaginary part (where `atan(real/imag)` is undefined) by collapsing to ±90° on the sign of the real part. That branch is unreachable with realistic price data. Extracted the phase-unwrap arithmetic into a private `compute_dc_phase(real, imag, smooth_period)` helper — a pure refactor with byte-identical output — and unit-tested it directly with crafted `(real, imag)` pairs, covering both the ±90 collapse and the normal `atan` path.
**`sar_ext`** (1 line) — `Accel::validate`'s non-finite guard was only ever hit for non-positive terms, never non-finite ones, despite the test comment claiming both. Added `NaN` / `infinity` cases on the long and short acceleration schedules.
No behaviour or public-API change. Locally: `cargo test -p wickra-core` (2578 + 297 doctests) and `clippy --workspace -D warnings` all green.
The TA-Lib parity batch (#148) bumped the indicator counter to 314, but `sync-about` only syncs the *number* — the family-table prose in the README still listed the pre-batch set. This fills the 19 new names into their existing family rows so the catalogue matches the count.
- **Momentum Oscillators**: ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100)
- **Trend & Directional**: MACD Fixed (MACDFIX), MACD Extended (MACDEXT), Plus DM, Minus DM, Plus DI, Minus DI, DX
- **Trailing Stops**: Parabolic SAR Extended (SAREXT)
- **Price Statistics**: Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast
- **Ehlers / Cycle (DSP)**: Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode
No new family — the "nineteen families" wording and the 314 counter are untouched. Docs-only, no code changes.
Version bump for 0.4.5: ships Anchored RSI, Volume Profile, TPO Profile and the Alt-Chart Bars family (Renko/Kagi/Point & Figure). Indicator count 289 -> 295.
Introduces a BarBuilder trait for price-driven chart constructors that emit a variable number of bars per candle (deliberately not Indicator). Adds Renko (box-size bricks, 2-box reversal), Kagi (reversal-amount segments) and Point & Figure (box-size X/O columns, N-box reversal) in a new Alt-Chart Bars family, with custom Python/Node/WASM bindings, a dedicated fuzz target, tests and docs. Indicator count 292 -> 295.
Volume Profile exposes the full per-bin volume histogram (price bounds plus raw distribution) that Value Area reduces to POC/VAH/VAL. TPO Profile is the volume-agnostic Time-Price-Opportunity letter count over a rolling window. Both candle-input, Vec-output, Market Profile family, with custom Python/Node/WASM bindings, fuzz, benches, tests and docs. Indicator count 290 -> 292.
Cumulative Relative Strength Index whose averaging begins at a runtime-chosen anchor bar (set_anchor), the momentum counterpart to Anchored VWAP. Scalar f64 input, 0..=100 output; wired through core, Python, Node and WASM bindings, fuzz, benches, tests and docs. Indicator count 289 -> 290.
Release 0.4.4.
Version bump only — no code changes. Ships the 40 TA-Lib candlestick patterns
(parts 2–9, #132–#141, 249 → 289 indicators) plus the candlestick rejection-
guard coverage tests (#142) that landed on `main` since 0.4.3.
Bumped: workspace `Cargo.toml` (+ `wickra-core` dep) and `Cargo.lock`,
`bindings/python/pyproject.toml`, `bindings/node/package.json` (+ 6 platform
`optionalDependencies`), the 6 `bindings/node/npm/*/package.json`, both
`package-lock.json` files, and `CHANGELOG.md` ([Unreleased] → [0.4.4]).
Clears the remaining zizmor code-scanning findings on this repo.
**Fixed**
- `dependabot-cooldown` (5): a 7-day cooldown on every update ecosystem
(cargo, npm, pip, ci-pip, github-actions) so Dependabot waits a week after a
release before opening the bump PR.
**Accepted via `.github/zizmor.yml`** (no workflow code changed)
- `template-injection` (sync-about.yml): false positive — every expansion is the
internal `grep -c` indicator count, not attacker-controllable.
- `use-trusted-publishing` (release.yml): OIDC migration tracked separately.
- `superfluous-actions` (release.yml): `softprops/action-gh-release` kept deliberately.
Verified with zizmor 1.25.2: 0 findings.
Closes the coverage gaps in the candlestick-pattern family that landed across
PRs #132–#141. Codecov flagged 25 uncovered lines on `main` (99.93%) — all in
the new candlestick files, and all early-return rejection guards or unused
`Default` impls that the accept-path unit tests never exercised.
This PR adds focused white-box unit tests (one or two per affected file) that
drive each rejection branch through the public `update()` API:
- **Zero-range guards** — a flat bar (`high == low`) at the relevant window
position: `DojiStar`, `InNeck`, `OnNeck`, `Thrusting`, `SeparatingLines`,
`EveningDojiStar`, `MorningDojiStar`, `GapSideBySideWhite`,
`FallingThreeMethods`, `RisingThreeMethods`, `MatHold`.
- **Too-short trigger body** — a wide-range bar with a tiny body that fails the
"long body" check: the three-bar stars, the three-methods pair, `MatHold`,
`SeparatingLines`.
- **Shape-specific guards** — `GapSideBySideWhite` body-size mismatch, `MatHold`
bar-2 fails to gap up.
- **`Default` impls** — `LongLine` / `ShortLine` (`default()` was never called).
No production code changes; tests only. `cargo test -p wickra-core` and
`cargo clippy -p wickra-core --all-targets -- -D warnings` pass locally.
The final batch of the TA-Lib candlestick roadmap. Adds five patterns, each a streaming `Indicator<Input = Candle, Output = f64>` emitting the family's uniform `±1.0 / 0.0` sign convention, fully wired across the Rust core, Python / Node / WASM bindings, fuzz target and reference tests.
- **Tasuki Gap** (`CDLTASUKIGAP`) — a 3-bar continuation: two same-coloured candles gap in the trend direction, then an opposite candle opens within the second body and closes back into the gap without filling it; upside +1, downside -1.
- **Unique Three River** (`CDLUNIQUE3RIVER`) — a 3-bar bullish reversal: a long black candle, a black candle probing a new low with its body inside the first, then a small white candle held below it; bullish +1.
- **Closing Marubozu** (`CDLCLOSINGMARUBOZU`) — a single long-bodied candle with no shadow on the close end; +1 (white, closes at the high) or -1 (black, closes at the low).
- **Opening Marubozu** — a single long-bodied candle with no shadow on the open end; +1 (white, opens at the low) or -1 (black, opens at the high). No direct TA-Lib equivalent — completes the pair with the closing marubozu.
- **Concealing Baby Swallow** (`CDLCONCEALBABYSWALL`) — a rare 4-bar bullish capitulation: two black marubozu, a black candle gapping down with an upper shadow into the second, then a large black candle engulfing it entirely; bullish +1.
Body and shadow thresholds follow the geometric house style (fixed fractions of the bar range) rather than TA-Lib's rolling averages.
Counter 284 → 289 (mod-count == lib counted block; FAMILIES total 279 → 284).
Stacked on #140 (`feat/cdl-gap-methods`); base retargets to `main` as the stack merges down.
Adds five TA-Lib candlestick patterns, each a streaming `Indicator<Input = Candle, Output = f64>` emitting the family's uniform `±1.0 / 0.0` sign convention, fully wired across the Rust core, Python / Node / WASM bindings, fuzz target and reference tests.
- **Upside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — a 3-bar bullish continuation: two white candles gap up, then a black candle opens within the second body and closes within the first; bullish +1.
- **Downside Gap Three Methods** (`CDLXSIDEGAP3METHODS`) — the bearish mirror: two black candles gap down, then a white candle opens within the second body and closes within the first; bearish -1.
- **Stalled Pattern** (`CDLSTALLEDPATTERN`) — a 3-bar bearish reversal warning: two long white candles then a small white candle riding the shoulder, signalling the rally is stalling; bearish -1.
- **Stick Sandwich** (`CDLSTICKSANDWICH`) — a 3-bar bullish reversal: two black candles closing at the same level sandwich a white candle, marking a support floor; bullish +1.
- **Takuri** (`CDLTAKURI`) — a single-bar bullish reversal, a strict Dragonfly Doji with a negligible upper shadow and very long lower shadow; bullish +1.
Body and shadow thresholds follow the geometric house style (fixed fractions of the bar range) rather than TA-Lib's rolling averages. Upside / Downside Gap Three Methods share the `CDLXSIDEGAP3METHODS` code, so the second carries a manual CHANGELOG entry (as with Rising / Falling Three Methods).
Counter 279 → 284 (mod-count == lib counted block; FAMILIES total 274 → 279).
Stacked on #139 (`feat/cdl-lines`); base retargets to `main` once the predecessor merges.
Adds five TA-Lib candlestick patterns, all `Input = Candle`, `Output = f64`
(`+1.0` bullish / `-1.0` bearish / `0.0` no pattern), wired across core,
Python/Node/WASM bindings, fuzz, and tests.
- **Matching Low** (`CDLMATCHINGLOW`) — 2-bar bullish reversal: two black candles in a decline share the same close, signalling selling pressure is exhausting; bullish +1.
- **Long Line** (`CDLLONGLINE`) — a candle whose range beats a rolling average of recent ranges with a body-dominated range; bullish +1 (white) / bearish -1 (black).
- **Short Line** (`CDLSHORTLINE`) — a compact candle whose range falls below the rolling average with a body-dominated range; bullish +1 (white) / bearish -1 (black).
- **Rising Three Methods** (`CDLRISEFALL3METHODS`) — 5-bar bullish continuation: a long white candle, three small bars holding within its range, then a white breakout to new highs; bullish +1.
- **Falling Three Methods** (`CDLRISEFALL3METHODS`) — the bearish mirror: a long black candle, three small bars within its range, then a black breakdown to new lows; bearish -1.
Counter 274 → 279 (mod-count == lib counted block; FAMILIES total 269 → 274).
Stacked on #138 (part 6 of 9); base retargets to `main` as the chain merges.
* feat: add hikkake-modified, homing-pigeon and neck-line candlestick patterns
Five patterns, all `Input = Candle`, `Output = f64`:
- Modified Hikkake (CDLHIKKAKEMOD) — a close-confirmed Hikkake: an inside bar
then a breakout that closes back inside the inside-bar range; bullish +1,
bearish -1.
- Homing Pigeon (CDLHOMINGPIGEON) — two black candles, the second a small body
inside the first, a bullish reversal; +1.
- On-Neck (CDLONNECK) — long black bar then a white bar closing at its low (the
neckline), a bearish continuation; -1.
- In-Neck (CDLINNECK) — long black bar then a white bar closing just into its
body, a bearish continuation; -1.
- Thrusting (CDLTHRUSTING) — long black bar then a white bar closing well into
but below the midpoint of its body, a bearish continuation; -1.
Counter 264 -> 269 (mod-count == lib counted block; FAMILIES total 259 -> 264).
* chore: sync indicator count to 269
---------
Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
* feat: add doji-star, gap, high-wave and hikkake candlestick patterns
Five patterns, all `Input = Candle`, `Output = f64`:
- Evening Doji Star (CDLEVENINGDOJISTAR) — bearish top reversal: long white bar,
a doji gapping up, then a black bar closing deep into the first body; -1
(penetration configurable, default 0.3).
- Morning Doji Star (CDLMORNINGDOJISTAR) — bullish bottom reversal mirror; +1.
- Gap Side-by-Side White (CDLGAPSIDESIDEWHITE) — two similar white candles
opening side by side after a gap, a continuation; gap up +1, gap down -1.
- High-Wave (CDLHIGHWAVE) — a small body with very long shadows on both sides,
an extreme indecision flag; +1 on detection.
- Hikkake (CDLHIKKAKE) — an inside bar followed by a failed breakout (a trap);
bullish +1, bearish -1.
Counter 259 -> 264 (mod-count == lib counted block; FAMILIES total 254 -> 259).
* chore: sync indicator count to 264
---------
Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
* feat: add Doji-family candlestick patterns
Five single-/two-bar Doji patterns, all `Input = Candle`, `Output = f64`:
- Doji Star (CDLDOJISTAR) — a long body followed by a doji gapping away in the
trend direction; bullish +1 (after a black bar), bearish -1 (after a white bar).
- Dragonfly Doji (CDLDRAGONFLYDOJI) — a doji opening and closing at the high with
a long lower shadow; bullish +1.
- Gravestone Doji (CDLGRAVESTONEDOJI) — a doji opening and closing at the low with
a long upper shadow; bearish -1.
- Long-Legged Doji (CDLLONGLEGGEDDOJI) — a doji with long shadows on both sides; a
non-directional indecision flag, +1 on detection.
- Rickshaw Man (CDLRICKSHAWMAN) — a long-legged doji with the body centred in the
range; a non-directional indecision flag, +1 on detection.
Counter 254 -> 259 (mod-count == lib counted block; FAMILIES total 249 -> 254).
* chore: sync indicator count to 259
---------
Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
* feat: add Abandoned Baby candlestick pattern (CDLABANDONEDBABY)
* feat: add Advance Block candlestick pattern (CDLADVANCEBLOCK)
* feat: add Belt Hold candlestick pattern (CDLBELTHOLD)
* feat: add Breakaway and Counterattack candlestick patterns (CDLBREAKAWAY, CDLCOUNTERATTACK)
Breakaway is a 5-bar reversal: a trend gaps away on the second bar, drifts
two more bars, then the fifth bar snaps back and closes inside the bar1/bar2
body gap (bullish +1, bearish -1). Counterattack is a 2-bar reversal where an
opposite-coloured long second bar closes level with the first (the counterattack
line; bullish +1, bearish -1).
Also suppress libtest's spanless `large_stack_arrays` false positive in
wickra-core test builds: the `#[test]` harness collects every test into a
compiler-generated array of references that crosses clippy's 16 KB threshold
once the suite passes ~2048 unit tests. The allow is scoped to `cfg(test)`, so
library code is still linted for genuinely large stack arrays.
* chore: sync indicator count to 254
---------
Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
* ci: pass ref context through env in release tag step
zizmor flagged the "Resolve target tag" step in release.yml for
template-injection: github.event_name / github.ref / github.ref_name
were interpolated directly into the shell script. On a tag push the tag
name is attacker-influenceable, so a crafted tag could inject commands.
Move all three context values into the step env and reference them as
shell variables instead. Verified with zizmor 1.16.3: template-injection
findings on release.yml drop from 2 to 0.
* ci: accept release.yml build caches via zizmor config
The release pipeline restores Swatinem/rust-cache and actions/setup-node
caches as a deliberate optimisation. zizmor flags all eight under
cache-poisoning because release.yml publishes to crates.io / PyPI / npm.
The caches are maintainer-controlled and the restore speedup is kept on
purpose, so accept the finding via a zizmor config ignore for release.yml
rather than running cache-free release builds. (Six of the eight are
actions/setup-node, reported at Low confidence.)
Adds .github/zizmor.yml; release.yml now reports 0 high findings.
* ci: drop persisted checkout credentials on read-only jobs
zizmor's artipacked audit flags every actions/checkout that keeps the
default persisted credential: the token is written to the runner's
.git/config, where it can leak if a later step packs .git into an
uploaded artifact, or be read by another step in the same job.
Set persist-credentials: false on the 20 checkouts whose jobs never push
or authenticate to git (build/test/clippy/msrv/coverage/supply-chain/
fuzz/python/wasm/node in ci.yml, plus bench.yml, codeql.yml, the seven
release.yml build/publish jobs, and sync-metadata.yml). The publish and
release jobs authenticate to crates.io / npm / PyPI / the GitHub API with
their own tokens, not persisted git credentials, so this is safe.
sync-about.yml genuinely pushes the indicator-count fix-up to the PR
branch, so it keeps its credential and is accepted via .github/zizmor.yml.
zizmor artipacked for the repo drops to 0 (0 high, 0 medium remaining).
* fix: keep docs/README indicator count in sync-about
The docs/README.md pointer prose names the indicator count
("**N indicators**") but was never part of the sync-about counter
pipeline, so it drifted to 214 while the real count (lib.rs) is 249.
Add docs/README.md to the PR-flow gate check, the patch sed, and the
fix-up commit so future count changes keep it in sync, and correct the
current stale value to 249.
* docs: fix stale Wiki reference in CONTRIBUTING layout table
The project-layout table still described docs/ as a "Pointer to the
project Wiki" even though the wiki was retired and the docs moved to
docs.wickra.org (wickra-lib/wickra-docs). Align the row with the
already-correct doc-site section further down the same file.
* feat: add Two Crows candlestick pattern (CDL2CROWS)
* feat: add Upside Gap Two Crows candlestick pattern (CDLUPSIDEGAP2CROWS)
* feat: add Identical Three Crows candlestick pattern (CDLIDENTICAL3CROWS)
* feat: add Three Line Strike candlestick pattern (CDL3LINESTRIKE)
* feat: add Three Stars in the South candlestick pattern (CDL3STARSINSOUTH)
* ci: fix webpage count sync crashing on removed public/hero.svg
The webpage indicator-count sync sed'd index.md, .vitepress/config.ts and
public/hero.svg, but hero.svg was removed from wickra-lib/webpage (the count is
now baked into og-banner.webp at build time from index.md). Under 'bash -e' the
missing file made sed exit 2 before the commit/push, so the count never reached
the webpage repo and its OG banner stayed stale — masked green by the step's
continue-on-error. Drops public/hero.svg from the sed and git add; index.md and
.vitepress/config.ts (both still carry the count) remain.
* docs: point README banner at the self-updating org profile image
Switches the top README banner from https://wickra.org/og-banner.webp (baked at
webpage deploy time) to the org profile banner that wickra-lib/.github's
banner.yml regenerates from the indicator count on every sync
(raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp).
The ?v=227 query busts GitHub's Camo image cache; the next commit teaches
sync-about to bump it with the count.
* ci: bump README banner cache-buster alongside the indicator count
Extends the PR-head README counter patch to also rewrite wickra-banner.webp?v=N
to the current count. The banner now points at the org profile image, whose
content changes when .github/banner.yml regenerates it; bumping the ?v query
busts GitHub's Camo cache so the README shows the new banner immediately. Rides
in the existing count-sync commit, so no extra commit lands on main.
* docs: changelog entry for self-updating README banner and sync fix
* ci: use npm ci instead of npm install for reproducible installs
Pins the node binding dependency install to the committed package-lock.json
integrity hashes (OpenSSF Scorecard PinnedDependencies). npm ci installs
strictly from the lockfile; npm install could resolve newer patch versions.
Covers ci.yml and both release.yml node steps.
* ci: hash-pin Python dev tooling in ci.yml (Scorecard #19)
Replaces the unpinned 'pip install maturin pytest numpy hypothesis' with a
hash-locked '--require-hashes -r' install (OpenSSF Scorecard PinnedDependencies).
Two lock files are needed because numpy publishes no single release with wheels
for both cp39 and cp313 (<=2.0.2 has cp39 only, >=2.1 drops cp39):
ci-dev-py39.txt numpy 2.0.2 (Python 3.9, + tomli/exceptiongroup)
ci-dev-py3.txt numpy 2.4.6 (Python 3.10+)
The step selects the file by matrix.python-version under shell: bash. Both are
generated from ci-dev.in via uv (scripts/update-lockfiles.sh, added next).
* ci: hash-pin Python deps in bench.yml (Scorecard #16)
Replaces the unpinned 'pip install maturin numpy pandas talipp finta' with a
hash-locked '--require-hashes -r .github/requirements/bench.txt' install.
bench.yml runs on a single Python version (3.11), so one lock file (generated
from bench.in via uv) is sufficient.
* build: add scripts/update-lockfiles.sh to regenerate all lockfiles
One command refreshes every committed lockfile across languages: Cargo.lock and
fuzz/Cargo.lock (cargo update), the Node binding package-lock.json, and the
hash-pinned Python requirements under .github/requirements/ (uv pip compile
--generate-hashes). Uses uv for the Python locks so a target Python version's
hashed transitive closure can be resolved without that interpreter installed
(needed for the numpy cp39/cp313 split); bootstraps uv if absent.
.gitattributes pins *.sh to LF so the script stays runnable on Linux/macOS.
* ci: split ci-dev requirements per Python version + Dependabot rehash
Splits the single ci-dev.in into ci-dev-py39.in (numpy <2.1, the last series
with cp39 wheels) and ci-dev-py3.in (3.10+), giving a 1:1 .in->.txt layout.
The cap keeps Python 3.9 permanently installable and stops Dependabot from
proposing 3.9-breaking numpy bumps.
Adds a Dependabot pip entry on /.github/requirements so the hash-locked tooling
is kept current automatically; the canonical manual refresh stays
scripts/update-lockfiles.sh. Only the '# via -r' provenance lines in the .txt
change; no package versions or hashes move.
* ci: cache pip and npm downloads in the PR-loop jobs
Adds setup-python cache: pip (ci.yml python matrix + bench.yml, keyed on the
hash-locked requirements) and setup-node cache: npm (ci.yml node job, keyed on
bindings/node/package-lock.json), on both the primary and retry setup steps.
Scoped to jobs that actually install dependencies; the examples-smoke and
clippy-bindings jobs install nothing and are left uncached. release.yml is
intentionally left out: it runs only on tag push (not the PR loop) and is the
publish-critical path, so no caching is added there.
* docs: document hash-pinned requirements and update-lockfiles.sh
Updates the lockfile-policy table: the bindings/python row no longer claims CI
installs tooling unpinned, and a new .github/requirements row documents the
hash-locked CI/bench tooling and the per-Python-version ci-dev split. Adds a
paragraph pointing contributors at scripts/update-lockfiles.sh (uv-based,
self-bootstrapping) as the canonical lockfile refresh.
* docs: changelog entry for hash-pinned CI dependency installs
* feat(core): add 3 trade-flow microstructure indicators
SignedVolume (per-trade size signed by aggressor), CumulativeVolumeDelta
(running signed-volume total), and TradeImbalance (rolling buy/sell volume
imbalance over a trade window). All consume the Trade type, with full unit
coverage. Extends the Microstructure family.
* feat(bindings): expose trade-flow microstructure indicators
Python, Node and WASM bindings for SignedVolume, CumulativeVolumeDelta and
TradeImbalance. Each takes a trade via update(price, size, is_buy); Python and
Node expose a batch over three parallel arrays, WASM exposes per-trade update.
Regenerates node index.d.ts/.js.
* test(bindings,fuzz,bench): cover trade-flow microstructure indicators
Python and Node: reference values, streaming-vs-batch, lifecycle/repr and input
validation (zero window, negative size, non-positive price, mismatched batch
lengths). New indicator_update_trade fuzz target. Synthetic trade-tape benches
(signed_volume cheapest, trade_imbalance windowed/expensive).
* docs: add trade-flow indicators + bump counter to 227
README Microstructure family row gains signed volume / CVD / trade imbalance and
the counter goes 224 -> 227; CHANGELOG records the trade-flow indicators.
* feat(core): add microstructure input types (OrderBook, Trade, TradeQuote)
New non-OHLCV value types for the order-book / trade-flow indicator family:
Level, OrderBook (sorted, uncrossed depth snapshot), Side, Trade (with
aggressor side), and TradeQuote (trade paired with prevailing mid). Each has a
validating constructor plus a new_unchecked hot-path constructor, with full
unit coverage. Adds InvalidOrderBook / InvalidTrade error variants.
* feat(core): add 5 order-book microstructure indicators
OrderBookImbalanceTop1/TopN/Full (signed depth imbalance), Microprice
(size-weighted fair value), and QuotedSpread (top-of-book spread in bps). All
consume the OrderBook snapshot type, emit f64, are stateless and ready after
the first snapshot, with full unit coverage. Registers a new Microstructure
family in the taxonomy.
* feat(bindings): expose order-book microstructure indicators
Python, Node, and WASM bindings for OrderBookImbalanceTop1/TopN/Full,
Microprice and QuotedSpread. Each takes a depth snapshot via four equal-length
(bid_px, bid_sz, ask_px, ask_sz) arrays. Python and Node expose a batch over a
list of snapshots; WASM exposes per-snapshot update (the streaming model that
fits a browser book feed). Regenerates node index.d.ts/.js and registers the
new InvalidOrderBook/InvalidTrade arms in the Python error mapping.
* test(bindings,fuzz): cover order-book microstructure indicators
Python: smoke, reference values, streaming-vs-batch, lifecycle/repr and input
validation (mismatched lengths, crossed book, misordered levels, zero levels)
for all five order-book indicators. Node: reference values, streaming-vs-batch,
and rejection cases. Adds an indicator_update_orderbook fuzz target driving
every order-book indicator over arbitrary (incl. degenerate) snapshots.
* bench(microstructure): synthetic order-book benchmarks
Add a bench_orderbook_input harness and synthesise a five-level book around
each candle close (no order-book dataset ships with the repo). Benches the
cheapest (top-of-book imbalance) and most-expensive (full-depth imbalance) plus
microprice, matching the curated cheapest/expensive-per-family approach.
* docs: add Microstructure family + bump indicator counter to 224
README gains the Microstructure family row (order-book imbalance, microprice,
quoted spread) and the indicator counter goes 219 -> 224 across seventeen
families; CHANGELOG records the new order-book indicators and value types.
The LICENSE now carries an Additional Permissions section on top of
PolyForm Noncommercial 1.0.0, so the bare SPDX id no longer describes
it exactly. Update the package manifests to reference the actual file
instead of claiming the unmodified standard:
- Cargo (workspace + all crates): license -> license-file = "LICENSE"
- npm (main + 6 platform packages): LicenseRef-Wickra-Noncommercial-1.0.0
- PyPI: license text notes the additional personal-account permissions
Append an Additional Permissions section to the PolyForm Noncommercial
License 1.0.0. The base PolyForm text is unmodified; the section only
broadens the grant. It explicitly permits a natural person to use the
software for their own personal account, including running a trading
bot on their own capital for profit, matching the README's plain-English
summary (hobby trading bots: all fine). Commercial sale of the software
or of services built around it still requires a commercial license.
* feat(core): add signed dragonfly/gravestone encoding to Doji
Doji gains an opt-in `.signed()` mode that classifies a detected Doji by the
position of its body within the bar range: dragonfly (long lower shadow) emits
+1.0 (bullish), gravestone (long upper shadow) emits -1.0 (bearish), and a
long-legged/standard Doji emits 0.0. The default detection-flag behaviour
(+1.0/0.0) is unchanged, so existing callers are unaffected.
The other 14 candlestick patterns already emit the uniform +1 bull / -1 bear /
0 none convention; document that explicitly with a "Signed +-1 encoding"
section on each so the whole family is a consistent drop-in ML feature.
* feat(bindings): expose Doji signed mode in python, node, wasm
Hand-write the Doji binding in all three language bindings (instead of the
shared candle-pattern macro) so it accepts an opt-in `signed` flag and exposes
an `is_signed`/`isSigned` accessor:
- Python: `Doji(signed=False)` keyword argument
- Node: `new Doji(signed?)` optional constructor argument (index.d.ts/.js
regenerated via napi build)
- WASM: `new Doji(signed?)` optional constructor argument
The default construction is unchanged, so existing callers keep the
direction-less +1/0 detection flag.
* test(bindings,fuzz): cover Doji signed dragonfly/gravestone encoding
- python: dragonfly(+1)/gravestone(-1)/neutral(0) and default-flag cases in
test_known_values
- node: equivalent signed/default assertions in indicators.test.js
- fuzz: drive a signed Doji alongside the default in indicator_update_candle
* docs: document signed candlestick convention and Doji signed mode
README gains a candlestick sign-convention note; CHANGELOG records the new
opt-in Doji signed dragonfly/gravestone encoding under [Unreleased].
Releases the cross-asset / pairwise indicator family (PR #109):
PairwiseBeta, PairSpreadZScore, LeadLagCrossCorrelation, Cointegration,
RelativeStrengthAB. Indicator count 214 -> 219.
Bumps workspace + binding versions and the CHANGELOG ([Unreleased] ->
[0.4.1]) with the new compare URL.
* feat(core): add PairwiseBeta cross-asset indicator
Rolling OLS slope of one asset's log-returns on another's. Unlike Beta,
which regresses the raw inputs it is fed, PairwiseBeta differences
consecutive prices into log-returns internally -- the conventional way to
measure cross-asset beta, where a beta on price levels would be dominated
by the shared trend.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with unit/known-value/streaming tests and a pair fuzz target.
* feat(core): add PairSpreadZScore cross-asset indicator
Standardised log-spread ln(a) - beta*ln(b) of a pair, where beta is a
rolling-OLS hedge ratio and the spread is z-scored over its own look-back.
The canonical mean-reversion / statistical-arbitrage entry signal, with
independent beta_period and z_period windows.
Two-series Indicator<Input = (f64, f64)>, exposed in Rust, Python, Node
and WASM, with sign/known-value/streaming tests and a pair fuzz target.
* feat(core): add LeadLagCrossCorrelation cross-asset indicator
Reports the integer offset k in [-max_lag, max_lag] that maximises
|corr(a[t], b[t+k])|, answering which of two assets leads the other and by
how many bars. A positive lag means a leads b. Fully causal: a's window is
held centred while b's window slides across the buffered history, so every
lag is evaluated only against data already seen.
Struct output { lag, correlation }, exposed in Rust, Python, Node and WASM
with lead-detection/streaming tests and a pair fuzz driver.
* feat(core): add Cointegration (Engle-Granger + ADF) indicator
Rolling pairs-trading screen: an OLS hedge ratio of a on b, the spread
(residual) a - (alpha + beta*b), and an augmented Dickey-Fuller t-statistic
on the spread with configurable lags. A strongly negative statistic flags a
mean-reverting, tradeable spread. Includes a small Gaussian-elimination
solver for the augmented regression.
Struct output { hedge_ratio, spread, adf_stat }, exposed in Rust, Python,
Node and WASM with stationarity/hedge-ratio/streaming tests and a pair fuzz
driver.
* feat(core): add RelativeStrengthAB cross-asset indicator
Comparative relative strength of two assets: the ratio line a/b together
with its moving average and its RSI, the classic asset-vs-asset /
asset-vs-index rotation screen. Composes the existing Sma and Rsi over the
ratio; a zero denominator or non-finite price is skipped.
Struct output { ratio, ratio_ma, ratio_rsi }, exposed in Rust, Python, Node
and WASM with flat/rising-ratio/streaming tests and a pair fuzz driver.
* test(cointegration): cover ADF guard branches
The ADF helper's short-series and degrees-of-freedom guards and the
zero-dispersion (perfect AR) path are unreachable through the public
Cointegration API (period >= 2*adf_lags + 4), so exercise them with direct
unit tests on adf_no_constant. The second linear solve cannot be singular
once the coefficient solve on the same matrix has succeeded, so it now uses
expect() instead of a dead error branch.
GitHub release immutability locks a release's assets at publish time. The
current flow publishes the release in github-release and only afterwards uploads
the Sigstore provenance bundle (P21.1e) via 'gh release upload', which
immutability would reject (actions/attest-build-provenance#734).
Reorder to draft -> attach everything -> publish:
- github-release now creates the release as a draft (draft: true) with all build
artefacts.
- attestations attaches the provenance bundle to the draft (gh release upload
works on drafts), unchanged otherwise.
- a new publish-release job flips the draft to published + latest, gated on
'always() && needs.github-release.result == success' so a Sigstore hiccup in
attestations costs only the provenance asset, never the release — the same
isolation as before. A skipped github-release (failed publish) skips this too.
Correct with immutability off (today: release ends published with every asset)
and on (later, user toggle: all assets present before the lock). No behaviour
removed; nothing deleted.
* docs(readme): surface the documentation site (docs.wickra.org)
The README never linked the canonical docs at docs.wickra.org — visitors had
no path from the repo to the per-indicator deep dives, quickstarts, and
guides. Add a docs badge, a Documentation section mirroring the binding
READMEs and docs/README.md, and an Indicators-Overview link in the indicator
section.
* ci(sync-about): keep the wiki pointer page's indicator count in sync
The GitHub wiki was collapsed to a single Home.md that points at
docs.wickra.org but still names the indicator count ('… for all N indicators').
Add a count-sync step mirroring the docs/webpage steps — clone wickra.wiki into
its own dir, sed Home.md, commit as wickra-bot, push — with the same
continue-on-error soft-skip so a missing PAT scope never fails the run.
OpenSSF Scorecard's Signed-Releases check scans the GitHub Release *assets* for
signed/provenance files (`*.intoto.jsonl`, `*.sig`, ...). It does not look at
GitHub's separate attestations store, so although the attestations job has signed
the published bytes since v0.4.0, the v0.4.0 release assets carried no provenance
file and the check stayed at 0.
Attach the Sigstore provenance bundle (already produced by
actions/attest-build-provenance) to the release as `wickra-<tag>.provenance.intoto.jsonl`:
- github-release now exposes its resolved tag as a job output.
- attestations `needs: github-release` (so the Release already exists), gains
`contents: write`, gives the attest step an id, and uploads the bundle with
`gh release upload --clobber` (idempotent on re-runs).
Publishes stay fully isolated — cargo/PyPI/npm all run upstream of github-release,
so a Sigstore hiccup here can never block or corrupt a publish; at worst the
release just lacks the provenance asset. Signed-Releases climbs over the next
releases as each tag carries the bundle.
The v0.4.0-era CI failure was a runner network blip — `napi build` invokes cargo,
whose fetch of index.crates.io hit "Could not resolve host: index.crates.io" and
failed the Node-on-macOS job, forcing a manual re-run. The earlier flake-hardening
(setup-node/setup-python + rust-cache retries) only covered toolchain download and
cache restore, not the registry fetches inside the actual build/publish steps.
Set tool-level network retries as workflow env so every cargo/napi/maturin/
wasm-pack/npm/pip invocation in every job inherits them — including the nested
cargo calls inside napi/maturin/wasm-pack:
- CARGO_NET_RETRY=10 (default 3): cargo classes DNS-resolve / connect / timeout
errors as spurious and retries with backoff; 10 attempts ride out a transient
blip instead of failing the job.
- CARGO_NET_GIT_FETCH_WITH_CLI=true: more robust git-dep fetches.
- npm_config_fetch_retries=5 / maxtimeout=120s: npm ci/install registry retries.
- PIP_RETRIES=5 / PIP_DEFAULT_TIMEOUT=120: pip install resilience.
Applied to ci.yml, release.yml and bench.yml (the workflows that build). No more
manual re-runs for transient registry flakes.
* ci(sync-about): fix docs version-sync clone collision + webpage npm race
Two real release-time bugs surfaced by the v0.4.0 release, where the docs
"Published versions" table never updated and the marketing-site Cloudflare
build failed:
1. docs version sync never ran. The "Sync docs version (wickra-docs)" step
cloned into a directory literally named `docs`, but on a tag push the job
checks out the wickra repo at the workspace root, which already contains a
top-level `docs/` directory. `git clone … docs` therefore failed with
"destination path 'docs' already exists", silenced by `2>/dev/null` and
misreported as a missing-token warning, so the docs version table stayed at
the previous release. Clone into `docs-ver` instead (mirrors the `docs-count`
dir the count step already uses); it collides with nothing in the repo.
2. webpage build broke on a version race. The "Sync webpage version" step bumps
package.json's `wickra-wasm` pin to the released version and pushes
immediately, but release.yml publishes wickra-wasm to npm in parallel on the
same tag and finishes minutes later. Cloudflare's `npm clean-install` then
hit `ETARGET: No matching version found for wickra-wasm@^0.4.0`. Poll npm for
wickra-wasm@<version> (up to ~15 min) before committing; if it never appears
the step skips with a warning rather than pushing a build-breaking commit.
Both steps were designed to mirror each other across docs/webpage; these fixes
restore that symmetry so every release self-heals both sites.
* ci(sync-about): regenerate webpage package-lock on version bump
Third v0.4.0 release-sync defect: the webpage version step seds package.json's
wickra-wasm pin but never touched package-lock.json, so even after wickra-wasm
went live on npm the Cloudflare build still failed with
`npm ci` EUSAGE: "lock file's wickra-wasm@0.3.1 does not satisfy
wickra-wasm@0.4.0".
After the package.json sed, run `npm install --package-lock-only` so the lockfile
(version + resolved + integrity) matches the new pin; commit package-lock.json
alongside package.json. The earlier npm-wait already guarantees the version is
resolvable. Guarded: if the regen fails the step skips the whole commit rather
than push a package.json/lock mismatch.
The live site was unblocked out-of-band by a matching lockfile commit on the
webpage repo; this makes it self-heal on every future release.
The auto-injected GITHUB_TOKEN defaulted to write-all in ci.yml, bench.yml
and release.yml (no top-level permissions block), and codeql.yml declared
its scopes only at job level. Add a top-level `permissions: contents: read`
to all four so the token starts read-only and only the jobs that genuinely
write through it raise the scope:
- release.yml: github-release keeps contents: write; node-/wasm-publish and
attestations keep their id-token / attestations: write blocks. The
cargo/python/node publish jobs push to crates.io/PyPI/npm via their own
registry secrets, not the GITHUB_TOKEN, so read-only is correct for them.
- codeql.yml: analyze keeps security-events: write (job level).
- ci.yml / bench.yml: no job writes back to the repo (coverage uploads via
CODECOV_TOKEN; bench only uploads an artifact), so no job override is needed.
sync-about.yml already had a top-level block but at contents: write; demote
the top level to read and move contents: write down to the single `sync` job
(the PR-head counter push is the only GITHUB_TOKEN write). The cross-repo
About/docs/webpage/org writes are unaffected — they run through the
fine-grained ABOUT_SYNC_TOKEN, which the permissions key does not govern.
Raises OpenSSF Scorecard Token-Permissions from 0 toward 10.
Minor release. The headline user-facing change is the Node binding now
rejecting invalid indicator periods instead of silently clamping period 0
to 1 (matches Python/WASM/core); plus per-ecosystem binding READMEs and a
corrected MSRV statement in CONTRIBUTING. See CHANGELOG [0.4.0].
- Cargo.toml (workspace.package + wickra-core dep) + Cargo.lock (6 members)
- bindings/python/pyproject.toml
- bindings/node/package.json (version + 6 optionalDependencies) + package-lock.json
- bindings/node/npm/*/package.json (6 platform subpackages)
- examples/node/package-lock.json (wickra-* platform pins)
- CHANGELOG: [Unreleased] -> [0.4.0] - 2026-05-31 + compare URL
No tag pushed (release publish is a separate, user-confirmed step).
Embed the brand banner at the top of the README (linked to wickra.org) and
drop the now-redundant "# Wickra" heading — the banner shows the wordmark.
The image is served from https://wickra.org/og-banner.webp, which the webpage
build regenerates on every deploy from the current indicator count (4K WebP),
so the README banner stays current with no committed binary in this repo and
renders on GitHub, crates.io and docs.rs alike.
Now that the wickra.org catch-all mailbox exists, move the project contact +
package-author email off the personal gmail to support@wickra.org across all
surfaces: CODE_OF_CONDUCT, SECURITY, CITATION.cff, Cargo.toml, the npm + PyPI
author fields, the release.yml npm author, and repo-metadata.toml. (The
package-author changes take effect on the next published release.)
repo-metadata.toml's [audit].forbidden still pins kingchencp@gmail.com (the
private commit email) as a banned substring — unchanged.
Also remove the FUNDING.yml custom "https://wickra.org/sponsor" entry: that
page 404s, so the Sponsor button linked to a dead URL. The GitHub Sponsors
entry (github: [kingchenc]) stays.
* ci(sync-about): auto-sync the published version to wickra-docs on release
* ci(sync-about): retarget indicator-count sync from wiki to wickra-docs + point About homepage at docs.wickra.org (P8.3)
* ci(sync-about): self-update the marketing site (webpage) count + version (P12.1)
* ci(sync-about): auto-sync the published version to wickra-docs on release
* ci(sync-about): retarget indicator-count sync from wiki to wickra-docs + point About homepage at docs.wickra.org (P8.3)
The documentation now lives in the wickra-lib/wickra-docs VitePress repo and
will deploy to docs.wickra.org. Rewrite every tracked, user-facing wiki link
in the main repo to the new canonical site:
- docs/README.md, CONTRIBUTING.md, PULL_REQUEST_TEMPLATE.md
- bindings/{node,python,wasm}/README.md, examples/wasm/README.md
- repo-metadata.toml: wiki_url/wiki_git -> docs_url/docs_git
Page paths map 1:1 to VitePress clean URLs (e.g. /wiki/Quickstart-Rust.md ->
docs.wickra.org/Quickstart-Rust). The sync-about.yml wiki-sync step is left
untouched on purpose: it stays until the docs site is live (tracked as P8.3).
The GitHub wiki itself is not deleted yet for the same reason.
* build(node): track the generated index.d.ts (P5.1)
index.js was committed but index.d.ts was gitignored, an inconsistency that
also contradicts CONTRIBUTING ('regenerate both .d.ts/.js when a binding API
changes'). Track index.d.ts too so the repo carries the TypeScript types as a
matched pair with index.js. Generated by napi build; ~214 indicator classes.
* fix(node): reject invalid periods instead of clamping them (P5.4)
The Node scalar-indicator macro clamped period 0 to 1 (via clamp_period + must)
and the multi-parameter constructors did the same, silently swallowing the
core's PeriodZero validation. The core rejects period 0 (Error::PeriodZero),
and the Python and WASM bindings already propagate it — Node was the outlier,
masking caller mistakes. Make the macro constructor fallible and let every
constructor propagate the core error via map_err, removing clamp_period/must.
Update the smoke test (period 0 now throws, matching core/Python/WASM).
* docs(changelog): note the Node period-validation change (P5.4)
Behavior change per CONTRIBUTING: Node constructors now reject invalid periods
instead of clamping. Add an [Unreleased] Changed entry.
* chore: drop accidentally committed scratch log
The .gitignore comment claimed package-lock.json is committed only under
bindings/node/, but examples/node/package-lock.json has also been tracked
since #80. Correct the comment and add a CONTRIBUTING 'Lockfile policy'
section spelling out every component: Cargo.lock + the two Node package-locks
are tracked; fuzz/Cargo.lock is ignored (cargo-fuzz default); the Python
package has no lockfile by PyO3 convention (pinned via Cargo.lock); the
ghost-ignored site keeps its lockfile local.
* examples(node): add RSI mean-reversion strategy
Node counterpart of strategy_rsi_mean_reversion.{py,rs}: RSI(14) < 30 long,
> 70 exit, 0.1% fees, hourly BTCUSDT. Output verified byte-identical to the
Rust reference (37 trades W24/L13, -17.84% return, 46.89% max drawdown).
* examples(node): add MACD + ADX trend-filter strategy
Node counterpart of strategy_macd_adx.{py,rs}: MACD(12,26,9) histogram
crossover entries gated by ADX(14) > 20, hourly BTCUSDT, 0.1% fees. Output
verified byte-identical to the Rust reference (246 trades W90/L156, -47.19%
return, 53.75% max drawdown).
* examples(node): add Bollinger-squeeze breakout strategy
Node counterpart of strategy_bollinger_squeeze.{py,rs}: enter on a fresh
180-bar Bollinger-bandwidth low + close above the upper band, exit on a
2*ATR(14) stop or upper-band collapse, daily BTCUSDT, 0.1% fees. Output
verified byte-identical to the Rust reference (1 trade, -7.82% return,
13.01% max drawdown).
* examples(wasm): add RSI mean-reversion strategy demo
Browser counterpart of strategy_rsi_mean_reversion.{py,js,rs}: RSI(14) < 30
long, > 70 exit, 0.1% fees, summary table. Same signal/fill/PnL/equity loop as
the runtime-verified Node example; loads via the established wickra_wasm.js
init + fetch-CSV pattern. (wasm32 build runs in CI.)
* examples(wasm): add MACD + ADX trend-filter strategy demo
Browser counterpart of strategy_macd_adx.{py,js,rs}: MACD(12,26,9) histogram
crossover gated by ADX(14) > 20, hourly BTCUSDT, 0.1% fees. Logic identical to
the runtime-verified Node example; standard wickra_wasm.js init + fetch-CSV
loader. (wasm32 build runs in CI.)
* examples(wasm): add Bollinger-squeeze breakout strategy demo
Browser counterpart of strategy_bollinger_squeeze.{py,js,rs}: fresh 180-bar
Bollinger-bandwidth low + upper-band breakout, 2*ATR(14) stop, daily BTCUSDT,
0.1% fees. Logic identical to the runtime-verified Node example; standard
wickra_wasm.js init + fetch-CSV loader. (wasm32 build runs in CI.)
* ci: add examples syntax-smoke job (P2.3)
The Rust examples are built by 'cargo build -p wickra-examples --bins'; the
Node, browser-WASM and Python examples had no build gate. New job parse-checks
every examples/{node,wasm}/*.js, extracts and node --checks each WASM .html
module script, and python -m py_compile's every examples/python/*.py — so a
broken example edit fails CI instead of landing silently.
* docs(examples): list the new Node + WASM strategy examples
Add the three Node strategy scripts and three WASM strategy demos to the
examples README tables, bringing Node and WASM to parity with the existing
Rust and Python strategy rows.
* chore(examples): refresh examples/node lockfile for the linked wickra binding
npm install rewrote the file: dependency snapshot of the local wickra binding
that the examples link against (version 0.1.4 -> 0.3.1, license + engines
fields), which had gone stale in the committed lockfile.
* test(node): add input-validation suite
Node counterpart of bindings/python/tests/test_input_validation.py: invalid
constructor parameters (ATR zero period, MACD non-increasing fast/slow,
BollingerBands negative multiplier, PSAR step > max, ValueArea period/pct,
InitialBalance/OpeningRange zero period, Ichimoku non-increasing periods,
Ehlers-family ordering) and unequal-length candle/ValueArea batch inputs all
throw a JS Error. Validated against the built binding.
* test(node): add indicator completeness contract
Introspects every exported indicator class and asserts the full interface
(update / batch / reset / isReady / warmupPeriod) plus the pre-warmup contract
for zero-arg indicators, and guards that the full catalogue (>= 200 classes)
is exported. Catches a new indicator wired without the standard methods, or a
stale/partial native build dropping exports, with no per-indicator boilerplate.
* test(wasm): broaden scalar streaming-vs-batch coverage
Extend the inline wasm-bindgen-test suite with a streaming==batch check across
~70 scalar indicators spanning moving averages, momentum, volatility,
statistics/regression, Ehlers/cycle and risk/performance families (previously
only EMA + the candle-input group were covered per-indicator), plus four more
invalid-constructor assertions. Constructor args mirror the CI-passing Node
factories. Host-compiles (cargo test -p wickra-wasm --no-run); executed in CI
via wasm-pack test --node.
* bench(node): add indicator throughput benchmark
Node counterpart of the Rust criterion benches / Python compare_libraries:
measures streaming (per-tick update) and batch throughput in Mupd/s across a
representative indicator set over a synthetic OHLCV series (--bars, default
200k). Dependency-free; wired as 'npm run bench'.
* docs(wasm): list strategy demos + document the benchmark story
Add the three new strategy demos to the WASM examples table and a Performance
section: parallel_assets.html is the in-browser benchmark, with raw throughput
covered by the Rust criterion / Python / Node benchmarks (the WASM engine is the
same core compiled to wasm32).
Extend the count-sync workflow to drive three more surfaces from the same
single count, so the org page never drifts again:
- Repo About **homepage** URL — enforced unconditionally via
`gh repo edit --homepage` in the existing About step (fixes the stale
kingchenc URL and self-heals). Uses the same Administration-write
permission as --description, so NO extra token scope.
- Org **profile README** count (wickra-lib/.github, profile/README.md) —
clone + sed + bot commit/push, same pattern as the Sync Wiki step.
- Org **description** field ('… N indicators, install-free.') — gh api PATCH.
The two org steps need ABOUT_SYNC_TOKEN scope the main-repo syncs do not
(write on wickra-lib/.github; admin:org for the description PATCH). Both are
written to soft-skip with a ::warning:: and continue-on-error, so the run
stays green until the scope is granted — then they sync with no further
change. Homepage swap-point to a docs domain is a one-line change.
Refs findings P9 / P10.
* chore: remove ROADMAP.md from the public repo
ROADMAP is kept as a local-only draft (ghost-ignored via
.git/info/exclude); it is not part of the published package surface.
* release: bump 0.3.0 -> 0.3.1
CI-only patch: fixes the release.yml CycloneDX SBOM step (cargo-cyclonedx
has no -p flag, see #79) that skipped the GitHub Release attach-assets job
on 0.3.0. No library changes — republishes the same code with a working
release pipeline.
- Cargo.toml (workspace.package + wickra-core dep) + Cargo.lock
- bindings/python/pyproject.toml
- bindings/node/package.json (version + 6 optionalDependencies) + package-lock.json
- bindings/node/npm/*/package.json (6 platform subpackages)
- CHANGELOG: finalize [0.3.0] (was still under [Unreleased]), add [0.3.1]
* chore: track examples/node/package-lock.json
Since the global package-lock ignore rule was dropped (#68) this file was
left untracked. Commit it for reproducible example installs, consistent
with bindings/node (findings P4.1).
cargo-cyclonedx 0.5.9 walks the whole workspace in a single pass and
writes a <package>.cdx.json next to each member's Cargo.toml; it has no
-p/--package selector. The previous three 'cargo cyclonedx ... -p <crate>'
invocations aborted with 'error: unexpected argument -p found', failing
the cargo-publish job after the crates were already published and, in
turn, skipping the github-release attach-assets job (it needs all four
publish jobs). v0.3.0 published to every registry but got no GitHub
Release page or SBOM assets as a result.
Replace the three invalid calls with a single workspace pass and copy
the three crates.io crate SBOMs into the upload dir.
Minor bump (not patch) because the [Unreleased] section since 0.2.7 has
accumulated a sweep of additive changes that justify a new minor:
- Family 9-16 indicator catalogue expansion (Bands & Channels, Trailing
Stops, Volume, Statistics, Ehlers/Cycle DSP, Pivots, DeMark, Ichimoku,
Candlestick Patterns, Market Profile, Risk/Performance) — roughly
100+ new indicators since 0.2.7 across all four bindings.
- New `wickra_core::FAMILIES` const + family-taxonomy guard tests.
- GitHub org migration (kingchenc -> wickra-lib) and new maintainer
email (wickra.lib@gmail.com).
- New `repo-metadata.toml` + `sync-metadata.yml` audit workflow.
- WASM CI tests now run on every PR (existing tests had been
manually-only).
- CycloneDX SBOMs + npm provenance attestations attached to releases.
- Three end-to-end strategy examples.
- Governance polish: ARCHITECTURE / ROADMAP / CITATION / FUNDING /
.editorconfig.
- Curated benchmark suite (~33 representative indicators).
- Three cold-path coverage fixes (mama, rsi, sine_wave).
- bindings/node/package-lock.json now committed.
Workspace + bindings (Rust crate, Python wheel, Node main + 6 platform
sub-packages, WASM) all step to 0.3.0. CHANGELOG opens the [0.3.0]
section dated 2026-05-28 with the full Changed / Added inventory.
Compare-URL block adds the v0.2.7...v0.3.0 line under [Unreleased] and
points [Unreleased] at v0.3.0...HEAD using the new wickra-lib org.
**Supersedes PR #61** (0.2.7 -> 0.2.8 patch bump). Close#61 when this
one merges. Merge ordering remains: #59 (org migration) + #60
(family-api) + the polish PRs first, rebase this PR on top of the new
main, then merge.
Tag-push `v0.3.0` is a SEPARATE, manual step after merge — it triggers
release.yml's irreversible publish to crates.io / PyPI / npm.
A full `cargo doc --workspace` (and docs.rs, which builds with
`-D rustdoc::all`) emitted five broken intra-doc links. docs.rs treats
these as hard errors, so any 0.2.x doc build was at risk of aborting.
- mama.rs: `[`Fama`]` -> `[`crate::Fama`]` (Fama lives in another module)
- standard_error.rs: `[`crate::Bollinger`]` -> `[`crate::BollingerBands`]`
(the public type is `BollingerBands`, not `Bollinger`)
- aggregator.rs: drop the link to the private `OpenBar::into_candle`,
keep it as plain code text
- resample.rs: drop the link to the private `RolledBar::into_candle`
- csv.rs: `CandleReader::with_timestamp_parser` never existed; reword to
state plainly that ISO/RFC timestamps must be converted to integers
Verified with `RUSTDOCFLAGS="-D rustdoc::broken_intra_doc_links \
-D rustdoc::private_intra_doc_links" cargo doc --workspace --no-deps`:
clean, zero warnings.
Until now `package-lock.json` was globally ignored. Two practical
consequences for the Node binding:
- A fresh `git clone && cd bindings/node && npm install` resolved
`@napi-rs/cli` and any transitive deps to whatever the npm registry
currently considered the latest matching the package.json semver
ranges. Contributors could get different dep graphs on different days.
- No protection against transitive-dep tampering at install time
(lockfile records resolved versions + integrity hashes, npm verifies
on subsequent installs).
Drop the global `package-lock.json` ignore and commit the freshly
generated `bindings/node/package-lock.json` (140 lines, only a couple
of direct deps because the binding is small). The `.gitignore` comment
notes that we still don't expect lockfiles at the workspace root.
No CI workflow changes — the existing `npm install` in the Node-test job
will now consume the committed lockfile, which is exactly what we want.
Two modern supply-chain-trust additions to the release pipeline,
neither of which changes what gets published — only adds verifiable
signals attached to existing releases.
1. **CycloneDX SBOMs.** `cargo-cyclonedx` is installed in the
cargo-publish job after the .crate files are built, and runs once
per published crate (`wickra-core`, `wickra-data`, `wickra`). The
resulting `*.cdx.json` files are uploaded as the `sboms` artifact,
then attached to the GitHub Release alongside the existing wheels,
tarballs, .node binaries and .crate files. Future security advisories
can answer "is my version of crate X transitive in wickra Y.Z?" by
reading the SBOM directly instead of resolving the lockfile.
2. **npm `--provenance` flag** on every npm publish call:
- main `wickra` package (node-publish, first + retry)
- per-platform `wickra-<triple>` subpackages (node-publish loop)
- `wickra-wasm` (wasm-publish)
Provenance attestations are generated server-side by npm from the
GitHub Actions OIDC token. The publishing jobs gain
`permissions: id-token: write` so the runner can exchange that
token. The npm page for each published version will then carry the
"Verified provenance" badge, which proves the tarball was built by
*this* workflow run and not by an arbitrary local laptop with the
NPM_TOKEN.
Skipped deliberately (to keep this PR focused, possibly follow-ups):
- Sigstore cosign signing of artefacts (different audit story; can be
layered on after npm-provenance lands).
- SLSA build-provenance attestations via `actions/attest-build-provenance`
(would target every artefact uniformly; the npm-provenance flag is
the more pragmatic first cut).
YAML structure validated (8 jobs intact). No production code touched.
This PR conflicts with PR #59 only in line-by-line URL substitutions
on release.yml — rebase after #59 should be clean.
Wires real indicators into complete signal -> fill -> PnL -> equity
loops over the checked-in BTCUSDT datasets, with per-trade Sharpe and
max-drawdown reported on stdout. Closes the gap where existing
examples showed only the mechanics of calling `update`/`batch` but not
how Wickra plugs into a trading-system shape.
Three strategies, each in Rust + Python (six files total):
- strategy_rsi_mean_reversion — RSI(14) thresholds (30/70) on 1h
BTCUSDT. Binary position, 0.1% per-trade fee.
- strategy_macd_adx — MACD crossover entries gated by ADX(14) > 20 on
1h BTCUSDT. Trend-follower demo of multi-indicator gating.
- strategy_bollinger_squeeze — Bollinger-bandwidth 180-day-low squeeze
+ upper-band breakout entry, ATR(14) * 2 stop. On 1d BTCUSDT for
interpretable lookback.
Each file is self-contained — print_summary is inlined per script so
the example stays a single-file read. Every script prints a
NOT-financial-advice notice next to its results.
examples/README.md updated to list the new bins/scripts.
* feat(wasm): wire wasm-bindgen-test into CI and expand binding coverage
The WASM binding already had 8 wasm_bindgen_test cases checked in but
they ran only when someone manually invoked `wasm-pack test` locally —
CI never executed them, so a breakage between the Rust core API and the
JS surface would only surface in user reports.
Two changes:
1. **New CI job `wasm-test`** in `.github/workflows/ci.yml` runs
`wasm-pack test --node bindings/wasm` on every push and pull-request.
Uses Node-runtime mode (no headless browser needed), pins
`taiki-e/install-action` for `wasm-pack`, installs the
`wasm32-unknown-unknown` target.
2. **10 new `#[wasm_bindgen_test]` cases** in `bindings/wasm/src/lib.rs`
covering families that the existing 8 tests did not touch:
- Family 4 — Macd multi-output batch shape (3-component packing)
- Family 5 — Bollinger {upper, middle, lower} ordering invariant
- Family 10 — FisherTransform streaming roundtrip (recursive DSP)
- Family 16 — SharpeRatio reset semantics, MaxDrawdown monotone-
uptrend invariant, ValueAtRisk constructor validation
- Cross-family — reset returns indicator to warmup (3 shapes),
warmup_period parity with wickra-core, NaN input rejection
does not advance warmup counter
No production code changed; additive tests only. Total wasm test count
goes from 8 to 18.
* fix(wasm-tests): WasmAtr's hand-coded wrapper has no is_ready accessor
The new `reset_returns_indicator_to_warmup` test in this branch called
`atr.is_ready()` but `WasmAtr` is hand-coded (not generated by the
`wasm_scalar_indicator!` macro) and does not expose `is_ready` on the
JS surface — that pattern is reserved for macro-generated scalar
wrappers.
Replace the second leg of the test with `WasmEma` so the test exercises
two macro-generated scalars whose `is_ready`/`reset` semantics are
guaranteed by the same code path. The candle-input lifecycle is
already covered by `candle_input_streaming_matches_batch_and_lifecycle`.
Workspace clippy locally green:
- cargo clippy -p wickra-wasm --all-targets -- -D warnings
- cargo check -p wickra-wasm --target wasm32-unknown-unknown --tests
* fix(wasm-tests): Bollinger batch layout is 4 floats per bar, not 3
The new `bollinger_batch_orders_upper_mid_lower` test indexed the batch
output as `[u0, m0, l0, u1, m1, l1, ...]` (3 floats per bar) but the
actual WasmBb::batch layout is `[u0, m0, l0, sd0, u1, m1, l1, sd1, ...]`
— four floats per bar including stddev. The assertion read an upper
band against a middle from the previous bar, which can violate the
expected upper >= middle ordering and made the test fail intermittently.
Switch the stride from `* 3` to `* 4` so we read upper / middle / lower
from the same bar. Also add an `is_finite()` precondition so the test
fails with a clear "warmup positions unexpectedly NaN" message rather
than a confusing NaN-comparison if the dataset ever shrinks.
* fix(wasm-tests): drop redundant wasm-test job; existing WASM build covers it
The CI workflow already had a `WASM build` job that runs
`wasm-pack test --node bindings/wasm` after building. The
`wasm-test` job I added in cc961b3 duplicated that step and was
the only one whose `taiki-e/install-action` SHA was wrong, so it
also kept failing on a non-resolvable action reference.
Removing the duplicate keeps the test coverage (it lives in the
existing WASM build job) and gets rid of the unresolvable SHA.
Previously every push of `cargo bench -p wickra` ran 114 indicators at
three workload sizes each (1k / 10k / 50k candles), which inflated bench
runtime past ten minutes for diminishing signal — most family members
are linear scalings of the same hot loop, so a regression in any one of
them shows up identically in the cheapest member.
This commit replaces the exhaustive list with a curated selection: the
cheapest baseline and the most expensive representative from each of
the sixteen families, totalling ~33 indicators across scalar, candle,
and multi-output APIs.
If you need to profile a specific indicator that is not in the curated
set, add it temporarily and run `cargo bench -- <name>` to target just
that bench; it does not need to be committed.
Fixed in passing:
- `Cci` is `Indicator<Input = Candle>`, not f64; corrected the bench
selector to `bench_candle_input`.
- `Psar::new` takes (af_start, af_step, af_max) — supplied all three.
- `TdSequential` uses `::classic` for the textbook (4, 9, 2, 13)
parameters; the old call was missing arguments.
Five governance + onboarding files that collectively close the
"no high-level documentation outside of README" gap.
- ARCHITECTURE.md: workspace layout, Indicator trait contract,
per-indicator file conventions, numerical-stability notes,
cross-crate flow diagram, navigation cheatsheet, deliberate
non-goals, performance characteristics, stability commitments.
- ROADMAP.md: north star, 0.3 / 0.4 / 0.5 release windows,
indicator-wishlist intake rules, explicit non-goals, versioning policy.
- CITATION.cff: GitHub-renders as "Cite this repository" button;
enables academic adoption.
- .github/FUNDING.yml: surfaces Sponsor button on the repo page.
- .editorconfig: normalises indent/EOL across IDEs (Rust/Python 4 spaces,
JS/TS/JSON/YAML/MD 2 spaces, Makefile tabs).
Touches no Rust source, no CI workflows, no behaviour. Additive only.
URLs in ROADMAP/CITATION already reference the post-transfer
`wickra-lib/wickra` org so the files stay valid once the org migration
in PR #59 lands; merge this PR only after #59 to keep main consistent.
* fix(bindings): clear clippy pedantic lints and lint bindings in CI
Resolve the pedantic lints that only surfaced under a full
`cargo clippy --workspace` (the CI clippy job covered only the core
crates, so the Python/Node bindings drifted):
- manual_midpoint: `(a + b) / 2.0` -> `f64::midpoint(a, b)` (node + python)
- new_without_default: add `Default` impls for the six no-arg Node nodes
- type_complexity: factor the pivot/Ichimoku return tuples into
`PivotLevels` / `WoodieLevels` / `IchimokuLines` aliases (python)
- many_single_char_names: allow at crate level — OHLCV batch helpers bind
the conventional o/h/l/c/v column names
Add a dedicated `clippy-bindings` CI job (ubuntu-only, with Python + Node
toolchains) so future binding lints fail CI instead of slipping through.
* ci: lint Python and Node bindings in a dedicated clippy job
The main `rust` job's clippy step only covers wickra-core/wickra/
wickra-data/wickra-wasm, so pedantic lints in the PyO3/napi bindings
slipped through. Add an ubuntu-only `clippy-bindings` job that
provisions Python + Node (needed by the build scripts) and runs
`cargo clippy -p wickra-node -p wickra-python --all-targets -- -D warnings`.
* feat(family-api): add FAMILIES const and family-taxonomy tests
Introduce `wickra_core::FAMILIES`, a `&'static [(&str, &[&str])]` mapping
every built-in indicator to one of 16 families (Moving Averages, Momentum
Oscillators, Trend & Directional, Price Oscillators, Volatility & Bands,
Bands & Channels, Trailing Stops, Volume, Price Statistics, Ehlers /
Cycle (DSP), Pivots & S/R, DeMark, Ichimoku & Charts, Candlestick
Patterns, Market Profile, Risk / Performance).
Two compile-time-anchored guards make sure the const stays trustworthy:
- `no_duplicates_across_families`: no indicator is listed in two families
- `total_count_matches_expected`: hard-coded total bumps in lockstep with
the indicator catalogue (214 today), so any new indicator added without
being filed under a family trips the test.
Also corrects the module doc comment which falsely claimed
indicators were grouped by category internally; the `mod` block is
alphabetical, and the canonical taxonomy now lives in `FAMILIES`.
* chore: sync indicator count to 214
---------
Co-authored-by: wickra-bot <wickra-bot@users.noreply.github.com>
Introduce repo-metadata.toml as single source of truth for repo identity
(org slug, maintainer email, canonical URLs) and add sync-metadata.yml
workflow with a Python audit script that fails CI if any tracked file
drifts back to pre-migration values.
Bulk-replace across 24 tracked files:
- kingchenc/wickra -> wickra-lib/wickra (URL segment)
- kingchencp@gmail.com -> wickra.lib@gmail.com (maintainer email)
- @kingchenc -> @wickra-lib (CODEOWNERS mention only)
Person-name credits are preserved: LICENSE copyright holder, Cargo.toml
authors handle, and CHANGELOG historical @kingchenc reference all remain
unchanged. Crate / PyPI / npm package names also untouched.
Merge this PR only after the kingchenc/wickra -> wickra-lib/wickra org
transfer has happened on the GitHub side, otherwise all badges and
repository links 404 until the transfer is performed.
* chore(sync-about): count public indicator types, not module files
The sync-about workflow counted `mod xxx;` lines in
crates/wickra-core/src/indicators/mod.rs to derive the indicator count
that gets propagated to README, the GitHub About description, and the
wiki. That under-reported by one because `vwap.rs` exports two public
types — `Vwap` (cumulative) and `RollingVwap` (finite window) — from
the same module. The bindings reach both, so users see 214 indicators
even though there are only 213 source files.
Fix the count by parsing the canonical `pub use indicators::{ ... }`
block in lib.rs, dropping the `FAMILIES` constant and any `*Output`
companion structs, and counting the remaining public types. Pure-shell
implementation so the workflow doesn't grow a python dependency.
Sync README to the corrected count (213 -> 214) in the same commit so
the PR validates cleanly through the workflow's PR-flow.
* docs(bindings): sync per-binding READMEs and docs/ pointer to 214 / 16
The bindings/{node,python,wasm}/README.md files (which ship to
npm / PyPI / npm again) and docs/README.md (the pointer to the
wiki) all carried the stale '71 streaming-first indicators across
eight families' header from before the family expansion. Pull the
canonical 16-family table out of the main README into all three
binding READMEs and update docs/README.md to list every family by
name, so a user landing on PyPI / npm sees the current catalogue
shape.
* feat(family-15): add 17 risk/performance metrics
Implements Family 15 pragmatically as standard `Indicator`s instead of a
separate `wickra-metrics` crate. Input is scalar `f64` per bar — period
return, equity sample, or per-trade P&L depending on the metric.
Scalar `Indicator<f64>` (14):
- SharpeRatio(period, risk_free)
- SortinoRatio(period, mar)
- CalmarRatio(period)
- OmegaRatio(period, threshold)
- MaxDrawdown(period) — rolling, peak-to-trough
- AverageDrawdown(period)
- DrawdownDuration — cumulative, bars under water (u32 output)
- PainIndex(period)
- ValueAtRisk(period, confidence)
- ConditionalValueAtRisk(period, confidence)
- ProfitFactor(period)
- GainLossRatio(period)
- RecoveryFactor — cumulative, net return / max drawdown
- KellyCriterion(period)
Two-series `Indicator<(f64, f64)>` for (asset, benchmark) returns (3):
- TreynorRatio(period, risk_free)
- InformationRatio(period)
- Alpha(period, risk_free) — Jensen / CAPM
Touchpoints:
- 17 new files under `crates/wickra-core/src/indicators/`.
- `mod.rs` + `lib.rs` re-exports.
- Python bindings (`bindings/python/src/lib.rs`, `__init__.py`).
- Node bindings (`bindings/node/src/lib.rs`, `index.js`).
- WASM bindings (`bindings/wasm/src/lib.rs`).
- Fuzz: scalar metrics appended to `indicator_update.rs`; new
`indicator_update_pair.rs` fuzz target for `(f64, f64)` indicators.
- Python tests: SCALAR + new PAIR parameter lists in `test_new_indicators.py`,
reference-value cases in `test_known_values.py`.
- Node tests: scalar factories + new pair-factory block in
`bindings/node/__tests__/indicators.test.js`.
- Benches: 5 Family-15 benches added in `crates/wickra/benches/indicators.rs`.
- Docs: README family-table row + counter (71 -> 88), CHANGELOG entry under
[Unreleased].
Note: Family 12 (statistik-regression, PR #51) introduces
`node_pair_indicator!` and `wasm_pair_indicator!` macros for Pearson /
Beta / Spearman. Family 15 needs the same pair-input pattern but Family 12
is not yet in main, so the three pair wrappers below are written by hand
in this PR. When PR #51 lands, the trivial merge-conflict is resolved by
keeping the macros from Family 12 and re-using them for Treynor / IR /
Alpha (drop the three handwritten wrappers).
cargo check --workspace --all-features: green.
* fix(family-15): satisfy clippy doc_markdown / if_not_else / digit_grouping
* fix(family-15): unused TreynorRatio import, duplicate pairFactories, _eq_nan inf handling
* fix(family-15): node eq() handles matching infinities for ratio indicators
* test(family-15): cover cold paths flagged by codecov patch
* feat(family-16): add ValueArea + InitialBalance + OpeningRange
Opens family #16 (Market Profile) with the three OHLCV-compatible scalar /
multi-output indicators:
- ValueArea(period, bin_count, value_area_pct) -> {poc, vah, val}.
Rolling bin-approximation volume profile over the last `period`
candles. Each candle's volume is spread uniformly across [low, high];
POC is the bin with highest cumulative volume; the value area expands
symmetrically from POC and always absorbs the higher-volume neighbour
next, until `value_area_pct` (default 0.70) of total volume is
enclosed. Defaults (20, 50, 0.70).
- InitialBalance(period) -> {high, low}. Tracks session-opening high
and low over the first `period` bars, then locks. Default period = 12
(one-hour IB on 5-minute bars for US equities). Callers MUST invoke
reset() at every session boundary, otherwise IB stays fixed for the
lifetime of the instance.
- OpeningRange(period) -> {high, low, breakout_distance}. Same
lock-after-N-bars semantics as IB with a shorter default period
(6 = 30 min on 5-minute bars) and a third output that tracks
close - or_mid (positive above the range mid, negative below).
Histogram-output Market Profile variants (Volume Profile, VPVR,
Composite Profile) are deferred because they need a new histogram
output API layer rather than fixed-arity scalars. Tick-data-only
variants (TPO Profile, Single Print, Order Flow Delta, Cumulative
Delta, Volume-Weighted Open) are out of scope because `wickra-data`
does not currently expose tick / L2 data.
All four bindings (Rust core, Python, Node, WASM) ship the new
indicators with parity tests; benches added; fuzz target extended.
Counter 71 -> 74 across 8 -> 9 families. cargo check --workspace
--all-features green.
* fix(family-16): cover cold paths in InitialBalance + ValueArea
InitialBalance::value() public getter had no test covering the post-update
Some(...) branch — extended accessors_and_metadata to call value() after one
update. ValueArea single-print bar path (c.high == c.low) was unreachable in
existing tests since the only single-print test used a uniform 100-price
window which exits early via the span == 0 guard; added a mixed-window test
that triggers the c.high <= c.low branch directly. The (None, None) arm of
the expansion match was by-construction unreachable (the loop condition
already requires at least one neighbour) and has been folded into an
if/else.
* feat(family-12): add 13 Statistik/Regression indicators
Brings the Price Statistics family to 20 indicators (7 → 20) and the
total catalogue to 84 (71 → 84). Every indicator ships in the Rust
core plus Python, Node, and WASM bindings with full streaming ↔ batch
parity, fuzz coverage, and benches.
Scalar (f64 → f64):
- Variance, CoefficientOfVariation: rolling population variance and
its dimensionless ratio with the mean. O(1) updates.
- Skewness, Kurtosis: rolling Pearson skewness and excess kurtosis,
derived from running sums of x, x², x³, x⁴ via the binomial
identities — also O(1) per bar.
- StandardError, DetrendedStdDev: standard error of estimate (n − 2)
and population StdDev (n) of OLS residuals, sharing the LinReg
O(1) sliding sums.
- RSquared: coefficient of determination of the rolling OLS fit; the
trend-quality filter, clamped to [0, 1].
- MedianAbsoluteDeviation: robust dispersion estimator; O(period log
period) per emission via two in-place sorts of a reusable scratch
buffer.
- Autocorrelation(period, lag): rolling lag-k Pearson autocorrelation.
- HurstExponent(period, chunks): R/S-analysis trend-persistence
estimator clamped to [0, 1].
Pair indicators (Input = (f64, f64)):
- PearsonCorrelation: rolling cross-series Pearson, O(1).
- Beta: rolling OLS slope of asset vs. benchmark (CAPM).
- SpearmanCorrelation: rolling rank correlation with mid-rank tie
handling; O(period log period).
Touchpoints:
- crates/wickra-core: 13 new indicator modules + mod.rs / lib.rs
re-exports.
- bindings/python: pyclasses + add_class registration + __init__.py
import & __all__ updates. The pair indicators expose
update(x, y) and batch(x, y) over two equally-sized numpy arrays.
- bindings/node: scalar indicators via node_scalar_indicator! macro;
pair indicators via new node_pair_indicator! macro; explicit
structs for Autocorrelation and HurstExponent (two-arg ctors).
index.js extended with the new exports.
- bindings/wasm: scalar wrappers via wasm_scalar_indicator!; pair
wrappers via new wasm_pair_indicator! macro.
- fuzz: every scalar drove through the generic helper; pair
indicators stress-tested by pairing adjacent samples of the fuzz
input.
- Python tests (test_new_indicators.py): added to SCALAR
parametrisation, plus algebraic reference values
(variance of [2,4,6] = 8/3, MAD ignoring outlier = 0, monotone
non-linear Spearman = 1, two-to-one Beta = 2, etc.) and a
streaming-vs-batch test for the pair indicators.
- Node tests (indicators.test.js): extended the scalar factories
map and added a pair-indicator section with the same algebraic
reference values.
- crates/wickra/benches: bench_scalar entries for all 10 single-
input new indicators.
- README: counter 71 → 84; Price Statistics family-table row
expanded with the 13 new indicators.
- CHANGELOG: Unreleased section documents the family addition.
Wiki drafts (ghost-ignored, manual sync to wickra.wiki at release
time): indicator-ideas/families/wiki/family-12-statistik-regression/
contains 13 deep-dive pages plus _Sidebar / Indicators-Overview /
Warmup-Periods / Home fragments for the curator merge.
cargo check --workspace --all-features: clean.
* fix(family-12): remove unreachable defensive guards in hurst_exponent
The three guards (m < 2 continue, end > buf.len() break, denom == 0.0
return) are by-construction unreachable given the constructor invariant
period >= 2 * chunks: m = period / k for k in 1..=chunks always
satisfies m >= 2 and end = (c+1) * m <= k * m <= period = buf.len(),
and m_1 = period and m_2 = period / 2 are always distinct so the slope
denominator is strictly positive. Removing them brings codecov/patch
back to 100%.
Two new indicators in a brand-new "Ichimoku & alternative charts"
family:
- `Ichimoku` (Ichimoku Kinko Hyo): the full five-line cloud system
(Tenkan-sen, Kijun-sen, Senkou Span A/B, Chikou Span). Classic
(9, 26, 52, 26) defaults; configurable. Forward displacement is
handled in an O(1) ring buffer so the visible Senkou A/B at bar n
are the values computed at bar n-displacement.
- `HeikinAshi`: recursive candle smoothing transform emitting a
four-field synthetic candle. Seeds ha_open from (open+close)/2 on
the first bar.
Touchpoints: core + unit tests, mod.rs/lib.rs re-exports, Python +
Node + WASM bindings (multi-output via PyArray2 / interleaved Vec<f64>
/ Object+Float64Array), Python tests across smoke/new-indicators/
input-validation, Node parity tests, fuzz target (Candle), benches,
README family table + counter (71 -> 73, 8 -> 9 families), CHANGELOG.
Note: Renko, Kagi, and Point & Figure from the family-13 ideas list
are intentionally skipped. They are bar generators (the bar boundary
is defined by price moves, not by a fixed time interval) rather than
indicators that consume a candle stream, and belong in wickra-data
as candle/tick transforms alongside the existing tick-to-candle
aggregator and resampler.
Add tests that exercise the protective fallbacks reachable via flat or
zero-valued input:
- `CenterOfGravity::zero_window_uses_zero_fallback` — den == 0 branch.
- `EhlersStochastic::flat_window_emits_zero` — range == 0 branch.
- `Mama::flat_input_uses_phase_fallback` and the matching
`SineWave` variant — `i1` collapses to zero on a constant series.
- `Fama::new_with_valid_limits_constructs_via_mama` exercises the
`Ok(Self { inner: Mama::new(..)? })` arm that no other test reaches.
Three branches were genuinely unreachable by construction, so the dead
code is removed rather than masked with an attribute:
- `Mama` clamped `alpha > fast_limit` after the lower-bound clamp; the
upper bound is implied by `delta_phase >= 1` and `alpha = fast / delta_phase`.
- `CyberneticCycle` had a `0.0` fallback after the warmup gate that the
3-slot ring buffers preclude (`count >= 7` => all five `Some`s).
- `DecyclerOscillator` used a `let-else { return None }` over a pair of
`Decycler::update` calls that always emit `Some` from the first bar.
Implements Family 10 (Ehlers / Cycle) end-to-end across Rust core,
Python / Node / WASM bindings, fuzz, tests, benches and docs. This
is an entirely new family covering John Ehlers' digital-signal-
processing school of cycle analytics — a strong differentiator
versus TA-Lib and pandas-ta, which ship only fragments.
Indicators:
- MAMA (Mesa Adaptive MA) — multi-output { mama, fama }
- FAMA (Following Adaptive MA) — scalar wrapper around MAMA's slow line
- Fisher Transform — Gaussian-normalising price transform
- Inverse Fisher Transform — bounded oscillator (tanh-based)
- SuperSmoother — 2-pole Butterworth lowpass
- Roofing Filter — high-pass + SuperSmoother bandpass
- Decycler — price minus 2-pole high-pass (lag-free trend)
- Decycler Oscillator — fast / slow Decycler difference (MACD-like)
- Hilbert Dominant Cycle — phase-derived period estimator [6, 50]
- Sine Wave Indicator — sin(phase) with 45° lead companion
- Adaptive Cycle Indicator — half-period driver for adaptive oscillators
- Center of Gravity Oscillator — weighted-mass momentum
- Cybernetic Cycle Component — EasyLanguage classic
- Empirical Mode Decomposition — bandpass + envelope mean
- Ehlers Stochastic — Stochastic on Roofing Filter input, [-1, +1]
- Instantaneous Trendline — Ehlers 2-pole lag-free trend
Indicator count rises 71 -> 87 across nine families (was eight).
All sixteen pass batch == streaming equivalence, expose the standard
Indicator surface (update / batch / reset / is_ready / warmup_period
/ name), are fuzz-tested, benchmarked against the checked-in BTCUSDT
1-minute dataset and reach across all four bindings.
Wiki deep-dive drafts for every indicator + Sidebar / Overview /
Home / Warmup updates are staged under indicator-ideas/families/
wiki/family-10-ehlers-cycle/ in the main repo (ghost-ignored) for
the maintainer to publish to the wiki repo manually.
* feat(family-11): add DeMark suite (TD Setup, Sequential, DeMarker, REI, Pressure)
Family 11 (DeMark) was previously empty; this PR adds five
streaming-first DeMark indicators in one batch.
- **TD Setup** (`TdSetup`): parameterised buy/sell setup counter.
Counts consecutive bars whose close is less-than (buy) or
greater-than (sell) the close `lookback` bars earlier, saturating
at `target`. Emits a signed `f64` so callers read direction from
the sign and run length from the magnitude. Classic config:
`lookback = 4`, `target = 9`.
- **TD Sequential** (`TdSequential`): the canonical Setup + Countdown
exhaustion pattern. Output struct `{ setup, countdown, direction }`
exposes both phase counts as signed numbers plus the active
countdown direction (+1 buy / -1 sell / 0 none). Countdown
activates when a setup completes and tracks the close-vs-high/low
comparison `countdown_lookback` bars back, capped at
`countdown_target`. Classic: 4/9/2/13.
- **TD DeMarker** (`TdDeMarker`): bounded [0, 1] oscillator from the
rolling average of upward high expansion (DeMax) and downward low
expansion (DeMin). Falls back to the neutral 0.5 on a flat market
(denominator zero).
- **TD REI** (`TdRei`): Range Expansion Index, bounded [-100, 100].
Per-bar numerator gated on a range-overlap condition vs the bars
5 and 6 back, normalised by a `period`-bar sum of absolute moves.
Classic period = 5. Saturates at +100 in a slow steady uptrend
and at -100 in the mirror downtrend; emits 0 on a flat market.
- **TD Pressure** (`TdPressure`): volume-weighted buying / selling
pressure normalised to [-100, 100]. Per-bar pressure is the
intra-bar close-vs-open ratio scaled by volume; the output is the
rolling mean divided by the rolling mean volume. Zero-range bars
contribute zero (avoid the undefined ratio) and a flat zero-volume
window falls back to 0.
Bindings: all five exposed in Python (`ta.TDSetup`, `ta.TDSequential`,
`ta.TDDeMarker`, `ta.TDREI`, `ta.TDPressure`), Node (`wickra.TDSetup`
etc.), and WASM. Multi-output classes (`TDSequential`) return either
a struct `{ setup, countdown, direction }` per bar (streaming) or a
flat interleaved Float64Array of length `3 * n` (batch).
Tests: 47 unit tests across the five new core files (pure-trend
saturation, flat-market neutral fallback, batch-equals-streaming,
zero-parameter rejection, reset semantics, accessors). Python
test_new_indicators.py picks up all five plus a multi-output TD
Sequential block. Node indicators.test.js picks up all five.
Reference values added to test_known_values.py.
Fuzz: candle fuzz target sweeps all five DeMark indicators with the
existing `Vec<f64>` -> `Vec<Candle>` driver.
Benches: BTCUSDT 1-minute dataset benches for each DeMark indicator
in `crates/wickra/benches/indicators.rs`.
Docs: README family table gains a "DeMark" row; indicator counter
bumped 71 -> 76. CHANGELOG entry added under [Unreleased]. Wiki
drafts (deep-dive pages + Sidebar / Overview / Warmup-Periods / Home
deltas) live under `indicator-ideas/families/wiki/family-11-demark/`
for manual merge into the wiki repo.
* feat(family-11): add 7 missing DeMark indicators
Complete the DeMark suite (family 11) with the seven indicators not
covered by the first commit: TD Combo, TD Countdown, TD Lines (TDST),
TD Range Projection, TD Differential, TD Open, and TD Risk Level.
- TdCombo: aggressive countdown variant with three strictness rules
on top of the classic close-vs-low/high lookback rule (monotone
low/high, monotone close vs prior bar).
- TdCountdown: standalone 13-bar countdown packaging only the signed
countdown count (the setup machine runs internally).
- TdLines: TDST horizontal support/resistance levels from the
highest-high / lowest-low bars of the most-recently-completed
setup, exposed as a multi-output struct.
- TdRangeProjection: DeMark X-projection of the next bar's high and
low from the current bar's OHLC via an open-vs-close-weighted
pivot (three branches: close<open, close>open, close==open).
- TdDifferential: two-bar buying-pressure vs selling-pressure
reversal pattern emitting +1/-1/0.
- TdOpen: gap-and-fade reversal pattern (open outside prior range
with subsequent recovery into it) emitting +1/-1/0.
- TdRiskLevel: protective stop levels derived from the setup
extreme bar +/- its true range.
All seven are wired through Rust core, Python, Node and WASM
bindings, registered in the candle-stream fuzz target, given
benchmark entries on the BTCUSDT 1-minute dataset, and covered by
streaming-vs-batch equivalence, reference-value, lifecycle and
input-validation tests on the Python and Node sides. README counter
moves 76 -> 83 and the CHANGELOG "family 11" entry is extended to
list all twelve indicators.
* fix(td_risk_level tests): check first emission at idx 12, not last bar
TdRiskLevel re-ratchets the sell-risk level on each subsequent setup
completion, so a strictly rising series produces 22.0 at idx 19 (latest
setup) rather than 15.0 (first setup). The test comment already named
idx 12 as the reference; switch the assertion from out[-1] to out[12]
to match the reference computation.
* test(family-11): cover buy-direction branches in TD indicators
Add downtrend tests to TdSequential, TdCombo and TdCountdown so the
buy-side countdown/combo increment branches are exercised; remove an
empty `if buy_countdown == target {}` block in TdSequential whose
behavior is already enforced by the outer strict `<` guard.
Closes codecov/patch gaps reported on PR #48 (10 missed lines across
the three files).
* feat(family-08): add Classic, Fibonacci, Camarilla, Woodie and DeMark pivots + Williams Fractals + ZigZag
Seven new indicators land the previously empty Pivots & S/R family
(family 08), each implemented in wickra-core with the full Indicator
trait surface (update / reset / warmup_period / is_ready / name),
exposed across Python (PyO3), Node (napi-rs) and WASM (wasm-bindgen)
with the standard streaming + batch APIs, and covered by Rust unit
tests, Python streaming-vs-batch + reference-value tests, Node
streaming-vs-batch tests, the candle-input fuzz target and Rust
microbenchmarks.
- ClassicPivots (7 levels): PP = (H+L+C)/3, three R/S tiers per the
floor-trader formulas.
- FibonacciPivots (7 levels): PP plus R/S spaced by 0.382 / 0.618 /
1.000 of the prior range.
- Camarilla (9 levels): Nick Stott's four-tier `C +/- (H - L) * 1.1 /
{12, 6, 4, 2}` levels.
- WoodiePivots (5 levels): close-weighted PP = (H + L + 2*C) / 4 plus
two R/S tiers.
- DemarkPivots (3 levels): conditional X sum based on the previous
bar's open-vs-close relationship.
- WilliamsFractals: five-bar swing detector emitting optional up/down
fractal prices at the centre of each window.
- ZigZag: percent-threshold swing tracker, non-repainting; emits the
just-completed extreme and direction on confirmed reversals only.
README family table updated to nine families / 78 indicators;
CHANGELOG records the family-08 addition under [Unreleased].
* fix(family-08 tests): unify MULTI dict to 3-tuple (factory, batch_call, k)
The HEAD-side family-08 test parametrised MULTI[name] as
`(factory, batch_call, output_arity)` so that pivots with arity 3/5/7/9
fit the same harness. Main's entries arrived as 2-tuples; convert them
all to the 3-tuple shape so `make, batch_call, k = MULTI[name]` unpacks
cleanly. Lifecycle test now indexes the tuple instead of destructuring.
* test(zig_zag): tighten flat-oscillation test (drop dead counter branch)
The previous version of `small_oscillations_yield_no_swings` counted
emitted swings, but the assertion proves the counter never increments
so codecov flagged `emitted += 1` as uncovered. Switch to a per-bar
`assert!(...is_none())` — same coverage of the no-swing path, no dead
branch.
* feat(family-09): add 7 trailing stops (HiLo, Volty, Yo-Yo, Donchian, Pct, Step, Renko)
Rounds out the Trailing Stops family from 5 to 12 indicators:
- HiLoActivator (Crabel): SMA-of-high/SMA-of-low trail with a one-bar
lag; emits the opposite-side SMA as the trailing stop.
- VoltyStop (Cynthia Kase): ATR trail anchored on the extreme close
since the trade was opened — tighter than AtrTrailingStop on
pullbacks.
- YoyoExit: long-only ATR trail with an explicit re-entry trigger at
trail + multiplier*ATR; exposes an in_trade flag.
- DonchianStop (Turtle): lowest low / highest high over the window;
multi-output {stop_long, stop_short}.
- PercentageTrailingStop: fixed-percent trail that scales across
instruments without per-asset tuning.
- StepTrailingStop: snaps to a step_size-aligned grid; mirrors
discretionary stop-by-hand workflow.
- RenkoTrailingStop: block-anchored trail; only moves on full-block
advances, ignores intra-block noise.
All seven are wired into wickra-core, the Python / Node / WASM
bindings, the indicator_update + indicator_update_candle fuzz targets,
the wickra bench harness, and the Python + Node test suites. README
counter bumps from 71 to 78; CHANGELOG entry under [Unreleased].
* fix(family-09): satisfy pedantic clippy lints
- hilo_activator: rewrite match-Some/None as if-let-else (single_match_else),
add backticks around the HiLo identifier in module/struct doc (doc_markdown).
- percentage / step / renko trailing stop tests: use f64::from(i32) instead
of `as f64` (cast_lossless).
- bench `benches()` is now >100 lines after Family 09 was wired in; allow
too_many_lines (matches the python pymodule fn).
* feat(kvo): add Klinger Volume Oscillator
Stephen J. Klinger's trend-aware volume-force MACD. Each bar produces a 'volume force' (vf) signed by the local trend (+1 / -1 / carry) and scaled by the ratio of the current accumulation horizon to its previous trend. KVO = EMA(vf, fast) - EMA(vf, slow), classic (34, 55).
Rust core (Kvo) with 7 unit tests (rejects zero / fast>=slow, accessors, constant series collapses to 0, warmup lands at slow+1, batch == streaming, reset clears state), plus Python (PyKvo + KVO export), Node (KvoNode), and WASM (WasmKvo) bindings. Fuzz target adds Kvo to the candle-input sweep, bench adds the candle-input KVO benchmark, README counter 71 -> 72 + family table row, CHANGELOG [Unreleased].
* feat(volume-oscillator): add Volume Oscillator (VO)
Percent difference between a fast and a slow SMA of the bar volume: 100 * (SMA(vol, fast) - SMA(vol, slow)) / SMA(vol, slow). Default (14, 28). The line stays near zero in stable conditions; positive readings show rising short-term participation, negative readings show waning interest.
Rust core (VolumeOscillator) with 8 unit tests (period validation, accessors, constant volume == 0, zero-volume window defensive branch, two reference values verified algebraically, batch == streaming, reset), plus Python (PyVolumeOscillator + VolumeOscillator export), Node (VolumeOscillatorNode), and WASM (WasmVolumeOscillator) bindings. Fuzz target adds VolumeOscillator to the candle-input sweep, bench adds the volume_oscillator benchmark, README counter 72 -> 73 + family table row, CHANGELOG [Unreleased].
* feat(nvi-pvi): add Negative & Positive Volume Index
Paul Dysart's cumulative volume-flow indices, popularised by Norman Fosback in 'Stock Market Logic'. Both run from a 1000.0 baseline and only update on a specific direction of volume change:
- NVI updates on volume-contraction bars (volume_t < volume_{t-1}), absorbing the percent close change. Tracks the 'smart money' leg per Fosback.
- PVI updates on volume-expansion bars (volume_t > volume_{t-1}). Tracks the 'crowd' leg.
Both expose with_baseline(f64) for custom starting indexes. The NVI/PVI pair is listed as a single line in indicator-ideas/families/07-volume.md and shares the same lifecycle/test/binding surface, so they ship as one commit.
Rust core (Nvi, Pvi) with 9 unit tests each (accessors, baseline seed, volume direction branches, zero-prev-close guard, custom baseline, batch == streaming, reset), plus Python (PyNvi/PyPvi + NVI/PVI exports), Node (NviNode/PviNode), and WASM (WasmNvi/WasmPvi) bindings. Fuzz target adds Nvi+Pvi to the candle-input sweep, bench adds nvi+pvi entries, README counter 73 -> 75 + family table row, CHANGELOG [Unreleased].
* feat(family-07): add Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index
Finishes the volume-flow family with the remaining (new) entries from
indicator-ideas/families/07-volume.md.
Indicators added:
- Williams A/D (`WilliamsAD`): Larry Williams' volume-less cumulative
accumulation/distribution line. Anchors each bar's contribution to
the previous close via true-high/true-low (gap-aware).
- Anchored VWAP (`AnchoredVwap`): cumulative VWAP whose accumulation
starts at a user-chosen anchor bar. Exposes `set_anchor()` (queued
to the next `update`) for click-to-anchor workflows. Reset clears
both state and pending-anchor flag.
- Demand Index (`DemandIndex`): James Sibbet's smoothed buying-vs-
selling pressure, in the streaming-friendly textbook form
`EMA(volume * close-return * (1 + range/close), period)`.
- Time Segmented Volume (`Tsv`): Don Worden's rolling window-sum of
`(close_t - close_{t-1}) * volume_t`. Default `period = 18`.
- Volume Zone Oscillator (`Vzo`): Walid Khalil's normalised volume-flow
oscillator bounded in `[-100, +100]`, defined as
`100 * EMA(signed_volume) / EMA(volume)`.
- Market Facilitation Index (`MarketFacilitationIndex`): Bill Williams'
per-bar `(high - low) / volume`. Returns `None` on zero-volume bars.
All six indicators ship with unit tests (`rejects_zero_period` where
applicable, `accessors_and_metadata`, constant-series behaviour,
batch == streaming equivalence, reset semantics, and reference-value
or saturation-extreme tests), Python / Node / WASM bindings, fuzz
coverage in `indicator_update_candle`, a `bench_candle_input` line per
indicator, README + CHANGELOG entries, and Python reference-value
tests in `test_new_indicators.py`.
The README indicator counter advances 75 -> 81.
* test(family-07): cover defensive cold paths + Default impls
- ad_oscillator: exercise `value()` after first emission.
- kvo: cover the `cm == 0.0` zero-OHLC defensive branch.
- nvi / pvi: exercise the Default impls.
* feat(adxr): add Wilder Average Directional Movement Index Rating
ADXR is the trend-strength smoother Wilder published alongside ADX in
*New Concepts in Technical Trading Systems* (1978):
ADXR_t = (ADX_t + ADX_{t - (period - 1)}) / 2
The lookback length is the same period that feeds the underlying ADX.
Because the older ADX is period - 1 bars stale, ADXR responds more
slowly than ADX and is the canonical metric for comparing
trend-strength across instruments.
Implementation reuses the existing wickra_core::Adx engine plus a
period-length ring of past ADX values; warmup is 3 * period - 1
(41 for period = 14). Bindings: Python PyAdxr (PyArray1 batch),
Node AdxrNode (number scalar), WASM WasmAdxr. Fuzz target covers
the candle-input path. Python + Node streaming-vs-batch tests
parametrised, plus a pure-uptrend reference value (ADXR == 100
when ADX saturates at 100). Criterion bench added under crates/
wickra/benches/indicators.rs.
README family table and indicator counter updated (71 -> 72).
* feat(rwi): add Mike Poulos Random Walk Index
RWI compares actual price displacement to what a random walk would
produce over the same horizon: for each lookback i in [2, period],
RWI_High_t(i) = (high_t - low_{t-i+1}) / (ATR_i(t) * sqrt(i))
RWI_Low_t(i) = (high_{t-i+1} - low_t) / (ATR_i(t) * sqrt(i))
Per-bar output is the maximum across lookbacks for each direction;
a reading > 1 means the trend beats random-walk noise, > 2 is the
typical strong-trend threshold. Multi-output (high, low). period
must be >= 2 (the shortest meaningful lookback); period < 2 returns
InvalidPeriod. Warmup = period (e.g. 14 for the standard default).
Bindings: Python PyRwi (PyArray2 shape (n, 2)), Node RwiNode +
RwiValue struct, WASM WasmRwi (Object/Reflect for update,
Float64Array interleaved for batch). Fuzz target adds the candle
input case. Python parametric streaming-vs-batch test and pure
uptrend reference test (RWI_High dominates RWI_Low and exceeds 1).
Node parametric streaming-vs-interleaved-batch test. Criterion
bench under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (72 -> 73).
* feat(tii): add M.H. Pee Trend Intensity Index
TII is a [0, 100] oscillator that asks 'what fraction of the recent
SMA deviations are positive?'. The construction is
dev_t = close_t - SMA(close, sma_period)_t
SD_pos = sum of positive dev_t over the last dev_period bars
SD_neg = sum of |negative dev_t| over the last dev_period bars
TII = 100 * SD_pos / (SD_pos + SD_neg)
Saturates at 100 on a pure uptrend (every close above the lagging
SMA), at 0 on a pure downtrend, and returns the neutral mid-point 50
on a perfectly flat window. The output is clamped to [0, 100] as
the rolling-sum subtraction loop can accumulate a few ULP of error
on long histories. Canonical Pee parameters (sma_period=60,
dev_period=30) wired as Python defaults; warmup is
sma_period + dev_period - 1 (89 for the defaults).
Bindings: Python PyTii (PyArray1 batch), Node TiiNode (scalar
update + batch), WASM WasmTii via the two-arg wasm_scalar_indicator!
macro. Fuzz target adds the scalar path. Python parametric
streaming-vs-batch test plus pure-uptrend (TII == 100) and
flat-market (TII == 50) reference tests. Node parametric
streaming-vs-batch test. Criterion bench under crates/wickra/
benches/indicators.rs.
README family table and indicator counter updated (73 -> 74).
* feat(kst): add Pring Know Sure Thing oscillator
KST is Martin Pring's long-horizon momentum gauge: four smoothed
rate-of-change components combined with fixed weights (1, 2, 3, 4),
plus an SMA signal line.
RCMA_i = SMA(ROC(close, roc_i), sma_i) for i in 1..=4
KST = 1*RCMA_1 + 2*RCMA_2 + 3*RCMA_3 + 4*RCMA_4
Signal = SMA(KST, signal_period)
Kst::classic() exposes Pring's recommended parameter set
(roc = (10, 15, 20, 30), sma = (10, 10, 10, 15), signal = 9);
warmup = max(roc_i + sma_i) + signal_period - 1 (53 for the classic
parameters). All four parallel branches are fed unconditionally so
they warm in lock-step.
Bindings: Python PyKst (PyArray2 shape (n, 2)) with a KST.classic()
staticmethod, Node KstNode + KstValue with a KST.classic() factory,
WASM WasmKst with both new(...) and classic() constructors plus
Object/Reflect for update and Float64Array for batch. Fuzz target
adds the scalar multi-output path. Python tests gain a new
MULTI_SCALAR section parametric over scalar-input/multi-output
indicators, plus a classic-on-constant-series reference test. Node
tests gain a KST entry in the multi-output section. Criterion
benchmark added under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (74 -> 75).
* feat(wave-trend): add LazyBear Wave Trend Oscillator
Two-line mean-reverting momentum gauge built from the typical price
and three cascaded EMAs:
ap = (high + low + close) / 3
esa = EMA(ap, channel_period)
d = EMA(|ap - esa|, channel_period)
ci = (ap - esa) / (0.015 * d)
wt1 = EMA(ci, average_period)
wt2 = SMA(wt1, signal_period)
WaveTrend::classic() exposes LazyBear's defaults
(channel = 10, average = 21, signal = 4); warmup is
2 * channel_period + average_period + signal_period - 3 (42 for the
classic defaults). On a perfectly flat market the SMA-seeded EMA
introduces a single-ULP drift between ap and esa, which on a tiny d
would make the ratio explode to -1/0.015 = -66.67; a price-scaled
flat-tolerance guard (d <= 16 * EPSILON * max(|esa|, 1)) collapses
the channel index to 0 in that regime so both lines remain at zero.
Bindings: Python PyWaveTrend (PyArray2 shape (n, 2)) with a
WaveTrend.classic() staticmethod, Node WaveTrendNode + WaveTrendValue
with a WaveTrend.classic() factory, WASM WasmWaveTrend with both
new(...) and classic() constructors. Fuzz target adds the candle
multi-output path (sorted alphabetically). Python parametric
streaming-vs-batch test plus a flat-market reference test. Node
parametric streaming-vs-interleaved-batch test. Criterion bench
under crates/wickra/benches/indicators.rs.
README family table and indicator counter updated (75 -> 76).
* fix(family-06): re-add KST::classic() factory + drop dup fuzz block
Family-06 PR's tests call ta.KST.classic() / wickra.KST.classic() — main's
KST binding shipped without the static factory. Add classic() in Python
(staticmethod) and Node (napi factory); WASM already had it. Also drop the
duplicate Kst::classic().unwrap() block in fuzz/indicator_update.rs that
the merge left behind (main's API no longer returns Result).
* test(rwi): drop dead count==0 guard
The loop `for i in 2..=period` makes `count = tr_end - tr_start = i - 1`
which is always >= 1, so the `if count == 0 { continue; }` branch was
unreachable defensive code that codecov flagged on the family-06 PR.
* feat(bands-channels): add Family 05 with 11 indicators
Eleven price-envelope overlays organised into a new "Bands & Channels"
family, exposed across all four bindings (Rust core, Python, Node, WASM)
plus fuzz/test/bench/docs coverage:
- MaEnvelope - SMA centerline with fixed-percent envelope (the oldest
band overlay still in regular use).
- AccelerationBands (Price Headley) - momentum-biased bands that widen
with the bar's relative range (H - L) / (H + L).
- StarcBands (Stoller Average Range Channel) - SMA(close) +/- k*ATR;
Keltner's SMA-centerline sibling.
- AtrBands - close-anchored envelope of width k*ATR; the standard
volatility-targeting stop/target band.
- HurstChannel - SMA centerline wrapped by the rolling high-low range
(Brian Millard / Hurst-cycle channel).
- LinRegChannel - rolling OLS endpoint +/- k * population stddev of the
residuals; dispersion about the trend rather than the mean.
- StandardErrorBands - regression line +/- k * OLS standard error
(denominator n - 2) for prediction-interval bands.
- DoubleBollinger (Kathy Lien) - two concentric BB envelopes
(typically +/- 1 sigma and +/- 2 sigma) for the zone-partition setup.
- TtmSqueeze (John Carter) - BB-inside-KC squeeze flag paired with a
detrended-close linear-regression momentum reading.
- FractalChaosBands - Bill Williams 5-bar fractal high/low envelope.
- VwapStdDevBands - cumulative VWAP with volume-weighted population
standard deviation bands.
Each indicator ships:
- Core impl with the full Indicator trait, classic() where applicable,
and unit tests (rejects_zero_period / multiplier, accessors, flat
market, monotonic ordering, batch == streaming, reset, plus
algebraically verifiable reference values).
- Python PyO3 binding with multi-column NumPy batch (PyArray2).
- Node napi binding with #[napi(object)] struct + interleaved flat
batch.
- WASM wasm-bindgen binding via Object/Reflect for update +
Float64Array for batch.
- Fuzz coverage in fuzz_targets/indicator_update{,_candle}.rs.
- Python streaming-vs-batch parametric test + reference test.
- Node streaming-vs-interleaved-batch test + reference test.
- Criterion microbench under crates/wickra/benches/indicators.rs.
README family table, README indicator-count line, and CHANGELOG
Unreleased entry updated: indicator total rises from 71 to 82 across
nine families. Wiki pages are updated in a separate commit in the
wickra.wiki repo.
* test(acceleration-bands): cover sum_hl==0 zero-price guard
Exercises line 104 (`0.0` branch of the `sum_hl == 0.0` guard) which
was the last patch-coverage miss on the family-05 PR. `Candle::new`
accepts a fully-zero bar so the branch is reachable in principle —
add a degenerate-candle unit test to hit it.
* feat(rvi): add Relative Volatility Index
Donald Dorsey's RSI-shaped volatility gauge. Partitions the rolling
population standard deviation of close into "up" samples (close rose
since the previous bar) and "down" samples (close fell), Wilder-smooths
each side, and reports 100 * AvgUp / (AvgUp + AvgDown). Output bounded
on [0, 100]; saturates at 100 in pure uptrends, 0 in pure downtrends,
and falls back to 50 on a completely flat series (same undefined-RS
convention as RSI).
Single period parameter (default 10) drives both the stddev window and
the Wilder smoothing constant. First emit lands at index 2*period - 2
(2*period - 1 bars are needed: period to fill the stddev window plus
period - 1 to seed the Wilder averages, overlapping by one bar).
Touchpoints: rvi.rs + mod.rs + lib.rs re-export, PyRvi + __init__.py +
test_new_indicators SCALAR + test_known_values uptrend reference,
RviNode + index.d.ts/index.js + indicators.test.js factory +
reference, WasmRvi via scalar macro, scalar-fuzz target, bench_scalar
entry, README + CHANGELOG.
* feat(parkinson): add Parkinson Volatility
Michael Parkinson's (1980) high-low realised volatility estimator.
Under a driftless Geometric-Brownian-Motion assumption, the extreme
range of a bar carries roughly 5x the variance information of the
close-to-close estimator, so for a given statistical efficiency
Parkinson needs five times fewer samples.
Formula:
sigma^2 = (1 / (4n * ln 2)) * Sum_{i=1..n} (ln(H_i / L_i))^2
out = sqrt(sigma^2) * sqrt(trading_periods) * 100
The output is annualised to a percent in the same style as
HistoricalVolatility (pass `trading_periods = 1` for the raw per-bar
sigma * 100 figure). Two parameters: `period` (default 20) for the
rolling window, `trading_periods` (default 252) for the annualisation
factor. First emit at index `period - 1`.
Touchpoints: parkinson.rs + mod.rs + lib.rs re-export,
PyParkinsonVolatility + __init__.py + test_new_indicators CANDLE_SCALAR
+ test_known_values zero-range reference, ParkinsonVolatilityNode +
index.d.ts/index.js + indicators.test.js factory + reference,
WasmParkinsonVolatility hand-rolled, candle-fuzz target,
bench_candle_input entry, README + CHANGELOG.
* feat(garman-klass): add Garman-Klass Volatility
Garman & Klass (1980) OHLC realised-volatility estimator. Extends
Parkinson's high-low estimator with an open-to-close term, lifting
statistical efficiency from ~5x to ~7.4x relative to close-to-close
stddev under driftless Geometric Brownian Motion.
Formula (per bar):
s_t = 0.5 * (ln(H_t / L_t))^2 - (2*ln(2) - 1) * (ln(C_t / O_t))^2
out = sqrt(max(mean(s_t over `period`), 0)) * sqrt(trading_periods) * 100
The per-bar sample can be marginally negative when the bar has a small
range relative to its open-to-close move; a max(., 0) clamp on the
rolling mean absorbs that and the FP cancellation noise before the
square root.
Still biased on data with meaningful overnight drift -- use Yang-Zhang
when gaps matter. Defaults: `period = 20`, `trading_periods = 252`
(annualised percent, same convention as HistoricalVolatility).
Touchpoints: garman_klass.rs + mod.rs + lib.rs re-export,
PyGarmanKlassVolatility + __init__.py + test_new_indicators
CANDLE_SCALAR + test_known_values zero-movement reference,
GarmanKlassVolatilityNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmGarmanKlassVolatility hand-rolled,
candle-fuzz target, bench_candle_input entry, README + CHANGELOG.
* feat(rogers-satchell): add Rogers-Satchell Volatility
Rogers, Satchell & Yoon (1994) OHLC realised-volatility estimator.
Unlike Garman-Klass, the per-bar sample is exact under arbitrary
Brownian drift -- the drift component cancels algebraically.
Formula (per bar):
s_t = ln(H_t / C_t) * ln(H_t / O_t) + ln(L_t / C_t) * ln(L_t / O_t)
out = sqrt(max(mean(s_t over `period`), 0)) * sqrt(trading_periods) * 100
Each per-bar sample is also non-negative by construction: with
`Candle::new` guaranteeing H >= max(O, L, C) and L <= min(O, H, C), the
four log factors have predictable signs (ln(H/.) >= 0, ln(L/.) <= 0),
so both products contribute >= 0. The max(., 0) clamp on the rolling
mean is only there to absorb FP cancellation.
Defaults: `period = 20`, `trading_periods = 252` (annualised percent,
same convention as HistoricalVolatility / Parkinson / Garman-Klass).
Touchpoints: rogers_satchell.rs + mod.rs + lib.rs re-export,
PyRogersSatchellVolatility + __init__.py + test_new_indicators
CANDLE_SCALAR + test_known_values zero-movement reference,
RogersSatchellVolatilityNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmRogersSatchellVolatility hand-rolled,
candle-fuzz target, bench_candle_input entry, README + CHANGELOG.
* feat(yang-zhang): add Yang-Zhang Volatility
Yang & Zhang (2000) drift- and gap-robust OHLC realised-volatility
estimator. Combines three independent components into a single estimate
with minimum variance:
overnight = sample_var(ln(O_t / C_{t-1})) over n bars (close-to-open)
open_close = sample_var(ln(C_t / O_t)) over n bars
rs = mean(ln(H/C)*ln(H/O) + ln(L/C)*ln(L/O)) over n bars
sigma^2_YZ = overnight + k*open_close + (1-k)*rs
k = 0.34 / (1.34 + (n+1)/(n-1))
out = sqrt(max(sigma^2_YZ, 0)) * sqrt(trading_periods) * 100
The overnight and open-to-close variances use Bessel's correction (the
sample estimator, divisor n-1), same convention as
HistoricalVolatility. The blending factor `k` is the one that
minimises estimator variance under driftless Geometric Brownian Motion
with overnight gaps.
This is the gold-standard OHLC estimator for assets with both
close-to-open gaps and intraday drift: equities, futures, and any
market that does not trade continuously. For pure intraday data (where
O_t == C_{t-1} and the open-to-close return is constant), the
overnight and open-close terms vanish and the estimator collapses to
(1-k) * Rogers-Satchell -- this is the indicator's
intraday_data_collapses_to_rs_only unit test.
Period >= 2 (Bessel correction needs >= 2 samples). First emit at
index `period` (the (period+1)-th bar): one bar seeds prev_close, the
next `period` fill the rolling windows. Defaults: `period = 20`,
`trading_periods = 252`.
Touchpoints: yang_zhang.rs + mod.rs + lib.rs re-export,
PyYangZhangVolatility + __init__.py + test_new_indicators
CANDLE_SCALAR + test_known_values zero-movement reference,
YangZhangVolatilityNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmYangZhangVolatility hand-rolled, candle-fuzz
target, bench_candle_input entry, README + CHANGELOG.
* fix(rvi): rename to RviVolatility to avoid clash with family-02 RVI
Family 02 (PR #40) ships a separate `Rvi` struct for Relative Vigor
Index. The two indicators have nothing to do with each other beyond
sharing the acronym, so disambiguate by giving the volatility one a
longer name everywhere:
- Rust crate: `Rvi` -> `RviVolatility`
- Rust file: `rvi.rs` -> `rvi_volatility.rs`
- Python: `RVI` -> `RVIVolatility`
- Node: `RVI` -> `RVIVolatility`
- WASM: `RVI` -> `RVIVolatility`
Once the two PRs are both merged, callers get `wickra::Rvi` for Vigor
and `wickra::RviVolatility` for Volatility. The shorter `RVI` acronym
stays with the Momentum family per the existing wiki pages and the
implementation that shipped first.
Updates: rvi_volatility.rs (renamed), mod.rs, lib.rs re-export,
bindings/python/src/lib.rs + __init__.py + tests, bindings/node/src/lib.rs
+ index.d.ts + index.js + __tests__, bindings/wasm/src/lib.rs,
fuzz/fuzz_targets/indicator_update.rs, crates/wickra/benches/indicators.rs,
README family-table label, CHANGELOG entry.
* test(volatility): Rename test_rvi -> test_rvi_volatility + drop dead match arms
The Python test test_rvi_pure_uptrend_saturates_at_one_hundred was
calling ta.RVI() expecting the volatility version, but ta.RVI now
means Family 02's Relative Vigor Index (candle input). Renamed to
ta.RVIVolatility to match the binding rename done at merge time.
In all four OHLC volatility tests, the existing `match (r, a) { ...,
_ => panic!() }` arm is dead in passing runs (every aligned pair is
either (None, None) or (Some, Some)). Codecov flagged it as a patch
miss on each of parkinson / garman_klass / rogers_satchell /
yang_zhang. Refactored per CLAUDE.md cold-path guidance to
`assert_eq!(r.is_some(), a.is_some()); if let (Some, Some) ...`.
* feat(apo): add Absolute Price Oscillator
EMA(close, fast) - EMA(close, slow). Like MACD without the signal EMA.
Defaults to (fast = 12, slow = 26); fast must be strictly less than
slow.
Touchpoints: apo.rs + mod.rs + lib.rs re-export, PyApo + __init__.py
+ test_new_indicators SCALAR + test_known_values flat reference,
ApoNode + index.d.ts/index.js + indicators.test.js factory + reference,
WasmApo via scalar macro, scalar-fuzz target, README + CHANGELOG.
* fix(apo): add PyApo + ApoNode + WasmApo bindings missed from ec269d8
The previous APO commit (ec269d8) only registered APO in the Python
__init__.py / Node index.js / Node index.d.ts / fuzz / tests / docs.
The actual PyApo pyclass, ApoNode napi class, and WasmApo wasm class
edits silently no-op'd because the underlying lib.rs files had been
touched by a branch switch between Read and Edit. The bindings were
therefore advertising APO from the Python module / Node package /
WASM module but not actually exposing it.
Fix: insert PyApo block + add_class call in bindings/python/src/lib.rs,
ApoNode block in bindings/node/src/lib.rs, WasmApo macro line in
bindings/wasm/src/lib.rs. cargo test workspace stays at 615 (no new
tests added; the existing test_known_values + indicators.test.js
references would have failed at import once the bindings rebuilt
without these classes).
* feat(ao-histogram): add Awesome Oscillator Histogram
AO - SMA(AO, sma_period). A configurable variant of the existing
AcceleratorOscillator (which fixes fast=5, slow=34, sma=5).
Three parameters; defaults match Bill Williams' Accelerator.
Touchpoints: awesome_oscillator_histogram.rs + mod.rs + lib.rs
re-export, PyAoHist + __init__.py + test_new_indicators CANDLE_SCALAR
+ test_known_values flat reference, AwesomeOscillatorHistogramNode +
index.d.ts/index.js + indicators.test.js factory + reference,
WasmAoHist, candle-fuzz target, README + CHANGELOG.
* feat(cfo): add Chande Forecast Oscillator
100 * (close - LinReg(close, period)) / close. Positive when close
overshoots the linear forecast, negative when it undershoots. Holds
the previous value if the close is zero (percentage form undefined).
Single param period (default 14).
Touchpoints: cfo.rs + mod.rs + lib.rs re-export, PyCfo + __init__.py
+ test_new_indicators SCALAR + test_known_values linear reference,
CfoNode + index.d.ts/index.js + indicators.test.js factory + reference,
WasmCfo via scalar macro, scalar-fuzz target, README + CHANGELOG.
* fix(cfo): add WasmCfo binding missed from 733afd9
* feat(zero-lag-macd): add Zero-Lag MACD
Classic MACD topology with ZLEMA substituted for EMA everywhere:
faster reaction to trend changes at the cost of slightly noisier
readings. Multi-output ZeroLagMacdOutput { macd, signal, histogram }.
Three parameters (fast = 12, slow = 26, signal = 9); fast must be
strictly less than slow.
Touchpoints: zero_lag_macd.rs + mod.rs + lib.rs re-export, PyZeroLagMacd
+ __init__.py + test_new_indicators MULTI + test_known_values flat
reference, ZeroLagMacdNode + ZeroLagMacdValue + index.d.ts/index.js +
indicators.test.js multi factory + reference, WasmZeroLagMacd, scalar
fuzz with hand-rolled drive (multi-output bypasses the f64-only
helper), README + CHANGELOG.
* feat(elder-impulse): add Alexander Elder Impulse System
Tri-state momentum gauge: +1 (green/buy) when EMA trend and MACD
histogram both rise, -1 (red/sell) when both fall, 0 (blue/neutral)
on disagreement. Four parameters (ema_period, macd_fast, macd_slow,
macd_signal); defaults (13, 12, 26, 9) match Elder.
Internally feeds both branches on every input so they warm in parallel;
needs one bar past the slowest branch to seed direction state.
Touchpoints: elder_impulse.rs + mod.rs + lib.rs re-export, PyElderImpulse
+ __init__.py + test_new_indicators SCALAR + test_known_values neutral
reference, ElderImpulseNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmElderImpulse via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(stc): add Schaff Trend Cycle
Doug Schaff's doubly-Stochastic-smoothed MACD. Bounded [0, 100]
reading that reacts faster than MACD by extracting the percentile of
MACD within a recent window, half-EMA-smoothing it, and re-stochasing
the smoothed series. Four parameters (fast = 23, slow = 50,
schaff_period = 10, factor = 0.5); fast must be strictly less than
slow and factor must lie in (0, 1].
Output clamped to [0, 100] to absorb floating-point rounding. The
stochastic stages clamp to 0 when their rolling range collapses (flat
input or perfectly monotone trend), so a flat series settles
deterministically at 0 after warmup.
Touchpoints: stc.rs + mod.rs + lib.rs re-export, PyStc + __init__.py
+ test_new_indicators SCALAR + test_known_values flat reference,
StcNode + index.d.ts/index.js + indicators.test.js factory + reference,
WasmStc via scalar macro, scalar-fuzz target, README + CHANGELOG.
* fix(stc): rename last_stc -> last_value to satisfy clippy
* ci: Retry setup-node and setup-python on CDN flakes
Setup-node on Windows runners and setup-python across all OSes
occasionally fail with a silent hang or 5xx mid-download ("Attempting
to download 18..." → fail in <1s) — pure upstream CDN flake. The fix
ran on this branch's previous merge commit (24e723f) had to be
re-triggered manually via `gh run rerun --failed`.
Wrap both setup actions with continue-on-error and a follow-up retry
step that waits 30s and re-runs the same setup. The retry only fires
when the first attempt failed (steps.<id>.outcome == 'failure'), so a
green setup costs nothing extra. The retry uses the identical pinned
SHA so we still get supply-chain verification on both attempts.
Applied to ci.yml (Python matrix and Node matrix). release.yml has
the same setup-node / setup-python steps but is rarely re-run, so
the existing manual rerun pattern stays sufficient for now.
* test(zero-lag-macd): Fix MULTI dict shape mismatch + cover warmup_period
ZeroLagMACD was registered in the Python MULTI dict (which asserts a
(n, 2) batch shape) but actually emits (n, 3) — macd, signal,
histogram — like MACD. Moved out into its own standalone test
test_zero_lag_macd_streaming_matches_batch (3-tuple shape), and
included in the lifecycle sweep. Mirrors the existing Alligator
pattern for 3-output candle indicators.
Also adds a unit test for ZeroLagMacd::warmup_period that pins both
the (12, 26, 9) classic case and a small-period config — these four
lines were the codecov/patch miss on PR 41.
Previously the workflow patched README.md on main after every push,
producing an unsigned 'chore: sync indicator count' commit per merge.
Now the README counter is kept in sync on the PR side instead: on
every pull_request event, the workflow checks out the PR's head ref,
compares grep -c '^mod ' to the README counter, and if they differ,
pushes a fix-up commit back onto the PR branch using the default
GITHUB_TOKEN.
When the PR is squash-merged, that fix-up commit is folded into the
single web-flow-signed merge commit on main — so main's history never
shows a separate bot commit. About description and Wiki sync still
run on push to main / v* tags via the existing PAT, since both reach
outside the main repo (Administration:write and the .wiki repo).
For PRs from forks the workflow cannot push back; it emits a hard
::error:: pointing at README.md so the contributor can fix the
counter manually.
Pushes via GITHUB_TOKEN do not re-trigger downstream workflows
(GitHub's anti-recursion policy), so the fix-up commit costs zero
extra CI minutes — only sync-about itself re-runs on the next
synchronize event and no-ops once the counter matches.
* feat(rvi): add Relative Vigor Index
Dorsey's RVI = SMA(close - open, period) / SMA(high - low, period) over
a rolling window of period candles. Candle input, single parameter
period (default 10). Positive on average-bullish windows, negative on
average-bearish. Holds the previous value if the entire window has
zero range (denominator undefined).
Reference: Donald Dorsey, also pandas-ta rvi.
Touchpoints: rvi.rs + mod.rs + lib.rs re-export, PyRvi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values reference,
RviNode (4-column OHLC batch) + index.d.ts/index.js + indicators.test
.js factory + reference, WasmRvi + make_candle_ohlc helper, candle-fuzz
target + criterion bench, README + CHANGELOG.
* feat(pgo): add Pretty Good Oscillator
Mark Johnson's PGO = (close - SMA(close, period)) / EMA(TR, period).
Counts roughly how many ATR-equivalents the close sits from its
period-bar mean. Candle input, single parameter period (default 14).
Johnson's heuristic uses +3/-3 crossings as entry signals.
Touchpoints: pgo.rs + mod.rs + lib.rs re-export, PyPgo + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-close
reference, PgoNode (h/l/c) + index.d.ts/index.js + indicators.test.js
factory + reference, WasmPgo, candle-fuzz target + bench, README +
CHANGELOG.
* feat(kst): add Know Sure Thing (Pring)
Pring's long-horizon momentum oscillator: weighted sum of four
SMA-smoothed ROC series with fixed weights 1, 2, 3, 4, plus an SMA
signal line. Nine parameters (four ROC periods, four SMA periods, one
signal period); classic() applies Pring's recommended defaults.
Multi-output indicator emitting KstOutput { kst, signal }.
Touchpoints: kst.rs + mod.rs + lib.rs re-export, PyKst + __init__.py
+ test_new_indicators MULTI + test_known_values flat-input reference,
KstNode + KstValue + index.d.ts/index.js + indicators.test.js multi
factory + reference, WasmKst (manual JsValue object), scalar-fuzz
target (handled outside the f64-output drive helper), README +
CHANGELOG.
* feat(smi): add Stochastic Momentum Index (Blau)
Blau's doubly-EMA-smoothed bounded oscillator: measures the close's
displacement from the centre of the recent high-low range, scaled by
the smoothed range. Candle input, three parameters (period, d_period,
d2_period) with defaults 5 / 3 / 3.
Internally feeds both the displacement-EMA stack and the range-EMA
stack on every candle so they warm up in parallel (gating either
behind the other starves the second by one input).
Touchpoints: smi.rs + mod.rs + lib.rs re-export, PySmi + __init__.py
+ test_new_indicators CANDLE_SCALAR + test_known_values flat-input
reference, SmiNode + index.d.ts/index.js + indicators.test.js factory
+ reference, WasmSmi, candle-fuzz target, README + CHANGELOG.
* feat(laguerre-rsi): add Ehlers Laguerre RSI
Four-stage Laguerre polynomial filter wrapped in an RSI-style up/down
accumulator. Single gamma in [0, 1] (default 0.5) trades lag for
smoothness. State is seeded by setting all four L_i to the first input
so a constant series stays at the neutral 50. Output clamped to
[0, 100] to absorb floating-point rounding.
Reference: Ehlers, Time Warp - Without Space Travel, 2002.
Touchpoints: laguerre_rsi.rs + mod.rs + lib.rs re-export, PyLaguerreRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values neutral
reference, LaguerreRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmLaguerreRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(connors-rsi): add Connors RSI (CRSI)
Larry Connors' 3-component aggregate: RSI(close), RSI(streak), and
PercentRank of the 1-period return over the last period_rank returns.
Each component is bounded in [0, 100] so the aggregate is too.
Three parameters (period_rsi, period_streak, period_rank) with
defaults 3 / 2 / 100. Streak tracks consecutive up/down runs (resets
to 0 on unchanged close).
Touchpoints: connors_rsi.rs + mod.rs + lib.rs re-export, PyConnorsRsi
+ __init__.py + test_new_indicators SCALAR + test_known_values bounded
reference, ConnorsRsiNode + index.d.ts/index.js + indicators.test.js
factory + reference, WasmConnorsRsi via scalar macro, scalar-fuzz
target, README + CHANGELOG.
* feat(inertia): add Dorsey Inertia (RVI + LinReg)
Donald Dorsey's Inertia — a LinearRegression smoothing of the RVI
series. Endpoint of an n-bar least-squares fit of RVI is the indicator
reading. Preserves trend direction while damping the ratio. Candle
input, two parameters (rvi_period, linreg_period) with defaults 14 / 20.
Touchpoints: inertia.rs + mod.rs + lib.rs re-export, PyInertia +
__init__.py + test_new_indicators CANDLE_SCALAR + test_known_values
constant reference, InertiaNode (4-column OHLC batch) + index.d.ts /
index.js + indicators.test.js factory + reference, WasmInertia,
candle-fuzz target, README + CHANGELOG.
* test(kst): Move KST out of MULTI dict (it is scalar-input)
KST sits in the MULTI dict (candle-input, multi-output) but its
update() takes a single f64, not a candle tuple. The shared streaming
loop in test_multi_streaming_matches_batch fed the OHLCV tuple in,
which crashed with `TypeError: argument 'value': must be real number,
not tuple` on every Python matrix entry.
Split into a new MULTI_SCALAR_INPUT dict with its own test function
that feeds the close-price stream as floats. KST is currently the
only such indicator; structure is ready for future scalar-input
multi-output additions (e.g. some MACD-shaped indicators).
* test(coverage): Cover SMI zero-range and ConnorsRsi zero-prev cold paths
codecov/patch on PR 40 flagged two uncovered defensive branches:
- SMI returns self.current early when the smoothed range collapses to
zero (`r2 <= 0.0`) so the formula stays defined. Exercised by feeding
bars where high == low.
- ConnorsRsi skips the ROC ring-buffer update when the previous price
is exactly zero so the divide-by-zero in `(input - prev) / prev` is
impossible. Exercised by seeding the first bar at 0.0.
site/ is local-only (listed in .git/info/exclude), so the sed call in
the Patch step aborted the workflow on every push to main with
"sed: can't read site/index.md: No such file or directory". That kept
the README counter from being committed and skipped the wiki sync.
Patches README only now. site/ stays out of CI until the marketing
site is promoted.
* feat(alma): add Arnaud Legoux Moving Average
Gaussian-weighted moving average with configurable centre (offset in
[0, 1]) and kernel width (sigma > 0). Pre-computes normalised weights
at construction so each update is a single rolling window dot product.
Reference: Arnaud Legoux and Dimitrios Kouzis-Loukas, 2009.
Touchpoints:
- crates/wickra-core: alma.rs + mod.rs + lib.rs re-export
- bindings/python: PyAlma + __init__.py + test_new_indicators +
test_known_values reference
- bindings/node: AlmaNode + index.d.ts/index.js + indicators.test.js
factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers ALMA(9, 0.85, 6.0)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(mcginley): add McGinley Dynamic moving average
John McGinley's self-adjusting moving average with the recurrence
MD + (price - MD) / (0.6 * period * (price / MD)^4). Speeds up when
price falls below the indicator and damps when price runs above the
indicator. Seeded with the simple average of the first period inputs.
Reference: McGinley, Technical Analysis of Stocks & Commodities, 1990.
Touchpoints:
- crates/wickra-core: mcginley_dynamic.rs + mod.rs + lib.rs re-export
- bindings/python: PyMcGinleyDynamic + __init__.py + test_new_indicators
+ test_known_values reference
- bindings/node: McGinleyDynamicNode (scalar macro) + index.d.ts/index.js
+ indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers McGinleyDynamic(10)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(frama): add Fractal Adaptive Moving Average
Ehlers' FRAMA adapts its smoothing constant to the fractal dimension of
the recent window: tight tracking in trends, heavy smoothing in chop.
Uses the close-only variant where max/min over each window half drive
the dimension estimate. Period must be even (default 16).
Reference: Ehlers, Fractal Adaptive Moving Average, 2005.
Touchpoints:
- crates/wickra-core: frama.rs + mod.rs + lib.rs re-export
- bindings/python: PyFrama + __init__.py + test_new_indicators +
test_known_values reference (constant series + uptrend tracking)
- bindings/node: FramaNode (scalar macro) + index.d.ts/index.js +
indicators.test.js factory + reference value
- bindings/wasm: wasm_scalar_indicator! macro
- fuzz: indicator_update target covers Frama(16)
- crates/wickra/benches: bench_scalar entry
- README + CHANGELOG: Moving Averages row + Unreleased entry
* feat(vidya): add Variable Index Dynamic Average
Chande's VIDYA — an EMA whose alpha scales with |CMO(cmo_period)| / 100.
Strong directional momentum lifts the smoothing constant toward the
EMA-of-period rate; flat or choppy windows shrink it toward zero so
VIDYA coasts on its previous value. Two parameters: period (14) and
cmo_period (9). Reuses the existing wickra-core Cmo internally.
Reference: Chande, Stocks & Commodities, 1992.
Also fixes a silent gap from d37fbd1 (feat(frama)): the PyFrama Python
class wrapper and its add_class registration were dropped because the
two edits hit "File has not been read yet" errors that scrolled past
in a batch. Adds them here alongside VIDYA's bindings.
Touchpoints (VIDYA): vidya.rs + mod.rs + lib.rs re-export, PyVidya +
__init__.py + test_new_indicators + test_known_values reference,
VidyaNode (manual two-param binding) + index.d.ts/index.js +
indicators.test.js factory + reference, wasm_scalar_indicator! macro,
fuzz target, bench, README + CHANGELOG.
* feat(jma): add Jurik Moving Average
Three-stage filter reconstruction of Mark Jurik's adaptive MA (the
algorithm is proprietary; this is the form used by most open-source
ports since the 1999 TASC article). Parameters: period (14), phase in
[-100, 100] (0), power in 1..=4 (2). State is seeded by setting
e0 = JMA = first input so a constant input stream is reproduced exactly.
Touchpoints: jma.rs + mod.rs + lib.rs re-export, PyJma + __init__.py +
test_new_indicators + test_known_values reference, JmaNode (manual
three-param binding) + index.d.ts/index.js + indicators.test.js factory
+ reference, wasm_scalar_indicator! macro, fuzz target, bench, README +
CHANGELOG.
* feat(alligator): add Bill Williams Alligator
Three SMMA lines (Jaw / Teeth / Lips) over the median price
(high + low) / 2 with default periods 13 / 8 / 5. Multi-output
indicator returning AlligatorOutput { jaw, teeth, lips }. The
original chart variant shifts each line forward for display; we
publish the unshifted SMMA values and leave the visual shift to
the consumer.
Reference: Bill Williams, Trading Chaos, 1995.
Touchpoints: alligator.rs + mod.rs + lib.rs re-export, PyAlligator
(Candle input, returns 3-tuple, ndarray (n, 3) batch) + __init__.py
+ test_new_indicators + test_known_values reference, AlligatorNode +
AlligatorValue + index.d.ts/index.js + indicators.test.js multi
factory + reference, WasmAlligator (manual JsValue object) +
candle-fuzz target + README + CHANGELOG.
* feat(evwma): add Elastic Volume-Weighted Moving Average
Christian P. Fries' elastic recurrence where the smoothing weight is the
bar's volume relative to the running window total:
V_sum_t = sum of volumes over the last period candles
EVWMA_t = ((V_sum_t - v_t) * EVWMA_{t-1} + v_t * close_t) / V_sum_t
A bar whose volume is small barely moves the average; a bar that
dominates the window pulls it strongly toward that bar's close. Seeded
with the close of the first full window; holds its previous value if
the entire window has zero volume.
Reference: Fries, Wilmott Magazine, 2001.
Touchpoints: evwma.rs + mod.rs + lib.rs re-export, PyEvwma (close +
volume batch) + __init__.py + test_new_indicators CANDLE_SCALAR +
test_known_values reference, EvwmaNode + index.d.ts/index.js +
indicators.test.js candleScalar factory + reference, WasmEvwma,
candle-fuzz target + README + CHANGELOG.
* ci: Force local wheel install in Python jobs
Use --no-index --no-deps so the Python matrix installs the freshly
built wheel from dist/ and never falls back to PyPI. Previously pip
sometimes picked the released 0.2.x wheel on macOS / Windows when its
platform tag was a wider match than the local build, which made the
job test the released package and miss any new symbols added in the
PR (e.g. AttributeError: module 'wickra' has no attribute 'ALMA').
numpy is already installed by the preceding pip step, so --no-deps
is safe.
Counts `mod xxx;` declarations in crates/wickra-core/src/indicators/mod.rs
on every push to main, every PR, and every v* tag push. On non-PR runs
it syncs the count into:
- GitHub repo About description (via `gh repo edit`)
- README.md + site/index.md (commit with [skip ci] back to main)
- Wiki: Home.md, FAQ.md, Streaming-vs-Batch.md
Requires a `ABOUT_SYNC_TOKEN` secret (classic PAT with `repo` scope, or
fine-grained PAT with Administration+Contents write on the wickra repo).
PR runs are read-only: count is logged but nothing is mutated, so forks
cannot trigger writes.
* chore(docs): rename benchmark CPU from 7950X3D to 9950X
The "Reproduced on" line in the umbrella + binding READMEs and the
benchmark page on the site listed the wrong AMD CPU. The benchmarks
were actually produced on a Ryzen 9 9950X, not a 7950X3D. Same
column for absolute µs values applies — the speedup ratios in the
tables are unchanged either way because they're relative across
libraries on the same machine.
The performance-regression issue template's CPU example also
updated for consistency (it was a generic placeholder, but matching
the canonical machine makes the example concrete).
* chore(npm): restore Windows ARM64 sub-package + napi matrix entry
npm Support unblocked the `wickra-win32-arm64-msvc` package name and
transferred write access to @kingchenc (placeholder 0.0.1-security
was published from their side; we ship our first real version on
top of that). This re-enables every change 8aa74cb temporarily
backed out for 0.2.1:
- bindings/node/package.json: re-add `aarch64-pc-windows-msvc` to
napi.triples.additional and `wickra-win32-arm64-msvc` to
optionalDependencies.
- bindings/node/npm/win32-arm64-msvc/package.json: restored — name,
cpu = arm64, os = win32, version pinned to the workspace.
- .github/workflows/release.yml: re-enable the
`windows-11-arm / aarch64-pc-windows-msvc` row in the node-build
matrix and drop the "temporarily skipped" comment block.
After the next tag-push this binding will be published alongside
the other five platforms and `npm install wickra` on Windows ARM64
will resolve to a native build instead of failing the loader's
optional-dep lookup.
* release: bump workspace + bindings to 0.2.7
Workspace, every binding (Python, Node, six platform stubs incl. the
restored win32-arm64-msvc), and the CHANGELOG all move together to
0.2.7. wickra-win32-arm64-msvc is now part of the standard publish
matrix and will land on npm alongside the other five binaries.
The 0.2.7 CHANGELOG entry consolidates the two changes this cycle:
- Windows ARM64 binding restored (npm Support unblocked the name).
- Benchmark CPU label corrected (Ryzen 9 9950X, not 7950X3D).
* fix(docs-rs): rename `doc_auto_cfg` to `doc_cfg` after Rust 1.92 merge
`doc_auto_cfg` was removed in Rust 1.92.0 and folded back into
`doc_cfg` (rust-lang/rust#138907). docs.rs builds with the latest
nightly and sets `--cfg docsrs`, so the previous
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
aborts compilation with E0557 on every published 0.2.x. GitHub CI
never tripped this — stable rustc ignores the line because nothing
sets the `docsrs` cfg there.
Switch all three published library crates (`wickra`, `wickra-core`,
`wickra-data`) to the merged-into `doc_cfg` gate. Same intent, same
on-docs.rs output, builds again on nightly.
* docs(readme): float Wickra to the top of the comparison tables
Reorders the "Why Wickra exists" library-comparison table and the two
benchmark headers so Wickra is the first row (with a ★ marker) instead
of the last. The previous order placed Wickra at the bottom, which
buries the only row a reader landing on the README is here to compare
against. Same column data, same ★/winner annotations, just the row
order flipped and a ★ prefix on the Wickra label.
Mirrored across the umbrella README and every binding README so the
crates.io / PyPI / npm landing pages stay in sync.
* release: bump workspace + bindings to 0.2.6
Workspace, every binding (Python, Node, Node platform stubs), the
release.yml comment and the CHANGELOG all move together to 0.2.6 so
the next tagged release lines every artefact up.
0.2.6 carries two changes from the [0.2.6] CHANGELOG entry:
- fix(docs-rs): swap the now-removed `doc_auto_cfg` feature gate for
the merged-into `doc_cfg` so docs.rs nightly builds resume.
- docs(readme): float ★ Wickra to the top of every comparison table
across the umbrella + binding READMEs.
wickra-win32-arm64-msvc stays excluded for this release with the same
npm spam-filter rationale that held for 0.2.5.
Adds five new issue templates (bug_report_detailed, feature_request_detailed,
performance_regression, documentation, question) alongside the existing
short forms, plus an optional detailed PR template under
.github/PULL_REQUEST_TEMPLATE/ that contributors can opt into via the
?template=detailed.md URL. Existing templates keep their behavior; only
title prefixes and prose-paren wording were capitalized for consistency.
Workspace, every binding (Python, Node, Node platform stubs), and the
release.yml comment are all updated together so the next tagged release
on `v0.2.5` lines every artefact up.
Also adds a short README "Disclaimer" section pointing out that Wickra
is an indicator toolkit, not a trading system, and that production
use is at the caller's own risk. The legal terms in LICENSE (PolyForm
Noncommercial 1.0.0, "No Liability") already cover the warranty / as-is
language — the README section just makes the trading-specific framing
visible without burying it in a click-through.
CHANGELOG carries the new 0.2.5 entry with the API addition
(`BinanceConfig` + `connect_with_config`) and the best-effort Pong
write change in `BinanceKlineStream::next_event`. wickra-win32-arm64-msvc
stays excluded for this release with the same npm-spam-filter rationale
that held for 0.2.1.
* refactor(binance): introduce BinanceConfig for endpoint + timing knobs
Replaces the file-private READ_TIMEOUT / MAX_RECONNECT_ATTEMPTS / size
limit constants with a Default-equipped BinanceConfig the caller can hand
to a new connect_with_config(). connect() forwards to it with the
defaults, so the public API stays backwards-compatible.
Behaviour-preserving: every default matches the value of the constant it
replaces, and the WebSocketConfig is built the same way. The change
unlocks two real use-cases — pointing at Binance Testnet
(wss://testnet.binance.vision) and pointing at a local mock server with
millisecond-scale reconnect timing in tests.
* test(binance): cover the Interval table and the empty-symbol guard
Three quick wins that don't need a live or mock socket:
- interval_as_str_covers_every_variant pins every wire-format mapping in
one table so a typo on any of the 14 variants is caught.
- binance_config_default_matches_production_endpoint guards the default
base URL and timing knobs against an accidental drift.
- connect_rejects_an_empty_symbol_list exercises the guard before the
WebSocket handshake — the one async path we can hit without a server.
* test(binance): cover the async / reconnect / control-frame paths
Adds a small mock-WebSocket scaffold built on a `127.0.0.1:0` listener
and tokio-tungstenite's `accept_async`, plus nine integration tests that
drive `BinanceKlineStream::next_event` through every branch:
- text + binary kline frames decode to a KlineEvent
- inbound Ping is answered with a Pong, then the kline arrives
- inbound Pong / Frame variants are silently skipped
- a server-side Close triggers a transparent reconnect that then
serves the kline
- a stalled connection trips read_timeout and reconnects on its own
- close() flips the closed flag and next_event() yields None forever
- when every reconnect attempt is refused, next_event surfaces an Err
- a "kline" envelope whose numbers are unparseable bubbles up as
Error::Malformed rather than being silently skipped
`one_shot_server` drops the listener as soon as the first accept is
done, so a follow-up reconnect lands on a refused port — that is what
lets the exhaustion test hit the final `last_err.expect(...)`.
The whole suite runs in ~4 s with millisecond-scale reconnect timings
supplied via the new test-only [`test_config`].
* test(binance): drop defensive cold-paths in the mock-WS scaffolding
Codecov's patch report on PR #34 flagged seven uncovered lines, all of
them in the test scaffolding rather than in production code:
- the `let Ok((stream, _)) = … else { return }` shortcut and the
`if let Ok(ws) = accept_async(stream).await { … }` branch in the
mock-server helpers — both error arms never fire on a passing test
- the closing braces of the spawned-task bodies in the close-frame and
read-timeout reconnect tests — the spawned async blocks were getting
killed mid-drain when the test asserted and returned
Refactor the helpers to `.unwrap()` every Result (a failure here is a
bug in the scaffold, not in production) and have `multi_shot_server`
accept a fixed `n_accepts`, await every spawned inner task, and hand
the outer JoinHandle back to the caller.
Refactor the two affected tests to capture that JoinHandle, collapse
the per-index `if/else` so both arms reach the same trailing
expression, swap the read-timeout drain for a bounded sleep, and
await `server_done` at the end. Every handler now reaches its closing
brace before the runtime is torn down, so coverage on the patch should
collapse from 97.89 % to 100 %.
* test(binance): cover the non-kline-skip path and simplify the Ping arm
After the scaffolding fix landed three lines on binance.rs were still
uncovered:
- L305 / L313: the Text- and Binary-arm "frame was not a kline, keep
reading" fall-throughs. No existing test drove the loop through a
non-kline frame followed by a kline; the new
`next_event_skips_non_kline_frames_and_returns_the_next_kline` does
exactly that (Text ack, Binary id frame, then a real kline).
- L317: the Ping-Err defensive arm that forced a reconnect when the
Pong reply itself failed to write. A failed Pong reply means the
socket is already dead, so the very next read will surface the error
and reconnect through the existing timeout/err branch — one tokio
scheduling iteration later. Drop the defensive arm and write the
Pong reply best-effort. Same observable behaviour, no test back
door, no dead-line guard.
Repo coverage on `cov/binance-mock-ws` now sits at 100 %.
After PR #27 brought psar.rs to 99.03 %, Codecov still flagged the
'violation found' tuple arms in the trend tests (line 256 in
pure_uptrend_sar_below_lows, line 285 in pure_downtrend_sar_above_highs)
as missed: both tests are designed to NEVER find a violation, so the
filter_map branch that constructs the (index, sar, bound) tuple is dead
by design.
Restructure both tests to use `.all(|(i, sar)| sar.is_none_or(|s|
<bound>))` instead of collecting violations into a Vec. The closure
runs on every emitted Some, asserts the SAR-vs-extreme bound directly,
and the iterator short-circuits on the first false — no cold tuple
construction left to count as uncovered. Semantics are identical (still
asserts every SAR sits on the correct side of every candle's extreme);
the diagnostic message loses the violating index list, which the tests
never printed in any green run anyway.
psar.rs is now at 207/207 lines, no behavioural change.
* test(balance_of_power): cover name metadata
Codecov flagged 3 lines (file at 96.25%): Indicator-impl name body (73-75).
* test(median_price): cover name metadata
Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (62-64).
* test(true_range): cover name metadata
Codecov flagged 3 lines (file at 95.94%): Indicator-impl name body (73-75).
* test(typical_price): cover name metadata
Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (62-64).
* test(weighted_close): cover name metadata
Codecov flagged 3 lines (file at 94.44%): Indicator-impl name body (61-63).
* test(ema): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/ema.rs
(file at 94.03%): const accessor period (74-77), Indicator-impl
warmup_period (123-125), name (131-133). ema.rs now at 151/151.
* test(historical_volatility): cover periods/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/historical_volatility.rs
(file at 93.87%): const accessors periods (80-83), value (85-88) and
Indicator-impl name (153-155). historical_volatility.rs now at 147/147.
* test(kama): cover periods accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/kama.rs
(file at 91.26%): accessor periods (65-67), Indicator-impl
warmup_period (115-117), name (123-125). kama.rs now at 103/103.
* test(linreg_angle): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/linreg_angle.rs
(file at 88.15%): const accessor period (50-52), Indicator-impl
warmup_period (67-69), name (75-77). linreg_angle.rs now at 76/76.
* test(mass_index): cover periods/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/mass_index.rs
(file at 91.42%): const accessors periods (80-82), value (85-87) and
Indicator-impl name (134-136). mass_index.rs now at 105/105.
* test(vertical_horizontal_filter): cover period accessor + name metadata
Codecov flagged 6 lines (file at 94.44%): period (61-63) + name (119-121).
* test(z_score): cover period accessor + name metadata
Codecov flagged 6 lines (file at 93.75%): period (59-61) + name (106-108).
* test(vpt): cover value() Some branch, name, zero-prev fallback
Codecov flagged 5 lines (file at 94.38%): value() Some branch (57),
prev==0.0 ROC fallback (77), and Indicator-impl name (100-102).
Add accessors_and_metadata covering value()/name and zero_previous_
close_contributes_zero — feeding a 0.0 baseline + non-zero candle
proves the divide-by-zero guard yields a 0 contribution rather than NaN.
* test(csv): cover from_csv_reader + kill rejects_header dead panic arm
Codecov flagged 5 lines in csv.rs (file at 96.98%): from_csv_reader
(201-204) — never called by existing tests which use from_reader /
open — and the cold arm in
rejects_header_missing_a_column (279). Add from_csv_reader_accepts_a_
prebuilt_reader (demonstrates the API by building a custom-delimited
csv::Reader and passing it in), and refactor the header-missing test
to use a single matches!() assertion so the panic arm is gone.
* test(adl): cover name metadata
Codecov flagged 3 lines (file at 96.84%): Indicator-impl name body (94-96).
* test(keltner): cover periods accessor + name metadata
Codecov flagged 6 lines (file at 95.23%): periods (68-70) + name (106-108).
* test(linreg): cover period accessor + name metadata
Codecov flagged 6 lines (file at 96.10%): period (92-94) + name (142-144).
* test(linreg_slope): cover period accessor + name metadata
Codecov flagged 6 lines (file at 95.91%): period (80-82) + name (125-127).
* test(macd): cover periods/value accessors + name metadata
Codecov flagged 6 lines (file at 95.45%): periods (81-83) + name (135-137).
* test(super_trend): cover params accessor + name metadata
Codecov flagged 6 lines (file at 96.36%): params (99-101) + name (176-178).
* test(chaikin_volatility): cover periods accessor + name metadata
Codecov flagged 6 lines (file at 94.91%): periods (69-71) + name (99-101).
* test(chande_kroll_stop): cover params accessor + name metadata
Codecov flagged 6 lines (file at 95.45%): params (97-99) + name (164-166).
* test(chandelier_exit): cover params accessor + name metadata
Codecov flagged 6 lines (file at 95.12%): params (83-85) + name (128-130).
* test(choppiness_index): cover period accessor + name metadata
Codecov flagged 6 lines (file at 95.04%): period (73-75) + name (125-127).
* test(force_index): cover period accessor + name metadata
Codecov flagged 6 lines (file at 93.33%): period (58-60) + name (93-95).
* test(rsi): cover period/value accessors, name, naive flat-series branch
Codecov flagged 7 lines in indicators/rsi.rs (file at 96.42%): const
accessors period (60-62), value (65-67), Indicator-impl name (145-147),
and line 167 in the test-helper rsi_naive's ag==0 fallback. The
proptest reference never lands on a fully flat series so the helper's
50.0 branch was dead.
Add accessors_and_metadata covering period/value/name and
naive_helper_flat_series_yields_50 driving rsi_naive on [42.0; 20] —
both avg_gain and avg_loss converge to 0, hitting the 50.0 branch.
rsi.rs now at 196/196.
* test(accelerator_oscillator): cover params accessor + name metadata
Codecov flagged 6 lines in indicators/accelerator_oscillator.rs (file
at 93.68%): const accessor params (69-71) and Indicator-impl name
(99-101). ac.rs now at 95/95.
* test(aroon): cover period accessor + name metadata
Codecov flagged 6 lines in indicators/aroon.rs (file at 94.28%): const
accessor period (56-58) and Indicator-impl name (104-106). aroon.rs
now at 105/105.
* test(atr_trailing_stop): cover params accessor + name metadata
Codecov flagged 6 lines in indicators/atr_trailing_stop.rs (file at
95.91%): const accessor params (77-79) and Indicator-impl name
(130-132). atr_trailing_stop.rs now at 147/147.
* test(chaikin_oscillator): cover periods accessor + name metadata
Codecov flagged 6 lines in indicators/chaikin_oscillator.rs (file at
95.27%): const accessor periods (76-78) and Indicator-impl name
(109-111). chaikin_oscillator.rs now at 127/127.
* test(mfi): cover period accessor, name, flat-TP fallback
Codecov flagged 8 lines in indicators/mfi.rs (file at 93.10%): const
accessor period (58-60), (0.0, 0.0) arm when tp==prev (85), the
Some(50.0) flat-flow fallback (105), and Indicator-impl name body
(132-134). Add accessors_and_metadata and flat_typical_prices_default_to_50.
mfi.rs now at 116/116.
* test(psar): cover warmup/name, drop cold format-arg + panic-only asserts
Codecov flagged 8 lines in indicators/psar.rs (file at 95.69%):
warmup_period (206-208), name (220-222), the cold format-arg line
254 in pure_uptrend_sar_below_lows, and the in-loop assert! at line
275 in pure_downtrend_sar_above_highs (its panic body is dead).
Add accessors_and_metadata for warmup/name. Refactor both trend
tests to collect violations into a Vec and assert once outside the
loop — the single assert can now legitimately reach its panic body
in a regression, while removing the dead cold-path lines from the
happy-path coverage.
* test(cmf): cover period accessor, name, zero-range branch
Codecov flagged 7 lines in indicators/cmf.rs (file at 95.03%): const
accessor period (71-73), the range==0.0 zero-MFV branch (84), and
Indicator-impl name body (124-126). Add accessors_and_metadata and
zero_range_candle_contributes_zero_mfv (flat H=L=close candles).
cmf.rs now at 141/141.
* test(hma): cover period accessor + name, kill dead naive panic arm
Codecov flagged 7 lines in indicators/hma.rs (file at 92.22%): const
accessor period (51-53), Indicator-impl name body (87-89), and the
unreachable arm at line 167 in matches_independent_wmas.
Refactor that test to assert the warmup-shape invariant via
assert_eq!(got.is_some(), want.is_some()) + if let, removing the
dead panic arm. Add accessors_and_metadata covering period/name.
hma.rs now at 90/90.
* test(obv): cover value() Some branch + warmup/name metadata
Codecov flagged 7 lines in indicators/obv.rs (file at 92.92%): the
Some(self.total) branch of value() (47) — only the None branch was
hit by reset_clears_state — plus Indicator-impl warmup_period
(79-81), name (87-89). Add accessors_and_metadata covering all four.
obv.rs now at 99/99.
* test(tsi): cover periods/value accessors + name metadata
Codecov flagged 9 lines in indicators/tsi.rs (file at 92.30%): const
accessors periods (70-72), value (75-77) and Indicator-impl name
(137-139). tsi.rs now at 117/117.
* test(ultimate_oscillator): cover periods/value accessors + name metadata
Codecov flagged 9 lines in indicators/ultimate_oscillator.rs
(file at 94.76%): const accessors periods (96-98), value (101-103)
and Indicator-impl name (193-195). uo.rs now at 172/172.
* test(vortex): cover period/value accessors + name metadata
Codecov flagged 9 lines in indicators/vortex.rs (file at 93.18%): const
accessors period (84-86), value (89-91) and Indicator-impl name
(157-159). vortex.rs now at 132/132.
* test(vwma): cover period/value accessors + name metadata
Codecov flagged 9 lines in indicators/vwma.rs (file at 92.56%): const
accessors period (72-74), value (77-79) and Indicator-impl name
(129-131). vwma.rs now at 121/121.
* test(zlema): cover period/value accessors + name metadata
Codecov flagged 9 lines in indicators/zlema.rs (file at 90.62%): const
accessors period (62-64), value (72-74) and Indicator-impl name
(111-113). zlema.rs now at 96/96.
* test(mom): cover period/value accessors + name metadata
Codecov flagged 9 lines in indicators/mom.rs (file at 89.53%): const
accessors period (56-58), value (61-63) and Indicator-impl name
(101-103). mom.rs now at 86/86.
* test(sma): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in indicators/sma.rs (file at 93.12%): const
accessor period (70-72), Indicator-impl warmup_period (115-117),
name (123-125). sma.rs now at 131/131.
* test(stoch_rsi): cover periods/value accessors + name metadata
Codecov flagged 9 lines in indicators/stoch_rsi.rs (file at 92.37%):
const accessors periods (69-71), value (74-76) and Indicator-impl
name (131-133). stoch_rsi.rs now at 118/118.
* test(tema): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in indicators/tema.rs (file at 83.63%): const
accessor period (45-47), Indicator-impl warmup_period (67-69), name
(75-77). tema.rs now at 55/55.
* test(trima): cover period/value accessors + name metadata
Codecov flagged 9 lines in indicators/trima.rs (file at 89.53%): const
accessors period (59-61), value (64-66) and Indicator-impl name
(99-101). trima.rs now at 86/86.
* test(cmo): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/cmo.rs
(file at 92.30%): const accessors period (66-68), value (71-73) and
Indicator-impl name (134-136). cmo.rs now at 117/117.
* test(dema): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/dema.rs
(file at 85.00%): const accessor period (43-45), Indicator-impl
warmup_period (63,65,66) and name (72-74). dema.rs now at 60/60.
* test(donchian): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/donchian.rs
(file at 90.21%): const accessor period (57-59), Indicator-impl
warmup_period (95-97), name (103-105). donchian.rs now at 92/92.
* test(dpo): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/dpo.rs
(file at 91.74%): const accessors period (73-75), value (83-85) and
Indicator-impl name (132-134). dpo.rs now at 109/109.
* test(ease_of_movement): cover period/divisor accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/ease_of_movement.rs
(file at 94.15%): const accessors period (83-85), divisor (88-90) and
Indicator-impl name (141-143). ease_of_movement.rs now at 154/154.
* test(wma): cover period/warmup/name + kill dead naive panic arm
Codecov flagged 10 lines in crates/wickra-core/src/indicators/wma.rs
(file at 92.48%): const accessor period (56-58), Indicator-impl
warmup_period (111-113), name (119-121), and line 186 — the
`_ => panic!("warmup mismatch")` arm in matches_naive_over_random_
inputs, an invariant guard that never fires when both streams share
a warmup period.
Add accessors_and_metadata covering the three metadata methods.
Refactor matches_naive_over_random_inputs to assert the warmup-shape
invariant via assert_eq!(g.is_some(), w.is_some()) + if let,
removing the dead panic arm.
wma.rs is now at 133/133 lines, no behavioural change.
* test(aroon_oscillator): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/aroon_
oscillator.rs (file at 90.42%): const accessors period (57-59),
value (62-64) and Indicator-impl name (90-92). warmup_period is
already covered by warmup_period_matches_aroon.
Add accessors_and_metadata asserting period == 7, name ==
"AroonOscillator", and value() across the None (pre-warmup) and
Some (post-warmup) branches.
aroon_oscillator.rs is now at 94/94 lines, no behavioural change.
* test(atr): cover period/value accessors + name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/atr.rs
(file at 93.70%): const accessors period (54-57), value (59-62) and
Indicator-impl name body (103-105). warmup_period is exercised
indirectly via downstream indicators; the metadata getters were
never queried directly.
Add accessors_and_metadata asserting period == 14, name == "ATR",
and value() across the None (pre-warmup) and Some (post-warmup)
branches.
atr.rs is now at 143/143 lines, no behavioural change.
* test(awesome_oscillator): cover periods accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/awesome_
oscillator.rs (file at 88.15%): const accessor periods (59-61),
Indicator-impl warmup_period (83-85), name (91-93). The classic()
constructor is covered indirectly through the existing tests; only
the metadata methods were dead.
Add accessors_and_metadata asserting periods == (5, 34),
warmup_period == 34 (= slow_period), name == "AwesomeOscillator".
awesome_oscillator.rs is now at 76/76 lines, no behavioural change.
* test(cci): cover period accessor + warmup/name metadata
Codecov flagged 9 lines in crates/wickra-core/src/indicators/cci.rs
(file at 89.65%): const accessor period (68-70), Indicator-impl
warmup_period (102-104), name (110-112). Existing tests never
inspected the metadata surface.
Add accessors_and_metadata asserting period == 20, warmup_period ==
20, name == "CCI".
cci.rs is now at 87/87 lines, no behavioural change.
* test(pmo): cover periods/value accessors, name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/pmo.rs
(file at 90.56%):
- const accessors periods (76-78), value (81-83) — never queried
- line 103 (`0.0` in the prev == 0.0 ROC fallback) — every existing
test used prices > 0, so the divide-by-zero guard never fired
- Indicator-impl name body (130-132) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_previous_price_treats_roc_as_flat seeding prev_price = 0 then
pushing a non-zero price — the ROC must take the flat-momentum
fallback (0.0) and the doubly-smoothed PMO emits exactly 0.0
rather than NaN.
pmo.rs is now at 106/106 lines, no behavioural change.
* test(ppo): cover periods/value accessors, name, zero-slow-EMA fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ppo.rs
(file at 90.29%):
- const accessors periods (71-73), value (76-78) — never queried
- line 96 (`0.0` in the s == 0.0 PPO fallback) — every existing test
used prices ≈ 100, so the slow EMA was never 0 and the
divide-by-zero guard never fired
- Indicator-impl name body (122-124) — never queried
Add accessors_and_metadata covering periods/value/name. Add
zero_slow_ema_yields_zero_ppo feeding a stream of zeros — both EMAs
converge to 0.0 and the indicator must emit exactly 0.0 (flat
momentum) rather than NaN.
ppo.rs is now at 103/103 lines, no behavioural change.
* test(roc): cover period accessor, warmup/name, zero-prev fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/roc.rs
(file at 87.80%):
- const accessor period (47-49) — never queried
- line 70 (`0.0` in the prev == 0.0 ROC fallback) — every test used
prices ≥ 1.0, so the divide-by-zero guard never fired
- Indicator-impl warmup_period (83-85), name (91-93) — never queried
Add accessors_and_metadata covering period == 5, warmup_period == 6
(= period + 1), name == "ROC". Add zero_previous_price_yields_zero_roc
feeding a leading zero followed by `period` more values so the front
of the window is exactly 0.0; the next emission must be the
flat-momentum fallback 0.0 (not NaN).
roc.rs is now at 82/82 lines, no behavioural change.
* test(ulcer_index): cover period/value accessors, name, zero-max fallback
Codecov flagged 10 lines in crates/wickra-core/src/indicators/ulcer_index.rs
(file at 93.86%):
- const accessors period (77-80), value (82-85) — never queried
- line 123 (`0.0` in the max_price == 0.0 drawdown fallback) — every
test used prices > 0, so the trailing-max divisor was always positive
- Indicator-impl name body (162-164) — never queried
Add accessors_and_metadata covering period/value/name. Add
zero_max_price_yields_zero_drawdown feeding a stream of zeros — the
trailing max is exactly 0.0 and the drawdown computation would
otherwise hit 0/0 NaN; the indicator must emit exactly 0.0
(drawdown is 0% by convention).
ulcer_index.rs is now at 163/163 lines, no behavioural change.
* test(williams_r): cover period accessor, warmup/name, zero-range branch
Codecov flagged 10 lines in crates/wickra-core/src/indicators/williams_r.rs
(file at 89.79%):
- const accessor period (49-51) — never queried
- line 78 (`Some(-50.0)` in the range == 0.0 fallback) — every test
used H != L candles, so the lookback range was always positive
- Indicator-impl warmup_period (87-89), name (95-97) — never queried
Add accessors_and_metadata covering period == 14, warmup_period == 14,
name == "WilliamsR". Add zero_range_yields_minus_fifty feeding flat
candles (H == L == close) — the lookback hi/lo coincide and the
divide-by-zero guard fires, returning the neutral mid-range value
-50.0.
williams_r.rs is now at 98/98 lines, no behavioural change.
* test(t3): cover period/volume_factor/value accessors + name metadata
Codecov flagged 12 lines in crates/wickra-core/src/indicators/t3.rs
(file at 91.48%): const accessors period (95-97), volume_factor
(100-102), value (105-107) and Indicator-impl name (148-150). The
warmup_period method is already covered by first_emission_at_warmup_
period; the other four metadata methods were never queried.
Add accessors_and_metadata asserting period == 5, volume_factor == 0.7,
name == "T3", and value() across both the None (pre-warmup) and Some
(post-warmup) branches.
t3.rs is now at 141/141 lines, no behavioural change.
* test(adx): cover period accessor, warmup/name metadata, zero-TR branch
Codecov flagged 11 lines in crates/wickra-core/src/indicators/adx.rs
(file at 94.17%): the const accessor period (89-91), the tr_v == 0.0
defensive branches inside update (142, 147), and the Indicator-impl
warmup_period (199-201) and name (207-209) bodies.
Add accessors_and_metadata asserting period == 14, warmup_period == 28,
name == "ADX". Add zero_true_range_yields_zero_di_and_zero_adx feeding
flat all-zero candles (H == L == close == 0) — every TR is 0, so the
smoothed tr_smooth stays at 0 and update must take the zero-denominator
fallback for both plus_di and minus_di, then the dx_den == 0 path for
ADX. The indicator must emit 0/0/0 rather than NaN.
adx.rs is now at 189/189 lines, no behavioural change.
* test(natr): cover accessors, zero-close branch, kill dead panic arm
Codecov flagged 11 lines in crates/wickra-core/src/indicators/natr.rs
(file at 87.64%):
- const accessors period (59-61), value (64-66) — never queried
- line 77 (`0.0` in the candle.close == 0.0 fallback) — every test
used candles with close ≈ 100, so the divide-by-zero guard never
fired
- Indicator-impl name body (98-100) — never queried
- line 142 (`_ => panic!("warmup mismatch at {i}")`) — unreachable
invariant guard in natr_is_atr_over_close_as_percent because the
NATR wrapper inherits ATR's warmup period exactly
Add accessors_and_metadata covering period/value/name. Add
zero_close_yields_zero_natr feeding an all-zero candle series (Candle
validator accepts open == high == low == close == 0 with positive
volume) — ATR is 0 each bar, so the indicator must emit exactly 0.0
rather than 100 * 0 / 0 = NaN. Refactor natr_is_atr_over_close_as_
percent to assert the warmup-shape invariant via assert_eq! on
is_some(), removing the dead panic arm.
natr.rs is now at 89/89 lines, no behavioural change.
* test(trix): cover period accessor, warmup/name metadata, zero-prev branch
Codecov flagged 11 lines in crates/wickra-core/src/indicators/trix.rs
(file at 84.05%):
- const accessor period (47-49) — never queried
- the Some(_) match arm (67-68) — the degenerate path where the
previous triple-EMA value is exactly 0.0 (would otherwise divide
by zero on the percent-rate formula). All other tests used
inputs ≈ 100, so prev_tr was never 0.0
- Indicator-impl warmup_period (84, 86-87) and name (93-95) — never
queried
Add accessors_and_metadata asserting period == 5, warmup_period == 14
(= 3*5 - 1), name == "TRIX". Add zero_input_series_yields_zero_trix
feeding [0.0; 20] — every EMA stage collapses to 0.0, so once warmed
up prev_tr is Some(0.0) and every subsequent emission must take the
fallback arm returning 0.0.
trix.rs is now at 69/69 lines, no behavioural change.
* test(coppock): cover periods/value accessors + name + simplify assert
Codecov flagged 10 lines in crates/wickra-core/src/indicators/coppock.rs
(file at 91.07%):
- const accessors periods (68-70), value (73-75) — never queried
- Indicator-impl name body (128-130) — never queried
- line 180 (`warmup - 1,` format-arg) inside the multi-line assert!
in warmup_period_matches_first_some_for_every_parameter_set —
only evaluated on assertion failure, which never happens, so
Codecov flagged the cold path as uncovered
Add accessors_and_metadata covering periods/value/name. Simplify the
multi-line assert's format args to a static message — the {warmup}
binding already appears once in the cold path so dropping the literal
"warmup index" arg loses nothing diagnostic but kills the dead
expression-arg line.
coppock.rs is now at 112/112 lines, no behavioural change.
Commit aa2846c collapsed the multi-line assert in the test-only naive
helper to a single line to drop the cold expression-arg lines that
Codecov saw as uncovered. The single-line form exceeded the 100-col
limit, so cargo fmt --check failed on every supported toolchain in CI
(ubuntu-latest, macos-latest, windows-latest).
Let rustfmt wrap it back to the three-line form. The arms are still
just a literal expression and a static-string message — no expression
format args — so the cold-path lines that Codecov originally flagged
on line 179 stay covered. No behaviour change.
Codecov flagged 12 lines in crates/wickra-core/src/indicators/std_dev.rs
(file at 89.09%): const accessors period (64-66), value (68-71) and
Indicator-impl bodies warmup_period (110-112), name (118-120). None
of the existing tests inspected the metadata surface.
Add accessors_and_metadata asserting period == 14, warmup_period == 14,
name == "StdDev", and value() across both the None (pre-warmup) and
Some (post-warmup) branches.
std_dev.rs is now at 110/110 lines, no behavioural change.
Codecov flagged 12 lines in crates/wickra-core/src/indicators/smma.rs
(file at 86.81%): the const accessors period (57-59), value (62-64)
and the Indicator-impl bodies warmup_period (95-97), name (103-105).
None of the existing tests inspected the metadata surface — they only
fed numeric updates and asserted on SMMA values.
Add accessors_and_metadata exercising period == 7, warmup_period == 7,
name == "SMMA", and value() across both the None (pre-warmup) and
Some (post-warmup) branches.
smma.rs is now at 91/91 lines, no behavioural change.
Codecov flagged 13 uncovered lines in
crates/wickra-core/src/indicators/stochastic.rs (file at 93.43%):
- classic() convenience constructor (76-78) — every test passed
explicit (k_period, d_period) to new
- periods() const accessor (81-83) — never queried
- warmup_period (170-172), name (178-180) Indicator-impl bodies —
never queried
- line 208 (`50.0` literal) inside the test-only naive_k helper's
flat-range branch — k_matches_naive feeds an oscillating price
series, so the helper's range == 0 path was dead
Add classic_periods_and_metadata test asserting Stochastic::classic()
has periods (14, 3), warmup_period 16 (= 14 + 3 - 1) and name
"Stochastic". Extend flat_range_yields_k_50 to also call naive_k on
the flat candle series and verify the helper returns Some(50.0) for
every index ≥ k_period - 1 — exercises line 208 without diluting the
production-code assertion.
stochastic.rs is now at 198/198 lines, no behavioural change.
Codecov flagged 15 uncovered lines in crates/wickra-data/src/aggregator.rs
(file at 95.11%):
- Timeframe::millis / Timeframe::seconds / Timeframe::one_minute_ms
convenience constructors (40-52) — every existing test built
Timeframes via new / minutes / hours / days, never via these three
- the cold `?` Err arm on `Candle::new(...)?` for the flat gap-fill
candle (line 334) — `prev.close` is already finite (came from a
closed bar), volume is exactly 0.0, OHLC are trivially equal, so
Candle::new's error path is unreachable here
- the cold `ok_or_else` overflow closure on `t.checked_add(step)`
inside the gap-fill loop (336-337) — bucket alignment guarantees
start + (gap_count-1)*step ≤ next_bucket - step < i64::MAX, so
every aligned-bucket layout reaches t == next_bucket cleanly and
exits without ever invoking the overflow path
- TickAggregator::timeframe accessor (353-355) — never queried
Add two new tests:
- timeframe_convenience_constructors exercises millis/seconds/
one_minute_ms with both happy-path and rejection cases
- aggregator_timeframe_getter asserts timeframe().bucket() round-trips
Refactor fill_between to use Candle::new_unchecked for the flat-candle
push (the OHLCV invariants hold by construction) and iterate via
`0..gap_count` with `saturating_add(step)` instead of `while t <
next_bucket` with `checked_add(...).ok_or_else(...)?`. gap_count
already controls iteration count and saturating_add cannot panic,
preserving observable behaviour on every reachable input while
removing the unreachable overflow-error branch.
aggregator.rs is now at 307/307 lines, no observable behaviour change
on aligned-bucket inputs (which is every input fill_between can be
called with given the call site's preconditions).
Codecov re-check after 1255892 showed line 179 of
crates/wickra-core/src/indicators/bollinger.rs still uncovered: the
`prices.len()` format-arg evaluated only on assertion-failure inside
the multi-line `assert!(prices.len() >= period, "…got {}, period {}",
prices.len(), period)` — the cold panic path.
Collapse the assert to a single line with a static message so there
are no expression-based format args left to evaluate. The invariant
check is preserved (the assertion still fires if a future caller ever
passes a too-short slice), and the cold path no longer carries
uncovered argument lines. bollinger.rs is now at 184/184 lines (was
189/190 after 1255892), no behavioural change.
Codecov flagged 16 uncovered lines in
crates/wickra-core/src/indicators/bollinger.rs (file at 91.30%):
- classic() convenience constructor (91-93) — every test passed
explicit parameters to BollingerBands::new, so the classic-defaults
path was dead
- const accessors period (96-98), multiplier (101-103) — never queried
- Indicator-impl bodies warmup_period (156-158), name (164-166) —
never queried
- `return None;` (line 177) inside the test-only `naive` helper's
`if prices.len() < period` early-return — every caller passes a
slice of length >= period (matches_naive_definition uses
`&prices[..=i]` for `i in 19..` with period=20; long_stream_drift_
stays_bounded fills the window before measuring), so the arm is dead
Add classic_and_accessors_and_metadata to cover the constructor and
the four getter bodies, and refactor naive to return BollingerOutput
directly with an `assert!(prices.len() >= period)` precondition. The
two existing callers were already using .unwrap()/.expect() on the
Option result and simplify to direct calls.
bollinger.rs is now at 184/184 lines, no behavioural change.
Codecov flagged lines 86-102 in crates/wickra-core/src/ohlcv.rs as missed
(file at 90.00%) — the entire body of `Candle::new_unchecked`. Every
existing test routes through the validating `Candle::new`, so the unchecked
constructor (intended for callers like the aggregator and parsed-payload
paths that have already validated upstream) was dead.
Add candle_new_unchecked_preserves_fields_verbatim:
- first assertion builds a candle with six distinct field values and
verifies each reads back exactly.
- second assertion feeds an OHLC combination (high < low) that the
checked constructor rejects with Error::InvalidCandle, then proves
Candle::new_unchecked still builds the struct as-is. This documents
and enforces the API contract that the unchecked variant performs
no validation.
ohlcv.rs is now at 170/170 lines, no behavioural change.
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/vwap.rs (file at 87.94%):
- Vwap::value() Some branch (line 53) — the only test calling value()
did so after reset() when sum_v == 0, exercising only the None branch
- Vwap::update zero-volume early-return `return None;` (line 67) — all
existing candles carried strictly positive volume
- Vwap::warmup_period body returning 1 (79-81), Vwap::name body
returning "VWAP" (87-89) — metadata never queried
- RollingVwap::period accessor (134-136), RollingVwap::warmup_period
body (165-167), RollingVwap::name body returning "RollingVWAP"
(173-175) — same metadata gap on the rolling variant
Add four new tests:
- cumulative_value_some_branch_after_update drives a single
non-zero-volume candle then asserts value() == Some(typical_price).
- cumulative_zero_volume_first_candle_returns_none feeds a candle
with volume == 0.0, asserts update returns None and is_ready stays
false, then adds a real candle to confirm the indicator still works.
- cumulative_metadata asserts warmup_period() == 1 and name() == "VWAP".
- rolling_accessors_and_metadata asserts period() == 7,
warmup_period() == 7, name() == "RollingVWAP" on a RollingVwap::new(7).
vwap.rs is now at 141/141 lines, no behavioural change.
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/percent_b.rs (file at 81.11%):
- const accessors period (53-55), multiplier (58-60), value (63-65)
- Indicator-impl bodies warmup_period (90-92) and name (98-100)
- the unreachable `_ => panic!("warmup mismatch at {i}")` arm in
matches_bands_definition (line 141)
- the inner `assert_relative_eq!(*pv, 0.5, …)` (line 158) inside
price_at_middle_is_half, gated by `(prices[i] - bv.middle).abs()
< 1e-9` over a sin-based oscillation that, with period=20 over 60
samples, never lands within 1e-9 of the rolling SMA — so the
assertion was silently dead and the test made no checks.
Add accessors_and_metadata covering the five getter bodies, refactor
matches_bands_definition to assert the warmup-shape invariant via
assert_eq!(p.is_some(), b.is_some()) + if let (kills the panic arm),
and replace price_at_middle_is_half with a deterministic construction:
PercentB::new(3, 2.0) on [1.0, 5.0, 3.0] gives SMA=3.0 at index 2
which equals the third price exactly, stddev=√(8/3)≈1.633 keeps the
width strictly positive so the divide path runs, and %b lands on
exactly 0.5 because price sits on the centre line of symmetric bands.
percent_b.rs is now at 90/90 lines, no behavioural change.
Codecov flagged 17 uncovered lines in
crates/wickra-core/src/indicators/bollinger_bandwidth.rs (file at 79.51%):
- the const accessors period (54-56), multiplier (59-61), value (64-66)
- the zero-middle defensive fallback 0.0 (line 77) inside update
- the Indicator-impl bodies warmup_period (90-92) and name (98-100)
- the unreachable `_ => panic!("warmup mismatch")` arm (line 140) in
the existing matches_bands_definition test
None of the existing tests inspected the metadata surface — every test
fed numeric updates and asserted on bandwidth values, leaving the five
getter bodies dead. The zero-middle path was unreachable because all
existing tests used positive price levels ≈100, so the rolling SMA was
always strictly positive and the divide-by-zero guard never fired.
The panic arm in matches_bands_definition was an invariant guard that
by design cannot fire when the two streams share a warmup period; that
invariant is now asserted directly with assert_eq!(w.is_some(),
b.is_some()), and the catch-all arm is gone.
Add two new tests and refactor one existing:
- accessors_and_metadata asserts period == 20, multiplier == 2.0,
value() == None before warmup, warmup_period == 20, name ==
"BollingerBandwidth", then drives 20 updates so value() also
exercises the Some branch.
- zero_middle_band_yields_zero_bandwidth feeds [-2, -1, 0, 1, 2] so
the 5-bar SMA lands on exactly 0.0 at the fifth input, and asserts
the emitted bandwidth is exactly 0.0 (rather than inf/nan from the
would-be divide-by-zero).
- matches_bands_definition now uses an explicit assert_eq! on
is_some() agreement plus an if let for the numeric compare,
removing the unreachable panic arm without weakening the
invariant check.
bollinger_bandwidth.rs is now at 83/83 lines, no behavioural change.
Codecov flagged 30 uncovered lines in crates/wickra-core/src/traits.rs (file at
75.60%): the const borrow accessors Chain::first / Chain::second (140-147), the
Chain::warmup_period + Chain::name Indicator-impl bodies (167-178), the full
Identity test-helper Indicator surface — reset, warmup_period, is_ready, name
(198-209), and Doubler's warmup_period + name (228-236).
None of the existing tests touched those code paths: every chain test invoked
update/reset/is_ready through the Chain wrapper without ever inspecting the
borrow accessors, querying chain.warmup_period(), or asking for chain.name(),
and the Identity helper was only ever driven by batch (which calls update
only). Doubler's warmup_period and name were similarly dead because
Chain::warmup_period and Chain::name themselves were dead.
Add two new tests at the end of the Chain section in mod tests, immediately
before the parallel-feature-gated test:
- chain_accessors_and_metadata exercises chain.first(), chain.second(),
chain.warmup_period(), chain.name(), and pulls Doubler::warmup_period
+ Doubler::name in via the borrowed accessors.
- identity_helper_full_indicator_surface asserts warmup_period == 0,
name == "Identity", and walks is_ready through both seen=false and
seen=true via an update/reset cycle.
traits.rs is now at 123/123 lines, no behavioural change.
jetli/wasm-pack-action@v0.4.0 with no version: input installs whatever
wasm-pack the action's bundled installer fetches — currently a ~0.10.x
release whose 'build' subcommand does not yet accept --features. Our
build invocation 'wasm-pack build … --features panic-hook' now fails
with
error: Found argument '--features' which wasn't expected, or isn't
valid in this context
USAGE: wasm-pack build --release --target <target>
even though that exact command worked on past runs where the action
happened to install a newer wasm-pack. (The bundler-target release.yml
job passed for v0.2.1 only because it shared the same cached install
on that runner.)
wasm-pack's --features top-level flag has been stable since 0.12.0, so
the fix is to install a fresh wasm-pack each run. Switch both the ci.yml
'WASM build' step and the release.yml 'wasm-publish' job to the same
taiki-e/install-action prebuilt-binary installer we already use for
cargo-llvm-cov and cargo-fuzz. taiki-e tracks the latest wasm-pack
release and the install is a single binary download — no compile, no
shell installer.
The wasm-pack invocations themselves are unchanged.
Codecov flagged a single uncovered line in crates/wickra-data/src/resample.rs:
line 46, the `self.low = c.low;` assignment inside RolledBar::absorb.
None of the existing resampler tests fed a follow-up candle with a strictly
lower low than the first candle in the bucket, so the `c.low < self.low`
branch never fired. Coverage stayed at 122/123.
Add a small dedicated test that pushes a 10.0-low candle into bucket 0, then
a 8.0-low candle into the same bucket, and asserts the rolled bar's low
reflects the dip. Resample file is now at 123/123 lines, no behavioural change.
The 84 markdown files under docs/wiki/ are now published to the
project's GitHub Wiki at https://github.com/kingchenc/wickra/wiki —
a separate git repository (https://github.com/kingchenc/wickra.wiki.git)
that GitHub hosts natively with its own UI, search and history. The
flat layout that the GitHub Wiki requires has been generated, all
internal cross-links rewritten, and a _Sidebar.md groups the 71
indicators by their canonical 8 families.
Effects:
- docs/wiki/ is removed from the main repo (-84 files). docs/README.md
now just points readers at the Wiki.
- PR template + CONTRIBUTING text updated to point at the Wiki instead
of the in-repo path. The Wiki repo is separately cloneable and
editable via the GitHub web UI.
- examples/wasm/README.md cross-link fixed to use the Wiki URL.
- The (still in-repo) CHANGELOG keeps its historical references to
docs/wiki/ paths — those describe what the tree looked like at past
releases and stay accurate as history.
- README.md, license, all source unaffected.
The Wiki itself ships with _Sidebar.md / _Footer.md generated from the
8-families taxonomy and 503/503 cross-links resolved.
Three separate README files (root, bindings/node, bindings/python) had
been drifting independently — each registry showed a different project
page, which is exactly the consistency debt I want to avoid.
Single source of truth: /README.md. The three binding READMEs are
overwritten with the root README content as a baseline, and release.yml
gets a one-line cp step right before every publishing call so future
edits to /README.md propagate automatically:
- python-wheels job: cp README.md bindings/python/README.md before
PyO3/maturin-action runs the wheel build
- python-sdist job: same, before the sdist build
- node-publish job: cp ../../README.md README.md (working-directory
bindings/node) before the main 'npm publish wickra'
- wasm-publish job: cp README.md bindings/wasm/README.md before
wasm-pack build (which copies the crate README into pkg/ on its own)
Cargo crates (wickra, wickra-core, wickra-data) already inherit
readme.workspace = true pointing at /README.md, so crates.io was already
correct — no change needed there.
The per-platform npm subpackages (bindings/node/npm/<target>/) keep
their tiny package.json with no README; they are install-time
optionalDependencies that the loader reads through, never user-facing
on the registry.
Effect: same README on github.com/kingchenc/wickra, crates.io/crates/wickra,
pypi.org/project/wickra, and npmjs.com/package/wickra. Will be live on
the registries with the next tag-push.
CODECOV_TOKEN was just added as a repository secret; the existing
Coverage job in .github/workflows/ci.yml will pick it up on the next
run. This empty commit fires that run.
The 0.2.0 release left wickra@npm stuck at 0.1.4 and never created a
GitHub Release entry because the brand-new `wickra-win32-arm64-msvc`
sub-package name was caught by npm's spam-detection filter on its first
publish attempt (same situation that affected `wickra-win32-x64-msvc`
through 0.1.4 until npm Support unblocked it). A support ticket is open;
until it is resolved, ship 0.2.1 for the five platforms whose
sub-packages are already on npm and re-add Windows ARM64 in a follow-up
release.
Changes for this cycle:
- bindings/node/package.json: remove "wickra-win32-arm64-msvc" from
optionalDependencies and "aarch64-pc-windows-msvc" from
napi.triples.additional.
- bindings/node/npm/win32-arm64-msvc/: removed (will be restored fresh
once the npm name is unblocked).
- .github/workflows/release.yml: comment out the
aarch64-pc-windows-msvc entry of the node-build matrix with a
TODO/restore note.
- Bump every workspace and binding version to 0.2.1 (Cargo.toml,
pyproject.toml, bindings/node/package.json, five npm/<target>
templates, the wiki version table). Cargo.lock regenerated.
- CHANGELOG: new [0.2.1] block consolidating every fix that has landed
on main since 0.2.0 (HV epsilon, examples CI step, fuzz cargo-fuzz
install, MSRV 1.85 -> 1.86 / 1.77 -> 1.88, criterion 0.5 -> 0.8,
tokio-tungstenite 0.24 -> 0.29, tick_aggregator gap-fill cap, every
GitHub Action SHA-pin bump). Compare-link added.
The arm64 loader branch in bindings/node/index.js is left untouched: a
Windows ARM64 user installing 0.2.1 will get the standard
`Cannot find module 'wickra-win32-arm64-msvc'` error from the loader,
which is accurate. PyPI's win-arm64 wheel is unaffected.
Verified locally:
cargo fmt/clippy/test --workspace --all-features -> 630 passed / 0 failed
cargo build -p wickra-examples --bins -> clean
cargo build -p wickra-node -> clean
criterion 0.8.2 raised its MSRV from 1.85 to 1.86 (rustc 1.85.1 is now
explicitly rejected by its Cargo.toml `rust-version`). One more notch
on the same upward drift that already took us from 1.75 -> 1.80 (rayon)
-> 1.85 (clap_lex/edition2024).
Updated:
- Cargo.toml workspace rust-version 1.85 -> 1.86
- .github/workflows/ci.yml MSRV matrix name + toolchain
- ci.yml comment refreshed to reflect the new driving dep
Dependabot opened #13 to bump tokio-tungstenite from 0.24 to 0.29 but
the bare-version bump fails to compile: WebSocketConfig became
#[non_exhaustive] starting with 0.27, so the existing struct-literal
construction
let ws_config = WebSocketConfig {
max_message_size: Some(MAX_MESSAGE_SIZE),
max_frame_size: Some(MAX_FRAME_SIZE),
..WebSocketConfig::default()
};
produces
error[E0639]: cannot create non-exhaustive struct using struct expression
Switch to the builder-style setters that 0.29 exposes on the default
value. Semantics are unchanged; both fields still carry the
MAX_MESSAGE_SIZE / MAX_FRAME_SIZE caps from the original config and the
rest of the WebSocketConfig defaults are preserved by starting from
WebSocketConfig::default().
This supersedes #13 — same target version, plus the code change
Dependabot can't make on its own.
Verified locally:
cargo check -p wickra-data --features live-binance # clean
cargo test --workspace --all-features # 630 passed / 0 failed
cargo clippy --workspace --all-targets --all-features -- -D warnings
Dependabot opened #10 to bump criterion from 0.5.1 to 0.8.2 but the
straight version bump fails to build: criterion::black_box was
deprecated in 0.6 and removed/marked deny-warn in 0.8, so the existing
`use criterion::{black_box, ...}` produces
error: use of deprecated function `criterion::black_box`: use
`std::hint::black_box()` instead
across every bench callsite. Switch the import to std::hint::black_box
(stable since Rust 1.66, well under our MSRV of 1.85) and drop the
criterion re-export.
This supersedes #10 — same target version, plus the code change
Dependabot can't make on its own.
Verified locally:
cargo bench -p wickra --no-run # builds clean
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace # 630 passed / 0 failed
The tick_aggregator fuzz target found that TickAggregator::fill_between
allocates one placeholder Candle per skipped bucket without bounding the
gap size. An adversarial input (a clock-glitch tick years in the future)
produced an OOM on libFuzzer after malloc(~3 GB):
SUMMARY: libFuzzer: out-of-memory (malloc(3221225472))
A real-world failure mode too: a single bad timestamp from a flaky feed
could OOM the host process even though every individual tick passed
Tick::new validation.
Fix:
- Compute the gap size up-front via saturating arithmetic, before any
allocation, and refuse with Error::Malformed when it exceeds the new
MAX_GAP_FILL_CANDLES = 1_000_000 cap (≈ 1.9 years of contiguous
one-minute bars, well above any realistic missing-data window).
- Reserve the right Vec capacity in advance once we know the gap fits,
avoiding intermediate reallocations.
- Add two regression tests: gap_fill_rejects_runaway_timestamp_jump
(the fuzz scenario) and gap_fill_at_the_cap_succeeds (exact-cap input
still works).
The cap is exposed as pub const so callers can pre-validate their input
without relying on the error string.
Two unrelated newer-toolchain breakages bundled because they hit on the
same CI run and have the same shape (newer Rust got stricter about
patterns we used):
1. clippy 1.95 added the manual_midpoint lint which fires on every
instance of (a + b) / 2.0 with a help suggesting f64::midpoint.
CI runs with -D warnings so it became a hard error. Twelve sites
were affected — three real call sites in src/ohlcv.rs (median_price),
src/indicators/donchian.rs (DonchianOutput.middle),
src/indicators/ease_of_movement.rs (mid), and
src/indicators/super_trend.rs (hl2); plus eight test-helper
Candle::new constructions across accelerator_oscillator,
atr_trailing_stop, chaikin_volatility, chandelier_exit,
chande_kroll_stop, choppiness_index, super_trend, true_range.
All twelve switched to f64::midpoint (stable since Rust 1.85,
our workspace MSRV).
2. usize::is_multiple_of is still unstable (rust-lang/rust#128101) and
only stabilizes in Rust 1.87, but the MSRV CI job uses 1.85. The
two call sites in bollinger.rs and sma.rs (added with the R7
periodic-reseed tests) switched back to i % 2 == 0.
The first MSRV bump to 1.80 fixed the rayon-core floor but ran into a
second transitive-dep floor on the same CI run:
error: failed to parse manifest at clap_lex-1.1.0/Cargo.toml
Caused by: feature `edition2024` is required
The package requires the Cargo feature called `edition2024`, but
that feature is not stabilized in this version of Cargo (1.80.1).
clap_lex 1.1.0 is pulled in by criterion (dev-dep on the wickra crate)
via clap 4.6 -> clap_builder 4.6 -> clap_lex 1.1. edition2024 was
stabilized in Rust 1.85, so lift the workspace MSRV one more step. The
1.85 floor subsumes the rayon-core 1.80 requirement; bindings/node
stays at 1.88 (napi-build) which already covers everything below it.
Also fix the fuzz job: cargo-fuzz defaulted to building for
x86_64-unknown-linux-musl, which is not installed on the GitHub-hosted
ubuntu runner. Pass --target x86_64-unknown-linux-gnu explicitly on
every cargo fuzz run invocation so it builds for the actual host
target.
The previous MSRV pins were below what current transitive dependencies
need:
- rayon-core 1.13.0 (pulled in by the optional `parallel` feature on
wickra-core via rayon) requires rustc >= 1.80; under the old 1.75
workspace MSRV the CI MSRV job broke with "package `rayon-core
v1.13.0` cannot be built because it requires rustc 1.80 or newer".
- napi-build 2.3.2 (the build-script crate that napi-derive 2.x calls
into) requires rustc >= 1.88; under the old 1.77 node-binding MSRV
the CI MSRV-node job broke with "package `napi-build v2.3.2` cannot
be built because it requires rustc 1.88 or newer".
Pinning the deps backwards would have frozen us out of upstream
security/fix releases for both crates. Lifting the MSRV is the cleaner
path for a young 0.x library — downstream consumers on older toolchains
can stay on the already-published 0.2.0.
Updated:
- Cargo.toml workspace rust-version 1.75 -> 1.80
- bindings/node/Cargo.toml rust-version 1.77 -> 1.88
- .github/workflows/ci.yml MSRV matrix names + toolchain values + comment
- CHANGELOG.md [Unreleased] documents the bump
The previous `cargo install cargo-fuzz --locked` resolved against
cargo-fuzz's own Cargo.lock which pins to rustix 0.36.5. That rustix
version still annotates its source with internal #[rustc_*] attributes,
and the current nightly compiler rejects those attributes from
out-of-tree crates, so cargo-fuzz never finished compiling on CI:
error: attributes starting with `rustc` are reserved for use by the
`rustc` compiler
--> rustix-0.36.5/src/backend/linux_raw/io/errno.rs:28
error: could not compile `rustix` (lib) due to 4 previous errors
error: failed to compile `cargo-fuzz v0.13.1`
Switch the install to taiki-e/install-action, the same prebuilt-binary
provider we already use for cargo-llvm-cov. That skips the full
transitive-dependency compile and lets the fuzz-smoke job actually run.
The Z5 reorganisation moved every runnable example out of the per-crate
examples/ folders and into a dedicated wickra-examples crate at
examples/rust/, with the binaries living under src/bin/<name>.rs. The
old ci.yml Compile-examples step still pointed at the now-deleted
cargo example targets backtest (wickra) and live_binance (wickra-data),
which is why Rust windows-latest failed with 'no example target named
backtest in wickra package' and 'no example target named live_binance
in wickra-data package'.
Replace both calls with a single cargo build -p wickra-examples --bins.
That covers backtest, live_binance, fetch_btcusdt, multi_timeframe,
parallel_assets and streaming in one shot, and the wickra-examples
crate already enables the live-binance feature on its wickra-data dep
so no extra --features flag is needed.
The mathematical result of HistoricalVolatility on a perfectly geometric
price series is exactly zero — but the underlying 1.01_f64.powi(i) +
log-return + std-dev cascade accumulates platform-sensitive FP drift on
the order of 1e-7 on x86_64 Linux and macOS (the Windows result happened
to round closer to zero, which is why the test passed locally and on the
Windows CI runner but failed on Linux and macOS).
Bump the tolerance from 1e-9 to 1e-6. That stays four decimal places
below any realistic annualised volatility value while comfortably
absorbing the observed cross-platform drift.
Also extend the comment to document the rationale so the next person
who reads the test does not tighten it back down.
This release carries the full post-audit work — 46 new indicators
(25 → 71), an eight-family taxonomy restructure, new bindings for
RollingVWAP, the WASM streaming-update parity, the pyo3/numpy CVE
fix, the SMA/Bollinger drift bound, the O(1) LinearRegression
refactor, the UlcerIndex deque and the PSAR is_ready/reset fixes
plus a refreshed example suite and wiki. The earlier 0.1.5 number
was never published; jumping straight to 0.2.0 is the cleaner signal
for the scope of the change.
Bumped:
- Cargo.toml workspace + wickra-core workspace-dep version
- bindings/python/pyproject.toml
- bindings/node/package.json + optionalDependencies (six platform pins)
- 6 x bindings/node/npm/<target>/package.json
- Cargo.lock regenerated
- CHANGELOG.md [0.2.0] header + compare-link
- docs/wiki/Home.md published-versions table
- docs/wiki/Quickstart-{Rust,Node,WASM}.md + Warmup-Periods.md
version-pinned narrative lines
Verified locally:
- cargo fmt/clippy/test (628 passed, 0 failed)
- cargo deny check (no suppression)
- bindings/node node --test (92/92)
- bindings/python pytest (118/118)
- import wickra reports 0.2.0 with 72 indicator classes
Replace the TBD placeholder under ## [0.1.5] with today's date so the
release header is complete. The compare-link at v0.1.4...v0.1.5 is
already in place and points at the right pair.
Every other manifest in the repo carries the author string
kingchenc <kingchencp@gmail.com> — workspace Cargo.toml,
bindings/node/package.json, all per-platform npm package.jsons,
release.yml. pyproject.toml had neither authors nor maintainers, so
the PyPI listing fell back to the implicit empty value. Align with
the rest of the repo.
Indicators-Overview.md listed RollingVwap as its own row in the Volume
table (count = 9, total = 72), but Home.md and README.md treat it as
a sub-variant of Vwap (count = 8, total = 71). Drop the standalone row
in Indicators-Overview and fold the rolling variant into the Vwap row.
Warmup-Periods.md keeps both constructors (Vwap::new() and
RollingVwap::new(n) have different warmup periods, which is the whole
purpose of that page) but adds a note explaining that the row count
exceeds the 71 canonical indicators by one.
The module doc-comment in bindings/node/src/lib.rs still pointed at
@wickra/wickra. The package is published as bare "wickra" — every
README, Quickstart and example already uses that name. Align the
inline doc with the published name.
The Quickstart-Node API-surface table claimed `warmupPeriod()` was
"not exposed on every multi-output class". After the B-series fixes
in `todo-detailed.md` and the R3/R8 pass on this branch, every Node
indicator class — single- and multi-output, scalar- and candle-input —
exposes `warmupPeriod()`. Updated the note accordingly.
The Quickstart-WASM page only mentioned `MACD` and `BollingerBands` as
multi-output indicators. After R3 (this branch) every candle-input
WASM class also exposes a structured `update`: `Stochastic`, `ADX`,
`Keltner`, `Donchian`, `Aroon` (plus `SuperTrend`, which has always
been there).
The "Multi-output indicators" section now lists every multi-output
shape in a table, and a short note at the end calls out the 0.1.5
parity: every candle-input indicator ships `update` / `batch` /
`reset` / `isReady` / `warmupPeriod`, so browser code doesn't need
to replay `batch` on each tick anymore.
Three indicator pages get a short follow-up paragraph that surfaces an
internal implementation detail the audit findings made user-visible:
- `Indicator-LinearRegression.md` gains a "Complexity" section explaining
the O(1) update (precomputed `Σx`, `Σxx`; incrementally slid `Σy`,
`Σxy` via the closed-form sliding identity), and the existing
"Reset" bullet mentions the additional running accumulators. The same
story applies to `LinRegSlope` and `LinRegAngle` (the page now links
to both rather than repeating the derivation three times).
- `Indicator-Sma.md` and `Indicator-BollingerBands.md` mention the
periodic reseed (`16 · period` updates) that caps floating-point
drift on long-running streams. Amortised cost is still O(1) and the
user-facing behaviour on benign inputs is unchanged.
No behavioural claim, no API claim, no example changes — just narrative
catching up with the implementation.
Versions bumped to 0.1.5 in every authoritative location:
- workspace `Cargo.toml` (`[workspace.package].version`, the
`wickra-core` path dependency pin).
- `bindings/python/pyproject.toml`.
- `bindings/node/package.json` (main + all six `optionalDependencies`
pins).
- All six per-platform `bindings/node/npm/<target>/package.json`
templates.
CHANGELOG: the accumulated `[Unreleased]` block is promoted to
`[0.1.5] - TBD` (date left for the user to set at tag time); the new
`[Unreleased]` header sits empty above it; the compare link table is
extended with `[0.1.5]: …compare/v0.1.4...v0.1.5` and the
`[Unreleased]` link is repointed to `…compare/v0.1.5...HEAD`.
Wiki refresh for 0.1.5 (R20 + Z2):
- `Home.md` version pin table updated; the Quickstart-Node hint replaces
the "spam filter holding back Windows" caveat with "0.1.5 is the
first release in which `npm install wickra` works end-to-end on
Windows" (npm Support released the name on 2026-05-22).
- `Quickstart-Node.md`'s Windows caveat is rewritten to explain the
history (`0.1.1`–`0.1.4` of `wickra-win32-x64-msvc` are burned) and
the resolution (0.1.5+ installs cleanly).
- `Quickstart-Rust.md` version mention bumped.
- `Warmup-Periods.md` note bumped + corrected: every Node and WASM
class — single- and multi-output — now exposes `warmupPeriod()` after
R3 (this branch), not only the single-output ones.
`release.yml` `publish_dir` no longer silently swallows a
second-attempt platform-package publish failure with a `::warning::`
and `return 0`. A real failure (after the existing 30s retry) now
emits an `::error::` and fails the job. The original mask is exactly
what allowed the `wickra-win32-x64-msvc@0.1.1–0.1.4` spam-filter
rejections to land four times in a row without anyone noticing (audit
finding R20). Failing loud means the next regression of this shape is
caught at the release run, not by a Windows user trying to
`require('wickra')`.
This commit does NOT push, tag, or trigger a release — the user
publishes the 0.1.5 tag themselves once the manual npm-republish
smoke test confirms `wickra-win32-x64-msvc@0.1.5` accepts publish on
the freshly-released name.
PSAR — the wiki page still claimed "Node streaming. Not exposed in the
Node binding." That was true for an early release but is no longer:
the Node binding has exposed `psar.update(high, low, close)` since the
B1 fix in todo-detailed.md, and the WASM binding now exposes the same
streaming surface (audit finding R3, this branch). The page lists all
three streaming + batch shapes and adds a paragraph on the new
`is_ready` convention (audit finding R6 — flips on the first non-None
SAR, not on the seed candle).
HistoricalVolatility — the "Non-positive prices" edge-case note still
described the old behaviour ("that return is treated as 0"). The new
behaviour skips the bad tick entirely (audit finding R13): the
indicator's state is left untouched, the previous valid value is
returned, and the next real tick re-anchors against the previous
*valid* price. Updated the note to describe the new behaviour and
explain why (silently treating bad ticks as "no movement" underreports
realised volatility).
R10 — `cross-library-bench` previously ran on every push and every PR
to `main`, adding 5–10 minutes of build + bench time per CI run with
no automated consumer of the artefact. It moves to a dedicated
workflow (`.github/workflows/bench.yml`) that fires nightly at 03:00
UTC and on-demand via `workflow_dispatch` (with optional `size` /
`iterations` inputs). The job in `ci.yml` is removed and a pointer
comment is left in its place so future readers find the new home.
R11 — `pyproject.toml`'s `requires-python = ">=3.9"` already allowed
Python 3.13 installs, but the classifier list stopped at 3.12 and CI
tested only 3.9 / 3.11 / 3.12. The Python CI matrix gains `"3.13"`
and the matching `Programming Language :: Python :: 3.13` classifier
is added so PyPI listings and version-search tooling reflect the
actually-tested range.
R15 — `bindings/node/package.json` and all six per-platform subpackage
templates (`npm/{darwin-arm64,darwin-x64,linux-arm64-gnu,linux-x64-gnu,
win32-arm64-msvc,win32-x64-msvc}/package.json`) plus the WASM
`release.yml` enrich step switch the `license` field from the npm
convention `SEE LICENSE IN LICENSE` to the SPDX identifier
`PolyForm-Noncommercial-1.0.0`. npm's license search and downstream
tooling can now surface the actual license.
R18 — every `engines.node` field is bumped from `>= 16` to `>= 18`.
The package's test script (`node --test __tests__/`) uses the built-in
`node --test` runner that landed in Node 18; advertising support for
Node 16 / 17 was a guarantee we never verified (CI tests on Node 18 /
20 only) and would have produced a confusing error on those versions.
R19 — README's benchmark section gains an explicit hardware /
software-version block and reframes the absolute µs values as a
relative-speedup snapshot rather than a universal contract. Also tells
the reader how to reproduce locally (`pip install -e
bindings/python[bench]` + `python -m benchmarks.compare_libraries`) and
where the CI artefact lives.
Side effects:
- R16 / R17 (built wheel under `bindings/python/dist/` and
`examples/node/node_modules/`) — verified that both are already
covered by `.gitignore` (`*.whl`, `dist/`, `**/node_modules/`) and
were never tracked by git. The local directories have been cleared;
no `.gitignore` change needed and no committed file removed.
R13 — `HistoricalVolatility::update` previously substituted `0.0` for
the log-return whenever `prev <= 0` or `input <= 0`. The log-return is
undefined there, and silently treating bad ticks as "no movement"
underreports realised volatility on broken data feeds. The fix skips
non-positive prices entirely: `self.last` is returned, state is left
untouched, and the next real tick re-anchors against the previous
*valid* `prev_price`. This matches how every other indicator handles
invalid inputs (SMA / EMA / ROC / Bollinger).
A new test `skips_non_positive_prices` proves the invariant: after a
warmed-up indicator, two consecutive bad ticks (`-5.0` and `0.0`) must
return the baseline value, and a subsequent real positive tick must
produce the same output as a control indicator that simply never saw
the bad ticks.
R14 — `Tick::new` previously returned `Error::InvalidCandle` for
negative volume. A tick is not a candle; downstream tick-stream
pipelines should be able to match on a semantically-correct error. A
new `Error::InvalidTick { message }` variant is added; the existing
test is updated to assert against it. Python's `map_err` is extended
to forward the new variant as `PyValueError`; the Node and WASM
bindings format via `Error::to_string()` and pick the new variant up
automatically without source changes.
`Sma` and `BollingerBands` both maintained their running `sum` (and
`sum_sq` for Bollinger) with a single-subtract incremental update. That
is correct in exact arithmetic, but in f64 the sequence `sum -= old;
sum += new` on long streams with alternating large/small magnitudes
can accumulate catastrophic-cancellation error. Bollinger's existing
`.max(0.0)` clamp on the computed variance was a band-aid for the same
root cause — the drift had already driven the running variance below
zero.
The fix: every `16 · period` finite updates, reseed `sum` (and `sum_sq`
for Bollinger) from the live window. Amortised cost stays at O(1) —
`O(period)` work amortised over `O(period)` updates — and the reseed
strategy is named after the constant `RECOMPUTE_EVERY` so the
intention is clear at the call site.
Behaviour is unchanged on inputs that did not drift to begin with
(every existing test still passes, including `batch_equals_streaming`
and the SMA proptest). Two new stress tests
(`long_stream_drift_stays_bounded` in each module) feed a
magnitude-alternating stream for `5 · RECOMPUTE_EVERY · period`
updates and assert the reported value tracks a fresh from-scratch
computation over the live window to within tight tolerance — these
would have failed without the reseed on Bollinger's `sum_sq`.
The misleading `sma.rs` comment that claimed drift was already
bounded by recomputing the sum after each pop is rewritten to
describe the actual reseed strategy (audit finding L2-Rust).
Follow-up to b340ecd — the doc comment on
`warmup_period_matches_first_some_for_every_parameter_set` had an
unbalanced inline-code span ("`Some``") that tripped
`clippy::doc_invalid_doc_attributes` (caught by `clippy -D warnings`
but not by `cargo build` or `cargo test`). Rephrased the sentence so
every backtick is paired. No code change, no test change.
Audit finding R12 claimed `Coppock::warmup_period()` was off by one
because it returns `max(roc_long, roc_short) + wma`, while
`Roc::warmup_period() = period + 1`. After tracing the actual emission
sequence the existing formula is correct: when both ROCs reach `Some`
at 0-based index L (the slower of `roc_long_period` and
`roc_short_period`), the WMA receives its first input there and emits
its `wma_period`-th value at 0-based index `L + wma_period − 1`. The
`warmup_period()` is the 1-based count of inputs needed before the
first `Some`, i.e. `L + wma_period`. R12 was a misread by both Sonnet
audit agents and the Opus verifier — none of them traced the actual
emission timeline.
This commit:
- Expands the doc comment on `warmup_period` with the precise emission
argument and a worked example for `Coppock::new(6, 4, 3)` (the
existing test) so a future reader cannot mis-derive the formula.
- Adds `warmup_period_matches_first_some_for_every_parameter_set`,
which asserts `out[warmup - 1].is_some()` for five parameter
combinations — including the audit's smoking gun `(4, 2, 3)`. The
audit's proposed `max + 1 + wma` formula would have predicted index
7 (the 8th input) for that combination; the real first `Some` lands
at index 6 (the 7th input), exactly what the current formula
reports.
No behaviour change — the audit was wrong and the test makes the
contract regression-proof.
`LinearRegression::fit` and `LinRegSlope::update` previously iterated the
full `period`-window on every tick to recompute `Σy` and `Σxy` from
scratch — O(period) per update, in violation of the `Indicator` trait's
O(1) contract. `LinRegAngle` inherits the cost transitively because it
delegates to `LinRegSlope`.
This commit slides the OLS state in closed form. The constant terms
(`Σx`, `Σxx`, the denominator `n·Σxx − (Σx)²`) were already precomputed
in `new`. The new running state is:
- `sum_y: f64` — running sum of the values currently in the window.
- `sum_xy: f64` — running Σ(x · y) where `x` is the position of each
value inside the trailing window (`0` for the oldest, `n−1` for the
newest).
On every push, when the window is already full the front value `y₀` is
popped and the indices of every remaining value shift down by 1; the
identity
new_Σxy = old_Σxy − old_Σy + y₀
closes the slide in O(1). The new value is then pushed at position `k`
(the current length before the push), contributing `k · new_value` to
`sum_xy` and `new_value` to `sum_y`. The output is the same TA-Lib OLS
formula evaluated against the incremental accumulators.
Behaviour is unchanged: same per-tick values, same warmup, same NaN
semantics. Two new tests compare the O(1) result bar-by-bar against a
fresh O(n) refit on a noisy ramp (sliding-phase dominated), a step
function (large pop/push deltas), and constants (tests floating-point
drift) — agreement is within `1e-9`.
`LinRegAngle` benefits automatically through its `LinRegSlope` field.
The fuzz suite previously covered only `Rsi(14)` and `Ema(20)` — 2 of
71 indicators, no OHLCV coverage at all. Audit finding R9 asked for
ATR/ADX/Stochastic/PSAR as a minimum; this commit goes further and
brings every indicator under fuzz.
- `indicator_update` (rewritten): drives every scalar-input indicator
through one streaming pass + one batch call per iteration. Covers
SMA, EMA, WMA, RSI, DEMA, TEMA, HMA, ROC, TRIX, SMMA, TRIMA, ZLEMA,
KAMA, T3, MOM, CMO, TSI, PMO, StochRSI, DPO, PPO, Coppock, StdDev,
UlcerIndex, HistoricalVolatility, LinearRegression, LinRegSlope,
LinRegAngle, VHF, ZScore, MACD, BollingerBands. A `drive` helper
marked `#[inline(never)]` keeps each indicator on its own panic
backtrace frame.
- `indicator_update_candle` (new): chunks the fuzz `f64` stream into
`[open, high, low, close, volume]` tuples, builds candles via
`Candle::new` (skipping ones that fail OHLCV validation — that path
is fuzz-tested separately), then drives every candle-input indicator
through streaming + batch. Covers ATR, NATR, TrueRange,
ChaikinVolatility, Keltner, Donchian, PSAR, SuperTrend,
ChandelierExit, ChandeKrollStop, ATRTrailingStop, ADX, Aroon,
AroonOscillator, Vortex, MassIndex, ChoppinessIndex, CCI, WilliamsR,
AwesomeOscillator, AcceleratorOscillator, UltimateOscillator,
BalanceOfPower, OBV, MFI, VWAP, RollingVWAP, VWMA, ADL, VPT, CMF,
ChaikinOscillator, ForceIndex, EaseOfMovement, TypicalPrice,
MedianPrice, WeightedClose, Stochastic.
- `fuzz/Cargo.toml` registers the new target; `fuzz/README.md`
describes both expanded targets.
- A `fuzz-smoke` CI job runs each of the five targets for 30 s on
every push and pull-request — enough to catch a regression in the
harness without slowing CI to a crawl. Long fuzz campaigns belong
on dedicated infrastructure with persistent corpora.
`Psar::is_ready` previously returned `self.initialised`, which flips to
`true` *after* the seed candle — but the seed candle itself returns
`None`. The contract every other indicator honours is
`is_ready() == true` ↔ "the most recent update produced (or could
produce) a real value". Streaming consumers writing
`if ind.is_ready() { use(ind.update(c)?) }` would hit an unexpected
`None` on the first post-seed update.
Fix: add a `has_emitted: bool` field that flips on the first
`Some(sar)` return; `is_ready` now reads that. New test
`is_ready_only_after_first_some_value` pins the contract.
While in the same file, `reset()` is corrected to restore the compute
fields (`prev_high`, `prev_low`, `sar`, `ep`) to `f64::NAN` sentinels
instead of `0.0` (Opus bonus finding). The fields are gated by
`initialised` today, so the `0.0` sentinel never leaked into output —
but a future refactor that read them pre-init would have silently
treated `0.0` as a real price. A `debug_assert!` at the read site makes
the invariant explicit and catches a re-introduction of the bug in
debug builds.
Bit-equivalence with the previous behaviour is preserved
(`reset_allows_clean_reuse` and `batch_equals_streaming` continue to
pass unchanged).
`UlcerIndex::update` previously scanned the full `period`-window every
tick via `prices.iter().fold(NEG_INFINITY, f64::max)`, breaking the
`Indicator` trait's O(1) contract. For long windows (e.g. period 50+ on
a live tick stream) this turned a constant-time update into an O(period)
one, and full-history batch replays into O(n · period).
The window of raw prices is replaced with a monotonically-decreasing
deque of `(index, price)` pairs. On every push, all back entries
`<= input` are popped (they can never be the trailing max again, since
they are dominated and at least as old). On every step, the front is
popped if its index is older than `count - period + 1`. The deque's
front is therefore always the trailing max in O(1). `count: u64` is the
1-based input counter that drives expiration; on `reset()` it returns
to zero alongside the deque and the drawdown state.
Behaviour is unchanged: same per-tick values, same warmup
(`2 * period - 1`), same non-finite-input semantics. A new test
`monotone_deque_matches_naive_max_on_adversarial_inputs` compares the
deque output bar-by-bar against an independent O(n) trailing-max scan on
inputs designed to hit every code path: strictly increasing (full tail
pops), strictly decreasing (head expirations only), constants (the
`<= input` pop rule keeps a single newest entry), and a sawtooth.
The doc comment on `warmup_period()` is also corrected (B-Opus-2): the
two windows overlap by one bar, so the formula is `2 * period - 1`, not
`2 * period`.
The rolling-window VWAP indicator (`wickra_core::RollingVwap`) was only
available in the Rust crate, even though the README's Volume-family
table already advertised "VWAP (cumulative + rolling)" as a cross-
language feature. Users on Python, Node or in the browser had to fall
back to the cumulative `VWAP` or re-implement the rolling variant
themselves.
This commit closes the gap end-to-end:
- Python: `wickra.RollingVWAP(period)` — same constructor / `update` /
`batch` / `reset` / `is_ready` / `warmup_period` surface as `VWAP`,
plus a `period` property and a typed `__repr__`. The `__init__.py`
re-exports it and `__all__` lists it; the `.pyi` stub matches.
- Node: `RollingVWAP(period)` — napi class with the same lifecycle,
exported from `index.js` and declared in `index.d.ts`.
- WASM: `RollingVWAP(period)` — wasm-bindgen class with the same
`Float64Array` I/O as `VWAP`.
Tests added:
- Python: `test_rolling_vwap_streaming_matches_batch` — exercises
`update == batch` plus the full lifecycle on the shared OHLC fixture.
- Node: `RollingVWAP` row in the `candleScalar` parity table — covered
by the generic streaming-vs-batch + lifecycle harness.
- WASM: dedicated `wasm-bindgen-test` mirrors the Python test.
The wiki page `Indicator-Vwap.md` drops the "Rust-only" caveat and
gains Python / Node / WASM examples.
Twelve WASM classes previously exposed only `batch()` (and not even
`reset()` for ten of them): ADX, WilliamsR, CCI, MFI, PSAR, Keltner,
Donchian, VWAP, AwesomeOscillator, Aroon, Stochastic, OBV. Browser
consumers wanting per-tick updates had to replay `batch()` on every new
candle — the opposite of the library's streaming-first promise.
Each class now exposes:
- `update(...)` — per-tick streaming update with the same column inputs
as `batch()`. Single-output indicators return `Option<f64>`. Multi-
output indicators (ADX, Keltner, Donchian, Aroon, Stochastic) return a
named JS object (`{ plusDi, minusDi, adx }`, `{ upper, middle, lower }`,
`{ up, down }`, `{ k, d }`) once warm, or `null` during warmup. This
matches the existing `SuperTrend` convention so JS code can treat all
multi-output WASM indicators uniformly.
- `reset()`, `isReady()`, `warmupPeriod()` — bring the lifecycle API to
full parity with Python and Node.
`WasmKama` also gains the previously missing `warmupPeriod()` (R8). A
single new `wasm-bindgen-test` exercises every newly wired class against
a deterministic 40-bar synthetic OHLCV stream, asserting that
streaming `update` matches `batch` value-by-value and that the lifecycle
contract behaves the same as the core indicator.
Bumps the Python binding from pyo3 0.22 / numpy 0.22 to 0.28 / 0.28,
which resolves RUSTSEC-2025-0020 — a buffer overflow in
`PyString::from_object` that affected every published Python wheel.
Migration:
- `into_pyarray_bound(py)` → `into_pyarray(py)` (numpy 0.23 dropped the
`_bound` transitional suffix; the method now returns `Bound<'py, _>`
directly).
- `downcast::<PyDict>` → `cast::<PyDict>` (pyo3 renamed the method on
`PyAnyMethods`).
- Every `#[pyclass]` declares `skip_from_py_object` to opt out of the
now-deprecated automatic `FromPyObject` derive for `Clone` types.
Indicators are stateful — silently extracting them by value-clone is
never the intended FFI semantics.
- Workspace clippy gains `unused_self = "allow"` on the python crate
only: Python's `__repr__` protocol forces `&self` even for parameter-
less indicators where the body does not read state.
- `map_err` arms collapsed into a single `PyValueError` arm
(clippy::match_same_arms).
`deny.toml` no longer suppresses RUSTSEC-2025-0020; `cargo deny check`
is green on advisories, bans, licenses and sources without exceptions.
* examples/README.md — replace the single-row WASM section with a build
block (one-time `wasm-pack` build), a serve note, and the full
five-row table (`index`, `backtest`, `live_trading`, `multi_timeframe`,
`parallel_assets`).
* examples/wasm/README.md — new dedicated index for the WASM demos
with the build and serve commands and a description of every file
including the module worker companion.
* CHANGELOG.md `[Unreleased]` gains three bullets: the Python and Node
`fetch_btcusdt` siblings; the four new WASM browser demos; and the
three new wiki pages from Z6 (TA-Lib-Migration, Cookbook, FAQ).
Close the final "parallel assets" cell of the cross-language matrix for
WASM.
* examples/wasm/parallel_assets.html — generates a synthetic
`(assets, bars)` panel deterministically (the LCG matches the Node
and Rust siblings so timings are directly comparable), runs the
serial baseline on the main thread, then dispatches the same workload
to a pool of module Workers and reports the speedup. The render is
three cards (serial / parallel / speedup) plus a sanity-check line
asserting per-asset agreement between the two paths.
* examples/wasm/parallel_worker.js — companion module worker that
loads its own copy of the WebAssembly module via `init()` and
processes whichever slice of the panel its parent dispatches.
Modern browsers ship module-worker support (`new Worker(url, {
type: "module" })`) which lets every worker do `import init, { SMA, RSI
} from "../../bindings/wasm/pkg/wickra_wasm.js"` without bundler
glue. The inline page module and the worker module both syntax-check
cleanly under `node --check`.
Close the "multi-timeframe" cell of the cross-language matrix for WASM.
* examples/wasm/multi_timeframe.html — fetches the bundled 1-minute
BTCUSDT CSV (or any 1-minute OHLCV CSV), rolls it up in-page to 5m,
15m, 1h, 4h and 1d buckets, and prints RSI(14), MACD(12,26,9)
histogram and ADX(14) per timeframe via the WebAssembly bindings.
Same inline-bucket aggregation as the Node sibling, same indicator
set as the Python and Rust siblings — the Rust version uses
`wickra-data::Resampler` directly which is currently a Rust-only API.
The render is a single table (one row per timeframe) so the cross-
language outputs sit side-by-side cleanly. The inline module script
syntax-checks cleanly under `node --check`.
Close the "live trading" cell of the cross-language matrix for WASM.
* examples/wasm/live_trading.html — opens a native browser `WebSocket`
to Binance's public kline stream, feeds every close through RSI(14),
MACD(12,26,9) and Bollinger(20, 2.0) via the WebAssembly bindings, and
flags BUY/SELL candidates when all three indicators agree. Mirrors
the Node and Python live-trading examples in indicator set, signal
logic and symbol/interval validation — the symbol is checked against
`^[A-Za-z0-9]+$` before being spliced into the stream URL, the
interval against the public-API allow-list.
The UI shows live close / RSI / MACD-histogram / Bollinger-band cards,
plus a scrolling log of the last 200 ticks with signal rows highlighted.
Browser-native `WebSocket` means no library dependency. Build the WASM
module once (`wasm-pack build bindings/wasm --target web --release
--features panic-hook`), serve the repository root and open
`examples/wasm/live_trading.html`.
The WASM example set had only `index.html` (the streaming canvas demo);
the "backtest" cell of the cross-language matrix was empty. Close it.
* examples/wasm/backtest.html — loads the wasm-pack `--target web`
bundle, fetches an OHLCV CSV from the same `examples/data/` directory
the other languages use (default: `btcusdt-1d.csv`), parses it
in-page and streams every candle through SMA, EMA, RSI, MACD,
Bollinger, ATR, ADX and OBV via the WebAssembly bindings. Renders a
summary table with mean / min / max / last per series — mirrors the
Rust, Python and Node backtest examples both in indicator set and in
output shape.
Build the WASM module once (`wasm-pack build bindings/wasm --target web
--release --features panic-hook`), then serve the repository root and
open `examples/wasm/backtest.html`. The inline module script
syntax-checks cleanly under `node --check` on the extracted body.
Node had no sibling for the Rust and Python `fetch_btcusdt`
data-generators — adding it closes the "fetch (data-gen)" cell for the
last remaining row of the cross-language example matrix where the
pattern makes sense.
* examples/node/fetch_btcusdt.js — uses Node 18+'s built-in global
`fetch` (no npm dependencies); same pagination logic as the Rust and
Python siblings (paginate backwards via `endTime`, drop the
in-progress bucket, sort and trim to the configured target). Applies
the same OHLC validity check the Rust `Candle::new` constructor
enforces so a malformed kline is skipped rather than written.
* JavaScript's `String(v)` already gives the shortest round-trip
representation and strips the `.0` suffix for whole-number floats, so
the CSV output is byte-for-byte identical to what the Rust and
Python fetchers produce on the same Binance snapshot. Verified by
running it and `git diff`-ing against the checked-in dataset: every
row older than the run is unchanged; only the most recent ~24 hours
drift because the market kept moving.
examples/README.md gains the new row.
Python had no sibling for the Rust `fetch_btcusdt` data-generator —
adding it closes the "fetch (data-gen)" cell for Python and lets users
without a Rust toolchain regenerate the bundled BTCUSDT datasets.
* examples/python/fetch_btcusdt.py — uses only the standard library
(urllib.request + json + csv); same pagination strategy as the Rust
version (paginate backwards via `endTime`, drop the in-progress
bucket, sort and trim to the configured target). Applies the same
OHLC validity check the Rust `Candle::new` constructor enforces
(finite fields, high >= low/open/close, low <= open/close,
volume >= 0) so a malformed kline is skipped rather than written.
* Number formatting matches Rust's `f64` Display: shortest round-trip,
no trailing `.0` for whole-number floats. Verified by running the
script and `git diff`-ing against the checked-in dataset: every row
older than the run is byte-identical to Rust's output; the diff only
shows the most recent ~24 hours where Binance has produced fresh
candles since the original snapshot.
examples/README.md gains the new row.
The README and Streaming-vs-Batch benchmark tables were a stale snapshot
("5 000-bar series", numbers from an older machine). Re-run
`python -m benchmarks.compare_libraries` on the current hardware against
the same peer set (finta + talipp; TA-Lib and pandas-ta stay excluded on
Windows) and replace the tables with the fresh numbers.
The new run uses the script's current defaults: a 20 000-bar batch series
and a 5 000-bar seed + 15 000-bar live streaming workload — both more
representative of real backtests than the previous 5 000 / 2 000-bar
sizes. Wickra still wins every batch row outright (3.5× to 1 244× faster
than the nearest peer) and the streaming RSI is ~13.8× faster than
talipp's incremental implementation.
Three content gaps in the wiki: there was no migration story for users
porting from TA-Lib, no strategy cookbook, and no FAQ. Add all three as
self-contained pages and link them from Home.md's "Wiki contents".
* docs/wiki/TA-Lib-Migration.md — full one-to-one mapping table from
every common talib.X(...) call to the equivalent Wickra expression,
plus a "what Wickra has that TA-Lib does not" / "what TA-Lib has that
Wickra does not (yet)" delta.
* docs/wiki/Cookbook.md — seven concrete strategy recipes (RSI mean
reversion, MACD histogram crossover, Bollinger breakout, ADX-gated
trend, multi-timeframe confirmation, SuperTrend trailing stop,
Chain<EMA, RSI>) with Rust or Python snippets.
* docs/wiki/FAQ.md — common questions on warmup, NaN handling, thread
safety, installation, performance and comparing Wickra to TA-Lib /
pandas-ta / talipp / finta.
Also extend the [Unreleased] CHANGELOG entry that records the
examples/<lang>/ restructure with the wiki additions; Home.md gains
three new bullets under "Wiki contents".
Python's parallel_assets.py demoed GIL-release multi-core throughput;
Rust and Node both lacked a sibling that shows their own native
parallelism. Close the gap with two real, runnable examples.
* examples/rust/src/bin/parallel_assets.rs — synthesises an (assets,
bars) panel with a deterministic per-asset LCG, runs a serial baseline,
then `Sma::batch_parallel` / `Rsi::batch_parallel` via rayon, asserts
the two outputs are element-wise identical and prints the speedup.
Toggle indicator with `--indicator sma|rsi`.
* examples/node/parallel_assets.js — same shape, but the parallel run is
a `worker_threads` pool that re-loads the native binding in each
worker. Each worker computes the last non-null indicator value for its
slice; the main thread aggregates and verifies serial == parallel
per asset.
Both examples report timings and the serial-vs-parallel sanity check
passes. Defaults (200 × 5000) keep the example fast on dev hardware;
larger `--assets`/`--bars` is where the speedup numbers move (Node's
worker spawn cost dominates the smallest sizes, which is honest and
educational).
examples/README.md gains the two new rows.
Python's examples/python/multi_timeframe.py had no Rust or Node sibling.
Add both — the Rust version uses wickra-data's `Resampler` /
`resample_all` (the canonical path; no manual roll-up), the Node version
mirrors the Python one's inline aggregation because wickra-data's
resampler is currently Rust-only.
* examples/rust/src/bin/multi_timeframe.rs — reads the bundled 1m CSV via
`CandleReader`, resamples to 5m / 15m / 1h / 4h / 1d via `resample_all`,
prints last RSI(14), MACD(12,26,9) histogram and ADX(14) per timeframe.
* examples/node/multi_timeframe.js — same outputs from a hand-rolled
bucket aggregator; reuses the new examples/data/ default path.
* examples/README.md gains the new rows.
Run side by side: the Rust and Node summaries are bit-identical at every
timeframe (50000 / 10000 / 3334 / 834 / 209 / 35 bars; same RSI, MACD
histogram and ADX to two decimals) — confirming both the Rust resampler
and the inline Node aggregator produce the same OHLC buckets.
Python and Rust both lacked a standalone "streaming indicators" example
that mirrors examples/node/streaming.js — the quickstart docs cover the
pattern, but a runnable file makes the parity visible across all four
languages.
* examples/python/streaming.py — argparse-driven synthetic streaming demo
feeding SMA(20) / EMA(20) / RSI(14) / MACD(12,26,9), tagging BUY?/SELL?
candidates when RSI extremes and MACD-histogram direction agree.
* examples/rust/src/bin/streaming.rs — same demo as a wickra-examples
binary, reusing the seeded LCG so its first 40 rows are bit-identical
to the Python (and Node) sibling — a strong cross-language consistency
signal verified by running both side by side.
* examples/README.md gains a `streaming` row in the Rust and Python tables.
Finish the per-language `examples/<lang>/` restructure by relocating the
WASM browser demo from bindings/wasm/examples/ to examples/wasm/.
* `examples/wasm/index.html` is the moved file; its WASM module import
becomes `../../bindings/wasm/pkg/wickra_wasm.js` so the demo still loads
the wasm-pack output without copying it.
* bindings/wasm/README.md, Quickstart-WASM.md, examples/README.md and the
root README "Languages" + project-layout block all point at the new
path. The serve command in the docs now says "serve the repository root
and open examples/wasm/index.html".
`bindings/wasm/examples/` is empty after the move; the now-empty
directory is removed.
Continue the per-language `examples/<lang>/` restructure: move the three
Node example files (streaming.js, backtest.js, live_trading.js) out of
bindings/node/examples/ and into a top-level examples/node/ directory.
* `examples/node/package.json` is a `private` package that pulls the
native binding via `file:../../bindings/node` and lists `ws` as a
dev-dependency for the live-trading example. `require('..')` in each
file becomes `require('wickra')` — exactly what a downstream user would
write — and the file-header run instructions are updated to the new
two-step workflow (`npm install` in bindings/node, then in
examples/node).
* `backtest.js`'s default-CSV path becomes the much shorter
`__dirname/../data/btcusdt-1d.csv` from the new location.
* `bindings/node/package.json` drops the now-unused `ws` devDependency.
* `.gitignore` is broadened from `bindings/node/node_modules/` to
`**/node_modules/` so the new `examples/node/node_modules/` directory is
not tracked.
* The README "Languages" table, project-layout block and
`examples/README.md` Node section are updated for the new paths and run
commands.
Verified by running `node backtest.js` (3200 BTCUSDT daily bars, matching
output), `node streaming.js`, and `node --check live_trading.js` from the
new location.
The three Rust examples (backtest, fetch_btcusdt, live_binance) used to
live each in their own crate's examples/ dir, splitting the example set
across crates and burying it inside the source tree. Move them into a new
workspace member crate at `examples/rust/` (package `wickra-examples`,
`publish = false`) so all language examples sit under one top-level
`examples/<lang>/` tree.
* `examples/rust/Cargo.toml` declares the per-binary deps (wickra,
wickra-data with the `live-binance` feature always on, serde_json, tokio
for the macro and current-thread runtime).
* `examples/rust/src/bin/{backtest,fetch_btcusdt,live_binance}.rs` are the
three migrated binaries; their doc-comments and the fetch_btcusdt output
path are updated for the new location and run command
(`cargo run -p wickra-examples --bin <name>`).
* Workspace `Cargo.toml` lists the new member; the now-empty
`[dev-dependencies]` extras (`wickra`, `tokio` in wickra-data and
`serde_json` in wickra) that existed only for these examples are dropped.
* The `[[example]] live_binance` table is removed from wickra-data's
manifest since the file moved out.
* README "Languages" + project-layout, examples/README.md, Quickstart-Rust
and Data-Layer are pointed at the new paths and commands.
`cargo build -p wickra-examples` and `cargo run --release -p wickra-examples
--bin backtest -- examples/data/btcusdt-1d.csv` both succeed; the rest of
the workspace (core, data, wickra) builds, clippies (`--all-targets -D
warnings`) and tests (508 core + 28 data + 1 integration + 74+3+1
doctests) all stay green.
The seven BTCUSDT OHLCV datasets used to live under
crates/wickra/examples/data/, which buried them inside a Rust crate even
though the Node backtest example and the upcoming Rust/Node/WASM example
restructure need to reach them too. Move them to the workspace-level
examples/data/ so every language's examples can resolve the same path.
The bench (crates/wickra/benches/indicators.rs), the example_data
integration test, fetch_btcusdt.rs and the Node backtest example all take
the new ../../examples/data/ path; Data-Layer.md, examples/README.md and
the CHANGELOG entry are updated to match. No data file content changes.
The top-level examples/ directory held only python/, which made the
examples look Python-only even though Rust, Node and WASM all ship their
own. Add examples/README.md: a single index of every runnable example
across Rust, Python, Node and WASM, each with its run command, plus a note
on the bundled BTCUSDT datasets.
Point the README "Languages" table at the Node backtest example and link
the new index from both the table and the project-layout section.
Mirror examples/python/live_trading.py for the Node binding: connect to the
public Binance kline WebSocket, stream close prices through RSI / MACD /
Bollinger Bands, and print BUY/SELL candidate signals when all three agree.
The symbol and interval are validated before being spliced into the stream
URL, and non-kline frames (acks, heartbeats) are skipped.
Uses the standard `ws` package, added as a devDependency so it installs
with `npm install` for anyone running the examples but never reaches a
consumer of the published package.
The Node binding shipped only one example (a synthetic streaming demo),
while Python and Rust both have a CSV backtest. Add the Node counterpart of
examples/python/backtest.py and crates/wickra/examples/backtest.rs: it reads
an OHLCV CSV, streams every candle through a basket of indicators (SMA, EMA,
RSI, MACD, Bollinger Bands, ATR, ADX, OBV) via the O(1) update call, and
prints a per-series summary.
With no argument it runs against the bundled BTCUSDT daily dataset, so it is
runnable out of the box; pass a path to use any other OHLCV CSV.
The Python binding README still advertised "63 indicators across four
families" with the pre-restructure five-group taxonomy, missing the eight
indicators added since. Update it to "71 indicators across eight families"
with the catalogue grouped to match the main README.
The Node binding README referred to the package as @wickra/wickra in its
title, install command and import example; the published package is named
wickra (per bindings/node/package.json). Correct all three.
Add the still-unreleased Z1/Z2 work to the [Unreleased] section: the seven
real-BTCUSDT example datasets plus the fetch_btcusdt example, the
Timeframe::minutes/hours/days constructors, and the switch of the indicator
benchmarks from a synthetic series to the checked-in BTCUSDT dataset.
Timeframe gained new/millis/seconds/one_minute_ms; add minutes, hours and
days alongside them. Each builds on seconds (minutes(5) -> a 300-second
bucket), consistent with Timeframe::seconds, and guards the multiplication
with checked_mul so an oversized n yields Error::InvalidTimeframe instead
of an overflow panic. A non-positive n is rejected by Timeframe::new.
Each method carries a runnable doctest, and unit tests cover the known
bucket sizes, non-positive rejection and overflow rejection.
Add seven OHLCV datasets under crates/wickra/examples/data/, one per
timeframe (1m/5m/15m/1h/12h/1d/1month), holding real BTCUSDT spot klines
fetched from the Binance REST API. The new fetch_btcusdt example
regenerates them: it paginates the klines endpoint through the system
curl, parses with serde_json, validates every candle via Candle::new and
keeps only fully closed buckets.
The indicator benchmarks now run against the 1m dataset instead of a
synthetic series, and a new example_data integration test checks that
every file parses and carries evenly spaced, monotonic timestamps.
The monthly file is named btcusdt-1month.csv rather than btcusdt-1M.csv
so it does not collide with btcusdt-1m.csv on case-insensitive
filesystems (Windows, default macOS).
The original taxonomy was four classical families plus a statistics group,
with the F1-F12 expansion slotted in as sub-categories. This regroups the
whole 71-indicator catalogue into eight top-level families, each with at
least five members:
Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9),
Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5),
Volume (9), Price Statistics (7).
- Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71
indicator pages moved with `git mv`. Every internal cross-link is
normalised to `../<family>/Indicator-X.md`, each page's `Family` field is
set to its new family, and two pre-existing `../Indicator-Chaining.md`
links (should have been `../../`) are corrected. A link check confirms
every relative wiki link resolves.
- Indicators-Overview.md fully rewritten around the eight families;
Home.md indicator reference and the README family table follow suit.
- Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the
46-indicator expansion (25 -> 71) and the eight-family taxonomy.
- Tests: Node indicators.test.js and Python test_new_indicators.py cover
all eight new indicators (Node 91/91, Python 117/117 green).
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests,
25 data tests and 74 doctests green.
Second half of the eight indicators that fill out the new family taxonomy.
- Rust core: true_range.rs (TrueRange — the raw single-bar volatility ATR
averages), chaikin_volatility.rs (ChaikinVolatility — rate of change of a
smoothed high-low spread), z_score.rs (ZScore — price normalised against
its rolling mean and standard deviation) and linreg_angle.rs (LinRegAngle
— the rolling regression slope as a degree angle). Each with a full
Indicator impl, runnable doctest and reference / property / warmup /
reset / batch==streaming tests.
- Python / Node / WASM: classes wired through all three bindings (ZScore
and LinRegAngle ride the scalar macros where possible) plus .pyi stubs
and __init__.py / __all__ entries.
- Wiki: four new Indicator-*.md pages.
The eight-family taxonomy restructure (Overview / Home / README / folder
layout) lands next in F13c.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests,
25 data tests and 74 doctests green.
First half of the eight indicators that fill out the new family taxonomy.
- Rust core: accelerator_oscillator.rs (AcceleratorOscillator — AO minus a
short SMA of itself), balance_of_power.rs (BalanceOfPower — per-bar
(close-open)/(high-low)), choppiness_index.rs (ChoppinessIndex — summed
true range over the high-low span, log-scaled) and
vertical_horizontal_filter.rs (VerticalHorizontalFilter — net move over
total move). Each with a full Indicator impl, runnable doctest and
reference / property / warmup / reset / batch==streaming tests.
- Python / Node / WASM: classes wired through all three bindings
(BalanceOfPower carries an explicit open column; VHF rides the scalar
macros) plus .pyi stubs and __init__.py / __all__ entries.
- Wiki: four new Indicator-*.md pages.
The eight-family taxonomy restructure (Overview / Home / README / folder
layout) lands in F13c once F13b's four indicators are in.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 481 core tests,
25 data tests and 70 doctests green.
The Python binding README still advertised the original 25 indicators and
the four-family list. Bring it in line with the root README: 63 indicators
across the four classical families plus the statistics group.
Finalises the F1-F12 indicator expansion (25 -> 63 indicators).
- Python `wickra/__init__.py`: import and re-export all 63 indicators,
grouped by family, with a matching `__all__`. The package previously
exposed only the original 25 even though the compiled module and the
`.pyi` stubs already carried the rest.
- Docs: `Home.md` and `README.md` indicator counts and family tables
updated to 63; `Indicators-Overview.md` already restructured per family
in F10-F12; `Warmup-Periods.md` gains all 38 new indicators across the
single- and multi-output tables (and the stale two-arg `Psar::new`
example is corrected to three args); `CHANGELOG.md` `[Unreleased]` lists
every new indicator by family.
- Tests: `bindings/node/__tests__/indicators.test.js` covers all 63
indicators (streaming==batch plus four new reference-value checks),
80/80 green; new `bindings/python/tests/test_new_indicators.py` covers
the 38 additions (streaming==batch, shapes, reference values,
lifecycle), Python suite 105/105 green.
- `bindings/node/index.js` regenerated by `napi build`.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 454 core tests,
25 data tests, 66 doctests, 80 Node tests and 105 Python tests green;
`cargo check -p wickra-wasm --tests` green.
- Rust core: cmf.rs (Chaikin Money Flow — summed money-flow volume over
summed volume, bounded to [-1, +1]), chaikin_oscillator.rs (Chaikin
Oscillator — the MACD of the ADL, EMA(ADL, fast) - EMA(ADL, slow)),
force_index.rs (Elder's Force Index — EMA of price change scaled by
volume), ease_of_movement.rs (Arms' Ease of Movement — SMA of distance
travelled per unit of volume). Each with a full Indicator impl,
runnable doctest and reference / property / warmup / reset /
batch==streaming tests.
- Python: PyChaikinMoneyFlow / PyChaikinOscillator / PyForceIndex /
PyEaseOfMovement PyO3 classes + module registration + .pyi stubs.
- Node: explicit ChaikinMoneyFlowNode / ChaikinOscillatorNode /
ForceIndexNode / EaseOfMovementNode; index.d.ts and index.js updated.
- WASM: WasmChaikinMoneyFlow / WasmChaikinOscillator / WasmForceIndex /
WasmEaseOfMovement.
- Wiki: Indicator-ChaikinMoneyFlow/ChaikinOscillator/ForceIndex/
EaseOfMovement.md plus a new "Oscillators" sub-table in
Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 402 core tests,
25 data tests and 57 doctests green.
Completes the F9 family (Cumulative volume) end to end:
- Rust core: adl.rs (Accumulation/Distribution Line — cumulative
range-weighted volume) and vpt.rs (Volume-Price Trend — cumulative
volume scaled by percentage price change). Each with a full Indicator
impl, runnable doctest and reference / cumulative-property / warmup /
reset / batch==streaming tests.
- Python: PyAdl / PyVolumePriceTrend PyO3 classes + module registration
+ .pyi stubs (no parameters, like OBV/VWAP).
- Node: explicit AdlNode and VolumePriceTrendNode; index.d.ts and
index.js updated.
- WASM: WasmAdl and WasmVolumePriceTrend.
- Wiki: Indicator-Adl.md and Indicator-VolumePriceTrend.md plus rows in
Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 373 core tests,
25 data tests and 53 doctests green.
Completes the F7 family (Volatility) end to end:
- Rust core: natr.rs (ATR as a percentage of close), std_dev.rs
(rolling population standard deviation), ulcer_index.rs (RMS of
trailing-high drawdowns — downside-only risk), historical_volatility.rs
(annualised sample stddev of log returns). Each with a full Indicator
impl, runnable doctest and reference / constant-series / warmup /
reset / batch==streaming tests.
- Python: PyNatr / PyStdDev / PyUlcerIndex / PyHistoricalVolatility
PyO3 classes + module registration + .pyi stubs.
- Node: StdDevNode / UlcerIndexNode via the scalar macro, explicit
NatrNode and HistoricalVolatilityNode; index.d.ts and index.js updated.
- WASM: WasmStdDev / WasmUlcerIndex / WasmHistoricalVolatility via the
scalar macro, explicit WasmNatr.
- Wiki: Indicator-Natr/StdDev/UlcerIndex/HistoricalVolatility.md plus
rows in Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 350 core tests,
25 data tests and 49 doctests green.
Completes the F5 family (Price oscillators) end to end:
- Rust core: ppo.rs (Percentage Price Oscillator — MACD as a percentage
of the slow EMA), dpo.rs (Detrended Price Oscillator — shifted price
minus its SMA), coppock.rs (Coppock Curve — WMA of two summed ROCs).
Each with a full Indicator impl, runnable doctest and reference /
constant-series / warmup / reset / batch==streaming / non-finite tests.
- Python: PyPpo / PyDpo / PyCoppock PyO3 classes + module registration
+ .pyi stubs (defaults PPO=(12,26), DPO=20, Coppock=(14,11,10)).
- Node: DpoNode via the scalar macro, explicit PpoNode and CoppockNode;
index.d.ts and index.js updated.
- WASM: WasmDpo / WasmPpo / WasmCoppock via the scalar macro.
- Wiki: Indicator-Ppo/Dpo/Coppock.md plus rows in Indicators-Overview.md
and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 300 core tests,
25 data tests and 42 doctests green.
Completes the F4 family (Stochastic oscillators) end to end:
- Rust core: stoch_rsi.rs (Stochastic Oscillator applied to the RSI
series, bounded [0,100]) and ultimate_oscillator.rs (Larry Williams'
weighted three-timeframe buying-pressure oscillator). Each with a full
Indicator impl, runnable doctest and reference / saturation / bounds /
warmup / reset / batch==streaming tests.
- Python: PyStochRsi / PyUltimateOscillator PyO3 classes + module
registration + .pyi stubs (defaults StochRSI=(14,14), UO=(7,14,28)).
- Node: explicit StochRsiNode and UltimateOscillatorNode; index.d.ts
and index.js updated.
- WASM: WasmStochRsi via the scalar macro, explicit
WasmUltimateOscillator.
- Wiki: Indicator-StochRsi.md and Indicator-UltimateOscillator.md plus
rows in Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 278 core tests,
25 data tests and 39 doctests green.
Completes the F2 family (Advanced MAs) end to end:
- Rust core: zlema.rs (Zero-Lag EMA over the de-lagged series
2·price − price[lag]), t3.rs (Tillson's six-EMA cascade with the
volume-factor polynomial), vwma.rs (volume-weighted rolling mean with
a zero-volume fallback to the unweighted mean). Each with a full
Indicator impl, runnable doctest and reference-value / warmup /
reset / batch==streaming / non-finite tests.
- Python: PyZlema / PyT3 / PyVwma PyO3 classes + module registration
+ .pyi stubs (T3 defaults v=0.7).
- Node: ZlemaNode via the scalar macro, explicit T3Node and VwmaNode
classes; index.d.ts and index.js updated.
- WASM: WasmZlema / WasmT3 via the scalar macro, explicit WasmVwma.
- Wiki: Indicator-Zlema.md, Indicator-T3.md, Indicator-Vwma.md plus
rows in Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 232 core tests,
25 data tests and 33 doctests green.
Completes the F1 family (Simple & Weighted MAs). The Rust core for both
SMMA (Wilder's RMA) and TRIMA (triangular MA) already landed; this adds
the remaining Definition-of-Done steps:
- Python: PySmma / PyTrima PyO3 classes + module registration + .pyi stubs.
- Node: SmmaNode / TrimaNode via the scalar-indicator macro; index.d.ts
and index.js updated for the two new classes.
- WASM: WasmSmma / WasmTrima via the scalar-indicator macro.
- Wiki: Indicator-Smma.md and Indicator-Trima.md (full pages) plus rows
in Indicators-Overview.md and entries in Home.md.
cargo fmt + clippy (core/wickra/data/wasm/node) clean; 208 core tests,
25 data tests and 31 doctests green.
First step of the indicator-family expansion (see the F section of
todo-detailed.md). Family F1 — Simple & Weighted MAs — gains two
members alongside the existing Sma/Ema/Wma:
- Smma — Wilder's smoothed moving average (RMA): SMA-seeded, then the
(prev*(n-1)+x)/n recurrence. The average underlying RSI and ATR.
- Trima — triangular moving average: two stacked SMAs (n1/n2 split by
parity) that triangular-weight the window. Genuine stacking — the
outer SMA consumes the inner SMA's output.
Both implement the full Indicator trait with reference-value, warmup,
reset, batch==streaming and non-finite-input tests, a runnable doctest,
and are re-exported from the crate root. 208 core tests + 30 doctests
pass; clippy and fmt clean.
The WASM binding's test module (added in B6) used `.ok().expect(...)`
on the Result-returning constructors. clippy's ok_expect lint rejects
this under the workspace's `-D warnings`, and the CI rust job lints
wickra-wasm with --all-targets — so the branch would fail CI.
Replace all seven `.ok().expect(...)` with `.expect(...)` directly;
JsError implements Debug, so this compiles and gives a better panic
message. clippy and fmt are now clean for wickra-wasm.
python-wheels built only glibc Linux (x86_64/aarch64), macOS, and
Windows x64 — Alpine/musl users and Windows arm64 had no wheel.
Add musllinux_1_2 wheels for x86_64 and aarch64 Linux and an
aarch64 wheel on the windows-11-arm runner. The upload artifact name
now includes the manylinux value so the glibc and musl builds of the
same architecture do not collide. The Node release matrix already
covers linux-arm64 and win32-arm64 (added in B11); Node musl is left
out, as B11 documented, because it needs a cross/container setup.
The repository had no fuzzing setup despite several natural targets —
the CSV parser, the Binance envelope deserializer, and the stateful
indicator/aggregator update paths.
Add a fuzz/ cargo-fuzz crate (detached from the workspace via its own
[workspace] table and the parent's exclude) with four targets:
- csv_reader — CandleReader over arbitrary bytes
- binance_envelope — RawWsEnvelope deserialization from arbitrary strings
- indicator_update — RSI/EMA streaming + batch over arbitrary f64 series
- tick_aggregator — TickAggregator over arbitrary tick triples
Each target asserts the no-panic contract: malformed input must surface
as an Err. fuzz/README.md documents running them (nightly + cargo-fuzz).
Only two doctests existed in wickra-core; none of the 25 indicator
types carried a runnable rustdoc example.
Add an "# Example" doctest to every public indicator type (all 26,
including RollingVwap): construct the indicator and stream 80 inputs
through update, asserting a value is produced. The candle-input
indicators build valid OHLCV candles inline. cargo test --doc
-p wickra-core now runs 28 doctests, all passing; fmt and clippy clean.
CI had no coverage measurement, and although .gitignore listed coverage
artefacts nothing produced them.
Add a `coverage` job that runs cargo-llvm-cov over the three pure-Rust
crates (with wickra-data's live-binance feature so the Binance parser
tests count), emits lcov, and uploads to Codecov. The Codecov upload
uses fail_ci_if_error: false so a Codecov outage cannot break CI. Both
new actions (taiki-e/install-action, codecov/codecov-action) are
SHA-pinned. Add a coverage badge to the README.
The wiki had quickstarts for Python, Rust, and Node but none for the
WebAssembly binding, and the wickra-data crate (CSV reader, tick
aggregator, resampler, Binance feed) was not documented anywhere.
- Quickstart-WASM.md: install via npm, building with wasm-pack, and
streaming/batch/multi-output usage in a browser or bundler.
- Data-Layer.md: the wickra-data crate — CandleReader, TickAggregator
(including the opt-in gap fill), Resampler/resample_all, and the
feature-gated Binance live feed.
- Home.md links both from the wiki contents list.
RollingVwap is a separate public type (pub struct RollingVwap in
vwap.rs) and Indicator-Vwap.md already documents it in a full
"## RollingVwap (finite window)" section, but it was not directly
reachable: the Overview row added in E12 pointed at a #rollingvwap
anchor that does not exist.
Fix the Overview link to the real #rollingvwap-finite-window anchor and
add a jump-to note at the top of Indicator-Vwap.md so both public types
are reachable in one click.
Indicators-Overview.md only had a "Deep dive" column in the Trend
tables; the Momentum, Volatility and Volume tables left readers without
a path to the per-indicator pages.
Add a "Deep dive" column to all eight remaining tables, linking each of
the 18 rows to its indicators/<family>/Indicator-*.md page. RollingVwap
points at the RollingVwap section of Indicator-Vwap.md (added in E18).
All link targets verified to exist.
Indicators-Overview.md listed Trix in the Trend section's EMA-family
table, while the README and the docs folder layout
(indicators/momentum/Indicator-Trix.md) place it under Momentum.
Move the Trix row into the Momentum "Unbounded oscillators" table — it
emits a rate of change, not a price-scale trend line — and leave a note
in the Trend section pointing there. Momentum is now the single
canonical family across the README, the folder layout, and the
overview.
A5 changed Keltner and HMA to feed their sibling sub-indicators
unconditionally, so warmup_period() is now the exact first-emission
index for every indicator. The wiki still described the old
?-starvation behavior as correct.
- Indicator-Keltner.md: the Warmup section, the worked example output
(first emission now at i=2, not i=4), the summary table row, and the
"reported warmup understates" pitfall now state that warmup_period()
is exact. Example output regenerated by running the code.
- Indicator-Hma.md: the Warmup section, all three language examples
(first Some at index 10, not 13), the table row, and the chaining
pitfall corrected. Outputs regenerated.
- Indicators-Overview.md: dropped the claim that Hma and Kama lag their
reported warmup — both were verified exact.
The repository had no supply-chain auditing — no deny.toml and no CI
job to catch vulnerable, unmaintained, wrongly-licensed, or
unexpectedly-sourced dependencies.
Add deny.toml covering advisories, bans, licenses and sources:
- licenses: an allow-list of the permissive licenses the dependency
tree actually uses, plus the workspace's own PolyForm-Noncommercial
license and a scoped LLVM-exception for target-lexicon.
- bans: warn on duplicate versions, deny external wildcard deps
(internal path deps are allowed).
- sources: only crates.io.
- advisories: RUSTSEC-2025-0020 (pyo3 0.22) is ignored with a documented
reason — it is reachable only through bindings/python and the pyo3
upgrade is tracked separately; the published crates do not use pyo3.
Add a `supply-chain` CI job running cargo-deny-action (SHA-pinned).
`cargo deny check` passes locally: advisories/bans/licenses/sources ok.
- "## Indicators in 0.1.0" -> "## Indicators" (the heading drifted from
the actual version; making it version-neutral stops the drift).
- Project layout: there is no top-level benches/ directory — Rust
benches and examples live inside their crate. The tree now shows
crates/wickra/benches, the per-crate examples, and the new
bindings/node and bindings/wasm examples/ directories.
- Node "Example" pointed at a test file; it now points at the real
bindings/node/examples/streaming.js (added in E17).
- "## Test counts" hardcoded numbers (171/11/56/7) that drift on every
added test. Replaced with a version-neutral "## Testing" section that
describes what each suite covers, including the WASM tests.
bindings/node had no examples/ directory — the README pointed at a test
file as its "Example". (bindings/wasm/examples/index.html already
exists and is a complete browser demo, so only the Node side was
missing.)
Add bindings/node/examples/streaming.js: a deterministic synthetic
price series fed tick by tick through SMA, EMA, RSI and MACD, printing
a status line and flagging overbought/oversold candidates — the same
O(1)-per-update streaming model a live bot would use. Verified against
the built native module.
The three published crates had no documentation link and no docs.rs
configuration, so wickra-data's feature-gated live-binance module would
not render on docs.rs.
Add documentation = "https://docs.rs/<crate>" and a
[package.metadata.docs.rs] section with all-features = true to
wickra-core, wickra, and wickra-data. No `exclude` is added: each crate
directory contains only src/ (plus benches/examples that are useful
source), so there is nothing irrelevant to drop from the .crate.
The workspace Cargo.toml declared authors = ["Wickra Contributors"]
while the npm package.json and the WASM package.json enrich step in
release.yml both use "kingchenc <kingchencp@gmail.com>".
Make the canonical author "kingchenc <kingchencp@gmail.com>" — the
actual maintainer, already used by both npm packages — and align the
workspace manifest to it. All three registries now agree.
Indicators-Overview.md referenced the absolute author-machine paths
D:\Coding\Wickra\crates\... and D:\Coding\Wickra\bindings\... in its
"Source-of-truth files" section, and seven trend-indicator pages had
Node examples that did require('D:/Coding/Wickra/bindings/node').
Replace the overview paths with repo-relative GitHub links and change
the Node examples to require('wickra'), the published npm package name
a reader would actually use. No D:/Coding path remains anywhere in docs.
The 33 Markdown files under docs/wiki/ were never tracked. Commit them
into the repository so the documentation is versioned alongside the
code: 8 top-level pages plus 25 per-indicator deep dives under
indicators/{momentum,trend,volatility,volume}/.
The pages are kept in-repo (not pushed to a flat GitHub Wiki), so the
relative indicators/<family>/... links in Home.md resolve correctly
when rendered on GitHub.
Every CI job used dtolnay/rust-toolchain on stable, so the declared
minimum supported Rust version was never exercised — an accidental use
of a newer API would only break for downstream users on an older
compiler.
Add an `msrv` job with a two-row matrix: the workspace crates
(wickra-core, wickra, wickra-data) build and test on Rust 1.75, and the
node binding on Rust 1.77, matching the rust-version each manifest
declares. Both rows use the SHA-pinned toolchain action.
Every action in ci.yml and release.yml was pinned to a movable tag
(actions/checkout@v4, dtolnay/rust-toolchain@stable, ...). A compromised
upstream tag would run with access to the crates.io / PyPI / npm
publish tokens.
Pin every `uses:` to the full 40-character commit SHA the referenced
ref currently resolves to, with the human-readable version kept as a
trailing comment so Dependabot can still bump them:
actions/checkout v4.3.1
actions/setup-python v5.6.0
actions/setup-node v4.4.0
actions/upload-artifact v4.6.2
actions/download-artifact v4.3.0
dtolnay/rust-toolchain stable branch @ 2026-03-27
Swatinem/rust-cache v2
jetli/wasm-pack-action v0.4.0
PyO3/maturin-action v1.51.0
softprops/action-gh-release v2.6.2
SHAs were resolved against the GitHub API. The github-actions Dependabot
ecosystem that keeps these pins current is added with E3.
release.yml triggers on every v* tag push and the four publish jobs
(crates.io, PyPI, npm, wasm) inject long-lived registry tokens straight
from secrets with no environment, no reviewer and no tag restriction.
Bind all four jobs to a `release` GitHub environment. With the
environment's protection rules (required reviewers, tag/branch
restrictions) configured under repo Settings -> Environments, the
registry secrets become reachable only from an approved release run
rather than from any workflow execution.
The main npm package already publishes and packs with --ignore-scripts,
but the per-platform subpackage loop did not: `npm publish --access
public`, its retry, and the per-platform `npm pack` all ran lifecycle
scripts from the package directory with the npm token in scope.
Add --ignore-scripts to all three, matching the main package, so no
prepublish/prepare hook can execute during a release.
The release workflow built the .crate attachments with
`cargo package --allow-dirty --no-verify`, so the attached artefact
could diverge from the tagged tree and was never proven to build.
Remove both flags. actions/checkout provides a clean tree and no prior
step mutates it, so --allow-dirty is unnecessary. The crates are
published to crates.io earlier in the same job, so the verification
build now resolves workspace dependencies from the registry and
confirms each .crate compiles before it is attached.
backtest.py and multi_timeframe.py read OHLCV CSVs with csv.DictReader
and crashed opaquely on malformed input: a missing column raised a bare
KeyError, a non-numeric cell surfaced NumPy's column-less ValueError,
and an empty or all-NaN series hit IndexError deep in summarize.
Validate the header against the required columns up front, catch
non-numeric cells and report the offending row/column, reject a
header-only file distinctly from a headerless one, and guard resample
and summarize against empty input. Every failure mode now raises a
ValueError naming the file, row and column.
Two issues in the live_trading example:
- The status line rendered indicator values with `... if snap.rsi
else "--"`. A genuine reading of 0.0 is falsy, so an RSI / MACD
histogram / Bollinger value of exactly zero was misreported as "--".
Switch to explicit `is not None` checks.
- --symbol and --interval were interpolated straight into the stream
name and WebSocket URL with no checks. Add validate_args: the symbol
must be strictly alphanumeric and the interval must be one Binance
recognises. main() validates before connecting and exits with a clear
error and code 2 otherwise; the strict values keep the URL well-formed
without escaping.
OpenBar::into_candle and RolledBar::into_candle built their result with
Candle::new_unchecked, skipping the finiteness check. volume is summed
across every absorbed tick/candle, so a long or large run can drift it
to +inf — and an inf-volume candle would silently poison every
downstream indicator.
Switch both to Candle::new, which validates volume finiteness, and
return Result<Candle>. The OHLC fields are finite and correctly ordered
by construction, so the only invariant Candle::new can reject here is a
non-finite volume. push propagates the error with `?`; both flush
methods now return Result<Option<Candle>> and resample_all pulls the
result through.
push rejected ticks that went backwards across buckets but absorbed any
tick whose timestamp fell inside the open bucket — including one older
than the last tick already absorbed. Such a stale tick silently
overwrote the bar's close with an outdated price.
Track last_ts on OpenBar (set in from_tick, advanced in absorb) and, on
the same-bucket path, reject a tick whose timestamp predates it with
Error::Malformed, leaving the open bar untouched. Ticks that share a
timestamp are still accepted, since several trades can land in the same
millisecond.
Timeframe::floor computed `ts - ts.rem_euclid(bucket)`. For a timestamp
within one bucket of i64::MIN the subtrahend is a positive remainder
and the true boundary lies below i64::MIN, so the subtraction overflowed
and panicked in debug builds.
Switch to saturating_sub: the result clamps to i64::MIN in that
practically unreachable case and stays exact everywhere else. floor
keeps its infallible `-> i64` signature, so neither push path changes.
The combined Binance stream interleaves the kline payloads with
subscription acks, heartbeats and error objects. The example pulled
k = payload.get("k", {}) and immediately did float(k.get("c")) — for
any non-kline frame k is {}, k.get("c") is None, and float(None) raises
TypeError, crashing the script the moment it connects.
Skip frames without a kline payload (no "k" object, or no "c" close
field) with a debug log line, matching the C2 fix on the Rust adapter.
The CSV reader set has_headers(true) with no trimming and no header
check, so three real-world inputs failed silently or opaquely:
- A file with no header row had its first data row consumed as the
header and silently dropped.
- A leading UTF-8 BOM (Excel exports it) became part of the first
header name, breaking the `timestamp` column mapping.
- Leading/trailing whitespace around values broke serde parsing.
Add a BomStripReader<R> Read adapter that discards a leading EF BB BF,
set csv::Trim::All on the builder, and validate after opening that the
header names every required OHLCV column — a missing column now yields
a clear Error::Malformed instead of a silent misread. open/from_reader
route through a shared build() helper; from_reader and from_csv_reader
now return Result because header validation can fail.
A tick that jumped across one or more empty buckets previously opened
the next non-empty bar directly, so the candle series silently grew
time holes — downstream indicators (EMA, ATR, ...) computed over such a
series drift from one computed over an unbroken series.
Add an opt-in gap-fill mode: with_gap_fill(true) makes push emit a flat
placeholder candle (open == high == low == close = the pre-gap close,
volume = 0) for every skipped bucket. push now returns Result<Vec<Candle>>
so a single tick can yield the closed bar plus its trailing fillers;
the empty vector replaces the former Ok(None). Timestamp overflow while
filling is reported as Error::Malformed. Default behaviour is unchanged
(gaps skipped) and is now documented on the type and on push.
Resampler::push previously closed the open bar and opened a new one for
any candle whose bucket differed from the open bar, including buckets
strictly before it — silently corrupting the output for out-of-order
input. TickAggregator::push already rejects this case with an error.
Change push to return Result<Option<Candle>>: candles in an earlier
bucket than the open bar now yield Error::Malformed, matching the
aggregator. resample_all propagates the error via `?`. The doc comment
keeps the input/output multiple relationship as a documented caller
responsibility, since Resampler does not know the input timeframe.
A 24-hour forced disconnect or a network blip permanently killed the
feed: next_event returned Ok(None)/Err and the stream was dead. The
struct now retains the subscribed symbols, an open() helper rebuilds the
socket, and reconnect() retries with exponential backoff (1s..30s, up to
MAX_RECONNECT_ATTEMPTS). next_event transparently reconnects on a
protocol error, a server close or a read stall, and only reports Ok(None)
after the caller has closed the stream. close() now takes &mut self.
connect() used connect_async with no WebSocketConfig and next_event
awaited the socket with no deadline, so a stalled server hung the feed
forever and an oversized message could force an unbounded allocation.
connect() now passes a WebSocketConfig capping message/frame size, and
next_event wraps the read in a 300s tokio timeout (well above Binance's
~3-minute ping), surfacing a stall as the new Error::Timeout.
BinanceKlineStream had no closed-state flag, so after the server closed
the connection (Ok(None)) a caller could keep calling next_event and
poll a dead socket. A closed: bool is now set when the server closes or
sends a Close frame; next_event short-circuits to Ok(None) once set, and
is_closed() exposes the state.
next_event deserialized every text frame straight into RawWsEnvelope, so
a subscription acknowledgement, heartbeat or error object propagated a
decode Err and killed the feed. Frames are now routed through
parse_frame, which inspects data.e: kline frames yield an event,
everything else is skipped, and an Err is raised only when a genuine
kline frame fails to decode. Adds tests for skipped acks/errors.
The napi loader resolves wickra-linux-arm64-gnu and
wickra-win32-arm64-msvc, but neither was published, so require('wickra')
failed on those platforms. Adds both to napi.triples and
optionalDependencies, adds their npm/ package templates, and extends the
release node-build matrix to build them on GitHub's native ARM runners
(ubuntu-24.04-arm, windows-11-arm).
MacdNode.batch and BollingerNode.batch return flat interleaved arrays
(3*n and 4*n) but index.d.ts only said Array<number>. Adds /// doc
comments describing the layout; napi-rs now propagates them into the
generated index.d.ts as JSDoc.
Normalises whitespace in sources committed earlier in this branch
before rustfmt was run over them (Node/WASM bindings, and three core
indicator test modules), and records the wasm-bindgen-test dependency
tree added in B6 into Cargo.lock. No functional change; cargo fmt --all
--check is now clean.
Every Python batch() did prices.as_slice().expect("contiguous"), so a
non-contiguous NumPy input (e.g. a strided view) aborted with a Rust
panic instead of a catchable exception. as_slice() failures now map to a
PyValueError pointing at np.ascontiguousarray; the scalar / MACD /
Bollinger batch methods that returned a bare array were lifted to
PyResult so the error can propagate. Adds input-validation tests
(non-contiguous arrays, unequal-length candle batches, ROC/TRIX
defaults). All 60 Python tests pass against the freshly built wheel.
The Node binding had only 7 smoke cases with no streaming-vs-batch or
candle-indicator coverage. Adds indicators.test.js: streaming update()
matches batch() for all 25 indicators (scalar, candle-scalar and
interleaved multi-output), a lifecycle check that every indicator
exposes reset/isReady/warmupPeriod, and reference values (SMA(3),
MFI(2)=1200/23, RSI uptrend, MACD histogram). 38 Node tests pass.
Every other Python momentum indicator (RSI, CCI, ...) carries a
#[pyo3(signature)] default, but ROC and TRIX required an explicit
period. Both now default to the TA-Lib convention (ROC period=10, TRIX
period=30), and the .pyi stubs reflect the defaults. Node and WASM
constructors deliberately stay explicit-only -- napi-rs and
wasm-bindgen do not support default arguments, and every constructor in
those bindings is uniformly explicit.
The WASM binding had no tests; CI only checked that artefacts existed.
Adds a wasm-bindgen-test suite covering SMA reference values, EMA
batch==streaming equivalence, RSI pure-uptrend behaviour, fallible
constructors returning JsError, and the unequal-length batch guards from
B3. Wires wasm-pack test --node into the CI wasm job. The suite
type-checks on the host; it executes under wasm-pack in CI (the local
environment is a non-rustup Rust install without the wasm32 target).
The .pyi shipped stubs for only 9 of the 25 exported classes, so with
py.typed set, type checkers flagged DEMA, TEMA, HMA, KAMA, CCI, ROC,
WilliamsR, ADX, MFI, TRIX, PSAR, Keltner, Donchian, VWAP,
AwesomeOscillator and Aroon as missing. All 16 are now stubbed with
signatures matching python/src/lib.rs (constructor defaults, update
return types, batch array shapes, lifecycle methods). Verified: the
stub set equals the 25 registered classes and mypy type-checks a script
exercising every class with no issues.
KAMA and the nine candle indicators (CCI, WilliamsR, MFI, PSAR,
Keltner, Donchian, VWAP, AO, Aroon) exposed none of the three lifecycle
methods; Stochastic/OBV/ADX exposed only reset; MACD/Bollinger/ATR
lacked warmupPeriod. Every indicator now exposes reset(), isReady() and
warmupPeriod(), matching the scalar-macro surface. Verified against the
rebuilt module (e.g. MACD warmupPeriod 34, Stochastic 16, KAMA 11).
Candle batch() methods that index parallel high/low/close/volume arrays
without first checking their lengths panic on a length mismatch. Adds an
equal-length guard returning a clean error to the 11 affected Node
methods (Stochastic, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian,
VWAP, AO, Aroon), the 10 affected WASM methods, and the 8 affected
Python methods (WilliamsR, ADX, MFI, PSAR, Keltner, VWAP, AO, Aroon) --
matching the guard ATR/OBV already had. Verified in Node: mismatched
arrays now throw instead of crashing.
The 14 candle and multi-parameter indicator constructors passed raw
parameters through must() (an expect()), so invalid arguments such as
new MACD(0,0,0) or new BollingerBands(20,-1) aborted the whole process
over the napi boundary (the release profile sets panic=abort).
napi-rs 2.16 does accept a #[napi(constructor)] returning
napi::Result<Self> (the old must() comment was wrong), so each now
returns napi::Result<Self> and throws a clean JS error via map_err.
must()/clamp_period stay for the scalar macro, where period is clamped
and the Result is provably Ok. Verified: every invalid constructor
throws, valid ones still build.
Stochastic, OBV, ADX, CCI, WilliamsR, MFI, PSAR, Keltner, Donchian,
VWAP, AwesomeOscillator and Aroon previously exposed only batch() in the
Node binding, so a streaming-first library could not actually stream
them from Node. Each now has an update() mirroring the AtrNode pattern,
with the same input arity and output type as its batch() counterpart.
Verified against the rebuilt native module.
Keltner::update gated atr.update behind ema.update(...)? and Hma::update
gated full_wma.update behind half_wma.update(...)?. The ? short-circuit
starved the trailing sibling of every candle consumed during the leading
one's warmup, so warmup_period() understated the true first emission
(Keltner classic: 29 instead of 20; HMA(9): 14 instead of 11) and
Keltner's ATR seeded over the wrong window.
Both now feed every sub-indicator unconditionally and gate only the
output, matching the MACD / Awesome Oscillator pattern. warmup_period()
is now exact. Adds first-emission tests and cross-checks against
independent EMA+ATR (Keltner) and independent WMAs (HMA).
ROC now stores its last emitted value and returns it on a non-finite
input instead of None, leaving the window untouched. This matches the
SMA / EMA convention. reset() clears the new field. Adds a
non-finite-input test.
Adds the reset tests the audit named as missing (aroon, awesome
oscillator, donchian, keltner, williams_r, and both VWAP variants),
non-finite-input tests for every scalar indicator that guards is_finite
(WMA, RSI, MACD, Bollinger, KAMA), and naive-reference proptests for EMA,
RSI and ATR. 189 core tests pass.
reset() now restores prev_high, prev_low and trend in addition to the
previously reset fields, keeping the struct fully consistent for
inspection. The misleading inline comment that claimed direction-dependent
seeding is corrected to describe the actual fixed-Up seed, which
self-corrects through PSAR's reversal logic. Adds a reset-reuse test.
The first candle now only seeds the previous typical price instead of
pushing a fabricated (0,0) money-flow pair into the window, matching the
TA-Lib / pandas-ta convention. warmup_period() returns period + 1 and the
dead prev_tp.is_none() guard is removed. Adds a first-emission test and a
hand-computed reference-value test (MFI(2) = 1200/23).
Pure tooling release on top of 0.1.3. The library code is unchanged;
only the release workflow grew a new github-release job that attaches
every built artefact to the GitHub Release page so users have direct
download links next to the source archives:
- Python wheels (5 platforms) + sdist
- Native Node bindings (linux-x64-gnu, darwin-x64, darwin-arm64, win32-x64-msvc)
- npm-pack tarballs for the main wickra package, every per-platform
subpackage, and wickra-wasm
- Cargo .crate files for wickra-core, wickra-data, wickra
The job runs at the end of the release pipeline and also accepts
workflow_dispatch so future asset-only fixups don't require a version
bump.
The Releases page previously showed only the source archives GitHub
auto-generates for every tag. Add an explicit github-release job that
runs after every publish job finishes, downloads every uploaded
artefact, and attaches them all to the release.
New per-publish-job upload-artifact additions:
- cargo-publish: 'cargo package' each crate and upload the .crate files
- wasm-publish: 'npm pack' inside pkg/ and upload the wickra-wasm tgz
- node-publish: 'npm pack' the main package + each per-platform subpackage,
upload all six tgz files (main + linux + 2*darwin + win32)
Wheels and .node binaries were already being uploaded earlier in the
matrix builds, so the new job just consumes them via download-artifact.
github-release job:
- Runs on tag pushes (normal) AND workflow_dispatch (so we can backfill
assets to an existing release without rebumping the version).
- Resolves the target tag from github.ref on a tag push, or from
'git tag --sort=-v:refname | head' on dispatch.
- Stages all files into release-assets/, uses softprops/action-gh-release
to create or update the release. generate_release_notes lets GitHub
fill in the commit-list changelog automatically.
CI is now green across all 20 jobs:
- Rust on Linux/macOS/Windows
- Python 3.9/3.11/3.12 on Linux/macOS/Windows
- Node 18/20 on Linux/macOS/Windows (Windows previously failed
because ci.yml built the native module without --platform; fixed
in the previous commit)
- WASM build
- Cross-library benchmark report
This tag re-publishes 0.1.3 across crates.io / PyPI / npm so a single
version covers every working binding. The release workflow's idempotent
publish steps mean re-runs are safe; the new code in this version is
just the build-script and loader changes that fixed CI.
The Windows Node 18 and Node 20 CI jobs failed because:
- ci.yml ran 'napi build --release' (no --platform), which produces a
filename without the target triple ('wickra.node').
- The committed loader at bindings/node/index.js looked for the
triple-suffixed file ('wickra.win32-x64-msvc.node') and then for the
per-platform npm subpackage as a fallback.
- 'wickra-win32-x64-msvc' isn't on npm yet (blocked by the spam filter),
so Windows had nothing to load. Linux and macOS passed only because
their optionalDependencies do exist on npm and got fetched.
Two-part fix:
- ci.yml now runs 'napi build --platform --release', matching release.yml.
--platform encodes the target triple in the filename, so the loader's
primary lookup always finds the freshly built binary on every host.
- bindings/node/index.js is now the canonical napi-rs auto-generated
loader (it gets regenerated by 'napi build'). It covers all the platforms
napi knows about (linux glibc/musl, Android, FreeBSD, ARM, etc.) rather
than just the four we publish, so unsupported platforms get a clear
'this binding isn't built for your system' error instead of a confusing
ENOENT.
Local sanity: 'npx napi build --platform --release' then 'node --test
__tests__/' succeeds end to end on Windows; the eight existing tests
pass.
The root cause of the previous failure was the package.json
"prepublishOnly": "napi prepublish -t npm" script. When the workflow
ran `npm publish` for the main wickra package it implicitly invoked
that hook, which re-attempted to publish every platform subpackage and
hit 403 "cannot publish over 0.1.2" because they were already on
the registry. The error trail looked like it came from the main
publish step but the failing command was actually the hook.
Two-part fix:
- Remove prepublishOnly entirely from bindings/node/package.json. The
release workflow already does the per-platform publish itself, in a
loop, idempotently.
- Belt-and-braces: pass --ignore-scripts when publishing the main
package so any prepublish-hook drift in the future is suppressed.
GitHub Actions runs shell steps with bash -e, so when npm view wickra@<v>
returned exit 1 (because the package was not yet on the registry) the
whole step aborted before reaching npm publish. Switch the step to
`set +e` and capture the rc explicitly, so a 404 from npm view is
treated as 'not published yet, go publish' instead of a fatal error.
Also adds a single retry after 30s for spam-filter blocks.
The previous 'napi prepublish -t npm' step was atomic — one failure killed
the whole step, and on retry it tried to republish already-uploaded
versions which 403'd. v0.1.2 left 3 of 4 platform packages and the wasm
package on npm but the main 'wickra' package and 'wickra-win32-x64-msvc'
never got out.
Replace it with a small bash loop that:
- Walks every npm/<platform>/ directory.
- Skips the publish if 'npm view <pkg>@<version>' confirms the version
is already on the registry (idempotent re-runs).
- Tolerates per-package failures (sets rc but always returns 0 from the
helper) so spam-filter blocks on one platform don't take down the
others. A 30-second retry handles transient rate-limit spam blocks.
- Publishes the main 'wickra' meta-package as a separate step with the
same skip-existing guard.
Same publishing semantics, just deconstructed into individually
recoverable steps.
`napi create-npm-dir` failed in CI with both invocations we tried:
positional arg form (`-t <triple> .`) was rejected as extraneous,
and the no-arg form crashed with "path must be a string, received
undefined". The fix used by every napi-rs reference project: commit
the four platform package.json templates directly under
bindings/node/npm/<target>/ instead of generating them at publish time.
- bindings/node/npm/{linux-x64-gnu,darwin-x64,darwin-arm64,win32-x64-msvc}/
each contain a static package.json with the correct os / cpu / libc
filters so npm only installs the right binary per platform.
- Main package.json gains optionalDependencies referencing all four
platform packages by version, so `npm install wickra` pulls the
matching binary on each user's machine.
- Release workflow drops the broken `create-npm-dir` loop. The
`napi artifacts` step now just copies the .node files from the
build artefacts into the existing npm/ directories before publish.
Bump every version to 0.1.2; cargo / pypi / wickra-wasm jobs are
idempotent so they accept the 0.1.1 they already published while still
emitting the new 0.1.2.
v0.1.0 reached crates.io, PyPI, and the wickra-wasm npm package, but
the wickra Node binding never published because the workflow called
`napi create-npm-dirs` (plural). The actual napi CLI command is
`create-npm-dir` (singular, per target).
Changes that make the next tag's release actually finish:
- workflow: replace the broken step with a loop that runs
`napi create-npm-dir -t <triple>` for each of our four build targets.
- workflow: every registry publish step now treats "version already
uploaded" as success. That makes re-runs (and partial-failure
recoveries) safe instead of failing the whole job.
- bindings/node/package.json: trim the napi.triples list to the four
platforms we actually build (linux-x64-gnu, darwin-x64, darwin-arm64,
win32-x64-msvc). The previous defaults+9-extras configuration would
have made napi prepublish expect platform packages we never produced.
- bindings/node/index.js: switch from local-only binary lookup to the
proper napi loader that tries the local `.node` file first and falls
back to the per-platform npm subpackage that's installed as an
optional dependency in production.
- Versions across all four published packages bumped to 0.1.1 so the
Node binding can publish for the first time alongside refreshed
cargo/pypi/wasm artefacts.
2026-05-21 21:06:42 +02:00
693 changed files with 298495 additions and 1542 deletions
if ! grep -qE "^${n} streaming-first indicators" README.md; then
echo "::error::README.md does not say '${n} streaming-first indicators' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps README.md), then push again."
ok=false
fi
if ! grep -qE "\*\*${n} indicators\*\*" docs/README.md; then
echo "::error::docs/README.md does not say '**${n} indicators**' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps docs/README.md), then push again."
ok=false
fi
if [ "$ok" = "true" ]; then
echo "README.md + docs/README.md counter already at ${n}; nothing to do."
else
exit 1
fi
# ----- main / tag flow ------------------------------------------
#
# After a PR squash-merges, this workflow runs again on the push to main.
# README.md / docs/README.md are already correct (the indicator wiring
# bumped them in the merged code commit); the only outward syncs left are
# the GitHub About description (repo metadata, not a commit) and the docs /
# webpage / wiki / org repos (separate repos, no main history pollution).
# The wickra repo's own README is not touched on main any more.
- name:Update GitHub About (description + homepage)
if:github.event_name != 'pull_request'
env:
GH_TOKEN:${{ secrets.ABOUT_SYNC_TOKEN }}
run:|
n="${{ steps.count.outputs.count }}"
# Canonical homepage — the docs site (P8.3). This is enforced on every
# run, so it must only point at docs.wickra.org once that domain is
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore
# gated on the domain resolving, otherwise the About link would 404.
homepage="https://docs.wickra.org"
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
# Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope).
# api/*.md "Latest" lines, the nav version label, and the wickra-wasm
# dep pin. The '.' anchors match the backtick / quote / caret without a
# literal in this shell string; historical "Since X.Y.Z" prose has no
# such anchor and is never matched.
sed -i -E "s/(Latest:\*\* \[.wickra(-wasm)? )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" api/*.md
sed -i -E "s/(text: .v)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" .vitepress/config.ts
sed -i -E "s/(.wickra-wasm.: .\^)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" package.json
# Keep package-lock.json in sync with the package.json bump. The site's
# Cloudflare build runs `npm clean-install` (npm ci), which hard-fails
# with EUSAGE if the lockfile still pins the previous wickra-wasm —
# editing package.json alone is not enough. The npm-wait above already
# proved wickra-wasm@$version is resolvable, so --package-lock-only
# regenerates the lock (version + resolved + integrity) without fetching
# node_modules. Guard it: if the regen fails, skip the whole commit so we
# never push a package.json/lock mismatch that would break the build.
if ! npm install --package-lock-only --no-audit --no-fund; then
echo "::warning::could not regenerate package-lock.json for wickra-wasm@${version}; skipping webpage version sync to avoid a lockfile-drift build break."
wasm-pack build bindings/wasm --target web --release --features panic-hook
wasm-pack test --node bindings/wasm
```
## Lockfile policy
| Component | Lockfile | Tracked? | Why |
| --- | --- | --- | --- |
| Workspace (Rust) | `Cargo.lock` | **yes** | The workspace ships binaries (examples, fuzz harness) and CI builds, so the dependency graph is pinned for reproducible builds. |
| `bindings/node` | `package-lock.json` | **yes** | Reproducible `npm install` for the native binding. |
| `examples/node` | `package-lock.json` | **yes** | Same — the runnable Node examples link the binding via a `file:` dependency. |
| `bindings/python` | — | n/a (no lockfile) | The published package pins only `numpy>=1.22` at runtime; its native code is pinned through the workspace `Cargo.lock`. The CI/bench dev tooling it installs is hash-locked separately — see the `.github/requirements` row. |
| `.github/requirements` | `*.txt` (hash-pinned) | **yes** | CI/bench Python tooling, locked with `uv pip compile --generate-hashes` (OpenSSF Scorecard PinnedDependencies). `ci-dev` is split per Python version — `ci-dev-py39.txt` and `ci-dev-py3.txt` — because numpy ships no single release with wheels for both cp39 and cp313; `bench.txt` covers the single-version bench job. |
| `fuzz` | `fuzz/Cargo.lock` | **no** (ignored) | `fuzz/` is a detached crate; `cargo-fuzz init` generates `fuzz/.gitignore` which ignores its `Cargo.lock`. The fuzz smoke job resolves dependencies fresh, so the lock is not needed for reproducibility here. |
| `site` (marketing) | `package-lock.json` | **no** (ghost-ignored) | The VitePress site is a local-only project excluded via `.git/info/exclude`; its lockfile stays local. |
When adding a new committed Node package, commit its `package-lock.json` too and
remove any matching ignore rule. Do **not** add a top-level `package-lock.json` —
the repository root is not an npm package.
To refresh every committed lockfile in the workspace — `Cargo.lock`,
`fuzz/Cargo.lock`, the Node binding lock, and the hash-pinned Python
requirements — run `./scripts/update-lockfiles.sh`. It uses `uv` for the Python
locks (and bootstraps it on Linux/macOS if absent) so each target Python
version's hashed transitive closure can be regenerated without that interpreter
installed. Dependabot also keeps the `.github/requirements` pins current.
## Standards for a change
- **Formatting & lints.** `cargo fmt` must leave the tree unchanged and
`cargo clippy ... -D warnings` must be clean. CI gates both.
- **Tests.** New behaviour needs tests; bug fixes need a regression test.
- **Indicator correctness.** A new or changed indicator must have a
reference-value test against a known-good source (TA-Lib, pandas-ta, or a
hand-computed value) and a `reset` test.
- **Streaming parity.** An indicator's `batch` output must equal the sequence
of `update` calls.
- **Bindings.** A change to a public indicator API must be mirrored across the
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
- **Docs.** Update the relevant page on the
[documentation site](https://docs.wickra.org) and the
`README.md` when behaviour or the public API changes. The docs live in
a separate git repository: `https://github.com/wickra-lib/wickra-docs`.
- **Changelog.** Add an entry under `## [Unreleased]` in `CHANGELOG.md`.
## Commit and pull-request workflow
1. Branch off `main`.
2. Keep commits focused — one logical change per commit, with an imperative
subject line and a body explaining *why*.
3. Open a pull request against `main` and fill in the template.
4. CI must be green before review.
## Reporting bugs and proposing features
Use the issue templates under
[`.github/ISSUE_TEMPLATE`](.github/ISSUE_TEMPLATE). For security-sensitive
reports, follow [`SECURITY.md`](SECURITY.md) instead of opening a public issue.
## Developer Certificate of Origin (DCO)
All contributions to Wickra are made under the [Developer Certificate of
Origin (DCO) 1.1](DCO). By signing off on your commits you certify that you
wrote the patch, or otherwise have the right to submit it under the project's
`MIT OR Apache-2.0` license.
Sign off every commit by adding a `Signed-off-by` trailer with your real name
and email — Git adds it automatically with the `-s` flag:
```bash
git commit -s -m "your message"
```
This produces a trailer of the form:
```
Signed-off-by: Your Name <you@example.com>
```
The name and email must match the commit author. Commits without a valid
sign-off line cannot be merged. To sign off a commit you already made, amend it
with `git commit -s --amend`, or sign off a range with an interactive rebase.
## Governance
Wickra's decision-making and maintainership are described in
[`GOVERNANCE.md`](GOVERNANCE.md); the current maintainers are listed in
| 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 |
This document describes Wickra's attack surface and the threats considered,
together with their mitigations. It complements the security assurance case in
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
library (a Rust core with Python, Node.js and WebAssembly bindings), not a
network service or trading system; the attack surface is correspondingly small.
## Assets
- **Integrity of computed indicator values** — consumers may use them in
automated decisions, so silently wrong output is the primary concern.
- **Availability of the calling process** — a library must not crash or hang
its host on malformed input.
- **Integrity of published artifacts** — the crates, wheels and npm packages
users install.
- **The build and release pipeline** and its secrets (publishing tokens).
## Actors / trust boundaries
- **Library consumer** (trusted) — calls the API with numeric data. Data may
originate from untrusted sources (e.g. a market feed), so *input values* are
treated as untrusted even though the caller is trusted.
- **Optional live feed** — with the `live-binance` feature, data crosses a
network boundary from an exchange over TLS.
- **Contributors** (semi-trusted) — propose changes via pull requests.
- **Supply chain** — upstream dependencies and the CI/CD platform.
## Threats and mitigations
| Threat | Mitigation |
| --- | --- |
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
| Adversary-in-the-middle on the optional live feed | Connection uses TLS via the platform library; transport security is delegated to that reviewed implementation. |
| Compromised dependency (supply chain) | Dependencies pinned (`Cargo.lock`, hash-locked CI requirements), monitored by Dependabot, audited by `cargo-deny` (advisories + licenses) on every change. |
| Malicious or accidental change to `main` | Branch protection requires signed commits and blocks force-push and deletion; all changes flow through pull requests with required CI; static analysis (CodeQL, Clippy) and fuzzing run on every change. |
| Compromised CI / leaked secrets | Workflows use least-privilege `permissions:`; secrets live only as encrypted GitHub Actions secrets; secret scanning with push protection is enabled; workflows are linted by `zizmor`. |
| Tampered release artifact | Releases are built in CI, tags are signed, and assets carry build provenance attestations (verifiable with `gh attestation verify`). |
## Out of scope
- Wickra implements no authentication, authorization or cryptography of its own,
stores no user data, and exposes no network listener; those threat classes do
not apply.
- Vulnerabilities in third-party dependencies that do not affect Wickra are
tracked as exploitability (VEX) records (see [`SECURITY.md`](SECURITY.md)).
## Maintenance
This threat model is reviewed when the architecture changes materially (for
example, a new input family, a new network feature, or a new release channel).
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.