Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 806ae22abe | |||
| fb9c39d4cd | |||
| b1653e2107 | |||
| 32e18cb3a3 | |||
| dc0c3d1736 | |||
| ad1231cde1 | |||
| 7ebb60b60a | |||
| d7cb771a28 | |||
| 3e5a19c94a | |||
| 8bfe24ac1d | |||
| 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:
|
||||
@@ -904,6 +926,16 @@ jobs:
|
||||
R CMD INSTALL bindings/r
|
||||
Rscript -e 'library(testthat); library(wickra); test_dir("bindings/r/tests/testthat", stop_on_failure = TRUE)'
|
||||
|
||||
- name: Build the vignette code
|
||||
shell: bash
|
||||
# The getting-started vignette runs at R CMD check time on r-universe /
|
||||
# CRAN (with pandoc); this job only INSTALLs, so execute the vignette's R
|
||||
# chunks here (knit, no pandoc needed) to catch a broken example before it
|
||||
# reaches the published build.
|
||||
run: |
|
||||
Rscript -e 'install.packages("knitr", repos = Sys.getenv("RSPM", unset = "https://cloud.r-project.org"))'
|
||||
Rscript -e 'knitr::knit("bindings/r/vignettes/getting-started.Rmd", output = tempfile(fileext = ".md"), quiet = TRUE); cat("vignette code OK\n")'
|
||||
|
||||
- name: Run the offline R examples
|
||||
shell: bash
|
||||
# No loader-path exports: the installed package is self-contained (bundled
|
||||
@@ -923,6 +955,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).
|
||||
|
||||
+67
-1
@@ -5,6 +5,67 @@ 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.8] - 2026-06-11
|
||||
### Fixed
|
||||
- R binding: declare `Depends: R (>= 2.10)`, clearing the `R CMD check` warning
|
||||
("package needs dependence on R (>= 2.10)") that the bundled, lazy-loaded
|
||||
`sample_ohlcv` dataset triggers on r-universe / CRAN.
|
||||
|
||||
## [0.8.7] - 2026-06-11
|
||||
### Added
|
||||
- R binding: a *Getting started* vignette and a synthetic `sample_ohlcv` example
|
||||
dataset, giving new users a runnable, self-contained walkthrough and populating
|
||||
the R-universe Articles and Datasets tabs. The vignette's code is exercised in
|
||||
CI so a broken example is caught before the published build.
|
||||
|
||||
## [0.8.6] - 2026-06-11
|
||||
### Changed
|
||||
- Package registry metadata for better discoverability:
|
||||
- R (R-universe): added the R-universe URL and `X-schema.org-keywords` to the
|
||||
R `DESCRIPTION`, plus a package logo at `bindings/r/man/figures/logo.png`
|
||||
(pkgdown convention).
|
||||
- Python (PyPI): added a `Documentation` project URL.
|
||||
- C# (NuGet): added a package icon via `PackageIcon`.
|
||||
|
||||
## [0.8.5] - 2026-06-11
|
||||
### Fixed
|
||||
- The R binding's golden-fixture parity test now skips gracefully when the shared
|
||||
`testdata/golden` fixtures are not bundled with the package — standalone
|
||||
r-universe / CRAN builds package only `bindings/r`, so the repo-root fixtures
|
||||
are unreachable there. The parity stays enforced by the repository CI, where
|
||||
the fixtures are present.
|
||||
|
||||
## [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 +1572,12 @@ 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.8...HEAD
|
||||
[0.8.8]: https://github.com/wickra-lib/wickra/compare/v0.8.7...v0.8.8
|
||||
[0.8.7]: https://github.com/wickra-lib/wickra/compare/v0.8.6...v0.8.7
|
||||
[0.8.6]: https://github.com/wickra-lib/wickra/compare/v0.8.5...v0.8.6
|
||||
[0.8.5]: https://github.com/wickra-lib/wickra/compare/v0.8.4...v0.8.5
|
||||
[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.8"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1967,14 +1967,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"wickra-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1984,7 +1984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -2001,7 +2001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2011,7 +2011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2021,7 +2021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2030,7 +2030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
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.8"
|
||||
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.8" }
|
||||
|
||||
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
|
||||
@@ -355,6 +358,12 @@ Every layer is covered; run the suites with the commands in
|
||||
- `bindings/java`: JUnit cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input) plus batch equivalence.
|
||||
|
||||
The four C-ABI bindings (C#, Go, Java, R) additionally replay a shared,
|
||||
language-neutral golden fixture (`testdata/golden/*.csv`, generated by
|
||||
`cargo run -p wickra-examples --bin gen_golden`) and assert exact parity with the
|
||||
Rust reference outputs across every archetype (SMA, EMA, RSI, ATR, MACD, ADX,
|
||||
Beta), catching FFI wiring bugs the math-only core tests cannot see.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome — issues, bug reports, ideas, and pull requests
|
||||
@@ -410,13 +419,13 @@ The library is provided **as is**, without warranty of any kind; see
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/wickra-lib/wickra/stargazers">
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=ffd866">
|
||||
<img alt="GitHub stars" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/stars.svg">
|
||||
</a>
|
||||
<a href="https://github.com/wickra-lib/wickra/network/members">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=78dce8">
|
||||
<img alt="GitHub forks" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/forks.svg">
|
||||
</a>
|
||||
<a href="https://github.com/wickra-lib/wickra/issues">
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=ff6188">
|
||||
<img alt="GitHub issues" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/issues.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
@@ -427,6 +436,6 @@ The library is provided **as is**, without warranty of any kind; see
|
||||
<p align="center">
|
||||
<a href="https://star-history.com/#wickra-lib/wickra&Date">
|
||||
<img alt="Wickra star history" width="640"
|
||||
src="https://api.star-history.com/svg?repos=wickra-lib/wickra&type=Date&theme=dark">
|
||||
src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/star-history.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
+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.8`
|
||||
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.8 (latest) | :white_check_mark: |
|
||||
| < 0.8.8 | :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. |
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-fixture parity: replay the shared <c>testdata/golden</c> input series
|
||||
/// through the C# FFI and assert every value matches the Rust reference output.
|
||||
/// Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
/// wiring bug (swapped parameter, wrong multi-output field) is caught.
|
||||
/// Fixtures are generated by <c>cargo run -p wickra-examples --bin gen_golden</c>.
|
||||
/// </summary>
|
||||
public class GoldenTests
|
||||
{
|
||||
private const double Tol = 1e-6;
|
||||
|
||||
private static string GoldenDir([CallerFilePath] string file = "") =>
|
||||
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
|
||||
|
||||
private static List<string[]> ReadCsv(string name)
|
||||
{
|
||||
var path = Path.Combine(GoldenDir(), name + ".csv");
|
||||
return File.ReadAllLines(path)
|
||||
.Skip(1) // header
|
||||
.Where(l => l.Length > 0)
|
||||
.Select(l => l.Split(','))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static double[][] Input()
|
||||
{
|
||||
return ReadCsv("input")
|
||||
.Select(r => r.Select(c => double.Parse(c, CultureInfo.InvariantCulture)).ToArray())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double Cell(string s) =>
|
||||
s == "nan" ? double.NaN : double.Parse(s, CultureInfo.InvariantCulture);
|
||||
|
||||
private static void AssertClose(double got, double want, int row, string field)
|
||||
{
|
||||
if (double.IsNaN(want))
|
||||
{
|
||||
Assert.True(double.IsNaN(got), $"row {row} {field}: expected warmup/NaN, got {got}");
|
||||
return;
|
||||
}
|
||||
var tol = Tol * Math.Max(1.0, Math.Abs(want));
|
||||
Assert.True(Math.Abs(got - want) <= tol, $"row {row} {field}: got {got}, want {want}");
|
||||
}
|
||||
|
||||
// --- scalar (close-driven) ------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData("sma")]
|
||||
[InlineData("ema")]
|
||||
[InlineData("rsi")]
|
||||
public void Scalar_MatchesGolden(string name)
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv(name);
|
||||
using var ind = (IDisposable)(name switch
|
||||
{
|
||||
"sma" => new Sma(14),
|
||||
"ema" => new Ema(14),
|
||||
"rsi" => new Rsi(14),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(name)),
|
||||
});
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var close = input[i][3];
|
||||
double got = ind switch
|
||||
{
|
||||
Sma s => s.Update(close),
|
||||
Ema e => e.Update(close),
|
||||
Rsi r => r.Update(close),
|
||||
_ => double.NaN,
|
||||
};
|
||||
AssertClose(got, Cell(expected[i][0]), i, name);
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle, single output ------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Candle_Atr_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("atr");
|
||||
using var atr = new Atr(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AssertClose(atr.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "atr");
|
||||
}
|
||||
}
|
||||
|
||||
// --- pairwise -------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Pairwise_Beta_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("beta");
|
||||
using var beta = new Beta(20);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
// generator fed (close, open)
|
||||
AssertClose(beta.Update(input[i][3], input[i][0]), Cell(expected[i][0]), i, "beta");
|
||||
}
|
||||
}
|
||||
|
||||
// --- scalar multi-output: MACD -------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Macd_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("macd");
|
||||
using var macd = new MacdIndicator(12, 26, 9);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
MacdOutput? got = macd.Update(input[i][3]);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.Macd, Cell(e[0]), i, "macd.macd");
|
||||
AssertClose(got.Value.Signal, Cell(e[1]), i, "macd.signal");
|
||||
AssertClose(got.Value.Histogram, Cell(e[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle multi-output: ADX --------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Adx_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("adx");
|
||||
using var adx = new Adx(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AdxOutput? got = adx.Update(o, h, l, c, v, i);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.PlusDi, Cell(e[0]), i, "adx.plus_di");
|
||||
AssertClose(got.Value.MinusDi, Cell(e[1]), i, "adx.minus_di");
|
||||
AssertClose(got.Value.Adx, Cell(e[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>Wickra</PackageId>
|
||||
<Version>0.8.3</Version>
|
||||
<Version>0.8.8</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>
|
||||
@@ -20,6 +20,7 @@
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>technical-analysis;indicators;trading;finance;streaming;ffi;native</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
@@ -42,6 +43,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,191 @@
|
||||
package wickra
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Golden-fixture parity: replay the shared testdata/golden input series through
|
||||
// the Go FFI and assert every value matches the Rust reference output. Where the
|
||||
// archetype test only checks finiteness, this pins exact values, catching wiring
|
||||
// bugs (swapped params, wrong multi-output field). Fixtures are generated by
|
||||
// `cargo run -p wickra-examples --bin gen_golden`.
|
||||
|
||||
const goldenTol = 1e-6
|
||||
|
||||
func readGolden(t *testing.T, name string) [][]string {
|
||||
t.Helper()
|
||||
f, err := os.Open("../../testdata/golden/" + name + ".csv")
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
var rows [][]string
|
||||
sc := bufio.NewScanner(f)
|
||||
first := true
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if first {
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, strings.Split(line, ","))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func goldenCell(s string) float64 {
|
||||
if s == "nan" {
|
||||
return math.NaN()
|
||||
}
|
||||
v, _ := strconv.ParseFloat(s, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func goldenInput(t *testing.T) [][]float64 {
|
||||
rows := readGolden(t, "input")
|
||||
out := make([][]float64, len(rows))
|
||||
for i, r := range rows {
|
||||
vals := make([]float64, len(r))
|
||||
for j, c := range r {
|
||||
vals[j] = goldenCell(c)
|
||||
}
|
||||
out[i] = vals
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertGoldenClose(t *testing.T, got, want float64, row int, field string) {
|
||||
t.Helper()
|
||||
if math.IsNaN(want) {
|
||||
if !math.IsNaN(got) {
|
||||
t.Errorf("row %d %s: expected warmup/NaN, got %v", row, field, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
tol := goldenTol * math.Max(1.0, math.Abs(want))
|
||||
if math.Abs(got-want) > tol {
|
||||
t.Errorf("row %d %s: got %v want %v", row, field, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenScalar(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
sma, err := NewSma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sma.Close()
|
||||
ema, err := NewEma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ema.Close()
|
||||
rsi, err := NewRsi(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsi.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
upd func(close float64) float64
|
||||
}{
|
||||
{"sma", sma.Update},
|
||||
{"ema", ema.Update},
|
||||
{"rsi", rsi.Update},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
exp := readGolden(t, tc.name)
|
||||
for i := range input {
|
||||
assertGoldenClose(t, tc.upd(input[i][3]), goldenCell(exp[i][0]), i, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAtr(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "atr")
|
||||
atr, err := NewAtr(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer atr.Close()
|
||||
for i := range input {
|
||||
got := atr.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "atr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenBeta(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "beta")
|
||||
beta, err := NewBeta(20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer beta.Close()
|
||||
for i := range input {
|
||||
// generator fed (close, open)
|
||||
assertGoldenClose(t, beta.Update(input[i][3], input[i][0]), goldenCell(exp[i][0]), i, "beta")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenMacd(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "macd")
|
||||
macd, err := NewMacdIndicator(12, 26, 9)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer macd.Close()
|
||||
for i := range input {
|
||||
out, ok := macd.Update(input[i][3])
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d macd: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d macd: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.Macd, goldenCell(exp[i][0]), i, "macd.macd")
|
||||
assertGoldenClose(t, out.Signal, goldenCell(exp[i][1]), i, "macd.signal")
|
||||
assertGoldenClose(t, out.Histogram, goldenCell(exp[i][2]), i, "macd.histogram")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAdx(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "adx")
|
||||
adx, err := NewAdx(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adx.Close()
|
||||
for i := range input {
|
||||
out, ok := adx.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d adx: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d adx: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.PlusDi, goldenCell(exp[i][0]), i, "adx.plus_di")
|
||||
assertGoldenClose(t, out.MinusDi, goldenCell(exp[i][1]), i, "adx.minus_di")
|
||||
assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx")
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,14 @@ Maven:
|
||||
<dependency>
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.8.3</version>
|
||||
<version>0.8.8</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Gradle:
|
||||
|
||||
```kotlin
|
||||
implementation("org.wickra:wickra:0.8.3")
|
||||
implementation("org.wickra:wickra:0.8.8")
|
||||
```
|
||||
|
||||
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.8</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Wickra</name>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.wickra;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Golden-fixture parity: replay the shared {@code testdata/golden} input series
|
||||
* through the Java FFI and assert every value matches the Rust reference output.
|
||||
* Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
* wiring bug (swapped parameter, wrong multi-output field) is caught. Fixtures
|
||||
* are generated by {@code cargo run -p wickra-examples --bin gen_golden}.
|
||||
*/
|
||||
class GoldenTests {
|
||||
private static final double TOL = 1e-6;
|
||||
|
||||
private static Path goldenDir() {
|
||||
File d = new File("").getAbsoluteFile();
|
||||
while (d != null) {
|
||||
File g = new File(d, "testdata/golden");
|
||||
if (g.isDirectory()) {
|
||||
return g.toPath();
|
||||
}
|
||||
d = d.getParentFile();
|
||||
}
|
||||
throw new IllegalStateException("testdata/golden not found from " + new File("").getAbsolutePath());
|
||||
}
|
||||
|
||||
private static List<String[]> readCsv(String name) throws Exception {
|
||||
List<String> lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
for (int i = 1; i < lines.size(); i++) { // skip header
|
||||
if (!lines.get(i).isEmpty()) {
|
||||
rows.add(lines.get(i).split(","));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static double cell(String s) {
|
||||
return s.equals("nan") ? Double.NaN : Double.parseDouble(s);
|
||||
}
|
||||
|
||||
private static double[][] input() throws Exception {
|
||||
List<String[]> rows = readCsv("input");
|
||||
double[][] out = new double[rows.size()][];
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
String[] r = rows.get(i);
|
||||
double[] v = new double[r.length];
|
||||
for (int j = 0; j < r.length; j++) {
|
||||
v[j] = Double.parseDouble(r[j]);
|
||||
}
|
||||
out[i] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void close(double got, double want, int row, String field) {
|
||||
if (Double.isNaN(want)) {
|
||||
assertTrue(Double.isNaN(got), "row " + row + " " + field + ": expected warmup/NaN, got " + got);
|
||||
return;
|
||||
}
|
||||
double tol = TOL * Math.max(1.0, Math.abs(want));
|
||||
assertTrue(Math.abs(got - want) <= tol, "row " + row + " " + field + ": got " + got + " want " + want);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scalarMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
try (Sma sma = new Sma(14)) {
|
||||
List<String[]> e = readCsv("sma");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(sma.update(in[i][3]), cell(e.get(i)[0]), i, "sma");
|
||||
}
|
||||
}
|
||||
try (Ema ema = new Ema(14)) {
|
||||
List<String[]> e = readCsv("ema");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(ema.update(in[i][3]), cell(e.get(i)[0]), i, "ema");
|
||||
}
|
||||
}
|
||||
try (Rsi rsi = new Rsi(14)) {
|
||||
List<String[]> e = readCsv("rsi");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(rsi.update(in[i][3]), cell(e.get(i)[0]), i, "rsi");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candleAtrMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("atr");
|
||||
try (Atr atr = new Atr(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(atr.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "atr");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pairwiseBetaMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("beta");
|
||||
try (Beta beta = new Beta(20)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
// generator fed (close, open)
|
||||
close(beta.update(in[i][3], in[i][0]), cell(e.get(i)[0]), i, "beta");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void macdMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("macd");
|
||||
try (MacdIndicator macd = new MacdIndicator(12, 26, 9)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
MacdOutput o = macd.update(in[i][3]);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " macd: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " macd: expected value");
|
||||
close(o.macd(), cell(row[0]), i, "macd.macd");
|
||||
close(o.signal(), cell(row[1]), i, "macd.signal");
|
||||
close(o.histogram(), cell(row[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void adxMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("adx");
|
||||
try (Adx adx = new Adx(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
AdxOutput o = adx.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " adx: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " adx: expected value");
|
||||
close(o.plusDi(), cell(row[0]), i, "adx.plus_di");
|
||||
close(o.minusDi(), cell(row[1]), i, "adx.minus_di");
|
||||
close(o.adx(), cell(row[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.8.3",
|
||||
"version": "0.8.8",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"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.8",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.8.3",
|
||||
"version": "0.8.8",
|
||||
"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.8",
|
||||
"wickra-darwin-x64": "0.8.8",
|
||||
"wickra-linux-arm64-gnu": "0.8.8",
|
||||
"wickra-linux-x64-gnu": "0.8.8",
|
||||
"wickra-win32-arm64-msvc": "0.8.8",
|
||||
"wickra-win32-x64-msvc": "0.8.8"
|
||||
}
|
||||
},
|
||||
"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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.8.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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.8.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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.8.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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.8.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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.8.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.8",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.8.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.8.3",
|
||||
"version": "0.8.8",
|
||||
"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.8",
|
||||
"wickra-linux-arm64-gnu": "0.8.8",
|
||||
"wickra-darwin-x64": "0.8.8",
|
||||
"wickra-darwin-arm64": "0.8.8",
|
||||
"wickra-win32-x64-msvc": "0.8.8",
|
||||
"wickra-win32-arm64-msvc": "0.8.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.8.3"
|
||||
version = "0.8.8"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
@@ -48,6 +48,7 @@ bench = [
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/wickra-lib/wickra"
|
||||
Documentation = "https://docs.wickra.org"
|
||||
Repository = "https://github.com/wickra-lib/wickra"
|
||||
Issues = "https://github.com/wickra-lib/wickra/issues"
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@
|
||||
^src/wickra-c$
|
||||
^src/Makevars$
|
||||
^\.gitignore$
|
||||
^data-raw$
|
||||
|
||||
+10
-3
@@ -1,15 +1,19 @@
|
||||
Package: wickra
|
||||
Type: Package
|
||||
Title: Streaming-First Technical Indicators
|
||||
Version: 0.8.3
|
||||
Version: 0.8.8
|
||||
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
|
||||
the Rust core and the other language bindings, so that live and historical
|
||||
evaluation use the exact same implementation.
|
||||
License: MIT + file LICENSE | Apache License 2.0
|
||||
URL: https://github.com/wickra-lib/wickra, https://docs.wickra.org
|
||||
URL: https://github.com/wickra-lib/wickra, https://docs.wickra.org,
|
||||
https://wickra-lib.r-universe.dev
|
||||
BugReports: https://github.com/wickra-lib/wickra/issues
|
||||
X-schema.org-keywords: technical-analysis, indicators, streaming, trading,
|
||||
finance, quant, algorithmic-trading, backtesting, time-series, ta-lib,
|
||||
candlestick, market-data
|
||||
Encoding: UTF-8
|
||||
NeedsCompilation: yes
|
||||
SystemRequirements: the Wickra C ABI shared library, downloaded automatically at
|
||||
@@ -17,6 +21,9 @@ SystemRequirements: the Wickra C ABI shared library, downloaded automatically at
|
||||
Set WICKRA_INCLUDE_DIR and WICKRA_LIB_DIR to build against a locally built C
|
||||
ABI instead (e.g. after `cargo build -p wickra-c --release`).
|
||||
Roxygen: list(markdown = TRUE)
|
||||
Suggests: testthat (>= 3.0.0)
|
||||
Depends: R (>= 2.10)
|
||||
Suggests: testthat (>= 3.0.0), knitr, rmarkdown
|
||||
VignetteBuilder: knitr
|
||||
LazyData: true
|
||||
Config/testthat/edition: 3
|
||||
Config/roxygen2/version: 8.0.0
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#' Synthetic daily OHLCV sample series
|
||||
#'
|
||||
#' A deterministic, synthetic daily OHLCV (open / high / low / close / volume)
|
||||
#' price series for use in the examples, the *Getting started* vignette, and
|
||||
#' tests. It is a seeded random walk, **not** real market data. Regenerate with
|
||||
#' `data-raw/sample_ohlcv.R`.
|
||||
#'
|
||||
#' @format A data frame with 250 rows and 6 columns:
|
||||
#' \describe{
|
||||
#' \item{date}{Trading date (`Date`).}
|
||||
#' \item{open}{Opening price.}
|
||||
#' \item{high}{Session high.}
|
||||
#' \item{low}{Session low.}
|
||||
#' \item{close}{Closing price.}
|
||||
#' \item{volume}{Traded volume.}
|
||||
#' }
|
||||
"sample_ohlcv"
|
||||
@@ -1,4 +1,4 @@
|
||||
# Wickra — R
|
||||
# Wickra — R <img src="man/figures/logo.png" align="right" height="120" alt="Wickra logo" />
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Generates data/sample_ohlcv.rda — a deterministic, synthetic daily OHLCV
|
||||
# series used by the examples, the getting-started vignette, and tests. It is a
|
||||
# seeded random walk, NOT real market data.
|
||||
#
|
||||
# Regenerate (run from the R package root, bindings/r):
|
||||
# Rscript data-raw/sample_ohlcv.R
|
||||
|
||||
set.seed(42)
|
||||
n <- 250L
|
||||
dates <- seq(as.Date("2023-01-02"), by = "day", length.out = n)
|
||||
|
||||
# Random-walk close with a mild upward drift; derive OHLC around it.
|
||||
returns <- rnorm(n, mean = 0.0004, sd = 0.012)
|
||||
close <- round(100 * cumprod(1 + returns), 2)
|
||||
open <- round(c(100, head(close, -1)) * (1 + rnorm(n, 0, 0.003)), 2)
|
||||
high <- round(pmax(open, close) * (1 + abs(rnorm(n, 0, 0.004))), 2)
|
||||
low <- round(pmin(open, close) * (1 - abs(rnorm(n, 0, 0.004))), 2)
|
||||
volume <- round(1e6 * exp(rnorm(n, 0, 0.3)))
|
||||
|
||||
sample_ohlcv <- data.frame(
|
||||
date = dates,
|
||||
open = open,
|
||||
high = high,
|
||||
low = low,
|
||||
close = close,
|
||||
volume = volume
|
||||
)
|
||||
|
||||
save(sample_ohlcv, file = "data/sample_ohlcv.rda", compress = "xz", version = 2)
|
||||
cat(sprintf("wrote data/sample_ohlcv.rda: %d rows x %d cols\n",
|
||||
nrow(sample_ohlcv), ncol(sample_ohlcv)))
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,27 @@
|
||||
% Generated by roxygen2: do not edit by hand
|
||||
% Please edit documentation in R/data.R
|
||||
\docType{data}
|
||||
\name{sample_ohlcv}
|
||||
\alias{sample_ohlcv}
|
||||
\title{Synthetic daily OHLCV sample series}
|
||||
\format{
|
||||
A data frame with 250 rows and 6 columns:
|
||||
\describe{
|
||||
\item{date}{Trading date (\code{Date}).}
|
||||
\item{open}{Opening price.}
|
||||
\item{high}{Session high.}
|
||||
\item{low}{Session low.}
|
||||
\item{close}{Closing price.}
|
||||
\item{volume}{Traded volume.}
|
||||
}
|
||||
}
|
||||
\usage{
|
||||
sample_ohlcv
|
||||
}
|
||||
\description{
|
||||
A deterministic, synthetic daily OHLCV (open / high / low / close / volume)
|
||||
price series for use in the examples, the \emph{Getting started} vignette, and
|
||||
tests. It is a seeded random walk, \strong{not} real market data. Regenerate with
|
||||
\code{data-raw/sample_ohlcv.R}.
|
||||
}
|
||||
\keyword{datasets}
|
||||
@@ -0,0 +1,119 @@
|
||||
# Golden-fixture parity: replay the shared testdata/golden input series through
|
||||
# the R FFI and assert every value matches the Rust reference output. Where the
|
||||
# archetype test only checks finiteness, this pins exact values, catching wiring
|
||||
# bugs (swapped params, wrong multi-output field). Fixtures are generated by
|
||||
# `cargo run -p wickra-examples --bin gen_golden`.
|
||||
#
|
||||
# The fixtures live at the repository root (testdata/golden) and are present
|
||||
# during the monorepo test run, but they are NOT bundled into the standalone R
|
||||
# package. A packaged check (r-universe / CRAN) therefore cannot find them, so
|
||||
# these tests skip there; the parity is already enforced by the repository CI.
|
||||
|
||||
find_golden_dir <- function() {
|
||||
d <- normalizePath(getwd(), winslash = "/", mustWork = FALSE)
|
||||
repeat {
|
||||
g <- file.path(d, "testdata", "golden")
|
||||
if (dir.exists(g)) return(g)
|
||||
parent <- dirname(d)
|
||||
if (identical(parent, d)) return(NULL)
|
||||
d <- parent
|
||||
}
|
||||
}
|
||||
|
||||
golden_dir <- find_golden_dir()
|
||||
|
||||
skip_if_no_golden <- function() {
|
||||
skip_if(is.null(golden_dir), "golden fixtures not bundled with the package")
|
||||
}
|
||||
|
||||
read_golden <- function(name) {
|
||||
read.csv(file.path(golden_dir, paste0(name, ".csv")),
|
||||
colClasses = "character", check.names = FALSE)
|
||||
}
|
||||
|
||||
read_golden_input <- function() read.csv(file.path(golden_dir, "input.csv"))
|
||||
|
||||
gcell <- function(s) if (identical(s, "nan")) NA_real_ else as.numeric(s)
|
||||
|
||||
expect_close <- function(got, want, row, field) {
|
||||
if (is.na(want)) {
|
||||
expect_true(is.na(got), info = paste("row", row, field, "expected warmup/NA"))
|
||||
} else {
|
||||
tol <- 1e-6 * max(1, abs(want))
|
||||
expect_lte(abs(got - want), tol, label = paste("row", row, field, "got", got, "want", want))
|
||||
}
|
||||
}
|
||||
|
||||
test_that("scalar indicators match golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
specs <- list(c("sma", 14), c("ema", 14), c("rsi", 14))
|
||||
for (spec in specs) {
|
||||
name <- spec[[1]]
|
||||
ind <- switch(name, sma = Sma(14), ema = Ema(14), rsi = Rsi(14))
|
||||
exp <- read_golden(name)
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
got <- update(ind, golden_input$close[i])
|
||||
expect_close(got, gcell(exp[i, 1]), i, name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test_that("candle Atr matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
atr <- Atr(14)
|
||||
exp <- read_golden("atr")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
got <- update(atr, golden_input$open[i], golden_input$high[i], golden_input$low[i],
|
||||
golden_input$close[i], golden_input$volume[i], i - 1)
|
||||
expect_close(got, gcell(exp[i, 1]), i, "atr")
|
||||
}
|
||||
})
|
||||
|
||||
test_that("pairwise Beta matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
beta <- Beta(20)
|
||||
exp <- read_golden("beta")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
# generator fed (close, open)
|
||||
got <- update(beta, golden_input$close[i], golden_input$open[i])
|
||||
expect_close(got, gcell(exp[i, 1]), i, "beta")
|
||||
}
|
||||
})
|
||||
|
||||
test_that("multi-output MACD matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
macd <- MacdIndicator(12, 26, 9)
|
||||
exp <- read_golden("macd")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
out <- update(macd, golden_input$close[i])
|
||||
if (identical(exp[i, "macd"], "nan")) {
|
||||
expect_true(all(is.na(out)), info = paste("row", i, "macd warmup"))
|
||||
} else {
|
||||
expect_close(out[["macd"]], gcell(exp[i, "macd"]), i, "macd.macd")
|
||||
expect_close(out[["signal"]], gcell(exp[i, "signal"]), i, "macd.signal")
|
||||
expect_close(out[["histogram"]], gcell(exp[i, "histogram"]), i, "macd.histogram")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test_that("multi-output ADX matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
adx <- Adx(14)
|
||||
exp <- read_golden("adx")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
out <- update(adx, golden_input$open[i], golden_input$high[i], golden_input$low[i],
|
||||
golden_input$close[i], golden_input$volume[i], i - 1)
|
||||
if (identical(exp[i, "plus_di"], "nan")) {
|
||||
expect_true(all(is.na(out)), info = paste("row", i, "adx warmup"))
|
||||
} else {
|
||||
expect_close(out[["plus_di"]], gcell(exp[i, "plus_di"]), i, "adx.plus_di")
|
||||
expect_close(out[["minus_di"]], gcell(exp[i, "minus_di"]), i, "adx.minus_di")
|
||||
expect_close(out[["adx"]], gcell(exp[i, "adx"]), i, "adx.adx")
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
title: "Getting started with wickra"
|
||||
output: rmarkdown::html_vignette
|
||||
vignette: >
|
||||
%\VignetteIndexEntry{Getting started with wickra}
|
||||
%\VignetteEngine{knitr::rmarkdown}
|
||||
%\VignetteEncoding{UTF-8}
|
||||
---
|
||||
|
||||
```{r setup, include = FALSE}
|
||||
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
|
||||
```
|
||||
|
||||
`wickra` exposes the Wickra technical-analysis library in R over its C ABI hub.
|
||||
Every indicator is a constructor returning a `wickra_indicator`; you feed it data
|
||||
one observation at a time with `update()` (an O(1) streaming step) or run a whole
|
||||
series at once with `batch()`. Both paths share the exact same Rust core, so a
|
||||
live feed and a historical backtest compute identical values.
|
||||
|
||||
```{r}
|
||||
library(wickra)
|
||||
```
|
||||
|
||||
## A sample series
|
||||
|
||||
The package ships a small synthetic OHLCV series, `sample_ohlcv`, for examples
|
||||
(a seeded random walk — not real market data).
|
||||
|
||||
```{r}
|
||||
head(sample_ohlcv)
|
||||
```
|
||||
|
||||
## Batch: a whole series at once
|
||||
|
||||
Scalar indicators run over a vector with `batch()`. Warmup positions are `NA`.
|
||||
|
||||
```{r}
|
||||
sma <- Sma(20)
|
||||
sma_values <- batch(sma, sample_ohlcv$close)
|
||||
tail(sma_values)
|
||||
```
|
||||
|
||||
## Streaming: one observation at a time
|
||||
|
||||
The same indicator fed tick-by-tick with `update()` returns the identical
|
||||
values — an equivalence the test suite enforces for every indicator.
|
||||
|
||||
```{r}
|
||||
sma_stream <- Sma(20)
|
||||
streamed <- vapply(sample_ohlcv$close, function(p) update(sma_stream, p), numeric(1))
|
||||
|
||||
same_warmup <- all(is.na(streamed) == is.na(sma_values))
|
||||
same_values <- all(streamed == sma_values, na.rm = TRUE)
|
||||
c(batch_equals_streaming = same_warmup && same_values)
|
||||
```
|
||||
|
||||
A typical streaming loop reacts to each value as it arrives:
|
||||
|
||||
```{r}
|
||||
rsi <- Rsi(14)
|
||||
overbought_days <- 0L
|
||||
for (price in sample_ohlcv$close) {
|
||||
v <- update(rsi, price) # NA during warmup
|
||||
if (!is.na(v) && v > 70) overbought_days <- overbought_days + 1L
|
||||
}
|
||||
overbought_days
|
||||
```
|
||||
|
||||
## Multi-output indicators
|
||||
|
||||
Indicators with several outputs return a *named* numeric vector (`NA` while
|
||||
warming up). MACD is the classic example — line, signal, and histogram:
|
||||
|
||||
```{r}
|
||||
macd <- MacdIndicator(12, 26, 9)
|
||||
last_macd <- c(macd = NA, signal = NA, histogram = NA)
|
||||
for (price in sample_ohlcv$close) last_macd <- update(macd, price)
|
||||
last_macd
|
||||
```
|
||||
|
||||
## Candle indicators
|
||||
|
||||
Indicators that need the whole bar take the OHLCV fields plus a timestamp:
|
||||
|
||||
```{r}
|
||||
atr <- Atr(14)
|
||||
last_atr <- NA_real_
|
||||
for (i in seq_len(nrow(sample_ohlcv))) {
|
||||
last_atr <- update(
|
||||
atr,
|
||||
sample_ohlcv$open[i], sample_ohlcv$high[i], sample_ohlcv$low[i],
|
||||
sample_ohlcv$close[i], sample_ohlcv$volume[i], i - 1
|
||||
)
|
||||
}
|
||||
last_atr
|
||||
```
|
||||
|
||||
## Resetting state
|
||||
|
||||
`reset()` returns an indicator to its warmup state so the same object can be
|
||||
reused on a fresh series:
|
||||
|
||||
```{r}
|
||||
reset(sma)
|
||||
update(sma, 100) # NaN — warming up again
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- Full indicator catalogue, guides, and per-indicator reference:
|
||||
<https://docs.wickra.org>.
|
||||
- Every constructor (`Sma()`, `Rsi()`, `MacdIndicator()`, `Atr()`, …) is listed
|
||||
in this package's help index.
|
||||
@@ -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.8</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.8</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.8",
|
||||
"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.8",
|
||||
"wickra-darwin-x64": "0.8.8",
|
||||
"wickra-linux-arm64-gnu": "0.8.8",
|
||||
"wickra-linux-x64-gnu": "0.8.8",
|
||||
"wickra-win32-arm64-msvc": "0.8.8",
|
||||
"wickra-win32-x64-msvc": "0.8.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wickra": {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Generate the language-neutral golden fixtures consumed by the per-binding
|
||||
//! parity runners (`bindings/{csharp,go,java,r}`).
|
||||
//!
|
||||
//! Run from the repo root: `cargo run -p wickra-examples --bin gen_golden`.
|
||||
//! It writes a deterministic OHLCV input series plus the reference outputs of a
|
||||
//! curated set of indicators spanning the FFI archetypes (scalar, candle,
|
||||
//! scalar multi-output, candle multi-output, pairwise), computed by the Rust
|
||||
//! core. Each binding runner replays the same input through its own FFI and
|
||||
//! checks it matches these values — catching wiring bugs (swapped params,
|
||||
//! wrong multi-output index) that the math-only core tests cannot see.
|
||||
//!
|
||||
//! `nan` marks a warmup slot where the indicator returned `None`.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use wickra::{Adx, Atr, Beta, Candle, Ema, Indicator, MacdIndicator, Rsi, Sma};
|
||||
|
||||
const N: usize = 80;
|
||||
|
||||
/// Deterministic OHLCV bar `i`: a varied, non-degenerate path so every
|
||||
/// indicator gets real movement (no constant returns, no zero ranges).
|
||||
fn bar(i: usize) -> (f64, f64, f64, f64, f64) {
|
||||
let t = i as f64;
|
||||
let close = 100.0 + 10.0 * (t * 0.3).sin() + 0.5 * t;
|
||||
let open = close - (t * 0.5).cos();
|
||||
let span = 1.0 + 0.5 * (t * 0.7).sin().abs();
|
||||
let high = close.max(open) + span;
|
||||
let low = close.min(open) - span;
|
||||
let volume = 1000.0 + 100.0 * (t * 0.2).sin().abs();
|
||||
(open, high, low, close, volume)
|
||||
}
|
||||
|
||||
fn cell(v: Option<f64>) -> String {
|
||||
match v {
|
||||
Some(x) => format!("{x}"),
|
||||
None => "nan".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_csv(dir: &Path, name: &str, header: &str, rows: &[String]) {
|
||||
let mut out = String::new();
|
||||
let _ = writeln!(out, "{header}");
|
||||
for r in rows {
|
||||
let _ = writeln!(out, "{r}");
|
||||
}
|
||||
let path = dir.join(format!("{name}.csv"));
|
||||
fs::write(&path, out).expect("write fixture");
|
||||
println!("wrote {}", path.display());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let dir = Path::new("testdata/golden");
|
||||
fs::create_dir_all(dir).expect("create testdata/golden");
|
||||
|
||||
let candles: Vec<Candle> = (0..N)
|
||||
.map(|i| {
|
||||
let (o, h, l, c, v) = bar(i);
|
||||
Candle::new(o, h, l, c, v, i as i64).expect("valid candle")
|
||||
})
|
||||
.collect();
|
||||
let closes: Vec<f64> = candles.iter().map(|c| c.close).collect();
|
||||
|
||||
// input.csv — the single shared series every runner reads.
|
||||
let mut input = vec![String::from("open,high,low,close,volume")];
|
||||
for c in &candles {
|
||||
input.push(format!(
|
||||
"{},{},{},{},{}",
|
||||
c.open, c.high, c.low, c.close, c.volume
|
||||
));
|
||||
}
|
||||
let input_header = input.remove(0);
|
||||
write_csv(dir, "input", &input_header, &input);
|
||||
|
||||
// scalar (close-driven), single output. Each is a distinct type, so write
|
||||
// them out separately rather than from a heterogeneous collection.
|
||||
{
|
||||
let mut sma = Sma::new(14).unwrap();
|
||||
let rows: Vec<String> = closes.iter().map(|&c| cell(sma.update(c))).collect();
|
||||
write_csv(dir, "sma", "sma", &rows);
|
||||
}
|
||||
{
|
||||
let mut ema = Ema::new(14).unwrap();
|
||||
let rows: Vec<String> = closes.iter().map(|&c| cell(ema.update(c))).collect();
|
||||
write_csv(dir, "ema", "ema", &rows);
|
||||
}
|
||||
{
|
||||
let mut rsi = Rsi::new(14).unwrap();
|
||||
let rows: Vec<String> = closes.iter().map(|&c| cell(rsi.update(c))).collect();
|
||||
write_csv(dir, "rsi", "rsi", &rows);
|
||||
}
|
||||
|
||||
// candle, single output.
|
||||
{
|
||||
let mut atr = Atr::new(14).unwrap();
|
||||
let rows: Vec<String> = candles.iter().map(|&c| cell(atr.update(c))).collect();
|
||||
write_csv(dir, "atr", "atr", &rows);
|
||||
}
|
||||
|
||||
// scalar multi-output: MACD(12,26,9).
|
||||
{
|
||||
let mut macd = MacdIndicator::new(12, 26, 9).unwrap();
|
||||
let rows: Vec<String> = closes
|
||||
.iter()
|
||||
.map(|&c| match macd.update(c) {
|
||||
Some(o) => format!("{},{},{}", o.macd, o.signal, o.histogram),
|
||||
None => "nan,nan,nan".to_owned(),
|
||||
})
|
||||
.collect();
|
||||
write_csv(dir, "macd", "macd,signal,histogram", &rows);
|
||||
}
|
||||
|
||||
// candle multi-output: ADX(14).
|
||||
{
|
||||
let mut adx = Adx::new(14).unwrap();
|
||||
let rows: Vec<String> = candles
|
||||
.iter()
|
||||
.map(|&c| match adx.update(c) {
|
||||
Some(o) => format!("{},{},{}", o.plus_di, o.minus_di, o.adx),
|
||||
None => "nan,nan,nan".to_owned(),
|
||||
})
|
||||
.collect();
|
||||
write_csv(dir, "adx", "plus_di,minus_di,adx", &rows);
|
||||
}
|
||||
|
||||
// pairwise: Beta(20) over (close, open).
|
||||
{
|
||||
let mut beta = Beta::new(20).unwrap();
|
||||
let rows: Vec<String> = candles
|
||||
.iter()
|
||||
.map(|&c| cell(beta.update((c.close, c.open))))
|
||||
.collect();
|
||||
write_csv(dir, "beta", "beta", &rows);
|
||||
}
|
||||
|
||||
println!("golden fixtures written to {}", dir.display());
|
||||
}
|
||||
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
plus_di,minus_di,adx
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
39.5757019682593,10.51553170206367,30.912276792040817
|
||||
37.536261624936586,10.906948307963743,32.63069750889788
|
||||
35.215165462446194,12.883060473369223,33.61637662813429
|
||||
32.86410166086221,15.606658308025656,33.758337048896145
|
||||
30.70333902684003,18.50628500756431,33.11744953242373
|
||||
28.583754140794692,21.990805693589266,31.683067176177246
|
||||
26.55934622279741,25.269437743189002,29.597761510222966
|
||||
24.849750313093512,27.34663879718962,27.82532441495794
|
||||
23.505186308008465,28.419972295720065,26.513882262566277
|
||||
22.24831894567969,28.255382095825222,25.469626589930424
|
||||
21.991214452481188,26.24217101526044,24.279889509686143
|
||||
23.0167314852192,24.206077112686202,22.725510472977113
|
||||
25.270075861089534,22.49633154108633,21.51703781909767
|
||||
28.167925277844184,20.797978949255484,21.055190823958306
|
||||
32.199512135355,19.002819811263755,21.39222118044018
|
||||
36.67556284467224,17.08189161927664,22.467654445091764
|
||||
39.66767810504092,15.312333278446436,24.027003737037383
|
||||
41.25710485757308,13.864063299710923,25.86050681733179
|
||||
41.90255145462447,12.778588478409224,27.8177163877859
|
||||
42.395628716412624,11.937994453509367,29.834786399099126
|
||||
41.39315313749411,11.291691494482587,31.784798356703554
|
||||
38.98369312390027,10.942263455121635,33.52631517717672
|
||||
36.80520205991526,11.790866997951634,34.808291852450445
|
||||
34.33471861125097,14.165199105275695,35.29246448522293
|
||||
31.575896425438806,17.056390539549085,34.90412360474046
|
||||
29.011655429985023,19.499990165743316,33.81146999165209
|
||||
26.941362744011517,21.41861333571675,32.21208520671275
|
||||
25.293388618220902,23.528371409730532,30.16945244226847
|
||||
23.70108159294569,26.085794270270323,28.356623123061247
|
||||
21.9818825371455,26.753559469925122,27.030505691607747
|
||||
20.4556018343849,25.575479039846915,25.894230469505597
|
||||
19.315305653157523,24.149775639951073,24.839117763267886
|
||||
20.15346427198841,22.836532180755725,23.510691119817217
|
||||
22.621623373749483,21.279561359099215,22.049713637827605
|
||||
26.102172398824052,19.564946484191104,21.497230627213543
|
||||
29.5794011460886,17.914862734831026,21.71599192763469
|
||||
33.103914522491884,16.421886804841026,22.570814571368153
|
||||
36.988461875248944,14.998303176938437,23.980008725527608
|
||||
39.75301194225335,13.75986552582622,25.73669586768207
|
||||
41.37315229671887,12.777991517838071,27.67023180773477
|
||||
42.48135161688847,11.981093863498268,29.693955639647385
|
||||
42.62294720176041,11.257736731480932,31.730983563876975
|
||||
40.47684671425082,10.451870979556956,33.67554918447296
|
||||
37.62379013468778,11.104415435913893,35.1575133843429
|
||||
35.445073236866044,12.840589537363048,35.99012453099969
|
||||
33.361147221464314,15.58241905716853,36.014040954862004
|
||||
30.784559395666363,20.10967847345435,34.9397977135121
|
||||
28.101022653590128,23.891333482064333,33.02243689823779
|
||||
25.724805235133402,26.22784634752717,30.732853434649453
|
||||
23.898483996930594,27.16675703876092,28.994806129209827
|
||||
22.507592062957475,27.59047661188655,27.648453495608493
|
||||
21.169522519356338,26.35196828602853,26.452526680749255
|
||||
21.224977830554703,24.775436644747092,25.11436900225873
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
atr
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
3.917642936098542
|
||||
3.871756865753318
|
||||
3.8256550237571303
|
||||
3.775585219414474
|
||||
3.7359112048078584
|
||||
3.751806385408204
|
||||
3.844730678370346
|
||||
3.9663267478375337
|
||||
4.065925146592011
|
||||
4.103990051025662
|
||||
4.122277884333844
|
||||
4.120405500552875
|
||||
4.1099068422738645
|
||||
4.067219182386907
|
||||
3.965656623010464
|
||||
3.883731209815142
|
||||
3.8454426000158177
|
||||
3.827649810828608
|
||||
3.8056867488416217
|
||||
3.7971798302715882
|
||||
3.795930185925992
|
||||
3.768312827006575
|
||||
3.700099728786484
|
||||
3.6306300896424184
|
||||
3.63084862338228
|
||||
3.6560272497167245
|
||||
3.6536783654804816
|
||||
3.670527021756155
|
||||
3.731163563648225
|
||||
3.855191519830406
|
||||
3.994403105040647
|
||||
4.097289247845801
|
||||
4.128368657741341
|
||||
4.103844480606131
|
||||
4.0291518193306395
|
||||
3.972922779511551
|
||||
3.9077919971718877
|
||||
3.8900842871217334
|
||||
3.9281940607734454
|
||||
3.970355102352287
|
||||
3.970344885666312
|
||||
3.9271768651715058
|
||||
3.891868629198634
|
||||
3.896746818914544
|
||||
3.888594808948336
|
||||
3.8241554941261624
|
||||
3.755340593345057
|
||||
3.742402894117046
|
||||
3.779811441067645
|
||||
3.833276428397188
|
||||
3.8832337640008086
|
||||
3.9482678364884123
|
||||
3.9963590513965954
|
||||
3.9961613679725096
|
||||
3.957618048219805
|
||||
3.9111378825809897
|
||||
3.9118754285284236
|
||||
3.9079875306228926
|
||||
3.8519602816708205
|
||||
3.8003043443211753
|
||||
3.824279210324508
|
||||
3.89030736805505
|
||||
3.946176568396809
|
||||
3.944383618517461
|
||||
3.889018639116889
|
||||
3.8395240928112067
|
||||
3.792174559624793
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
beta
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
0.9195261318785413
|
||||
0.922546057952995
|
||||
0.9270670303012646
|
||||
0.9360823119678988
|
||||
0.9520494101604974
|
||||
0.9734365033467753
|
||||
0.995093627699459
|
||||
1.0111583960294632
|
||||
1.018359059410051
|
||||
1.0174553154888744
|
||||
1.0121204526718954
|
||||
1.0067713468887387
|
||||
1.0049777031298115
|
||||
1.0087588074727343
|
||||
1.0182494903866188
|
||||
1.031286972171161
|
||||
1.0431058848104742
|
||||
1.0473159800278413
|
||||
1.0395185963505182
|
||||
1.0218520432290943
|
||||
1.0027356441997792
|
||||
0.989852175809267
|
||||
0.9830850966851594
|
||||
0.9757086459066595
|
||||
0.9639743320260056
|
||||
0.9518863451049362
|
||||
0.945167696468917
|
||||
0.9459710447944064
|
||||
0.9530453598334451
|
||||
0.9634788824656162
|
||||
0.973948405670025
|
||||
0.9815754255825168
|
||||
0.9846435781849213
|
||||
0.9830670028068416
|
||||
0.9783981708129912
|
||||
0.9733920844663336
|
||||
0.9713725848572005
|
||||
0.9755741087472364
|
||||
0.988273819307749
|
||||
1.0091885908656193
|
||||
1.0332355593672604
|
||||
1.0510340548483572
|
||||
1.057262107302167
|
||||
1.058580145340488
|
||||
1.0645933328050556
|
||||
1.0735088672694193
|
||||
1.0743308164692502
|
||||
1.0612002349944065
|
||||
1.0392533178013483
|
||||
1.0174419588972756
|
||||
1.0016230577141623
|
||||
0.9934855658174216
|
||||
0.9919410932606791
|
||||
0.9943630990130676
|
||||
0.9973488461009995
|
||||
0.9974248103764315
|
||||
0.9919840212101835
|
||||
0.9802751136245323
|
||||
0.9638604974880948
|
||||
0.9461133999713321
|
||||
0.9309659374652403
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
ema
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
107.082865308575
|
||||
105.90971557088022
|
||||
104.81838000454273
|
||||
103.91437652548925
|
||||
103.29137341232037
|
||||
103.02217097393634
|
||||
103.15163412061464
|
||||
103.6921955736008
|
||||
104.62232136443316
|
||||
105.88806700052659
|
||||
107.40757775297398
|
||||
109.07812453770966
|
||||
110.78504123504803
|
||||
112.4117601975411
|
||||
113.85004525232907
|
||||
115.00950442946956
|
||||
115.8255294789832
|
||||
116.26495019544112
|
||||
116.32889606739172
|
||||
116.05260755010886
|
||||
115.50221201839392
|
||||
114.76875083248335
|
||||
113.95998970819002
|
||||
113.19074277367604
|
||||
112.57257361958058
|
||||
112.2037924360836
|
||||
112.16064199938042
|
||||
112.49045917546245
|
||||
113.20742223024638
|
||||
114.29126332917505
|
||||
115.68906080413531
|
||||
117.3199507165269
|
||||
119.08233652305883
|
||||
120.86295254590979
|
||||
122.54697139145239
|
||||
124.02825224132444
|
||||
125.2188143840051
|
||||
126.05668958634725
|
||||
126.51145173900844
|
||||
126.58692971020658
|
||||
126.32086097368011
|
||||
125.78151686199148
|
||||
125.06160082390045
|
||||
124.26996466927172
|
||||
123.5218827065933
|
||||
122.92875250508669
|
||||
122.58814223802023
|
||||
122.57507361058863
|
||||
122.93531900967452
|
||||
123.68131159273575
|
||||
124.79103363078006
|
||||
126.20998238510259
|
||||
127.85603789338172
|
||||
129.62679782427364
|
||||
131.408724185901
|
||||
133.0872848279711
|
||||
134.5571838169111
|
||||
135.7317668260377
|
||||
136.550761377433
|
||||
136.98566051598186
|
||||
137.04226900211742
|
||||
136.7601845743594
|
||||
136.20926061449748
|
||||
135.48336619117217
|
||||
134.6920008752563
|
||||
133.95051334772793
|
||||
133.369797540168
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
open,high,low,close,volume
|
||||
99,101,98,100,1000
|
||||
102.57761950472302,104.77731091023225,101.25551066110417,103.4552020666134,1019.8669330795061
|
||||
106.10612242808222,108.13914959894458,104.61339756308799,106.64642473395035,1038.941834230865
|
||||
109.26253189460714,110.76487377959927,107.8309272112827,109.33326909627483,1056.4642473395036
|
||||
111.7365376962194,112.90403177129735,110.1528967845943,111.32039085967226,1071.7356090899523
|
||||
113.27609348158748,114.45148509543229,111.29955825219574,112.47494986604055,1084.1470984807897
|
||||
113.7284688053824,115.1642566915892,111.30268842257516,112.73847630878196,1093.2039085967226
|
||||
113.06855035377954,114.55977666009171,110.64086736017657,112.13209366648874,1098.544972998846
|
||||
111.40827542637513,112.72390874531129,109.43899848657534,110.75463180551151,1099.9573603041506
|
||||
108.98459460176909,109.99300155201126,107.76539185209613,108.7737988023383,1097.3847630878195
|
||||
106.12753789513545,107.73969337995807,104.79904459577605,106.41120008059868,1090.929742682568
|
||||
103.21387328427625,105.41662717550601,101.71978916733775,103.92254305856751,1080.849640381959
|
||||
100.61462528040111,103.00209502109563,99.18732582635697,101.57479556705148,1067.546318055115
|
||||
98.64575078243223,100.78188758933494,97.48620160125755,99.62233840816026,1051.5501371821465
|
||||
97.53034002152081,99.46748184049008,96.34710045689485,98.28424227586412,1033.4988150155905
|
||||
97.378063505514,99.16454670333486,95.93821562552817,97.72469882334903,1014.1120008059867
|
||||
98.18385394545021,99.67344281002588,96.54876504706593,98.0383539116416,1005.837414342758
|
||||
99.8438650794075,101.15293363552601,97.93278462060415,99.24185317672267,1025.5541102026832
|
||||
102.1834853863248,103.20029690993537,100.25554360082955,101.27235512444012,1044.2520443294852
|
||||
104.99031673022002,106.32510161131832,102.65835969292533,103.99314457402363,1061.185789094272
|
||||
108.04491654708718,109.54022022493461,105.7105413401633,107.20584501801073,1075.680249530793
|
||||
111.14367593283949,112.56654934841096,109.24526558927202,110.6681390048435,1087.157577241359
|
||||
114.11098793714572,115.26697281350663,112.95942875877287,114.11541363513378,1095.1602073889517
|
||||
116.80109288512898,118.47543335247398,115.61005717653698,117.28439764388199,1099.3691003633464
|
||||
119.09282467975903,121.38046215528227,117.64904116296829,119.93667863849153,1099.616460883584
|
||||
120.88220148856881,123.36781277048146,119.39438848583472,121.87999976774739,1095.8924274663138
|
||||
122.07798667229586,124.2878498649492,120.77557026109272,122.98543345374605,1088.3454655720152
|
||||
122.60397744514097,124.22410945235427,121.57876610123756,123.19889810845086,1077.2764487555987
|
||||
122.40925186267498,123.88697089091687,121.06827005264091,122.5459890808828,1063.126663787232
|
||||
121.48461656761053,122.98099977052848,119.63330909790388,121.12969230082183,1046.4602179413757
|
||||
119.8808727652764,121.29920058454442,117.70285703314954,119.12118485241757,1027.9415498198925
|
||||
117.72299769788953,118.8665240235534,115.60101790940676,116.74454423507063,1008.3089402817496
|
||||
115.21439166809358,116.4131695096543,113.05795434620948,114.2567321877702,1011.6549204850494
|
||||
112.6270381197495,114.07463180365934,110.47704737833695,111.92464106224679,1031.154136351338
|
||||
110.27641646211617,111.76231568498811,108.51535390119264,110.00125312406458,1049.4113351138608
|
||||
108.48360243707184,109.99872116521585,107.18792367213928,108.7030424002833,1065.6986598718788
|
||||
107.530320991091,109.22424173559783,106.49671695482826,108.19063769933508,1079.3667863849153
|
||||
107.61494922421186,109.90155645208623,106.26786689008574,108.55447411796011,1089.8708095811626
|
||||
108.81801012516657,111.30403713029216,107.32068773822765,109.80671474335324,1096.7919672031487
|
||||
111.08434919099572,113.29382811110736,109.67068524069803,111.88016416080967,1099.8543345374605
|
||||
114.22618875818226,115.76972371414958,113.09073586402833,114.63427081999565,1098.9358246623383
|
||||
117.94724565362051,119.15370942824079,116.6612183117217,117.86768208634197,1094.0730556679773
|
||||
121.88395973243564,123.33523703654073,119.88495316810628,121.33623047221137,1085.459890808828
|
||||
125.65653611891825,127.14038418498515,123.29089632531003,124.77474439137693,1073.4397097874114
|
||||
128.92069597346688,130.20955349568973,126.63187762484937,127.92073514707224,1058.4917192891762
|
||||
131.41114890560974,132.45313613345562,129.49585703767033,130.5378442655162,1041.2118485241756
|
||||
132.96978971477444,134.3228744433646,131.08387196585088,132.43695669444105,1022.2889914100246
|
||||
133.5549991814736,135.053120145851,131.99497292310176,133.49309388747918,1002.4775425453357
|
||||
133.2323987581558,135.065460892756,131.8235156308926,133.65657776549278,1017.4326781222982
|
||||
132.1510588175171,134.08480947785137,131.0237176510951,132.95746831142935,1036.647912925193
|
||||
130.5116755897077,132.71696973631924,129.29758425495962,131.50287840157117,1054.402111088937
|
||||
128.53409061924222,130.9222390672229,127.07925728332546,129.46740573130614,1069.9874687593544
|
||||
126.4306172006658,128.5591966352313,124.94895708842891,127.07753652299444,1082.7826469085653
|
||||
124.38927906587094,125.87336879788326,123.1073244542458,124.59141418625812,1092.2775421612807
|
||||
122.56791894474918,123.61827749324543,121.2254215875191,122.27578013601534,1098.093623006649
|
||||
121.09703354508844,122.45602084147426,119.02315927992295,120.38214657630877,1099.9990206550704
|
||||
120.08693553049852,121.58571423995242,117.62555095473105,119.12432966418496,1097.9177729151318
|
||||
119.63399522490424,121.03798192673773,117.25536324735009,118.65934994918358,1091.9328525664675
|
||||
119.82146372498268,120.94065716085713,117.95421275941922,119.07340619529367,1082.2828594968707
|
||||
120.7124947130592,121.93415307848436,119.152517136663,120.37417550208815,1069.3525084777123
|
||||
122.33587608239566,123.94838830624106,120.87761530843784,122.49012753228324,1053.6572918000436
|
||||
124.66785812762632,126.75625008370868,123.18852214765032,125.27691410373268,1035.8229282236828
|
||||
127.61552102482923,129.80523536741393,126.34054904004905,128.53026338263376,1016.5604175448309
|
||||
131.00775897051096,133.06294250948164,129.94904333909741,132.0042268780681,1003.3623047221139
|
||||
134.59892592769242,136.79793765689536,133.234137558996,135.43314928819893,1023.1509825101539
|
||||
138.08766685694894,140.0546941334019,136.58837142074304,138.555398697196,1042.016703682664
|
||||
141.1500141222941,142.5489901806554,139.73776131670974,141.13673737507105,1059.2073514707224
|
||||
143.48228026040917,144.5932922665057,141.8802335303824,142.99124553647894,1074.037588995245
|
||||
144.84649927621132,146.0756620034572,142.76876627418082,143.9979290014267,1085.9161814856498
|
||||
145.1098734724699,146.57143212166807,142.649968595823,144.11152724502116,1094.3695669444105
|
||||
144.27024859045207,145.7471249168318,141.88968005898082,143.36655638536055,1099.060735569487
|
||||
142.46200877506277,143.72992039092952,140.60631434563564,141.8742259615024,1099.8026652716362
|
||||
139.9404686061768,141.00752471999962,138.7454488027266,139.81250491654941,1096.5657776549278
|
||||
137.04703875358243,138.78071046739933,135.67655244818158,137.41022416199849,1089.4791172140503
|
||||
134.16122174198705,136.42630679247307,132.66155074344638,134.9266357939324,1078.8252067375315
|
||||
131.6480122345848,134.02210748888697,130.25415962109273,132.6282548753949,1065.0287840157116
|
||||
129.80997879551026,131.86785162975886,128.70717960530897,130.76505243955756,1048.6398688853799
|
||||
128.85205701193928,130.78472862032595,127.6154547134165,129.54812632180318,1030.3118356745701
|
||||
128.8642014864336,130.59557044803222,127.39947545719488,129.13084441879352,1010.7753652299442
|
||||
129.82321172551153,131.29749357237068,128.12086294416926,129.59514479102842,1009.1906850227682
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
macd,signal,histogram
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
nan,nan,nan
|
||||
3.553825374871522,4.7567646581597245,-1.2029392832882024
|
||||
2.8208654161048656,4.369584809748753,-1.5487193936438874
|
||||
2.1109014543067843,3.9178481386603594,-1.806946684353575
|
||||
1.4897304373886868,3.4322245984060253,-1.9424941610173385
|
||||
1.0151049910054155,2.9488006769259036,-1.9336956859204881
|
||||
0.7315731385597957,2.5053551692526823,-1.7737820306928866
|
||||
0.6664991548332893,2.137583966368804,-1.4710848115355146
|
||||
0.8276206486027746,1.8755913028155982,-1.0479706542128235
|
||||
1.2023598043004569,1.74094500311257,-0.5385851988121131
|
||||
1.7589500704087442,1.744546016571805,0.014404053836939212
|
||||
2.449277456832604,1.885492304623965,0.563785152208639
|
||||
3.2131824784813148,2.1510303393954353,1.0621521390858795
|
||||
3.9838383370451567,2.5175919389253796,1.466246398119777
|
||||
4.693724820711168,2.9528185152825377,1.7409063054286302
|
||||
5.280664208546995,3.418387653935429,1.8622765546115656
|
||||
5.693379944743711,3.8733861120970854,1.8199938326466256
|
||||
5.896081491642136,4.277925188006096,1.6181563036360398
|
||||
5.871665770714699,4.596673304547816,1.274992466166883
|
||||
5.623249186369037,4.801988480912061,0.821260705456976
|
||||
5.17389335937969,4.876369456605587,0.29752390277410345
|
||||
4.564549053715282,4.814005376027526,-0.2494563223122439
|
||||
3.8504019492156516,4.621284690665152,-0.7708827414495003
|
||||
3.0959466751243525,4.316217087556993,-1.2202704124326402
|
||||
2.369229123514458,3.926819494748486,-1.5575903712340278
|
||||
1.735771359392487,3.488609867677286,-1.7528385082847988
|
||||
1.252721799050505,3.0414322539519296,-1.7887104549014246
|
||||
0.9637532073126067,2.6258964446240656,-1.6621432373114589
|
||||
0.8951642651140048,2.2797500087220537,-1.3845857436080489
|
||||
1.0535329484060725,2.0345065966588574,-0.9809736482527849
|
||||
1.4251313413431745,1.9126315455957208,-0.48750020425254625
|
||||
1.9771541634642489,1.9255360691694265,0.051618094294822336
|
||||
2.6606512771767967,2.072559110770901,0.5880921664058958
|
||||
3.414902230493496,2.34102773471542,1.073874495778076
|
||||
4.17284207727586,2.707390603227508,1.4654514740483515
|
||||
4.867053809684606,3.139323244518928,1.727730565165678
|
||||
5.4357921236225195,3.5986170203396464,1.8371751032828731
|
||||
5.828500438890558,4.044593704049829,1.7839067348407287
|
||||
6.010328361604678,4.437740635560799,1.5725877260438788
|
||||
5.965246063689932,4.743241721186626,1.2220043425033058
|
||||
5.697477387222705,4.934088854393842,0.7633885328288628
|
||||
5.231123664416998,4.9934958163984735,0.23762784801852455
|
||||
4.608011861752942,4.916399025469367,-0.30838716371642505
|
||||
3.883959272298,4.709911074835094,-0.8259518025370935
|
||||
3.123788425025225,4.39268654487312,-1.2688981198478952
|
||||
2.395537519050208,3.993256739708538,-1.59771922065833
|
||||
1.764383551803462,3.547482102127523,-1.7830985503240608
|
||||
1.286820974089835,3.0953498765199856,-1.8085289024301505
|
||||
|
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
rsi
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
nan
|
||||
46.84518809898699
|
||||
45.82961265424322
|
||||
46.5293990131247
|
||||
49.23903870201344
|
||||
53.518768674122796
|
||||
58.56047460456064
|
||||
63.58342577996474
|
||||
68.07459111891804
|
||||
71.80327260156207
|
||||
74.72559789038102
|
||||
76.88488357198254
|
||||
78.34471541432595
|
||||
79.15131540353094
|
||||
79.31156608926268
|
||||
77.35312513119456
|
||||
73.13439349750769
|
||||
67.51119167055117
|
||||
61.48673113238304
|
||||
55.86675382432369
|
||||
51.14733221714866
|
||||
47.57753664027756
|
||||
45.28036590056008
|
||||
44.36975889174273
|
||||
45.212282377402985
|
||||
48.12434665867438
|
||||
52.61537254620642
|
||||
57.83678169397702
|
||||
62.992663568874654
|
||||
67.57346293490359
|
||||
71.3583982125327
|
||||
74.31265622918312
|
||||
76.4855340725887
|
||||
77.9435594091973
|
||||
78.73329989554925
|
||||
78.85948376472163
|
||||
76.76197022138767
|
||||
72.44443440532005
|
||||
66.78373698621982
|
||||
60.77877328002407
|
||||
55.21662487911296
|
||||
50.57413251002351
|
||||
47.087563552178636
|
||||
44.87458874458458
|
||||
44.050409895373384
|
||||
45.01878876311954
|
||||
48.0601943425273
|
||||
52.64880490307121
|
||||
57.92140340103244
|
||||
63.088714334306665
|
||||
67.65616719708338
|
||||
71.41589642993111
|
||||
74.34074717674663
|
||||
76.4832369420561
|
||||
77.91026866443379
|
||||
78.66702552588708
|
||||
78.75547116040536
|
||||
76.51507736151031
|
||||
72.09088140590961
|
||||
66.3803507173797
|
||||
60.378854393296976
|
||||
54.856911707280055
|
||||
50.27461841837877
|
||||
46.857548898064
|
||||
44.719625963574536
|
||||
43.978649539628535
|
||||
45.06927522550655
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user