Compare commits

...

6 Commits

Author SHA1 Message Date
kingchenc 395a2289f4 release: bump 0.8.3 -> 0.8.4 (#256)
Version bump `0.8.3` → `0.8.4`.

Ships the work merged via #254:

### Fixed
- A single non-finite (NaN/inf) tick no longer poisons indicator state — 38 more scalar/pairwise indicators (linear-regression family, rolling quantiles/IQR, `Variance`/`StdDev`-derived stats, `Kurtosis`/`Skewness`, trailing stops, `KalmanHedgeRatio`, `SpreadBollingerBands`, …) now reject non-finite input and return `None`, joining the 16 pairwise indicators fixed earlier.

### Added
- Catalogue-wide property-based invariant harness (`crates/wickra-core/tests/invariants.rs`) asserting `batch == streaming`, `reset == fresh`, and non-finite-input rejection for every indicator and bar-builder.

### Changed
- CI: every job now has a runtime cap and the flaky Node test step auto-retries.
- Documentation accuracy fixes in `SECURITY.md`, `ARCHITECTURE.md`, and `THREAT_MODEL.md`.

Bump touches the manual release touchpoints only (`Cargo.toml`/`Cargo.lock`, Python/Node/Java/C#/R manifests, lockfiles, `SECURITY.md`, `CHANGELOG.md`). docs/webpage version strings are left to `sync-about.yml` on the tag.
2026-06-11 14:57:15 +02:00
kingchenc 9973d1a6bf test(core): catalogue-wide invariant harness + guard non-finite input in 38 indicators (#254)
Adds `crates/wickra-core/tests/invariants.rs` — a property-based (`proptest`) harness asserting three invariants for **every** indicator and bar-builder in the catalogue (every module entry implementing `Indicator` or `BarBuilder`; the lone non-indicator helper `pattern_swing`/`SwingTracker` is excluded by design) — and fixes the non-finite bugs the harness surfaced.

## Invariants
1. **batch == streaming** — `batch()` must replay `update()` exactly.
2. **reset == fresh** — after `reset()`, re-feeding the same data matches a fresh instance.
3. **non-finite rejected without poisoning** (`f64` / `(f64, f64)` families) — a NaN/inf tick returns `None` and leaves state identical to never having seen it.

## How it works
- A single generic `check_seq<I: Indicator>` covers **all** input families — `f64`, `Candle`, `(f64, f64)`, and the exotic `CrossSection`, `Trade`, `DerivativesTick`, `OrderBook`, `TradeQuote` — via per-family `proptest` generators that produce *valid* inputs (e.g. order books are strictly monotonic and uncrossed).
- `check_bars<B: BarBuilder>` covers the bar-builder trait.
- Outputs are compared by `Debug` string so bit-identical `NaN` outputs count as equal — these properties test **determinism**, not NaN-freeness.

## The 38 non-finite fixes
The harness surfaced **38 more** scalar/pairwise indicators that let a NaN/inf tick poison their state — the same class as the 16 pairwise indicators fixed earlier (#251), but missed by the grep-based audit (`kalman_hedge_ratio` and `spread_bollinger_bands` carried `is_finite` on their **constructor** params, not the update input). All 38 now reject non-finite input via the established first-statement guard; signatures unchanged, so every binding inherits the fix. With these in, the non-finite invariant is enforced for every `f64`/`(f64,f64)` indicator going forward — the permanent regression net that would have caught #251.

Warmup-exactness was evaluated and **left out** (not universal — many multi-component/candlestick indicators emit before `warmup_period` by design).

## Verification
- `cargo test -p wickra-core`: 4225 unit + harness + 464 doc/integration, all green.
- `cargo clippy --workspace --all-targets --all-features -- -D warnings`: clean.
- Coverage runs integration tests, so the new guard branches are exercised by the harness's NaN feed.

Also documents the harness in README's Testing section and adds the pending 0.8.4 entries to CHANGELOG `[Unreleased]`.
2026-06-11 14:51:46 +02:00
kingchenc cb216668ee docs: fix accuracy drift in SECURITY, ARCHITECTURE, THREAT_MODEL (#253)
Documentation-only accuracy fixes from the codebase audit (no code changes).

## SECURITY.md
- Supported-version policy was stale at `0.5.x` while `0.8.3` is published. Bump to the exact `0.8.3` (prose + table `0.8.3 (latest)` / `< 0.8.3`). The exact `x.y.z` form lets `bump_version.py` keep it current automatically (now wired as a touchpoint).

## ARCHITECTURE.md
- `three` → **four** binding crates (the C ABI crate was added).
- workspace diagram `214` → **514** indicators (matches the `mod`-count and `lib.rs` public-type count; now wired into the indicator-wiring automation so it self-heals).
- WASM "does not have automated tests yet" → corrected: `bindings/wasm/src/lib.rs` carries **21** `wasm-bindgen-test` cases.
- **Numerical-stability notes rewritten to match the code:** the sliding-window variance family (`StdDev`, `Variance`, `ZScore`, `Bollinger`) uses running `Σx²−mean²` with clamping (and periodic reseed for `Bollinger`), **not** Welford. True Welford is used only by `IntradayVolatilityProfile` and `SeasonalZScore` (it does not transfer cleanly to a sliding window). The **Kahan-summation** bullet is removed — no Kahan summation exists in the crate.

## THREAT_MODEL.md
- The C ABI is built with `panic = "abort"` and has no `catch_unwind`. Replace the false "catches panics so none cross the boundary" claim with the honest abort strategy (terminates deterministically instead of unwinding across the FFI boundary, which would be UB).
2026-06-11 03:28:19 +02:00
kingchenc 20c0002f8e ci: cap job runtimes and auto-retry the flaky Node test step (#252)
## Problem

A Node test job wedged on a macOS runner: the **Run Node tests** step (`node --test`) hung for **over an hour** while the identical tests passed in ~1.5 min on every other runner (Node 20 macOS same run: 1m46s; Node 18 macOS on a sibling PR: 1m34s). It is a one-off stuck process — the macOS analogue of the documented `setup-node` Windows CDN flake — not a real Node 18 vs 20 difference.

No job in `ci.yml` set any `timeout-minutes`, so a hung step runs toward GitHub's **6h default** before the platform kills it.

## Changes

- **Job-level `timeout-minutes: 20` backstop on all 15 jobs.** The slowest real job is ~5 min, so 20 min is generous headroom (survives cold-cache spikes) while turning a 6h hang into a 20-min fail. The clock counts execution time only — queued/waiting time does not count against it.
- **`Run Node tests` wrapped in `nick-fields/retry` (SHA-pinned, v4.0.0):** a hung attempt is killed after 6 min and retried once (`timeout_minutes: 6`, `max_attempts: 2`), so a one-off wedge self-heals without a manual re-run. Normal run is well under a minute; worst case 2×6 min stays under the 20-min job backstop.

## Notes

- New SHA-pinned third-party action (`nick-fields/retry@ad98453…` = v4.0.0) — satisfies the SHA-pin convention; `zizmor` runs on this PR.
- `ci.yml` validated as well-formed YAML; all 15 jobs confirmed to carry a job-level timeout.
2026-06-11 03:27:05 +02:00
kingchenc 99497eb062 fix(core): reject non-finite input in 16 pairwise indicators (#251)
## Problem

16 of the 24 pairwise indicators (`type Input = (f64, f64)`) accepted non-finite input unchecked. A single NaN/Inf tick produced a NaN reading, contradicting the streaming-robustness guarantee that *a single bad tick cannot silently poison state* (README, `ARCHITECTURE.md`).

The other 8 pairwise indicators (`Alpha`, `InformationRatio`, `KalmanHedgeRatio`, `PairSpreadZScore`, `PairwiseBeta`, `RelativeStrengthAb`, `SpreadBollingerBands`, `TreynorRatio`) already guard finite input and are untouched. Scalar (169 files) and candle (`Candle::new`) inputs were already protected.

## Severity split

- **7 running-sum indicators** \(permanent corruption — NaN entered `Σ` and stayed until `reset()`\): `Beta`, `BetaNeutralSpread`, `Cointegration`, `HasbrouckInformationShare`, `PearsonCorrelation`, `RollingCorrelation`, `RollingCovariance`.
- **9 buffer-recompute indicators** \(transient — NaN cleared once evicted, but still surfaced in the reading\): `DistanceSsd`, `GrangerCausality`, `KendallTau`, `LeadLagCrossCorrelation`, `OuHalfLife`, `SpearmanCorrelation`, `SpreadAr1Coefficient`, `SpreadHurst`, `VarianceRatio`.

## Fix

Add the established finite-input guard (the same pattern `Alpha` already uses) as the first step of `update()`, before any window or sum mutation. Signature unchanged → bindings re-export unchanged, no binding regen needed; core-only.

Each indicator gains a `non_finite_input_returns_none` test that exercises the guard (NaN and Inf, covering both `||` operands) and proves a clean warmup is unaffected by the rejected ticks.

Split into two commits by severity class.

## Verification

- `cargo fmt --all` clean
- `cargo test --workspace --all-features` — 4225 core tests pass (+16 new)
- `cargo clippy --workspace --all-targets --all-features -- -D warnings` clean
2026-06-11 03:01:24 +02:00
kingchenc 7e51f31f02 sync-about: propagate published version into Java install snippets (#250)
## Problem

Java is the only target whose install snippet pins a version (the Maven coordinate). Every other language exposes its version through an api/*.md `Latest:` line or the docs published-versions table, both of which `sync-about.yml` already rewrites on each `v*` tag. The Java `<version>` tag and the `org.wickra:wickra:<v>` Gradle coordinate were never wired in, so they drifted:

- `webpage/index.md` install tab → stuck on `0.8.0`
- `webpage/api/java.md` → stuck on `0.7.9`
- `wickra-docs/Quickstart-Java.md` (Maven `<version>` + Gradle coord) → stuck on `0.7.9`

while the published version is already `0.8.3`.

## Fix

Extend the webpage and docs version-sync steps to rewrite the Maven `<version>` tag and the Gradle coordinate to the released `${version}`, and add `index.md` / `Quickstart-Java.md` to the respective commits. Java publishes on every release, so it tracks the same version as the rest — future releases self-heal these snippets.

Patterns dry-run-verified against the live files (all three resolve to `0.8.3`). The current drift is corrected directly in companion PRs on the webpage and wickra-docs repos.
2026-06-11 01:38:54 +02:00
79 changed files with 1576 additions and 82 deletions
+25 -2
View File
@@ -34,6 +34,7 @@ jobs:
rust:
name: Rust ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -104,6 +105,7 @@ jobs:
examples-smoke:
name: Examples (syntax smoke)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -193,6 +195,7 @@ jobs:
clippy-bindings:
name: Clippy bindings
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -276,6 +279,7 @@ jobs:
msrv:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -324,6 +328,7 @@ jobs:
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -365,6 +370,7 @@ jobs:
supply-chain:
name: Supply-chain (cargo-deny)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -383,6 +389,7 @@ jobs:
fuzz-smoke:
name: Fuzz (smoke)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -436,6 +443,7 @@ jobs:
python:
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -517,6 +525,7 @@ jobs:
wasm:
name: WASM build
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -560,6 +569,7 @@ jobs:
node:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -618,13 +628,22 @@ jobs:
# exist yet for win32-x64-msvc).
run: npx napi build --platform --release
# The Node test process has wedged on macOS runners (the step hung for
# 1h+ while the same tests passed in ~1.5 min elsewhere). Wrap it so a
# hung attempt is killed after 6 min and retried once, rather than
# running up to the job-level backstop. A normal run is under a minute.
- name: Run Node tests
working-directory: bindings/node
run: node --test __tests__/
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 6
max_attempts: 2
command: cd bindings/node && node --test __tests__/
shell: bash
c-abi:
name: C ABI on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -680,6 +699,7 @@ jobs:
csharp:
name: C# on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -730,6 +750,7 @@ jobs:
go:
name: Go on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -852,6 +873,7 @@ jobs:
r:
name: R on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
@@ -923,6 +945,7 @@ jobs:
java:
name: Java on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
strategy:
fail-fast: false
matrix:
+16 -2
View File
@@ -341,13 +341,19 @@ jobs:
# are never matched.
sed -i -E "s/^(\| \[?(crates\.io|PyPI|npm|NuGet|Maven Central|Go|r-universe)\]?.*\| )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" overview.md
sed -i -E "s/(published crate is at version .)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" Quickstart-Rust.md
# Quickstart-Java is the only quickstart whose install block pins a
# version (the Maven `<version>` tag and the `org.wickra:wickra:<v>`
# Gradle coordinate). Java publishes on every release, so it tracks the
# same ${version}.
sed -i -E "s|<version>[0-9]+\.[0-9]+\.[0-9]+</version>|<version>${version}</version>|" Quickstart-Java.md
sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" Quickstart-Java.md
if git diff --quiet; then
echo "Docs version already at ${version}."
exit 0
fi
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add overview.md Quickstart-Rust.md
git add overview.md Quickstart-Rust.md Quickstart-Java.md
git commit -m "chore: sync published version to ${version}"
if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
@@ -433,6 +439,14 @@ jobs:
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
# Java is the only target whose install snippet pins a version: the
# Maven `<version>` tag in the api/java.md dependency block and the
# home-page install tab in index.md, plus any `org.wickra:wickra:<v>`
# Gradle coordinate. Java publishes on every release, so it tracks the
# same ${version} as the rest. (Other languages' api/*.md carry a
# "Latest:" line handled above; Java uses the XML snippet instead.)
sed -i -E "s|<version>[0-9]+\.[0-9]+\.[0-9]+</version>|<version>${version}</version>|" api/java.md index.md
sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" api/java.md index.md
# 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 —
@@ -451,7 +465,7 @@ jobs:
fi
git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com"
git add api/*.md .vitepress/config.ts package.json package-lock.json
git add api/*.md index.md .vitepress/config.ts package.json package-lock.json
git commit -m "chore: sync published version to ${version}"
if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
+16 -13
View File
@@ -7,7 +7,7 @@ for the day-to-day workflow.
## Workspace layout
Wickra is a Cargo workspace of three Rust crates plus three binding crates.
Wickra is a Cargo workspace of three Rust crates plus four binding crates.
The split is deliberate: every concern that one user might want to disable
or replace lives behind a separate crate boundary.
@@ -20,7 +20,7 @@ or replace lives behind a separate crate boundary.
┌───────────▼──────────┐ ┌──────────▼─────────┐
│ wickra-core │ │ wickra-data │
│ indicator engine │ │ i/o + aggregation │
│ • 214 indicators │ │ • CSV reader │
│ • 514 indicators │ │ • CSV reader │
│ • Indicator trait │ │ • Tick aggregator │
│ • BatchExt impl │ │ • Resampler │
│ • OHLCV / Candle │ │ • Live feeds │
@@ -203,14 +203,17 @@ typed object arrays for Node/WASM).
A handful of indicators need care beyond naive accumulation:
- **Welford's online variance** is used in `StdDev`, `Variance`, `ZScore`,
`BollingerBands`, and several others. Standard sum-of-squares is
catastrophically lossy for low-variance inputs; Welford's recurrence
keeps O(eps) error.
- **Kahan summation** is used wherever rolling sums could span > 1e6
elements without resetting — currently only Hurst-exponent's R/S
chunks. Most rolling sums are bounded by the window size and don't need
it.
- **Rolling variance is running-sum, not Welford.** The sliding-window
variance family — `StdDev`, `Variance`, `ZScore`, `Bollinger` — keeps
running `Σx` and `Σx²` over the window and reports `var = Σx²/n mean²`,
clamping to zero the tiny negative values floating-point cancellation can
produce. `Bollinger` periodically reseeds its `Σx²` from the live window
so error cannot accumulate over a long stream. Welford's online algorithm
(an incremental `M2` accumulator) does **not** transfer cleanly to a
sliding window — removing the oldest point from `M2` is numerically
unstable — so it is used only where the statistic is *not* a fixed
window: `IntradayVolatilityProfile` and `SeasonalZScore` accumulate
per-bucket variance that way.
- **Logarithm bases** matter for some indicators (Hurst, MFI). Wickra
uses natural log everywhere unless the reference math explicitly
requires `log10` or `log2` — and then it documents the choice in the
@@ -327,9 +330,9 @@ re-discovering them.
- **`FAMILIES` (from PR #60) is hand-maintained.** Adding a new
indicator requires a separate entry in `FAMILIES`. The
`total_count_matches_expected` test will fail if you forget.
- **WASM does not have automated tests yet.** Smoke-validated only
through the manual examples. Adding `wasm-bindgen-test` coverage is
on the roadmap.
- **WASM is covered by `wasm-bindgen-test`.** `bindings/wasm/src/lib.rs`
carries 21 in-crate tests (run under `wasm-pack test` in CI), in
addition to the manual browser examples.
For the high-level project goals see [`ROADMAP.md`](ROADMAP.md); for
day-to-day contribution mechanics see [`CONTRIBUTING.md`](CONTRIBUTING.md).
+33 -1
View File
@@ -5,6 +5,37 @@ All notable changes to Wickra are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.8.4] - 2026-06-11
### Fixed
- A single non-finite (NaN/inf) tick no longer poisons indicator state.
The 16 pairwise running-sum/buffer indicators fixed first (`Beta`,
`BetaNeutralSpread`, `Cointegration`, `HasbrouckInformationShare`,
`PearsonCorrelation`, `RollingCorrelation`, `RollingCovariance`,
`DistanceSsd`, `GrangerCausality`, `KendallTau`, `LeadLagCrossCorrelation`,
`OuHalfLife`, `SpearmanCorrelation`, `SpreadAr1Coefficient`, `SpreadHurst`,
`VarianceRatio`) were joined by 38 more scalar/pairwise indicators the new
property harness surfaced (the linear-regression family, rolling quantiles
and IQR, `Variance`/`StdDev`-derived stats, `Kurtosis`/`Skewness`, the
trailing stops, `KalmanHedgeRatio`, `SpreadBollingerBands`, and more). Every
`f64` / `(f64, f64)` indicator now rejects non-finite input and returns
`None`, matching the streaming-robustness guarantee — and the harness enforces
it going forward.
### Added
- Catalogue-wide property-based invariant harness
(`crates/wickra-core/tests/invariants.rs`) asserting `batch == streaming`,
`reset == fresh`, and non-finite-input rejection for every indicator and
bar-builder.
### Changed
- CI: every job now has a runtime cap and the historically flaky Node test step
auto-retries, so a wedged runner fails fast instead of hanging for hours.
- Documentation accuracy fixes in `SECURITY.md`, `ARCHITECTURE.md`, and
`THREAT_MODEL.md` (supported version, indicator count, WASM test coverage,
numerical-stability notes, and the C-ABI panic strategy).
## [0.8.3] - 2026-06-10
### Added
- **Per-binding throughput benchmarks** — every target now ships a `throughput`
@@ -1511,7 +1542,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.8.3...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.8.4...HEAD
[0.8.4]: https://github.com/wickra-lib/wickra/compare/v0.8.3...v0.8.4
[0.8.3]: https://github.com/wickra-lib/wickra/compare/v0.8.2...v0.8.3
[0.8.2]: https://github.com/wickra-lib/wickra/compare/v0.8.1...v0.8.2
[0.8.1]: https://github.com/wickra-lib/wickra/compare/v0.8.0...v0.8.1
Generated
+9 -9
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"criterion",
"kand",
@@ -1967,14 +1967,14 @@ dependencies = [
[[package]]
name = "wickra-c"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"wickra-core",
]
[[package]]
name = "wickra-core"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"approx",
"proptest",
@@ -1984,7 +1984,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"approx",
"csv",
@@ -2001,7 +2001,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"serde_json",
"tokio",
@@ -2011,7 +2011,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"napi",
"napi-build",
@@ -2021,7 +2021,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"numpy",
"pyo3",
@@ -2030,7 +2030,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.8.3"
version = "0.8.4"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -14,7 +14,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.8.3"
version = "0.8.4"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -26,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.8.3" }
wickra-core = { path = "crates/wickra-core", version = "0.8.4" }
thiserror = "2"
rayon = "1.10"
+4 -1
View File
@@ -334,7 +334,10 @@ Every layer is covered; run the suites with the commands in
- `wickra-core`: unit tests per indicator — textbook reference values
(Wilder RSI, Bollinger Bands, MACD, ATR, Stochastic), `batch == streaming`
equivalence, `reset` semantics, NaN/Inf handling, and property tests.
equivalence, `reset` semantics, NaN/Inf handling, and property tests. A
catalogue-wide property harness (`tests/invariants.rs`) additionally asserts
`batch == streaming`, `reset == fresh`, and non-finite-input rejection for
**every** indicator and bar-builder.
- `wickra-data`: unit tests for CSV decoding, the tick aggregator, the
resampler, and the Binance payload parser.
- `bindings/python`: pytest covering smoke checks, streaming/batch
+3 -3
View File
@@ -2,13 +2,13 @@
## Supported versions
Wickra is pre-1.0. Security fixes are applied to the latest released `0.5.x`
Wickra is pre-1.0. Security fixes are applied to the latest released `0.8.4`
version only; please upgrade to the newest release before reporting an issue.
| Version | Supported |
| --- | --- |
| 0.5.x (latest) | :white_check_mark: |
| older 0.5.x | :x: |
| 0.8.4 (latest) | :white_check_mark: |
| < 0.8.4 | :x: |
## Reporting a vulnerability
+1 -1
View File
@@ -33,7 +33,7 @@ small.
| Threat | Mitigation |
| --- | --- |
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and is built with `panic = "abort"` so a panic terminates the process deterministically instead of unwinding across the FFI boundary (which would be undefined behaviour). A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
| 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. |
+1 -1
View File
@@ -11,7 +11,7 @@
<!-- NuGet package metadata -->
<PackageId>Wickra</PackageId>
<Version>0.8.3</Version>
<Version>0.8.4</Version>
<Authors>kingchenc</Authors>
<Description>High-performance streaming technical-analysis indicators (514 indicators) for .NET, backed by the native Rust core via the Wickra C ABI.</Description>
<PackageLicenseExpression>MIT OR Apache-2.0</PackageLicenseExpression>
+2 -2
View File
@@ -30,14 +30,14 @@ Maven:
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.3</version>
<version>0.8.4</version>
</dependency>
```
Gradle:
```kotlin
implementation("org.wickra:wickra:0.8.3")
implementation("org.wickra:wickra:0.8.4")
```
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.3</version>
<version>0.8.4</version>
<packaging>jar</packaging>
<name>Wickra</name>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-arm64-gnu",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-x64-gnu.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-arm64-msvc",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-arm64-msvc.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-x64-msvc",
"version": "0.8.3",
"version": "0.8.4",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.8.3",
"version": "0.8.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.8.3",
"version": "0.8.4",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-linux-x64-gnu": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3"
"wickra-darwin-arm64": "0.8.4",
"wickra-darwin-x64": "0.8.4",
"wickra-linux-arm64-gnu": "0.8.4",
"wickra-linux-x64-gnu": "0.8.4",
"wickra-win32-arm64-msvc": "0.8.4",
"wickra-win32-x64-msvc": "0.8.4"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.4.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.4.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.4.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.4.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.4.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.3.tgz",
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.4.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.8.3",
"version": "0.8.4",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>",
"main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-darwin-arm64": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3"
"wickra-linux-x64-gnu": "0.8.4",
"wickra-linux-arm64-gnu": "0.8.4",
"wickra-darwin-x64": "0.8.4",
"wickra-darwin-arm64": "0.8.4",
"wickra-win32-x64-msvc": "0.8.4",
"wickra-win32-arm64-msvc": "0.8.4"
},
"scripts": {
"build": "napi build --platform --release",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.8.3"
version = "0.8.4"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+1 -1
View File
@@ -1,7 +1,7 @@
Package: wickra
Type: Package
Title: Streaming-First Technical Indicators
Version: 0.8.3
Version: 0.8.4
Authors@R: person("Wickra contributors", role = c("aut", "cre"), email = "support@wickra.org")
Description: R bindings for the Wickra technical-analysis library over its C ABI
hub. Exposes 514 indicators, each an O(1) streaming state machine shared with
@@ -84,6 +84,9 @@ impl Indicator for Autocorrelation {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
+14
View File
@@ -92,6 +92,9 @@ impl Indicator for Beta {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
let (oa, ob) = self.window.pop_front().expect("non-empty");
self.sum_a -= oa;
@@ -225,4 +228,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut b = Beta::new(3).unwrap();
assert_eq!(b.update((f64::NAN, 1.0)), None);
assert_eq!(b.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(b.update((1.0, 2.0)), None);
assert_eq!(b.update((2.0, 5.0)), None);
assert!(b.update((3.0, 7.0)).is_some());
}
}
@@ -88,6 +88,9 @@ impl Indicator for BetaNeutralSpread {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
let (oa, ob) = self.window.pop_front().expect("non-empty");
self.sum_a -= oa;
@@ -244,4 +247,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| s.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut s = BetaNeutralSpread::new(3).unwrap();
assert_eq!(s.update((f64::NAN, 1.0)), None);
assert_eq!(s.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(s.update((1.0, 2.0)), None);
assert_eq!(s.update((2.0, 5.0)), None);
assert!(s.update((3.0, 7.0)).is_some());
}
}
@@ -106,6 +106,9 @@ impl Indicator for BomarBands {
type Output = BomarBandsOutput;
fn update(&mut self, value: f64) -> Option<BomarBandsOutput> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
+3
View File
@@ -60,6 +60,9 @@ impl Indicator for Cfo {
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return None;
}
let forecast = self.linreg.update(input)?;
// Hold the previous value if the close is zero — the percentage form
// is undefined and a return of inf would propagate badly.
@@ -71,6 +71,9 @@ impl Indicator for CoefficientOfVariation {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
@@ -116,6 +116,9 @@ impl Indicator for Cointegration {
fn update(&mut self, input: (f64, f64)) -> Option<CointegrationOutput> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
let (oa, ob) = self.window.pop_front().expect("non-empty");
self.sum_a -= oa;
@@ -443,4 +446,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| c.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut c = Cointegration::new(4, 0).unwrap();
assert_eq!(c.update((f64::NAN, 1.0)), None);
assert_eq!(c.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(c.update((1.0, 2.0)), None);
assert_eq!(c.update((2.0, 5.0)), None);
assert_eq!(c.update((3.0, 7.0)), None);
assert!(c.update((4.0, 11.0)).is_some());
}
}
@@ -96,6 +96,9 @@ impl Indicator for DetrendedStdDev {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let y0 = self.window.pop_front().expect("non-empty");
self.sum_xy = self.sum_xy - self.sum_y + y0;
@@ -76,6 +76,9 @@ impl Indicator for DistanceSsd {
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
if !input.0.is_finite() || !input.1.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -232,4 +235,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| d.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut d = DistanceSsd::new(3).unwrap();
assert_eq!(d.update((f64::NAN, 1.0)), None);
assert_eq!(d.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(d.update((1.0, 1.0)), None);
assert_eq!(d.update((2.0, 4.0)), None);
assert!(d.update((3.0, 9.0)).is_some());
}
}
@@ -77,6 +77,9 @@ impl Indicator for Expectancy {
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("window is non-empty");
self.sum -= old;
@@ -96,6 +96,9 @@ impl Indicator for GrangerCausality {
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
if !input.0.is_finite() || !input.1.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -332,4 +335,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| g.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut g = GrangerCausality::new(5, 1).unwrap();
assert_eq!(g.update((f64::NAN, 1.0)), None);
assert_eq!(g.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
for t in 0..4 {
assert_eq!(g.update((f64::from(t), f64::from(t) * 0.5)), None);
}
assert!(g.update((4.0, 2.0)).is_some());
}
}
@@ -87,6 +87,9 @@ impl Indicator for HasbrouckInformationShare {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (x, y) = input;
if !x.is_finite() || !y.is_finite() {
return None;
}
let Some((px, py)) = self.prev else {
self.prev = Some((x, y));
return None;
@@ -248,4 +251,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut h = HasbrouckInformationShare::new(2).unwrap();
assert_eq!(h.update((f64::NAN, 1.0)), None);
assert_eq!(h.update((1.0, f64::INFINITY)), None);
// First finite tick seeds prev; two more returns fill the window.
assert_eq!(h.update((1.0, 1.0)), None);
assert_eq!(h.update((2.0, 3.0)), None);
assert!(h.update((3.0, 4.0)).is_some());
}
}
@@ -138,6 +138,9 @@ impl Indicator for HurstExponent {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -116,6 +116,9 @@ impl Indicator for KalmanHedgeRatio {
fn update(&mut self, input: (f64, f64)) -> Option<KalmanHedgeRatioOutput> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
// Predicted state covariance: add the transition noise to the diagonal
// (the very first observation starts from a zero prior).
let mut cov_pred = self.cov;
@@ -130,6 +130,9 @@ impl Indicator for KendallTau {
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
if !input.0.is_finite() || !input.1.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -293,4 +296,14 @@ mod tests {
let v = k.update((3.0, 3.0)).unwrap();
assert!((-1.0..=1.0).contains(&v), "got {v}");
}
#[test]
fn non_finite_input_returns_none() {
let mut k = KendallTau::new(2).unwrap();
assert_eq!(k.update((f64::NAN, 1.0)), None);
assert_eq!(k.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(k.update((1.0, 2.0)), None);
assert!(k.update((2.0, 5.0)).is_some());
}
}
@@ -80,6 +80,9 @@ impl Indicator for Kurtosis {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
let sq = old * old;
@@ -152,6 +152,9 @@ impl Indicator for LeadLagCrossCorrelation {
fn update(&mut self, input: (f64, f64)) -> Option<LeadLagCrossCorrelationOutput> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.a_buf.len() == self.len {
self.a_buf.pop_front();
self.b_buf.pop_front();
@@ -321,4 +324,17 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| ll.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
// len = window + 2*max_lag = 2 + 2 = 4 finite ticks fill the buffers.
let mut ll = LeadLagCrossCorrelation::new(2, 1).unwrap();
assert_eq!(ll.update((f64::NAN, 1.0)), None);
assert_eq!(ll.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(ll.update((1.0, 2.0)), None);
assert_eq!(ll.update((2.0, 1.0)), None);
assert_eq!(ll.update((3.0, 4.0)), None);
assert!(ll.update((4.0, 2.0)).is_some());
}
}
@@ -99,6 +99,9 @@ impl Indicator for LinearRegression {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
// Sliding phase: pop the oldest, then shift every remaining index
// down by 1 in the running `sum_xy`. The identity
@@ -57,6 +57,9 @@ impl Indicator for LinRegAngle {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
self.slope.update(value).map(|s| s.atan().to_degrees())
}
@@ -98,6 +98,9 @@ impl Indicator for LinRegChannel {
type Output = LinRegChannelOutput;
fn update(&mut self, value: f64) -> Option<LinRegChannelOutput> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -77,6 +77,9 @@ impl Indicator for LinRegIntercept {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let y0 = self.window.pop_front().expect("non-empty");
self.sum_xy = self.sum_xy - self.sum_y + y0;
@@ -87,6 +87,9 @@ impl Indicator for LinRegSlope {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
// Sliding-window identity: when the window slides one step forward
// the indices `x` for every kept entry shift down by 1, so
@@ -89,6 +89,9 @@ impl Indicator for MedianAbsoluteDeviation {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -96,6 +96,9 @@ impl Indicator for MedianChannel {
type Output = MedianChannelOutput;
fn update(&mut self, value: f64) -> Option<MedianChannelOutput> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -59,6 +59,9 @@ impl Indicator for MidPoint {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -80,6 +80,9 @@ impl Indicator for OuHalfLife {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -242,4 +245,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| hl.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut hl = OuHalfLife::new(4).unwrap();
assert_eq!(hl.update((f64::NAN, 1.0)), None);
assert_eq!(hl.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(hl.update((1.0, 0.0)), None);
assert_eq!(hl.update((2.0, 0.0)), None);
assert_eq!(hl.update((3.0, 0.0)), None);
assert!(hl.update((4.0, 0.0)).is_some());
}
}
@@ -89,6 +89,9 @@ impl Indicator for PearsonCorrelation {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (x, y) = input;
if !x.is_finite() || !y.is_finite() {
return None;
}
if self.window.len() == self.period {
let (ox, oy) = self.window.pop_front().expect("non-empty");
self.sum_x -= ox;
@@ -243,4 +246,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut p = PearsonCorrelation::new(3).unwrap();
assert_eq!(p.update((f64::NAN, 1.0)), None);
assert_eq!(p.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(p.update((1.0, 2.0)), None);
assert_eq!(p.update((2.0, 5.0)), None);
assert!(p.update((3.0, 7.0)).is_some());
}
}
@@ -74,6 +74,9 @@ impl Indicator for PercentageTrailingStop {
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if !close.is_finite() {
return None;
}
let step = close.abs() * self.percent / 100.0;
let stop = match self.prev_stop {
Some(prev) => {
@@ -82,6 +82,9 @@ impl Indicator for PolarizedFractalEfficiency {
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if !close.is_finite() {
return None;
}
if let Some(prev) = self.prev_close {
let diff = close - prev;
let segment = diff.mul_add(diff, 1.0).sqrt();
@@ -80,6 +80,9 @@ impl Indicator for QuartileBands {
type Output = QuartileBandsOutput;
fn update(&mut self, value: f64) -> Option<QuartileBandsOutput> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -95,6 +95,9 @@ impl Indicator for RSquared {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let y0 = self.window.pop_front().expect("non-empty");
self.sum_xy = self.sum_xy - self.sum_y + y0;
@@ -74,6 +74,9 @@ impl Indicator for RenkoTrailingStop {
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if !close.is_finite() {
return None;
}
let anchor = match self.anchor {
Some(prev) => {
if self.long {
@@ -90,6 +90,9 @@ impl Indicator for RollingCorrelation {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (x, y) = input;
if !x.is_finite() || !y.is_finite() {
return None;
}
let Some((px, py)) = self.prev else {
// First level in each channel: store it, no return yet.
self.prev = Some((x, y));
@@ -272,4 +275,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut rc = RollingCorrelation::new(2).unwrap();
assert_eq!(rc.update((f64::NAN, 1.0)), None);
assert_eq!(rc.update((1.0, f64::INFINITY)), None);
// First finite tick seeds prev; two more returns fill the window.
assert_eq!(rc.update((1.0, 1.0)), None);
assert_eq!(rc.update((2.0, 3.0)), None);
assert!(rc.update((3.0, 5.0)).is_some());
}
}
@@ -86,6 +86,9 @@ impl Indicator for RollingCovariance {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (x, y) = input;
if !x.is_finite() || !y.is_finite() {
return None;
}
let Some((px, py)) = self.prev else {
self.prev = Some((x, y));
return None;
@@ -240,4 +243,15 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut rc = RollingCovariance::new(2).unwrap();
assert_eq!(rc.update((f64::NAN, 1.0)), None);
assert_eq!(rc.update((1.0, f64::INFINITY)), None);
// First finite tick seeds prev; two more returns fill the window.
assert_eq!(rc.update((1.0, 1.0)), None);
assert_eq!(rc.update((2.0, 3.0)), None);
assert!(rc.update((3.0, 5.0)).is_some());
}
}
@@ -71,6 +71,9 @@ impl Indicator for RollingIqr {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -70,6 +70,9 @@ impl Indicator for RollingPercentileRank {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -104,6 +104,9 @@ impl Indicator for RollingQuantile {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -79,6 +79,9 @@ impl Indicator for Skewness {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
@@ -123,6 +123,9 @@ impl Indicator for SpearmanCorrelation {
type Output = f64;
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
if !input.0.is_finite() || !input.1.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -311,4 +314,14 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut s = SpearmanCorrelation::new(2).unwrap();
assert_eq!(s.update((f64::NAN, 1.0)), None);
assert_eq!(s.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(s.update((1.0, 2.0)), None);
assert!(s.update((2.0, 5.0)).is_some());
}
}
@@ -89,6 +89,9 @@ impl Indicator for SpreadAr1Coefficient {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -248,4 +251,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| ar1.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut ar1 = SpreadAr1Coefficient::new(4).unwrap();
assert_eq!(ar1.update((f64::NAN, 1.0)), None);
assert_eq!(ar1.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(ar1.update((1.0, 0.0)), None);
assert_eq!(ar1.update((2.0, 0.0)), None);
assert_eq!(ar1.update((3.0, 0.0)), None);
assert!(ar1.update((4.0, 0.0)).is_some());
}
}
@@ -116,6 +116,9 @@ impl Indicator for SpreadBollingerBands {
fn update(&mut self, input: (f64, f64)) -> Option<SpreadBollingerBandsOutput> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
let spread = a - b;
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
@@ -84,6 +84,9 @@ impl Indicator for SpreadHurst {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -268,4 +271,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut h = SpreadHurst::new(8).unwrap();
assert_eq!(h.update((f64::NAN, 1.0)), None);
assert_eq!(h.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
for t in 0..7 {
assert_eq!(h.update((f64::from(t), 0.0)), None);
}
assert!(h.update((7.0, 0.0)).is_some());
}
}
@@ -93,6 +93,9 @@ impl Indicator for StandardError {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
// Slide: pop oldest, shift indices, then push the new value at index n 1.
let y0 = self.window.pop_front().expect("non-empty");
@@ -106,6 +106,9 @@ impl Indicator for StandardErrorBands {
type Output = StandardErrorBandsOutput;
fn update(&mut self, value: f64) -> Option<StandardErrorBandsOutput> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -84,6 +84,9 @@ impl Indicator for StepTrailingStop {
type Output = f64;
fn update(&mut self, close: f64) -> Option<f64> {
if !close.is_finite() {
return None;
}
let stop = match self.prev_stop {
Some(prev) => {
if self.long {
@@ -72,6 +72,9 @@ impl Indicator for TrendLabel {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -75,6 +75,9 @@ impl Indicator for TrendStrengthIndex {
type Output = f64;
fn update(&mut self, price: f64) -> Option<f64> {
if !price.is_finite() {
return None;
}
self.buf.push_back(price);
if self.buf.len() > self.period {
self.buf.pop_front();
+3
View File
@@ -80,6 +80,9 @@ impl Indicator for Tsf {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let y0 = self.window.pop_front().expect("non-empty");
self.sum_xy = self.sum_xy - self.sum_y + y0;
@@ -73,6 +73,9 @@ impl Indicator for TsfOscillator {
type Output = f64;
fn update(&mut self, input: f64) -> Option<f64> {
if !input.is_finite() {
return None;
}
let forecast = self.tsf.update(input)?;
// Hold the previous value if the close is zero — the percentage form
// is undefined and a return of inf would propagate badly.
@@ -70,6 +70,9 @@ impl Indicator for Variance {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
@@ -98,6 +98,9 @@ impl Indicator for VarianceRatio {
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
let (a, b) = input;
if !a.is_finite() || !b.is_finite() {
return None;
}
if self.window.len() == self.period {
self.window.pop_front();
}
@@ -264,4 +267,16 @@ mod tests {
let streamed: Vec<_> = pairs.iter().map(|p| vr.update(*p)).collect();
assert_eq!(batch, streamed);
}
#[test]
fn non_finite_input_returns_none() {
let mut vr = VarianceRatio::new(4, 2).unwrap();
assert_eq!(vr.update((f64::NAN, 1.0)), None);
assert_eq!(vr.update((1.0, f64::INFINITY)), None);
// The rejected ticks leave no trace: a fresh window still warms up.
assert_eq!(vr.update((1.0, 0.0)), None);
assert_eq!(vr.update((2.0, 0.0)), None);
assert_eq!(vr.update((3.0, 0.0)), None);
assert!(vr.update((4.0, 0.0)).is_some());
}
}
@@ -68,6 +68,9 @@ impl Indicator for VerticalHorizontalFilter {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.closes.len() == self.period {
self.closes.pop_front();
}
@@ -66,6 +66,9 @@ impl Indicator for WinRate {
type Output = f64;
fn update(&mut self, ret: f64) -> Option<f64> {
if !ret.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("window is non-empty");
if old > 0.0 {
@@ -66,6 +66,9 @@ impl Indicator for ZScore {
type Output = f64;
fn update(&mut self, value: f64) -> Option<f64> {
if !value.is_finite() {
return None;
}
if self.window.len() == self.period {
let old = self.window.pop_front().expect("non-empty");
self.sum -= old;
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra.examples</groupId>
<artifactId>wickra-examples</artifactId>
<version>0.8.3</version>
<version>0.8.4</version>
<packaging>jar</packaging>
<name>Wickra Java examples</name>
@@ -21,7 +21,7 @@
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.3</version>
<version>0.8.4</version>
</dependency>
<!-- Only the network examples (fetch_btcusdt) parse JSON. -->
<dependency>
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.8.3",
"version": "0.8.4",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-linux-x64-gnu": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3"
"wickra-darwin-arm64": "0.8.4",
"wickra-darwin-x64": "0.8.4",
"wickra-linux-arm64-gnu": "0.8.4",
"wickra-linux-x64-gnu": "0.8.4",
"wickra-win32-arm64-msvc": "0.8.4",
"wickra-win32-x64-msvc": "0.8.4"
}
},
"node_modules/wickra": {