Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 395a2289f4 | |||
| 9973d1a6bf | |||
| cb216668ee | |||
| 20c0002f8e | |||
| 99497eb062 | |||
| 7e51f31f02 |
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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. |
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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,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,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": [
|
||||
|
||||
Generated
+20
-20
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Generated
+7
-7
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user