Compare commits

...

35 Commits

Author SHA1 Message Date
kingchenc 96cd9b3f02 release: bump 0.9.6 -> 0.9.7 (#338) 2026-06-21 16:07:53 +02:00
dependabot[bot] 98bb57c868 deps(actions): bump the github-actions group with 4 updates (#336)
Bumps the github-actions group with 4 updates: [actions/setup-python](https://github.com/actions/setup-python), [actions/upload-artifact](https://github.com/actions/upload-artifact), [taiki-e/install-action](https://github.com/taiki-e/install-action) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/setup-python` from 5.6.0 to 6.2.0
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.6.0...a309ff8b426b58ec0e2a45f0f869d46889d02405)

Updates `actions/upload-artifact` from 4.6.2 to 7.0.1
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4.6.2...043fb46d1a93c77aae656e7c1c64a875d1fc6a0a)

Updates `taiki-e/install-action` from 2.81.6 to 2.81.10
- [Release notes](https://github.com/taiki-e/install-action/releases)
- [Changelog](https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/taiki-e/install-action/compare/59012be0884e296ca2da49b530610e72c49039ad...7a79fe8c3a13344501c80d99cae481c1c9085912)

Updates `codecov/codecov-action` from 6.0.1 to 7.0.0
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/e79a6962e0d4c0c17b229090214935d2e33f8354...fb8b3582c8e4def4969c97caa2f19720cb33a72f)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: taiki-e/install-action
  dependency-version: 2.81.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: codecov/codecov-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 15:30:13 +02:00
dependabot[bot] be05eb26d5 deps(nuget): Bump the nuget group with 3 updates (#337)
Bumps Microsoft.NET.Test.Sdk from 17.11.1 to 18.6.0
Bumps xunit from 2.9.2 to 2.9.3
Bumps xunit.runner.visualstudio from 2.8.2 to 3.1.5

---
updated-dependencies:
- dependency-name: Microsoft.NET.Test.Sdk
  dependency-version: 18.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: nuget
- dependency-name: xunit
  dependency-version: 2.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nuget
- dependency-name: xunit.runner.visualstudio
  dependency-version: 3.1.5
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: nuget
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 15:30:10 +02:00
dependabot[bot] af59e6eec0 deps(maven): bump org.wickra:wickra (#333)
Bumps the maven group with 1 update in the /bindings/java/benchmarks directory: [org.wickra:wickra](https://github.com/wickra-lib/wickra).


Updates `org.wickra:wickra` from 0.8.2 to 0.8.8
- [Release notes](https://github.com/wickra-lib/wickra/releases)
- [Changelog](https://github.com/wickra-lib/wickra/blob/main/CHANGELOG.md)
- [Commits](https://github.com/wickra-lib/wickra/compare/v0.8.2...v0.8.8)

---
updated-dependencies:
- dependency-name: org.wickra:wickra
  dependency-version: 0.8.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: maven
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-21 15:30:07 +02:00
kingchenc 96ae63799d fix(r): credit kingchenc as R package author/maintainer (#332)
Every other binding (Python, Node, Rust core) credits kingchenc; the R
DESCRIPTION was an outlier with "Wickra contributors" as the sole
aut/cre, introduced when the binding was added (#230). Use kingchenc to
match, regenerate the package .Rd, and align the LICENSE copyright holder
with the root LICENSE-MIT ("kingchenc and the Wickra contributors").
2026-06-18 04:49:24 +02:00
kingchenc d5f5e14dda release: bump 0.9.5 -> 0.9.6 (#331)
Documentation release for the R binding. The library API and every indicator are
unchanged from `0.9.5`; only the R package's help pages change.

### What's in 0.9.6
- **R package documentation** (landed in #330): the twelve undocumented data-layer
  exports now have full man pages, the stale `AwesomeOscillatorHistogram` codoc is
  fixed, and a broken `push()` example is corrected — clearing the two `R CMD check`
  warnings r-universe reported for `0.9.5`. CI now runs `R CMD check` so doc drift
  fails the PR instead of reaching r-universe.

Pure version-string bump on top — `bump_version.py` touched 19 files. `cargo fmt`
clean.

Tag/publish waits for explicit GO (irreversible publish to crates.io / PyPI / npm
/ NuGet / Maven / Go / r-universe).
2026-06-18 02:10:03 +02:00
kingchenc 7e5d394f2a fix(r): document the data-layer exports + add an R CMD check gate (#330)
Clears the R CMD check **WARNING**s r-universe surfaced when it finally built
0.9.5 (it was stuck on 0.9.2, before the data layer existed): 11/13 platform
builds reported the same two warnings.

### Warnings fixed
1. **Undocumented code objects** — the data layer added in 0.9.3 (`BinanceFeed`,
   `CandleReader`, `Resampler`, `TickAggregator`, `fetch_binance_klines` and the
   generics `name` / `is_ready` / `warmup_period` / `push` / `read`). The roxygen
   blocks existed in `methods.R`, but the `man/*.Rd` were never regenerated, and
   the constructors had only a title.
2. **Codoc mismatch** — `AwesomeOscillatorHistogram.Rd` still documented
   `sma_period` after the argument was renamed to `lookback`.

### Changes
- Full `@param`/`@return` roxygen on the seven data-layer constructors.
- Regenerated `man/*.Rd` + `NAMESPACE` with roxygen2: 13 new pages, the stale
  AwesomeOscillator usage refreshed, and `flush` registered on `base::flush`.
- Fixed the `push()` example (`TickAggregator(1000)` was missing the required
  `gap_fill`) — it only ran once `push()` got a generated `.Rd`.
- **New CI gate:** a ubuntu-only `R CMD check` in the `r` job that fails on doc
  WARNING/ERROR, so stale docs fail the PR instead of reaching r-universe.

Verified locally with `R CMD check` (R 4.6.0 + Rtools45): *missing documentation
entries* and *Rd \usage sections* now **OK**, examples **OK**, tests **OK**. This
PR's own CI exercises the new gate.

Follow-up: a separate `0.9.6` release ships these doc fixes to r-universe.
2026-06-18 01:38:44 +02:00
kingchenc ff268500ef release: bump 0.9.4 -> 0.9.5 (#329)
Maintenance release. The library API and every indicator are unchanged from
`0.9.4`; the only change that ships to users is the R package's build script.

### What's in 0.9.5
- **R package: retry the C ABI download** (`bindings/r/configure[.win]`). A freshly
  cut release can briefly 404 while assets propagate; the download is now retried
  with a ~2 min backoff instead of failing with `cannot open URL … 404`. Landed in
  #328; ships to r-universe / source installs with this release.
- The rest of #328 — CI/release-pipeline hardening (R/NuGet dependency caching,
  job timeouts 20→30 / release 45, network-install retries, wasm-publish cache) —
  is infrastructure and does not affect the published artifacts, but makes this
  release's own pipeline more robust.

Pure version-string bump on top — `bump_version.py` touched 19 files (Cargo +
Lock, pyproject, node package.json/locks + 6 platform stubs, pom + csproj +
DESCRIPTION, SECURITY, CHANGELOG `[0.9.5]` + compare URLs). `cargo fmt` clean.

Tag/publish waits for explicit GO (irreversible publish to crates.io / PyPI /
npm / NuGet / Maven / Go / r-universe).
2026-06-17 23:55:18 +02:00
kingchenc 929fc17127 ci: harden cache, timeouts and retries across CI and release (#328)
Hardens both workflows after the R-on-ubuntu job repeatedly hit the 20-minute
job cap and was cancelled (no R-package cache + no retry + a slow source build).
Each item below maps to the requested checklist.

### CI (`ci.yml`)
- **Timeouts 20 → 30 min** on every job (backstop only; real jobs finish well under).
- **R dependencies cached**: replace the manual `install.packages(testthat/knitr)`
  with `r-lib/actions/setup-r-dependencies` — restores a cached R library and pulls
  **RSPM binaries** instead of compiling from source (the slow/flaky path that blew
  the cap). This is the actual root-cause fix.
- **NuGet cache** for the C# job (`~/.nuget/packages`, keyed on the project files).
- **Retry** the network installs that had none — `npm ci`, `dotnet test`,
  `mvn install` — via `nick-fields/retry` (2–3 attempts, backoff). On top of the
  existing env-level retries (`CARGO_NET_RETRY`, `npm_config_fetch_retries`,
  `PIP_RETRIES`) and the setup-* CDN-flake retries.
- **Go stays `cache: false`** on purpose: the module has no `go.sum` / external
  deps, so there is nothing to cache (enabling it would only warn).

### Release (`release.yml`)
- **Per-job timeouts** added (there were none — only GitHub's 6h default): **45 min**,
  higher than CI's 30 because the wheel/build jobs compile from source incl.
  **vendored OpenSSL** and must not be killed mid-build.
- **wasm-publish** gets a `Swatinem/rust-cache` like the other Rust-build jobs.
- **Retry** the no-retry network installs (`npm ci` ×2, `dotnet pack`). The actual
  publish/deploy steps are left alone — they are already idempotent
  (skip-existing / skip-duplicate), so re-running the job is the safe recovery.

### R binding download (`bindings/r/configure[.win]`)
- A freshly cut release can 404 for 1–2 min while assets propagate, which broke
  the C ABI download (`cannot open URL … 404`). Add a `wickra_download` retry
  helper (6 × 20s ≈ 2 min backoff) for both the release-asset and wasm-source
  downloads. Note: the CI R job builds the C ABI **locally** (`WICKRA_*_DIR`), so
  it never downloads — this fix covers the real-world r-universe / end-user build.

This PR's own CI exercises the CI changes (the reworked R job, caches, retries,
timeouts) before merge; the release-only changes are validated on the next tag.
2026-06-17 23:42:06 +02:00
kingchenc 41d5a7dd25 fix: build Linux Python wheels with vendored OpenSSL (manylinux + musllinux) (#327)
Fixes the Linux Python wheel build that broke the `0.9.3` release (and would
have broken `0.9.4`), and adds a CI guard so it cannot regress silently.

### Root cause
The `live-binance` data layer links `native-tls` -> `openssl-sys`, which needs
OpenSSL at build time. Neither wheel container provides it:
- **manylinux** ships no OpenSSL headers, and
- **musllinux** cross-compiles against a musl sysroot that has no OpenSSL at all,
  so installing a host package (`yum`/`apk`) cannot reach the cross target.

The 3-OS Python CI jobs build natively on the runner, which already has system
OpenSSL, so CI stayed green while the release container build failed.

### Fix
- New opt-in **`vendored-tls`** feature on `wickra-data` and the Python binding:
  enables `native-tls/vendored`, compiling OpenSSL from source and linking it
  statically. No system OpenSSL needed on either libc. No-op on macOS/Windows
  (Security.framework / SChannel — `openssl-sys` is never in the graph there).
- `release.yml` builds the Linux wheels with `--features vendored-tls` (replaces
  the manylinux-only `before-script-linux` header install, which could not fix
  the musllinux cross build).
- CI gains a **`manylinux` + `musllinux` container build-smoke** matrix job, so
  both container builds run on every PR. This PR's own CI is the proof the fix
  works before any release re-attempt.

### Notes
- No version bump: `0.9.4` published nowhere (the release run was cancelled
  before any publish job ran), so this lands on `0.9.4` and the tag is re-pointed
  at the fixed commit.
- Adds checks to `ci.yml` (the smoke job is now a 2-entry matrix).
2026-06-17 22:26:21 +02:00
kingchenc e595ea8bfe release: bump 0.9.3 -> 0.9.4 (#326)
Packaging fix for the `0.9.3` data layer.

`0.9.3` published to crates.io, Maven Central, NuGet, npm, and the Go mirror, but
the **Linux Python wheels failed to build**, so `Publish to PyPI` was skipped and
no GitHub Release was attached. Root cause: the `live-binance` data layer links
`native-tls` -> `openssl-sys`, and the `manylinux` / `musllinux` wheel-build
containers do not ship the system OpenSSL headers. The native macOS and Windows
wheels were unaffected (system TLS), which is why CI — running Python natively on
the runners, where OpenSSL is present — stayed green.

### Changes
- **`ci`**: install the OpenSSL headers inside the wheel container via
  maturin-action's `before-script-linux` (`openssl-devel` on `manylinux`,
  `openssl-dev` on `musllinux`) before maturin compiles. No library code changed.
- **`release: bump 0.9.3 -> 0.9.4`**: version-string bump across the manual
  touchpoints + `CHANGELOG` `[0.9.4]`.

`0.9.4` is functionally identical to `0.9.3`; it exists only because the already-
published registries cannot re-release `0.9.3`. PyPI publishes for the first time
starting at `0.9.4` (it skips `0.9.3`).
2026-06-17 21:35:14 +02:00
kingchenc a5fe2e71c4 release: bump 0.9.2 -> 0.9.3 (#325)
Release the native data-layer bundle.

`0.9.3` ships everything merged since `0.9.2`:

- **Data layer in all 10 languages** — `CandleReader` (CSV), `TickAggregator`, `Resampler`, live `BinanceFeed` (WebSocket) and the historical `fetch_binance_klines` (REST), each cross-language golden-pinned.
- **`name()` on every indicator** in all 10 languages (514 indicators, golden-pinned).
- **Python is now zero third-party deps** — NumPy is optional (`pip install wickra[numpy]`); `batch` returns a stdlib `array.array` / buffer-protocol `Matrix` (breaking; results numerically identical, batch throughput tradeoff noted in the CHANGELOG).
- **Binance feed: missing `3d` / `1M` intervals** fixed.

Pure version-string bump on top — `bump_version.py` touched 19 files (Cargo + Lock, pyproject, all node package.json/locks + 6 platform stubs, pom + csproj + DESCRIPTION, SECURITY, CHANGELOG `[0.9.3]` + compare URLs). `cargo fmt`/`clippy -D warnings`/`test --workspace --all-features` all green locally (4225+ tests, 0 failed).

The tag/publish is **not** part of this PR — it waits for explicit GO (irreversible publish to crates.io / PyPI / npm / NuGet / Maven / Go / r-universe).
2026-06-17 21:14:22 +02:00
kingchenc 75eefbbd08 examples: fix and harmonize the strategy backtests across all languages (#324)
The strategy_* examples were only syntax-smoked in CI, never run, which hid two
classes of problem:

1. Python strategy_macd_adx / strategy_bollinger_squeeze passed three separate
   arguments to the candle indicators ADX/ATR, whose .update() takes a single
   candle — a TypeError at runtime — and read the ADX tuple at index 0 (plus_di)
   instead of 2 (adx). Both fixed.

2. The Go / C# / R / Java strategies defaulted to synthetic data and used a
   different (annualised) one-line summary, so they printed wildly different
   numbers from the Rust/Python/Node/C/WASM suite. Rewrite them to the shared
   per-trade backtest (load the bundled BTCUSDT CSV by default, same entry/exit
   logic, same print_summary output).

All nine runnable bindings now print byte-identical backtest summaries on the
same data (MACD+ADX 246 trades / -47.19%, RSI 37 / -17.84%, Bollinger 1 / -7.82%),
verified by diffing each language's output against the Python reference. WASM
shares the same logic and bundled dataset (browser-rendered).
2026-06-17 17:56:22 +02:00
kingchenc 2e07c07a40 docs(examples): drop stale third-party install hints (#323)
live_binance.py uses the native BinanceFeed and the node examples no longer
depend on ws, so the examples README no longer tells readers to pip install
websockets or that npm install pulls ws. The whole examples set runs on
Wickra alone.
2026-06-17 17:56:17 +02:00
kingchenc ec937b8281 docs: re-measure per-binding throughput on the 9950X (#322)
Refresh the section 3 / README per-binding throughput table with a fresh
SMA(20) run on the reference machine. The Python batch figure now reflects
the stdlib array.array output path (NumPy is optional since the zero-dep
change), so batch is no longer near-core for Python and Node; the prose is
updated to match.
2026-06-17 15:30:20 +02:00
kingchenc 1f5eb90b0d docs: PyPI README reflects the zero-dependency Python binding (#321)
The package README shown on **PyPI** still advertised a NumPy quick-start. Now that Python dropped its NumPy runtime dependency (#317), this updates it:

- Tagline + install note: `pip install wickra` pulls **zero** third-party packages (not even NumPy); NumPy is an optional extra (`wickra[numpy]`).
- Quick start imports no NumPy; `batch` returns `array.array('d')` (with a note that `np.asarray` wraps it zero-copy if you use NumPy).

Ships with the data-layer release.
2026-06-17 03:40:18 +02:00
kingchenc bbda70f75b docs: lead with the zero-dependency native data layer (README) (#320)
Updates the README now that the data layer is native in all ten languages and Python no longer needs NumPy (#317): the docs lead with **zero third-party deps for data I/O, in every language**.

- **'Batteries included'** bullet now covers the full data layer — CSV reader, tick aggregator, resampler, live WebSocket feed, historical REST fetcher — as zero third-party deps in every language. Drops the stale 'indicator chaining' wording (chaining stays documented under Reference).
- **Hero Python example** no longer imports NumPy; `batch` returns `array.array('d')` (with a note that `np.asarray` wraps it zero-copy if you do use NumPy).
- **'Live data sources'** lists the native `fetch_binance_klines` and the `BinanceFeed` naming, and drops the stale reference to the third-party `websockets` package.
- Intro tagline notes 'zero third-party packages'.

Docs-only; ships with the data-layer release. The matching webpage / wickra-docs / wickra-go / org-profile updates follow with the release.
2026-06-17 03:27:19 +02:00
kingchenc 2d2d5970b4 bench: native data-layer throughput benchmark (#318)
Adds a measured benchmark for the native data layer (Task 6).

## What
- New criterion bench `crates/wickra/benches/data_layer.rs` exercising the data layer on **50 000 real BTCUSDT 1m candles** (`examples/data/btcusdt-1m.csv`):
  - **CSV parse** (`CandleReader`)
  - **Tick aggregation** → 1m candles (`TickAggregator`)
  - **Resample** 1m → 5m (`Resampler`)
- New **"4. Data layer"** section in `BENCHMARKS.md` with the measured numbers and the same "one core, FFI boundary" framing as the per-binding table.

## Measured (Rust core, Ryzen 9 9950X, median of 100 samples)
| Operation | Throughput | Per element |
|---|--:|--:|
| CSV parse | 3.0 M candles/s | 329 ns |
| Tick aggregate → 1m | 44 M ticks/s | 22.6 ns |
| Resample 1m → 5m | 234 M candles/s | 4.3 ns |

This is the same native code every binding rides through the FFI boundary characterised in §3 — the data I/O that replaces pandas / csv-parse / manual bucketing / pandas.resample, with zero third-party packages.

Run: `cargo bench -p wickra --bench data_layer`.

Verified locally: `cargo clippy -p wickra --benches --all-features -- -D warnings` clean; bench runs green.
2026-06-17 03:27:14 +02:00
kingchenc 8228be7069 python: drop the NumPy runtime dependency (zero third-party deps) (#317)
Makes the Python binding truly dependency-free (Task 5b). `pip install wickra` now pulls **zero** third-party packages, and `import wickra` never imports NumPy (verified: `"numpy" not in sys.modules` after a batch call).

## What changed
- **Inputs** accept any sequence or buffer of numbers — `array.array`, `memoryview`, a NumPy `ndarray`, or a plain `list` — via `Vec` extraction (newtypes `Buf1`/`BufI64`). `PyBuffer` is unavailable under `abi3-py39`, and `unsafe_code = forbid` rules out a zero-copy slice, so inputs are copied once (negligible vs. compute).
- **Single-output `batch(...)`** returns a stdlib `array.array('d')` (native buffer protocol → `numpy.asarray` zero-copy).
- **Multi-output `batch(...)`** returns a buffer-protocol `Matrix` preserving `.shape`, integer-row and `[i, j]` access, and `.tolist()`.
- **NumPy** moves to an optional extra (`pip install wickra[numpy]`); it is never required.
- Streaming `update(...)` is unchanged; results are numerically identical.

## Implementation
- Trait `IntoPyData` + a `matrix()`/`f64_array()` helper collapse the ~400 batch call sites; `array.array` is built via `bytemuck` (Zlib/MIT/Apache — `cargo deny check licenses` ok).
- Tests migrated to `array.array`/`Matrix` (1-D `.shape`→`len()`, `.dtype`→`.typecode`; numeric comparisons normalize through a `_to_np` helper). The non-contiguous-input test now asserts acceptance instead of rejection.

## Verification (local)
- `pytest bindings/python/tests` — **1991 passed**.
- `cargo fmt --all`, `cargo clippy --workspace --all-targets --all-features -- -D warnings`, `cargo test --workspace --all-features`, `cargo deny check licenses` — all green.

**BREAKING for Python**: batch return types change from NumPy arrays to `array.array`/`Matrix`. Documented in CHANGELOG; ships with the data-layer release bundle (no separate tag).
2026-06-17 03:19:50 +02:00
kingchenc 677ea37402 examples: migrate to the native data layer (drop ws/coder-websocket/jackson/jsonlite) (#316)
Stacked on #315 (the native Binance REST fetcher). Retarget to `main` once #315 merges.

Migrates the runnable examples off third-party data-I/O packages onto Wickra's
native data layer (`CandleReader`, `Resampler`, `BinanceFeed`, `fetch_*klines`).

## Third-party packages removed (the zero-dep selling point)
- **Node**: `ws` (live feed → BinanceFeed) — dropped from package.json + lockfile
- **Go**: `github.com/coder/websocket` — dropped from go.mod / go.sum (`go mod tidy`)
- **Java**: `jackson-databind` (live feed + REST fetch) — dropped from pom.xml
- **R**: `jsonlite` + `websocket` + `later` — dropped from the README notes

Each language's CSV loading now goes through `CandleReader`, manual resampling
through `Resampler`, the live feed through `BinanceFeed`, and (Java/R) the REST
download through the native fetcher.

## Verification
Ran the offline examples per language against the bundled data — backtest and
multi_timeframe produce identical output across Python / Node / Go / Java / R
(e.g. ATR(14) last 345.1010; 1h→5m resamples to 240 bars, →15m to 80 bars).

C# / C / WASM (stdlib-only, no third-party deps to remove) follow in this branch.

Note: the streaming `strategy_*` examples have pre-existing candle-indicator
runtime bugs (CI only syntax-smokes them); the CSV migration preserves their
shape and leaves those bugs for a separate fix.
2026-06-17 01:49:11 +02:00
kingchenc 2ae76bb90e feat(data): native Binance REST kline fetcher in 9 languages (#315)
Adds `BinanceRest::fetch_klines` to `wickra-data`: a blocking historical kline
downloader (`GET /api/v3/klines`) and the historical counterpart to the F4 live
`BinanceFeed`. It is the last native data-layer primitive needed to drop
third-party HTTP/JSON download helpers (`jackson`, `jsonlite`, `urllib`, …) from
the examples.

## What

- **Core** (`wickra-data`): `fetch_klines(symbol, interval, limit, start?, end?)`
  built on `ureq` with native-tls — sharing the exact same TLS backend
  (native-tls 0.2 / SChannel) as the existing tokio-tungstenite live feed, so the
  two pull one TLS stack, not two. Parses Binance's 12-element array rows via the
  existing serde infrastructure into validated `Candle`s. Blocking by design (a
  one-shot request needs no async runtime; the FFI boundary is synchronous
  anyway). Nine mock-HTTP-server tests cover parse / empty / limit / transport /
  JSON / invariant-violation paths.
- **C ABI**: `wickra_binance_fetch_klines(...)` (blocking drain into a caller
  buffer, `-1` on error) + regenerated cbindgen header and its vendored Go copy.
- **Bindings**: native Node `fetchBinanceKlines` / Python `fetch_binance_klines`;
  generated Go `FetchBinanceKlines` / C# `BinanceFeed.FetchKlines` / Java
  `BinanceFeed.fetchKlines` / R `fetch_binance_klines`. C / C++ call the C ABI
  directly. **WASM is excluded** (browsers use the host `fetch`).

The four C-ABI bindings are regenerated from the ScriptHelpers generators (not
hand-edited); the regen diff is exactly the new wrapper in each.

## Verification

All ten toolchains green locally: Rust (`cargo test`/`clippy`/`fmt`), Node, Python,
Go, C#, Java, R (`R CMD INSTALL` + smoke), WASM (`cargo check`, confirmed `ureq`
is not pulled). Each binding has an error-path smoke test; the parse/HTTP success
path is covered by the Rust mock-server tests.

No release in this PR — ships with the data-layer + numpy bundle later.
2026-06-17 01:48:24 +02:00
kingchenc b92ad32037 docs(crates): point wickra documentation link to docs.wickra.org (#314)
## What

Change the `documentation` field of the main `wickra` crate from the auto-generated rustdoc URL to the project's documentation site:

```toml
# crates/wickra/Cargo.toml
documentation = "https://docs.wickra.org"   # was: https://docs.rs/wickra
```

## Why

On crates.io the "Documentation" link defaulted to `docs.rs/wickra`. The richer landing page for someone arriving from crates.io is the prose docs site — it carries the Rust quickstart, the API guide, and the full indicator catalogue. The auto-built docs.rs API reference stays reachable at `docs.rs/wickra`; only the crates.io link target changes.

## Scope

- Only the user-facing `wickra` crate is changed.
- `wickra-core` and `wickra-data` keep their `docs.rs` links — those low-level crates have no dedicated guide page on the docs site, so docs.rs remains the correct target for them.
- Metadata only; no code, build, or runtime change. Takes effect on the next crates.io publish.
2026-06-17 01:06:01 +02:00
kingchenc 3a709d9a66 feat(data): native live Binance kline feed in 9 languages (+ 3d/1M intervals) (#313)
* feat(data): C-ABI Binance feed + Go binding (F4 wip)

C ABI exposes the existing async BinanceKlineStream (tokio + TLS, auto-reconnect,
mock-server-tested in wickra-data) through a blocking poll: wickra_binance_connect
/ _next(out, timeout_ms) -> {1 event, 0 timeout, -1 closed} / _close / _free over
an opaque BinanceStream that owns a current-thread runtime. WickraKlineEvent
carries OHLCV + open_time + is_closed + a 16-byte symbol buffer. `live-binance`
is now a default feature of wickra-c (the published DLL ships the feed; the wasm
build drops it via --no-default-features).

Go: NewBinanceFeed(symbols, interval, baseURL) + Next(timeout) + Close, with a
deterministic error-path smoke (the connect->event pipeline is covered by the
Rust mock-WS-server tests).

* feat(data): Binance feed C# + Java bindings (F4 wip)

C#: BinanceFeed(symbols, interval, baseUrl?) + Next(timeout) -> KlineEvent? +
Dispose; bespoke WickraKlineEvent native struct (fixed symbol buffer + byte
is_closed) since the scalar struct parser can't model it. `char` maps to `byte`
for the const char* params.

Java: BinanceFeed + KlineEvent record + BinanceInterval enum over Panama FFM;
the event is read at hand-computed offsets (symbol@0, doubles@16..48,
open_time@56, is_closed@64; 72-byte struct). Both with deterministic error-path
smokes (pipeline covered by the Rust mock-WS-server tests).

* feat(data): Binance feed R binding (F4 wip)

R: BinanceFeed(symbols, interval, base_url) + binance_next(feed, timeout_ms) ->
named list | NULL + binance_close, via bespoke .Call glue (wk_binance_*). The
glue + its registration entries are gated out of the Emscripten/wasm build
(#ifndef __EMSCRIPTEN__) since r-universe/webR has no raw sockets. NAMESPACE
exports added by hand (roxygen2 not installed locally). Deterministic error-path
smoke; pipeline covered by the Rust mock-WS-server tests.

* feat(data): native Binance feed for Node + Python; CHANGELOG (F4 complete)

Node (napi) BinanceFeed: new(symbols, interval, baseUrl?) + next(timeoutMs) ->
KlineEvent | null + close. Python (pyo3) BinanceFeed: same, with next releasing
the GIL (py.detach) while it waits. Both drive the mock-server-tested async
BinanceKlineStream on a single-thread tokio runtime (blocking poll); wickra-data
gains the live-binance feature + tokio in each binding.

Completes F4: the live Binance kline feed is now native in all 9 languages
(WASM excluded), with no third-party WebSocket client in any of them.
2026-06-16 02:01:37 +02:00
kingchenc baf4d0ff47 fix(data): add missing 3d and 1M Binance kline intervals (#312)
Binance supports 16 kline intervals (1s,1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,
12h,1d,3d,1w,1M); the live-binance `Interval` enum listed only 14, missing
three-day (`3d`) and one-month (`1M`). Add both variants in Binance order
with their wire-format strings, and extend the exhaustive as_str test.
2026-06-16 00:51:52 +02:00
kingchenc d362ae26a3 feat(data): expose CandleReader (CSV) natively in all 10 languages (#311)
Add the data-layer CSV candle reader to every binding so loading OHLCV
candles from a CSV no longer needs a per-language CSV/dataframe dependency.

- C ABI: wickra_candle_reader_new(bytes, len) / _count / _read / _free over
  an opaque CandleReader handle (parse the whole buffer up front, then drain).
- Native: Node/WASM CandleReader.read() -> Candle[], Python read() -> list[tuple].
- C-ABI languages: Go Read() []Candle, C# Candle[] Read(), Java Candle[] read(),
  R read() S3 generic (n x 6 matrix); C / C++ call the C ABI directly.
- Cross-language golden testdata/golden/data_csv*.csv pins the parsed candles
  bit-for-bit across every binding.

Verified locally across Rust (test+clippy+fmt), Node, WASM, Python, C#, Go,
Java, R, and the C/C++ cmake parity suite.
2026-06-16 00:10:58 +02:00
kingchenc cb6da4d737 feat(data-layer): Resampler (candle resampling) in all 10 languages (#310)
* feat(data-layer): Resampler (candle resampling) in all 10 languages

Second data-layer feature (F3): resample candles into a higher timeframe.

- Native (Node.js/WASM): new Resampler(timeframe) -> update(o,h,l,c,v,ts):
  Candle|null + flush(): Candle|null. Python the same -> tuple|None.
- C ABI: wickra_resampler_new/update/flush/free (update has the multi-output
  shape so the generators auto-emit it; flush is bespoke). Go Update -> (Candle,
  bool) + Flush; C# Candle? Update/Flush; Java Candle update/flush; R update()
  generic + a flush() S3 method (extends base::flush); C/C++ direct.
- Cross-language golden (testdata/golden/data_resampled.csv): the shared input
  candles resampled into 5-unit buckets, the final partial bucket via flush,
  pinned bit-for-bit across every binding.

Verified locally in all 10 (3 candles for the 5-unit smoke; 16 for the golden).
The WickraCandle output record is shared with the tick aggregator (deduped).

* test(node): exclude data-layer types from the indicator completeness contract

The Resampler exposes update(), so the completeness test flagged it as an
indicator and required batch/reset/isReady/warmupPeriod, which a data-layer type
does not have. Exclude TickAggregator and Resampler like the bar builders.
2026-06-15 22:36:16 +02:00
kingchenc 8a103ef920 feat(data-layer): TickAggregator (tick-to-candle) in all 10 languages (#309)
* feat(data-layer): TickAggregator in Node, WASM, Python + C ABI hub

First data-layer feature (F2): roll trade ticks up into fixed-timeframe OHLCV
candles, exposed natively and over the C ABI.

- wickra-data wired as a binding dependency (workspace dep; its wickra-core dep
  is default-features=false so it never forces rayon into the rayon-free WASM
  build — native bindings re-enable parallel through their own dependency).
- Node `TickAggregator(bucket, gapFill?)` -> `push(price, size, ts): Candle[]`;
  WASM the same (array of objects); Python `push(...) -> list[tuple]`.
- C ABI: `WickraCandle` struct + `wickra_tick_aggregator_new/push/free` (push
  writes candles into a caller buffer and returns the count), generated via the
  capi generator's new DATA_LAYER section; cbindgen now parses wickra-data so
  `TickAggregator` is a forward-declared opaque; header vendored to bindings/go.

Verified bit-identical across Node/WASM/Python/C/C++ (o=100 h=101 l=100 c=101
v=3 ts=0 for the shared 3-tick probe). WIP: Go/C#/Java/R generated bindings and
the cross-language golden are still pending.

* feat(data-layer): TickAggregator in Go, C#, Java, R (lossless push/drain)

Complete F2 across all 10 languages: the C-ABI tick aggregator now uses a
two-step push/drain so gap-fill candles are never lost, and the four generated
bindings expose it idiomatically.

- C ABI redesigned: opaque TickAggregator handle (inner aggregator + pending
  buffer); push consumes a tick and returns the closed-candle count, drain copies
  them into a count-sized caller buffer.
- Go: NewTickAggregator + Push(price,size,ts) []Candle; C#: TickAggregator +
  Candle[] Push(...); Java: TickAggregator + Candle[] push(...); R: TickAggregator
  constructor + push() S3 generic returning an (n x 6) numeric matrix.
- Candle output record generated per language from WickraCandle.

Verified bit-identical to the native bindings (o=100 h=101 l=100 c=101 v=3 ts=0)
in Go, C#, Java, and R at runtime; R passes R CMD check (pre-existing doc
warnings only). WIP: cross-language data-layer golden + CHANGELOG still pending.

* test(data-layer): cross-language golden for the tick aggregator + CHANGELOG

gen_golden emits a deterministic tick stream (testdata/golden/data_ticks.csv) and
the reference candle streams with and without gap filling (data_candles.csv,
data_candles_gap.csv). Every binding replays the shared ticks through its
TickAggregator and checks the candles bit-for-bit (fp tolerance) against the Rust
reference:

- Node / WASM / Python / Go / C# / Java / R: a dedicated parity test each.
- C / C++: data_layer_test.c (compiled as both, run as ctest).

The gap-fill fixture closes several candles from a single push, exercising the
lossless push/drain path. Records the feature under CHANGELOG [Unreleased].

* fix(examples): rename the CSV-loader candle to WickraBar

The example CSV helper (wickra_csv.h) defined its own struct WickraCandle, which
now collides with the public C ABI WickraCandle (the tick aggregator output) in
any example that includes both headers (backtest, multi_timeframe, the strategy
examples). The public type owns the name; rename the example loader's bar to
WickraBar. The generated golden_test.c is untouched (its only match was the
unrelated WickraCandleVolumeOutput).
2026-06-15 21:24:33 +02:00
kingchenc fd9f4c8bc6 feat(bindings): expose name() on every indicator in all 10 languages (#308)
* feat(bindings): expose name() on every indicator in Node, WASM, and Python

Surface the core Indicator::name() / BarBuilder::name() accessor through the
three native bindings so every indicator reports its canonical name at runtime,
matching the existing reset/isReady/warmupPeriod surface.

- Node (napi): name(): string on all 514 classes (regenerated index.d.ts)
- WASM (wasm-bindgen): name(): string on all 514 classes
- Python (pyo3): name() -> str on all classes

* feat(bindings): expose name() across the C ABI and C/C++/Go/C#/Java/R

Regenerate the C ABI and the four generated language bindings from the updated
ScriptHelpers generators so every indicator and bar builder reports its
canonical name at runtime, completing name() coverage across all 10 languages.

- C ABI (bindings/c): wickra_<ind>_name() -> *const c_char for all 514, cached
  in a per-function OnceLock<CString> with ind.name() as the source of truth;
  cbindgen header regenerated and vendored into bindings/go/include.
- Go: Name() string; C#: string Name(); Java: String name(); R: name() S3
  generic over the wk_<ind>_name C glue (methods.R + NAMESPACE).

The Java regeneration also restores two fixes that had drifted out of the
generator (bool* arrays via boolSegment; uint8_t ctor args cast to byte) and C#
re-emits '#nullable enable'; these are no-op vs the previous committed output
apart from the new name() accessors.

* test(golden): pin canonical name() across all 10 language bindings

Add a cross-language name() consistency check: every indicator must report the
exact core Indicator::name() (which can differ from the registered class name,
e.g. ChaikinMoneyFlow -> "CMF", Donchian -> "DonchianChannels"). The 514 core
names are committed as testdata/golden/names.json (keyed by Rust canonical) and
asserted by each binding's golden replay, which already reconstructs the whole
catalogue:

- node / wasm: assert against names.json in the existing golden test
- python: new test_golden_names.py over the shared node manifest
- go / csharp / java / c+c++ / r: the golden-test generators load names.json and
  emit a name assertion per indicator (regenerated test artifacts committed)

All 10 bindings return identical names by construction (each delegates to core),
so this pins that contract and guards against a future binding breaking the
passthrough.

* docs(changelog): record name() across all 10 bindings under Unreleased

* fix(r): restore bool* flag marshalling in the regenerated C glue

The name() regeneration had reverted the cross-section bool fix: the R glue
emitted (bool *)REAL(x) for const bool* inputs, reinterpreting 8-byte doubles as
1-byte bools so every flag read as false (PercentAboveMa, NewHighsNewLows,
HighLowIndex, BullishPercentIndex returned 0 instead of the breadth value). The
wk_bool_vec() helper is restored in the generator and the glue routes bool arrays
through it again.
2026-06-15 17:19:24 +02:00
kingchenc 59acefa1ea ci: retry-harden the Rust toolchain install across all jobs (#307) 2026-06-15 07:09:39 +02:00
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
1257 changed files with 152138 additions and 4942 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
+46
View File
@@ -0,0 +1,46 @@
name: Setup Rust toolchain (with retry)
description: >
Install a Rust toolchain via dtolnay/rust-toolchain, retrying once after a
short pause on the transient rustup CDN failure (a corrupt channel manifest /
checksum mismatch on `channel-rust-<channel>.toml`) that rustup itself does
not retry — the same hardening already applied to setup-node / setup-go.
inputs:
toolchain:
description: Rust toolchain (channel or version) to install.
required: false
default: stable
components:
description: Comma-separated list of rustup components (e.g. "rustfmt, clippy").
required: false
default: ""
targets:
description: Comma-separated list of rustup targets (e.g. "wasm32-unknown-unknown").
required: false
default: ""
runs:
using: composite
steps:
- id: first
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
continue-on-error: true
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
targets: ${{ inputs.targets }}
- name: Wait before retrying the toolchain install
if: steps.first.outcome == 'failure'
shell: bash
run: |
echo "::warning::rustup toolchain install failed (likely a CDN flake / corrupt channel manifest); retrying in 30s"
sleep 30
- name: Retry the Rust toolchain install
if: steps.first.outcome == 'failure'
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
with:
toolchain: ${{ inputs.toolchain }}
components: ${{ inputs.components }}
targets: ${{ inputs.targets }}
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
@@ -126,7 +126,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
+161 -44
View File
@@ -34,7 +34,7 @@ jobs:
rust:
name: Rust ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -45,7 +45,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
components: rustfmt, clippy
@@ -105,7 +105,7 @@ jobs:
examples-smoke:
name: Examples (syntax smoke)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -195,21 +195,21 @@ jobs:
clippy-bindings:
name: Clippy bindings
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
components: clippy
- name: Set up Python
id: setup_python
continue-on-error: true
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -222,7 +222,7 @@ jobs:
- name: Set up Python (retry)
if: steps.setup_python.outcome == 'failure'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
@@ -279,7 +279,7 @@ jobs:
msrv:
name: ${{ matrix.name }}
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -296,7 +296,7 @@ jobs:
persist-credentials: false
- name: Install Rust ${{ matrix.toolchain }}
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
toolchain: ${{ matrix.toolchain }}
@@ -328,14 +328,14 @@ jobs:
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
components: llvm-tools-preview
@@ -345,7 +345,7 @@ jobs:
timeout-minutes: 6
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cargo-llvm-cov
@@ -358,7 +358,7 @@ jobs:
--lcov --output-path lcov.info
- name: Upload to Codecov
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
files: lcov.info
fail_ci_if_error: false
@@ -370,7 +370,7 @@ jobs:
supply-chain:
name: Supply-chain (cargo-deny)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
@@ -389,14 +389,14 @@ jobs:
fuzz-smoke:
name: Fuzz (smoke)
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install nightly Rust
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
toolchain: nightly
@@ -415,7 +415,7 @@ jobs:
# attributes the modern nightly compiler rejects, so the install
# never gets off the ground. The prebuilt binary avoids the entire
# transitive-dep compile.
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cargo-fuzz
@@ -443,7 +443,7 @@ jobs:
python:
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -455,7 +455,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -525,14 +525,14 @@ jobs:
wasm:
name: WASM build
runs-on: ubuntu-latest
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install Rust toolchain (with wasm target)
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
with:
targets: wasm32-unknown-unknown
@@ -549,7 +549,7 @@ jobs:
# same taiki-e prebuilt-binary installer we already use for
# cargo-llvm-cov and cargo-fuzz; it tracks the latest wasm-pack
# release, which has `--features` as a top-level flag (since 0.12).
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: wasm-pack
@@ -566,10 +566,16 @@ 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 }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -581,7 +587,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -616,9 +622,17 @@ jobs:
cache: npm
cache-dependency-path: bindings/node/package-lock.json
# npm's own fetch-retry (npm_config_fetch_retries) rides out per-request
# blips; wrap the whole `npm ci` once more so a longer registry hiccup
# retries the install instead of failing the job.
- name: Install Node dependencies
working-directory: bindings/node
run: npm ci
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 20
command: cd bindings/node && npm ci
shell: bash
- name: Build native module
working-directory: bindings/node
@@ -643,7 +657,7 @@ jobs:
c-abi:
name: C ABI on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -654,7 +668,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -662,7 +676,7 @@ jobs:
timeout-minutes: 6
- name: Install cbindgen
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cbindgen
@@ -699,7 +713,7 @@ jobs:
csharp:
name: C# on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -710,13 +724,26 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
# Cache the restored NuGet packages so dotnet test/build resolve from the
# local store instead of hitting nuget.org every run. No packages.lock.json
# exists, so key on the project files; never block the job on a slow restore.
- name: Cache NuGet packages
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
continue-on-error: true
timeout-minutes: 6
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
# The binding links against the C ABI hub at runtime; build it first so the
# DllImportResolver finds target/release/wickra.{dll,so,dylib}. .NET 8 SDK is
# preinstalled on the GitHub runners, so no setup-dotnet step is needed.
@@ -726,8 +753,17 @@ jobs:
- name: .NET info
run: dotnet --info
# dotnet test restores from nuget.org first; retry so a transient restore
# blip retries instead of failing the job (the cached packages above make
# the retry cheap).
- name: Test the C# binding
run: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 15
max_attempts: 2
retry_wait_seconds: 20
command: dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj -c Release
shell: bash
- name: Build the C# examples
shell: bash
@@ -750,7 +786,7 @@ jobs:
go:
name: Go on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -786,7 +822,7 @@ jobs:
cache: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -873,7 +909,7 @@ jobs:
r:
name: R on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -887,7 +923,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -906,10 +942,17 @@ jobs:
r-version: "release"
use-public-rspm: true
# Use the repos configured by setup-r (use-public-rspm) so Linux installs
# binary packages — building testthat's deps from source is slow and flaky.
- name: Install test dependency
run: Rscript -e 'install.packages("testthat")'
# Install the R dependencies via setup-r-dependencies: it restores a cached
# package library (actions/cache) and pulls RSPM *binaries* instead of
# compiling testthat / knitr and their deps from source — the slow, flaky
# path that previously blew past the job timeout on the ubuntu runner.
- name: Install R dependencies (cached binaries)
uses: r-lib/actions/setup-r-dependencies@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2
with:
working-directory: bindings/r
extra-packages: |
any::testthat
any::knitr
- name: Install and test the R binding
shell: bash
@@ -926,14 +969,39 @@ jobs:
R CMD INSTALL bindings/r
Rscript -e 'library(testthat); library(wickra); test_dir("bindings/r/tests/testthat", stop_on_failure = TRUE)'
# Full R CMD check, gated on the documentation problems r-universe surfaces
# (undocumented exported objects, codoc mismatches) that R CMD INSTALL above
# does not catch — exactly what shipped stale to r-universe with the 0.9.3
# data layer. Ubuntu-only; these checks are platform-independent. Vignettes
# are skipped here (building them needs pandoc and the vignette is exercised
# in the next step), so the two --no-build-vignettes warnings are ignored.
- name: R CMD check (documentation & consistency, like r-universe)
if: matrix.os == 'ubuntu-latest'
shell: bash
run: |
export WICKRA_INCLUDE_DIR="${WICKRA_INCLUDE_DIR//\\//}"
export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}"
R CMD build bindings/r --no-build-vignettes --no-manual
R CMD check wickra_*.tar.gz --no-manual --no-vignettes --no-tests || true
log=$(find . -maxdepth 2 -name 00check.log | head -1)
echo "::group::00check.log"; cat "$log"; echo "::endgroup::"
problems=$(grep -E '\.\.\. (WARNING|ERROR)' "$log" \
| grep -vE "checking (files in .vignettes.|package vignettes)" || true)
if [ -n "$problems" ]; then
echo "::error::R CMD check found problems (run roxygen2::roxygenise() in bindings/r if the docs are stale):"
echo "$problems"
exit 1
fi
echo "R CMD check: documentation and consistency clean."
- name: Build the vignette code
shell: bash
# The getting-started vignette runs at R CMD check time on r-universe /
# CRAN (with pandoc); this job only INSTALLs, so execute the vignette's R
# chunks here (knit, no pandoc needed) to catch a broken example before it
# reaches the published build.
# knitr is installed by the cached setup-r-dependencies step above.
run: |
Rscript -e 'install.packages("knitr", repos = Sys.getenv("RSPM", unset = "https://cloud.r-project.org"))'
Rscript -e 'knitr::knit("bindings/r/vignettes/getting-started.Rmd", output = tempfile(fileext = ".md"), quiet = TRUE); cat("vignette code OK\n")'
- name: Run the offline R examples
@@ -955,7 +1023,7 @@ jobs:
java:
name: Java on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
strategy:
fail-fast: false
matrix:
@@ -966,7 +1034,7 @@ jobs:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
uses: ./.github/actions/setup-rust
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
@@ -1010,8 +1078,16 @@ jobs:
# `install` runs the archetype test suite (the real FFI boundary check) and
# installs the binding to the local repo so the examples can resolve it.
# Maven resolves plugins/deps from the network on a cache miss; retry so a
# transient Central blip retries instead of failing the job.
- name: Test and install the Java binding
run: mvn -B -f bindings/java install
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 20
max_attempts: 2
retry_wait_seconds: 20
command: mvn -B -f bindings/java install
shell: bash
- name: Build the Java examples
run: mvn -B -f examples/java compile
@@ -1025,6 +1101,47 @@ jobs:
mvn -B -q -f examples/java exec:exec -Dexec.mainClass="org.wickra.examples.$cls"
done
# Build a Python wheel inside both the manylinux and the musllinux container,
# mirroring the Linux wheel build in release.yml. The 3-OS Python jobs build
# natively on the runner, which already ships system OpenSSL, so they cannot
# catch a build-time gap that only exists inside the slim release containers —
# exactly what broke the 0.9.3 Linux wheels (the live-binance data layer links
# native-tls -> openssl-sys, and the containers provide no OpenSSL: manylinux
# lacks the headers, musllinux cross-compiles against a musl sysroot without
# OpenSSL at all). The wheels are built with the `vendored-tls` feature, which
# statically compiles OpenSSL from source. This job exercises both container
# builds on every PR, so the same class of breakage now fails CI, not release.
python-wheel-container-smoke:
name: Python wheel (${{ matrix.manylinux }} smoke)
runs-on: ubuntu-latest
timeout-minutes: 30 # backstop: vendored OpenSSL adds a from-source compile
strategy:
fail-fast: false
matrix:
manylinux: [auto, musllinux_1_2]
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Sync root README into bindings/python so the build matches release
run: cp README.md bindings/python/README.md
- uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
with:
working-directory: bindings/python
target: x86_64
# Keep in sync with release.yml: vendored OpenSSL for the Linux wheels.
args: --release --out dist --features vendored-tls
manylinux: ${{ matrix.manylinux }}
# Building OpenSSL from source needs Perl modules (IPC::Cmd,
# Time::Piece, ...) the minimal manylinux (CentOS 7) image lacks.
# perl-core pulls the full distribution; the explicit names document
# the ones OpenSSL's Configure has required. The musllinux cross image
# ships a complete Perl and has no yum, so this is a no-op there.
before-script-linux: |
if command -v yum >/dev/null 2>&1; then yum install -y perl-core perl-IPC-Cmd perl-Data-Dumper perl-Time-Piece; fi
# The cross-library benchmark has moved to a dedicated scheduled workflow
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
+64 -15
View File
@@ -36,6 +36,7 @@ jobs:
# --------------------------------------------------------------------------
cargo-publish:
name: Publish to crates.io
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
runs-on: ubuntu-latest
# The publish jobs run with long-lived registry tokens. Binding them to a
# protected GitHub environment lets the org require a reviewer to approve
@@ -48,7 +49,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
@@ -110,7 +111,7 @@ jobs:
# consumers can audit the published dependency tree without
# re-resolving Cargo.lock.
- name: Install cargo-cyclonedx
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cargo-cyclonedx
@@ -139,6 +140,7 @@ jobs:
# --------------------------------------------------------------------------
python-wheels:
name: Build wheels (${{ matrix.target }}/${{ matrix.manylinux }} on ${{ matrix.os }})
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
strategy:
fail-fast: false
matrix:
@@ -184,8 +186,22 @@ jobs:
with:
working-directory: bindings/python
target: ${{ matrix.target }}
args: --release --strip --out dist
# The live-binance data layer links native-tls -> openssl-sys, which
# needs OpenSSL at build time. The manylinux containers lack the headers
# and the musllinux build cross-compiles against a musl sysroot with no
# OpenSSL at all, so build the Linux wheels with vendored OpenSSL
# (compiled from source, linked statically). No-op on the native
# macOS/Windows runners, where native-tls never pulls openssl-sys. The
# CI `python-wheel-container-smoke` job exercises both containers on PRs.
args: --release --strip --out dist --features vendored-tls
manylinux: ${{ matrix.manylinux }}
# Building OpenSSL from source needs Perl modules (IPC::Cmd,
# Time::Piece, ...) the minimal manylinux (CentOS 7) image lacks.
# perl-core pulls the full distribution; the explicit names document
# the ones OpenSSL's Configure has required. The musllinux cross image
# ships a complete Perl and has no yum, so this is a no-op there.
before-script-linux: |
if command -v yum >/dev/null 2>&1; then yum install -y perl-core perl-IPC-Cmd perl-Data-Dumper perl-Time-Piece; fi
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
# Include manylinux in the name so the glibc and musl x86_64/aarch64
@@ -195,6 +211,7 @@ jobs:
python-sdist:
name: Build Python sdist
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -214,6 +231,7 @@ jobs:
python-publish:
name: Publish to PyPI
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: [python-wheels, python-sdist]
runs-on: ubuntu-latest
environment: release
@@ -237,6 +255,7 @@ jobs:
# --------------------------------------------------------------------------
node-build:
name: Node build (${{ matrix.target }})
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
strategy:
fail-fast: false
matrix:
@@ -273,7 +292,7 @@ jobs:
with:
node-version: "22"
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
with:
targets: ${{ matrix.target }}
@@ -282,8 +301,13 @@ jobs:
timeout-minutes: 6
- name: Install Node deps
working-directory: bindings/node
run: npm ci
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 20
command: cd bindings/node && npm ci
shell: bash
- name: Build native module
working-directory: bindings/node
@@ -298,6 +322,7 @@ jobs:
node-publish:
name: Publish to npm
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: node-build
runs-on: ubuntu-latest
environment: release
@@ -337,8 +362,13 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Install Node deps
working-directory: bindings/node
run: npm ci
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 10
max_attempts: 3
retry_wait_seconds: 20
command: cd bindings/node && npm ci
shell: bash
- name: Download all platform binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -470,6 +500,7 @@ jobs:
# which requires the `id-token: write` permission set at the job level.
wasm-publish:
name: Publish wickra-wasm to npm
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
runs-on: ubuntu-latest
environment: release
# `id-token: write` lets npm publish embed a Sigstore provenance
@@ -505,14 +536,18 @@ jobs:
node-version: "22"
registry-url: "https://registry.npmjs.org"
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Install wasm-pack (latest, via prebuilt binary)
# See the matching note in ci.yml: jetli's default installs an old
# 0.10.x wasm-pack whose build subcommand rejects --features.
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
uses: taiki-e/install-action@7a79fe8c3a13344501c80d99cae481c1c9085912 # v2.81.10
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: wasm-pack
@@ -576,6 +611,7 @@ jobs:
# --------------------------------------------------------------------------
c-abi-build:
name: C ABI library (${{ matrix.target }})
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
strategy:
fail-fast: false
matrix:
@@ -592,7 +628,7 @@ jobs:
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- uses: ./.github/actions/setup-rust
with:
targets: ${{ matrix.target }}
@@ -631,6 +667,7 @@ jobs:
# workflow release.yml) exchanges the GitHub OIDC token for a short-lived key.
csharp-publish:
name: Publish to NuGet
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: c-abi-build
runs-on: ubuntu-latest
environment: release
@@ -675,11 +712,18 @@ jobs:
echo "staged ${RID[$target]}:"; ls -l "$dest"
done
# dotnet pack restores from nuget.org first; retry so a transient restore
# blip retries instead of failing the release.
- name: Pack
shell: bash
run: |
version="${GITHUB_REF_NAME#v}"
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
timeout_minutes: 15
max_attempts: 2
retry_wait_seconds: 20
shell: bash
command: |
version="${GITHUB_REF_NAME#v}"
dotnet pack bindings/csharp/Wickra/Wickra.csproj -c Release -p:Version="$version" -o nupkg
# Exchange the GitHub OIDC token for a short-lived (~1h) NuGet API key.
# 'user' is the nuget.org profile name (the package owner), not an email.
@@ -714,6 +758,7 @@ jobs:
# match the release tag (kept in sync by the version-bump checklist).
java-publish:
name: Publish to Maven Central
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: c-abi-build
runs-on: ubuntu-latest
environment: release
@@ -785,6 +830,7 @@ jobs:
# derived artifact, so its bot commit is intentionally unsigned.
go-mirror:
name: Mirror the Go module to wickra-go
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: c-abi-build
runs-on: ubuntu-latest
steps:
@@ -868,6 +914,7 @@ jobs:
github-release:
name: Attach assets to the draft GitHub Release
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
runs-on: ubuntu-latest
permissions:
@@ -993,6 +1040,7 @@ jobs:
# --------------------------------------------------------------------------
attestations:
name: Attest build provenance
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: [cargo-publish, python-wheels, python-sdist, github-release]
runs-on: ubuntu-latest
# Signed SLSA build-provenance attestations for the published crates and
@@ -1076,6 +1124,7 @@ jobs:
# --------------------------------------------------------------------------
publish-release:
name: Publish the GitHub Release
timeout-minutes: 45 # backstop only: release build/publish jobs compile from source (vendored OpenSSL) and must not be killed mid-build; far above the ~10 min real runtime
needs: [github-release, attestations]
if: always() && needs.github-release.result == 'success'
runs-on: ubuntu-latest
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
publish_results: true
- name: Upload SARIF artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: SARIF file
path: results.sarif
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Audit repo-metadata.toml drift
+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 ───┐
+59 -19
View File
@@ -109,9 +109,11 @@ series through three indicators chosen by call-signature archetype — `SMA(20)`
(1-in → 1-out), `ATR(14)` (multi-in → 1-out) and `MACD(12,26,9)` (1-in →
multi-out). Two things fall out of the numbers:
- **Batch converges.** A `batch` call crosses the boundary once and the Rust core
computes the whole series internally, so batch throughput is roughly the same
in every binding — close to the core speed.
- **Batch crosses once.** A `batch` call crosses the boundary a single time and the
Rust core computes the whole series internally, so batch throughput stays high for
most bindingsthe exceptions are the ones that copy or box the result array on
the way out (Node's JS `Array`, Python's stdlib `array.array` now that NumPy is
optional).
- **Streaming reveals the boundary.** A per-tick `update` crosses the boundary
once per value, so streaming throughput is where the bindings differ: the raw C
ABI and P/Invoke-style calls are nearly free, while managed or interpreted
@@ -126,23 +128,23 @@ AMD Ryzen 9 9950X):
| Target | streaming (Mupd/s) | batch (Mupd/s) |
|----------------------|-------------------:|---------------:|
| Rust core (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 |
| Rust core (no FFI) | 380 | 498 |
| C / C++ | 365 | 358 |
| C# | 348 | 259 |
| Python | 31 | 46 |
| Java | 38 | 173 |
| Go | 23 | 394 |
| WASM | 21 | 169 |
| Node.js | 16 | 9 |
| R | 0.1 | 279 |
Streaming spans more than three orders of magnitude — the raw C ABI (383) is
nearly the FFI-free Rust ceiling (391), while R's per-call interpreter overhead
(0.1) makes streaming ~2000× slower than its own batch. Batch converges near the
core speed for the zero-copy bindings (numpy, slices, typed arrays); the two
outliers are Node — whose napi `batch` boxes every element into a JS `Array`
and R. These are machine-dependent and reflect FFI overhead, not algorithm
speed.
Streaming spans more than three orders of magnitude — the raw C ABI (365) sits
just under the FFI-free Rust ceiling (380), while R's per-call interpreter overhead
(0.1) makes streaming ~2800× slower than its own batch. Batch stays high for the
bindings that hand back a contiguous buffer (slices, typed arrays); the two low
outliers are Node — whose napi `batch` boxes every element into a JS `Array` and
Python, which now copies into a stdlib `array.array` since NumPy became optional.
These are machine-dependent and reflect FFI overhead, not algorithm speed.
These are throughput numbers, not competitive numbers — the "Wickra is fast"
claim lives in sections 1 and 2 (Rust core + the Python/Rust cross-library runs).
@@ -166,3 +168,41 @@ mvn -q -f bindings/java install -DskipTests \
&& mvn -q -f bindings/java/benchmarks exec:exec -Dexec.mainClass=org.wickra.benchmarks.Throughput
Rscript bindings/r/benchmarks/throughput.R # R (.Call)
```
## 4. Data layer — native I/O throughput
Wickra ships its own data layer — a CSV candle reader, a tick-to-candle
aggregator, and a timeframe resampler — so loading and reshaping market data
needs **no third-party package** (`pandas`, `csv-parse`, manual bucketing,
`pandas.resample`, …) in any of the ten languages. These run on the same Rust
core as the indicators, so every binding reaches these speeds minus the FFI
boundary characterised in section 3 (a `read` / `push` / `flush` call crosses the
boundary once per batch, like `batch`, so bindings land close to the core).
Rust core, 50 000 real BTCUSDT one-minute candles
(`examples/data/btcusdt-1m.csv`), median of 100 samples, on the reference machine
(Windows 11, AMD Ryzen 9 9950X):
| Operation | Throughput | Per element |
|------------------------------------|--------------------:|------------:|
| CSV parse (`CandleReader`) | 3.0 M candles/s | 329 ns |
| Tick aggregate → 1m (`TickAggregator`) | 44 M ticks/s | 22.6 ns |
| Resample 1m → 5m (`Resampler`) | 234 M candles/s | 4.3 ns |
Reading and validating a 50 000-row CSV into typed candles takes ~16 ms;
aggregating 50 000 ticks into one-minute bars ~1.1 ms; resampling 50 000
one-minute candles to five-minute bars ~0.2 ms. CSV parsing is the floor because
it does the most per row (UTF-8 scan, field split, six `f64` parses, finiteness
checks); aggregation and resampling are pure arithmetic over already-typed
candles.
The live and historical Binance feeds (`BinanceFeed`, `fetch_binance_klines`) are
network-bound — their throughput is set by the exchange and the socket, not by
Wickra — so they are not micro-benchmarked here; the relevant Wickra cost is the
per-event parse, which is the same arithmetic measured above.
Run it with:
```bash
cargo bench -p wickra --bench data_layer
```
+237 -1
View File
@@ -7,6 +7,236 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.7] - 2026-06-21
Maintenance release. The library API and every indicator are unchanged from
`0.9.6`; this release carries an R package metadata fix and routine dependency
and CI tooling updates.
### Fixed
- **R package: credit `kingchenc` as the package author and maintainer.** The R
`DESCRIPTION` was the only binding still listing "Wickra contributors" as the
sole `aut`/`cre` — an outlier introduced when the binding was added. It now
matches the Python, Node, and Rust core metadata, the package `.Rd` is
regenerated, and the binding `LICENSE` copyright holder is aligned with the
root `LICENSE-MIT`.
### Changed
- **Dependency and CI housekeeping.** Bump the C# test dependencies
(`Microsoft.NET.Test.Sdk`, `xunit`, `xunit.runner.visualstudio`), the GitHub
Actions used by CI (`setup-python`, `upload-artifact`, `codecov-action`,
`taiki-e/install-action`), and the Java benchmark's `org.wickra:wickra`
dependency. No runtime code changes.
## [0.9.6] - 2026-06-18
Documentation release for the R binding. The library API and every indicator
are unchanged from `0.9.5`; only the R package's help pages change.
### Fixed
- **R package: document the data-layer exports and refresh the man pages.**
r-universe's `R CMD check` reported two warnings against the `0.9.3` data layer
it built for the first time in `0.9.5`: twelve undocumented exported objects
(`BinanceFeed`, `CandleReader`, `Resampler`, `TickAggregator`,
`fetch_binance_klines` and the `name` / `is_ready` / `warmup_period` / `push` /
`read` generics) and a codoc mismatch on `AwesomeOscillatorHistogram` (its help
page still listed `sma_period` after the argument was renamed to `lookback`).
The roxygen sources existed but the `man/*.Rd` had never been regenerated; they
are now complete, and a `push()` example that constructed a `TickAggregator`
without its required `gap_fill` argument is fixed. CI now runs `R CMD check` so
documentation drift fails the pull request instead of surfacing on r-universe.
## [0.9.5] - 2026-06-17
Maintenance release. The library API and every indicator are unchanged from
`0.9.4`; the only change that ships to users is to the R package's build script.
The rest of the release is CI / release-pipeline hardening (dependency caching,
job timeouts, and network-install retries) that does not affect the artifacts.
### Fixed
- **R package: retry the C ABI download.** `configure` / `configure.win` fetch the
prebuilt `wickra-c-<triple>.tar.gz` from the matching GitHub release. A freshly
cut release can briefly return 404 while its assets propagate across the CDN
(and a transient network blip would also fail it), so the single-shot download
is now retried with a backoff (~2 min) before giving up. Fixes
`cannot open URL … 404 Not Found` on r-universe / source installs taken right
after a release.
## [0.9.4] - 2026-06-17
Packaging fix for the `0.9.3` data layer. The library is identical to `0.9.3` on
every platform that already published; the only additions are an opt-in
`vendored-tls` build feature and the Linux Python wheels, which `0.9.3` could not
build.
### Fixed
- **Linux Python wheels (`manylinux` / `musllinux`) now build.** The `live-binance`
data layer links `native-tls` -> `openssl-sys`, which needs OpenSSL at build
time. The `manylinux` wheel containers ship no OpenSSL headers and the
`musllinux` build cross-compiles against a musl sysroot that has no OpenSSL at
all, so the wheels failed to compile. The Linux wheels are now built with a new
opt-in `vendored-tls` feature that compiles OpenSSL from source and links it
statically (no system OpenSSL required, on either libc). The native macOS and
Windows wheels were unaffected (Security.framework / SChannel). As a result
`0.9.3` shipped to crates.io, Maven Central, NuGet, and npm but not to PyPI;
PyPI publishes starting with `0.9.4`.
### Added
- **`vendored-tls` feature** on `wickra-data` (and the Python binding): builds the
`live-binance` TLS stack against a statically compiled OpenSSL. Off by default;
used by the release wheels and exercised on every PR by a `manylinux` /
`musllinux` container build-smoke CI job.
## [0.9.3] - 2026-06-17
### Changed
- **Python: zero third-party dependencies — NumPy is no longer required**
(breaking). `pip install wickra` now pulls nothing else. Batch inputs accept any
sequence or buffer of numbers (`array.array`, `memoryview`, a NumPy array, or a
plain `list`); single-output `batch(...)` now returns a stdlib `array.array('d')`
and multi-output indicators return a buffer-protocol `Matrix` (with `.shape`,
integer-row and `[i, j]` element access, `.tolist()`) instead of 1-D / 2-D NumPy
arrays. Both expose the buffer protocol, so `numpy.asarray(result)` still wraps a
1-D result zero-copy when NumPy is installed — it is now an optional extra
(`pip install wickra[numpy]`). Streaming `update(...)` is unchanged, and results
are numerically identical. Single-output `batch(...)` is slower than the previous
NumPy path — a stdlib `array.array` cannot take ownership of the Rust result, so
it is copied rather than moved — though absolute batch latency stays in the
low-millisecond range. The other nine languages were already dependency-free.
### Removed
- **Python: `numpy` runtime dependency** (see *Changed*). NumPy moves to the
optional `numpy`/`test`/`bench` extras.
### Fixed
- **Binance kline feed: add the missing `3d` and `1M` intervals.** The
`live-binance` `Interval` enum was missing three-day (`3d`) and one-month (`1M`)
candles, two of Binance's 16 supported kline intervals. Both are now selectable
and map to the correct wire-format strings.
### Added
- **Native historical Binance REST kline fetcher in 9 languages (data layer).**
`fetchBinanceKlines` (Node.js / Python `fetch_binance_klines` / Go
`FetchBinanceKlines` / C# `BinanceFeed.FetchKlines` / Java `BinanceFeed.fetchKlines`
/ R `fetch_binance_klines`; C / C++ call `wickra_binance_fetch_klines`) downloads
historical OHLCV candles straight from Binance's public REST endpoint — no
third-party HTTP/JSON client (`jackson`, `jsonlite`, `urllib`, …) needed. Pass a
symbol, interval, and limit (`1..=1000`) plus optional millisecond start/end
bounds; it blocks until the response arrives and returns the parsed candles. It
is built on `ureq` with native-tls, sharing the live feed's TLS stack, and is
covered by mock-HTTP-server tests. The historical counterpart to the live
`BinanceFeed`; WASM is excluded (browsers use the host `fetch`). Ships with the
C ABI's default `live-binance` feature.
- **Native live Binance kline feed in 9 languages (data layer).** `BinanceFeed`
streams live OHLCV candles straight from Binance's public WebSocket — no
third-party WebSocket client (`ws`, `websockets`, `gorilla/websocket`, …) in any
binding. Construct it with comma-separated symbols + an interval, then poll
`next(timeout)` for the next event (or `null`/`None` on timeout); the connection
reconnects transparently. Exposed natively (Node.js / Python — a blocking poll
that drives the tested async stream on a tokio runtime) and over the C ABI as Go
`Next()`, C# `Next()`, Java `next()`, and the R `binance_next()`; C / C++ call
`wickra_binance_connect` / `_next` / `_close` / `_free` directly. The connect →
read → reconnect pipeline is covered by the existing mock-WS-server tests. WASM
is excluded (a browser has no raw sockets; use the host `WebSocket`). The C ABI
ships the feed by default (`live-binance` feature); the wasm build drops it.
- **CSV candle reading in all 10 languages (data layer).** The `CandleReader`
parses a `timestamp,open,high,low,close,volume` CSV buffer (a leading UTF-8 BOM
and field whitespace are tolerated) into candles: construct it from a CSV string
and call `read()` for every candle in file order. Exposed natively (Node.js /
WASM `read(): Candle[]`, Python `read() -> list[tuple]`) and over the C ABI as Go
`Read() []Candle`, C# `Candle[] Read()`, Java `Candle[] read()`, and the R
`read()` S3 generic (an `n×6` matrix); C / C++ call `wickra_candle_reader_new` /
`_count` / `_read` directly. A cross-language golden
(`testdata/golden/data_csv*.csv`) pins the parsed candles identically across
every binding. This makes CSV backtest loading dependency-free in every binding.
- **Candle resampling in all 10 languages (data layer).** The `Resampler`
aggregates candles into a higher timeframe (e.g. 1m → 5m): `update(open, high,
low, close, volume, timestamp)` returns the completed higher-timeframe candle on
a bucket boundary (else `null`/`None`/`NA`), and `flush()` emits the final,
still-open candle. Exposed natively (Node.js / WASM / Python) and over the C ABI
(Go / C# / Java return `(Candle, bool)` / `Candle?` / `Candle`; R via `update()`
and a `flush()` S3 method; C / C++ directly). A cross-language golden
(`testdata/golden/data_resampled.csv`) pins the resampled stream identically.
- **Tick-to-candle aggregation in all 10 languages (data layer).** The
`TickAggregator` — roll trade ticks up into fixed-timeframe OHLCV candles, with
optional gap filling — is now exposed natively (Node.js / WASM `push(price,
size, ts): Candle[]`, Python `push(...) -> list[tuple]`) and over the C ABI as
Go `Push() []Candle`, C# `Candle[] Push()`, Java `Candle[] push()`, and the R
`push()` generic (an `n×6` matrix); C / C++ call the C ABI directly. The C ABI
uses a lossless two-step `wickra_tick_aggregator_push` / `_drain` so a single
push that gap-fills across many empty buckets never overflows a fixed buffer. A
new cross-language golden (`testdata/golden/data_*.csv`) pins the candle stream
identically across every binding. This is the first feature of a data layer that
makes the non-Rust bindings dependency-free for tick aggregation.
- **`name()` on every indicator in all 10 languages.** The canonical
`Indicator::name()` / `BarBuilder::name()` accessor is now exposed through every
binding — Node.js `name()`, WASM `name()`, Python `name()`, and the C ABI
`wickra_<ind>_name()` surfaced as Go `Name()`, C# `Name()`, Java `name()`, and
the R `name()` S3 generic (C/C++ call the C ABI directly). The returned string
is the core canonical name, which may differ from the registered class name
(e.g. `ChaikinMoneyFlow` reports `"CMF"`, `Donchian` reports
`"DonchianChannels"`). A new cross-language golden (`testdata/golden/names.json`)
pins this name for all 514 indicators identically across every binding.
## [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 +1893,13 @@ 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.7...HEAD
[0.9.7]: https://github.com/wickra-lib/wickra/compare/v0.9.6...v0.9.7
[0.9.6]: https://github.com/wickra-lib/wickra/compare/v0.9.5...v0.9.6
[0.9.5]: https://github.com/wickra-lib/wickra/compare/v0.9.4...v0.9.5
[0.9.4]: https://github.com/wickra-lib/wickra/compare/v0.9.3...v0.9.4
[0.9.3]: https://github.com/wickra-lib/wickra/compare/v0.9.2...v0.9.3
[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
+56 -293
View File
@@ -32,12 +32,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
[[package]]
name = "anyhow"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "approx"
version = "0.5.1"
@@ -64,6 +58,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -100,6 +100,12 @@ version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -398,12 +404,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "foreign-types"
version = "0.3.2"
@@ -489,23 +489,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi 5.3.0",
"r-efi",
"wasip2",
]
[[package]]
name = "getrandom"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"wasip2",
"wasip3",
]
[[package]]
name = "half"
version = "2.7.1"
@@ -517,15 +504,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "hashbrown"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [
"foldhash",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
@@ -636,12 +614,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "id-arena"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "idna"
version = "1.1.0"
@@ -670,9 +642,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown 0.17.1",
"serde",
"serde_core",
"hashbrown",
]
[[package]]
@@ -712,12 +682,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.186"
@@ -758,16 +722,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "matrixmultiply"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08"
dependencies = [
"autocfg",
"rawpointer",
]
[[package]]
name = "memchr"
version = "2.8.0"
@@ -869,21 +823,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "ndarray"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841"
dependencies = [
"matrixmultiply",
"num-complex",
"num-integer",
"num-traits",
"portable-atomic",
"portable-atomic-util",
"rawpointer",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -893,24 +832,6 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-integer"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
dependencies = [
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -943,22 +864,6 @@ dependencies = [
"syn",
]
[[package]]
name = "numpy"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5b15d63a5ff39e378daed0e1340d3a5964703ea9712eb09a0dc66fade996f4"
dependencies = [
"libc",
"ndarray",
"num-complex",
"num-integer",
"num-traits",
"pyo3",
"pyo3-build-config",
"rustc-hash",
]
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -1002,6 +907,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-src"
version = "300.5.4+3.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a507b3792995dae9b0df8a1c1e3771e8418b7c2d9f0baeba32e6fe8b06c7cb72"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.116"
@@ -1010,6 +924,7 @@ checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
@@ -1076,15 +991,6 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -1103,16 +1009,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "prettyplease"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
dependencies = [
"proc-macro2",
"syn",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
@@ -1228,12 +1124,6 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r-efi"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.4"
@@ -1260,7 +1150,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
"getrandom",
]
[[package]]
@@ -1272,12 +1162,6 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rawpointer"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
[[package]]
name = "rayon"
version = "1.12.0"
@@ -1327,12 +1211,6 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "rustc-hash"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustix"
version = "1.1.4"
@@ -1557,7 +1435,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"getrandom",
"once_cell",
"rustix",
"windows-sys",
@@ -1725,10 +1603,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
[[package]]
name = "unicode-xid"
version = "0.2.6"
name = "ureq"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d"
dependencies = [
"base64",
"log",
"native-tls",
"once_cell",
"url",
]
[[package]]
name = "url"
@@ -1791,16 +1676,7 @@ version = "1.0.3+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
dependencies = [
"wit-bindgen 0.57.1",
]
[[package]]
name = "wasip3"
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
"wit-bindgen",
]
[[package]]
@@ -1897,40 +1773,6 @@ version = "0.2.121"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4d8ae7ad5440360e9799dfd42857d126454a88441ddf72d288ef83fa47f527"
[[package]]
name = "wasm-encoder"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
dependencies = [
"leb128fmt",
"wasmparser",
]
[[package]]
name = "wasm-metadata"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
dependencies = [
"anyhow",
"indexmap",
"wasm-encoder",
"wasmparser",
]
[[package]]
name = "wasmparser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
"hashbrown 0.15.5",
"indexmap",
"semver",
]
[[package]]
name = "web-sys"
version = "0.3.98"
@@ -1943,7 +1785,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"approx",
"criterion",
@@ -1954,7 +1796,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"criterion",
"kand",
@@ -1966,14 +1808,16 @@ dependencies = [
[[package]]
name = "wickra-c"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"tokio",
"wickra-core",
"wickra-data",
]
[[package]]
name = "wickra-core"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"approx",
"proptest",
@@ -1983,24 +1827,26 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"approx",
"csv",
"futures-util",
"native-tls",
"serde",
"serde_json",
"tempfile",
"thiserror",
"tokio",
"tokio-tungstenite",
"ureq",
"url",
"wickra-core",
]
[[package]]
name = "wickra-examples"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"serde_json",
"tokio",
@@ -2010,26 +1856,30 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"napi",
"napi-build",
"napi-derive",
"tokio",
"wickra-core",
"wickra-data",
]
[[package]]
name = "wickra-python"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"numpy",
"bytemuck",
"pyo3",
"tokio",
"wickra-core",
"wickra-data",
]
[[package]]
name = "wickra-wasm"
version = "0.9.1"
version = "0.9.7"
dependencies = [
"console_error_panic_hook",
"js-sys",
@@ -2038,6 +1888,7 @@ dependencies = [
"wasm-bindgen",
"wasm-bindgen-test",
"wickra-core",
"wickra-data",
]
[[package]]
@@ -2095,100 +1946,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
dependencies = [
"anyhow",
"heck",
"wit-parser",
]
[[package]]
name = "wit-bindgen-rust"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
dependencies = [
"anyhow",
"heck",
"indexmap",
"prettyplease",
"syn",
"wasm-metadata",
"wit-bindgen-core",
"wit-component",
]
[[package]]
name = "wit-bindgen-rust-macro"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
dependencies = [
"anyhow",
"prettyplease",
"proc-macro2",
"quote",
"syn",
"wit-bindgen-core",
"wit-bindgen-rust",
]
[[package]]
name = "wit-component"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags",
"indexmap",
"log",
"serde",
"serde_derive",
"serde_json",
"wasm-encoder",
"wasm-metadata",
"wasmparser",
"wit-parser",
]
[[package]]
name = "wit-parser"
version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
dependencies = [
"anyhow",
"id-arena",
"indexmap",
"log",
"semver",
"serde",
"serde_derive",
"serde_json",
"unicode-xid",
"wasmparser",
]
[[package]]
name = "writeable"
version = "0.6.3"
+3 -2
View File
@@ -14,7 +14,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.9.1"
version = "0.9.7"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -26,7 +26,8 @@ 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.7" }
wickra-data = { path = "crates/wickra-data", version = "0.9.7" }
thiserror = "2"
rayon = "1.10"
+72 -24
View File
@@ -18,8 +18,9 @@
[![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.**
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies, zero third-party packages.**
Wickra is a multi-language technical-analysis library with a Rust core and
native bindings for Python, Node.js and WASM, plus a C ABI that C, C++,
@@ -28,13 +29,13 @@ state machine that updates in O(1) per new data point, so live trading bots and
historical backtests share the exact same implementation.
```python
import numpy as np
import wickra as ta
import wickra as ta # zero third-party deps — not even NumPy
# Batch: classic TA-Lib-style usage
prices = np.linspace(100, 200, 1000)
prices = [100.0 + i * 0.1 for i in range(1000)]
rsi = ta.RSI(14)
values = rsi.batch(prices) # numpy array, NaN during warmup
values = rsi.batch(prices) # array.array('d'), NaN during warmup
# np.asarray(values) wraps it zero-copy if you use NumPy
# Streaming: same indicator, fed tick by tick
rsi = ta.RSI(14)
@@ -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
@@ -97,8 +104,13 @@ times to get there.
- **Install in one line, anywhere.** `pip install wickra` / `npm install wickra`
precompiled wheels and binaries, **no C toolchain, none of TA-Lib's setup pain**.
macOS · Linux · Windows.
- **Batteries included.** Indicator chaining, a streaming OHLCV CSV reader, and a
live Binance kline feed ship in the box.
- **Batteries included — zero third-party deps, in every language.** A full native
data layer ships in the box: a CSV candle reader, a tick-to-candle aggregator, a
timeframe resampler, a live Binance WebSocket feed and a historical Binance REST
fetcher — in **all 10 languages**. Loading a CSV, rolling ticks into candles,
resampling and streaming live data needs **no foreign package** — no pandas, no
`csv-parse`, no `ws`/`websockets`, no `jackson`, no `jsonlite`, not even NumPy.
`pip install wickra` / `npm install wickra` / `go get` / … pulls **nothing else**.
- **Truly permissive.** **MIT OR Apache-2.0** — drop it straight into commercial
and closed-source work.
@@ -137,12 +149,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 stays high for most bindings;
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) | 380 | 498 |
| C / C++ | 365 | 358 |
| C# | 348 | 259 |
| Python | 31 | 46 |
| Java | 38 | 173 |
| Go | 23 | 394 |
| WASM | 21 | 169 |
| Node.js | 16 | 9 |
| R | 0.1 | 279 |
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 +191,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 |
@@ -248,12 +286,17 @@ chain.update(price);
## Live data sources
`wickra-data` (separate crate, opt-in) ships:
Wickra ships a complete, **native data layer** — exposed in **all 10 languages**,
pulling **zero third-party packages** (no pandas / `csv-parse` / `ws` / `jackson`
/ `jsonlite`). In Rust it lives in the `wickra-data` crate; every binding exposes
the same building blocks:
- A streaming OHLCV **CSV reader**.
- A **tick-to-candle aggregator** with arbitrary timeframes.
- A **candle resampler** for multi-timeframe analysis (1m → 5m → 1h on the fly).
- A **Binance Spot WebSocket** kline adapter (feature `live-binance`).
- A streaming OHLCV **CSV reader** (`CandleReader`).
- A **tick-to-candle aggregator** with arbitrary timeframes (`TickAggregator`).
- A **candle resampler** for multi-timeframe analysis (1m → 5m → 1h on the fly, `Resampler`).
- A live **Binance Spot WebSocket** kline feed (`BinanceFeed`, feature `live-binance`).
- A historical **Binance REST** kline fetcher (`fetch_binance_klines`) — native
HTTP + JSON, no third-party client.
```rust
use wickra::{Indicator, Rsi};
@@ -270,8 +313,9 @@ 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`.
Native live-feed and historical-fetch examples — using `wickra.BinanceFeed` and
`wickra.fetch_binance_klines`, **with no third-party HTTP/WebSocket client** — live
under `examples/python/` (and the matching directory for every other language).
## Project layout
@@ -294,8 +338,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 +425,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.7`
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.7 (latest) | :white_check_mark: |
| < 0.9.7 | :x: |
## Reporting a vulnerability
+14 -1
View File
@@ -48,8 +48,16 @@ float_cmp = "allow"
# package's wasm build (r-universe / webR) compiles this crate with
# --no-default-features to drop rayon; wickra-core falls back to its serial
# batch path, which is cfg-gated behind the same feature.
default = ["parallel"]
#
# `live-binance` is on by default too so the published C ABI ships the native
# Binance kline feed (tokio + TLS WebSocket) — the six C-ABI languages with no
# native WebSocket (C, C++, Go, R, plus Node/Python via their own bindings) get
# it without a third-party client. The wasm build drops it via
# --no-default-features (a browser has no raw TCP/TLS sockets; WASM users use the
# host `WebSocket`), exactly like rayon.
default = ["parallel", "live-binance"]
parallel = ["wickra-core/parallel"]
live-binance = ["wickra-data/live-binance", "dep:tokio"]
[dependencies]
# Direct path dep rather than `workspace = true`: a member-level
@@ -57,3 +65,8 @@ parallel = ["wickra-core/parallel"]
# does not set it, which would leave rayon in the wasm build. wickra-c is
# `publish = false`, so no version pin is needed (and none to keep in sync).
wickra-core = { path = "../../crates/wickra-core", default-features = false }
wickra-data = { path = "../../crates/wickra-data" }
# Only pulled in by `live-binance`: a single-thread runtime drives the async
# Binance feed behind the blocking C-ABI poll. The feed's own I/O features come
# transitively from wickra-data; this just needs the runtime + timeout.
tokio = { version = "1", features = ["rt", "net", "time"], optional = true }
+1 -1
View File
@@ -17,4 +17,4 @@ documentation = false
# discovered and emitted as forward-declared opaque structs. Their fields are
# never exposed — only `T *` handles cross the boundary.
parse_deps = true
include = ["wickra-core"]
include = ["wickra-core", "wickra-data"]
File diff suppressed because it is too large Load Diff
+10745 -5
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,53 @@
using System;
using Wickra;
using Xunit;
// The live Binance feed's connect → read → reconnect pipeline is covered
// deterministically by the Rust mock-WS-server tests in wickra-data. Here we
// only assert the binding's error paths, which need no network.
public class BinanceFeedTests
{
[Fact]
public void RejectsUnknownInterval()
{
Assert.Throws<ArgumentException>(() =>
new BinanceFeed("BTCUSDT", (BinanceInterval)99));
}
[Fact]
public void RejectsEmptySymbols()
{
Assert.Throws<ArgumentException>(() =>
new BinanceFeed("", BinanceInterval.OneMinute));
}
[Fact]
public void RejectsUnreachableEndpoint()
{
Assert.Throws<ArgumentException>(() =>
new BinanceFeed("BTCUSDT", BinanceInterval.OneMinute, "ws://127.0.0.1:1"));
}
// The REST fetcher's parse/HTTP success path is covered by the Rust
// mock-HTTP-server tests; here we only assert the binding's error paths.
[Fact]
public void FetchKlinesRejectsUnknownInterval()
{
Assert.Throws<ArgumentException>(() =>
BinanceFeed.FetchKlines("BTCUSDT", (BinanceInterval)99, 1));
}
[Fact]
public void FetchKlinesRejectsZeroLimit()
{
Assert.Throws<ArgumentException>(() =>
BinanceFeed.FetchKlines("BTCUSDT", BinanceInterval.OneHour, 0));
}
[Fact]
public void FetchKlinesSurfacesUnreachableEndpoint()
{
Assert.Throws<ArgumentException>(() =>
BinanceFeed.FetchKlines("BTCUSDT", BinanceInterval.OneHour, 1, baseUrl: "http://127.0.0.1:1"));
}
}
@@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using Wickra;
using Xunit;
// Cross-language data-layer parity: replay the shared golden tick stream through
// the TickAggregator and check the candles against the Rust reference, with and
// without gap filling.
public class DataLayerTests
{
private static string GoldenDir([System.Runtime.CompilerServices.CallerFilePath] string file = "") =>
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
private static double[][] Read(string name)
{
var lines = File.ReadAllLines(Path.Combine(GoldenDir(), name + ".csv"));
var rows = new List<double[]>();
for (var i = 1; i < lines.Length; i++)
{
if (lines[i].Length == 0)
{
continue;
}
rows.Add(Array.ConvertAll(lines[i].Split(','), s => double.Parse(s, CultureInfo.InvariantCulture)));
}
return rows.ToArray();
}
[Fact]
public void ResamplerMatchesGolden()
{
var input = Read("input"); // open,high,low,close,volume (timestamp = row index)
using var r = new Resampler(5);
var got = new List<double[]>();
for (var i = 0; i < input.Length; i++)
{
var c = r.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], i);
if (c.HasValue)
{
got.Add(new[] { c.Value.Open, c.Value.High, c.Value.Low, c.Value.Close, c.Value.Volume, (double)c.Value.Timestamp });
}
}
var f = r.Flush();
if (f.HasValue)
{
got.Add(new[] { f.Value.Open, f.Value.High, f.Value.Low, f.Value.Close, f.Value.Volume, (double)f.Value.Timestamp });
}
var want = Read("data_resampled");
Assert.Equal(want.Length, got.Count);
for (var i = 0; i < got.Count; i++)
{
for (var j = 0; j < 6; j++)
{
var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j]));
Assert.True(Math.Abs(got[i][j] - want[i][j]) <= tol, $"resample row {i} col {j}: {got[i][j]} vs {want[i][j]}");
}
}
}
[Fact]
public void CandleReaderMatchesGolden()
{
var csv = File.ReadAllText(Path.Combine(GoldenDir(), "data_csv.csv"));
using var reader = new CandleReader(csv);
var candles = reader.Read();
var want = Read("data_csv_candles");
Assert.Equal(want.Length, candles.Length);
for (var i = 0; i < candles.Length; i++)
{
var c = candles[i];
var got = new[] { c.Open, c.High, c.Low, c.Close, c.Volume, (double)c.Timestamp };
for (var j = 0; j < 6; j++)
{
var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j]));
Assert.True(Math.Abs(got[j] - want[i][j]) <= tol, $"candle reader row {i} col {j}: {got[j]} vs {want[i][j]}");
}
}
}
[Theory]
[InlineData(false, "data_candles")]
[InlineData(true, "data_candles_gap")]
public void TickAggregatorMatchesGolden(bool gapFill, string fixture)
{
var ticks = Read("data_ticks");
using var agg = new TickAggregator(1000, gapFill);
var got = new List<double[]>();
foreach (var t in ticks)
{
foreach (var c in agg.Push(t[0], t[1], (long)t[2]))
{
got.Add(new[] { c.Open, c.High, c.Low, c.Close, c.Volume, (double)c.Timestamp });
}
}
var want = Read(fixture);
Assert.Equal(want.Length, got.Count);
for (var i = 0; i < got.Count; i++)
{
for (var j = 0; j < 6; j++)
{
var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j]));
Assert.True(
Math.Abs(got[i][j] - want[i][j]) <= tol,
$"{fixture} row {i} col {j}: {got[i][j]} vs {want[i][j]}");
}
}
}
}
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");
}
}
}
@@ -9,9 +9,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.6.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -11,7 +11,7 @@
<!-- NuGet package metadata -->
<PackageId>Wickra</PackageId>
<Version>0.9.1</Version>
<Version>0.9.7</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()
{
+304
View File
@@ -0,0 +1,304 @@
"""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()
# Canonical core Indicator::name() per indicator, shared across every binding.
NAMES = json.load(open(os.path.join(G, "names.json")))
# 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(f" Assert.Equal({json.dumps(NAMES[canon])}, ind.Name());")
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")
+35
View File
@@ -0,0 +1,35 @@
package wickra
import "testing"
// The live Binance feed's connect → read → reconnect pipeline is covered
// deterministically by the Rust mock-WS-server tests in wickra-data. Here we
// only assert the binding's error paths, which need no network: a bad interval,
// an empty symbol list, and an unreachable endpoint must all surface as an
// error rather than a usable handle.
func TestBinanceFeedRejectsBadParams(t *testing.T) {
if _, err := NewBinanceFeed("BTCUSDT", BinanceInterval(99), ""); err == nil {
t.Fatal("expected an error for an unknown interval code")
}
if _, err := NewBinanceFeed("", OneMinute, ""); err == nil {
t.Fatal("expected an error for an empty symbol list")
}
if _, err := NewBinanceFeed("BTCUSDT", OneMinute, "ws://127.0.0.1:1"); err == nil {
t.Fatal("expected an error connecting to an unreachable endpoint")
}
}
// The REST fetcher's parse/HTTP success path is covered by the Rust
// mock-HTTP-server tests in wickra-data; here we only assert the binding's
// error paths, which need no reachable network.
func TestFetchBinanceKlinesRejectsBadParams(t *testing.T) {
if _, err := FetchBinanceKlines("BTCUSDT", BinanceInterval(99), 1, -1, -1, ""); err == nil {
t.Fatal("expected an error for an unknown interval code")
}
if _, err := FetchBinanceKlines("BTCUSDT", OneHour, 0, -1, -1, ""); err == nil {
t.Fatal("expected an error for a zero limit")
}
if _, err := FetchBinanceKlines("BTCUSDT", OneHour, 1, -1, -1, "http://127.0.0.1:1"); err == nil {
t.Fatal("expected an error connecting to an unreachable endpoint")
}
}
+119
View File
@@ -0,0 +1,119 @@
package wickra
import (
"math"
"os"
"strconv"
"testing"
)
// Cross-language data-layer parity: replay the shared golden tick stream through
// the TickAggregator and assert the candles match the Rust reference, with and
// without gap filling. Fixtures are generated by
// `cargo run -p wickra-examples --bin gen_golden`.
func dataParseF(t *testing.T, s string) float64 {
t.Helper()
v, err := strconv.ParseFloat(s, 64)
if err != nil {
t.Fatalf("parse %q: %v", s, err)
}
return v
}
func TestResamplerGolden(t *testing.T) {
input := readGolden(t, "input") // open,high,low,close,volume (timestamp = row index)
r, err := NewResampler(5)
if err != nil {
t.Fatalf("new: %v", err)
}
var got [][6]float64
for i, row := range input {
o, h, l, c, v := dataParseF(t, row[0]), dataParseF(t, row[1]), dataParseF(t, row[2]), dataParseF(t, row[3]), dataParseF(t, row[4])
if k, ok := r.Update(o, h, l, c, v, int64(i)); ok {
got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)})
}
}
if k, ok := r.Flush(); ok {
got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)})
}
r.Close()
want := readGolden(t, "data_resampled")
if len(got) != len(want) {
t.Fatalf("resample: %d candles vs %d", len(got), len(want))
}
for i := range got {
for j := 0; j < 6; j++ {
w := dataParseF(t, want[i][j])
if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) {
t.Errorf("resample row %d col %d: %v vs %v", i, j, got[i][j], w)
}
}
}
}
func TestCandleReaderGolden(t *testing.T) {
csv, err := os.ReadFile("../../testdata/golden/data_csv.csv")
if err != nil {
t.Fatalf("read data_csv.csv: %v", err)
}
r, err := NewCandleReader(string(csv))
if err != nil {
t.Fatalf("new: %v", err)
}
defer r.Close()
candles := r.Read()
got := make([][6]float64, len(candles))
for i, c := range candles {
got[i] = [6]float64{c.Open, c.High, c.Low, c.Close, c.Volume, float64(c.Timestamp)}
}
want := readGolden(t, "data_csv_candles")
if len(got) != len(want) {
t.Fatalf("candle reader: %d candles vs %d", len(got), len(want))
}
for i := range got {
for j := 0; j < 6; j++ {
w := dataParseF(t, want[i][j])
if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) {
t.Errorf("candle reader row %d col %d: %v vs %v", i, j, got[i][j], w)
}
}
}
}
func TestTickAggregatorGolden(t *testing.T) {
ticks := readGolden(t, "data_ticks")
cases := []struct {
gap bool
fixture string
}{
{false, "data_candles"},
{true, "data_candles_gap"},
}
for _, c := range cases {
agg, err := NewTickAggregator(1000, c.gap)
if err != nil {
t.Fatalf("new: %v", err)
}
var got [][6]float64
for _, r := range ticks {
price, size, ts := dataParseF(t, r[0]), dataParseF(t, r[1]), int64(dataParseF(t, r[2]))
for _, k := range agg.Push(price, size, ts) {
got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)})
}
}
agg.Close()
want := readGolden(t, c.fixture)
if len(got) != len(want) {
t.Fatalf("%s: %d candles vs %d", c.fixture, len(got), len(want))
}
for i := range got {
for j := 0; j < 6; j++ {
w := dataParseF(t, want[i][j])
if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) {
t.Errorf("%s row %d col %d: %v vs %v", c.fixture, i, j, got[i][j], w)
}
}
}
}
}
+343
View File
@@ -0,0 +1,343 @@
"""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()
# Canonical core Indicator::name() per indicator, shared across every binding.
NAMES = json.load(open(os.path.join(G, "names.json")))
# 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(f'\t\tif n := ind.Name(); n != {json.dumps(NAMES[canon])} {{')
lines.append(f'\t\t\tt.Errorf("name: got %q want %q", n, {json.dumps(NAMES[canon])})')
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")
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4
View File
@@ -28,3 +28,7 @@ import "errors"
// ErrInvalidParams is returned by a New<Indicator> constructor when the native
// constructor rejects the supplied parameters (for example a zero period).
var ErrInvalidParams = errors.New("wickra: invalid indicator parameters")
// ErrFeedClosed is returned by BinanceFeed.Next once the stream has been closed
// or has errored out and exhausted its reconnect attempts.
var ErrFeedClosed = errors.New("wickra: binance feed closed")
+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.7</version>
</dependency>
```
Gradle:
```kotlin
implementation("org.wickra:wickra:0.9.1")
implementation("org.wickra:wickra:0.9.7")
```
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
+1 -1
View File
@@ -21,7 +21,7 @@
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.2</version>
<version>0.8.8</version>
</dependency>
</dependencies>
+293
View File
@@ -0,0 +1,293 @@
"""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)
# Canonical core Indicator::name() per indicator, shared across every binding.
NAMES = json.load(open(os.path.join(ROOT, "testdata", "golden", "names.json")))
names_block = "\n".join(
f" NAMES.put({json.dumps(c)}, {json.dumps(NAMES[c])});" for c in sorted(NAMES)
)
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 final java.util.Map<String, String> NAMES = new java.util.HashMap<>();
static {
%NAMES%
}
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);
String gotName = (String) ind.getClass().getMethod("name").invoke(ind);
assertTrue(gotName.equals(NAMES.get(s.canonical())),
s.canonical() + ": name() " + gotName + " != " + NAMES.get(s.canonical()));
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).replace("%NAMES%", names_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.7</version>
<packaging>jar</packaging>
<name>Wickra</name>
@@ -91,6 +91,16 @@ public final class AbandonedBaby implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ABANDONED_BABY_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class Abcd implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ABCD_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -77,6 +77,16 @@ public final class AbsoluteBreadthIndex implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -67,6 +67,16 @@ public final class AccelerationBands implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ACCELERATION_BANDS_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -100,6 +100,16 @@ public final class AcceleratorOscillator implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class AdOscillator implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AD_OSCILLATOR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -77,6 +77,16 @@ public final class AdVolumeLine implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AD_VOLUME_LINE_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class AdaptiveCci implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_CCI_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -71,6 +71,16 @@ public final class AdaptiveCycle implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_CYCLE_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class AdaptiveLaguerreFilter implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class AdaptiveRsi implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_RSI_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class Adl implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADL_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class AdvanceBlock implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADVANCE_BLOCK_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -77,6 +77,16 @@ public final class AdvanceDecline implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADVANCE_DECLINE_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -77,6 +77,16 @@ public final class AdvanceDeclineRatio implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADVANCE_DECLINE_RATIO_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -67,6 +67,16 @@ public final class Adx implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADX_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class Adxr implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ADXR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -73,6 +73,16 @@ public final class Alligator implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ALLIGATOR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class Alma implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ALMA_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -78,6 +78,16 @@ public final class Alpha implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ALPHA_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -59,6 +59,16 @@ public final class AmihudIlliquidity implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AMIHUD_ILLIQUIDITY_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -71,6 +71,16 @@ public final class AnchoredRsi implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ANCHORED_RSI_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class AnchoredVwap implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ANCHORED_VWAP_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -67,6 +67,16 @@ public final class AndrewsPitchfork implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ANDREWS_PITCHFORK_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -77,6 +77,16 @@ public final class Apo implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_APO_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -66,6 +66,16 @@ public final class Aroon implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AROON_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class AroonOscillator implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AROON_OSCILLATOR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class Atr implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ATR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -67,6 +67,16 @@ public final class AtrBands implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ATR_BANDS_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -66,6 +66,16 @@ public final class AtrRatchet implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ATR_RATCHET_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class AtrTrailingStop implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_ATR_TRAILING_STOP_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -68,6 +68,16 @@ public final class AutoFib implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AUTO_FIB_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -77,6 +77,16 @@ public final class Autocorrelation implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AUTOCORRELATION_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -77,6 +77,16 @@ public final class AutocorrelationPeriodogram implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AUTOCORRELATION_PERIODOGRAM_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class AverageDailyRange implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AVERAGE_DAILY_RANGE_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class AverageDrawdown implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AVERAGE_DRAWDOWN_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class AvgPrice implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AVG_PRICE_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -97,6 +97,16 @@ public final class AwesomeOscillator implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AWESOME_OSCILLATOR_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -13,19 +13,19 @@ public final class AwesomeOscillatorHistogram implements AutoCloseable {
private final MemorySegment handle;
private final Cleaner.Cleanable cleanable;
public AwesomeOscillatorHistogram(int fast, int slow, int smaPeriod) {
public AwesomeOscillatorHistogram(int fast, int slow, int lookback) {
if (fast < 0) {
throw new IllegalArgumentException("fast must be non-negative");
}
if (slow < 0) {
throw new IllegalArgumentException("slow must be non-negative");
}
if (smaPeriod < 0) {
throw new IllegalArgumentException("smaPeriod must be non-negative");
if (lookback < 0) {
throw new IllegalArgumentException("lookback must be non-negative");
}
MemorySegment h;
try {
h = (MemorySegment) NativeMethods.WICKRA_AWESOME_OSCILLATOR_HISTOGRAM_NEW.invokeExact((long) fast, (long) slow, (long) smaPeriod);
h = (MemorySegment) NativeMethods.WICKRA_AWESOME_OSCILLATOR_HISTOGRAM_NEW.invokeExact((long) fast, (long) slow, (long) lookback);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
@@ -100,6 +100,16 @@ public final class AwesomeOscillatorHistogram implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_AWESOME_OSCILLATOR_HISTOGRAM_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class BalanceOfPower implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BALANCE_OF_POWER_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class BandpassFilter implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BANDPASS_FILTER_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class Bat implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BAT_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class BeltHold implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BELT_HOLD_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -78,6 +78,16 @@ public final class Beta implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BETA_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -78,6 +78,16 @@ public final class BetaNeutralSpread implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BETA_NEUTRAL_SPREAD_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -94,6 +94,16 @@ public final class BetterVolume implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BETTER_VOLUME_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -0,0 +1,137 @@
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
package org.wickra;
import org.wickra.internal.NativeMethods;
import org.wickra.internal.WickraNative;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.ref.Cleaner;
import java.nio.charset.StandardCharsets;
import static java.lang.foreign.ValueLayout.*;
/** A live Binance kline stream over the Wickra C ABI. Not thread-safe; close when done. */
public final class BinanceFeed implements AutoCloseable {
private final MemorySegment handle;
private final Cleaner.Cleanable cleanable;
/** Connect to Binance's live kline stream for the given comma-separated
* symbols (case-insensitive) at interval. baseUrl overrides the endpoint
* (null = production; pass a ws:// URL to target a test server). */
public BinanceFeed(String symbols, BinanceInterval interval, String baseUrl) {
if (symbols == null) {
throw new NullPointerException("symbols");
}
MemorySegment h;
try (Arena a = Arena.ofConfined()) {
byte[] sb = symbols.getBytes(StandardCharsets.UTF_8);
MemorySegment sym = a.allocate(sb.length + 1L);
MemorySegment.copy(sb, 0, sym, JAVA_BYTE, 0L, sb.length);
MemorySegment url = MemorySegment.NULL;
if (baseUrl != null) {
byte[] ub = baseUrl.getBytes(StandardCharsets.UTF_8);
url = a.allocate(ub.length + 1L);
MemorySegment.copy(ub, 0, url, JAVA_BYTE, 0L, ub.length);
}
h = (MemorySegment) NativeMethods.WICKRA_BINANCE_CONNECT.invokeExact(
sym, (byte) interval.ordinal(), url);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
if (h.address() == 0L) {
throw new IllegalArgumentException("invalid BinanceFeed parameters");
}
this.handle = h;
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_BINANCE_FREE);
}
/** Connect with the production endpoint. */
public BinanceFeed(String symbols, BinanceInterval interval) {
this(symbols, interval, null);
}
/** Poll for the next kline event, waiting up to timeoutMillis. Returns the
* event, or null on timeout (call again). Throws once the stream is closed. */
public KlineEvent next(long timeoutMillis) {
try (Arena a = Arena.ofConfined()) {
MemorySegment out = a.allocate(72L);
int code = (int) NativeMethods.WICKRA_BINANCE_NEXT.invokeExact(handle, out, timeoutMillis);
if (code == 0) {
return null;
}
if (code != 1) {
throw new IllegalStateException("binance feed closed");
}
byte[] symBytes = new byte[16];
MemorySegment.copy(out, JAVA_BYTE, 0L, symBytes, 0, 16);
int n = 0;
while (n < 16 && symBytes[n] != 0) {
n++;
}
return new KlineEvent(
new String(symBytes, 0, n, StandardCharsets.UTF_8),
out.get(JAVA_DOUBLE, 16L),
out.get(JAVA_DOUBLE, 24L),
out.get(JAVA_DOUBLE, 32L),
out.get(JAVA_DOUBLE, 40L),
out.get(JAVA_DOUBLE, 48L),
out.get(JAVA_LONG, 56L),
out.get(JAVA_BYTE, 64L) != 0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Fetch historical klines from Binance's REST endpoint. symbol is the
* trading pair (case-insensitive), limit the number of candles (1..=1000).
* startMs/endMs are inclusive Unix-millisecond bounds (negative = unset);
* baseUrl overrides the host (null = production). Blocks until done. */
public static Candle[] fetchKlines(String symbol, BinanceInterval interval, int limit,
long startMs, long endMs, String baseUrl) {
if (symbol == null) {
throw new NullPointerException("symbol");
}
if (limit <= 0) {
throw new IllegalArgumentException("limit must be in 1..=1000");
}
try (Arena a = Arena.ofConfined()) {
byte[] sb = symbol.getBytes(StandardCharsets.UTF_8);
MemorySegment sym = a.allocate(sb.length + 1L);
MemorySegment.copy(sb, 0, sym, JAVA_BYTE, 0L, sb.length);
MemorySegment url = MemorySegment.NULL;
if (baseUrl != null) {
byte[] ub = baseUrl.getBytes(StandardCharsets.UTF_8);
url = a.allocate(ub.length + 1L);
MemorySegment.copy(ub, 0, url, JAVA_BYTE, 0L, ub.length);
}
MemorySegment out = a.allocate(48L * limit);
long n = (long) NativeMethods.WICKRA_BINANCE_FETCH_KLINES.invokeExact(
sym, (byte) interval.ordinal(), limit, startMs, endMs, url, out, (long) limit);
if (n < 0) {
throw new IllegalArgumentException("invalid fetchKlines parameters or transport error");
}
Candle[] result = new Candle[(int) n];
for (int i = 0; i < n; i++) {
long b = (long) i * 48L;
result[i] = new Candle(
out.get(JAVA_DOUBLE, b + 0L),
out.get(JAVA_DOUBLE, b + 8L),
out.get(JAVA_DOUBLE, b + 16L),
out.get(JAVA_DOUBLE, b + 24L),
out.get(JAVA_DOUBLE, b + 32L),
(double) out.get(JAVA_LONG, b + 40L));
}
return result;
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
@Override public void close() {
try {
NativeMethods.WICKRA_BINANCE_CLOSE.invokeExact(handle);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
cleanable.clean();
}
}
@@ -0,0 +1,22 @@
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
package org.wickra;
/** Kline interval for the live Binance feed (ordinal = the C ABI code). */
public enum BinanceInterval {
ONE_SECOND,
ONE_MINUTE,
THREE_MINUTES,
FIVE_MINUTES,
FIFTEEN_MINUTES,
THIRTY_MINUTES,
ONE_HOUR,
TWO_HOURS,
FOUR_HOURS,
SIX_HOURS,
EIGHT_HOURS,
TWELVE_HOURS,
ONE_DAY,
THREE_DAYS,
ONE_WEEK,
ONE_MONTH
}
@@ -74,6 +74,16 @@ public final class BipowerVariation implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BIPOWER_VARIATION_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class BodySizePct implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BODY_SIZE_PCT_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -68,6 +68,16 @@ public final class BollingerBands implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BOLLINGER_BANDS_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class BollingerBandwidth implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BOLLINGER_BANDWIDTH_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -67,6 +67,16 @@ public final class BomarBands implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BOMAR_BANDS_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -80,6 +80,16 @@ public final class BreadthThrust implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BREADTH_THRUST_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class Breakaway implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BREAKAWAY_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -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);
@@ -77,6 +77,16 @@ public final class BullishPercentIndex implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BULLISH_PERCENT_INDEX_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class BurkeRatio implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BURKE_RATIO_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -91,6 +91,16 @@ public final class Butterfly implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_BUTTERFLY_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -56,6 +56,16 @@ public final class CalendarSpread implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_CALENDAR_SPREAD_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {
@@ -74,6 +74,16 @@ public final class CalmarRatio implements AutoCloseable {
}
}
/** The indicator's canonical name. */
public String name() {
try {
MemorySegment s = (MemorySegment) NativeMethods.WICKRA_CALMAR_RATIO_NAME.invokeExact(handle);
return s.address() == 0 ? "" : s.reinterpret(Long.MAX_VALUE).getString(0);
} catch (Throwable t) {
throw WickraNative.rethrow(t);
}
}
/** Reset to the just-constructed state. */
public void reset() {
try {

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