Compare commits

...

6 Commits

Author SHA1 Message Date
kingchenc 5f0677d62d release: bump 0.9.1 -> 0.9.2 (#306) 2026-06-15 06:02:05 +02:00
kingchenc ce792cf8b8 fix: clear the last two build warnings (C# CA2255, WASM license files) (#304)
- C#: the [ModuleInitializer] that registers the DllImport resolver is the
  one legitimate library use of the attribute (a static ctor would run too
  late), so suppress CA2255 with a documented justification — the dotnet
  build is now warning-clean.
- WASM: add LICENSE-MIT + LICENSE-APACHE to bindings/wasm so wasm-pack stops
  warning about missing license files and the published npm package ships
  its license texts.

Full warnings audit: Rust, C, C++, Go, Java, Node, Python, C#, WASM all
build clean; R installs clean.
2026-06-15 05:26:06 +02:00
kingchenc 120b6ac265 docs+ci: surface 10-language golden verification (#303)
* docs+ci: surface 10-language golden verification; add WASM golden CI

- README: add C++ to the Quickstart list and state prominently that all 514
  indicators are replayed through all 10 languages and checked bit-for-bit
  against the Rust reference.
- CHANGELOG: document the cross-language golden suite, the Java and R C-ABI
  bool-marshalling fixes, the C# nullable directive and the live_binance rename.
- ci.yml: run the WASM golden suite (nodejs-target build + node --test); the
  C/C++ golden tests already run via the C-ABI job's ctest and the other
  bindings pick up their golden runners in their existing test suites.

* readme: add verified badge + prominent per-language throughput table

- Add the 'verified across 10 languages' badge to the badge row, linking to
  the FAQ that explains the cross-language golden parity.
- Surface a per-binding throughput table (the cost of each language's FFI
  boundary) so readers can pick a binding that keeps up with streaming hot
  loops — the cross-library benchmarks stay in BENCHMARKS.md.

* changelog: note the verified badge and per-binding throughput table

* docs: fix cross-language consistency (audit)

- docs/README.md: add the missing C++ quickstart link.
- README testing section: the golden parity now covers all 10 languages and
  all 514 indicators (was 'four C-ABI bindings, 7 archetype indicators').
- CHANGELOG: the live_binance rename also covers the C examples.
2026-06-15 05:07:52 +02:00
kingchenc 4f708d410d test: golden-pin the four de-duplicated indicators across all bindings (#305)
* test: golden-pin the four de-duplicated indicators across all C-ABI bindings

Extend gen_golden to emit reference fixtures for AdOscillator (ADOSC),
IntradayIntensity, AwesomeOscillatorHistogram and AverageDrawdown, and replay
them through the Go / C# / Java / R golden harnesses so their corrected
definitions stay bit-identical to the Rust core in every binding. Go suite
verified locally (gcc 13 + cgo): all 9 golden tests pass; C#/Java/R use the
same fixtures and harness pattern (CI-verified). First step of extending the
golden coverage beyond the seven archetype representatives.

* test: golden-pin the scalar-output tranche (308 indicators) against Rust

Extend gen_golden with a generated emit_scalar that writes reference fixtures
for every single-f64-output indicator (scalar / candle / pairwise input) using
valid constructor params, and add a manifest-driven generic Python golden
replay that reconstructs each by its native name and checks it bit-for-bit
against the Rust output. 308 indicators now value-tied to the Rust core in
Python (pytest: 308/308). Takes golden coverage from the 7 archetype
representatives to 308+ of the catalogue.

22 scalar indicators with non-default constructor constraints are skipped by
gen_golden for now (logged), as are non-f64-output ones; multi-output, exotic
inputs and the per-indicator arg arities of the C-ABI/Node replays follow.
Generated + verified locally with the full toolchain.

* test: golden-pin the multi-output tranche (70 indicators) in Python

Add a generated emit_multi to gen_golden (per-indicator Output-field access,
one CSV column per field) and a manifest-driven generic Python replay that
checks every field of each multi-output indicator against the Rust reference.
70 multi-output indicators now value-tied to Rust in Python; combined with the
scalar tranche, 378 indicators are golden-pinned. 8 multi with non-default
param constraints and 5 with non-f64 Output fields (Option/Vec/i64) are
deferred. pytest green.

* test(golden): add 30 constraint-tuned indicators to scalar/multi golden suite

Emit golden fixtures for 22 scalar-output and 8 multi-output indicators
whose constructors need non-default parameters (Alma, Jma, Psar, T3, Mama,
DoubleBollinger, ZigZag, ...). All 408 fixtures replay bit-for-bit through
the Python binding.

* test(golden): cover 36 missed scalar/multi indicators

Add 26 single-output (LinearRegression family, HT cycle, Candle
volatility estimators, DrawdownDuration) and 10 multi-output
(BollingerBands, MACD/MACDEXT/MACDFIX, Camarilla, VWAP bands, ...)
indicators to the golden suite. 444 fixtures replay bit-for-bit
through the Python binding.

* test(golden): cover 50 exotic-input indicators

Add deterministic synthetic feeders for the DerivativesTick (17),
CrossSection (15), Trade (8), TradeQuote (3) and OrderBook (7)
families, derived from the shared OHLCV input series in both
gen_golden and a new Python replay harness (test_golden_exotic).
All 494 fixtures replay bit-for-bit through the Python binding.

* test(golden): complete 514-indicator golden coverage

Add the final tranches: 3 mixed multi-output indicators (Ichimoku,
WilliamsFractals, LeadLagCrossCorrelation), 6 histogram profiles
(time/volume seasonality + TPO/volume price profiles), 10 alt-chart
bar builders and the footprint. Every one of the 514 distinct
indicators now has a Rust-generated g_<Canonical>.csv fixture and a
generic Python replay (scalar/multi/exotic/profile/bars), all passing
bit-for-bit.

* test(golden): add generic Node replay for all 514 indicators

A manifest-driven node:test harness reconstructs every indicator by its
native class, feeds the same synthetic stream derived from the shared
golden input, and checks output bit-for-bit against the Rust reference
fixtures (scalar/multi/exotic/profile/bars). node_manifest.json is
generated from index.d.ts plus the Python-side manifests. 514/514 pass.

* test(golden): add generated Go replay for all 514 indicators

golden_all_test.go (generated by gen_golden_test.py) reconstructs every
Go indicator, feeds the shared synthetic stream and checks output
bit-for-bit against the Rust reference fixtures. A reflection-based
comparator flattens multi-output structs, profiles and bar slices so one
path covers all archetypes. This is the first C-ABI binding verified
across the full catalogue. 514/514 pass.

* test(golden): add generated C# replay for all 514 indicators

GoldenAllTests.g.cs (generated by gen_golden_test.py) reconstructs every
C# indicator, feeds the shared synthetic stream and checks output
bit-for-bit against the Rust reference fixtures via a reflection-based
flatten covering scalar/multi/profile/bar archetypes. 514/514 pass.

Also add the '#nullable enable' directive the compiler requires to the
generated Indicators.g.cs, clearing the four CS8669 warnings on the
nullable double[] profile return types.

* fix(java): marshal C ABI bool params correctly; add 514 golden replay

The Java FFM binding marshalled the cross-section state flags (newHigh,
newLow, aboveMa, onBuySignal) as JAVA_DOUBLE arrays, but the C ABI takes
them as const bool* (one byte each), so the native side read the low byte
of each 8-byte double and saw every flag as false. Add WickraNative.
boolSegment and use it across the 15 cross-section indicators. Also pass
the MacdExt MaType arguments as byte to match the uint8_t downcall
descriptor (was int, throwing WrongMethodTypeException).

Add GoldenAllTest.java (generated by gen_golden_test.py): a reflection
runner replaying all 514 indicators against the Rust reference fixtures.
The bugs above were found by this test; 514/514 now pass.

* fix(r): marshal C ABI bool flags correctly; add 514 golden replay

The R wrapper passed the cross-section state flags as (bool *)REAL(x),
reinterpreting the 8-byte doubles as 1-byte bools so the native side read
every flag as false. Add wk_bool_vec to convert each flag vector into a
real C bool buffer and use it for all 15 cross-section update wrappers.

Add test-golden-all.R + generated golden_specs.R: a reflective runner
replaying all 514 indicators against the Rust reference fixtures. The bug
above was found by this test; verified 514/514 pass locally.

* test(golden): add WASM replay for all 514 indicators

A manifest-driven node:test harness loads the nodejs-target wasm-pack
build, reconstructs every indicator by its JS class, feeds the shared
synthetic stream and checks output bit-for-bit against the Rust
reference fixtures. wasm_manifest.json is generated from the wasm .d.ts
plus the shared manifests; a recursive flattener covers scalar, multi
(Reflect objects), profile and bar shapes. 514/514 pass locally
(wasm-pack build --target nodejs, then node --test).

* test(golden): add C and C++ replay for all 514 indicators

golden_test.c (generated by gen_golden_test.py) drives every indicator
through the C ABI (wickra.h) and checks output bit-for-bit against the
Rust reference fixtures. golden_test.cpp #includes the same source so the
identical runner is compiled and run under both gcc (C) and g++ (C++) via
the CMake targets golden_test / golden_test_cpp — proving the extern "C"
header is consumable from each language. Both 514/514 (verified via ctest).

* test(golden): gofmt the generated Go golden replay

* test(golden): make the Node fixture reader CRLF-safe and pin fixtures to LF
2026-06-15 04:48:51 +02:00
kingchenc de1112ea91 chore(examples): rename live_trading examples to live_binance (#301)
The examples stream a live Binance feed into the indicators and print signals;
they place no orders, so 'live_trading' overstated them and was inconsistent
with the C/Go/R examples already named live_binance. Rename the Python/Node/WASM
files to live_binance.* and update every reference, run command, header, and the
project-tree listings. Accurate use-case wording ('suitable for live trading
bots') and the risk disclaimers are left unchanged.
2026-06-15 03:41:19 +02:00
kingchenc 82d7479011 fix: de-duplicate four indicators by correcting their definitions (#300)
* fix(core): de-duplicate 3 indicators by correcting their definitions

Behavioral audit found these computed identically to another indicator:

- AverageDrawdown was the mean per-bar under-water fraction = PainIndex.
  Now the conventional average drawdown: mean of the maximum depths of the
  distinct drawdown episodes in the window.
- IntradayIntensity was a cumulative line = the A/D Line (Adl); its normalized
  form is the Chaikin Money Flow (Cmf). Now the raw per-bar Bostian intensity
  volume*(2c-h-l)/(h-l), distinct from both.
- AwesomeOscillatorHistogram was AO - SMA(AO, n) = AcceleratorOscillator. Now
  the AO momentum AO[t] - AO[t-lookback] (the histogram delta); the 3rd
  parameter is reinterpreted from sma_period to lookback (default 1).

Constructor signatures are unchanged, so the bindings keep their API. Core
unit tests rewritten with the new reference values; workspace tests + clippy
green. Binding value-tests and deep-dive docs are updated separately.

* fix(core): redefine AdOscillator as the A/D Oscillator (was a Wad duplicate)

AdOscillator computed the cumulative volume-free Williams A/D line, identical
to the Wad indicator. Redefine it as the Williams A/D *Oscillator*: the same
line minus its 13-bar SMA, so it oscillates around zero (mean-reverting) while
Wad stays the drifting cumulative line for divergence analysis. The canonical
name AdOscillator is now accurate; the trait name() becomes "ADOSC".

Constructor stays no-arg (internal 13-bar signal). Unit tests rewritten and
cross-checked against Wad - SMA(Wad, 13). The native bindings' "WilliamsAD"
alias is renamed to "ADOSC" separately.

* fix(bindings): rename WilliamsAD alias to ADOSC and update value tests

Follows the core de-duplication: the native bindings exposed the Williams A/D
line as 'WilliamsAD', which is now the A/D Oscillator. Rename the Python /
Node.js / WASM alias to 'ADOSC' (regenerated node index.js / index.d.ts) and
update the binding value-tests for the four redefined indicators
(AverageDrawdown episode mean, AwesomeOscillatorHistogram momentum warmup,
the Wad-line reference test now uses ta.Wad()). Python suite and node suite
both pass (pytest all green, node 584/584).

* docs: record indicator de-duplication in README and CHANGELOG

README volume family: 'Williams A/D' -> 'Williams A/D Oscillator', 'Intraday
Intensity Index' -> 'Intraday Intensity'. CHANGELOG [Unreleased] documents the
four redefinitions and the native WilliamsAD -> ADOSC rename as breaking.

* test(core): cover Default impl and drop dead match arm

Codecov flagged AdOscillator::default() (never exercised) and the unreachable
_ => panic!() arm in the AwesomeOscillatorHistogram test. Exercise Default in
the accessors test and rewrite the histogram check as an if-let, removing the
dead arm.
2026-06-15 03:41:15 +02:00
618 changed files with 103059 additions and 375 deletions
+7
View File
@@ -32,3 +32,10 @@ bindings/r/src/Makevars.in text eol=lf
bindings/r/src/Makevars.win text eol=lf
bindings/r/src/wickra.c text eol=lf
# Golden fixtures are replayed byte-for-byte by every binding's parity test. Pin
# them to LF so a Windows `core.autocrlf=true` checkout doesn't rewrite them as
# CRLF — which silently broke the Node reader (`Number('inf\r')` is NaN, and a
# blank "no-bar" row gained a stray `\r`), failing only on Windows runners. The
# tolerant readers (Python `splitlines`/`float`) hid the same hazard.
testdata/golden/** text eol=lf
+6
View File
@@ -566,6 +566,12 @@ jobs:
test -f bindings/wasm/pkg/wickra_wasm_bg.wasm
test -f bindings/wasm/pkg/wickra_wasm.d.ts
- name: Build WASM package (nodejs target) for the golden suite
run: wasm-pack build bindings/wasm --target nodejs --release --out-dir pkg
- name: Golden parity — all 514 indicators vs the Rust reference
run: node --test bindings/wasm/tests/golden.test.js
node:
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
+1 -1
View File
@@ -227,7 +227,7 @@ A handful of indicators need care beyond naive accumulation:
A typical full-stack call sequence for a Python live-trading example:
```
[ Python: live_trading.py ]
[ Python: live_binance.py ]
[ binance.AsyncClient WebSocket ] ──── wickra_data live feed ───┐
+58 -1
View File
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.2] - 2026-06-15
### Added
- **Cross-language golden parity for all 514 indicators across all 10 languages.**
A new `gen_golden` reference emits a deterministic OHLCV input series plus the
Rust output of every one of the 514 indicators to `testdata/golden/`. Each
binding now replays that shared input and is checked **bit-for-bit against the
Rust reference**, covering every archetype (scalar, multi-output, pairwise,
derivatives-tick, cross-section, order-book, trade, profile, alt-chart bars,
footprint):
- Python, Node.js, Java and R via reflection-driven runners.
- Go, C# and C/C++ via generated dispatch (`golden_all_test.go`,
`GoldenAllTests.g.cs`, `examples/c/golden_test.c` compiled as both C and C++).
- WASM via a `node --test` runner over the nodejs-target build.
- CI now runs the WASM golden suite; the C/C++ golden tests run as `ctest`
targets in the existing C-ABI job, and the Python/Node/Go/C#/Java/R suites pick
up their golden runners automatically.
- **README:** a "verified across 10 languages" badge (linking to the FAQ that
explains the cross-language golden parity) and a per-binding throughput table so
readers can pick a binding by its streaming FFI cost.
### Fixed
- **Java binding marshalled C ABI `bool` parameters incorrectly.** The
cross-section state flags (`newHigh`, `newLow`, `aboveMa`, `onBuySignal`) were
allocated as `JAVA_DOUBLE` arrays and passed to `const bool*` parameters, so the
native side read the low byte of each 8-byte double and saw every flag as
`false` (affecting e.g. `NewHighsNewLows`, `HighLowIndex`, `BullishPercentIndex`,
`PercentAboveMa`). They are now packed into a real `bool` buffer. `MacdExt`'s
`MaType` arguments are now passed as `byte` to match the `uint8_t` downcall.
- **R binding marshalled C ABI `bool` flags incorrectly.** `(bool *)REAL(x)`
reinterpreted the 8-byte doubles as 1-byte bools across the 15 cross-section
update wrappers, reading every flag as `false`; the flags are now converted into
a real C `bool` buffer.
- C# binding: added the `#nullable enable` directive the generated
`Indicators.g.cs` requires, clearing four `CS8669` warnings.
### Changed
- Renamed the `live_trading` examples to `live_binance` across the Python, Node.js,
WASM and C examples — they poll Binance market data, they do not place trades.
- **Breaking — de-duplicated four indicators that computed identically to another
one.** Each is now its own distinct, correctly-defined indicator (the catalogue
stays at the same count):
- `AverageDrawdown` now reports the mean of the maximum depths of the distinct
drawdown episodes in the window (previously the per-bar mean under-water
fraction, which equalled `PainIndex`).
- `IntradayIntensity` now reports the raw per-bar Bostian intensity
`volume * (2*close high low) / (high low)` (previously a cumulative line
that equalled the A/D Line `Adl`; its normalized form is `Cmf`).
- `AwesomeOscillatorHistogram` now reports the AO momentum
`AO[t] AO[tlookback]`; its third parameter is the momentum `lookback`
(default 1) instead of an SMA period (the old `AO SMA(AO, n)` equalled
`AcceleratorOscillator`).
- `AdOscillator` is now the Williams **A/D Oscillator** (`WAD SMA(WAD, 13)`),
distinct from the cumulative Williams A/D line `Wad`. Its native (Python /
Node.js / WASM) alias is renamed **`WilliamsAD``ADOSC`**.
## [0.9.1] - 2026-06-14
### Added
@@ -1663,7 +1719,8 @@ public API changes.
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.9.1...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.9.2...HEAD
[0.9.2]: https://github.com/wickra-lib/wickra/compare/v0.9.1...v0.9.2
[0.9.1]: https://github.com/wickra-lib/wickra/compare/v0.9.0...v0.9.1
[0.9.0]: https://github.com/wickra-lib/wickra/compare/v0.8.9...v0.9.0
[0.8.9]: https://github.com/wickra-lib/wickra/compare/v0.8.8...v0.8.9
Generated
+9 -9
View File
@@ -1943,7 +1943,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"approx",
"criterion",
@@ -1954,7 +1954,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"criterion",
"kand",
@@ -1966,14 +1966,14 @@ dependencies = [
[[package]]
name = "wickra-c"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"wickra-core",
]
[[package]]
name = "wickra-core"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"approx",
"proptest",
@@ -1983,7 +1983,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"approx",
"csv",
@@ -2000,7 +2000,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"serde_json",
"tokio",
@@ -2010,7 +2010,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"napi",
"napi-build",
@@ -2020,7 +2020,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"numpy",
"pyo3",
@@ -2029,7 +2029,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.9.1"
version = "0.9.2"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -14,7 +14,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.9.1"
version = "0.9.2"
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.9.1" }
wickra-core = { path = "crates/wickra-core", version = "0.9.2" }
thiserror = "2"
rayon = "1.10"
+49 -12
View File
@@ -18,6 +18,7 @@
[![OpenSSF Best Practices](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/best-practices.svg)](https://www.bestpractices.dev/projects/13094)
[![Build provenance](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/provenance.svg)](https://github.com/wickra-lib/wickra/attestations)
[![Docs](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/docs.svg)](https://docs.wickra.org)
[![Verified across 10 languages](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/verified.svg)](https://docs.wickra.org/FAQ#do-all-the-language-bindings-compute-the-same-values)
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.**
@@ -53,6 +54,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C),
[C++](https://docs.wickra.org/Quickstart-C),
[C#](https://docs.wickra.org/Quickstart-CSharp),
[Go](https://docs.wickra.org/Quickstart-Go),
[Java](https://docs.wickra.org/Quickstart-Java),
@@ -89,6 +91,11 @@ times to get there.
runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
for all 514 indicators**.
- **Identical across every language — proven, not promised.** All 514 indicators
are replayed through **all 10 languages** (Rust · Python · Node.js · WASM · C ·
C++ · C# · Go · Java · R) and checked **bit-for-bit against the Rust reference**
via shared golden fixtures in CI. The math is verifiably the same everywhere —
this very check caught and fixed two real cross-language marshalling bugs.
- **Orders of magnitude faster where it counts.** In streaming Wickra is **1156×**
faster than the only other incremental peer and **thousands of times** faster
than recompute-on-every-tick libraries. On batch it wins several rows outright
@@ -137,12 +144,38 @@ elsewhere for `None`-warmup, NaN-safety and bit-exact `batch == streaming`.
Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
**[BENCHMARKS.md](BENCHMARKS.md)**.
### Pick your language with eyes open — per-binding throughput
Every binding calls the **same** Rust core, so this is **not** a speed claim — it
is the raw cost of crossing each language's FFI boundary (`SMA(20)`, 200 000 bars,
Ryzen 9 9950X, million updates/sec). **Batch is near-core everywhere; streaming is
where the boundary shows** — so if you stream tick-by-tick, the table tells you
which binding keeps up and which to avoid for hot loops.
| Language | streaming (Mupd/s) | batch (Mupd/s) |
|-----------------|-------------------:|---------------:|
| Rust (no FFI) | 391 | 500 |
| C / C++ | 383 | 330 |
| C# | 337 | 244 |
| Python | 33 | 488 |
| Java | 28 | 175 |
| Go | 24 | 400 |
| WASM | 19 | 167 |
| Node.js | 17 | 10 |
| R | 0.1 | 193 |
All ten share one verified implementation (see the verification badge above), so
the *numbers* differ but the *values* are bit-for-bit identical. Methodology and
the per-indicator breakdown are in [BENCHMARKS.md](BENCHMARKS.md#3-per-binding-throughput--the-cost-of-the-boundary).
## Indicators
514 streaming-first indicators across twenty-four families. Every one passes the
`batch == streaming` equivalence test, reference-value tests, and reset
semantics tests. Each has a per-indicator deep dive (formula, parameters,
warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
semantics tests — and is replayed through **all 10 languages** and checked
bit-for-bit against the Rust reference (golden fixtures, in CI). Each has a
per-indicator deep dive (formula, parameters, warmup) at
[docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Family | Indicators |
|--------|-----------|
@@ -153,7 +186,7 @@ warmup) at [docs.wickra.org](https://docs.wickra.org/Indicators-Overview).
| Volatility & Bands | ATR, Bollinger Bands, Keltner Channels, Donchian Channels, NATR, StdDev, Ulcer Index, Historical Volatility, Bollinger Bandwidth, %B, True Range, Chaikin Volatility, RVI (Relative Volatility Index), Parkinson Volatility, Garman-Klass Volatility, Rogers-Satchell Volatility, Yang-Zhang Volatility, Volatility Cone |
| Bands & Channels | MA Envelope, Acceleration Bands, STARC Bands, ATR Bands, Hurst Channel, LinReg Channel, Standard Error Bands, Double Bollinger Bands, TTM Squeeze, Fractal Chaos Bands, VWAP StdDev Bands, Quartile Bands, Bomar Bands, Median Channel, Projection Bands, Projection Oscillator |
| Trailing Stops | Parabolic SAR, Parabolic SAR Extended (SAREXT), SuperTrend, Chandelier Exit, Chande Kroll Stop, ATR Trailing Stop, HiLo Activator, Volty Stop, Yo-Yo Exit, Donchian Channel Stop, Percentage Trailing Stop, Step Trailing Stop, Renko Trailing Stop, Kase DevStop, Elder SafeZone, ATR Ratchet, NRTR, Time-Based Stop, Modified MA Stop |
| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index, Volume RSI, Williams Accumulation/Distribution, Twiggs Money Flow, Trade Volume Index, Intraday Intensity Index, Better Volume, Volume-Weighted MACD |
| Volume | OBV, VWAP (cumulative + rolling), ADL, Volume-Price Trend, Chaikin Money Flow, Chaikin Oscillator, Force Index, Ease of Movement, Klinger Volume Oscillator, Volume Oscillator, NVI, PVI, Williams A/D Oscillator, Anchored VWAP, Demand Index, TSV, VZO, Market Facilitation Index, Volume RSI, Williams Accumulation/Distribution, Twiggs Money Flow, Trade Volume Index, Intraday Intensity, Better Volume, Volume-Weighted MACD |
| Price Statistics | Typical Price, Median Price, Weighted Close, Linear Regression, Linear Regression Slope, Z-Score, Linear Regression Angle, Variance, Coefficient of Variation, Skewness, Kurtosis, Standard Error, Detrended StdDev, R², Median Absolute Deviation, Autocorrelation, Hurst Exponent, Pearson Correlation, Beta, Pairwise Beta, Pair Spread Z-Score, Lead-Lag Cross-Correlation, Cointegration, Relative Strength A-vs-B, Spearman Correlation, Mid Price, Mid Point, Average Price, Linear Regression Intercept, Time Series Forecast, Rolling Correlation, Rolling Covariance, OU Half-Life, Spread Hurst, Distance SSD, Beta-Neutral Spread, Variance Ratio, Granger Causality, Kalman Hedge Ratio, Spread Bollinger Bands, Spread AR(1) Coefficient, Jarque-Bera, Rolling Min-Max Scaler, Shannon Entropy, Sample Entropy, Kendall Tau |
| Ehlers / Cycle (DSP) | MAMA, FAMA, Fisher Transform, Inverse Fisher Transform, SuperSmoother, Hilbert Dominant Cycle, Hilbert Phasor, Hilbert DC Phase, Hilbert Trend Mode, Sine Wave, Decycler, Decycler Oscillator, Roofing Filter, Center of Gravity, Cybernetic Cycle, Adaptive Cycle, Empirical Mode Decomposition, Ehlers Stochastic, Instantaneous Trendline, Highpass Filter, Reflex, Trendflex, Correlation Trend Indicator, Adaptive RSI, Universal Oscillator, Adaptive CCI, Bandpass Filter, Even Better Sinewave, Autocorrelation Periodogram |
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag, Central Pivot Range, Murrey Math Lines, Andrews Pitchfork, Volume-Weighted Support/Resistance, Pivot Reversal |
@@ -270,8 +303,8 @@ while let Some(event) = stream.next_event().await? {
}
```
A Python live-trading example using the public `websockets` package lives at
`examples/python/live_trading.py`.
A Python live Binance feed example using the public `websockets` package lives at
`examples/python/live_binance.py`.
## Project layout
@@ -294,8 +327,8 @@ wickra/
├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`)
│ ├── python/ backtest, live Binance feed, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live Binance feed (load `wickra`)
│ ├── wasm/ browser demo for `wickra-wasm`
│ ├── c/ C smoke + streaming, C++ RAII wrapper
│ ├── csharp/ streaming, backtest, strategies (load `Wickra`)
@@ -381,11 +414,15 @@ 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.
On top of those per-binding tests, **all 10 languages** (Rust, Python, Node.js,
WASM, C, C++, C#, Go, Java, R) replay a shared, language-neutral golden fixture
(`testdata/golden/*.csv`, generated by
`cargo run -p wickra-examples --bin gen_golden`) and assert **bit-for-bit parity
with the Rust reference for every one of the 514 indicators** across every
archetype (scalar, multi-output, pairwise, derivatives-tick, cross-section,
order-book, trade, profile, alt-chart bars, footprint). This catches FFI wiring
bugs the math-only core tests cannot see — it has already found and fixed real
cross-language marshalling bugs in the Java and R bindings.
## Contributing
+3 -3
View File
@@ -2,13 +2,13 @@
## Supported versions
Wickra is pre-1.0. Security fixes are applied to the latest released `0.9.1`
Wickra is pre-1.0. Security fixes are applied to the latest released `0.9.2`
version only; please upgrade to the newest release before reporting an issue.
| Version | Supported |
| --- | --- |
| 0.9.1 (latest) | :white_check_mark: |
| < 0.9.1 | :x: |
| 0.9.2 (latest) | :white_check_mark: |
| < 0.9.2 | :x: |
## Reporting a vulnerability
File diff suppressed because it is too large Load Diff
@@ -159,4 +159,58 @@ public class GoldenTests
AssertClose(got.Value.Adx, Cell(e[2]), i, "adx.adx");
}
}
// --- the four de-duplicated indicators ------------------------------------
[Fact]
public void Candle_AdOscillator_MatchesGolden()
{
var input = Input();
var expected = ReadCsv("ad_oscillator");
using var ad = new AdOscillator();
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(ad.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "ad_oscillator");
}
}
[Fact]
public void Candle_IntradayIntensity_MatchesGolden()
{
var input = Input();
var expected = ReadCsv("intraday_intensity");
using var ii = new IntradayIntensity();
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(ii.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "intraday_intensity");
}
}
[Fact]
public void Candle_AwesomeOscillatorHistogram_MatchesGolden()
{
var input = Input();
var expected = ReadCsv("awesome_oscillator_histogram");
using var aoh = new AwesomeOscillatorHistogram(5, 34, 1);
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(aoh.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "awesome_oscillator_histogram");
}
}
[Fact]
public void Scalar_AverageDrawdown_MatchesGolden()
{
var input = Input();
var expected = ReadCsv("average_drawdown");
using var avg = new AverageDrawdown(20);
for (var i = 0; i < input.Length; i++)
{
// generator fed the close column as the equity-curve sample.
AssertClose(avg.Update(input[i][3]), Cell(expected[i][0]), i, "average_drawdown");
}
}
}
@@ -1,5 +1,6 @@
// <auto-generated />
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
#nullable enable
using System.Runtime.InteropServices;
namespace Wickra;
+1 -1
View File
@@ -11,7 +11,7 @@
<!-- NuGet package metadata -->
<PackageId>Wickra</PackageId>
<Version>0.9.1</Version>
<Version>0.9.2</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>
+7
View File
@@ -23,6 +23,13 @@ internal static class WickraNative
// Any exported symbol works as a fingerprint; sma_new exists in every build.
private const string SentinelSymbol = "wickra_sma_new";
// CA2255 warns against [ModuleInitializer] in libraries, but registering the
// native-library resolver before any P/Invoke runs is exactly the advanced
// scenario the attribute exists for: a static constructor would run too late
// (only on first access to this type), letting the default resolver fail first.
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Usage", "CA2255:The 'ModuleInitializer' attribute should not be used in libraries",
Justification = "The DllImport resolver must be registered before the first P/Invoke; a static constructor would run too late.")]
[ModuleInitializer]
internal static void Register()
{
+300
View File
@@ -0,0 +1,300 @@
"""Generate Wickra.Tests/GoldenAllTests.g.cs: a value-parity test that replays
the shared golden input through every one of the 514 C# indicators and checks
output bit-for-bit against the Rust reference fixtures g_<Canonical>.csv.
Run from repo root: python bindings/csharp/gen_golden_test.py
"""
import glob
import json
import os
import re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
G = os.path.join(ROOT, "testdata", "golden")
GEN = open(os.path.join(ROOT, "bindings", "csharp", "Wickra", "Generated", "Indicators.g.cs"), encoding="utf-8").read()
# C# constructor parameter types per class.
ctor_types = {}
cur = None
for line in GEN.splitlines():
m = re.match(r"public sealed class (\w+)", line)
if m:
cur = m.group(1)
continue
if cur:
cm = re.match(r"\s*public %s\(([^)]*)\)" % re.escape(cur), line)
if cm:
ps = cm.group(1).strip()
types = [p.strip().rsplit(" ", 1)[0].strip() for p in ps.split(",")] if ps else []
ctor_types[cur] = types
cur = None
# Unified archetype + params, keyed by canonical (== C# class name).
spec = {}
for e in json.load(open(os.path.join(G, "scalar_manifest.json"))):
arch = {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[e["input"]]
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
arch = {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[e["input"]]
spec[e["canonical"]] = {"arch": arch, "params": e["params"], "n": e["n"]}
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
for e in ex["deriv"]:
spec[e["canonical"]] = {"arch": "deriv_multi" if "n" in e else "deriv", "params": e["params"], "n": e.get("n")}
for e in ex["cross"]:
spec[e["canonical"]] = {"arch": "cross", "params": e["params"]}
for e in ex["trade"]:
spec[e["canonical"]] = {"arch": "trade", "params": e["params"]}
for e in ex["trademid"]:
spec[e["canonical"]] = {"arch": "trademid", "params": e["params"]}
for e in ex["ob"]:
spec[e["canonical"]] = {"arch": "ob", "params": e["params"]}
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
spec[e["canonical"]] = {"arch": "profile_" + e["kind"], "params": e["params"], "width": e["width"]}
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
arch = "footprint" if e["canonical"] == "Footprint" else "bars_" + e["feed"]
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
canons = sorted(os.path.basename(f)[2:-4] for f in glob.glob(os.path.join(G, "g_*.csv")))
def lit(value, cstype):
if cstype == "int":
return str(int(round(value)))
if cstype == "uint":
return f"{int(round(value))}u"
if cstype == "byte":
return f"(byte){int(round(value))}"
f = float(value)
s = repr(f)
return s if ("." in s or "e" in s or "E" in s) else s + ".0"
def ctor_call(canon):
types = ctor_types.get(canon, [])
vals = spec[canon]["params"]
args = ", ".join(lit(v, t) for v, t in zip(vals, types))
return f"new Wickra.{canon}({args})"
def block(canon):
s = spec[canon]
a = s["arch"]
L = [f" [Fact]", f" public void Golden_{canon}()", " {"]
L.append(f" using var ind = {ctor_call(canon)};")
L.append(" var got = new List<double[]>();")
L.append(" for (var i = 0; i < Rows.Length; i++)")
L.append(" {")
L.append(" var r = Rows[i];")
if a == "scalar_f64":
L.append(" got.Add(new[] { ind.Update(r[3]) });")
elif a == "pairwise":
L.append(" got.Add(new[] { ind.Update(r[3], r[0]) });")
elif a == "scalar_candle":
L.append(" got.Add(new[] { ind.Update(r[0], r[1], r[2], r[3], r[4], i) });")
elif a == "trade":
L.append(" got.Add(new[] { ind.Update(r[3], r[4], r[3] >= r[0], i) });")
elif a == "trademid":
L.append(" got.Add(new[] { ind.Update(r[3], r[4], r[3] >= r[0], i, (r[1] + r[2]) / 2) });")
elif a == "ob":
L.append(" var (bp, bs, ap, asz) = ObLists(r);")
L.append(" got.Add(new[] { ind.Update(bp, bs, ap, asz) });")
elif a == "deriv":
L.append(" var d = DerivFields(r);")
L.append(" got.Add(new[] { ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], i) });")
elif a == "deriv_multi":
L.append(" var d = DerivFields(r);")
L.append(" got.Add(FlattenNullable(ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], i), %d));" % s["n"])
elif a == "cross":
L.append(" var (ch, vo, nh, nl, am, ob) = CrossLists(r);")
L.append(" got.Add(new[] { ind.Update(ch, vo, nh, nl, am, ob, i) });")
elif a == "multi_f64":
L.append(" got.Add(FlattenNullable(ind.Update(r[3]), %d));" % s["n"])
elif a == "multi_pairwise":
L.append(" got.Add(FlattenNullable(ind.Update(r[3], r[0]), %d));" % s["n"])
elif a == "multi_candle":
L.append(" got.Add(FlattenNullable(ind.Update(r[0], r[1], r[2], r[3], r[4], i), %d));" % s["n"])
elif a == "profile_bins":
L.append(" var bins = ind.Update(r[0], r[1], r[2], r[3], r[4], i);")
L.append(" got.Add(bins ?? NanRow(%d));" % s["width"])
elif a == "profile_pricebins":
L.append(" got.Add(FlattenNullable(ind.Update(r[0], r[1], r[2], r[3], r[4], i), %d));" % s["width"])
elif a == "bars_close":
L.append(" got.Add(FlattenBars(ind.Update(r[3], r[3], r[3], r[3], 1.0, 0)));")
elif a == "bars_candle4":
L.append(" got.Add(FlattenBars(ind.Update(r[0], r[1], r[2], r[3], 1.0, 0)));")
elif a == "bars_candle5":
L.append(" got.Add(FlattenBars(ind.Update(r[0], r[1], r[2], r[3], r[4], 0)));")
elif a == "footprint":
L.append(" got.Add(FlattenBars(ind.Update(r[3], r[4], r[3] >= r[0], i)));")
else:
raise SystemExit("arch " + a)
L.append(" }")
L.append(f' Compare("{canon}", got);')
L.append(" }")
return "\n".join(L)
HEADER = '''// <auto-generated>
// Generated by gen_golden_test.py. DO NOT EDIT.
//
// Value-parity for every one of the 514 C# indicators: the shared golden input
// is replayed through each one and checked bit-for-bit against the Rust
// reference fixtures testdata/golden/g_<Canonical>.csv. Multi-output, profile
// and bar shapes are flattened by reflection so one comparator covers all
// archetypes. Regenerate with: python bindings/csharp/gen_golden_test.py
// </auto-generated>
#nullable enable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using Xunit;
namespace Wickra.Tests;
public class GoldenAllTests
{
private const double Tol = 1e-6;
private static readonly double[][] Rows = LoadInput();
private static string GoldenDir([System.Runtime.CompilerServices.CallerFilePath] string file = "") =>
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
private static double Cell(string s) =>
s == "nan" ? double.NaN
: s == "inf" ? double.PositiveInfinity
: s == "-inf" ? double.NegativeInfinity
: double.Parse(s, CultureInfo.InvariantCulture);
private static double[][] LoadInput()
{
var lines = File.ReadAllLines(Path.Combine(GoldenDir(), "input.csv"));
return lines.Skip(1).Where(l => l.Length > 0)
.Select(l => l.Split(',').Select(x => double.Parse(x, CultureInfo.InvariantCulture)).ToArray())
.ToArray();
}
// Keep blank lines (a candle on which no bar closed) so rows stay aligned.
private static double[]?[] ReadFixture(string name)
{
var lines = File.ReadAllLines(Path.Combine(GoldenDir(), "g_" + name + ".csv"));
return lines.Skip(1).Select(l => l.Length == 0 ? Array.Empty<double>() : l.Split(',').Select(Cell).ToArray()).ToArray();
}
private static double[] NanRow(int n)
{
var r = new double[n];
for (var i = 0; i < n; i++) r[i] = double.NaN;
return r;
}
private static double[] FlattenStruct(object o)
{
var props = o.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(p => p.MetadataToken);
var list = new List<double>();
foreach (var p in props)
{
var v = p.GetValue(o);
switch (v)
{
case double d: list.Add(d); break;
case float f: list.Add(f); break;
case long l: list.Add(l); break;
case int n: list.Add(n); break;
case double[] arr: list.AddRange(arr); break;
}
}
return list.ToArray();
}
private static double[] FlattenNullable<T>(T? value, int width) where T : struct =>
value.HasValue ? FlattenStruct(value.Value) : NanRow(width);
private static double[] FlattenBars<T>(T[] bars)
{
var list = new List<double>();
foreach (var bar in bars) list.AddRange(FlattenStruct(bar!));
return list.ToArray();
}
private static double[] DerivFields(double[] r)
{
double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4];
return new[]
{
(c - o) / c * 0.01, c, c - 0.5, c + 1.0, v * 10.0, v * 0.6, v * 0.4,
v * 0.55, v * 0.45, h - c, c - l,
};
}
private static (double[], double[], bool[], bool[], bool[], bool[]) CrossLists(double[] r)
{
double o = r[0], c = r[3], v = r[4];
var change = new double[5];
var volume = new double[5];
var nh = new bool[5];
var nl = new bool[5];
var am = new bool[5];
var ob = new bool[5];
for (var j = 0; j < 5; j++)
{
change[j] = (c - o) + j;
volume[j] = v + j * 10.0;
nh[j] = j % 2 == 0;
nl[j] = j % 3 == 0;
am[j] = j % 2 == 0;
ob[j] = j % 3 == 0;
}
return (change, volume, nh, nl, am, ob);
}
private static (double[], double[], double[], double[]) ObLists(double[] r)
{
double c = r[3], v = r[4];
var bp = new double[5];
var bs = new double[5];
var ap = new double[5];
var asz = new double[5];
for (var k = 0; k < 5; k++)
{
var kf = k + 1;
bp[k] = c - 0.1 * kf;
bs[k] = v / kf;
ap[k] = c + 0.1 * kf;
asz[k] = v * 0.9 / kf;
}
return (bp, bs, ap, asz);
}
private static void Compare(string name, List<double[]> got)
{
var exp = ReadFixture(name);
Assert.True(exp.Length == got.Count, $"{name}: {exp.Length} fixture rows vs {got.Count} computed");
for (var i = 0; i < exp.Length; i++)
{
var want = exp[i]!;
var g = got[i];
Assert.True(want.Length == g.Length, $"{name} row {i}: arity {g.Length} vs {want.Length}");
for (var k = 0; k < want.Length; k++)
{
var w = want[k];
if (double.IsNaN(w)) { Assert.True(double.IsNaN(g[k]), $"{name} row {i} col {k}: want NaN got {g[k]}"); continue; }
if (double.IsInfinity(w)) { Assert.True(double.IsInfinity(g[k]) && Math.Sign(g[k]) == Math.Sign(w), $"{name} row {i} col {k}: want {w} got {g[k]}"); continue; }
var tol = Tol * Math.Max(1.0, Math.Abs(w));
Assert.True(Math.Abs(g[k] - w) <= tol, $"{name} row {i} col {k}: got {g[k]} want {w}");
}
}
}
'''
out = [HEADER]
for canon in canons:
out.append(block(canon))
out.append("}")
open(os.path.join(ROOT, "bindings", "csharp", "Wickra.Tests", "GoldenAllTests.g.cs"), "w", encoding="utf-8").write("\n".join(out) + "\n")
print("generated GoldenAllTests.g.cs with", len(canons), "indicators")
+337
View File
@@ -0,0 +1,337 @@
"""Generate golden_all_test.go: a value-parity test that replays the shared
golden input through every one of the 514 Go indicators and checks output
bit-for-bit against the Rust-generated g_<Canonical>.csv fixtures.
Run from repo root: python bindings/go/gen_golden_test.py
"""
import glob
import json
import os
import re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
G = os.path.join(ROOT, "testdata", "golden")
GEN = open(os.path.join(ROOT, "bindings", "go", "indicators_gen.go"), encoding="utf-8").read()
# Go constructor parameter types, keyed by canonical (== Go type name).
ctor_types = {}
for m in re.finditer(r"func New(\w+)\(([^)]*)\)\s*\(\*\w+, error\)", GEN):
name, ps = m.group(1), m.group(2).strip()
types = []
if ps:
for p in ps.split(","):
p = p.strip()
_, _, ty = p.partition(" ")
types.append(ty.strip())
ctor_types[name] = types
# Unified archetype + params, keyed by canonical.
spec = {} # canon -> dict(arch, params, width?, n?)
scal = json.load(open(os.path.join(G, "scalar_manifest.json")))
for e in scal:
inp = e["input"]
arch = {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[inp]
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
inp = e["input"]
arch = {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[inp]
spec[e["canonical"]] = {"arch": arch, "params": e["params"], "n": e["n"]}
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
for e in ex["deriv"]:
spec[e["canonical"]] = {"arch": "deriv_multi" if "n" in e else "deriv", "params": e["params"], "n": e.get("n")}
for e in ex["cross"]:
spec[e["canonical"]] = {"arch": "cross", "params": e["params"]}
for e in ex["trade"]:
spec[e["canonical"]] = {"arch": "trade", "params": e["params"]}
for e in ex["trademid"]:
spec[e["canonical"]] = {"arch": "trademid", "params": e["params"]}
for e in ex["ob"]:
spec[e["canonical"]] = {"arch": "ob", "params": e["params"]}
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
spec[e["canonical"]] = {"arch": "profile_" + e["kind"], "params": e["params"], "width": e["width"]}
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
arch = "footprint" if e["canonical"] == "Footprint" else "bars_" + e["feed"]
spec[e["canonical"]] = {"arch": arch, "params": e["params"]}
canons = sorted(os.path.basename(f)[2:-4] for f in glob.glob(os.path.join(G, "g_*.csv")))
def go_param(value, gotype):
intlike = gotype in ("int", "int32", "int64", "uint", "uintptr", "usize")
if intlike:
return str(int(round(value)))
# float64
return repr(float(value)) if "." in repr(float(value)) or "e" in repr(float(value)) else f"{float(value)}"
def ctor_call(canon):
types = ctor_types.get(canon, [])
vals = spec[canon]["params"]
args = ", ".join(go_param(v, t) for v, t in zip(vals, types))
return f"New{canon}({args})"
# Update-call expression + output handling per archetype.
def block(canon):
s = spec[canon]
a = s["arch"]
ctor = ctor_call(canon)
lines = [f'\tt.Run("{canon}", func(t *testing.T) {{']
lines.append(f"\t\tind, err := {ctor}")
lines.append('\t\tif err != nil {')
lines.append(f'\t\t\tt.Fatalf("new {canon}: %v", err)')
lines.append("\t\t}")
lines.append("\t\tgot := make([][]float64, len(rows))")
lines.append("\t\tfor i, r := range rows {")
if a == "scalar_f64":
upd = "ind.Update(r[3])"
lines.append(f"\t\t\tgot[i] = []float64{{{upd}}}")
elif a == "pairwise":
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[0])}")
elif a == "scalar_candle":
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))}")
elif a == "trade":
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[4], r[3] >= r[0], int64(i))}")
elif a == "trademid":
lines.append("\t\t\tgot[i] = []float64{ind.Update(r[3], r[4], r[3] >= r[0], int64(i), (r[1]+r[2])/2)}")
elif a == "ob":
lines.append("\t\t\tbp, bs, ap, as_ := obLists(r)")
lines.append("\t\t\tgot[i] = []float64{ind.Update(bp, bs, ap, as_)}")
elif a == "deriv":
lines.append("\t\t\td := derivFields(r)")
lines.append("\t\t\tgot[i] = []float64{ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], int64(i))}")
elif a == "deriv_multi":
lines.append("\t\t\td := derivFields(r)")
lines.append("\t\t\tout, ok := ind.Update(d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], int64(i))")
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
elif a == "cross":
lines.append("\t\t\tch, vo, nh, nl, am, ob_ := crossLists(r)")
lines.append("\t\t\tgot[i] = []float64{ind.Update(ch, vo, nh, nl, am, ob_, int64(i))}")
elif a in ("multi_f64",):
lines.append("\t\t\tout, ok := ind.Update(r[3])")
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
elif a == "multi_pairwise":
lines.append("\t\t\tout, ok := ind.Update(r[3], r[0])")
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
elif a == "multi_candle":
lines.append("\t\t\tout, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['n']})")
elif a == "profile_bins":
lines.append("\t\t\tbins, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
lines.append(f"\t\t\tif ok {{ got[i] = bins }} else {{ got[i] = nanRow({s['width']}) }}")
elif a == "profile_pricebins":
lines.append("\t\t\tout, ok := ind.Update(r[0], r[1], r[2], r[3], r[4], int64(i))")
lines.append(f"\t\t\tgot[i] = reflectRow(out, ok, {s['width']})")
elif a == "bars_close":
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[3], r[3], r[3], r[3], 1.0, 0))")
elif a == "bars_candle4":
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[0], r[1], r[2], r[3], 1.0, 0))")
elif a == "bars_candle5":
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[0], r[1], r[2], r[3], r[4], 0))")
elif a == "footprint":
lines.append("\t\t\tgot[i] = flattenBars(ind.Update(r[3], r[4], r[3] >= r[0], int64(i)))")
else:
raise SystemExit("unknown arch " + a)
lines.append("\t\t}")
lines.append(f'\t\tcompareGolden(t, "{canon}", got)')
lines.append("\t})")
return "\n".join(lines)
HEADER = '''// Code generated by gen_golden_test.py. DO NOT EDIT.
//
// Value-parity for every one of the 514 Go indicators: the shared golden input
// is replayed through each one and checked bit-for-bit against the Rust
// reference fixtures testdata/golden/g_<Canonical>.csv. Multi-output, profile
// and bar shapes are flattened by reflection so a single comparator covers all
// archetypes. Regenerate with: python bindings/go/gen_golden_test.py
package wickra
import (
\t"bufio"
\t"math"
\t"os"
\t"reflect"
\t"strings"
\t"testing"
)
// readGoldenRaw keeps blank lines (a candle on which no bar closed) so bar rows
// stay aligned to the input; non-bar fixtures contain no blank lines.
func readGoldenRaw(t *testing.T, name string) [][]string {
\tt.Helper()
\tf, err := os.Open("../../testdata/golden/" + name + ".csv")
\tif err != nil {
\t\tt.Fatalf("open %s: %v", name, err)
\t}
\tdefer f.Close()
\tvar rows [][]string
\tsc := bufio.NewScanner(f)
\tsc.Buffer(make([]byte, 0, 1024*1024), 1024*1024)
\tfirst := true
\tfor sc.Scan() {
\t\tline := sc.Text()
\t\tif first {
\t\t\tfirst = false
\t\t\tcontinue
\t\t}
\t\tif line == "" {
\t\t\trows = append(rows, []string{})
\t\t\tcontinue
\t\t}
\t\trows = append(rows, strings.Split(line, ","))
\t}
\treturn rows
}
func nanRow(n int) []float64 {
\tr := make([]float64, n)
\tfor i := range r {
\t\tr[i] = math.NaN()
\t}
\treturn r
}
func reflectRow(out any, ok bool, width int) []float64 {
\tif !ok {
\t\treturn nanRow(width)
\t}
\tv := reflect.ValueOf(out)
\trow := make([]float64, 0, width)
\tfor k := 0; k < v.NumField(); k++ {
\t\trow = appendField(row, v.Field(k))
\t}
\treturn row
}
func appendField(row []float64, f reflect.Value) []float64 {
\tswitch f.Kind() {
\tcase reflect.Float64, reflect.Float32:
\t\treturn append(row, f.Float())
\tcase reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
\t\treturn append(row, float64(f.Int()))
\tcase reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
\t\treturn append(row, float64(f.Uint()))
\tcase reflect.Slice:
\t\tfor j := 0; j < f.Len(); j++ {
\t\t\trow = appendField(row, f.Index(j))
\t\t}
\t\treturn row
\tdefault:
\t\treturn row
\t}
}
func flattenBars(bars any) []float64 {
\tv := reflect.ValueOf(bars)
\trow := []float64{}
\tfor i := 0; i < v.Len(); i++ {
\t\tbar := v.Index(i)
\t\tfor k := 0; k < bar.NumField(); k++ {
\t\t\trow = appendField(row, bar.Field(k))
\t\t}
\t}
\treturn row
}
// Synthetic feeds derived from one OHLCV row, identical to gen_golden's Rust
// construction (DerivativesTick / CrossSection / OrderBook).
func derivFields(r []float64) [11]float64 {
\to, h, l, c, v := r[0], r[1], r[2], r[3], r[4]
\treturn [11]float64{
\t\t(c - o) / c * 0.01, // funding_rate
\t\tc, // mark_price
\t\tc - 0.5, // index_price
\t\tc + 1.0, // futures_price
\t\tv * 10.0, // open_interest
\t\tv * 0.6, // long_size
\t\tv * 0.4, // short_size
\t\tv * 0.55, // taker_buy_volume
\t\tv * 0.45, // taker_sell_volume
\t\th - c, // long_liquidation
\t\tc - l, // short_liquidation
\t}
}
func crossLists(r []float64) ([]float64, []float64, []bool, []bool, []bool, []bool) {
\to, c, v := r[0], r[3], r[4]
\tchange := make([]float64, 5)
\tvolume := make([]float64, 5)
\tnewHigh := make([]bool, 5)
\tnewLow := make([]bool, 5)
\taboveMa := make([]bool, 5)
\tonBuy := make([]bool, 5)
\tfor j := 0; j < 5; j++ {
\t\tjf := float64(j)
\t\tchange[j] = (c - o) + jf
\t\tvolume[j] = v + jf*10.0
\t\tnewHigh[j] = j%2 == 0
\t\tnewLow[j] = j%3 == 0
\t\taboveMa[j] = j%2 == 0
\t\tonBuy[j] = j%3 == 0
\t}
\treturn change, volume, newHigh, newLow, aboveMa, onBuy
}
func obLists(r []float64) ([]float64, []float64, []float64, []float64) {
\tc, v := r[3], r[4]
\tbidPx := make([]float64, 5)
\tbidSz := make([]float64, 5)
\taskPx := make([]float64, 5)
\taskSz := make([]float64, 5)
\tfor k := 0; k < 5; k++ {
\t\tkf := float64(k + 1)
\t\tbidPx[k] = c - 0.1*kf
\t\tbidSz[k] = v / kf
\t\taskPx[k] = c + 0.1*kf
\t\taskSz[k] = v * 0.9 / kf
\t}
\treturn bidPx, bidSz, askPx, askSz
}
func compareGolden(t *testing.T, name string, got [][]float64) {
\tt.Helper()
\texp := readGoldenRaw(t, "g_"+name)
\tif len(exp) != len(got) {
\t\tt.Fatalf("%s: %d fixture rows vs %d computed", name, len(exp), len(got))
\t}
\tfor i := range exp {
\t\tif len(exp[i]) != len(got[i]) {
\t\t\tt.Fatalf("%s row %d: arity %d vs %d", name, i, len(got[i]), len(exp[i]))
\t\t}
\t\tfor k := range exp[i] {
\t\t\twant := goldenCell(exp[i][k])
\t\t\tg := got[i][k]
\t\t\tif math.IsNaN(want) {
\t\t\t\tif !math.IsNaN(g) {
\t\t\t\t\tt.Fatalf("%s row %d col %d: want NaN got %v", name, i, k, g)
\t\t\t\t}
\t\t\t\tcontinue
\t\t\t}
\t\t\tif math.IsInf(want, 0) {
\t\t\t\tif !math.IsInf(g, 0) || (g > 0) != (want > 0) {
\t\t\t\t\tt.Fatalf("%s row %d col %d: want %v got %v", name, i, k, want, g)
\t\t\t\t}
\t\t\t\tcontinue
\t\t\t}
\t\t\ttol := goldenTol * math.Max(1.0, math.Abs(want))
\t\t\tif math.Abs(g-want) > tol {
\t\t\t\tt.Fatalf("%s row %d col %d: got %v want %v", name, i, k, g, want)
\t\t\t}
\t\t}
\t}
}
func TestGoldenAll(t *testing.T) {
\trows := goldenInput(t)
'''
# bars need blank-line-preserving fixture reads; reuse readGolden but it skips
# blanks. We need a raw reader for bars and input.
out = [HEADER]
for canon in canons:
out.append(block(canon))
out.append("}")
open(os.path.join(ROOT, "bindings", "go", "golden_all_test.go"), "w", encoding="utf-8").write("\n".join(out) + "\n")
print("generated golden_all_test.go with", len(canons), "indicators")
File diff suppressed because it is too large Load Diff
+59
View File
@@ -189,3 +189,62 @@ func TestGoldenAdx(t *testing.T) {
assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx")
}
}
// The four de-duplicated indicators: pin their corrected definitions against
// the Rust reference so the Go FFI stays bit-identical.
func TestGoldenAdOscillator(t *testing.T) {
input := goldenInput(t)
exp := readGolden(t, "ad_oscillator")
ad, err := NewAdOscillator()
if err != nil {
t.Fatal(err)
}
defer ad.Close()
for i := range input {
got := ad.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, "ad_oscillator")
}
}
func TestGoldenIntradayIntensity(t *testing.T) {
input := goldenInput(t)
exp := readGolden(t, "intraday_intensity")
ii, err := NewIntradayIntensity()
if err != nil {
t.Fatal(err)
}
defer ii.Close()
for i := range input {
got := ii.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, "intraday_intensity")
}
}
func TestGoldenAwesomeOscillatorHistogram(t *testing.T) {
input := goldenInput(t)
exp := readGolden(t, "awesome_oscillator_histogram")
aoh, err := NewAwesomeOscillatorHistogram(5, 34, 1)
if err != nil {
t.Fatal(err)
}
defer aoh.Close()
for i := range input {
got := aoh.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, "awesome_oscillator_histogram")
}
}
func TestGoldenAverageDrawdown(t *testing.T) {
input := goldenInput(t)
exp := readGolden(t, "average_drawdown")
avg, err := NewAverageDrawdown(20)
if err != nil {
t.Fatal(err)
}
defer avg.Close()
for i := range input {
// generator fed the close column as the equity-curve sample.
assertGoldenClose(t, avg.Update(input[i][3]), goldenCell(exp[i][0]), i, "average_drawdown")
}
}
+2 -2
View File
@@ -30,14 +30,14 @@ Maven:
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.9.1</version>
<version>0.9.2</version>
</dependency>
```
Gradle:
```kotlin
implementation("org.wickra:wickra:0.9.1")
implementation("org.wickra:wickra:0.9.2")
```
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
+279
View File
@@ -0,0 +1,279 @@
"""Generate src/test/java/org/wickra/GoldenAllTest.java: a reflection-driven
value-parity test that replays the shared golden input through every one of the
514 Java indicators and checks output bit-for-bit against the Rust reference
fixtures g_<Canonical>.csv. The per-indicator spec is embedded so the test has
no JSON dependency; a single reflective runner covers all archetypes.
Run from repo root: python bindings/java/gen_golden_test.py
"""
import json
import os
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
MAN = json.load(open(os.path.join(ROOT, "testdata", "golden", "golden_manifest.json")))
def params_lit(ps):
if not ps:
return "new double[]{}"
return "new double[]{" + ", ".join(repr(float(p)) for p in ps) + "}"
specs = []
for e in MAN:
n = e.get("n", e.get("width", 0))
specs.append(f' new Spec("{e["canonical"]}", "{e["arch"]}", {params_lit(e["params"])}, {n}),')
specs_block = "\n".join(specs)
TEMPLATE = '''// Code generated by gen_golden_test.py. DO NOT EDIT.
package org.wickra;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Reflection-driven value parity for the whole 514-indicator catalogue: each
* indicator is reconstructed by its class name, fed the synthetic stream derived
* from the shared golden input (identical to gen_golden's Rust construction) and
* checked bit-for-bit against testdata/golden/g_&lt;Canonical&gt;.csv. One runner
* flattens scalar, multi-output records, profiles and bar arrays by reflection.
*/
class GoldenAllTest {
private static final double TOL = 1e-6;
private record Spec(String canonical, String arch, double[] params, int width) {}
private static final Spec[] SPECS = {
%SPECS%
};
private static Path goldenDir() {
java.io.File d = new java.io.File("").getAbsoluteFile();
while (d != null) {
java.io.File g = new java.io.File(d, "testdata/golden");
if (g.isDirectory()) {
return g.toPath();
}
d = d.getParentFile();
}
throw new IllegalStateException("testdata/golden not found");
}
private static double cell(String s) {
return switch (s) {
case "nan" -> Double.NaN;
case "inf" -> Double.POSITIVE_INFINITY;
case "-inf" -> Double.NEGATIVE_INFINITY;
default -> Double.parseDouble(s);
};
}
private static double[][] input() throws IOException {
List<String> lines = Files.readAllLines(goldenDir().resolve("input.csv"));
List<double[]> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
if (lines.get(i).isEmpty()) continue;
String[] p = lines.get(i).split(",");
double[] r = new double[p.length];
for (int j = 0; j < p.length; j++) r[j] = Double.parseDouble(p[j]);
rows.add(r);
}
return rows.toArray(new double[0][]);
}
// Keep blank lines (a candle on which no bar closed) so rows stay aligned.
private static double[][] fixture(String name) throws IOException {
List<String> lines = Files.readAllLines(goldenDir().resolve("g_" + name + ".csv"));
List<double[]> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
String ln = lines.get(i);
if (ln.isEmpty()) {
rows.add(new double[0]);
continue;
}
String[] p = ln.split(",");
double[] r = new double[p.length];
for (int j = 0; j < p.length; j++) r[j] = cell(p[j]);
rows.add(r);
}
return rows.toArray(new double[0][]);
}
private static double[] nanRow(int n) {
double[] r = new double[n];
java.util.Arrays.fill(r, Double.NaN);
return r;
}
private static double[] derivFields(double[] r) {
double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4];
return new double[]{
(c - o) / c * 0.01, c, c - 0.5, c + 1.0, v * 10.0, v * 0.6, v * 0.4,
v * 0.55, v * 0.45, h - c, c - l,
};
}
private static double[] crossList(double[] r, int which) {
double o = r[0], c = r[3], v = r[4];
double[] out = new double[5];
for (int j = 0; j < 5; j++) {
out[j] = switch (which) {
case 0 -> (c - o) + j;
case 1 -> v + j * 10.0;
case 2 -> j % 2 == 0 ? 1.0 : 0.0; // newHigh
case 3 -> j % 3 == 0 ? 1.0 : 0.0; // newLow
case 4 -> j % 2 == 0 ? 1.0 : 0.0; // aboveMa
default -> j % 3 == 0 ? 1.0 : 0.0; // onBuySignal
};
}
return out;
}
private static double[] obList(double[] r, int which) {
double c = r[3], v = r[4];
double[] out = new double[5];
for (int k = 0; k < 5; k++) {
double kf = k + 1;
out[k] = switch (which) {
case 0 -> c - 0.1 * kf;
case 1 -> v / kf;
case 2 -> c + 0.1 * kf;
default -> v * 0.9 / kf;
};
}
return out;
}
private static double[] flattenRecord(Object o) throws Exception {
RecordComponent[] rc = o.getClass().getRecordComponents();
List<Double> list = new ArrayList<>();
for (RecordComponent c : rc) {
Object v = c.getAccessor().invoke(o);
if (v instanceof double[] arr) {
for (double d : arr) list.add(d);
} else if (v instanceof Number num) {
list.add(num.doubleValue());
}
}
double[] out = new double[list.size()];
for (int i = 0; i < out.length; i++) out[i] = list.get(i);
return out;
}
private static double[] flattenArray(Object arr) throws Exception {
int len = Array.getLength(arr);
List<Double> list = new ArrayList<>();
for (int i = 0; i < len; i++) {
for (double d : flattenRecord(Array.get(arr, i))) list.add(d);
}
double[] out = new double[list.size()];
for (int i = 0; i < out.length; i++) out[i] = list.get(i);
return out;
}
private static Object construct(Spec s) throws Exception {
Class<?> cls = Class.forName("org.wickra." + s.canonical());
Constructor<?> ctor = cls.getConstructors()[0];
Class<?>[] pt = ctor.getParameterTypes();
Object[] args = new Object[pt.length];
for (int i = 0; i < pt.length; i++) {
double v = s.params()[i];
if (pt[i] == int.class) args[i] = (int) Math.round(v);
else if (pt[i] == long.class) args[i] = (long) Math.round(v);
else args[i] = v;
}
return ctor.newInstance(args);
}
private static Method updateMethod(Object ind) {
for (Method m : ind.getClass().getMethods()) {
if (m.getName().equals("update")) return m;
}
throw new IllegalStateException("no update on " + ind.getClass());
}
private static double[] row(Spec s, Object ind, Method upd, double[] r, int i) throws Exception {
double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4];
Object res = switch (s.arch()) {
case "scalar_f64", "multi_f64" -> upd.invoke(ind, c);
case "pairwise", "multi_pairwise" -> upd.invoke(ind, c, o);
case "scalar_candle", "multi_candle", "profile_bins", "profile_pricebins" ->
upd.invoke(ind, o, h, l, c, v, (long) i);
case "trade" -> upd.invoke(ind, c, v, c >= o, (long) i);
case "trademid" -> upd.invoke(ind, c, v, c >= o, (long) i, (h + l) / 2);
case "ob" -> upd.invoke(ind, obList(r, 0), obList(r, 1), obList(r, 2), obList(r, 3));
case "cross" -> upd.invoke(ind, crossList(r, 0), crossList(r, 1), crossList(r, 2),
crossList(r, 3), crossList(r, 4), crossList(r, 5), (long) i);
case "deriv", "deriv_multi" -> {
double[] d = derivFields(r);
yield upd.invoke(ind, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], (long) i);
}
case "bars_close" -> upd.invoke(ind, c, c, c, c, 1.0, 0L);
case "bars_candle4" -> upd.invoke(ind, o, h, l, c, 1.0, 0L);
case "bars_candle5" -> upd.invoke(ind, o, h, l, c, v, 0L);
case "footprint" -> upd.invoke(ind, c, v, c >= o, (long) i);
default -> throw new IllegalStateException("arch " + s.arch());
};
return switch (s.arch()) {
case "scalar_f64", "scalar_candle", "pairwise", "trade", "trademid", "ob", "cross", "deriv" ->
new double[]{((Number) res).doubleValue()};
case "multi_f64", "multi_candle", "multi_pairwise", "deriv_multi", "profile_pricebins" ->
res == null ? nanRow(s.width()) : flattenRecord(res);
case "profile_bins" -> res == null ? nanRow(s.width()) : (double[]) res;
default -> flattenArray(res); // bars_*, footprint
};
}
@TestFactory
List<DynamicTest> golden() throws Exception {
double[][] rows = input();
List<DynamicTest> tests = new ArrayList<>();
for (Spec s : SPECS) {
tests.add(dynamicTest(s.canonical(), () -> {
Object ind = construct(s);
Method upd = updateMethod(ind);
double[][] exp = fixture(s.canonical());
assertTrue(exp.length == rows.length, s.canonical() + ": row count " + exp.length + " vs " + rows.length);
for (int i = 0; i < rows.length; i++) {
double[] got = row(s, ind, upd, rows[i], i);
double[] want = exp[i];
assertTrue(got.length == want.length,
s.canonical() + " row " + i + ": arity " + got.length + " vs " + want.length);
for (int k = 0; k < want.length; k++) {
double w = want[k], g = got[k];
if (Double.isNaN(w)) {
assertTrue(Double.isNaN(g), s.canonical() + " row " + i + " col " + k + ": want NaN got " + g);
} else if (Double.isInfinite(w)) {
assertTrue(Double.isInfinite(g) && Math.signum(g) == Math.signum(w),
s.canonical() + " row " + i + " col " + k + ": want " + w + " got " + g);
} else {
double tol = TOL * Math.max(1.0, Math.abs(w));
assertTrue(Math.abs(g - w) <= tol,
s.canonical() + " row " + i + " col " + k + ": got " + g + " want " + w);
}
}
}
}));
}
return tests;
}
}
'''
out = TEMPLATE.replace("%SPECS%", specs_block)
dest = os.path.join(ROOT, "bindings", "java", "src", "test", "java", "org", "wickra", "GoldenAllTest.java")
open(dest, "w", encoding="utf-8").write(out)
print("generated GoldenAllTest.java with", len(MAN), "indicators")
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.9.1</version>
<version>0.9.2</version>
<packaging>jar</packaging>
<name>Wickra</name>
@@ -47,10 +47,10 @@ public final class AbsoluteBreadthIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class AdVolumeLine implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_AD_VOLUME_LINE_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class AdvanceDecline implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_ADVANCE_DECLINE_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class AdvanceDeclineRatio implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_ADVANCE_DECLINE_RATIO_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -50,10 +50,10 @@ public final class BreadthThrust implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_BREADTH_THRUST_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class BullishPercentIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_BULLISH_PERCENT_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class CumulativeVolumeIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_CUMULATIVE_VOLUME_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -50,10 +50,10 @@ public final class HighLowIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_HIGH_LOW_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -25,7 +25,7 @@ public final class MacdExt implements AutoCloseable {
}
MemorySegment h;
try {
h = (MemorySegment) NativeMethods.WICKRA_MACD_EXT_NEW.invokeExact((long) fast, fastType, (long) slow, slowType, (long) signal, signalType);
h = (MemorySegment) NativeMethods.WICKRA_MACD_EXT_NEW.invokeExact((long) fast, (byte) fastType, (long) slow, (byte) slowType, (long) signal, (byte) signalType);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
@@ -47,10 +47,10 @@ public final class McClellanOscillator implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_MC_CLELLAN_OSCILLATOR_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class McClellanSummationIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_MC_CLELLAN_SUMMATION_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class NewHighsNewLows implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_NEW_HIGHS_NEW_LOWS_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class PercentAboveMa implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_PERCENT_ABOVE_MA_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class TickIndex implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_TICK_INDEX_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class Trin implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_TRIN_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -47,10 +47,10 @@ public final class UpDownVolumeRatio implements AutoCloseable {
try (Arena a = Arena.ofConfined()) {
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
MemorySegment newHighSeg = a.allocateFrom(JAVA_DOUBLE, newHigh);
MemorySegment newLowSeg = a.allocateFrom(JAVA_DOUBLE, newLow);
MemorySegment aboveMaSeg = a.allocateFrom(JAVA_DOUBLE, aboveMa);
MemorySegment onBuySignalSeg = a.allocateFrom(JAVA_DOUBLE, onBuySignal);
MemorySegment newHighSeg = WickraNative.boolSegment(a, newHigh);
MemorySegment newLowSeg = WickraNative.boolSegment(a, newLow);
MemorySegment aboveMaSeg = WickraNative.boolSegment(a, aboveMa);
MemorySegment onBuySignalSeg = WickraNative.boolSegment(a, onBuySignal);
return (double) NativeMethods.WICKRA_UP_DOWN_VOLUME_RATIO_UPDATE.invokeExact(handle, changeSeg, volumeSeg, newHighSeg, newLowSeg, aboveMaSeg, onBuySignalSeg, (long) change.length, timestamp);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
@@ -58,6 +58,20 @@ public final class WickraNative {
return CLEANER.register(owner, new FreeAction(handle, free));
}
/**
* Allocate a C {@code bool*} buffer (one byte per element) from flag values
* supplied as doubles, treating any non-zero value as {@code true}. The C
* ABI takes the cross-section state flags as {@code const bool*}, so they
* must be one byte each rather than eight-byte doubles.
*/
public static MemorySegment boolSegment(Arena arena, double[] flags) {
byte[] bytes = new byte[flags.length];
for (int i = 0; i < flags.length; i++) {
bytes[i] = (byte) (flags[i] != 0.0 ? 1 : 0);
}
return arena.allocateFrom(java.lang.foreign.ValueLayout.JAVA_BYTE, bytes);
}
/** Re-throw a {@link MethodHandle#invokeExact} {@link Throwable} as an unchecked exception. */
public static RuntimeException rethrow(Throwable t) {
if (t instanceof RuntimeException re) {
@@ -0,0 +1,759 @@
// Code generated by gen_golden_test.py. DO NOT EDIT.
package org.wickra;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.RecordComponent;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Reflection-driven value parity for the whole 514-indicator catalogue: each
* indicator is reconstructed by its class name, fed the synthetic stream derived
* from the shared golden input (identical to gen_golden's Rust construction) and
* checked bit-for-bit against testdata/golden/g_&lt;Canonical&gt;.csv. One runner
* flattens scalar, multi-output records, profiles and bar arrays by reflection.
*/
class GoldenAllTest {
private static final double TOL = 1e-6;
private record Spec(String canonical, String arch, double[] params, int width) {}
private static final Spec[] SPECS = {
new Spec("AbandonedBaby", "scalar_candle", new double[]{}, 0),
new Spec("Abcd", "scalar_candle", new double[]{}, 0),
new Spec("AbsoluteBreadthIndex", "cross", new double[]{}, 0),
new Spec("AccelerationBands", "multi_candle", new double[]{14.0, 2.0}, 3),
new Spec("AcceleratorOscillator", "scalar_candle", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("AdOscillator", "scalar_candle", new double[]{}, 0),
new Spec("AdVolumeLine", "cross", new double[]{}, 0),
new Spec("AdaptiveCci", "scalar_candle", new double[]{14.0}, 0),
new Spec("AdaptiveCycle", "scalar_f64", new double[]{}, 0),
new Spec("AdaptiveLaguerreFilter", "scalar_f64", new double[]{20.0}, 0),
new Spec("AdaptiveRsi", "scalar_f64", new double[]{14.0}, 0),
new Spec("Adl", "scalar_candle", new double[]{}, 0),
new Spec("AdvanceBlock", "scalar_candle", new double[]{}, 0),
new Spec("AdvanceDecline", "cross", new double[]{}, 0),
new Spec("AdvanceDeclineRatio", "cross", new double[]{}, 0),
new Spec("Adx", "multi_candle", new double[]{14.0}, 3),
new Spec("Adxr", "scalar_candle", new double[]{14.0}, 0),
new Spec("Alligator", "multi_candle", new double[]{3.0, 7.0, 14.0}, 3),
new Spec("Alma", "scalar_f64", new double[]{9.0, 0.85, 6.0}, 0),
new Spec("Alpha", "pairwise", new double[]{14.0, 2.0}, 0),
new Spec("AmihudIlliquidity", "trade", new double[]{20.0}, 0),
new Spec("AnchoredRsi", "scalar_f64", new double[]{}, 0),
new Spec("AnchoredVwap", "scalar_candle", new double[]{}, 0),
new Spec("AndrewsPitchfork", "multi_candle", new double[]{14.0}, 3),
new Spec("Apo", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Aroon", "multi_candle", new double[]{14.0}, 2),
new Spec("AroonOscillator", "scalar_candle", new double[]{14.0}, 0),
new Spec("Atr", "scalar_candle", new double[]{14.0}, 0),
new Spec("AtrBands", "multi_candle", new double[]{14.0, 2.0}, 3),
new Spec("AtrRatchet", "multi_candle", new double[]{14.0, 2.0, 0.5}, 2),
new Spec("AtrTrailingStop", "scalar_candle", new double[]{14.0, 2.0}, 0),
new Spec("AutoFib", "multi_candle", new double[]{}, 7),
new Spec("Autocorrelation", "scalar_f64", new double[]{10.0, 1.0}, 0),
new Spec("AutocorrelationPeriodogram", "scalar_f64", new double[]{10.0, 48.0}, 0),
new Spec("AverageDailyRange", "scalar_candle", new double[]{14.0, 0.0}, 0),
new Spec("AverageDrawdown", "scalar_f64", new double[]{14.0}, 0),
new Spec("AvgPrice", "scalar_candle", new double[]{}, 0),
new Spec("AwesomeOscillator", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("AwesomeOscillatorHistogram", "scalar_candle", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("BalanceOfPower", "scalar_candle", new double[]{}, 0),
new Spec("BandpassFilter", "scalar_f64", new double[]{20.0, 0.3}, 0),
new Spec("Bat", "scalar_candle", new double[]{}, 0),
new Spec("BeltHold", "scalar_candle", new double[]{}, 0),
new Spec("Beta", "pairwise", new double[]{14.0}, 0),
new Spec("BetaNeutralSpread", "pairwise", new double[]{14.0}, 0),
new Spec("BetterVolume", "scalar_candle", new double[]{14.0}, 0),
new Spec("BipowerVariation", "scalar_f64", new double[]{14.0}, 0),
new Spec("BodySizePct", "scalar_candle", new double[]{}, 0),
new Spec("BollingerBands", "multi_f64", new double[]{20.0, 2.0}, 4),
new Spec("BollingerBandwidth", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("BomarBands", "multi_f64", new double[]{4.0, 0.85}, 3),
new Spec("BreadthThrust", "cross", new double[]{10.0}, 0),
new Spec("Breakaway", "scalar_candle", new double[]{}, 0),
new Spec("BullishPercentIndex", "cross", new double[]{}, 0),
new Spec("BurkeRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("Butterfly", "scalar_candle", new double[]{}, 0),
new Spec("CalendarSpread", "deriv", new double[]{}, 0),
new Spec("CalmarRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("Camarilla", "multi_candle", new double[]{}, 9),
new Spec("CandleVolume", "multi_candle", new double[]{14.0}, 2),
new Spec("Cci", "scalar_candle", new double[]{14.0}, 0),
new Spec("CenterOfGravity", "scalar_f64", new double[]{14.0}, 0),
new Spec("CentralPivotRange", "multi_candle", new double[]{}, 3),
new Spec("Cfo", "scalar_f64", new double[]{14.0}, 0),
new Spec("ChaikinMoneyFlow", "scalar_candle", new double[]{20.0}, 0),
new Spec("ChaikinOscillator", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("ChaikinVolatility", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("ChandeKrollStop", "multi_candle", new double[]{3.0, 2.0, 7.0}, 2),
new Spec("ChandelierExit", "multi_candle", new double[]{14.0, 2.0}, 2),
new Spec("ChoppinessIndex", "scalar_candle", new double[]{14.0}, 0),
new Spec("ClassicPivots", "multi_candle", new double[]{}, 7),
new Spec("CloseVsOpen", "scalar_candle", new double[]{}, 0),
new Spec("ClosingMarubozu", "scalar_candle", new double[]{}, 0),
new Spec("Cmo", "scalar_f64", new double[]{14.0}, 0),
new Spec("CoefficientOfVariation", "scalar_f64", new double[]{14.0}, 0),
new Spec("Cointegration", "multi_pairwise", new double[]{40.0, 1.0}, 3),
new Spec("CommonSenseRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("CompositeProfile", "multi_candle", new double[]{20.0, 24.0, 0.7}, 3),
new Spec("ConcealingBabySwallow", "scalar_candle", new double[]{}, 0),
new Spec("ConditionalValueAtRisk", "scalar_f64", new double[]{20.0, 0.95}, 0),
new Spec("ConnorsRsi", "scalar_f64", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("Coppock", "scalar_f64", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("CorrelationTrendIndicator", "scalar_f64", new double[]{14.0}, 0),
new Spec("Counterattack", "scalar_candle", new double[]{}, 0),
new Spec("Crab", "scalar_candle", new double[]{}, 0),
new Spec("CumulativeVolumeDelta", "trade", new double[]{}, 0),
new Spec("CumulativeVolumeIndex", "cross", new double[]{}, 0),
new Spec("CupAndHandle", "scalar_candle", new double[]{}, 0),
new Spec("CyberneticCycle", "scalar_f64", new double[]{14.0}, 0),
new Spec("Cypher", "scalar_candle", new double[]{}, 0),
new Spec("DayOfWeekProfile", "profile_bins", new double[]{0.0}, 7),
new Spec("Decycler", "scalar_f64", new double[]{14.0}, 0),
new Spec("DecyclerOscillator", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Dema", "scalar_f64", new double[]{14.0}, 0),
new Spec("DemandIndex", "scalar_candle", new double[]{14.0}, 0),
new Spec("DemarkPivots", "multi_candle", new double[]{}, 3),
new Spec("DepthSlope", "ob", new double[]{}, 0),
new Spec("DerivativeOscillator", "scalar_f64", new double[]{3.0, 7.0, 14.0, 28.0}, 0),
new Spec("DetrendedStdDev", "scalar_f64", new double[]{14.0}, 0),
new Spec("DisparityIndex", "scalar_f64", new double[]{14.0}, 0),
new Spec("DistanceSsd", "pairwise", new double[]{14.0}, 0),
new Spec("Doji", "scalar_candle", new double[]{}, 0),
new Spec("DojiStar", "scalar_candle", new double[]{}, 0),
new Spec("DollarBars", "bars_candle5", new double[]{50000.0}, 0),
new Spec("Donchian", "multi_candle", new double[]{14.0}, 3),
new Spec("DonchianStop", "multi_candle", new double[]{14.0}, 2),
new Spec("DoubleBollinger", "multi_f64", new double[]{20.0, 1.0, 2.0}, 5),
new Spec("DoubleTopBottom", "scalar_candle", new double[]{}, 0),
new Spec("DownsideGapThreeMethods", "scalar_candle", new double[]{}, 0),
new Spec("Dpo", "scalar_f64", new double[]{14.0}, 0),
new Spec("DragonflyDoji", "scalar_candle", new double[]{}, 0),
new Spec("DrawdownDuration", "scalar_f64", new double[]{}, 0),
new Spec("DumplingTop", "scalar_candle", new double[]{14.0}, 0),
new Spec("Dx", "scalar_candle", new double[]{14.0}, 0),
new Spec("DynamicMomentumIndex", "scalar_f64", new double[]{14.0}, 0),
new Spec("EaseOfMovement", "scalar_candle", new double[]{14.0}, 0),
new Spec("EffectiveSpread", "trademid", new double[]{}, 0),
new Spec("EhlersStochastic", "scalar_f64", new double[]{14.0}, 0),
new Spec("Ehma", "scalar_f64", new double[]{14.0}, 0),
new Spec("ElderImpulse", "scalar_f64", new double[]{3.0, 7.0, 14.0, 28.0}, 0),
new Spec("ElderRay", "multi_candle", new double[]{14.0}, 2),
new Spec("ElderSafeZone", "multi_candle", new double[]{10.0, 2.0}, 2),
new Spec("Ema", "scalar_f64", new double[]{14.0}, 0),
new Spec("EmpiricalModeDecomposition", "scalar_f64", new double[]{20.0, 0.1}, 0),
new Spec("Engulfing", "scalar_candle", new double[]{}, 0),
new Spec("Equivolume", "multi_candle", new double[]{14.0}, 2),
new Spec("EstimatedLeverageRatio", "deriv", new double[]{}, 0),
new Spec("EvenBetterSinewave", "scalar_f64", new double[]{40.0, 10.0}, 0),
new Spec("EveningDojiStar", "scalar_candle", new double[]{}, 0),
new Spec("Evwma", "scalar_candle", new double[]{14.0}, 0),
new Spec("EwmaVolatility", "scalar_f64", new double[]{0.94}, 0),
new Spec("Expectancy", "scalar_f64", new double[]{14.0}, 0),
new Spec("FallingThreeMethods", "scalar_candle", new double[]{}, 0),
new Spec("Fama", "scalar_f64", new double[]{0.5, 0.05}, 0),
new Spec("FibArcs", "multi_candle", new double[]{}, 3),
new Spec("FibChannel", "multi_candle", new double[]{}, 4),
new Spec("FibConfluence", "multi_candle", new double[]{}, 2),
new Spec("FibExtension", "multi_candle", new double[]{}, 5),
new Spec("FibFan", "multi_candle", new double[]{}, 3),
new Spec("FibProjection", "multi_candle", new double[]{}, 4),
new Spec("FibRetracement", "multi_candle", new double[]{}, 7),
new Spec("FibTimeZones", "multi_candle", new double[]{}, 2),
new Spec("FibonacciPivots", "multi_candle", new double[]{}, 7),
new Spec("FisherRsi", "scalar_f64", new double[]{14.0}, 0),
new Spec("FisherTransform", "scalar_f64", new double[]{14.0}, 0),
new Spec("FlagPennant", "scalar_candle", new double[]{}, 0),
new Spec("Footprint", "footprint", new double[]{1.0}, 0),
new Spec("ForceIndex", "scalar_candle", new double[]{14.0}, 0),
new Spec("FractalChaosBands", "multi_candle", new double[]{14.0}, 2),
new Spec("Frama", "scalar_f64", new double[]{14.0}, 0),
new Spec("FryPanBottom", "scalar_candle", new double[]{14.0}, 0),
new Spec("FundingBasis", "deriv", new double[]{}, 0),
new Spec("FundingImpliedApr", "deriv", new double[]{1095.0}, 0),
new Spec("FundingRate", "deriv", new double[]{}, 0),
new Spec("FundingRateMean", "deriv", new double[]{20.0}, 0),
new Spec("FundingRateZScore", "deriv", new double[]{20.0}, 0),
new Spec("GainLossRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("GainToPainRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("GapSideBySideWhite", "scalar_candle", new double[]{}, 0),
new Spec("Garch11", "scalar_f64", new double[]{2e-06, 0.1, 0.88}, 0),
new Spec("GarmanKlassVolatility", "scalar_candle", new double[]{20.0, 252.0}, 0),
new Spec("Gartley", "scalar_candle", new double[]{}, 0),
new Spec("GatorOscillator", "multi_candle", new double[]{3.0, 7.0, 14.0}, 2),
new Spec("GeneralizedDema", "scalar_f64", new double[]{5.0, 0.7}, 0),
new Spec("GeometricMa", "scalar_f64", new double[]{14.0}, 0),
new Spec("GoldenPocket", "multi_candle", new double[]{}, 3),
new Spec("GrangerCausality", "pairwise", new double[]{60.0, 1.0}, 0),
new Spec("GravestoneDoji", "scalar_candle", new double[]{}, 0),
new Spec("Hammer", "scalar_candle", new double[]{}, 0),
new Spec("HangingMan", "scalar_candle", new double[]{}, 0),
new Spec("Harami", "scalar_candle", new double[]{}, 0),
new Spec("HaramiCross", "scalar_candle", new double[]{}, 0),
new Spec("HasbrouckInformationShare", "pairwise", new double[]{14.0}, 0),
new Spec("HeadAndShoulders", "scalar_candle", new double[]{}, 0),
new Spec("HeikinAshi", "multi_candle", new double[]{}, 4),
new Spec("HeikinAshiOscillator", "scalar_candle", new double[]{14.0}, 0),
new Spec("HiLoActivator", "scalar_candle", new double[]{14.0}, 0),
new Spec("HighLowIndex", "cross", new double[]{10.0}, 0),
new Spec("HighLowRange", "scalar_candle", new double[]{}, 0),
new Spec("HighLowVolumeNodes", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("HighWave", "scalar_candle", new double[]{}, 0),
new Spec("HighpassFilter", "scalar_f64", new double[]{14.0}, 0),
new Spec("Hikkake", "scalar_candle", new double[]{}, 0),
new Spec("HikkakeModified", "scalar_candle", new double[]{}, 0),
new Spec("HilbertDominantCycle", "scalar_f64", new double[]{}, 0),
new Spec("HistoricalVolatility", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Hma", "scalar_f64", new double[]{14.0}, 0),
new Spec("HoltWinters", "scalar_f64", new double[]{0.5, 0.1}, 0),
new Spec("HomingPigeon", "scalar_candle", new double[]{}, 0),
new Spec("HtDcPhase", "scalar_f64", new double[]{}, 0),
new Spec("HtPhasor", "multi_f64", new double[]{}, 2),
new Spec("HtTrendMode", "scalar_f64", new double[]{}, 0),
new Spec("HurstChannel", "multi_candle", new double[]{14.0, 2.0}, 3),
new Spec("HurstExponent", "scalar_f64", new double[]{100.0, 4.0}, 0),
new Spec("Ichimoku", "multi_candle", new double[]{9.0, 26.0, 52.0, 26.0}, 5),
new Spec("IdenticalThreeCrows", "scalar_candle", new double[]{}, 0),
new Spec("ImbalanceBars", "bars_candle4", new double[]{5.0}, 0),
new Spec("InNeck", "scalar_candle", new double[]{}, 0),
new Spec("Inertia", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("InformationRatio", "pairwise", new double[]{14.0}, 0),
new Spec("InitialBalance", "multi_candle", new double[]{14.0}, 2),
new Spec("InstantaneousTrendline", "scalar_f64", new double[]{14.0}, 0),
new Spec("IntradayIntensity", "scalar_candle", new double[]{}, 0),
new Spec("IntradayMomentumIndex", "scalar_candle", new double[]{14.0}, 0),
new Spec("IntradayVolatilityProfile", "profile_bins", new double[]{24.0, 0.0}, 24),
new Spec("InverseFisherTransform", "scalar_f64", new double[]{2.0}, 0),
new Spec("InvertedHammer", "scalar_candle", new double[]{}, 0),
new Spec("JarqueBera", "scalar_f64", new double[]{14.0}, 0),
new Spec("Jma", "scalar_f64", new double[]{7.0, 0.0, 2.0}, 0),
new Spec("JumpIndicator", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("KRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("KagiBars", "bars_close", new double[]{2.0}, 0),
new Spec("KalmanHedgeRatio", "multi_pairwise", new double[]{0.01, 0.001}, 3),
new Spec("Kama", "scalar_f64", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("KaseDevStop", "multi_candle", new double[]{14.0, 2.0}, 2),
new Spec("KasePermissionStochastic", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("KellyCriterion", "scalar_f64", new double[]{14.0}, 0),
new Spec("Keltner", "multi_candle", new double[]{3.0, 7.0, 2.0}, 3),
new Spec("KendallTau", "pairwise", new double[]{14.0}, 0),
new Spec("Kicking", "scalar_candle", new double[]{}, 0),
new Spec("KickingByLength", "scalar_candle", new double[]{}, 0),
new Spec("Kst", "multi_f64", new double[]{3.0, 7.0, 14.0, 28.0, 35.0, 42.0, 56.0, 63.0, 70.0}, 2),
new Spec("Kurtosis", "scalar_f64", new double[]{14.0}, 0),
new Spec("Kvo", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("KylesLambda", "trademid", new double[]{20.0}, 0),
new Spec("LadderBottom", "scalar_candle", new double[]{}, 0),
new Spec("LaguerreRsi", "scalar_f64", new double[]{0.5}, 0),
new Spec("LeadLagCrossCorrelation", "multi_pairwise", new double[]{20.0, 10.0}, 2),
new Spec("LinRegAngle", "scalar_f64", new double[]{14.0}, 0),
new Spec("LinRegChannel", "multi_f64", new double[]{14.0, 2.0}, 3),
new Spec("LinRegIntercept", "scalar_f64", new double[]{14.0}, 0),
new Spec("LinRegSlope", "scalar_f64", new double[]{14.0}, 0),
new Spec("LinearRegression", "scalar_f64", new double[]{14.0}, 0),
new Spec("LiquidationFeatures", "deriv_multi", new double[]{}, 5),
new Spec("LogReturn", "scalar_f64", new double[]{14.0}, 0),
new Spec("LongLeggedDoji", "scalar_candle", new double[]{}, 0),
new Spec("LongLine", "scalar_candle", new double[]{}, 0),
new Spec("LongShortRatio", "deriv", new double[]{}, 0),
new Spec("M2Measure", "scalar_f64", new double[]{14.0, 2.0, 0.5}, 0),
new Spec("MaEnvelope", "multi_f64", new double[]{14.0, 2.0}, 3),
new Spec("MacdExt", "multi_f64", new double[]{12.0, 0.0, 26.0, 0.0, 9.0, 0.0}, 3),
new Spec("MacdFix", "multi_f64", new double[]{9.0}, 3),
new Spec("MacdHistogram", "scalar_f64", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("MacdIndicator", "multi_f64", new double[]{12.0, 26.0, 9.0}, 3),
new Spec("Mama", "multi_f64", new double[]{0.5, 0.05}, 2),
new Spec("MarketFacilitationIndex", "scalar_candle", new double[]{}, 0),
new Spec("MartinRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("Marubozu", "scalar_candle", new double[]{}, 0),
new Spec("MassIndex", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("MatHold", "scalar_candle", new double[]{}, 0),
new Spec("MatchingLow", "scalar_candle", new double[]{}, 0),
new Spec("MaxDrawdown", "scalar_f64", new double[]{14.0}, 0),
new Spec("McClellanOscillator", "cross", new double[]{}, 0),
new Spec("McClellanSummationIndex", "cross", new double[]{}, 0),
new Spec("McGinleyDynamic", "scalar_f64", new double[]{14.0}, 0),
new Spec("MedianAbsoluteDeviation", "scalar_f64", new double[]{14.0}, 0),
new Spec("MedianChannel", "multi_f64", new double[]{14.0, 2.0}, 3),
new Spec("MedianMa", "scalar_f64", new double[]{14.0}, 0),
new Spec("MedianPrice", "scalar_candle", new double[]{}, 0),
new Spec("Mfi", "scalar_candle", new double[]{14.0}, 0),
new Spec("Microprice", "ob", new double[]{}, 0),
new Spec("MidPoint", "scalar_f64", new double[]{14.0}, 0),
new Spec("MidPrice", "scalar_candle", new double[]{14.0}, 0),
new Spec("MinusDi", "scalar_candle", new double[]{14.0}, 0),
new Spec("MinusDm", "scalar_candle", new double[]{14.0}, 0),
new Spec("ModifiedMaStop", "multi_candle", new double[]{14.0}, 2),
new Spec("Mom", "scalar_f64", new double[]{14.0}, 0),
new Spec("MorningDojiStar", "scalar_candle", new double[]{}, 0),
new Spec("MorningEveningStar", "scalar_candle", new double[]{}, 0),
new Spec("MurreyMathLines", "multi_candle", new double[]{14.0}, 9),
new Spec("NakedPoc", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("Natr", "scalar_candle", new double[]{14.0}, 0),
new Spec("NewHighsNewLows", "cross", new double[]{}, 0),
new Spec("NewPriceLines", "scalar_candle", new double[]{14.0}, 0),
new Spec("Nrtr", "multi_candle", new double[]{2.0}, 2),
new Spec("Nvi", "scalar_candle", new double[]{}, 0),
new Spec("OIPriceDivergence", "deriv", new double[]{20.0}, 0),
new Spec("OIWeighted", "deriv", new double[]{}, 0),
new Spec("Obv", "scalar_candle", new double[]{}, 0),
new Spec("OiToVolumeRatio", "deriv", new double[]{}, 0),
new Spec("OmegaRatio", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("OnNeck", "scalar_candle", new double[]{}, 0),
new Spec("OpenInterestDelta", "deriv", new double[]{}, 0),
new Spec("OpenInterestMomentum", "deriv", new double[]{10.0}, 0),
new Spec("OpeningMarubozu", "scalar_candle", new double[]{}, 0),
new Spec("OpeningRange", "multi_candle", new double[]{14.0}, 3),
new Spec("OrderBookImbalanceFull", "ob", new double[]{}, 0),
new Spec("OrderBookImbalanceTop1", "ob", new double[]{}, 0),
new Spec("OrderBookImbalanceTopN", "ob", new double[]{5.0}, 0),
new Spec("OrderFlowImbalance", "ob", new double[]{20.0}, 0),
new Spec("OuHalfLife", "pairwise", new double[]{14.0}, 0),
new Spec("OvernightGap", "scalar_candle", new double[]{0.0}, 0),
new Spec("OvernightIntradayReturn", "multi_candle", new double[]{14.0}, 2),
new Spec("PainIndex", "scalar_f64", new double[]{14.0}, 0),
new Spec("PairSpreadZScore", "pairwise", new double[]{20.0, 20.0}, 0),
new Spec("PairwiseBeta", "pairwise", new double[]{14.0}, 0),
new Spec("ParkinsonVolatility", "scalar_candle", new double[]{20.0, 252.0}, 0),
new Spec("PearsonCorrelation", "pairwise", new double[]{14.0}, 0),
new Spec("PercentAboveMa", "cross", new double[]{}, 0),
new Spec("PercentB", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("PercentageTrailingStop", "scalar_f64", new double[]{2.0}, 0),
new Spec("PerpetualPremiumIndex", "deriv", new double[]{}, 0),
new Spec("Pgo", "scalar_candle", new double[]{14.0}, 0),
new Spec("PiercingDarkCloud", "scalar_candle", new double[]{}, 0),
new Spec("Pin", "trade", new double[]{20.0}, 0),
new Spec("PivotReversal", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("PlusDi", "scalar_candle", new double[]{14.0}, 0),
new Spec("PlusDm", "scalar_candle", new double[]{14.0}, 0),
new Spec("Pmo", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("PointAndFigureBars", "bars_close", new double[]{2.0, 3.0}, 0),
new Spec("PolarizedFractalEfficiency", "scalar_f64", new double[]{10.0, 5.0}, 0),
new Spec("Ppo", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("PpoHistogram", "scalar_f64", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("ProfileShape", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("ProfitFactor", "scalar_f64", new double[]{14.0}, 0),
new Spec("ProjectionBands", "multi_candle", new double[]{14.0}, 3),
new Spec("ProjectionOscillator", "scalar_candle", new double[]{14.0}, 0),
new Spec("Psar", "scalar_candle", new double[]{0.02, 0.02, 0.2}, 0),
new Spec("Pvi", "scalar_candle", new double[]{}, 0),
new Spec("Qqe", "multi_f64", new double[]{3.0, 7.0, 2.0}, 2),
new Spec("Qstick", "scalar_candle", new double[]{14.0}, 0),
new Spec("QuartileBands", "multi_f64", new double[]{14.0}, 3),
new Spec("QuotedSpread", "ob", new double[]{}, 0),
new Spec("RSquared", "scalar_f64", new double[]{14.0}, 0),
new Spec("RangeBars", "bars_close", new double[]{2.0}, 0),
new Spec("RealizedSpread", "trademid", new double[]{20.0}, 0),
new Spec("RealizedVolatility", "scalar_f64", new double[]{14.0}, 0),
new Spec("RecoveryFactor", "scalar_f64", new double[]{}, 0),
new Spec("RectangleRange", "scalar_candle", new double[]{}, 0),
new Spec("Reflex", "scalar_f64", new double[]{14.0}, 0),
new Spec("RegimeLabel", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("RelativeStrengthAB", "multi_pairwise", new double[]{14.0, 14.0}, 3),
new Spec("RenkoBars", "bars_close", new double[]{2.0}, 0),
new Spec("RenkoTrailingStop", "scalar_f64", new double[]{2.0}, 0),
new Spec("RickshawMan", "scalar_candle", new double[]{}, 0),
new Spec("RisingThreeMethods", "scalar_candle", new double[]{}, 0),
new Spec("Rmi", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Roc", "scalar_f64", new double[]{14.0}, 0),
new Spec("Rocp", "scalar_f64", new double[]{14.0}, 0),
new Spec("Rocr", "scalar_f64", new double[]{14.0}, 0),
new Spec("Rocr100", "scalar_f64", new double[]{14.0}, 0),
new Spec("RogersSatchellVolatility", "scalar_candle", new double[]{20.0, 252.0}, 0),
new Spec("RollMeasure", "trade", new double[]{20.0}, 0),
new Spec("RollingCorrelation", "pairwise", new double[]{14.0}, 0),
new Spec("RollingCovariance", "pairwise", new double[]{14.0}, 0),
new Spec("RollingIqr", "scalar_f64", new double[]{14.0}, 0),
new Spec("RollingMinMaxScaler", "scalar_f64", new double[]{14.0}, 0),
new Spec("RollingPercentileRank", "scalar_f64", new double[]{14.0}, 0),
new Spec("RollingQuantile", "scalar_f64", new double[]{20.0, 0.5}, 0),
new Spec("RollingVwap", "scalar_candle", new double[]{14.0}, 0),
new Spec("RoofingFilter", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Rsi", "scalar_f64", new double[]{14.0}, 0),
new Spec("Rsx", "scalar_f64", new double[]{14.0}, 0),
new Spec("RunBars", "bars_candle4", new double[]{3.0}, 0),
new Spec("Rvi", "scalar_candle", new double[]{14.0}, 0),
new Spec("RviVolatility", "scalar_f64", new double[]{14.0}, 0),
new Spec("Rwi", "multi_candle", new double[]{14.0}, 2),
new Spec("SampleEntropy", "scalar_f64", new double[]{20.0, 2.0, 0.2}, 0),
new Spec("SarExt", "scalar_candle", new double[]{2.0, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5}, 0),
new Spec("SeasonalZScore", "scalar_candle", new double[]{14.0}, 0),
new Spec("SeparatingLines", "scalar_candle", new double[]{}, 0),
new Spec("SessionHighLow", "multi_candle", new double[]{14.0}, 2),
new Spec("SessionRange", "multi_candle", new double[]{14.0}, 3),
new Spec("SessionVwap", "scalar_candle", new double[]{14.0}, 0),
new Spec("ShannonEntropy", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Shark", "scalar_candle", new double[]{}, 0),
new Spec("SharpeRatio", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("ShootingStar", "scalar_candle", new double[]{}, 0),
new Spec("ShortLine", "scalar_candle", new double[]{}, 0),
new Spec("SignedVolume", "trade", new double[]{}, 0),
new Spec("SineWave", "scalar_f64", new double[]{}, 0),
new Spec("SineWeightedMa", "scalar_f64", new double[]{14.0}, 0),
new Spec("SinglePrints", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("Skewness", "scalar_f64", new double[]{14.0}, 0),
new Spec("Sma", "scalar_f64", new double[]{14.0}, 0),
new Spec("Smi", "scalar_candle", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("Smma", "scalar_f64", new double[]{14.0}, 0),
new Spec("SmoothedHeikinAshi", "multi_candle", new double[]{14.0}, 4),
new Spec("SortinoRatio", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("SpearmanCorrelation", "pairwise", new double[]{14.0}, 0),
new Spec("SpinningTop", "scalar_candle", new double[]{}, 0),
new Spec("SpreadAr1Coefficient", "pairwise", new double[]{14.0}, 0),
new Spec("SpreadBollingerBands", "multi_pairwise", new double[]{14.0, 2.0}, 4),
new Spec("SpreadHurst", "pairwise", new double[]{14.0}, 0),
new Spec("StalledPattern", "scalar_candle", new double[]{}, 0),
new Spec("StandardError", "scalar_f64", new double[]{14.0}, 0),
new Spec("StandardErrorBands", "multi_f64", new double[]{14.0, 2.0}, 3),
new Spec("StarcBands", "multi_candle", new double[]{3.0, 7.0, 2.0}, 3),
new Spec("Stc", "scalar_f64", new double[]{10.0, 23.0, 10.0, 0.5}, 0),
new Spec("StdDev", "scalar_f64", new double[]{14.0}, 0),
new Spec("StepTrailingStop", "scalar_f64", new double[]{2.0}, 0),
new Spec("SterlingRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("StickSandwich", "scalar_candle", new double[]{}, 0),
new Spec("StochRsi", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Stochastic", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("StochasticCci", "scalar_candle", new double[]{14.0}, 0),
new Spec("SuperSmoother", "scalar_f64", new double[]{14.0}, 0),
new Spec("SuperTrend", "multi_candle", new double[]{14.0, 2.0}, 2),
new Spec("T3", "scalar_f64", new double[]{5.0, 0.7}, 0),
new Spec("TailRatio", "scalar_f64", new double[]{14.0}, 0),
new Spec("TakerBuySellRatio", "deriv", new double[]{}, 0),
new Spec("Takuri", "scalar_candle", new double[]{}, 0),
new Spec("TasukiGap", "scalar_candle", new double[]{}, 0),
new Spec("TdCamouflage", "scalar_candle", new double[]{}, 0),
new Spec("TdClop", "scalar_candle", new double[]{}, 0),
new Spec("TdClopwin", "scalar_candle", new double[]{}, 0),
new Spec("TdCombo", "scalar_candle", new double[]{3.0, 7.0, 14.0, 28.0}, 0),
new Spec("TdCountdown", "scalar_candle", new double[]{3.0, 7.0, 14.0, 28.0}, 0),
new Spec("TdDWave", "scalar_candle", new double[]{2.0}, 0),
new Spec("TdDeMarker", "scalar_candle", new double[]{14.0}, 0),
new Spec("TdDifferential", "scalar_candle", new double[]{}, 0),
new Spec("TdLines", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("TdMovingAverage", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("TdOpen", "scalar_candle", new double[]{}, 0),
new Spec("TdPressure", "scalar_candle", new double[]{14.0}, 0),
new Spec("TdPropulsion", "scalar_candle", new double[]{}, 0),
new Spec("TdRangeProjection", "multi_candle", new double[]{}, 2),
new Spec("TdRei", "scalar_candle", new double[]{14.0}, 0),
new Spec("TdRiskLevel", "multi_candle", new double[]{3.0, 7.0}, 2),
new Spec("TdSequential", "multi_candle", new double[]{3.0, 7.0, 14.0, 28.0}, 3),
new Spec("TdSetup", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("TdTrap", "scalar_candle", new double[]{}, 0),
new Spec("Tema", "scalar_f64", new double[]{14.0}, 0),
new Spec("TermStructureBasis", "deriv", new double[]{}, 0),
new Spec("ThreeDrives", "scalar_candle", new double[]{}, 0),
new Spec("ThreeInside", "scalar_candle", new double[]{}, 0),
new Spec("ThreeLineBreak", "scalar_candle", new double[]{14.0}, 0),
new Spec("ThreeLineBreakBars", "bars_close", new double[]{3.0}, 0),
new Spec("ThreeLineStrike", "scalar_candle", new double[]{}, 0),
new Spec("ThreeOutside", "scalar_candle", new double[]{}, 0),
new Spec("ThreeSoldiersOrCrows", "scalar_candle", new double[]{}, 0),
new Spec("ThreeStarsInSouth", "scalar_candle", new double[]{}, 0),
new Spec("Thrusting", "scalar_candle", new double[]{}, 0),
new Spec("TickBars", "bars_candle5", new double[]{2.0}, 0),
new Spec("TickIndex", "cross", new double[]{}, 0),
new Spec("Tii", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("TimeBasedStop", "scalar_candle", new double[]{14.0}, 0),
new Spec("TimeOfDayReturnProfile", "profile_bins", new double[]{24.0, 0.0}, 24),
new Spec("TowerTopBottom", "scalar_candle", new double[]{}, 0),
new Spec("TpoProfile", "profile_pricebins", new double[]{30.0, 50.0}, 52),
new Spec("TradeImbalance", "trade", new double[]{20.0}, 0),
new Spec("TradeSignAutocorrelation", "trade", new double[]{20.0}, 0),
new Spec("TradeVolumeIndex", "scalar_candle", new double[]{2.0}, 0),
new Spec("TrendLabel", "scalar_f64", new double[]{14.0}, 0),
new Spec("TrendStrengthIndex", "scalar_f64", new double[]{14.0}, 0),
new Spec("Trendflex", "scalar_f64", new double[]{14.0}, 0),
new Spec("TreynorRatio", "pairwise", new double[]{14.0, 2.0}, 0),
new Spec("Triangle", "scalar_candle", new double[]{}, 0),
new Spec("Trima", "scalar_f64", new double[]{14.0}, 0),
new Spec("Trin", "cross", new double[]{}, 0),
new Spec("TripleTopBottom", "scalar_candle", new double[]{}, 0),
new Spec("Tristar", "scalar_candle", new double[]{}, 0),
new Spec("Trix", "scalar_f64", new double[]{14.0}, 0),
new Spec("TrueRange", "scalar_candle", new double[]{}, 0),
new Spec("Tsf", "scalar_f64", new double[]{14.0}, 0),
new Spec("TsfOscillator", "scalar_f64", new double[]{14.0}, 0),
new Spec("Tsi", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("Tsv", "scalar_candle", new double[]{14.0}, 0),
new Spec("TtmSqueeze", "multi_candle", new double[]{14.0, 2.0, 0.5}, 2),
new Spec("TtmTrend", "scalar_candle", new double[]{14.0}, 0),
new Spec("TurnOfMonth", "scalar_candle", new double[]{3.0, 3.0, 0.0}, 0),
new Spec("Tweezer", "scalar_candle", new double[]{}, 0),
new Spec("TwiggsMoneyFlow", "scalar_candle", new double[]{14.0}, 0),
new Spec("TwoCrows", "scalar_candle", new double[]{}, 0),
new Spec("TypicalPrice", "scalar_candle", new double[]{}, 0),
new Spec("UlcerIndex", "scalar_f64", new double[]{14.0}, 0),
new Spec("UltimateOscillator", "scalar_candle", new double[]{3.0, 7.0, 14.0}, 0),
new Spec("UniqueThreeRiver", "scalar_candle", new double[]{}, 0),
new Spec("UniversalOscillator", "scalar_f64", new double[]{14.0}, 0),
new Spec("UpDownVolumeRatio", "cross", new double[]{}, 0),
new Spec("UpsideGapThreeMethods", "scalar_candle", new double[]{}, 0),
new Spec("UpsideGapTwoCrows", "scalar_candle", new double[]{}, 0),
new Spec("UpsidePotentialRatio", "scalar_f64", new double[]{14.0, 2.0}, 0),
new Spec("ValueArea", "multi_candle", new double[]{20.0, 50.0, 0.7}, 3),
new Spec("ValueAtRisk", "scalar_f64", new double[]{20.0, 0.95}, 0),
new Spec("Variance", "scalar_f64", new double[]{14.0}, 0),
new Spec("VarianceRatio", "pairwise", new double[]{60.0, 2.0}, 0),
new Spec("VerticalHorizontalFilter", "scalar_f64", new double[]{14.0}, 0),
new Spec("Vidya", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("VolatilityCone", "multi_candle", new double[]{3.0, 7.0}, 5),
new Spec("VolatilityOfVolatility", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("VolatilityRatio", "scalar_candle", new double[]{14.0}, 0),
new Spec("VoltyStop", "scalar_candle", new double[]{14.0, 2.0}, 0),
new Spec("VolumeBars", "bars_candle5", new double[]{500.0}, 0),
new Spec("VolumeByTimeProfile", "profile_bins", new double[]{24.0, 0.0}, 24),
new Spec("VolumeOscillator", "scalar_candle", new double[]{3.0, 7.0}, 0),
new Spec("VolumePriceTrend", "scalar_candle", new double[]{}, 0),
new Spec("VolumeProfile", "profile_pricebins", new double[]{20.0, 50.0}, 52),
new Spec("VolumeRsi", "scalar_candle", new double[]{14.0}, 0),
new Spec("VolumeWeightedMacd", "multi_candle", new double[]{3.0, 7.0, 14.0}, 3),
new Spec("VolumeWeightedSr", "multi_candle", new double[]{14.0}, 2),
new Spec("Vortex", "multi_candle", new double[]{14.0}, 2),
new Spec("Vpin", "trade", new double[]{5000.0, 10.0}, 0),
new Spec("Vwap", "scalar_candle", new double[]{}, 0),
new Spec("VwapStdDevBands", "multi_candle", new double[]{2.0}, 4),
new Spec("Vwma", "scalar_candle", new double[]{14.0}, 0),
new Spec("Vzo", "scalar_candle", new double[]{14.0}, 0),
new Spec("Wad", "scalar_candle", new double[]{}, 0),
new Spec("WavePm", "scalar_f64", new double[]{3.0, 7.0}, 0),
new Spec("WaveTrend", "multi_candle", new double[]{3.0, 7.0, 14.0}, 2),
new Spec("Wedge", "scalar_candle", new double[]{}, 0),
new Spec("WeightedClose", "scalar_candle", new double[]{}, 0),
new Spec("WickRatio", "scalar_candle", new double[]{}, 0),
new Spec("WilliamsFractals", "multi_candle", new double[]{}, 2),
new Spec("WilliamsR", "scalar_candle", new double[]{14.0}, 0),
new Spec("WinRate", "scalar_f64", new double[]{14.0}, 0),
new Spec("Wma", "scalar_f64", new double[]{14.0}, 0),
new Spec("WoodiePivots", "multi_candle", new double[]{}, 5),
new Spec("YangZhangVolatility", "scalar_candle", new double[]{20.0, 252.0}, 0),
new Spec("YoyoExit", "scalar_candle", new double[]{14.0, 2.0}, 0),
new Spec("ZScore", "scalar_f64", new double[]{14.0}, 0),
new Spec("ZeroLagMacd", "multi_f64", new double[]{3.0, 7.0, 14.0}, 3),
new Spec("ZigZag", "multi_candle", new double[]{0.02}, 2),
new Spec("Zlema", "scalar_f64", new double[]{14.0}, 0),
};
private static Path goldenDir() {
java.io.File d = new java.io.File("").getAbsoluteFile();
while (d != null) {
java.io.File g = new java.io.File(d, "testdata/golden");
if (g.isDirectory()) {
return g.toPath();
}
d = d.getParentFile();
}
throw new IllegalStateException("testdata/golden not found");
}
private static double cell(String s) {
return switch (s) {
case "nan" -> Double.NaN;
case "inf" -> Double.POSITIVE_INFINITY;
case "-inf" -> Double.NEGATIVE_INFINITY;
default -> Double.parseDouble(s);
};
}
private static double[][] input() throws IOException {
List<String> lines = Files.readAllLines(goldenDir().resolve("input.csv"));
List<double[]> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
if (lines.get(i).isEmpty()) continue;
String[] p = lines.get(i).split(",");
double[] r = new double[p.length];
for (int j = 0; j < p.length; j++) r[j] = Double.parseDouble(p[j]);
rows.add(r);
}
return rows.toArray(new double[0][]);
}
// Keep blank lines (a candle on which no bar closed) so rows stay aligned.
private static double[][] fixture(String name) throws IOException {
List<String> lines = Files.readAllLines(goldenDir().resolve("g_" + name + ".csv"));
List<double[]> rows = new ArrayList<>();
for (int i = 1; i < lines.size(); i++) {
String ln = lines.get(i);
if (ln.isEmpty()) {
rows.add(new double[0]);
continue;
}
String[] p = ln.split(",");
double[] r = new double[p.length];
for (int j = 0; j < p.length; j++) r[j] = cell(p[j]);
rows.add(r);
}
return rows.toArray(new double[0][]);
}
private static double[] nanRow(int n) {
double[] r = new double[n];
java.util.Arrays.fill(r, Double.NaN);
return r;
}
private static double[] derivFields(double[] r) {
double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4];
return new double[]{
(c - o) / c * 0.01, c, c - 0.5, c + 1.0, v * 10.0, v * 0.6, v * 0.4,
v * 0.55, v * 0.45, h - c, c - l,
};
}
private static double[] crossList(double[] r, int which) {
double o = r[0], c = r[3], v = r[4];
double[] out = new double[5];
for (int j = 0; j < 5; j++) {
out[j] = switch (which) {
case 0 -> (c - o) + j;
case 1 -> v + j * 10.0;
case 2 -> j % 2 == 0 ? 1.0 : 0.0; // newHigh
case 3 -> j % 3 == 0 ? 1.0 : 0.0; // newLow
case 4 -> j % 2 == 0 ? 1.0 : 0.0; // aboveMa
default -> j % 3 == 0 ? 1.0 : 0.0; // onBuySignal
};
}
return out;
}
private static double[] obList(double[] r, int which) {
double c = r[3], v = r[4];
double[] out = new double[5];
for (int k = 0; k < 5; k++) {
double kf = k + 1;
out[k] = switch (which) {
case 0 -> c - 0.1 * kf;
case 1 -> v / kf;
case 2 -> c + 0.1 * kf;
default -> v * 0.9 / kf;
};
}
return out;
}
private static double[] flattenRecord(Object o) throws Exception {
RecordComponent[] rc = o.getClass().getRecordComponents();
List<Double> list = new ArrayList<>();
for (RecordComponent c : rc) {
Object v = c.getAccessor().invoke(o);
if (v instanceof double[] arr) {
for (double d : arr) list.add(d);
} else if (v instanceof Number num) {
list.add(num.doubleValue());
}
}
double[] out = new double[list.size()];
for (int i = 0; i < out.length; i++) out[i] = list.get(i);
return out;
}
private static double[] flattenArray(Object arr) throws Exception {
int len = Array.getLength(arr);
List<Double> list = new ArrayList<>();
for (int i = 0; i < len; i++) {
for (double d : flattenRecord(Array.get(arr, i))) list.add(d);
}
double[] out = new double[list.size()];
for (int i = 0; i < out.length; i++) out[i] = list.get(i);
return out;
}
private static Object construct(Spec s) throws Exception {
Class<?> cls = Class.forName("org.wickra." + s.canonical());
Constructor<?> ctor = cls.getConstructors()[0];
Class<?>[] pt = ctor.getParameterTypes();
Object[] args = new Object[pt.length];
for (int i = 0; i < pt.length; i++) {
double v = s.params()[i];
if (pt[i] == int.class) args[i] = (int) Math.round(v);
else if (pt[i] == long.class) args[i] = (long) Math.round(v);
else args[i] = v;
}
return ctor.newInstance(args);
}
private static Method updateMethod(Object ind) {
for (Method m : ind.getClass().getMethods()) {
if (m.getName().equals("update")) return m;
}
throw new IllegalStateException("no update on " + ind.getClass());
}
private static double[] row(Spec s, Object ind, Method upd, double[] r, int i) throws Exception {
double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4];
Object res = switch (s.arch()) {
case "scalar_f64", "multi_f64" -> upd.invoke(ind, c);
case "pairwise", "multi_pairwise" -> upd.invoke(ind, c, o);
case "scalar_candle", "multi_candle", "profile_bins", "profile_pricebins" ->
upd.invoke(ind, o, h, l, c, v, (long) i);
case "trade" -> upd.invoke(ind, c, v, c >= o, (long) i);
case "trademid" -> upd.invoke(ind, c, v, c >= o, (long) i, (h + l) / 2);
case "ob" -> upd.invoke(ind, obList(r, 0), obList(r, 1), obList(r, 2), obList(r, 3));
case "cross" -> upd.invoke(ind, crossList(r, 0), crossList(r, 1), crossList(r, 2),
crossList(r, 3), crossList(r, 4), crossList(r, 5), (long) i);
case "deriv", "deriv_multi" -> {
double[] d = derivFields(r);
yield upd.invoke(ind, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], (long) i);
}
case "bars_close" -> upd.invoke(ind, c, c, c, c, 1.0, 0L);
case "bars_candle4" -> upd.invoke(ind, o, h, l, c, 1.0, 0L);
case "bars_candle5" -> upd.invoke(ind, o, h, l, c, v, 0L);
case "footprint" -> upd.invoke(ind, c, v, c >= o, (long) i);
default -> throw new IllegalStateException("arch " + s.arch());
};
return switch (s.arch()) {
case "scalar_f64", "scalar_candle", "pairwise", "trade", "trademid", "ob", "cross", "deriv" ->
new double[]{((Number) res).doubleValue()};
case "multi_f64", "multi_candle", "multi_pairwise", "deriv_multi", "profile_pricebins" ->
res == null ? nanRow(s.width()) : flattenRecord(res);
case "profile_bins" -> res == null ? nanRow(s.width()) : (double[]) res;
default -> flattenArray(res); // bars_*, footprint
};
}
@TestFactory
List<DynamicTest> golden() throws Exception {
double[][] rows = input();
List<DynamicTest> tests = new ArrayList<>();
for (Spec s : SPECS) {
tests.add(dynamicTest(s.canonical(), () -> {
Object ind = construct(s);
Method upd = updateMethod(ind);
double[][] exp = fixture(s.canonical());
assertTrue(exp.length == rows.length, s.canonical() + ": row count " + exp.length + " vs " + rows.length);
for (int i = 0; i < rows.length; i++) {
double[] got = row(s, ind, upd, rows[i], i);
double[] want = exp[i];
assertTrue(got.length == want.length,
s.canonical() + " row " + i + ": arity " + got.length + " vs " + want.length);
for (int k = 0; k < want.length; k++) {
double w = want[k], g = got[k];
if (Double.isNaN(w)) {
assertTrue(Double.isNaN(g), s.canonical() + " row " + i + " col " + k + ": want NaN got " + g);
} else if (Double.isInfinite(w)) {
assertTrue(Double.isInfinite(g) && Math.signum(g) == Math.signum(w),
s.canonical() + " row " + i + " col " + k + ": want " + w + " got " + g);
} else {
double tol = TOL * Math.max(1.0, Math.abs(w));
assertTrue(Math.abs(g - w) <= tol,
s.canonical() + " row " + i + " col " + k + ": got " + g + " want " + w);
}
}
}
}));
}
return tests;
}
}
@@ -157,4 +157,51 @@ class GoldenTests {
}
}
}
// The four de-duplicated indicators, pinned against the Rust reference.
@Test
void adOscillatorMatchesGolden() throws Exception {
double[][] in = input();
List<String[]> e = readCsv("ad_oscillator");
try (AdOscillator ad = new AdOscillator()) {
for (int i = 0; i < in.length; i++) {
close(ad.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "ad_oscillator");
}
}
}
@Test
void intradayIntensityMatchesGolden() throws Exception {
double[][] in = input();
List<String[]> e = readCsv("intraday_intensity");
try (IntradayIntensity ii = new IntradayIntensity()) {
for (int i = 0; i < in.length; i++) {
close(ii.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "intraday_intensity");
}
}
}
@Test
void awesomeOscillatorHistogramMatchesGolden() throws Exception {
double[][] in = input();
List<String[]> e = readCsv("awesome_oscillator_histogram");
try (AwesomeOscillatorHistogram aoh = new AwesomeOscillatorHistogram(5, 34, 1)) {
for (int i = 0; i < in.length; i++) {
close(aoh.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "awesome_oscillator_histogram");
}
}
}
@Test
void averageDrawdownMatchesGolden() throws Exception {
double[][] in = input();
List<String[]> e = readCsv("average_drawdown");
try (AverageDrawdown avg = new AverageDrawdown(20)) {
for (int i = 0; i < in.length; i++) {
// generator fed the close column as the equity-curve sample.
close(avg.update(in[i][3]), cell(e.get(i)[0]), i, "average_drawdown");
}
}
}
}
@@ -0,0 +1,122 @@
"""Build node_manifest.json: for every one of the 514 indicators, record the
native class name, constructor parameter values, the ordered update argument
names (parsed from index.d.ts, each flagged scalar/array) and how to read its
output against the shared g_<Canonical>.csv fixtures. Run from repo root."""
import json
import os
import re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
DTS = os.path.join(ROOT, "bindings", "node", "index.d.ts")
GOLDEN = os.path.join(ROOT, "testdata", "golden")
def camel(snake):
head, *rest = snake.split("_")
return head + "".join(p[:1].upper() + p[1:] for p in rest)
def parse_args(sig):
out = []
for p in sig.split(","):
p = p.strip()
if not p:
continue
name = p.split(":")[0].strip().rstrip("?")
typ = p.split(":", 1)[1].strip() if ":" in p else ""
out.append({"name": name, "array": "Array" in typ})
return out
dts = open(DTS, encoding="utf-8").read()
node_upd = {}
for name, body in re.findall(r"export declare class (\w+) \{(.*?)\n\}", dts, re.S):
um = re.search(r"\bupdate\(([^)]*)\)", body)
node_upd[name] = parse_args(um.group(1)) if um else []
# constructor params: reuse the values pinned in the Python-side manifests.
params = {}
for fn in ("scalar_manifest", "multi_manifest"):
for e in json.load(open(os.path.join(GOLDEN, fn + ".json"))):
params[e["native"]] = e["params"]
for fam in json.load(open(os.path.join(GOLDEN, "exotic_manifest.json"))).values():
for e in fam:
params[e["native"]] = e["params"]
for e in json.load(open(os.path.join(GOLDEN, "profile_manifest.json"))):
params[e["native"]] = e["params"]
for e in json.load(open(os.path.join(GOLDEN, "bars_manifest.json"))):
params[e["native"]] = e["params"]
# the 11 hand-written archetypes (separate, non-g_ fixtures) keep their own test;
# everything else is g_<Canonical>.csv. Output shape per native:
multi_specs = {e["native"]: e for e in json.load(open(os.path.join(GOLDEN, "multi_manifest.json")))}
deriv_multi = {e["native"]: e for e in json.load(open(os.path.join(GOLDEN, "exotic_manifest.json")))["deriv"] if "n" in e}
profile_specs = {e["native"]: e for e in json.load(open(os.path.join(GOLDEN, "profile_manifest.json")))}
bars_specs = {e["native"]: e for e in json.load(open(os.path.join(GOLDEN, "bars_manifest.json")))}
# bar output field order (camelCase), mirroring gen_golden's per-bar tuple.
BAR_FIELDS = {
"RenkoBars": ["open", "close", "direction"],
"KagiBars": ["start", "end", "direction"],
"PointAndFigureBars": ["direction", "high", "low"],
"RangeBars": ["open", "close", "direction"],
"ThreeLineBreakBars": ["open", "close", "direction"],
"ImbalanceBars": ["open", "high", "low", "close", "imbalance", "direction"],
"RunBars": ["open", "high", "low", "close", "length", "direction"],
"DollarBars": ["open", "high", "low", "close", "volume", "dollar"],
"TickBars": ["open", "high", "low", "close", "volume"],
"VolumeBars": ["open", "high", "low", "close", "volume"],
"Footprint": ["price", "bidVol", "askVol"],
}
def header_fields(canon):
with open(os.path.join(GOLDEN, "g_" + canon + ".csv"), encoding="utf-8") as f:
return f.readline().strip().split(",")
# canonical -> native, gathered from every Python-side manifest. The g_ fixture
# is named by canonical (the Rust struct); the Node class is the native alias.
canon_native = {}
for fn in ("scalar_manifest", "multi_manifest"):
for e in json.load(open(os.path.join(GOLDEN, fn + ".json"))):
canon_native[e["canonical"]] = e["native"]
for fam in json.load(open(os.path.join(GOLDEN, "exotic_manifest.json"))).values():
for e in fam:
canon_native[e["canonical"]] = e["native"]
for e in json.load(open(os.path.join(GOLDEN, "profile_manifest.json"))):
canon_native[e["canonical"]] = e["native"]
for e in json.load(open(os.path.join(GOLDEN, "bars_manifest.json"))):
canon_native[e["canonical"]] = e["native"]
out = []
for canon in sorted(canon_native):
native = canon_native[canon]
if native not in node_upd:
raise SystemExit(f"node class for {canon} (native {native}) not found in index.d.ts")
ctor = params.get(native, [])
# EaseOfMovement's volume divisor is an optional Rust constructor argument
# (default 1e8) but a required Node constructor parameter; pass it explicitly.
if native == "EaseOfMovement":
ctor = [ctor[0], 100000000.0]
entry = {"canonical": canon, "native": native, "ctor": ctor, "args": node_upd[native]}
if native in bars_specs:
entry["out"] = "footprint" if native == "Footprint" else "bars"
entry["fields"] = BAR_FIELDS[native]
elif native in profile_specs:
ps = profile_specs[native]
entry["out"] = "profile_" + ps["kind"]
entry["width"] = ps["width"]
if ps["kind"] == "pricebins":
entry["arrayField"] = "counts" if native == "TpoProfile" else "bins"
elif native in deriv_multi or native in multi_specs:
entry["out"] = "multi"
entry["fields"] = [camel(c) for c in header_fields(canon)]
else:
entry["out"] = "scalar"
out.append(entry)
json.dump(out, open(os.path.join(GOLDEN, "node_manifest.json"), "w"), indent=1)
print("node_manifest entries:", len(out))
from collections import Counter
print(Counter(e["out"] for e in out))
+195
View File
@@ -0,0 +1,195 @@
// Generic golden-fixture parity for the Node binding.
//
// Every one of the 514 indicators is reconstructed from `node_manifest.json`
// (native class, constructor params, ordered update args), fed the synthetic
// stream derived from the shared `testdata/golden/input.csv` — the exact same
// construction the Rust `gen_golden` binary uses — and checked bit-for-bit
// against the Rust-generated `g_<Canonical>.csv`. This pins the Node FFI to the
// Rust reference for the whole indicator catalogue, not just a few archetypes.
//
// cd bindings/node && npm run build && npm test
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const wickra = require('..');
const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden');
function cell(s) {
if (s === 'nan') return NaN;
if (s === 'inf') return Infinity;
if (s === '-inf') return -Infinity;
return Number(s);
}
function readCsv(name) {
// Split on \r?\n so a CRLF checkout (Windows core.autocrlf) parses identically
// to LF — otherwise `cell('inf\r')` falls through to Number() and becomes NaN.
const lines = fs.readFileSync(path.join(GOLDEN, name + '.csv'), 'utf8').split(/\r?\n/);
lines.shift(); // header
return lines.filter((l) => l.length > 0).map((l) => l.split(',').map(cell));
}
// Bars keep blank lines (one row per candle, blank == no bar closed).
function readBarRows(name) {
const lines = fs.readFileSync(path.join(GOLDEN, name + '.csv'), 'utf8').split(/\r?\n/);
lines.shift();
// Drop only the single trailing-newline artifact, keeping legitimate blank
// rows (a candle on which no bar closed) so rows stay aligned to the input.
if (lines.length && lines[lines.length - 1] === '') lines.pop();
return lines.map((l) => (l.length === 0 ? [] : l.split(',').map(cell)));
}
const MANIFEST = JSON.parse(fs.readFileSync(path.join(GOLDEN, 'node_manifest.json'), 'utf8'));
const ROWS = readCsv('input');
function derivFields(o, h, l, c, v) {
return {
fundingRate: ((c - o) / c) * 0.01,
markPrice: c,
indexPrice: c - 0.5,
futuresPrice: c + 1.0,
openInterest: v * 10.0,
longSize: v * 0.6,
shortSize: v * 0.4,
takerBuyVolume: v * 0.55,
takerSellVolume: v * 0.45,
longLiquidation: h - c,
shortLiquidation: c - l,
};
}
function resolveArg(arg, o, h, l, c, v, i) {
const name = arg.name;
if (arg.array) {
switch (name) {
case 'change':
return [0, 1, 2, 3, 4].map((j) => c - o + j);
case 'volume':
return [0, 1, 2, 3, 4].map((j) => v + j * 10.0);
case 'newHigh':
return [0, 1, 2, 3, 4].map((j) => j % 2 === 0);
case 'newLow':
return [0, 1, 2, 3, 4].map((j) => j % 3 === 0);
case 'aboveMa':
return [0, 1, 2, 3, 4].map((j) => j % 2 === 0);
case 'onBuySignal':
return [0, 1, 2, 3, 4].map((j) => j % 3 === 0);
case 'bidPx':
return [0, 1, 2, 3, 4].map((k) => c - 0.1 * (k + 1));
case 'bidSz':
return [0, 1, 2, 3, 4].map((k) => v / (k + 1));
case 'askPx':
return [0, 1, 2, 3, 4].map((k) => c + 0.1 * (k + 1));
case 'askSz':
return [0, 1, 2, 3, 4].map((k) => (v * 0.9) / (k + 1));
default:
throw new Error('unknown array arg ' + name);
}
}
switch (name) {
case 'value':
case 'close':
case 'price':
case 'x':
case 'a':
case 'asset':
return c;
case 'y':
case 'b':
case 'benchmark':
case 'open':
return o;
case 'high':
return h;
case 'low':
return l;
case 'volume':
case 'size':
return v;
case 'timestamp':
return i;
case 'isBuy':
return c >= o;
case 'mid':
return (h + l) / 2.0;
default: {
const d = derivFields(o, h, l, c, v);
if (name in d) return d[name];
throw new Error('unknown scalar arg ' + name);
}
}
}
function closeEq(got, want, label) {
if (Number.isNaN(want)) {
assert.ok(Number.isNaN(got), `${label}: want NaN got ${got}`);
return;
}
if (!Number.isFinite(want)) {
assert.ok(got === want, `${label}: want ${want} got ${got}`);
return;
}
const tol = 1e-6 * Math.max(1.0, Math.abs(want));
assert.ok(Math.abs(got - want) <= tol, `${label}: got ${got} want ${want}`);
}
for (const spec of MANIFEST) {
test(`golden: ${spec.canonical}`, () => {
const Cls = wickra[spec.native];
assert.ok(Cls, `missing Node class ${spec.native}`);
const ind = new Cls(...spec.ctor);
const isBars = spec.out === 'bars' || spec.out === 'footprint';
const expected = isBars ? readBarRows('g_' + spec.canonical) : readCsv('g_' + spec.canonical);
for (let i = 0; i < ROWS.length; i++) {
const [o, h, l, c, v] = ROWS[i];
const args = spec.args.map((a) => resolveArg(a, o, h, l, c, v, i));
const got = ind.update(...args);
const want = expected[i];
const label = `${spec.canonical} row ${i}`;
if (spec.out === 'scalar') {
closeEq(got === null || got === undefined ? NaN : got, want[0], label);
} else if (spec.out === 'multi') {
if (got === null || got === undefined) {
assert.ok(want.every(Number.isNaN), `${label}: want ${want} got null`);
continue;
}
// napi serialises the output struct's fields in declaration order, which
// matches the CSV column order — compare positionally to avoid relying on
// the exact camelCase of each field name.
const vals = Object.values(got);
assert.equal(vals.length, want.length, `${label}: arity ${vals.length} vs ${want.length}`);
vals.forEach((gv, k) => {
closeEq(gv === null || gv === undefined ? NaN : gv, want[k], `${label} col ${k}`);
});
} else if (spec.out === 'profile_bins') {
if (got === null || got === undefined) {
assert.ok(want.every(Number.isNaN), `${label}: want all-NaN got null`);
continue;
}
assert.equal(got.length, want.length, `${label}: width ${got.length} vs ${want.length}`);
got.forEach((gv, k) => closeEq(gv, want[k], `${label} bin ${k}`));
} else if (spec.out === 'profile_pricebins') {
if (got === null || got === undefined) {
assert.ok(want.every(Number.isNaN), `${label}: want all-NaN got null`);
continue;
}
const flat = [got.priceLow, got.priceHigh, ...got[spec.arrayField]];
assert.equal(flat.length, want.length, `${label}: width ${flat.length} vs ${want.length}`);
flat.forEach((gv, k) => closeEq(gv, want[k], `${label} col ${k}`));
} else {
// bars / footprint: flatten array-of-objects in declared field order.
const flat = [];
for (const bar of got) {
for (const f of spec.fields) flat.push(Number(bar[f]));
}
assert.equal(flat.length, want.length, `${label}: arity ${flat.length} vs ${want.length}`);
flat.forEach((gv, k) => closeEq(gv, want[k], `${label} col ${k}`));
}
}
});
}
+3 -3
View File
@@ -259,7 +259,7 @@ const candleScalar = {
VolumeOscillator: { make: () => new wickra.VolumeOscillator(14, 28), step: (ind, i) => ind.update(volume[i]), batch: (ind) => ind.batch(volume) },
NVI: { make: () => new wickra.NVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
PVI: { make: () => new wickra.PVI(), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
WilliamsAD: { make: () => new wickra.WilliamsAD(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
ADOSC: { make: () => new wickra.ADOSC(), step: (ind, i) => ind.update(high[i], low[i], close[i]), batch: (ind) => ind.batch(high, low, close) },
AnchoredVWAP: { make: () => new wickra.AnchoredVWAP(), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
DemandIndex: { make: () => new wickra.DemandIndex(10), step: (ind, i) => ind.update(high[i], low[i], close[i], volume[i]), batch: (ind) => ind.batch(high, low, close, volume) },
TSV: { make: () => new wickra.TSV(18), step: (ind, i) => ind.update(close[i], volume[i]), batch: (ind) => ind.batch(close, volume) },
@@ -1009,8 +1009,8 @@ test('AwesomeOscillatorHistogram on a flat median converges to zero', () => {
Array(n).fill(11),
Array(n).fill(9),
);
// warmup = 5 + 3 - 1 = 7.
for (let i = 6; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12);
// AO momentum; warmup = slow + lookback = 5 + 3 = 8.
for (let i = 7; i < n; i++) assert.ok(Math.abs(out[i]) < 1e-12);
});
test('STC on a flat series stays at zero', () => {
+2 -2
View File
@@ -2505,8 +2505,8 @@ export declare class KVO {
isReady(): boolean
warmupPeriod(): number
}
export type AdOscillatorNode = WilliamsAD
export declare class WilliamsAD {
export type AdOscillatorNode = ADOSC
export declare class ADOSC {
constructor()
update(high: number, low: number, close: number): number | null
batch(high: Array<number>, low: Array<number>, close: Array<number>): Array<number>
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.9.1",
"version": "0.9.2",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.9.1",
"version": "0.9.2",
"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.9.1",
"version": "0.9.2",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.9.1",
"version": "0.9.2",
"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.9.1",
"version": "0.9.2",
"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.9.1",
"version": "0.9.2",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.9.1",
"version": "0.9.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.9.1",
"version": "0.9.2",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 20"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.9.1",
"wickra-darwin-x64": "0.9.1",
"wickra-linux-arm64-gnu": "0.9.1",
"wickra-linux-x64-gnu": "0.9.1",
"wickra-win32-arm64-msvc": "0.9.1",
"wickra-win32-x64-msvc": "0.9.1"
"wickra-darwin-arm64": "0.9.2",
"wickra-darwin-x64": "0.9.2",
"wickra-linux-arm64-gnu": "0.9.2",
"wickra-linux-x64-gnu": "0.9.2",
"wickra-win32-arm64-msvc": "0.9.2",
"wickra-win32-x64-msvc": "0.9.2"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.9.2.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.9.2.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.9.2.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.9.2.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.9.2.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.9.1.tgz",
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.9.2.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.9.1",
"version": "0.9.2",
"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": ">= 20"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.9.1",
"wickra-linux-arm64-gnu": "0.9.1",
"wickra-darwin-x64": "0.9.1",
"wickra-darwin-arm64": "0.9.1",
"wickra-win32-x64-msvc": "0.9.1",
"wickra-win32-arm64-msvc": "0.9.1"
"wickra-linux-x64-gnu": "0.9.2",
"wickra-linux-arm64-gnu": "0.9.2",
"wickra-darwin-x64": "0.9.2",
"wickra-darwin-arm64": "0.9.2",
"wickra-win32-x64-msvc": "0.9.2",
"wickra-win32-arm64-msvc": "0.9.2"
},
"scripts": {
"build": "napi build --platform --release",
+1 -1
View File
@@ -5926,7 +5926,7 @@ impl KvoNode {
// ============================== Williams A/D ==============================
#[napi(js_name = "WilliamsAD")]
#[napi(js_name = "ADOSC")]
pub struct AdOscillatorNode {
inner: wc::AdOscillator,
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.9.1"
version = "0.9.2"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+2 -2
View File
@@ -236,7 +236,7 @@ from ._wickra import (
VolumeOscillator,
NVI,
PVI,
WilliamsAD,
ADOSC,
AnchoredVWAP,
DemandIndex,
TSV,
@@ -780,7 +780,7 @@ __all__ = [
"VolumeOscillator",
"NVI",
"PVI",
"WilliamsAD",
"ADOSC",
"AnchoredVWAP",
"DemandIndex",
"TSV",
+2 -2
View File
@@ -9450,7 +9450,7 @@ impl PyKvo {
// ============================== Williams A/D Oscillator ==============================
#[pyclass(name = "WilliamsAD", module = "wickra._wickra", skip_from_py_object)]
#[pyclass(name = "ADOSC", module = "wickra._wickra", skip_from_py_object)]
#[derive(Clone)]
struct PyAdOscillator {
inner: wc::AdOscillator,
@@ -9507,7 +9507,7 @@ impl PyAdOscillator {
self.inner.warmup_period()
}
fn __repr__(&self) -> String {
"WilliamsAD()".to_string()
"ADOSC()".to_string()
}
}
+70
View File
@@ -0,0 +1,70 @@
"""Generic golden replay for the alt-chart bar builders and the footprint.
Each builder turns one candle into 0..n completed bars, so the fixture stores
one CSV line per input candle holding every bar flattened (an empty line means
no bar closed on that candle). Close-driven builders (Renko, Kagi, P&F, Range,
Three-Line-Break) receive a flat candle, mirroring the binding's `update(close)`.
Values are checked bit-for-bit against the Rust-generated `g_<Canonical>.csv`.
"""
import json
import math
import os
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _cell(s):
return math.nan if s == "nan" else float(s)
def _bar_rows(name):
# Keep blank lines: one row per input candle, blank == no bar closed.
with open(os.path.join(GOLDEN, name + ".csv")) as f:
lines = f.read().splitlines()[1:]
return [[] if not ln.strip() else [_cell(x) for x in ln.split(",")] for ln in lines]
def _input():
with open(os.path.join(GOLDEN, "input.csv")) as f:
return [[float(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
with open(os.path.join(GOLDEN, "bars_manifest.json")) as _mf:
MANIFEST = json.load(_mf)
ROWS = _input()
def _flatten(bars):
out = []
for bar in bars:
out.extend(float(x) for x in bar)
return out
@pytest.mark.parametrize("spec", MANIFEST, ids=[m["canonical"] for m in MANIFEST])
def test_bars_match_golden(spec):
ind = getattr(ta, spec["native"])(*spec["params"])
expected = _bar_rows("g_" + spec["canonical"])
feed = spec["feed"]
assert len(expected) == len(ROWS), f"{spec['canonical']}: {len(expected)} rows vs {len(ROWS)} inputs"
for i, (o, h, l, c, v) in enumerate(ROWS):
if feed == "close":
produced = ind.update(c)
elif feed == "candle4":
produced = ind.update(o, h, l, c)
elif feed == "candle5":
produced = ind.update(o, h, l, c, v)
else: # trade footprint
produced = ind.update(c, v, c >= o)
got = _flatten(produced)
want = expected[i]
assert len(got) == len(want), f"{spec['canonical']} row {i}: arity {len(got)} vs {len(want)}"
for gv, w in zip(got, want):
if math.isnan(w):
assert math.isnan(gv), f"{spec['canonical']} row {i}: want NaN got {gv}"
else:
assert abs(gv - w) <= 1e-6 * max(1.0, abs(w)), f"{spec['canonical']} row {i}: got {gv} want {w}"
+128
View File
@@ -0,0 +1,128 @@
"""Generic golden replay for the exotic-input families: DerivativesTick,
CrossSection, Trade, TradeQuote and OrderBook indicators.
Each family feeds a synthetic stream deterministically derived from the shared
`testdata/golden/input.csv` OHLCV rows the exact same construction the Rust
`gen_golden` binary uses and every value is checked bit-for-bit against the
Rust-generated `g_<Canonical>.csv`. This pins the Python FFI for indicators
whose inputs cannot be expressed as a plain close/candle/pair stream.
"""
import json
import math
import os
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _cell(s):
return math.nan if s == "nan" else float(s)
def _rows(name):
with open(os.path.join(GOLDEN, name + ".csv")) as f:
return [[_cell(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
def _input():
with open(os.path.join(GOLDEN, "input.csv")) as f:
return [[float(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
with open(os.path.join(GOLDEN, "exotic_manifest.json")) as _mf:
MANIFEST = json.load(_mf)
ROWS = _input()
def _deriv_fields(o, h, l, c, v):
return {
"funding_rate": (c - o) / c * 0.01,
"mark_price": c,
"index_price": c - 0.5,
"futures_price": c + 1.0,
"open_interest": v * 10.0,
"long_size": v * 0.6,
"short_size": v * 0.4,
"taker_buy_volume": v * 0.55,
"taker_sell_volume": v * 0.45,
"long_liquidation": h - c,
"short_liquidation": c - l,
}
def _cross_lists(o, h, l, c, v):
change = [(c - o) + j for j in range(5)]
volume = [v + j * 10.0 for j in range(5)]
new_high = [j % 2 == 0 for j in range(5)]
new_low = [j % 3 == 0 for j in range(5)]
above_ma = [j % 2 == 0 for j in range(5)]
on_buy_signal = [j % 3 == 0 for j in range(5)]
return change, volume, new_high, new_low, above_ma, on_buy_signal
def _ob_lists(o, h, l, c, v):
bid_px = [c - 0.1 * (k + 1) for k in range(5)]
bid_sz = [v / (k + 1) for k in range(5)]
ask_px = [c + 0.1 * (k + 1) for k in range(5)]
ask_sz = [v * 0.9 / (k + 1) for k in range(5)]
return bid_px, bid_sz, ask_px, ask_sz
def _assert_scalar(got, want, canonical, i):
got = math.nan if got is None else got
if math.isnan(want):
assert math.isnan(got), f"{canonical} row {i}: want NaN got {got}"
elif math.isinf(want):
assert math.isinf(got) and (got > 0) == (want > 0), f"{canonical} row {i}: got {got} want {want}"
else:
assert abs(got - want) <= 1e-6 * max(1.0, abs(want)), f"{canonical} row {i}: got {got} want {want}"
def _specs(family):
return [(family, s) for s in MANIFEST[family]]
@pytest.mark.parametrize(
"family,spec",
_specs("deriv") + _specs("cross") + _specs("trade") + _specs("trademid") + _specs("ob"),
ids=[s["canonical"] for fam in ("deriv", "cross", "trade", "trademid", "ob") for s in MANIFEST[fam]],
)
def test_exotic_matches_golden(family, spec):
ind = getattr(ta, spec["native"])(*spec["params"])
expected = _rows("g_" + spec["canonical"])
n = spec.get("n")
for i, (o, h, l, c, v) in enumerate(ROWS):
if family == "deriv":
f = _deriv_fields(o, h, l, c, v)
got = ind.update(*[f[a] for a in spec["args"]])
elif family == "cross":
change, volume, nh, nl, above_ma, on_buy = _cross_lists(o, h, l, c, v)
extra = spec.get("extra")
if extra == "above_ma":
got = ind.update(change, volume, nh, nl, above_ma)
elif extra == "on_buy_signal":
got = ind.update(change, volume, nh, nl, on_buy)
else:
got = ind.update(change, volume, nh, nl)
elif family == "trade":
got = ind.update(c, v, c >= o)
elif family == "trademid":
got = ind.update(c, v, c >= o, (h + l) / 2.0)
else: # ob
bid_px, bid_sz, ask_px, ask_sz = _ob_lists(o, h, l, c, v)
got = ind.update(bid_px, bid_sz, ask_px, ask_sz)
want = expected[i]
if n: # multi-output (LiquidationFeatures)
if got is None:
assert all(math.isnan(w) for w in want), f"{spec['canonical']} row {i}: want {want} got None"
continue
vals = list(got)
assert len(vals) == len(want), f"{spec['canonical']} row {i}: arity {len(vals)} vs {len(want)}"
for gv, w in zip(vals, want):
_assert_scalar(gv, w, spec["canonical"], i)
else:
_assert_scalar(got, want[0], spec["canonical"], i)
@@ -0,0 +1,60 @@
"""Generic golden replay for multi-output indicators: each entry in
multi_manifest.json is reconstructed by its native name and every output field
is checked against the Rust-generated g_<Canonical>.csv (one column per field).
"""
import json
import math
import os
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _cell(s):
return math.nan if s == "nan" else float(s)
def _rows(name):
with open(os.path.join(GOLDEN, name + ".csv")) as f:
return [[_cell(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
def _input():
with open(os.path.join(GOLDEN, "input.csv")) as f:
return [[float(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
with open(os.path.join(GOLDEN, "multi_manifest.json")) as _mf:
MANIFEST = json.load(_mf)
ROWS = _input()
@pytest.mark.parametrize("spec", MANIFEST, ids=[m["canonical"] for m in MANIFEST])
def test_multi_matches_golden(spec):
ind = getattr(ta, spec["native"])(*spec["params"])
expected = _rows("g_" + spec["canonical"])
inp = spec["input"]
for i, (o, h, l, c, v) in enumerate(ROWS):
if inp == "f64":
got = ind.update(c)
elif inp == "Candle":
got = ind.update((o, h, l, c, v, i))
else:
got = ind.update(c, o)
want = expected[i]
if got is None:
assert all(math.isnan(w) for w in want), f"{spec['canonical']} row {i}: want {want} got None"
continue
vals = list(got)
assert len(vals) == len(want), f"{spec['canonical']} row {i}: arity {len(vals)} vs {len(want)}"
for gv, w in zip(vals, want):
gv = math.nan if gv is None else gv
if math.isnan(w):
assert math.isnan(gv), f"{spec['canonical']} row {i}: want NaN got {gv}"
elif math.isinf(w):
assert math.isinf(gv) and (gv > 0) == (w > 0)
else:
assert abs(gv - w) <= 1e-6 * max(1.0, abs(w)), f"{spec['canonical']} row {i}: got {gv} want {w}"
@@ -0,0 +1,61 @@
"""Generic golden replay for the profile family: time/volume seasonality
histograms (`bins`) and price-binned market profiles (`price_low, price_high,
bins`). Each profile emits a fixed-width row once warm and `NaN`s during warmup.
The shared `testdata/golden/input.csv` candle series is replayed through the
Python FFI and the flattened histogram is checked bit-for-bit against the
Rust-generated `g_<Canonical>.csv`.
"""
import json
import math
import os
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _cell(s):
return math.nan if s == "nan" else float(s)
def _rows(name):
with open(os.path.join(GOLDEN, name + ".csv")) as f:
return [[_cell(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
def _input():
with open(os.path.join(GOLDEN, "input.csv")) as f:
return [[float(x) for x in line.split(",")] for line in f.read().splitlines()[1:] if line.strip()]
with open(os.path.join(GOLDEN, "profile_manifest.json")) as _mf:
MANIFEST = json.load(_mf)
ROWS = _input()
@pytest.mark.parametrize("spec", MANIFEST, ids=[m["canonical"] for m in MANIFEST])
def test_profile_matches_golden(spec):
ind = getattr(ta, spec["native"])(*spec["params"])
expected = _rows("g_" + spec["canonical"])
width = spec["width"]
for i, (o, h, l, c, v) in enumerate(ROWS):
got = ind.update((o, h, l, c, v, i))
want = expected[i]
assert len(want) == width, f"{spec['canonical']} row {i}: fixture width {len(want)} != {width}"
if got is None:
assert all(math.isnan(w) for w in want), f"{spec['canonical']} row {i}: want {want} got None"
continue
if spec["kind"] == "pricebins":
price_low, price_high, bins = got
vals = [price_low, price_high, *list(bins)]
else:
vals = list(got)
assert len(vals) == width, f"{spec['canonical']} row {i}: arity {len(vals)} != {width}"
for gv, w in zip(vals, want):
if math.isnan(w):
assert math.isnan(gv), f"{spec['canonical']} row {i}: want NaN got {gv}"
else:
assert abs(gv - w) <= 1e-6 * max(1.0, abs(w)), f"{spec['canonical']} row {i}: got {gv} want {w}"
@@ -0,0 +1,67 @@
"""Generic golden replay: every scalar indicator in scalar_manifest.json is
constructed by its native name with the recorded params, fed the shared golden
input, and checked bit-for-bit against the Rust-generated g_<Canonical>.csv.
This ties the Python binding to the Rust reference for the whole scalar-output
tranche (not just the seven archetype representatives). Fixtures + manifest are
produced by `cargo run -p wickra-examples --bin gen_golden`.
"""
import json
import math
import os
import numpy as np
import pytest
import wickra as ta
HERE = os.path.dirname(__file__)
GOLDEN = os.path.normpath(os.path.join(HERE, "..", "..", "..", "testdata", "golden"))
def _cell(s):
return math.nan if s == "nan" else float(s)
def _load(name):
with open(os.path.join(GOLDEN, name + ".csv")) as f:
return [_cell(line.strip()) for line in f.read().splitlines()[1:] if line.strip()]
def _input():
rows = []
with open(os.path.join(GOLDEN, "input.csv")) as f:
for line in f.read().splitlines()[1:]:
if line.strip():
rows.append([float(x) for x in line.split(",")])
return rows
with open(os.path.join(GOLDEN, "scalar_manifest.json")) as _mf:
MANIFEST = json.load(_mf)
ROWS = _input()
@pytest.mark.parametrize("spec", MANIFEST, ids=[m["canonical"] for m in MANIFEST])
def test_scalar_matches_golden(spec):
cls = getattr(ta, spec["native"])
ind = cls(*spec["params"])
expected = _load("g_" + spec["canonical"])
inp = spec["input"]
for i, (o, h, l, c, v) in enumerate(ROWS):
if inp == "f64":
got = ind.update(c)
elif inp == "Candle":
got = ind.update((o, h, l, c, v, i))
else: # pairwise (f64, f64): generator fed (close, open)
got = ind.update(c, o)
want = expected[i]
got = math.nan if got is None else got
if math.isnan(want):
assert math.isnan(got), f"{spec['canonical']} row {i}: want NaN got {got}"
elif math.isinf(want):
assert math.isinf(got) and (got > 0) == (want > 0), (
f"{spec['canonical']} row {i}: got {got} want {want}"
)
else:
tol = 1e-6 * max(1.0, abs(want))
assert abs(got - want) <= tol, f"{spec['canonical']} row {i}: got {got} want {want}"
+6 -6
View File
@@ -258,9 +258,9 @@ def test_awesome_oscillator_histogram_flat_series_converges_to_zero():
n = 50
high = np.full(n, 11.0)
low = np.full(n, 9.0)
out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low)
# warmup = slow + sma - 1 = 5 + 3 - 1 = 7.
np.testing.assert_allclose(out[6:], 0.0, atol=1e-12)
out = ta.AwesomeOscillatorHistogram(3, 5, 3).batch(high, low) # AO momentum
# warmup = slow + lookback = 5 + 3 = 8.
np.testing.assert_allclose(out[7:], 0.0, atol=1e-12)
def test_stc_constant_series_yields_zero():
@@ -521,10 +521,10 @@ def test_calmar_ratio_known_path():
def test_average_drawdown_known_window():
# window [100, 120, 90, 110]: dd = 0, 0, 0.25, 10/120;
# mean = (0.25 + 10/120) / 4.
# window [100, 120, 90, 110]: one drawdown episode (peak 120, trough 90),
# never recovering -> depth (120-90)/120 = 0.25; one episode -> AvgDD = 0.25.
out = ta.AverageDrawdown(4).batch(np.array([100.0, 120.0, 90.0, 110.0]))
expected = (0.25 + 10.0 / 120.0) / 4.0
expected = 0.25
assert math.isclose(out[-1], expected, rel_tol=1e-12)
+3 -3
View File
@@ -634,8 +634,8 @@ CANDLE_SCALAR = {
lambda: ta.PVI(),
lambda ind, h, l, c, v: ind.batch(c, v),
),
"WilliamsAD": (
lambda: ta.WilliamsAD(),
"ADOSC": (
lambda: ta.ADOSC(),
lambda ind, h, l, c, v: ind.batch(h, l, c),
),
"AnchoredVWAP": (
@@ -1762,7 +1762,7 @@ def test_wad_reference():
# TR_l = min(10, 8) = 8 -> delta = 12 - 8 = 4. AD = 4.
# bar 2: prev=12, today high=11, low=7, close=7 (down day).
# TR_h = max(12, 11) = 12 -> delta = 7 - 12 = -5. AD = 4 - 5 = -1.
ad = ta.WilliamsAD()
ad = ta.Wad()
high = np.array([11.0, 13.0, 11.0])
low = np.array([9.0, 8.0, 7.0])
close = np.array([10.0, 12.0, 7.0])
+1 -1
View File
@@ -1,7 +1,7 @@
Package: wickra
Type: Package
Title: Streaming-First Technical Indicators
Version: 0.9.1
Version: 0.9.2
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
+27
View File
@@ -0,0 +1,27 @@
"""Generate tests/testthat/golden_specs.R: the per-indicator spec list (canonical
name, archetype, ctor params, output width) consumed by test-golden-all.R, which
replays all 514 indicators against the Rust reference fixtures g_<Canonical>.csv.
Run from repo root: python bindings/r/gen_golden_test.py
"""
import json
import os
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
MAN = json.load(open(os.path.join(ROOT, "testdata", "golden", "golden_manifest.json")))
lines = [
"# Code generated by gen_golden_test.py. DO NOT EDIT.",
"# Per-indicator golden spec: canonical name, archetype, ctor params, output width.",
"GOLDEN_SPECS <- list(",
]
for e in MAN:
n = e.get("n", e.get("width", 0))
params = ",".join(repr(float(p)) for p in e["params"])
lines.append(f' list(canon="{e["canonical"]}", arch="{e["arch"]}", params=c({params}), width={n}L),')
lines[-1] = lines[-1].rstrip(",")
lines.append(")")
dest = os.path.join(ROOT, "bindings", "r", "tests", "testthat", "golden_specs.R")
open(dest, "w", encoding="utf-8").write("\n".join(lines) + "\n")
print("wrote golden_specs.R with", len(MAN), "specs")
+30 -15
View File
@@ -7,6 +7,21 @@
#include <stddef.h>
#include "wickra.h"
/* Convert an R numeric vector of flags (non-zero == TRUE) into a C `bool*`
* buffer (one byte per element). The cross-section indicators take their state
* flags as `const bool*`; casting `wk_bool_vec(x)` would reinterpret the
* 8-byte doubles as 1-byte bools and read every flag as false. The buffer is
* allocated with R_alloc, so it lives until the enclosing .Call returns. */
static bool *wk_bool_vec(SEXP x) {
R_xlen_t n = Rf_xlength(x);
bool *out = (bool *)R_alloc(n, sizeof(bool));
double *src = REAL(x);
for (R_xlen_t i = 0; i < n; i++) {
out[i] = src[i] != 0.0;
}
return out;
}
static void abandoned_baby_fin(SEXP e) {
struct AbandonedBaby *h = (struct AbandonedBaby *)R_ExternalPtrAddr(e);
if (h) wickra_abandoned_baby_free(h);
@@ -104,7 +119,7 @@ SEXP wk_absolute_breadth_index_new(void) {
}
SEXP wk_absolute_breadth_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct AbsoluteBreadthIndex *h = (struct AbsoluteBreadthIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_absolute_breadth_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_absolute_breadth_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_absolute_breadth_index_warmup_period(SEXP e) {
struct AbsoluteBreadthIndex *h = (struct AbsoluteBreadthIndex *)R_ExternalPtrAddr(e);
@@ -260,7 +275,7 @@ SEXP wk_ad_volume_line_new(void) {
}
SEXP wk_ad_volume_line_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct AdVolumeLine *h = (struct AdVolumeLine *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_ad_volume_line_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_ad_volume_line_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_ad_volume_line_warmup_period(SEXP e) {
struct AdVolumeLine *h = (struct AdVolumeLine *)R_ExternalPtrAddr(e);
@@ -531,7 +546,7 @@ SEXP wk_advance_decline_new(void) {
}
SEXP wk_advance_decline_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct AdvanceDecline *h = (struct AdvanceDecline *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_advance_decline_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_advance_decline_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_advance_decline_warmup_period(SEXP e) {
struct AdvanceDecline *h = (struct AdvanceDecline *)R_ExternalPtrAddr(e);
@@ -562,7 +577,7 @@ SEXP wk_advance_decline_ratio_new(void) {
}
SEXP wk_advance_decline_ratio_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct AdvanceDeclineRatio *h = (struct AdvanceDeclineRatio *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_advance_decline_ratio_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_advance_decline_ratio_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_advance_decline_ratio_warmup_period(SEXP e) {
struct AdvanceDeclineRatio *h = (struct AdvanceDeclineRatio *)R_ExternalPtrAddr(e);
@@ -2059,7 +2074,7 @@ SEXP wk_breadth_thrust_new(SEXP a0) {
}
SEXP wk_breadth_thrust_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct BreadthThrust *h = (struct BreadthThrust *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_breadth_thrust_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_breadth_thrust_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_breadth_thrust_warmup_period(SEXP e) {
struct BreadthThrust *h = (struct BreadthThrust *)R_ExternalPtrAddr(e);
@@ -2131,7 +2146,7 @@ SEXP wk_bullish_percent_index_new(void) {
}
SEXP wk_bullish_percent_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct BullishPercentIndex *h = (struct BullishPercentIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_bullish_percent_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_bullish_percent_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_bullish_percent_index_warmup_period(SEXP e) {
struct BullishPercentIndex *h = (struct BullishPercentIndex *)R_ExternalPtrAddr(e);
@@ -3462,7 +3477,7 @@ SEXP wk_cumulative_volume_index_new(void) {
}
SEXP wk_cumulative_volume_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct CumulativeVolumeIndex *h = (struct CumulativeVolumeIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_cumulative_volume_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_cumulative_volume_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_cumulative_volume_index_warmup_period(SEXP e) {
struct CumulativeVolumeIndex *h = (struct CumulativeVolumeIndex *)R_ExternalPtrAddr(e);
@@ -7104,7 +7119,7 @@ SEXP wk_high_low_index_new(SEXP a0) {
}
SEXP wk_high_low_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct HighLowIndex *h = (struct HighLowIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_high_low_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_high_low_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_high_low_index_warmup_period(SEXP e) {
struct HighLowIndex *h = (struct HighLowIndex *)R_ExternalPtrAddr(e);
@@ -10118,7 +10133,7 @@ SEXP wk_mc_clellan_oscillator_new(void) {
}
SEXP wk_mc_clellan_oscillator_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct McClellanOscillator *h = (struct McClellanOscillator *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_mc_clellan_oscillator_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_mc_clellan_oscillator_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_mc_clellan_oscillator_warmup_period(SEXP e) {
struct McClellanOscillator *h = (struct McClellanOscillator *)R_ExternalPtrAddr(e);
@@ -10149,7 +10164,7 @@ SEXP wk_mc_clellan_summation_index_new(void) {
}
SEXP wk_mc_clellan_summation_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct McClellanSummationIndex *h = (struct McClellanSummationIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_mc_clellan_summation_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_mc_clellan_summation_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_mc_clellan_summation_index_warmup_period(SEXP e) {
struct McClellanSummationIndex *h = (struct McClellanSummationIndex *)R_ExternalPtrAddr(e);
@@ -10914,7 +10929,7 @@ SEXP wk_new_highs_new_lows_new(void) {
}
SEXP wk_new_highs_new_lows_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct NewHighsNewLows *h = (struct NewHighsNewLows *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_new_highs_new_lows_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_new_highs_new_lows_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_new_highs_new_lows_warmup_period(SEXP e) {
struct NewHighsNewLows *h = (struct NewHighsNewLows *)R_ExternalPtrAddr(e);
@@ -11870,7 +11885,7 @@ SEXP wk_percent_above_ma_new(void) {
}
SEXP wk_percent_above_ma_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct PercentAboveMa *h = (struct PercentAboveMa *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_percent_above_ma_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_percent_above_ma_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_percent_above_ma_warmup_period(SEXP e) {
struct PercentAboveMa *h = (struct PercentAboveMa *)R_ExternalPtrAddr(e);
@@ -17276,7 +17291,7 @@ SEXP wk_tick_index_new(void) {
}
SEXP wk_tick_index_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct TickIndex *h = (struct TickIndex *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_tick_index_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_tick_index_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_tick_index_warmup_period(SEXP e) {
struct TickIndex *h = (struct TickIndex *)R_ExternalPtrAddr(e);
@@ -17853,7 +17868,7 @@ SEXP wk_trin_new(void) {
}
SEXP wk_trin_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct Trin *h = (struct Trin *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_trin_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_trin_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_trin_warmup_period(SEXP e) {
struct Trin *h = (struct Trin *)R_ExternalPtrAddr(e);
@@ -18651,7 +18666,7 @@ SEXP wk_up_down_volume_ratio_new(void) {
}
SEXP wk_up_down_volume_ratio_update(SEXP e, SEXP a0, SEXP a1, SEXP a2, SEXP a3, SEXP a4, SEXP a5, SEXP a6) {
struct UpDownVolumeRatio *h = (struct UpDownVolumeRatio *)R_ExternalPtrAddr(e);
return Rf_ScalarReal(wickra_up_down_volume_ratio_update(h, (double *)REAL(a0), (double *)REAL(a1), (bool *)REAL(a2), (bool *)REAL(a3), (bool *)REAL(a4), (bool *)REAL(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
return Rf_ScalarReal(wickra_up_down_volume_ratio_update(h, (double *)REAL(a0), (double *)REAL(a1), wk_bool_vec(a2), wk_bool_vec(a3), wk_bool_vec(a4), wk_bool_vec(a5), (uintptr_t)Rf_xlength(a0), (int64_t)Rf_asReal(a6)));
}
SEXP wk_up_down_volume_ratio_warmup_period(SEXP e) {
struct UpDownVolumeRatio *h = (struct UpDownVolumeRatio *)R_ExternalPtrAddr(e);
+518
View File
@@ -0,0 +1,518 @@
# Code generated by gen_golden_test.py. DO NOT EDIT.
# Per-indicator golden spec: canonical name, archetype, ctor params, output width.
GOLDEN_SPECS <- list(
list(canon="AbandonedBaby", arch="scalar_candle", params=c(), width=0L),
list(canon="Abcd", arch="scalar_candle", params=c(), width=0L),
list(canon="AbsoluteBreadthIndex", arch="cross", params=c(), width=0L),
list(canon="AccelerationBands", arch="multi_candle", params=c(14.0,2.0), width=3L),
list(canon="AcceleratorOscillator", arch="scalar_candle", params=c(3.0,7.0,14.0), width=0L),
list(canon="AdOscillator", arch="scalar_candle", params=c(), width=0L),
list(canon="AdVolumeLine", arch="cross", params=c(), width=0L),
list(canon="AdaptiveCci", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="AdaptiveCycle", arch="scalar_f64", params=c(), width=0L),
list(canon="AdaptiveLaguerreFilter", arch="scalar_f64", params=c(20.0), width=0L),
list(canon="AdaptiveRsi", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Adl", arch="scalar_candle", params=c(), width=0L),
list(canon="AdvanceBlock", arch="scalar_candle", params=c(), width=0L),
list(canon="AdvanceDecline", arch="cross", params=c(), width=0L),
list(canon="AdvanceDeclineRatio", arch="cross", params=c(), width=0L),
list(canon="Adx", arch="multi_candle", params=c(14.0), width=3L),
list(canon="Adxr", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Alligator", arch="multi_candle", params=c(3.0,7.0,14.0), width=3L),
list(canon="Alma", arch="scalar_f64", params=c(9.0,0.85,6.0), width=0L),
list(canon="Alpha", arch="pairwise", params=c(14.0,2.0), width=0L),
list(canon="AmihudIlliquidity", arch="trade", params=c(20.0), width=0L),
list(canon="AnchoredRsi", arch="scalar_f64", params=c(), width=0L),
list(canon="AnchoredVwap", arch="scalar_candle", params=c(), width=0L),
list(canon="AndrewsPitchfork", arch="multi_candle", params=c(14.0), width=3L),
list(canon="Apo", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Aroon", arch="multi_candle", params=c(14.0), width=2L),
list(canon="AroonOscillator", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Atr", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="AtrBands", arch="multi_candle", params=c(14.0,2.0), width=3L),
list(canon="AtrRatchet", arch="multi_candle", params=c(14.0,2.0,0.5), width=2L),
list(canon="AtrTrailingStop", arch="scalar_candle", params=c(14.0,2.0), width=0L),
list(canon="AutoFib", arch="multi_candle", params=c(), width=7L),
list(canon="Autocorrelation", arch="scalar_f64", params=c(10.0,1.0), width=0L),
list(canon="AutocorrelationPeriodogram", arch="scalar_f64", params=c(10.0,48.0), width=0L),
list(canon="AverageDailyRange", arch="scalar_candle", params=c(14.0,0.0), width=0L),
list(canon="AverageDrawdown", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="AvgPrice", arch="scalar_candle", params=c(), width=0L),
list(canon="AwesomeOscillator", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="AwesomeOscillatorHistogram", arch="scalar_candle", params=c(3.0,7.0,14.0), width=0L),
list(canon="BalanceOfPower", arch="scalar_candle", params=c(), width=0L),
list(canon="BandpassFilter", arch="scalar_f64", params=c(20.0,0.3), width=0L),
list(canon="Bat", arch="scalar_candle", params=c(), width=0L),
list(canon="BeltHold", arch="scalar_candle", params=c(), width=0L),
list(canon="Beta", arch="pairwise", params=c(14.0), width=0L),
list(canon="BetaNeutralSpread", arch="pairwise", params=c(14.0), width=0L),
list(canon="BetterVolume", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="BipowerVariation", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="BodySizePct", arch="scalar_candle", params=c(), width=0L),
list(canon="BollingerBands", arch="multi_f64", params=c(20.0,2.0), width=4L),
list(canon="BollingerBandwidth", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="BomarBands", arch="multi_f64", params=c(4.0,0.85), width=3L),
list(canon="BreadthThrust", arch="cross", params=c(10.0), width=0L),
list(canon="Breakaway", arch="scalar_candle", params=c(), width=0L),
list(canon="BullishPercentIndex", arch="cross", params=c(), width=0L),
list(canon="BurkeRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Butterfly", arch="scalar_candle", params=c(), width=0L),
list(canon="CalendarSpread", arch="deriv", params=c(), width=0L),
list(canon="CalmarRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Camarilla", arch="multi_candle", params=c(), width=9L),
list(canon="CandleVolume", arch="multi_candle", params=c(14.0), width=2L),
list(canon="Cci", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="CenterOfGravity", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="CentralPivotRange", arch="multi_candle", params=c(), width=3L),
list(canon="Cfo", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="ChaikinMoneyFlow", arch="scalar_candle", params=c(20.0), width=0L),
list(canon="ChaikinOscillator", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="ChaikinVolatility", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="ChandeKrollStop", arch="multi_candle", params=c(3.0,2.0,7.0), width=2L),
list(canon="ChandelierExit", arch="multi_candle", params=c(14.0,2.0), width=2L),
list(canon="ChoppinessIndex", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="ClassicPivots", arch="multi_candle", params=c(), width=7L),
list(canon="CloseVsOpen", arch="scalar_candle", params=c(), width=0L),
list(canon="ClosingMarubozu", arch="scalar_candle", params=c(), width=0L),
list(canon="Cmo", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="CoefficientOfVariation", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Cointegration", arch="multi_pairwise", params=c(40.0,1.0), width=3L),
list(canon="CommonSenseRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="CompositeProfile", arch="multi_candle", params=c(20.0,24.0,0.7), width=3L),
list(canon="ConcealingBabySwallow", arch="scalar_candle", params=c(), width=0L),
list(canon="ConditionalValueAtRisk", arch="scalar_f64", params=c(20.0,0.95), width=0L),
list(canon="ConnorsRsi", arch="scalar_f64", params=c(3.0,7.0,14.0), width=0L),
list(canon="Coppock", arch="scalar_f64", params=c(3.0,7.0,14.0), width=0L),
list(canon="CorrelationTrendIndicator", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Counterattack", arch="scalar_candle", params=c(), width=0L),
list(canon="Crab", arch="scalar_candle", params=c(), width=0L),
list(canon="CumulativeVolumeDelta", arch="trade", params=c(), width=0L),
list(canon="CumulativeVolumeIndex", arch="cross", params=c(), width=0L),
list(canon="CupAndHandle", arch="scalar_candle", params=c(), width=0L),
list(canon="CyberneticCycle", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Cypher", arch="scalar_candle", params=c(), width=0L),
list(canon="DayOfWeekProfile", arch="profile_bins", params=c(0.0), width=7L),
list(canon="Decycler", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="DecyclerOscillator", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Dema", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="DemandIndex", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="DemarkPivots", arch="multi_candle", params=c(), width=3L),
list(canon="DepthSlope", arch="ob", params=c(), width=0L),
list(canon="DerivativeOscillator", arch="scalar_f64", params=c(3.0,7.0,14.0,28.0), width=0L),
list(canon="DetrendedStdDev", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="DisparityIndex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="DistanceSsd", arch="pairwise", params=c(14.0), width=0L),
list(canon="Doji", arch="scalar_candle", params=c(), width=0L),
list(canon="DojiStar", arch="scalar_candle", params=c(), width=0L),
list(canon="DollarBars", arch="bars_candle5", params=c(50000.0), width=0L),
list(canon="Donchian", arch="multi_candle", params=c(14.0), width=3L),
list(canon="DonchianStop", arch="multi_candle", params=c(14.0), width=2L),
list(canon="DoubleBollinger", arch="multi_f64", params=c(20.0,1.0,2.0), width=5L),
list(canon="DoubleTopBottom", arch="scalar_candle", params=c(), width=0L),
list(canon="DownsideGapThreeMethods", arch="scalar_candle", params=c(), width=0L),
list(canon="Dpo", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="DragonflyDoji", arch="scalar_candle", params=c(), width=0L),
list(canon="DrawdownDuration", arch="scalar_f64", params=c(), width=0L),
list(canon="DumplingTop", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Dx", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="DynamicMomentumIndex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="EaseOfMovement", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="EffectiveSpread", arch="trademid", params=c(), width=0L),
list(canon="EhlersStochastic", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Ehma", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="ElderImpulse", arch="scalar_f64", params=c(3.0,7.0,14.0,28.0), width=0L),
list(canon="ElderRay", arch="multi_candle", params=c(14.0), width=2L),
list(canon="ElderSafeZone", arch="multi_candle", params=c(10.0,2.0), width=2L),
list(canon="Ema", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="EmpiricalModeDecomposition", arch="scalar_f64", params=c(20.0,0.1), width=0L),
list(canon="Engulfing", arch="scalar_candle", params=c(), width=0L),
list(canon="Equivolume", arch="multi_candle", params=c(14.0), width=2L),
list(canon="EstimatedLeverageRatio", arch="deriv", params=c(), width=0L),
list(canon="EvenBetterSinewave", arch="scalar_f64", params=c(40.0,10.0), width=0L),
list(canon="EveningDojiStar", arch="scalar_candle", params=c(), width=0L),
list(canon="Evwma", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="EwmaVolatility", arch="scalar_f64", params=c(0.94), width=0L),
list(canon="Expectancy", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="FallingThreeMethods", arch="scalar_candle", params=c(), width=0L),
list(canon="Fama", arch="scalar_f64", params=c(0.5,0.05), width=0L),
list(canon="FibArcs", arch="multi_candle", params=c(), width=3L),
list(canon="FibChannel", arch="multi_candle", params=c(), width=4L),
list(canon="FibConfluence", arch="multi_candle", params=c(), width=2L),
list(canon="FibExtension", arch="multi_candle", params=c(), width=5L),
list(canon="FibFan", arch="multi_candle", params=c(), width=3L),
list(canon="FibProjection", arch="multi_candle", params=c(), width=4L),
list(canon="FibRetracement", arch="multi_candle", params=c(), width=7L),
list(canon="FibTimeZones", arch="multi_candle", params=c(), width=2L),
list(canon="FibonacciPivots", arch="multi_candle", params=c(), width=7L),
list(canon="FisherRsi", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="FisherTransform", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="FlagPennant", arch="scalar_candle", params=c(), width=0L),
list(canon="Footprint", arch="footprint", params=c(1.0), width=0L),
list(canon="ForceIndex", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="FractalChaosBands", arch="multi_candle", params=c(14.0), width=2L),
list(canon="Frama", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="FryPanBottom", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="FundingBasis", arch="deriv", params=c(), width=0L),
list(canon="FundingImpliedApr", arch="deriv", params=c(1095.0), width=0L),
list(canon="FundingRate", arch="deriv", params=c(), width=0L),
list(canon="FundingRateMean", arch="deriv", params=c(20.0), width=0L),
list(canon="FundingRateZScore", arch="deriv", params=c(20.0), width=0L),
list(canon="GainLossRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="GainToPainRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="GapSideBySideWhite", arch="scalar_candle", params=c(), width=0L),
list(canon="Garch11", arch="scalar_f64", params=c(2e-06,0.1,0.88), width=0L),
list(canon="GarmanKlassVolatility", arch="scalar_candle", params=c(20.0,252.0), width=0L),
list(canon="Gartley", arch="scalar_candle", params=c(), width=0L),
list(canon="GatorOscillator", arch="multi_candle", params=c(3.0,7.0,14.0), width=2L),
list(canon="GeneralizedDema", arch="scalar_f64", params=c(5.0,0.7), width=0L),
list(canon="GeometricMa", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="GoldenPocket", arch="multi_candle", params=c(), width=3L),
list(canon="GrangerCausality", arch="pairwise", params=c(60.0,1.0), width=0L),
list(canon="GravestoneDoji", arch="scalar_candle", params=c(), width=0L),
list(canon="Hammer", arch="scalar_candle", params=c(), width=0L),
list(canon="HangingMan", arch="scalar_candle", params=c(), width=0L),
list(canon="Harami", arch="scalar_candle", params=c(), width=0L),
list(canon="HaramiCross", arch="scalar_candle", params=c(), width=0L),
list(canon="HasbrouckInformationShare", arch="pairwise", params=c(14.0), width=0L),
list(canon="HeadAndShoulders", arch="scalar_candle", params=c(), width=0L),
list(canon="HeikinAshi", arch="multi_candle", params=c(), width=4L),
list(canon="HeikinAshiOscillator", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="HiLoActivator", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="HighLowIndex", arch="cross", params=c(10.0), width=0L),
list(canon="HighLowRange", arch="scalar_candle", params=c(), width=0L),
list(canon="HighLowVolumeNodes", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="HighWave", arch="scalar_candle", params=c(), width=0L),
list(canon="HighpassFilter", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Hikkake", arch="scalar_candle", params=c(), width=0L),
list(canon="HikkakeModified", arch="scalar_candle", params=c(), width=0L),
list(canon="HilbertDominantCycle", arch="scalar_f64", params=c(), width=0L),
list(canon="HistoricalVolatility", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Hma", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="HoltWinters", arch="scalar_f64", params=c(0.5,0.1), width=0L),
list(canon="HomingPigeon", arch="scalar_candle", params=c(), width=0L),
list(canon="HtDcPhase", arch="scalar_f64", params=c(), width=0L),
list(canon="HtPhasor", arch="multi_f64", params=c(), width=2L),
list(canon="HtTrendMode", arch="scalar_f64", params=c(), width=0L),
list(canon="HurstChannel", arch="multi_candle", params=c(14.0,2.0), width=3L),
list(canon="HurstExponent", arch="scalar_f64", params=c(100.0,4.0), width=0L),
list(canon="Ichimoku", arch="multi_candle", params=c(9.0,26.0,52.0,26.0), width=5L),
list(canon="IdenticalThreeCrows", arch="scalar_candle", params=c(), width=0L),
list(canon="ImbalanceBars", arch="bars_candle4", params=c(5.0), width=0L),
list(canon="InNeck", arch="scalar_candle", params=c(), width=0L),
list(canon="Inertia", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="InformationRatio", arch="pairwise", params=c(14.0), width=0L),
list(canon="InitialBalance", arch="multi_candle", params=c(14.0), width=2L),
list(canon="InstantaneousTrendline", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="IntradayIntensity", arch="scalar_candle", params=c(), width=0L),
list(canon="IntradayMomentumIndex", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="IntradayVolatilityProfile", arch="profile_bins", params=c(24.0,0.0), width=24L),
list(canon="InverseFisherTransform", arch="scalar_f64", params=c(2.0), width=0L),
list(canon="InvertedHammer", arch="scalar_candle", params=c(), width=0L),
list(canon="JarqueBera", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Jma", arch="scalar_f64", params=c(7.0,0.0,2.0), width=0L),
list(canon="JumpIndicator", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="KRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="KagiBars", arch="bars_close", params=c(2.0), width=0L),
list(canon="KalmanHedgeRatio", arch="multi_pairwise", params=c(0.01,0.001), width=3L),
list(canon="Kama", arch="scalar_f64", params=c(3.0,7.0,14.0), width=0L),
list(canon="KaseDevStop", arch="multi_candle", params=c(14.0,2.0), width=2L),
list(canon="KasePermissionStochastic", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="KellyCriterion", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Keltner", arch="multi_candle", params=c(3.0,7.0,2.0), width=3L),
list(canon="KendallTau", arch="pairwise", params=c(14.0), width=0L),
list(canon="Kicking", arch="scalar_candle", params=c(), width=0L),
list(canon="KickingByLength", arch="scalar_candle", params=c(), width=0L),
list(canon="Kst", arch="multi_f64", params=c(3.0,7.0,14.0,28.0,35.0,42.0,56.0,63.0,70.0), width=2L),
list(canon="Kurtosis", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Kvo", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="KylesLambda", arch="trademid", params=c(20.0), width=0L),
list(canon="LadderBottom", arch="scalar_candle", params=c(), width=0L),
list(canon="LaguerreRsi", arch="scalar_f64", params=c(0.5), width=0L),
list(canon="LeadLagCrossCorrelation", arch="multi_pairwise", params=c(20.0,10.0), width=2L),
list(canon="LinRegAngle", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="LinRegChannel", arch="multi_f64", params=c(14.0,2.0), width=3L),
list(canon="LinRegIntercept", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="LinRegSlope", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="LinearRegression", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="LiquidationFeatures", arch="deriv_multi", params=c(), width=5L),
list(canon="LogReturn", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="LongLeggedDoji", arch="scalar_candle", params=c(), width=0L),
list(canon="LongLine", arch="scalar_candle", params=c(), width=0L),
list(canon="LongShortRatio", arch="deriv", params=c(), width=0L),
list(canon="M2Measure", arch="scalar_f64", params=c(14.0,2.0,0.5), width=0L),
list(canon="MaEnvelope", arch="multi_f64", params=c(14.0,2.0), width=3L),
list(canon="MacdExt", arch="multi_f64", params=c(12.0,0.0,26.0,0.0,9.0,0.0), width=3L),
list(canon="MacdFix", arch="multi_f64", params=c(9.0), width=3L),
list(canon="MacdHistogram", arch="scalar_f64", params=c(3.0,7.0,14.0), width=0L),
list(canon="MacdIndicator", arch="multi_f64", params=c(12.0,26.0,9.0), width=3L),
list(canon="Mama", arch="multi_f64", params=c(0.5,0.05), width=2L),
list(canon="MarketFacilitationIndex", arch="scalar_candle", params=c(), width=0L),
list(canon="MartinRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Marubozu", arch="scalar_candle", params=c(), width=0L),
list(canon="MassIndex", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="MatHold", arch="scalar_candle", params=c(), width=0L),
list(canon="MatchingLow", arch="scalar_candle", params=c(), width=0L),
list(canon="MaxDrawdown", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="McClellanOscillator", arch="cross", params=c(), width=0L),
list(canon="McClellanSummationIndex", arch="cross", params=c(), width=0L),
list(canon="McGinleyDynamic", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="MedianAbsoluteDeviation", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="MedianChannel", arch="multi_f64", params=c(14.0,2.0), width=3L),
list(canon="MedianMa", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="MedianPrice", arch="scalar_candle", params=c(), width=0L),
list(canon="Mfi", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Microprice", arch="ob", params=c(), width=0L),
list(canon="MidPoint", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="MidPrice", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="MinusDi", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="MinusDm", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="ModifiedMaStop", arch="multi_candle", params=c(14.0), width=2L),
list(canon="Mom", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="MorningDojiStar", arch="scalar_candle", params=c(), width=0L),
list(canon="MorningEveningStar", arch="scalar_candle", params=c(), width=0L),
list(canon="MurreyMathLines", arch="multi_candle", params=c(14.0), width=9L),
list(canon="NakedPoc", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="Natr", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="NewHighsNewLows", arch="cross", params=c(), width=0L),
list(canon="NewPriceLines", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Nrtr", arch="multi_candle", params=c(2.0), width=2L),
list(canon="Nvi", arch="scalar_candle", params=c(), width=0L),
list(canon="OIPriceDivergence", arch="deriv", params=c(20.0), width=0L),
list(canon="OIWeighted", arch="deriv", params=c(), width=0L),
list(canon="Obv", arch="scalar_candle", params=c(), width=0L),
list(canon="OiToVolumeRatio", arch="deriv", params=c(), width=0L),
list(canon="OmegaRatio", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="OnNeck", arch="scalar_candle", params=c(), width=0L),
list(canon="OpenInterestDelta", arch="deriv", params=c(), width=0L),
list(canon="OpenInterestMomentum", arch="deriv", params=c(10.0), width=0L),
list(canon="OpeningMarubozu", arch="scalar_candle", params=c(), width=0L),
list(canon="OpeningRange", arch="multi_candle", params=c(14.0), width=3L),
list(canon="OrderBookImbalanceFull", arch="ob", params=c(), width=0L),
list(canon="OrderBookImbalanceTop1", arch="ob", params=c(), width=0L),
list(canon="OrderBookImbalanceTopN", arch="ob", params=c(5.0), width=0L),
list(canon="OrderFlowImbalance", arch="ob", params=c(20.0), width=0L),
list(canon="OuHalfLife", arch="pairwise", params=c(14.0), width=0L),
list(canon="OvernightGap", arch="scalar_candle", params=c(0.0), width=0L),
list(canon="OvernightIntradayReturn", arch="multi_candle", params=c(14.0), width=2L),
list(canon="PainIndex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="PairSpreadZScore", arch="pairwise", params=c(20.0,20.0), width=0L),
list(canon="PairwiseBeta", arch="pairwise", params=c(14.0), width=0L),
list(canon="ParkinsonVolatility", arch="scalar_candle", params=c(20.0,252.0), width=0L),
list(canon="PearsonCorrelation", arch="pairwise", params=c(14.0), width=0L),
list(canon="PercentAboveMa", arch="cross", params=c(), width=0L),
list(canon="PercentB", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="PercentageTrailingStop", arch="scalar_f64", params=c(2.0), width=0L),
list(canon="PerpetualPremiumIndex", arch="deriv", params=c(), width=0L),
list(canon="Pgo", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="PiercingDarkCloud", arch="scalar_candle", params=c(), width=0L),
list(canon="Pin", arch="trade", params=c(20.0), width=0L),
list(canon="PivotReversal", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="PlusDi", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="PlusDm", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Pmo", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="PointAndFigureBars", arch="bars_close", params=c(2.0,3.0), width=0L),
list(canon="PolarizedFractalEfficiency", arch="scalar_f64", params=c(10.0,5.0), width=0L),
list(canon="Ppo", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="PpoHistogram", arch="scalar_f64", params=c(3.0,7.0,14.0), width=0L),
list(canon="ProfileShape", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="ProfitFactor", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="ProjectionBands", arch="multi_candle", params=c(14.0), width=3L),
list(canon="ProjectionOscillator", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Psar", arch="scalar_candle", params=c(0.02,0.02,0.2), width=0L),
list(canon="Pvi", arch="scalar_candle", params=c(), width=0L),
list(canon="Qqe", arch="multi_f64", params=c(3.0,7.0,2.0), width=2L),
list(canon="Qstick", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="QuartileBands", arch="multi_f64", params=c(14.0), width=3L),
list(canon="QuotedSpread", arch="ob", params=c(), width=0L),
list(canon="RSquared", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RangeBars", arch="bars_close", params=c(2.0), width=0L),
list(canon="RealizedSpread", arch="trademid", params=c(20.0), width=0L),
list(canon="RealizedVolatility", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RecoveryFactor", arch="scalar_f64", params=c(), width=0L),
list(canon="RectangleRange", arch="scalar_candle", params=c(), width=0L),
list(canon="Reflex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RegimeLabel", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="RelativeStrengthAB", arch="multi_pairwise", params=c(14.0,14.0), width=3L),
list(canon="RenkoBars", arch="bars_close", params=c(2.0), width=0L),
list(canon="RenkoTrailingStop", arch="scalar_f64", params=c(2.0), width=0L),
list(canon="RickshawMan", arch="scalar_candle", params=c(), width=0L),
list(canon="RisingThreeMethods", arch="scalar_candle", params=c(), width=0L),
list(canon="Rmi", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Roc", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Rocp", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Rocr", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Rocr100", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RogersSatchellVolatility", arch="scalar_candle", params=c(20.0,252.0), width=0L),
list(canon="RollMeasure", arch="trade", params=c(20.0), width=0L),
list(canon="RollingCorrelation", arch="pairwise", params=c(14.0), width=0L),
list(canon="RollingCovariance", arch="pairwise", params=c(14.0), width=0L),
list(canon="RollingIqr", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RollingMinMaxScaler", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RollingPercentileRank", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RollingQuantile", arch="scalar_f64", params=c(20.0,0.5), width=0L),
list(canon="RollingVwap", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="RoofingFilter", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Rsi", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Rsx", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="RunBars", arch="bars_candle4", params=c(3.0), width=0L),
list(canon="Rvi", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="RviVolatility", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Rwi", arch="multi_candle", params=c(14.0), width=2L),
list(canon="SampleEntropy", arch="scalar_f64", params=c(20.0,2.0,0.2), width=0L),
list(canon="SarExt", arch="scalar_candle", params=c(2.0,0.5,0.5,0.5,0.5,0.5,0.5,0.5), width=0L),
list(canon="SeasonalZScore", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="SeparatingLines", arch="scalar_candle", params=c(), width=0L),
list(canon="SessionHighLow", arch="multi_candle", params=c(14.0), width=2L),
list(canon="SessionRange", arch="multi_candle", params=c(14.0), width=3L),
list(canon="SessionVwap", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="ShannonEntropy", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Shark", arch="scalar_candle", params=c(), width=0L),
list(canon="SharpeRatio", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="ShootingStar", arch="scalar_candle", params=c(), width=0L),
list(canon="ShortLine", arch="scalar_candle", params=c(), width=0L),
list(canon="SignedVolume", arch="trade", params=c(), width=0L),
list(canon="SineWave", arch="scalar_f64", params=c(), width=0L),
list(canon="SineWeightedMa", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="SinglePrints", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="Skewness", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Sma", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Smi", arch="scalar_candle", params=c(3.0,7.0,14.0), width=0L),
list(canon="Smma", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="SmoothedHeikinAshi", arch="multi_candle", params=c(14.0), width=4L),
list(canon="SortinoRatio", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="SpearmanCorrelation", arch="pairwise", params=c(14.0), width=0L),
list(canon="SpinningTop", arch="scalar_candle", params=c(), width=0L),
list(canon="SpreadAr1Coefficient", arch="pairwise", params=c(14.0), width=0L),
list(canon="SpreadBollingerBands", arch="multi_pairwise", params=c(14.0,2.0), width=4L),
list(canon="SpreadHurst", arch="pairwise", params=c(14.0), width=0L),
list(canon="StalledPattern", arch="scalar_candle", params=c(), width=0L),
list(canon="StandardError", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="StandardErrorBands", arch="multi_f64", params=c(14.0,2.0), width=3L),
list(canon="StarcBands", arch="multi_candle", params=c(3.0,7.0,2.0), width=3L),
list(canon="Stc", arch="scalar_f64", params=c(10.0,23.0,10.0,0.5), width=0L),
list(canon="StdDev", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="StepTrailingStop", arch="scalar_f64", params=c(2.0), width=0L),
list(canon="SterlingRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="StickSandwich", arch="scalar_candle", params=c(), width=0L),
list(canon="StochRsi", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Stochastic", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="StochasticCci", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="SuperSmoother", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="SuperTrend", arch="multi_candle", params=c(14.0,2.0), width=2L),
list(canon="T3", arch="scalar_f64", params=c(5.0,0.7), width=0L),
list(canon="TailRatio", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TakerBuySellRatio", arch="deriv", params=c(), width=0L),
list(canon="Takuri", arch="scalar_candle", params=c(), width=0L),
list(canon="TasukiGap", arch="scalar_candle", params=c(), width=0L),
list(canon="TdCamouflage", arch="scalar_candle", params=c(), width=0L),
list(canon="TdClop", arch="scalar_candle", params=c(), width=0L),
list(canon="TdClopwin", arch="scalar_candle", params=c(), width=0L),
list(canon="TdCombo", arch="scalar_candle", params=c(3.0,7.0,14.0,28.0), width=0L),
list(canon="TdCountdown", arch="scalar_candle", params=c(3.0,7.0,14.0,28.0), width=0L),
list(canon="TdDWave", arch="scalar_candle", params=c(2.0), width=0L),
list(canon="TdDeMarker", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TdDifferential", arch="scalar_candle", params=c(), width=0L),
list(canon="TdLines", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="TdMovingAverage", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="TdOpen", arch="scalar_candle", params=c(), width=0L),
list(canon="TdPressure", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TdPropulsion", arch="scalar_candle", params=c(), width=0L),
list(canon="TdRangeProjection", arch="multi_candle", params=c(), width=2L),
list(canon="TdRei", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TdRiskLevel", arch="multi_candle", params=c(3.0,7.0), width=2L),
list(canon="TdSequential", arch="multi_candle", params=c(3.0,7.0,14.0,28.0), width=3L),
list(canon="TdSetup", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="TdTrap", arch="scalar_candle", params=c(), width=0L),
list(canon="Tema", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TermStructureBasis", arch="deriv", params=c(), width=0L),
list(canon="ThreeDrives", arch="scalar_candle", params=c(), width=0L),
list(canon="ThreeInside", arch="scalar_candle", params=c(), width=0L),
list(canon="ThreeLineBreak", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="ThreeLineBreakBars", arch="bars_close", params=c(3.0), width=0L),
list(canon="ThreeLineStrike", arch="scalar_candle", params=c(), width=0L),
list(canon="ThreeOutside", arch="scalar_candle", params=c(), width=0L),
list(canon="ThreeSoldiersOrCrows", arch="scalar_candle", params=c(), width=0L),
list(canon="ThreeStarsInSouth", arch="scalar_candle", params=c(), width=0L),
list(canon="Thrusting", arch="scalar_candle", params=c(), width=0L),
list(canon="TickBars", arch="bars_candle5", params=c(2.0), width=0L),
list(canon="TickIndex", arch="cross", params=c(), width=0L),
list(canon="Tii", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="TimeBasedStop", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TimeOfDayReturnProfile", arch="profile_bins", params=c(24.0,0.0), width=24L),
list(canon="TowerTopBottom", arch="scalar_candle", params=c(), width=0L),
list(canon="TpoProfile", arch="profile_pricebins", params=c(30.0,50.0), width=52L),
list(canon="TradeImbalance", arch="trade", params=c(20.0), width=0L),
list(canon="TradeSignAutocorrelation", arch="trade", params=c(20.0), width=0L),
list(canon="TradeVolumeIndex", arch="scalar_candle", params=c(2.0), width=0L),
list(canon="TrendLabel", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TrendStrengthIndex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Trendflex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TreynorRatio", arch="pairwise", params=c(14.0,2.0), width=0L),
list(canon="Triangle", arch="scalar_candle", params=c(), width=0L),
list(canon="Trima", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Trin", arch="cross", params=c(), width=0L),
list(canon="TripleTopBottom", arch="scalar_candle", params=c(), width=0L),
list(canon="Tristar", arch="scalar_candle", params=c(), width=0L),
list(canon="Trix", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TrueRange", arch="scalar_candle", params=c(), width=0L),
list(canon="Tsf", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="TsfOscillator", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Tsi", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="Tsv", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TtmSqueeze", arch="multi_candle", params=c(14.0,2.0,0.5), width=2L),
list(canon="TtmTrend", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TurnOfMonth", arch="scalar_candle", params=c(3.0,3.0,0.0), width=0L),
list(canon="Tweezer", arch="scalar_candle", params=c(), width=0L),
list(canon="TwiggsMoneyFlow", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="TwoCrows", arch="scalar_candle", params=c(), width=0L),
list(canon="TypicalPrice", arch="scalar_candle", params=c(), width=0L),
list(canon="UlcerIndex", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="UltimateOscillator", arch="scalar_candle", params=c(3.0,7.0,14.0), width=0L),
list(canon="UniqueThreeRiver", arch="scalar_candle", params=c(), width=0L),
list(canon="UniversalOscillator", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="UpDownVolumeRatio", arch="cross", params=c(), width=0L),
list(canon="UpsideGapThreeMethods", arch="scalar_candle", params=c(), width=0L),
list(canon="UpsideGapTwoCrows", arch="scalar_candle", params=c(), width=0L),
list(canon="UpsidePotentialRatio", arch="scalar_f64", params=c(14.0,2.0), width=0L),
list(canon="ValueArea", arch="multi_candle", params=c(20.0,50.0,0.7), width=3L),
list(canon="ValueAtRisk", arch="scalar_f64", params=c(20.0,0.95), width=0L),
list(canon="Variance", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="VarianceRatio", arch="pairwise", params=c(60.0,2.0), width=0L),
list(canon="VerticalHorizontalFilter", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Vidya", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="VolatilityCone", arch="multi_candle", params=c(3.0,7.0), width=5L),
list(canon="VolatilityOfVolatility", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="VolatilityRatio", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="VoltyStop", arch="scalar_candle", params=c(14.0,2.0), width=0L),
list(canon="VolumeBars", arch="bars_candle5", params=c(500.0), width=0L),
list(canon="VolumeByTimeProfile", arch="profile_bins", params=c(24.0,0.0), width=24L),
list(canon="VolumeOscillator", arch="scalar_candle", params=c(3.0,7.0), width=0L),
list(canon="VolumePriceTrend", arch="scalar_candle", params=c(), width=0L),
list(canon="VolumeProfile", arch="profile_pricebins", params=c(20.0,50.0), width=52L),
list(canon="VolumeRsi", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="VolumeWeightedMacd", arch="multi_candle", params=c(3.0,7.0,14.0), width=3L),
list(canon="VolumeWeightedSr", arch="multi_candle", params=c(14.0), width=2L),
list(canon="Vortex", arch="multi_candle", params=c(14.0), width=2L),
list(canon="Vpin", arch="trade", params=c(5000.0,10.0), width=0L),
list(canon="Vwap", arch="scalar_candle", params=c(), width=0L),
list(canon="VwapStdDevBands", arch="multi_candle", params=c(2.0), width=4L),
list(canon="Vwma", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Vzo", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="Wad", arch="scalar_candle", params=c(), width=0L),
list(canon="WavePm", arch="scalar_f64", params=c(3.0,7.0), width=0L),
list(canon="WaveTrend", arch="multi_candle", params=c(3.0,7.0,14.0), width=2L),
list(canon="Wedge", arch="scalar_candle", params=c(), width=0L),
list(canon="WeightedClose", arch="scalar_candle", params=c(), width=0L),
list(canon="WickRatio", arch="scalar_candle", params=c(), width=0L),
list(canon="WilliamsFractals", arch="multi_candle", params=c(), width=2L),
list(canon="WilliamsR", arch="scalar_candle", params=c(14.0), width=0L),
list(canon="WinRate", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="Wma", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="WoodiePivots", arch="multi_candle", params=c(), width=5L),
list(canon="YangZhangVolatility", arch="scalar_candle", params=c(20.0,252.0), width=0L),
list(canon="YoyoExit", arch="scalar_candle", params=c(14.0,2.0), width=0L),
list(canon="ZScore", arch="scalar_f64", params=c(14.0), width=0L),
list(canon="ZeroLagMacd", arch="multi_f64", params=c(3.0,7.0,14.0), width=3L),
list(canon="ZigZag", arch="multi_candle", params=c(0.02), width=2L),
list(canon="Zlema", arch="scalar_f64", params=c(14.0), width=0L)
)
+121
View File
@@ -0,0 +1,121 @@
# Generic golden-fixture parity for the whole 514-indicator catalogue: every
# indicator is reconstructed by its constructor, fed the synthetic stream derived
# from the shared testdata/golden input (identical to gen_golden's Rust
# construction) and checked bit-for-bit against g_<Canonical>.csv. One reflective
# runner flattens scalar, multi-output, profile and bar shapes.
#
# Like test-golden.R, the fixtures live at the repo root and are not bundled into
# the standalone package, so packaged checks (r-universe / CRAN) skip; the parity
# is enforced by the monorepo CI. Specs are generated by gen_golden_test.py.
find_golden_dir_all <- 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_all <- find_golden_dir_all()
test_that("all 514 indicators match the Rust golden fixtures", {
skip_if(is.null(golden_dir_all), "golden fixtures not bundled with the package")
source(test_path("golden_specs.R"), local = TRUE)
gcell <- function(s) {
if (s == "nan") NA_real_ else if (s == "inf") Inf else if (s == "-inf") -Inf else as.numeric(s)
}
read_rows <- function(name) {
lines <- readLines(file.path(golden_dir_all, paste0(name, ".csv")))[-1]
lapply(lines, function(l) {
if (nchar(l) == 0) return(numeric(0))
vapply(strsplit(l, ",", fixed = TRUE)[[1]], gcell, numeric(1), USE.NAMES = FALSE)
})
}
input_rows <- lapply(
readLines(file.path(golden_dir_all, "input.csv"))[-1],
function(l) as.numeric(strsplit(l, ",", fixed = TRUE)[[1]])
)
deriv_fields <- function(r) {
o <- r[1]; h <- r[2]; l <- r[3]; c <- r[4]; v <- r[5]
c((c - o) / c * 0.01, c, c - 0.5, c + 1.0, v * 10, v * 0.6, v * 0.4,
v * 0.55, v * 0.45, h - c, c - l)
}
cross_lists <- function(r) {
o <- r[1]; c <- r[4]; v <- r[5]; j <- 0:4
list(change = (c - o) + j, volume = v + j * 10,
newHigh = as.numeric(j %% 2 == 0), newLow = as.numeric(j %% 3 == 0),
aboveMa = as.numeric(j %% 2 == 0), onBuy = as.numeric(j %% 3 == 0))
}
ob_lists <- function(r) {
c <- r[4]; v <- r[5]; k <- 1:5
list(bp = c - 0.1 * k, bs = v / k, ap = c + 0.1 * k, asz = v * 0.9 / k)
}
flatten <- function(o, arch, width) {
if (arch %in% c("profile_bins")) {
if (is.null(o) || length(o) == 0 || all(is.na(o))) return(rep(NA_real_, width))
return(as.numeric(o))
}
if (arch == "profile_pricebins") {
if (is.list(o)) return(c(o$price_low, o$price_high, as.numeric(o$values)))
return(rep(NA_real_, width))
}
if (arch %in% c("bars_close", "bars_candle4", "bars_candle5", "footprint")) {
if (is.null(o) || length(o) == 0) return(numeric(0))
if (is.matrix(o)) return(as.numeric(t(o)))
return(as.numeric(o))
}
as.numeric(o)
}
compute <- function(spec, ind, r, i) {
o <- r[1]; h <- r[2]; l <- r[3]; cl <- r[4]; v <- r[5]; ts <- as.integer(i - 1)
out <- switch(spec$arch,
scalar_f64 = update(ind, cl),
multi_f64 = update(ind, cl),
pairwise = , multi_pairwise = update(ind, cl, o),
scalar_candle = , multi_candle = , profile_bins = , profile_pricebins =
update(ind, o, h, l, cl, v, ts),
trade = update(ind, cl, v, cl >= o, ts),
trademid = update(ind, cl, v, cl >= o, ts, (h + l) / 2),
ob = { L <- ob_lists(r); update(ind, L$bp, L$bs, L$ap, L$asz) },
cross = { L <- cross_lists(r)
update(ind, L$change, L$volume, L$newHigh, L$newLow, L$aboveMa, L$onBuy, ts) },
deriv = , deriv_multi = { d <- deriv_fields(r)
do.call(update, c(list(ind), as.list(d), list(ts))) },
bars_close = update(ind, cl, cl, cl, cl, 1, 0L),
bars_candle4 = update(ind, o, h, l, cl, 1, 0L),
bars_candle5 = update(ind, o, h, l, cl, v, 0L),
footprint = update(ind, cl, v, cl >= o, ts),
stop("arch ", spec$arch)
)
flatten(out, spec$arch, spec$width)
}
for (spec in GOLDEN_SPECS) {
ind <- do.call(get(spec$canon), as.list(spec$params))
exp <- read_rows(paste0("g_", spec$canon))
for (i in seq_along(input_rows)) {
got <- compute(spec, ind, input_rows[[i]], i)
want <- exp[[i]]
expect_equal(length(got), length(want),
info = sprintf("%s row %d arity", spec$canon, i))
for (k in seq_along(want)) {
w <- want[k]; g <- got[k]
if (is.na(w)) {
expect_true(is.na(g), info = sprintf("%s row %d col %d: want NA", spec$canon, i, k))
} else if (is.infinite(w)) {
expect_true(is.infinite(g) && sign(g) == sign(w),
info = sprintf("%s row %d col %d: want %g", spec$canon, i, k, w))
} else {
expect_lte(abs(g - w), 1e-6 * max(1, abs(w)),
label = sprintf("%s row %d col %d (got %s want %g)", spec$canon, i, k, as.character(g), w))
}
}
}
}
})
+34
View File
@@ -117,3 +117,37 @@ test_that("multi-output ADX matches golden", {
}
}
})
# The four de-duplicated indicators, pinned against the Rust reference.
test_that("de-duplicated candle indicators match golden", {
skip_if_no_golden()
golden_input <- read_golden_input()
specs <- list(
list("ad_oscillator", AdOscillator()),
list("intraday_intensity", IntradayIntensity()),
list("awesome_oscillator_histogram", AwesomeOscillatorHistogram(5, 34, 1))
)
for (spec in specs) {
name <- spec[[1]]
ind <- spec[[2]]
exp <- read_golden(name)
for (i in seq_len(nrow(golden_input))) {
got <- update(ind, 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, name)
}
}
})
test_that("AverageDrawdown matches golden", {
skip_if_no_golden()
golden_input <- read_golden_input()
avg <- AverageDrawdown(20)
exp <- read_golden("average_drawdown")
for (i in seq_len(nrow(golden_input))) {
# generator fed the close column as the equity-curve sample.
got <- update(avg, golden_input$close[i])
expect_close(got, gcell(exp[i, 1]), i, "average_drawdown")
}
})
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or Derivative
Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 kingchenc and the Wickra contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 kingchenc and the Wickra contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+89
View File
@@ -0,0 +1,89 @@
"""Generate wasm_manifest.json for the WASM golden replay: for every one of the
514 indicators, record the JS class name, constructor params, the ordered update
argument names (parsed from pkg/wickra_wasm.d.ts, each flagged array/bigint) and
the output archetype (from golden_manifest.json). Run from repo root after
`wasm-pack build --target nodejs`: python bindings/wasm/gen_golden_test.py
"""
import json
import os
import re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
G = os.path.join(ROOT, "testdata", "golden")
DTS = os.path.join(ROOT, "bindings", "wasm", "pkg", "wickra_wasm.d.ts")
# canonical -> native (JS class) and canonical -> (arch, n/width) from the manifests.
native = {}
arch = {}
extra = {}
for e in json.load(open(os.path.join(G, "scalar_manifest.json"))):
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[e["input"]]
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[e["input"]]
extra[e["canonical"]] = {"n": e["n"]}
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
for e in ex["deriv"]:
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = "deriv_multi" if "n" in e else "deriv"
if "n" in e:
extra[e["canonical"]] = {"n": e["n"]}
for fam, a in (("cross", "cross"), ("trade", "trade"), ("trademid", "trademid"), ("ob", "ob")):
for e in ex[fam]:
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = a
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = "profile_" + e["kind"]
extra[e["canonical"]] = {"width": e["width"], **({"arrayField": "counts" if e["canonical"] == "TpoProfile" else "bins"} if e["kind"] == "pricebins" else {})}
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
native[e["canonical"]] = e["native"]
arch[e["canonical"]] = "footprint" if e["canonical"] == "Footprint" else "bars"
# params per canonical (constructor values) — same as the other bindings.
params = {}
for fn in ("scalar_manifest", "multi_manifest"):
for e in json.load(open(os.path.join(G, fn + ".json"))):
params[e["canonical"]] = e["params"]
for fam in json.load(open(os.path.join(G, "exotic_manifest.json"))).values():
for e in fam:
params[e["canonical"]] = e["params"]
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
params[e["canonical"]] = e["params"]
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
params[e["canonical"]] = e["params"]
# parse update args per JS class from the wasm .d.ts
dts = open(DTS, encoding="utf-8").read()
cls_args = {}
for m in re.finditer(r"export class (\w+) \{(.*?)\n\}", dts, re.S):
name, body = m.group(1), m.group(2)
um = re.search(r"\bupdate\(([^)]*)\)", body)
args = []
if um and um.group(1).strip():
for p in um.group(1).split(","):
p = p.strip()
nm = p.split(":")[0].strip()
typ = p.split(":", 1)[1].strip() if ":" in p else ""
args.append({"name": nm, "array": "Array" in typ, "bigint": "bigint" in typ})
cls_args[name] = args
out = []
for canon in sorted(native):
js = native[canon]
if js not in cls_args:
raise SystemExit(f"WASM class {js} (for {canon}) not in d.ts")
ctor = params.get(canon, [])
# EaseOfMovement's volume divisor is an optional Rust constructor argument
# (default 1e8) but a required WASM constructor parameter; pass it explicitly.
if canon == "EaseOfMovement":
ctor = [ctor[0], 100000000.0]
e = {"canonical": canon, "js": js, "ctor": ctor,
"args": cls_args[js], "out": arch[canon]}
e.update(extra.get(canon, {}))
out.append(e)
json.dump(out, open(os.path.join(G, "wasm_manifest.json"), "w"), indent=1)
from collections import Counter
print("wasm_manifest:", len(out), dict(Counter(e["out"] for e in out)))
+2 -2
View File
@@ -3145,12 +3145,12 @@ impl WasmKvo {
}
}
#[wasm_bindgen(js_name = WilliamsAD)]
#[wasm_bindgen(js_name = ADOSC)]
pub struct WasmAdOscillator {
inner: wc::AdOscillator,
}
#[wasm_bindgen(js_class = WilliamsAD)]
#[wasm_bindgen(js_class = ADOSC)]
impl WasmAdOscillator {
#[wasm_bindgen(constructor)]
#[allow(clippy::new_without_default)]
+156
View File
@@ -0,0 +1,156 @@
// Generic golden-fixture parity for the WASM (wasm-bindgen) binding, run under
// Node's test runner against the nodejs-target build in ../pkg.
//
// Every one of the 514 indicators is reconstructed from wasm_manifest.json (JS
// class, constructor params, ordered update args parsed from the generated
// .d.ts), fed the synthetic stream derived from the shared golden input — the
// same construction gen_golden uses — and checked bit-for-bit against the Rust
// reference fixtures g_<Canonical>.csv.
//
// wasm-pack build --target nodejs --out-dir pkg
// node --test tests/
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const W = require('../pkg/wickra_wasm.js');
const GOLDEN = path.resolve(__dirname, '..', '..', '..', 'testdata', 'golden');
function cell(s) {
if (s === 'nan') return NaN;
if (s === 'inf') return Infinity;
if (s === '-inf') return -Infinity;
return Number(s);
}
function readCsv(name) {
const lines = fs.readFileSync(path.join(GOLDEN, name + '.csv'), 'utf8').split('\n');
lines.shift();
return lines.filter((l) => l.length > 0).map((l) => l.split(',').map(cell));
}
function readBarRows(name) {
const lines = fs.readFileSync(path.join(GOLDEN, name + '.csv'), 'utf8').split('\n');
lines.shift();
if (lines.length && lines[lines.length - 1] === '') lines.pop();
return lines.map((l) => (l.length === 0 ? [] : l.split(',').map(cell)));
}
const MANIFEST = JSON.parse(fs.readFileSync(path.join(GOLDEN, 'wasm_manifest.json'), 'utf8'));
const ROWS = readCsv('input');
function deriv(o, h, l, c, v) {
return {
funding_rate: ((c - o) / c) * 0.01,
mark_price: c,
index_price: c - 0.5,
futures_price: c + 1.0,
open_interest: v * 10.0,
long_size: v * 0.6,
short_size: v * 0.4,
taker_buy_volume: v * 0.55,
taker_sell_volume: v * 0.45,
long_liquidation: h - c,
short_liquidation: c - l,
};
}
function resolveArg(arg, o, h, l, c, v, i) {
const n = arg.name;
if (arg.array) {
const j5 = [0, 1, 2, 3, 4];
switch (n) {
case 'change': return Float64Array.from(j5.map((j) => c - o + j));
case 'volume': return Float64Array.from(j5.map((j) => v + j * 10.0));
case 'new_high': return Float64Array.from(j5.map((j) => (j % 2 === 0 ? 1 : 0)));
case 'new_low': return Float64Array.from(j5.map((j) => (j % 3 === 0 ? 1 : 0)));
case 'above_ma': return Float64Array.from(j5.map((j) => (j % 2 === 0 ? 1 : 0)));
case 'on_buy_signal': return Float64Array.from(j5.map((j) => (j % 3 === 0 ? 1 : 0)));
case 'bid_px': return Float64Array.from(j5.map((k) => c - 0.1 * (k + 1)));
case 'bid_sz': return Float64Array.from(j5.map((k) => v / (k + 1)));
case 'ask_px': return Float64Array.from(j5.map((k) => c + 0.1 * (k + 1)));
case 'ask_sz': return Float64Array.from(j5.map((k) => (v * 0.9) / (k + 1)));
default: throw new Error('array arg ' + n);
}
}
if (arg.bigint) return BigInt(i);
switch (n) {
case 'value': case 'close': case 'price': case 'x': case 'a': case 'asset': return c;
case 'y': case 'b': case 'open': case 'benchmark': return o;
case 'high': return h;
case 'low': return l;
case 'volume': case 'size': return v;
case 'is_buy': return c >= o;
case 'mid': return (h + l) / 2.0;
case 'timestamp': return BigInt(i);
default: {
const d = deriv(o, h, l, c, v);
if (n in d) return d[n];
throw new Error('scalar arg ' + n);
}
}
}
// Recursively flatten a WASM output (number, object with field props, typed/array
// of either) into a flat number list. Returns null for warmup (null/undefined).
function flat(v) {
if (v === null || v === undefined) return null;
if (typeof v === 'number' || typeof v === 'bigint') return [Number(v)];
if (Array.isArray(v) || ArrayBuffer.isView(v)) {
const out = [];
for (const e of v) { const f = flat(e); if (f) out.push(...f); }
return out;
}
if (typeof v === 'object') {
const out = [];
for (const val of Object.values(v)) { const f = flat(val); if (f) out.push(...f); }
return out;
}
return [Number(v)];
}
function nanRow(n) {
return Array.from({ length: n }, () => NaN);
}
function widthOf(spec) {
if (spec.out.startsWith('multi') || spec.out === 'deriv_multi') return spec.n;
if (spec.out.startsWith('profile')) return spec.width;
return 1; // scalar archetypes
}
function closeEq(got, want, label) {
if (Number.isNaN(want)) { assert.ok(Number.isNaN(got), `${label}: want NaN got ${got}`); return; }
if (!Number.isFinite(want)) { assert.ok(got === want, `${label}: want ${want} got ${got}`); return; }
const tol = 1e-6 * Math.max(1.0, Math.abs(want));
assert.ok(Math.abs(got - want) <= tol, `${label}: got ${got} want ${want}`);
}
for (const spec of MANIFEST) {
test(`wasm golden: ${spec.canonical}`, () => {
const Cls = W[spec.js];
assert.ok(Cls, `missing WASM class ${spec.js}`);
const ind = new Cls(...spec.ctor);
const isBars = spec.out === 'bars' || spec.out === 'footprint';
const expected = isBars ? readBarRows('g_' + spec.canonical) : readCsv('g_' + spec.canonical);
for (let i = 0; i < ROWS.length; i++) {
const [o, h, l, c, v] = ROWS[i];
const args = spec.args.map((a) => resolveArg(a, o, h, l, c, v, i));
const raw = ind.update(...args);
const want = expected[i];
const label = `${spec.canonical} row ${i}`;
let got = flat(raw);
if (isBars) {
if (got === null) got = [];
} else if (got === null) {
got = nanRow(widthOf(spec));
}
assert.equal(got.length, want.length, `${label}: arity ${got.length} vs ${want.length}`);
for (let k = 0; k < want.length; k++) closeEq(got[k], want[k], `${label} col ${k}`);
}
});
}
+125 -102
View File
@@ -1,28 +1,35 @@
//! Williams Accumulation/Distribution.
//! Williams A/D Oscillator (ADOSC).
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Larry Williams' Accumulation/Distribution — a cumulative volume-less price
/// flow that classifies each bar as accumulation or distribution based on its
/// close relative to the previous close, then sums the directional component.
/// Smoothing window applied to the Williams A/D line to form the oscillator.
const SIGNAL_PERIOD: usize = 13;
/// Williams **A/D Oscillator** — the volume-free Williams Accumulation/
/// Distribution line measured against its own moving average, so it oscillates
/// around zero instead of drifting like the cumulative line.
///
/// Williams' definition (1972) uses a *true* high/low that includes the prior
/// close as an anchor — the same idea that motivates true range:
/// The underlying line is Larry Williams' volume-less A/D (1972), which uses a
/// *true* high/low anchored on the prior close; the oscillator subtracts its
/// 13-bar simple moving average:
///
/// ```text
/// TR_h_t = max(close_{t1}, high_t)
/// TR_l_t = min(close_{t1}, low_t)
/// AD_t = AD_{t1} + (close_t TR_l_t) if close_t > close_{t1} (accumulation)
/// AD_t = AD_{t1} + (close_t TR_h_t) if close_t < close_{t1} (distribution)
/// AD_t = AD_{t1} if close_t == close_{t1} (no change)
/// WAD_t = WAD_{t1} + (close_t TR_l_t) if close_t > close_{t1}
/// WAD_t = WAD_{t1} + (close_t TR_h_t) if close_t < close_{t1}
/// WAD_t = WAD_{t1} if close_t == close_{t1}
/// ADOSC_t = WAD_t SMA(WAD, 13)_t
/// ```
///
/// Unlike Chaikin's Accumulation/Distribution Line, the Williams A/D ignores
/// volume entirely — Williams argued that the relative position of the close
/// already encodes the day's "true" buying or selling pressure. The series is
/// unbounded and used primarily for divergence analysis. The first candle only
/// seeds the previous close; the first emission lands at bar 2.
/// This is distinct from the raw cumulative line, which Wickra ships as
/// [`Wad`](crate::Wad): `Wad` is the drifting line for divergence analysis,
/// while this oscillator is its zero-centred, mean-reverting form (positive
/// when accumulation is running ahead of its recent average, negative when
/// distribution is). The first bar only seeds the previous close; the first
/// oscillator value lands once the 13-bar average of the line is full.
///
/// # Example
///
@@ -39,30 +46,35 @@ use crate::traits::Indicator;
/// }
/// assert!(last.is_some());
/// ```
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct AdOscillator {
prev_close: Option<f64>,
total: f64,
has_emitted: bool,
line: f64,
signal: Sma,
last: Option<f64>,
}
impl Default for AdOscillator {
fn default() -> Self {
Self::new()
}
}
impl AdOscillator {
/// Construct a new Williams A/D starting at zero.
pub const fn new() -> Self {
/// Construct a new Williams A/D Oscillator.
#[must_use]
pub fn new() -> Self {
Self {
prev_close: None,
total: 0.0,
has_emitted: false,
line: 0.0,
signal: Sma::new(SIGNAL_PERIOD).expect("SIGNAL_PERIOD is non-zero"),
last: None,
}
}
/// Current cumulative value if at least one emission has happened.
/// Current oscillator value if available.
pub const fn value(&self) -> Option<f64> {
if self.has_emitted {
Some(self.total)
} else {
None
}
self.last
}
}
@@ -78,45 +90,47 @@ impl Indicator for AdOscillator {
};
let delta = if candle.close > prev {
// Accumulation: distance from the true low.
let tr_l = prev.min(candle.low);
candle.close - tr_l
candle.close - prev.min(candle.low)
} else if candle.close < prev {
// Distribution: distance from the true high (negative).
let tr_h = prev.max(candle.high);
candle.close - tr_h
candle.close - prev.max(candle.high)
} else {
// Unchanged close contributes nothing.
0.0
};
self.total += delta;
self.line += delta;
self.prev_close = Some(candle.close);
self.has_emitted = true;
Some(self.total)
let signal = self.signal.update(self.line)?;
let osc = self.line - signal;
self.last = Some(osc);
Some(osc)
}
fn reset(&mut self) {
self.prev_close = None;
self.total = 0.0;
self.has_emitted = false;
self.line = 0.0;
self.signal.reset();
self.last = None;
}
fn warmup_period(&self) -> usize {
// One seed bar; the second bar is the first emission.
2
// One seed bar establishes the prior close; the line then feeds the
// 13-bar signal SMA, which is full after `SIGNAL_PERIOD` line values.
1 + SIGNAL_PERIOD
}
fn is_ready(&self) -> bool {
self.has_emitted
self.last.is_some()
}
fn name(&self) -> &'static str {
"WilliamsAD"
"ADOSC"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::indicators::wad::Wad;
use crate::traits::BatchExt;
use approx::assert_relative_eq;
@@ -127,94 +141,103 @@ mod tests {
#[test]
fn accessors_and_metadata() {
let ad = AdOscillator::new();
assert_eq!(ad.name(), "WilliamsAD");
assert_eq!(ad.warmup_period(), 2);
assert_eq!(ad.value(), None);
}
#[test]
fn value_returns_total_after_first_emission() {
let mut ad = AdOscillator::new();
ad.update(c(10.0, 11.0, 9.0, 10.0, 0));
let v = ad.update(c(11.0, 13.0, 8.0, 12.0, 1)).unwrap();
assert_relative_eq!(ad.value().unwrap(), v, epsilon = 1e-12);
}
#[test]
fn first_bar_only_seeds() {
let mut ad = AdOscillator::new();
assert_eq!(ad.update(c(10.0, 11.0, 9.0, 10.0, 0)), None);
assert_eq!(ad.name(), "ADOSC");
assert_eq!(ad.warmup_period(), 14);
assert!(!ad.is_ready());
assert_eq!(ad.value(), None);
// `Default` matches `new`.
assert_eq!(AdOscillator::default().warmup_period(), 14);
}
#[test]
fn accumulation_adds_distance_from_true_low() {
// prev close = 10, today low = 8, today close = 12 (up day).
// TR_l = min(10, 8) = 8, delta = 12 - 8 = 4. AD = 0 + 4 = 4.
fn seed_bar_returns_none() {
let mut ad = AdOscillator::new();
ad.update(c(10.0, 11.0, 9.0, 10.0, 0));
let v = ad.update(c(11.0, 13.0, 8.0, 12.0, 1)).unwrap();
assert_relative_eq!(v, 4.0, epsilon = 1e-12);
assert_eq!(ad.update(c(100.0, 101.0, 99.0, 100.0, 0)), None);
}
#[test]
fn distribution_adds_distance_from_true_high() {
// prev close = 10, today high = 11, today close = 7 (down day).
// TR_h = max(10, 11) = 11, delta = 7 - 11 = -4. AD = -4.
let mut ad = AdOscillator::new();
ad.update(c(10.0, 11.0, 9.0, 10.0, 0));
let v = ad.update(c(10.0, 11.0, 7.0, 7.0, 1)).unwrap();
assert_relative_eq!(v, -4.0, epsilon = 1e-12);
fn equals_wad_line_minus_its_sma() {
// The oscillator is exactly the Williams A/D line minus its 13-SMA, so
// it must match the standalone `Wad` line passed through an SMA(13).
let candles: Vec<Candle> = (0..80_i64)
.map(|i| {
let base = 100.0 + (i as f64 * 0.3).sin() * 6.0;
c(
base,
base + 2.0,
base - 2.0,
base + (i as f64 * 0.5).cos(),
i,
)
})
.collect();
let osc = AdOscillator::new().batch(&candles);
// Reconstruct: Wad line, then line SMA(line, 13).
let line = Wad::new().batch(&candles);
let mut sma = Sma::new(SIGNAL_PERIOD).unwrap();
let expected: Vec<Option<f64>> = line
.iter()
.map(|v| v.and_then(|l| sma.update(l).map(|s| l - s)))
.collect();
assert_eq!(osc, expected);
}
#[test]
fn unchanged_close_keeps_total() {
// close equals prev close -> no contribution.
fn flat_market_oscillates_at_zero() {
// A flat market never accumulates or distributes, so the line is
// constant and the oscillator sits at zero once warm.
let mut ad = AdOscillator::new();
ad.update(c(10.0, 11.0, 9.0, 10.0, 0));
let v = ad.update(c(10.0, 12.0, 8.0, 10.0, 1)).unwrap();
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
}
#[test]
fn constant_series_yields_zero() {
// Every close equals the previous -> AD stays at zero forever.
let candles: Vec<Candle> = (0..40).map(|i| c(10.0, 11.0, 9.0, 10.0, i)).collect();
let mut ad = AdOscillator::new();
for v in ad.batch(&candles).into_iter().flatten() {
assert_relative_eq!(v, 0.0, epsilon = 1e-12);
let candles: Vec<Candle> = (0..40).map(|i| c(50.0, 50.0, 50.0, 50.0, i)).collect();
let out = ad.batch(&candles);
for v in out.iter().skip(ad.warmup_period() - 1).flatten() {
assert_relative_eq!(*v, 0.0, epsilon = 1e-12);
}
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..80i64)
fn warmup_emits_at_warmup_period() {
let mut ad = AdOscillator::new();
let candles: Vec<Candle> = (0..20)
.map(|i| {
let f = i as f64;
let mid = 100.0 + (f * 0.3).sin() * 5.0;
c(mid, mid + 2.0, mid - 2.0, mid + 0.5, i)
let close = 100.0 + f64::from(i);
c(close, close + 2.0, close - 2.0, close, i64::from(i))
})
.collect();
let mut a = AdOscillator::new();
let mut b = AdOscillator::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
let out = ad.batch(&candles);
assert_eq!(ad.warmup_period(), 14);
for v in out.iter().take(13) {
assert!(v.is_none());
}
assert!(out[13].is_some());
}
#[test]
fn reset_clears_state() {
let mut ad = AdOscillator::new();
ad.batch(&[
c(10.0, 11.0, 9.0, 10.0, 0),
c(10.0, 12.0, 9.0, 11.0, 1),
c(11.0, 13.0, 10.0, 12.0, 2),
]);
let candles: Vec<Candle> = (0..30)
.map(|i| {
let close = 100.0 + f64::from(i);
c(close, close + 2.0, close - 2.0, close, i64::from(i))
})
.collect();
ad.batch(&candles);
assert!(ad.is_ready());
ad.reset();
assert!(!ad.is_ready());
assert_eq!(ad.value(), None);
assert_eq!(ad.update(c(10.0, 11.0, 9.0, 10.0, 3)), None);
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..100_i64)
.map(|i| {
let base = 100.0 + (i as f64 * 0.2).sin() * 5.0;
c(base, base + 1.5, base - 1.5, base + 0.4, i)
})
.collect();
let batch = AdOscillator::new().batch(&candles);
let mut s = AdOscillator::new();
let streamed: Vec<_> = candles.iter().map(|x| s.update(*x)).collect();
assert_eq!(batch, streamed);
}
}
@@ -7,19 +7,23 @@ use crate::traits::Indicator;
/// Rolling Average Drawdown.
///
/// Input is treated as an equity-curve sample. The indicator scans the
/// trailing window of `period` values, tracks the running peak inside the
/// window, and reports the **mean** of all bar-by-bar drawdowns (the average
/// "pain" of being under water):
/// Input is treated as an equity-curve sample. Over the trailing window of
/// `period` values the indicator identifies each **distinct drawdown episode**
/// — a stretch where equity is below the running peak — and reports the **mean
/// of the episodes' maximum depths**:
///
/// ```text
/// drawdown_t = (peak_t equity_t) / peak_t (running peak inside window)
/// AvgDD = mean(drawdown_t over window)
/// episode opens when equity < running peak
/// episode closes when equity reaches a new peak (full recovery)
/// depth(episode) = (episode_peak episode_trough) / episode_peak
/// AvgDD = mean(depth over episodes in window) (0 if no drawdown)
/// ```
///
/// Output is non-negative (a fraction; `0.05` ≈ 5 % average drawdown). This
/// is the **Pain Index** under a different name — see [`crate::PainIndex`]
/// for the same metric exposed under its conventional label.
/// This is the conventional "average drawdown" (mean depth across separate
/// drawdowns), which is distinct from the [`crate::PainIndex`] — the latter
/// averages the under-water fraction at *every* bar, so a long shallow
/// drawdown weighs more there than here. Output is a non-negative fraction
/// (`0.05` ≈ 5 % mean episode depth).
///
/// Each `update` is O(period).
#[derive(Debug, Clone)]
@@ -65,16 +69,40 @@ impl Indicator for AverageDrawdown {
return None;
}
let mut peak = f64::NEG_INFINITY;
let mut sum_dd = 0.0_f64;
let mut sum_depth = 0.0_f64;
let mut episodes = 0_u32;
let mut in_dd = false;
let mut episode_peak = 0.0_f64;
let mut episode_trough = 0.0_f64;
for &v in &self.window {
if v > peak {
if v >= peak {
if in_dd {
if episode_peak > 0.0 {
sum_depth += (episode_peak - episode_trough) / episode_peak;
episodes += 1;
}
in_dd = false;
}
peak = v;
}
if peak > 0.0 {
sum_dd += (peak - v) / peak;
} else if in_dd {
if v < episode_trough {
episode_trough = v;
}
} else {
in_dd = true;
episode_peak = peak;
episode_trough = v;
}
}
Some(sum_dd / self.period as f64)
if in_dd && episode_peak > 0.0 {
sum_depth += (episode_peak - episode_trough) / episode_peak;
episodes += 1;
}
Some(if episodes == 0 {
0.0
} else {
sum_depth / f64::from(episodes)
})
}
fn reset(&mut self) {
@@ -124,13 +152,24 @@ mod tests {
#[test]
fn reference_value() {
// window [100, 120, 90, 110]:
// peaks: 100, 120, 120, 120; dd: 0, 0, (30/120)=.25, (10/120)=.0833...
// avg = (.25 + .0833...) / 4 = .0833...
// window [100, 120, 90, 110]: one drawdown episode, opened at 90 (peak
// 120) and never recovering to 120 within the window. Its depth is
// (120 - 90) / 120 = 0.25; 110 stays inside the same episode and does
// not deepen the trough. One episode -> AvgDD = 0.25.
let mut a = AverageDrawdown::new(4).unwrap();
let out = a.batch(&[100.0, 120.0, 90.0, 110.0]);
let expected = (0.25 + (10.0 / 120.0)) / 4.0;
assert_relative_eq!(out[3].unwrap(), expected, epsilon = 1e-12);
assert_relative_eq!(out[3].unwrap(), 0.25, epsilon = 1e-12);
}
#[test]
fn averages_distinct_episodes() {
// [100, 90, 100, 80, 100]: episode 1 troughs at 90 then recovers to 100
// -> depth 0.10; episode 2 troughs at 80 then recovers -> depth 0.20.
// Mean of the two episode depths = 0.15 (distinct from the Pain Index,
// which would weight every under-water bar instead).
let mut a = AverageDrawdown::new(5).unwrap();
let out = a.batch(&[100.0, 90.0, 100.0, 80.0, 100.0]);
assert_relative_eq!(out[4].unwrap(), 0.15, epsilon = 1e-12);
}
#[test]
@@ -1,24 +1,28 @@
//! Awesome Oscillator Histogram.
use std::collections::VecDeque;
use crate::error::{Error, Result};
use crate::indicators::awesome_oscillator::AwesomeOscillator;
use crate::indicators::sma::Sma;
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// "Awesome Oscillator Histogram" — the difference between the Awesome
/// Oscillator and its `sma_period`-bar `SMA`. Positive bars mean `AO` is
/// trending up (bullish acceleration); negative bars mean `AO` is trending
/// down (bearish acceleration).
/// Awesome Oscillator Histogram — the bar-to-bar **momentum** of the Awesome
/// Oscillator over a `lookback` window. This is the value behind the coloured
/// histogram bars in Bill Williams' charts: each bar shows how much `AO` has
/// changed, so positive values mean `AO` is rising (the histogram "greens up")
/// and negative values mean it is falling.
///
/// ```text
/// AO = SMA(median, fast) SMA(median, slow)
/// AOHist = AO SMA(AO, sma_period)
/// AO = SMA(median, fast) SMA(median, slow)
/// AOHist = AO_t AO_{tlookback}
/// ```
///
/// With Williams' default `sma_period = 5`, this collapses to the existing
/// `AcceleratorOscillator` for `fast = 5, slow = 34, sma_period = 5`; for any
/// other parameterisation this is a more flexible variant.
/// This is distinct from the two related indicators Wickra ships: the raw
/// [`AwesomeOscillator`](crate::AwesomeOscillator) is `AO` itself, and the
/// [`AcceleratorOscillator`](crate::AcceleratorOscillator) is `AO SMA(AO, n)`.
/// The histogram instead reports `AO`'s rate of change. The default `lookback`
/// is `1` (the classic one-bar histogram delta).
///
/// # Example
///
@@ -38,17 +42,18 @@ use crate::traits::Indicator;
pub struct AwesomeOscillatorHistogram {
fast_period: usize,
slow_period: usize,
sma_period: usize,
lookback: usize,
ao: AwesomeOscillator,
sma: Sma,
history: VecDeque<f64>,
emitted: bool,
}
impl AwesomeOscillatorHistogram {
/// # Errors
/// - [`Error::PeriodZero`] if any period is zero.
/// - [`Error::InvalidPeriod`] if `fast >= slow`.
pub fn new(fast: usize, slow: usize, sma_period: usize) -> Result<Self> {
if fast == 0 || slow == 0 || sma_period == 0 {
pub fn new(fast: usize, slow: usize, lookback: usize) -> Result<Self> {
if fast == 0 || slow == 0 || lookback == 0 {
return Err(Error::PeriodZero);
}
if fast >= slow {
@@ -59,20 +64,21 @@ impl AwesomeOscillatorHistogram {
Ok(Self {
fast_period: fast,
slow_period: slow,
sma_period,
lookback,
ao: AwesomeOscillator::new(fast, slow)?,
sma: Sma::new(sma_period)?,
history: VecDeque::with_capacity(lookback + 1),
emitted: false,
})
}
/// Bill Williams' Accelerator-equivalent defaults `(5, 34, 5)`.
/// Bill Williams' defaults with a one-bar histogram delta `(5, 34, 1)`.
pub fn classic() -> Self {
Self::new(5, 34, 5).expect("classic Awesome Oscillator Histogram parameters are valid")
Self::new(5, 34, 1).expect("classic Awesome Oscillator Histogram parameters are valid")
}
/// Configured `(fast_period, slow_period, sma_period)`.
/// Configured `(fast_period, slow_period, lookback)`.
pub const fn periods(&self) -> (usize, usize, usize) {
(self.fast_period, self.slow_period, self.sma_period)
(self.fast_period, self.slow_period, self.lookback)
}
}
@@ -82,23 +88,29 @@ impl Indicator for AwesomeOscillatorHistogram {
fn update(&mut self, candle: Candle) -> Option<f64> {
let ao = self.ao.update(candle)?;
let sma = self.sma.update(ao)?;
Some(ao - sma)
self.history.push_back(ao);
if self.history.len() <= self.lookback {
return None;
}
let prev = self.history.pop_front().expect("history is non-empty");
self.emitted = true;
Some(ao - prev)
}
fn reset(&mut self) {
self.ao.reset();
self.sma.reset();
self.history.clear();
self.emitted = false;
}
fn warmup_period(&self) -> usize {
// AO emits at `slow` candles; the SMA then needs `sma_period - 1`
// more AO values to fill its window.
self.slow_period + self.sma_period - 1
// AO first emits at `slow` candles; `lookback` more AO values are then
// needed before `AO_t AO_{tlookback}` can be formed.
self.slow_period + self.lookback
}
fn is_ready(&self) -> bool {
self.sma.is_ready()
self.emitted
}
fn name(&self) -> &'static str {
@@ -119,11 +131,11 @@ mod tests {
#[test]
fn rejects_zero_period() {
assert!(matches!(
AwesomeOscillatorHistogram::new(0, 34, 5),
AwesomeOscillatorHistogram::new(0, 34, 1),
Err(Error::PeriodZero)
));
assert!(matches!(
AwesomeOscillatorHistogram::new(5, 0, 5),
AwesomeOscillatorHistogram::new(5, 0, 1),
Err(Error::PeriodZero)
));
assert!(matches!(
@@ -135,7 +147,7 @@ mod tests {
#[test]
fn rejects_fast_geq_slow() {
assert!(matches!(
AwesomeOscillatorHistogram::new(34, 5, 5),
AwesomeOscillatorHistogram::new(34, 5, 1),
Err(Error::InvalidPeriod { .. })
));
}
@@ -143,15 +155,15 @@ mod tests {
#[test]
fn accessors_and_metadata() {
let hist = AwesomeOscillatorHistogram::classic();
assert_eq!(hist.periods(), (5, 34, 5));
assert_eq!(hist.warmup_period(), 38);
assert_eq!(hist.periods(), (5, 34, 1));
assert_eq!(hist.warmup_period(), 35);
assert_eq!(hist.name(), "AwesomeOscillatorHistogram");
}
#[test]
fn constant_series_converges_to_zero() {
// AO of a flat series is 0; SMA of 0 is 0; difference is 0.
let mut hist = AwesomeOscillatorHistogram::new(3, 5, 3).unwrap();
fn constant_series_yields_zero() {
// AO of a flat series is 0, so its momentum is 0.
let mut hist = AwesomeOscillatorHistogram::new(3, 5, 1).unwrap();
let candles: Vec<Candle> = (0..30).map(|i| candle(42.0, i)).collect();
let out = hist.batch(&candles);
for v in out.iter().skip(hist.warmup_period() - 1).flatten() {
@@ -161,7 +173,7 @@ mod tests {
#[test]
fn warmup_emits_first_value_at_warmup_period() {
let mut hist = AwesomeOscillatorHistogram::new(2, 4, 3).unwrap();
let mut hist = AwesomeOscillatorHistogram::new(2, 4, 2).unwrap();
assert_eq!(hist.warmup_period(), 6);
let candles: Vec<Candle> = (0..8)
.map(|i| candle(10.0 + f64::from(i), i64::from(i)))
@@ -173,6 +185,27 @@ mod tests {
assert!(out[5].is_some());
}
#[test]
fn equals_ao_difference() {
// The histogram must equal AO_t AO_{tlookback} bar for bar.
let candles: Vec<Candle> = (0..60_i64)
.map(|i| candle(100.0 + (i as f64 * 0.3).sin() * 5.0, i))
.collect();
let lookback = 1;
let ao_series = AwesomeOscillator::new(5, 34).unwrap().batch(&candles);
let hist = AwesomeOscillatorHistogram::new(5, 34, lookback)
.unwrap()
.batch(&candles);
for i in 0..candles.len() {
if let Some(h) = hist[i] {
let ao_now = ao_series[i].expect("AO present once histogram emits");
let ao_prev =
ao_series[i - lookback].expect("prior AO present once histogram emits");
assert_relative_eq!(h, ao_now - ao_prev, epsilon = 1e-9);
}
}
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..100_i64)
@@ -1,29 +1,28 @@
//! Intraday Intensity Index (Bostian) — a cumulative volume-weighted close-location line.
//! Intraday Intensity (Bostian) — the per-bar volume-weighted close-location.
use crate::ohlcv::Candle;
use crate::traits::Indicator;
/// Intraday Intensity Index — David Bostian's cumulative line that weights each
/// bar's volume by where the close lands inside the bar's range.
/// Intraday Intensity — David Bostian's per-bar measure that weights each bar's
/// volume by where the close lands inside the bar's range:
///
/// ```text
/// II_t = volume * (2*close high low) / (high low) (0 if high == low)
/// III_t = III_{t1} + II_t
/// II_t = volume * (2*close high low) / (high low) (0 if high == low)
/// ```
///
/// The fraction `(2*close high low) / (high low)` is `+1` when the bar
/// closes on its high, `1` when it closes on its low, and `0` at the midpoint.
/// Scaling it by volume and accumulating produces a running measure of how
/// aggressively the close is being pushed toward the extremes — Bostian's proxy
/// for institutional accumulation (rising line) or distribution (falling line).
/// closes on its high, `1` when it closes on its low, and `0` at the midpoint,
/// so `II_t` is the volume pushed toward the extremes on that single bar —
/// Bostian's proxy for per-bar accumulation (positive) or distribution
/// (negative).
///
/// This is the **cumulative** Intraday Intensity (the original index), not the
/// normalized "Intraday Intensity %" — the latter divides a windowed sum of `II`
/// by a windowed sum of volume and is mathematically identical to
/// [`Cmf`](crate::Cmf), so it is not duplicated here. The level of this line is
/// arbitrary; only its slope and divergences against price matter. A doji whose
/// `high == low` contributes nothing. Each `update` is O(1) and the first bar
/// already emits a value.
/// This emits the **raw per-bar** intensity, which is distinct from the two
/// derived forms Wickra ships separately: the **cumulative** running total is
/// the Accumulation/Distribution Line ([`Adl`](crate::Adl)), and the
/// volume-normalized windowed form ("Intraday Intensity %") is mathematically
/// the Chaikin Money Flow ([`Cmf`](crate::Cmf)). A doji whose `high == low`
/// contributes nothing. Each `update` is O(1) and the first bar already emits a
/// value.
///
/// # Example
///
@@ -41,12 +40,11 @@ use crate::traits::Indicator;
/// ```
#[derive(Debug, Clone, Default)]
pub struct IntradayIntensity {
iii: f64,
last: Option<f64>,
}
impl IntradayIntensity {
/// Construct a new Intraday Intensity Index. The line is parameter-free.
/// Construct a new Intraday Intensity. It is parameter-free.
#[must_use]
pub fn new() -> Self {
Self::default()
@@ -69,13 +67,11 @@ impl Indicator for IntradayIntensity {
} else {
0.0
};
self.iii += ii;
self.last = Some(self.iii);
Some(self.iii)
self.last = Some(ii);
Some(ii)
}
fn reset(&mut self) {
self.iii = 0.0;
self.last = None;
}
@@ -149,11 +145,14 @@ mod tests {
}
#[test]
fn accumulates_across_bars() {
fn each_bar_is_independent() {
// Per-bar (non-cumulative): each output depends only on that bar, so a
// close-on-high +1000 bar is not carried into the next close-on-low bar.
let mut iii = IntradayIntensity::new();
iii.update(candle(110.0, 100.0, 110.0, 1_000.0)); // +1000
let v = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap(); // -400 -> 600
assert_relative_eq!(v, 600.0, epsilon = 1e-9);
let a = iii.update(candle(110.0, 100.0, 110.0, 1_000.0)).unwrap();
let b = iii.update(candle(110.0, 100.0, 100.0, 400.0)).unwrap();
assert_relative_eq!(a, 1_000.0, epsilon = 1e-9);
assert_relative_eq!(b, -400.0, epsilon = 1e-9);
}
#[test]
+1
View File
@@ -9,6 +9,7 @@ That includes:
[Node](https://docs.wickra.org/Quickstart-Node),
[WASM](https://docs.wickra.org/Quickstart-WASM),
[C](https://docs.wickra.org/Quickstart-C),
[C++](https://docs.wickra.org/Quickstart-C),
[C#](https://docs.wickra.org/Quickstart-CSharp),
[Go](https://docs.wickra.org/Quickstart-Go),
[Java](https://docs.wickra.org/Quickstart-Java), and
+4 -4
View File
@@ -142,7 +142,7 @@ build-checked but not run in CI.
| --- | --- | --- |
| `streaming.py` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `python -m examples.python.streaming` |
| `backtest.py` | Basket of indicators over an OHLCV CSV. | `python -m examples.python.backtest <ohlcv.csv>` |
| `live_trading.py` | Live Binance feed → RSI / MACD / Bollinger → signals. | `python -m examples.python.live_trading --symbol BTCUSDT --interval 1m` |
| `live_binance.py` | Live Binance feed → RSI / MACD / Bollinger → signals. | `python -m examples.python.live_binance --symbol BTCUSDT --interval 1m` |
| `multi_timeframe.py` | Resample a 1-minute CSV to coarser timeframes and compare. | `python -m examples.python.multi_timeframe <1m.csv>` |
| `parallel_assets.py` | Process many symbols in parallel — the Rust extension releases the GIL during batch computation. | `python -m examples.python.parallel_assets --assets 200 --bars 5000` |
| `fetch_btcusdt.py` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (urllib + stdlib only). | `python -m examples.python.fetch_btcusdt` |
@@ -150,7 +150,7 @@ build-checked but not run in CI.
| `strategy_macd_adx.py` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `python -m examples.python.strategy_macd_adx` |
| `strategy_bollinger_squeeze.py` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `python -m examples.python.strategy_bollinger_squeeze` |
`live_trading.py` additionally needs `pip install websockets`.
`live_binance.py` additionally needs `pip install websockets`.
## Node.js — `examples/node/`
@@ -167,7 +167,7 @@ cd ../../examples/node && npm install # links wickra + installs `ws`
| `backtest.js` | Basket of indicators over an OHLCV CSV; defaults to the bundled BTCUSDT daily dataset. | `node backtest.js [ohlcv.csv]` |
| `multi_timeframe.js` | Roll a 1-minute CSV up to 5m / 15m / 1h / 4h / 1d and print indicators per timeframe. | `node multi_timeframe.js [path/to/1m.csv]` |
| `parallel_assets.js` | Serial vs `worker_threads` pool over a synthetic panel, with speedup. | `node parallel_assets.js --assets 200 --bars 5000` |
| `live_trading.js` | Live Binance feed → RSI / MACD / Bollinger → signals. | `node live_trading.js --symbol BTCUSDT --interval 1m` |
| `live_binance.js` | Live Binance feed → RSI / MACD / Bollinger → signals. | `node live_binance.js --symbol BTCUSDT --interval 1m` |
| `fetch_btcusdt.js` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (built-in `fetch`, Node 18+). | `node fetch_btcusdt.js` |
| `strategy_rsi_mean_reversion.js` | Hourly BTCUSDT mean-reversion using RSI(14) thresholds, with PnL / Sharpe / max-DD summary. | `node strategy_rsi_mean_reversion.js` |
| `strategy_macd_adx.js` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `node strategy_macd_adx.js` |
@@ -188,7 +188,7 @@ Then serve the repository root (`python -m http.server`, `npx http-server`,
| --- | --- |
| `index.html` | Streams a synthetic price series through six indicators and draws a live `<canvas>` chart. |
| `backtest.html` | Streams a fetched OHLCV CSV through a basket of indicators (SMA, EMA, RSI, MACD, Bollinger, ATR, ADX, OBV) and prints a per-series summary table. |
| `live_trading.html` | Opens a browser-native `WebSocket` to Binance, runs RSI / MACD / Bollinger and flags BUY/SELL candidates. |
| `live_binance.html` | Opens a browser-native `WebSocket` to Binance, runs RSI / MACD / Bollinger and flags BUY/SELL candidates. |
| `multi_timeframe.html` | Fetches a 1-minute CSV, rolls it up to 5m / 15m / 1h / 4h / 1d in-page, prints RSI / MACD hist / ADX per timeframe. |
| `parallel_assets.html` | Spawns a pool of module Workers (each loading its own copy of the WASM module) and reports the speedup over a serial baseline. |
| `strategy_rsi_mean_reversion.html` | Hourly BTCUSDT RSI(14) mean-reversion (long &lt; 30, exit &gt; 70); prints a PnL / Sharpe / max-DD summary table. |
+27
View File
@@ -78,6 +78,33 @@ if(OpenMP_C_FOUND)
target_link_libraries(parallel_assets PRIVATE OpenMP::OpenMP_C)
endif()
# Golden-fixture parity over the whole 514-indicator catalogue, built and run as
# BOTH C (golden_test.c) and C++ (golden_test.cpp #includes the same source) to
# prove the C ABI header is consumable from each language. The fixtures live at
# the repo root; pass the absolute path as the test argument.
get_filename_component(WICKRA_GOLDEN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../testdata/golden" ABSOLUTE)
foreach(gt_pair "golden_test:golden_test.c" "golden_test_cpp:golden_test.cpp")
string(REPLACE ":" ";" gt_list "${gt_pair}")
list(GET gt_list 0 gt_name)
list(GET gt_list 1 gt_src)
add_executable(${gt_name} ${gt_src})
target_include_directories(${gt_name} PRIVATE "${WICKRA_INCLUDE_DIR}")
target_link_libraries(${gt_name} PRIVATE "${WICKRA_LINK_LIB}")
if(UNIX AND NOT APPLE)
target_link_libraries(${gt_name} PRIVATE m)
endif()
if(WIN32)
add_custom_command(TARGET ${gt_name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${gt_name}>")
endif()
add_test(NAME ${gt_name} COMMAND ${gt_name} "${WICKRA_GOLDEN_DIR}")
if(NOT WIN32)
set_tests_properties(${gt_name} PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
endif()
endforeach()
# Network examples built (so they stay compilable) but not run in CI.
add_wickra_example(fetch_btcusdt fetch_btcusdt.c FALSE) # downloads CSVs via curl
add_wickra_example(live_binance live_binance.c FALSE) # polls Binance REST via curl
+340
View File
@@ -0,0 +1,340 @@
"""Generate examples/c/golden_test.c: a value-parity test that replays the shared
golden input through every one of the 514 indicators via the C ABI (wickra.h)
and checks output bit-for-bit against the Rust reference fixtures
g_<Canonical>.csv. The same source compiles under both a C compiler (the C
binding) and a C++ compiler (the C++ binding) wickra.h is `extern "C"`.
Run from repo root: python examples/c/gen_golden_test.py
"""
import json
import os
import re
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
G = os.path.join(ROOT, "testdata", "golden")
HDR = open(os.path.join(ROOT, "bindings", "c", "include", "wickra.h"), encoding="utf-8").read()
# canonical -> C prefix, from the R wrappers (.wk_obj first arg == C symbol prefix).
RSRC = open(os.path.join(ROOT, "bindings", "r", "R", "indicators.R"), encoding="utf-8").read()
PREFIX = {m.group(1): m.group(2)
for m in re.finditer(r"^(\w+) <- function\([^)]*\) \{.*?\.wk_obj\(\"([^\"]+)\"", RSRC, re.S | re.M)}
# wickra_* function signatures (return type, args), multiline-collapsed.
SIG = {}
for m in re.finditer(r"([A-Za-z_][\w ]*\*?)\s*(wickra_\w+)\(([^;]*?)\);", HDR, re.S):
SIG[m.group(2)] = (re.sub(r"\s+", " ", m.group(1)).strip(), re.sub(r"\s+", " ", m.group(3)).strip())
# archetype + n/width per canonical
spec = {}
for e in json.load(open(os.path.join(G, "scalar_manifest.json"))):
spec[e["canonical"]] = {"arch": {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[e["input"]], "params": e["params"]}
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
spec[e["canonical"]] = {"arch": {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[e["input"]], "params": e["params"], "n": e["n"]}
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
for e in ex["deriv"]:
spec[e["canonical"]] = {"arch": "deriv_multi" if "n" in e else "deriv", "params": e["params"], **({"n": e["n"]} if "n" in e else {})}
for fam, a in (("cross", "cross"), ("trade", "trade"), ("trademid", "trademid"), ("ob", "ob")):
for e in ex[fam]:
spec[e["canonical"]] = {"arch": a, "params": e["params"]}
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
spec[e["canonical"]] = {"arch": "profile_" + e["kind"], "params": e["params"], "width": e["width"]}
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
spec[e["canonical"]] = {"arch": "footprint" if e["canonical"] == "Footprint" else "bars_" + e["feed"], "params": e["params"]}
canons = sorted(os.path.basename(f)[2:-4] for f in __import__("glob").glob(os.path.join(G, "g_*.csv")))
BAR_FIELDS = {
"RenkoBars": ["open", "close", "direction"], "KagiBars": ["start", "end", "direction"],
"PointAndFigureBars": ["direction", "high", "low"], "RangeBars": ["open", "close", "direction"],
"ThreeLineBreakBars": ["open", "close", "direction"],
"ImbalanceBars": ["open", "high", "low", "close", "imbalance", "direction"],
"RunBars": ["open", "high", "low", "close", "length", "direction"],
"DollarBars": ["open", "high", "low", "close", "volume", "dollar"],
"TickBars": ["open", "high", "low", "close", "volume"],
"VolumeBars": ["open", "high", "low", "close", "volume"],
"Footprint": ["price", "bid_vol", "ask_vol"],
}
# cbindgen appends '_' to struct fields that collide with C/C++ reserved words.
_C_RESERVED = {"long", "short", "int", "char", "float", "double", "new", "class",
"this", "delete", "register", "auto", "const", "void"}
def c_field(name):
return name + "_" if name in _C_RESERVED else name
def csv_header(canon):
with open(os.path.join(G, "g_" + canon + ".csv"), encoding="utf-8") as f:
return f.readline().strip().split(",")
def out_struct(prefix):
"""The pointer-to-struct out param type of wickra_<prefix>_update, if any."""
_, args = SIG["wickra_" + prefix + "_update"]
m = re.search(r"struct (\w+) \*out", args)
if m:
return "struct " + m.group(1)
m = re.search(r"struct (\w+) \*scalars", args)
if m:
return "struct " + m.group(1)
return None
def ctor_casts(prefix, params):
_, args = SIG["wickra_" + prefix + "_new"]
if not args:
return ""
types = [a.strip().rsplit(" ", 1)[0].strip() for a in args.split(",")]
out = []
for t, v in zip(types, params):
if t in ("uintptr_t", "intptr_t", "size_t"):
out.append(f"(uintptr_t){int(round(v))}")
elif t in ("uint8_t",):
out.append(f"(uint8_t){int(round(v))}")
elif t in ("int32_t",):
out.append(f"(int32_t){int(round(v))}")
elif t in ("int64_t",):
out.append(f"(int64_t){int(round(v))}")
else:
out.append(repr(float(v)))
return ", ".join(out)
def gen_check(canon):
s = spec[canon]
p = PREFIX[canon]
a = s["arch"]
new = f"wickra_{p}_new({ctor_casts(p, s['params'])})"
upd = f"wickra_{p}_update"
L = [f"static int check_{canon}(void) {{",
f" struct {struct_name(p)} *h = {new};",
f' if (!h) {{ printf("FAIL {canon}: new returned NULL\\n"); return 1; }}',
f" double **exp; int rows = read_fixture(\"g_{canon}\", &exp);",
" int fails = 0;",
" for (int i = 0; i < N_INPUT; i++) {",
" double o = IN[i][0], hi = IN[i][1], lo = IN[i][2], c = IN[i][3], v = IN[i][4];",
" (void)o; (void)hi; (void)lo; (void)c; (void)v;",
" double got[128]; int gn = 0;"]
if a in ("scalar_f64", "multi_f64"):
call_args = "h, c"
elif a in ("pairwise", "multi_pairwise"):
call_args = "h, c, o"
elif a in ("scalar_candle", "multi_candle") or a.startswith("profile"):
call_args = "h, o, hi, lo, c, v, (int64_t)i"
elif a == "trade":
call_args = "h, c, v, c >= o, (int64_t)i"
elif a == "trademid":
call_args = "h, c, v, c >= o, (int64_t)i, (hi + lo) / 2.0"
elif a == "ob":
L.append(" double bp[5], bs[5], ap[5], asz[5];")
L.append(" for (int k = 0; k < 5; k++) { double kf = k + 1; bp[k] = c - 0.1*kf; bs[k] = v/kf; ap[k] = c + 0.1*kf; asz[k] = v*0.9/kf; }")
call_args = "h, bp, bs, 5, ap, asz, 5"
elif a == "cross":
L.append(" double chg[5], vol[5]; bool nh[5], nl[5], am[5], ob[5];")
L.append(" for (int j = 0; j < 5; j++) { chg[j] = (c-o)+j; vol[j] = v + j*10.0; nh[j] = (j%2==0); nl[j] = (j%3==0); am[j] = (j%2==0); ob[j] = (j%3==0); }")
call_args = "h, chg, vol, nh, nl, am, ob, 5, (int64_t)i"
elif a in ("deriv", "deriv_multi"):
L.append(" double fr=(c-o)/c*0.01, mp=c, ip=c-0.5, fp=c+1.0, oi=v*10.0, ls=v*0.6, ss=v*0.4, tbv=v*0.55, tsv=v*0.45, ll=hi-c, sl=c-lo;")
call_args = "h, fr, mp, ip, fp, oi, ls, ss, tbv, tsv, ll, sl, (int64_t)i"
elif a == "bars_close":
call_args = "h, c, c, c, c, 1.0, 0"
elif a == "bars_candle4":
call_args = "h, o, hi, lo, c, 1.0, 0"
elif a == "bars_candle5":
call_args = "h, o, hi, lo, c, v, 0"
elif a == "footprint":
call_args = "h, c, v, c >= o, (int64_t)i"
else:
raise SystemExit("arch " + a)
# output handling
if a in ("scalar_f64", "scalar_candle", "pairwise", "trade", "trademid", "ob", "cross", "deriv"):
L.append(f" got[gn++] = {upd}({call_args});")
elif a in ("multi_f64", "multi_candle", "multi_pairwise", "deriv_multi"):
st = out_struct(p)
fields = csv_header(canon)
L.append(f" {st} out;")
L.append(f" if ({upd}({call_args}, &out)) {{")
for f in fields:
L.append(f" got[gn++] = out.{c_field(f)};")
L.append(" } else {")
L.append(f" for (int z = 0; z < {len(fields)}; z++) got[gn++] = NANV;")
L.append(" }")
elif a == "profile_bins":
w = s["width"]
L.append(" double vbuf[256];")
L.append(f" intptr_t k = {upd}({call_args}, vbuf, 256);")
L.append(f" if (k < 0) {{ for (int z = 0; z < {w}; z++) got[gn++] = NANV; }}")
L.append(f" else {{ for (int z = 0; z < {w}; z++) got[gn++] = vbuf[z]; }}")
elif a == "profile_pricebins":
w = s["width"]
st = out_struct(p)
L.append(" double vbuf[256];")
L.append(f" {st} sc;")
L.append(f" intptr_t k = {upd}({call_args}, &sc, vbuf, 256);")
L.append(f" if (k < 0) {{ for (int z = 0; z < {w}; z++) got[gn++] = NANV; }}")
L.append(" else { got[gn++] = sc.price_low; got[gn++] = sc.price_high;")
L.append(f" for (int z = 0; z < {w - 2}; z++) got[gn++] = vbuf[z]; }}")
else: # bars_* / footprint
elem = out_struct(p)
fields = BAR_FIELDS[canon]
cap = 256
L.append(f" {elem} bbuf[{cap}];")
if a == "footprint":
L.append(f" intptr_t k = {upd}({call_args}, bbuf, {cap});")
L.append(" if (k < 0) k = 0;")
else:
L.append(f" uintptr_t k = {upd}({call_args}, bbuf, {cap});")
L.append(" for (uintptr_t b = 0; b < (uintptr_t)k; b++) {")
for f in fields:
L.append(f" got[gn++] = (double)bbuf[b].{f};")
L.append(" }")
L.append(" fails += cmp_row(\"" + canon + "\", i, exp[i], EXPLEN[i], got, gn);")
L.append(" }")
L.append(" free_fixture(exp, rows);")
L.append(f" wickra_{p}_free(h);")
L.append(" return fails;")
L.append("}")
return "\n".join(L)
def struct_name(prefix):
ret, _ = SIG["wickra_" + prefix + "_new"]
m = re.search(r"struct (\w+)", ret)
return m.group(1)
HEADER = r'''/* Generated by gen_golden_test.py. DO NOT EDIT.
*
* Value-parity for the whole 514-indicator catalogue through the Wickra C ABI.
* The same source compiles as C (gcc) and C++ (g++) since wickra.h is extern "C".
* Each indicator replays the shared golden input and is checked bit-for-bit
* against the Rust reference fixtures testdata/golden/g_<Canonical>.csv. */
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "wickra.h"
#define NANV (nan(""))
#define MAXROWS 512
#define MAXCOLS 256
static double IN[MAXROWS][8];
static int N_INPUT = 0;
static int EXPLEN[MAXROWS];
static const char *GDIR = NULL;
static double parse_cell(const char *s) {
if (strcmp(s, "nan") == 0) return NANV;
if (strcmp(s, "inf") == 0) return INFINITY;
if (strcmp(s, "-inf") == 0) return -INFINITY;
return atof(s);
}
static int split_line(char *line, double *out, int max) {
int n = 0;
char *p = line;
while (*p && n < max) {
char *comma = strchr(p, ',');
if (comma) *comma = '\0';
out[n++] = parse_cell(p);
if (!comma) break;
p = comma + 1;
}
return n;
}
static FILE *open_fixture(const char *name) {
char path[1024];
snprintf(path, sizeof(path), "%s/%s.csv", GDIR, name);
return fopen(path, "r");
}
static void load_input(void) {
FILE *f = open_fixture("input");
if (!f) { fprintf(stderr, "cannot open input.csv in %s\n", GDIR); exit(2); }
char line[8192];
int first = 1;
while (fgets(line, sizeof(line), f)) {
line[strcspn(line, "\r\n")] = '\0';
if (first) { first = 0; continue; }
if (line[0] == '\0') continue;
N_INPUT += (split_line(line, IN[N_INPUT], 8) > 0);
}
fclose(f);
}
/* Read a fixture, keeping blank rows (a candle on which no bar closed). */
static int read_fixture(const char *name, double ***outp) {
FILE *f = open_fixture(name);
if (!f) { fprintf(stderr, "cannot open %s.csv\n", name); exit(2); }
double **rows = (double **)malloc(sizeof(double *) * MAXROWS);
int n = 0, first = 1;
char line[8192];
while (fgets(line, sizeof(line), f) && n < MAXROWS) {
line[strcspn(line, "\r\n")] = '\0';
if (first) { first = 0; continue; }
double *vals = (double *)malloc(sizeof(double) * MAXCOLS);
int c = (line[0] == '\0') ? 0 : split_line(line, vals, MAXCOLS);
EXPLEN[n] = c;
rows[n++] = vals;
}
fclose(f);
*outp = rows;
return n;
}
static void free_fixture(double **rows, int n) {
for (int i = 0; i < n; i++) free(rows[i]);
free(rows);
}
static int close_to(double g, double w) {
if (isnan(w)) return isnan(g);
if (isinf(w)) return isinf(g) && ((g > 0) == (w > 0));
double tol = 1e-6 * (fabs(w) > 1.0 ? fabs(w) : 1.0);
return fabs(g - w) <= tol;
}
static int cmp_row(const char *name, int i, const double *want, int wn, const double *got, int gn) {
if (wn != gn) {
printf("FAIL %s row %d: arity %d vs %d\n", name, i, gn, wn);
return 1;
}
for (int k = 0; k < wn; k++) {
if (!close_to(got[k], want[k])) {
printf("FAIL %s row %d col %d: got %g want %g\n", name, i, k, got[k], want[k]);
return 1;
}
}
return 0;
}
'''
MAIN_HEAD = r'''
int main(int argc, char **argv) {
GDIR = (argc > 1) ? argv[1] : "testdata/golden";
load_input();
int total = 0, failed = 0;
'''
out = [HEADER]
for canon in canons:
out.append(gen_check(canon))
out.append(MAIN_HEAD)
for canon in canons:
out.append(f" total++; if (check_{canon}()) failed++;")
out.append(r''' printf("\nC/C++ golden: %d passed, %d failed (of %d)\n", total - failed, failed, total);
return failed ? 1 : 0;
}''')
dest = os.path.join(ROOT, "examples", "c", "golden_test.c")
open(dest, "w", encoding="utf-8").write("\n".join(out) + "\n")
print("generated golden_test.c with", len(canons), "indicators")
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
// C++ build of the 514-indicator golden parity test. wickra.h is `extern "C"`,
// so the generated C runner compiles unchanged under a C++ compiler; this target
// proves the C++ binding consumes the same C ABI and computes identical values.
#include "golden_test.c"
+1 -1
View File
@@ -1,7 +1,7 @@
/* Live BTCUSDT indicator with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/live_binance.rs`,
* `examples/python/live_trading.py` and `examples/node/live_trading.js`. Those
* `examples/python/live_binance.py` and `examples/node/live_binance.js`. Those
* stream Binance over a WebSocket; the C ABI ships only the indicators and no
* socket layer, so this example polls the Binance REST klines endpoint via the
* system `curl` once per interval and feeds each newly *closed* candle into a
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra.examples</groupId>
<artifactId>wickra-examples</artifactId>
<version>0.9.1</version>
<version>0.9.2</version>
<packaging>jar</packaging>
<name>Wickra Java examples</name>
@@ -21,7 +21,7 @@
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.9.1</version>
<version>0.9.2</version>
</dependency>
<!-- Only the network examples (fetch_btcusdt) parse JSON. -->
<dependency>
@@ -1,17 +1,17 @@
// Live trading skeleton for the Wickra Node binding.
// Live Binance feed example for the Wickra Node binding.
//
// Connects to Binance's public WebSocket feed (no API key needed) and runs
// RSI / MACD / Bollinger Bands on the incoming close prices. When RSI crosses
// the common overbought / oversold thresholds *and* the MACD histogram
// confirms the direction *and* price pierces the matching Bollinger band, a
// signal line is printed. No orders are placed. It is the Node counterpart of
// examples/python/live_trading.py.
// examples/python/live_binance.py.
//
// Run it from the repository after building the native binding:
//
// cd bindings/node && npm install && npx napi build --platform --release
// cd ../../examples/node && npm install # pulls `ws` for this example
// node live_trading.js --symbol BTCUSDT --interval 1m
// node live_binance.js --symbol BTCUSDT --interval 1m
//
// Stop it with Ctrl+C.
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.9.1",
"version": "0.9.2",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.9.1",
"wickra-darwin-x64": "0.9.1",
"wickra-linux-arm64-gnu": "0.9.1",
"wickra-linux-x64-gnu": "0.9.1",
"wickra-win32-arm64-msvc": "0.9.1",
"wickra-win32-x64-msvc": "0.9.1"
"wickra-darwin-arm64": "0.9.2",
"wickra-darwin-x64": "0.9.2",
"wickra-linux-arm64-gnu": "0.9.2",
"wickra-linux-x64-gnu": "0.9.2",
"wickra-win32-arm64-msvc": "0.9.2",
"wickra-win32-x64-msvc": "0.9.2"
}
},
"node_modules/wickra": {
@@ -1,4 +1,4 @@
"""Live trading skeleton: stream Binance kline ticks → incremental indicators → signals.
"""Live Binance feed: stream Binance kline ticks → incremental indicators → signals.
This example connects to Binance's public WebSocket feed (no API key needed)
and runs RSI / MACD / Bollinger Bands on the close prices coming in. When the
@@ -7,7 +7,7 @@ confirms the direction, a `Signal` event is printed. No orders are placed.
Run with::
python -m examples.python.live_trading --symbol BTCUSDT --interval 1m
python -m examples.python.live_binance --symbol BTCUSDT --interval 1m
Dependencies::
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -38,7 +38,7 @@ Then open the demo you want at `http://localhost:8000/examples/wasm/<file>`.
| --- | --- |
| `index.html` | The original showcase: streams a synthetic price series through six indicators and draws a live `<canvas>` chart with `SMA`, `EMA`, `RSI`, `MACD`, `BollingerBands` and `ATR` cards. |
| `backtest.html` | Backtest: fetches an OHLCV CSV (default the bundled BTCUSDT daily dataset), streams every candle through a basket of eight indicators, prints a summary table. Mirrors `examples/python/backtest.py`. |
| `live_trading.html` | Browser-native `WebSocket` to Binance kline streams; runs RSI / MACD / Bollinger on the incoming closes and flags BUY/SELL candidates when the three agree. Mirrors `examples/python/live_trading.py`. |
| `live_binance.html` | Browser-native `WebSocket` to Binance kline streams; runs RSI / MACD / Bollinger on the incoming closes and flags BUY/SELL candidates when the three agree. Mirrors `examples/python/live_binance.py`. |
| `multi_timeframe.html` | Fetches a 1-minute CSV, rolls it up in-page to 5m / 15m / 1h / 4h / 1d buckets and prints RSI / MACD-histogram / ADX per timeframe. Mirrors `examples/python/multi_timeframe.py`. |
| `parallel_assets.html` | Synthetic `(assets, bars)` panel, serial baseline on the main thread vs. a pool of module Workers each loading its own copy of the WASM module. Mirrors `examples/python/parallel_assets.py`. |
| `parallel_worker.js` | Module worker used by `parallel_assets.html` (not loaded directly). |
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Wickra WASM — live trading</title>
<title>Wickra WASM — live Binance feed</title>
<style>
body { font-family: ui-sans-serif, system-ui, sans-serif; max-width: 980px; margin: 2rem auto; padding: 0 1rem; color: #1d1d1d; }
h1 { margin-bottom: .25rem; }
@@ -22,14 +22,14 @@
</style>
</head>
<body>
<h1>Wickra WASM — live trading</h1>
<h1>Wickra WASM — live Binance feed</h1>
<p class="meta">
Connects to Binance's public kline WebSocket from the browser, streams
every tick through RSI(14), MACD(12,26,9) and Bollinger(20, 2.0) via
the WebAssembly bindings, and flags BUY/SELL candidates when all
three indicators agree. No orders are placed. Mirrors
<code>examples/python/live_trading.py</code> and
<code>examples/node/live_trading.js</code>.
<code>examples/python/live_binance.py</code> and
<code>examples/node/live_binance.js</code>.
</p>
<p>
+81
View File
@@ -0,0 +1,81 @@
ad_oscillator
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
-8.707352694334602
-9.55383489048154
-10.14583263837026
-7.716297064947573
-5.415123860960578
-2.4582949833648513
1.089975121991027
4.696280572705112
8.134366418062353
11.139886165841459
13.441608928194814
14.831171905898678
15.682268342180382
15.9374382006224
15.375320862892893
11.844484625612038
8.060314754372618
4.217673873654952
0.5154190742606168
-2.897345501480764
-5.727866727021226
-7.735401540219346
-8.752721804087948
-9.182159376975637
-6.2632569344388855
-3.1449666933596028
-0.30319497786799054
2.9959756573154515
6.422574509168438
9.675090799346341
12.465531880413298
14.538632000083673
15.690164238983918
15.79822259186901
15.242096487702092
14.78004441080553
11.364799581360547
7.9100904036895905
4.222306132949534
0.5339213340415121
-2.8476220551432476
-5.631827518096841
-7.747932992047389
-9.797710430835835
-11.339764291930237
-9.268768456321464
-6.970170355782635
-3.9039149984445523
-0.24018851807128883
3.553124481651839
7.187932205193405
10.357360760324106
12.796107189912675
14.304138194947562
14.76455851338833
14.345553629020067
13.857179803145186
9.526827762286814
5.950187351572687
2.4261734293132875
-1.0908438344866767
-4.283755254915981
-6.864403056782976
-8.599321150467368
-9.348752590786908
-9.97346349734272
-7.574008310030209
1 ad_oscillator
2 nan
3 nan
4 nan
5 nan
6 nan
7 nan
8 nan
9 nan
10 nan
11 nan
12 nan
13 nan
14 nan
15 -8.707352694334602
16 -9.55383489048154
17 -10.14583263837026
18 -7.716297064947573
19 -5.415123860960578
20 -2.4582949833648513
21 1.089975121991027
22 4.696280572705112
23 8.134366418062353
24 11.139886165841459
25 13.441608928194814
26 14.831171905898678
27 15.682268342180382
28 15.9374382006224
29 15.375320862892893
30 11.844484625612038
31 8.060314754372618
32 4.217673873654952
33 0.5154190742606168
34 -2.897345501480764
35 -5.727866727021226
36 -7.735401540219346
37 -8.752721804087948
38 -9.182159376975637
39 -6.2632569344388855
40 -3.1449666933596028
41 -0.30319497786799054
42 2.9959756573154515
43 6.422574509168438
44 9.675090799346341
45 12.465531880413298
46 14.538632000083673
47 15.690164238983918
48 15.79822259186901
49 15.242096487702092
50 14.78004441080553
51 11.364799581360547
52 7.9100904036895905
53 4.222306132949534
54 0.5339213340415121
55 -2.8476220551432476
56 -5.631827518096841
57 -7.747932992047389
58 -9.797710430835835
59 -11.339764291930237
60 -9.268768456321464
61 -6.970170355782635
62 -3.9039149984445523
63 -0.24018851807128883
64 3.553124481651839
65 7.187932205193405
66 10.357360760324106
67 12.796107189912675
68 14.304138194947562
69 14.76455851338833
70 14.345553629020067
71 13.857179803145186
72 9.526827762286814
73 5.950187351572687
74 2.4261734293132875
75 -1.0908438344866767
76 -4.283755254915981
77 -6.864403056782976
78 -8.599321150467368
79 -9.348752590786908
80 -9.97346349734272
81 -7.574008310030209
+81
View File
@@ -0,0 +1,81 @@
average_drawdown
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
nan
0.1331735000951348
0.1331735000951348
0.1331735000951348
0.1331735000951348
0.1331735000951348
0.1331735000951348
0.1331735000951348
0.12848591667244894
0.11764684482941928
0.05343916178664985
0.04921355423392666
0.04636884595497022
0.04514688029877185
0.045815749429085506
0.048602879867116276
0.10712469987165406
0.11766221882444922
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.12182138508985806
0.11714256410361094
0.106819842069383
0.048495266448839086
0.04469198482259159
0.04221729767595633
0.04129261916801016
0.04214212542806673
0.04493167704509762
0.09931745531053977
0.10872826720736029
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.11220718102346297
0.10753903894104662
0.09766728005121854
0.04432515342740658
0.0408846402070768
0.03872168033510641
0.038038413171863426
0.03902283524154844
0.041793273597892346
0.09261212521030097
0.10105646093429439
0.10395200934035767
0.10395200934035767
1 average_drawdown
2 nan
3 nan
4 nan
5 nan
6 nan
7 nan
8 nan
9 nan
10 nan
11 nan
12 nan
13 nan
14 nan
15 nan
16 nan
17 nan
18 nan
19 nan
20 nan
21 0.1331735000951348
22 0.1331735000951348
23 0.1331735000951348
24 0.1331735000951348
25 0.1331735000951348
26 0.1331735000951348
27 0.1331735000951348
28 0.12848591667244894
29 0.11764684482941928
30 0.05343916178664985
31 0.04921355423392666
32 0.04636884595497022
33 0.04514688029877185
34 0.045815749429085506
35 0.048602879867116276
36 0.10712469987165406
37 0.11766221882444922
38 0.12182138508985806
39 0.12182138508985806
40 0.12182138508985806
41 0.12182138508985806
42 0.12182138508985806
43 0.12182138508985806
44 0.12182138508985806
45 0.12182138508985806
46 0.12182138508985806
47 0.12182138508985806
48 0.12182138508985806
49 0.11714256410361094
50 0.106819842069383
51 0.048495266448839086
52 0.04469198482259159
53 0.04221729767595633
54 0.04129261916801016
55 0.04214212542806673
56 0.04493167704509762
57 0.09931745531053977
58 0.10872826720736029
59 0.11220718102346297
60 0.11220718102346297
61 0.11220718102346297
62 0.11220718102346297
63 0.11220718102346297
64 0.11220718102346297
65 0.11220718102346297
66 0.11220718102346297
67 0.11220718102346297
68 0.11220718102346297
69 0.11220718102346297
70 0.10753903894104662
71 0.09766728005121854
72 0.04432515342740658
73 0.0408846402070768
74 0.03872168033510641
75 0.038038413171863426
76 0.03902283524154844
77 0.041793273597892346
78 0.09261212521030097
79 0.10105646093429439
80 0.10395200934035767
81 0.10395200934035767
+81
View File
@@ -0,0 +1,81 @@
awesome_oscillator_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
-2.5465708339042976
-2.3455680907694614
-1.9183114349650765
-1.2944880271234354
-0.5275159653568977
0.30966275885398886
1.1321827319644342
1.853304497311072
2.3954107601774837
2.7001719302630107
2.736181583567415
2.502786052996541
2.0295153145464155
1.371319641160568
0.600557921537316
-0.20278694033696354
-0.9609784199580531
-1.6069335072552065
-2.0900573641307005
-2.379022914620819
-2.4616730624015304
-2.3427633455721946
-2.040565227680645
-1.5833610703260206
-1.0066024302738725
-0.3510627241916069
0.3381737686129185
1.0124404584062603
1.6206771076918614
2.1117457548869396
2.438216711865607
2.5614779053997694
2.4574975201079496
2.122158747547033
1.5749095562668458
0.8595979048638469
0.04178796738165147
-0.7975047848210863
-1.5710000605662628
-2.1959628533200544
-2.604933699888676
-2.754602894965899
-2.631338600597104
-2.25246876630203
-1.6631866243396303
-0.9297348570175075
1 awesome_oscillator_histogram
2 nan
3 nan
4 nan
5 nan
6 nan
7 nan
8 nan
9 nan
10 nan
11 nan
12 nan
13 nan
14 nan
15 nan
16 nan
17 nan
18 nan
19 nan
20 nan
21 nan
22 nan
23 nan
24 nan
25 nan
26 nan
27 nan
28 nan
29 nan
30 nan
31 nan
32 nan
33 nan
34 nan
35 nan
36 -2.5465708339042976
37 -2.3455680907694614
38 -1.9183114349650765
39 -1.2944880271234354
40 -0.5275159653568977
41 0.30966275885398886
42 1.1321827319644342
43 1.853304497311072
44 2.3954107601774837
45 2.7001719302630107
46 2.736181583567415
47 2.502786052996541
48 2.0295153145464155
49 1.371319641160568
50 0.600557921537316
51 -0.20278694033696354
52 -0.9609784199580531
53 -1.6069335072552065
54 -2.0900573641307005
55 -2.379022914620819
56 -2.4616730624015304
57 -2.3427633455721946
58 -2.040565227680645
59 -1.5833610703260206
60 -1.0066024302738725
61 -0.3510627241916069
62 0.3381737686129185
63 1.0124404584062603
64 1.6206771076918614
65 2.1117457548869396
66 2.438216711865607
67 2.5614779053997694
68 2.4574975201079496
69 2.122158747547033
70 1.5749095562668458
71 0.8595979048638469
72 0.04178796738165147
73 -0.7975047848210863
74 -1.5710000605662628
75 -2.1959628533200544
76 -2.604933699888676
77 -2.754602894965899
78 -2.631338600597104
79 -2.25246876630203
80 -1.6631866243396303
81 -0.9297348570175075
+91
View File
@@ -0,0 +1,91 @@
[
{
"canonical": "RenkoBars",
"native": "RenkoBars",
"params": [
2.0
],
"feed": "close"
},
{
"canonical": "KagiBars",
"native": "KagiBars",
"params": [
2.0
],
"feed": "close"
},
{
"canonical": "PointAndFigureBars",
"native": "PointAndFigureBars",
"params": [
2.0,
3
],
"feed": "close"
},
{
"canonical": "RangeBars",
"native": "RangeBars",
"params": [
2.0
],
"feed": "close"
},
{
"canonical": "ThreeLineBreakBars",
"native": "ThreeLineBreakBars",
"params": [
3
],
"feed": "close"
},
{
"canonical": "ImbalanceBars",
"native": "ImbalanceBars",
"params": [
5.0
],
"feed": "candle4"
},
{
"canonical": "RunBars",
"native": "RunBars",
"params": [
3
],
"feed": "candle4"
},
{
"canonical": "DollarBars",
"native": "DollarBars",
"params": [
50000.0
],
"feed": "candle5"
},
{
"canonical": "TickBars",
"native": "TickBars",
"params": [
2
],
"feed": "candle5"
},
{
"canonical": "VolumeBars",
"native": "VolumeBars",
"params": [
500.0
],
"feed": "candle5"
},
{
"canonical": "Footprint",
"native": "Footprint",
"params": [
1.0
],
"feed": "trade"
}
]
+364
View File
@@ -0,0 +1,364 @@
{
"deriv": [
{
"canonical": "CalendarSpread",
"native": "CalendarSpread",
"params": [],
"args": [
"futures_price",
"mark_price"
]
},
{
"canonical": "EstimatedLeverageRatio",
"native": "EstimatedLeverageRatio",
"params": [],
"args": [
"open_interest",
"long_size",
"short_size"
]
},
{
"canonical": "FundingBasis",
"native": "FundingBasis",
"params": [],
"args": [
"mark_price",
"index_price"
]
},
{
"canonical": "FundingImpliedApr",
"native": "FundingImpliedApr",
"params": [
1095.0
],
"args": [
"funding_rate"
]
},
{
"canonical": "FundingRate",
"native": "FundingRate",
"params": [],
"args": [
"funding_rate"
]
},
{
"canonical": "FundingRateMean",
"native": "FundingRateMean",
"params": [
20
],
"args": [
"funding_rate"
]
},
{
"canonical": "FundingRateZScore",
"native": "FundingRateZScore",
"params": [
20
],
"args": [
"funding_rate"
]
},
{
"canonical": "LongShortRatio",
"native": "LongShortRatio",
"params": [],
"args": [
"long_size",
"short_size"
]
},
{
"canonical": "OpenInterestDelta",
"native": "OpenInterestDelta",
"params": [],
"args": [
"open_interest"
]
},
{
"canonical": "OIPriceDivergence",
"native": "OIPriceDivergence",
"params": [
20
],
"args": [
"open_interest",
"mark_price"
]
},
{
"canonical": "OiToVolumeRatio",
"native": "OiToVolumeRatio",
"params": [],
"args": [
"open_interest",
"taker_buy_volume",
"taker_sell_volume"
]
},
{
"canonical": "OIWeighted",
"native": "OIWeighted",
"params": [],
"args": [
"mark_price",
"open_interest"
]
},
{
"canonical": "OpenInterestMomentum",
"native": "OpenInterestMomentum",
"params": [
10
],
"args": [
"open_interest"
]
},
{
"canonical": "PerpetualPremiumIndex",
"native": "PerpetualPremiumIndex",
"params": [],
"args": [
"mark_price",
"index_price"
]
},
{
"canonical": "TakerBuySellRatio",
"native": "TakerBuySellRatio",
"params": [],
"args": [
"taker_buy_volume",
"taker_sell_volume"
]
},
{
"canonical": "TermStructureBasis",
"native": "TermStructureBasis",
"params": [],
"args": [
"futures_price",
"index_price"
]
},
{
"canonical": "LiquidationFeatures",
"native": "LiquidationFeatures",
"params": [],
"args": [
"long_liquidation",
"short_liquidation"
],
"n": 5
}
],
"cross": [
{
"canonical": "AbsoluteBreadthIndex",
"native": "AbsoluteBreadthIndex",
"params": []
},
{
"canonical": "AdvanceDecline",
"native": "AdvanceDecline",
"params": []
},
{
"canonical": "AdvanceDeclineRatio",
"native": "AdvanceDeclineRatio",
"params": []
},
{
"canonical": "AdVolumeLine",
"native": "AdVolumeLine",
"params": []
},
{
"canonical": "BreadthThrust",
"native": "BreadthThrust",
"params": [
10
]
},
{
"canonical": "BullishPercentIndex",
"native": "BullishPercentIndex",
"params": [],
"extra": "on_buy_signal"
},
{
"canonical": "CumulativeVolumeIndex",
"native": "CumulativeVolumeIndex",
"params": []
},
{
"canonical": "HighLowIndex",
"native": "HighLowIndex",
"params": [
10
]
},
{
"canonical": "McClellanOscillator",
"native": "McClellanOscillator",
"params": []
},
{
"canonical": "McClellanSummationIndex",
"native": "McClellanSummationIndex",
"params": []
},
{
"canonical": "NewHighsNewLows",
"native": "NewHighsNewLows",
"params": []
},
{
"canonical": "PercentAboveMa",
"native": "PercentAboveMa",
"params": [],
"extra": "above_ma"
},
{
"canonical": "TickIndex",
"native": "TickIndex",
"params": []
},
{
"canonical": "Trin",
"native": "Trin",
"params": []
},
{
"canonical": "UpDownVolumeRatio",
"native": "UpDownVolumeRatio",
"params": []
}
],
"trade": [
{
"canonical": "AmihudIlliquidity",
"native": "AmihudIlliquidity",
"params": [
20
]
},
{
"canonical": "CumulativeVolumeDelta",
"native": "CumulativeVolumeDelta",
"params": []
},
{
"canonical": "Pin",
"native": "Pin",
"params": [
20
]
},
{
"canonical": "RollMeasure",
"native": "RollMeasure",
"params": [
20
]
},
{
"canonical": "SignedVolume",
"native": "SignedVolume",
"params": []
},
{
"canonical": "TradeImbalance",
"native": "TradeImbalance",
"params": [
20
]
},
{
"canonical": "TradeSignAutocorrelation",
"native": "TradeSignAutocorrelation",
"params": [
20
]
},
{
"canonical": "Vpin",
"native": "Vpin",
"params": [
5000.0,
10
]
}
],
"trademid": [
{
"canonical": "KylesLambda",
"native": "KylesLambda",
"params": [
20
]
},
{
"canonical": "RealizedSpread",
"native": "RealizedSpread",
"params": [
20
]
},
{
"canonical": "EffectiveSpread",
"native": "EffectiveSpread",
"params": []
}
],
"ob": [
{
"canonical": "DepthSlope",
"native": "DepthSlope",
"params": []
},
{
"canonical": "Microprice",
"native": "Microprice",
"params": []
},
{
"canonical": "OrderBookImbalanceFull",
"native": "OrderBookImbalanceFull",
"params": []
},
{
"canonical": "OrderBookImbalanceTop1",
"native": "OrderBookImbalanceTop1",
"params": []
},
{
"canonical": "OrderBookImbalanceTopN",
"native": "OrderBookImbalanceTopN",
"params": [
5
]
},
{
"canonical": "OrderFlowImbalance",
"native": "OrderFlowImbalance",
"params": [
20
]
},
{
"canonical": "QuotedSpread",
"native": "QuotedSpread",
"params": []
}
]
}
+81
View File
@@ -0,0 +1,81 @@
AbandonedBaby
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1 AbandonedBaby
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
10 0
11 0
12 0
13 0
14 0
15 0
16 0
17 0
18 0
19 0
20 0
21 0
22 0
23 0
24 0
25 0
26 0
27 0
28 0
29 0
30 0
31 0
32 0
33 0
34 0
35 0
36 0
37 0
38 0
39 0
40 0
41 0
42 0
43 0
44 0
45 0
46 0
47 0
48 0
49 0
50 0
51 0
52 0
53 0
54 0
55 0
56 0
57 0
58 0
59 0
60 0
61 0
62 0
63 0
64 0
65 0
66 0
67 0
68 0
69 0
70 0
71 0
72 0
73 0
74 0
75 0
76 0
77 0
78 0
79 0
80 0
81 0
+81
View File
@@ -0,0 +1,81 @@
Abcd
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-1
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-1
0
0
0
0
0
0
0
1 Abcd
2 0
3 0
4 0
5 0
6 0
7 0
8 0
9 0
10 0
11 0
12 0
13 0
14 0
15 0
16 0
17 0
18 0
19 0
20 0
21 0
22 0
23 0
24 0
25 0
26 0
27 0
28 0
29 0
30 0
31 0
32 0
33 0
34 0
35 0
36 0
37 0
38 0
39 0
40 0
41 0
42 0
43 0
44 0
45 0
46 0
47 0
48 0
49 0
50 0
51 0
52 0
53 -1
54 0
55 0
56 0
57 0
58 0
59 0
60 0
61 0
62 0
63 0
64 0
65 0
66 0
67 0
68 0
69 0
70 0
71 0
72 0
73 0
74 -1
75 0
76 0
77 0
78 0
79 0
80 0
81 0
+81
View File
@@ -0,0 +1,81 @@
AbsoluteBreadthIndex
5
5
5
5
3
3
3
3
3
3
5
5
5
5
5
5
3
3
3
3
3
3
5
5
5
5
5
5
5
3
3
3
3
3
3
5
5
5
5
5
5
3
3
3
3
3
3
3
5
5
5
5
5
5
3
3
3
3
3
3
5
5
5
5
5
5
3
3
3
3
3
3
3
5
5
5
5
5
5
3
1 AbsoluteBreadthIndex
2 5
3 5
4 5
5 5
6 3
7 3
8 3
9 3
10 3
11 3
12 5
13 5
14 5
15 5
16 5
17 5
18 3
19 3
20 3
21 3
22 3
23 3
24 5
25 5
26 5
27 5
28 5
29 5
30 5
31 3
32 3
33 3
34 3
35 3
36 3
37 5
38 5
39 5
40 5
41 5
42 5
43 3
44 3
45 3
46 3
47 3
48 3
49 3
50 5
51 5
52 5
53 5
54 5
55 5
56 3
57 3
58 3
59 3
60 3
61 3
62 5
63 5
64 5
65 5
66 5
67 5
68 3
69 3
70 3
71 3
72 3
73 3
74 3
75 5
76 5
77 5
78 5
79 5
80 5
81 3

Some files were not shown because too many files have changed in this diff Show More