Compare commits
176 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5f5e14dda | |||
| 7e5d394f2a | |||
| ff268500ef | |||
| 929fc17127 | |||
| 41d5a7dd25 | |||
| e595ea8bfe | |||
| a5fe2e71c4 | |||
| 75eefbbd08 | |||
| 2e07c07a40 | |||
| ec937b8281 | |||
| 1f5eb90b0d | |||
| bbda70f75b | |||
| 2d2d5970b4 | |||
| 8228be7069 | |||
| 677ea37402 | |||
| 2ae76bb90e | |||
| b92ad32037 | |||
| 3a709d9a66 | |||
| baf4d0ff47 | |||
| d362ae26a3 | |||
| cb6da4d737 | |||
| 8a103ef920 | |||
| fd9f4c8bc6 | |||
| 59acefa1ea | |||
| 5f0677d62d | |||
| ce792cf8b8 | |||
| 120b6ac265 | |||
| 4f708d410d | |||
| de1112ea91 | |||
| 82d7479011 | |||
| a3950bf31b | |||
| a0a7bc4e62 | |||
| 0c925aa9d5 | |||
| d9e6807ca0 | |||
| eb50ae4e90 | |||
| 4a12f60a88 | |||
| 8003572d92 | |||
| db5bd5e028 | |||
| 2ef60874b9 | |||
| d59cd44043 | |||
| ee5ee6980e | |||
| 60b4705d7e | |||
| 08b2e5abd3 | |||
| 495d9edcc3 | |||
| c32b802461 | |||
| 7e3be0d1ac | |||
| 1a90de89ea | |||
| bb901fbd25 | |||
| 692473452f | |||
| 2a5b012068 | |||
| 513e1111d2 | |||
| 4b0bdef7c6 | |||
| 3e4b6c7e22 | |||
| dd6a4affb2 | |||
| e5e094d370 | |||
| 97940046d1 | |||
| f7f0bfbc48 | |||
| 8ccfd638b2 | |||
| 452f270b4d | |||
| baffebd3da | |||
| e631e55195 | |||
| 63949f6fc8 | |||
| 92210eb7b8 | |||
| c3029f2548 | |||
| 806ae22abe | |||
| fb9c39d4cd | |||
| b1653e2107 | |||
| 32e18cb3a3 | |||
| dc0c3d1736 | |||
| ad1231cde1 | |||
| 7ebb60b60a | |||
| d7cb771a28 | |||
| 3e5a19c94a | |||
| 8bfe24ac1d | |||
| 395a2289f4 | |||
| 9973d1a6bf | |||
| cb216668ee | |||
| 20c0002f8e | |||
| 99497eb062 | |||
| 7e51f31f02 | |||
| 3ee3fb67ec | |||
| 3ebcb3f758 | |||
| b31a9a3624 | |||
| 6433c9b3de | |||
| 447bbc8220 | |||
| c9ef2fc037 | |||
| 49c2872ad4 | |||
| 0c4bbaf314 | |||
| 99ad2ab107 | |||
| 8e225a5872 | |||
| 87bb008aa1 | |||
| b06ce2678a | |||
| 5b7523265c | |||
| d188959aab | |||
| 167f7b3ffe | |||
| 8659b42bef | |||
| b7ef63400d | |||
| 8225e1ab91 | |||
| d0061c73b8 | |||
| 23d636fd97 | |||
| fce26cf881 | |||
| 91f6f67257 | |||
| 4caaa1db97 | |||
| 12681e4b1b | |||
| 91e05e3c26 | |||
| 9d0983b666 | |||
| 13c8250488 | |||
| e5305ffa94 | |||
| 46be7a54ea | |||
| bca61322b5 | |||
| fc6f619550 | |||
| 91aa6fffbf | |||
| 5862401958 | |||
| ff5a047078 | |||
| dc415a77fd | |||
| e385734275 | |||
| 3a46b210bb | |||
| 943825d6a0 | |||
| d16df1e224 | |||
| 34c097aee2 | |||
| acd7e8dc52 | |||
| ceaeb90a22 | |||
| 57e26fb22f | |||
| 8431b1400c | |||
| ed01604a18 | |||
| 05fe7ffa90 | |||
| e97c3389fe | |||
| 4526278fa0 | |||
| 80850c81f7 | |||
| 707f29e8e4 | |||
| 389200f855 | |||
| 81406e7a1b | |||
| c78b84e186 | |||
| fc6f3d80c2 | |||
| 2991ba411d | |||
| 83e34c6f71 | |||
| 67feec598a | |||
| 3dfbc415c5 | |||
| 6b8c6a0e7f | |||
| db186b18d3 | |||
| 654da5722f | |||
| aacb9280f1 | |||
| d2bc000892 | |||
| 1f4bf9e3a6 | |||
| d36d514f56 | |||
| 6e0464930e | |||
| 13bc801f89 | |||
| ac8f6acf08 | |||
| 4f81222aed | |||
| 0d2acad28d | |||
| b228a70d7d | |||
| 8dc7158912 | |||
| fcb221ec03 | |||
| a93af60796 | |||
| 5a1d607807 | |||
| ea9da12d86 | |||
| 716eb40206 | |||
| 8115d3b33d | |||
| 4250ed99f4 | |||
| 995f119010 | |||
| 05d2e5dc61 | |||
| 404bcb040c | |||
| 00ce899cc3 | |||
| b6ead740e8 | |||
| 755f4aa0f6 | |||
| 4d602df8a3 | |||
| 3ab2d6ec2d | |||
| 5e96d41916 | |||
| 099ae66b57 | |||
| 11dd659b5f | |||
| c096943bdf | |||
| c44f625e69 | |||
| 46dc8f5a00 | |||
| a3a1ae4dba | |||
| 53941b7b07 | |||
| 72ec65bbde |
@@ -1,3 +1,41 @@
|
||||
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and
|
||||
# local shells regardless of the committer's platform autocrlf setting.
|
||||
*.sh text eol=lf
|
||||
|
||||
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
|
||||
# check (regenerate + `git diff`) never trips on a CRLF normalization.
|
||||
bindings/c/include/wickra.h text eol=lf
|
||||
|
||||
# C# sources (including the generated binding) are pinned to LF so the committed
|
||||
# files stay stable regardless of the committer's autocrlf setting.
|
||||
*.cs text eol=lf
|
||||
|
||||
# Go sources (including the generated binding) are pinned to LF so gofmt's CI
|
||||
# check never trips on a CRLF checkout on Windows. The vendored C ABI header is
|
||||
# a committed copy of bindings/c/include/wickra.h (the parent dir is outside the
|
||||
# Go module), pinned to LF so the drift check never trips on CRLF.
|
||||
*.go text eol=lf
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
bindings/go/include/wickra.h text eol=lf
|
||||
|
||||
# Java sources (including the generated binding) are pinned to LF so the
|
||||
# committed files stay stable regardless of the committer's autocrlf setting.
|
||||
*.java text eol=lf
|
||||
|
||||
# R binding: `R CMD check` requires LF in sources, Makevars and shell scripts.
|
||||
*.R text eol=lf
|
||||
*.Rd text eol=lf
|
||||
bindings/r/configure text eol=lf
|
||||
bindings/r/configure.win text eol=lf
|
||||
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
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ assignees: ""
|
||||
## Environment
|
||||
|
||||
- Wickra version:
|
||||
- Language / binding: <!-- Rust crate / Python / Node / WASM -->
|
||||
- Language / binding: <!-- Rust crate / Python / Node.js / WASM / C / C++ / C# / Go / Java / R -->
|
||||
- OS and architecture:
|
||||
- Rust / Python / Node version (If relevant):
|
||||
- Rust / Python / Node.js / .NET version (If relevant):
|
||||
|
||||
## Additional context
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ assignees: []
|
||||
- [ ] Rust crate (`wickra`)
|
||||
- [ ] Python (`pip install wickra`)
|
||||
- [ ] Node.js (`npm install wickra`)
|
||||
- [ ] WebAssembly
|
||||
- [ ] WASM
|
||||
- [ ] C ABI (`bindings/c`)
|
||||
- [ ] C# (`Wickra` on NuGet)
|
||||
- [ ] Go (`bindings/go`)
|
||||
- [ ] Java (`org.wickra:wickra` on Maven Central)
|
||||
- [ ] R (`bindings/r`)
|
||||
- [ ] Docs / examples only
|
||||
|
||||
## Environment
|
||||
@@ -26,7 +31,7 @@ assignees: []
|
||||
| Binding version | `e.g. python 0.4.2 / node 0.4.2` |
|
||||
| OS / arch | `e.g. Windows 11 x86_64, Linux glibc` |
|
||||
| Rust toolchain | `rustc --version` (If building from source) |
|
||||
| Python / Node version | `python --version` / `node --version` |
|
||||
| Python / Node.js / .NET version | `python --version` / `node --version` / `dotnet --version` |
|
||||
|
||||
## Minimal reproducer
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ assignees: ""
|
||||
- [ ] Should be exposed in the Python binding
|
||||
- [ ] Should be exposed in the Node binding
|
||||
- [ ] Should be exposed in the WASM binding
|
||||
- [ ] Should be exposed in the C ABI
|
||||
- [ ] Should be exposed in the C# binding
|
||||
- [ ] Should be exposed in the Go binding
|
||||
- [ ] Should be exposed in the Java binding
|
||||
- [ ] Should be exposed in the R binding
|
||||
|
||||
## Additional context
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ assignees: []
|
||||
## Affected code path
|
||||
|
||||
- Indicator / API: `e.g. EMA.update`
|
||||
- Binding: `Rust / Python / Node / Wasm`
|
||||
- Binding: `Rust / Python / Node.js / WASM / C / C++ / C# / Go / Java / R`
|
||||
- Hot loop or one-shot call?
|
||||
|
||||
## Versions compared
|
||||
|
||||
@@ -33,4 +33,4 @@ import wickra as ta
|
||||
## Environment (Only if relevant)
|
||||
|
||||
- Wickra version: `e.g. 0.4.2`
|
||||
- Binding: `Rust / Python / Node / Wasm`
|
||||
- Binding: `Rust / Python / Node.js / WASM / C / C++ / C# / Go / Java / R`
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean.
|
||||
- [ ] `cargo test --workspace` passes.
|
||||
- [ ] New behaviour has tests; bug fixes have a regression test.
|
||||
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings
|
||||
- [ ] Public API changes are mirrored in the Python / Node.js / WASM bindings, and the C ABI + C# + Go + Java + R bindings are regenerated
|
||||
and their type stubs (If applicable).
|
||||
- [ ] The relevant page on the [documentation site](https://docs.wickra.org)
|
||||
and the `README.md` are updated (If applicable). Docs edits go to a
|
||||
|
||||
@@ -22,7 +22,12 @@ Please fill in the sections below. Delete any that don't apply.
|
||||
- [ ] Rust crate (`crates/wickra`)
|
||||
- [ ] Python binding (`bindings/python`)
|
||||
- [ ] Node.js binding (`bindings/node`)
|
||||
- [ ] WebAssembly binding (`bindings/wasm`)
|
||||
- [ ] WASM binding (`bindings/wasm`)
|
||||
- [ ] C ABI (`bindings/c`)
|
||||
- [ ] C# binding (`bindings/csharp`)
|
||||
- [ ] Go binding (`bindings/go`)
|
||||
- [ ] Java binding (`bindings/java`)
|
||||
- [ ] R binding (`bindings/r`)
|
||||
- [ ] Examples / docs
|
||||
|
||||
## Linked issues
|
||||
@@ -60,7 +65,7 @@ Closes #
|
||||
- [ ] Public API changes are reflected in `CHANGELOG.md`
|
||||
- [ ] Public API changes are reflected in rustdoc / README / examples
|
||||
- [ ] No `todo*.md` or other local-only notes are staged
|
||||
- [ ] License header / `LICENSE` reference unchanged (PolyForm-NC-1.0.0)
|
||||
- [ ] License header / `LICENSE` reference unchanged (MIT OR Apache-2.0)
|
||||
|
||||
## Notes for reviewers
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
@@ -10,6 +10,9 @@ updates:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(cargo)"
|
||||
groups:
|
||||
cargo:
|
||||
patterns: ["*"]
|
||||
|
||||
# Node binding npm dependencies.
|
||||
- package-ecosystem: npm
|
||||
@@ -21,6 +24,9 @@ updates:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(npm)"
|
||||
groups:
|
||||
node-binding:
|
||||
patterns: ["*"]
|
||||
|
||||
# Python binding pip dependencies.
|
||||
- package-ecosystem: pip
|
||||
@@ -32,6 +38,9 @@ updates:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(pip)"
|
||||
groups:
|
||||
python-binding:
|
||||
patterns: ["*"]
|
||||
|
||||
# Hash-pinned CI/bench Python tooling under .github/requirements/. Each
|
||||
# <name>.in is the loose source; the matching hash-locked <name>.txt is the
|
||||
@@ -47,6 +56,9 @@ updates:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(ci-pip)"
|
||||
groups:
|
||||
ci-pip:
|
||||
patterns: ["*"]
|
||||
|
||||
# GitHub Actions — keeps the SHA-pinned actions current (Dependabot reads
|
||||
# the version comment after each pinned SHA and bumps both together).
|
||||
@@ -59,3 +71,73 @@ updates:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(actions)"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns: ["*"]
|
||||
|
||||
# Java binding + examples (Maven). Tracks the C-ABI binding's build plugins
|
||||
# (e.g. central-publishing-maven-plugin) and the examples' jackson dependency,
|
||||
# which had no Dependabot coverage before — a stale publishing plugin slipped
|
||||
# through unnoticed until an OSV scan flagged it.
|
||||
- package-ecosystem: maven
|
||||
directories:
|
||||
- "/bindings/java"
|
||||
- "/bindings/java/benchmarks"
|
||||
- "/examples/java"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(maven)"
|
||||
groups:
|
||||
maven:
|
||||
patterns: ["*"]
|
||||
|
||||
# C# binding (NuGet). The published Wickra.csproj is a thin C-ABI wrapper with
|
||||
# no external packages, but the test and benchmark projects pull xunit,
|
||||
# Microsoft.NET.Test.Sdk and BenchmarkDotNet.
|
||||
- package-ecosystem: nuget
|
||||
directories:
|
||||
- "/bindings/csharp/Wickra.Tests"
|
||||
- "/bindings/csharp/benchmarks"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(nuget)"
|
||||
groups:
|
||||
nuget:
|
||||
patterns: ["*"]
|
||||
|
||||
# Node examples (npm) — separate from the binding's own package.json.
|
||||
- package-ecosystem: npm
|
||||
directory: "/examples/node"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(npm)"
|
||||
groups:
|
||||
node-examples:
|
||||
patterns: ["*"]
|
||||
|
||||
# Go examples (Go modules). The binding's own go.mod has no external deps;
|
||||
# the examples pull coder/websocket.
|
||||
- package-ecosystem: gomod
|
||||
directory: "/examples/go"
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 10
|
||||
cooldown:
|
||||
default-days: 7
|
||||
commit-message:
|
||||
prefix: "deps(gomod)"
|
||||
groups:
|
||||
go-examples:
|
||||
patterns: ["*"]
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
maturin
|
||||
numpy
|
||||
pandas
|
||||
TA-Lib
|
||||
tulipy
|
||||
talipp
|
||||
finta
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# ./scripts/update-lockfiles.sh
|
||||
build==1.5.0 \
|
||||
--hash=sha256:13f3eecb844759ab66efec90ca17639bbf14dc06cb2fdf37a9010322d9c50a6f \
|
||||
--hash=sha256:302c22c3ba2a0fd5f3911918651341ebb3896176cbdec15bd421f80b1afc7647
|
||||
# via ta-lib
|
||||
colorama==0.4.6 \
|
||||
--hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
|
||||
--hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
|
||||
# via build
|
||||
finta==1.3 \
|
||||
--hash=sha256:b94b94df311c18bf5402eb2fe8fd2db5e1bdaff08baf58a7367d05c7abdd10d3 \
|
||||
--hash=sha256:f2fa0673748f4be8f57e57cf6d5c00a4d44bc6071ea69dbb9a1d329d045cbba2
|
||||
@@ -97,6 +105,12 @@ numpy==2.4.6 \
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
# pandas
|
||||
# ta-lib
|
||||
# tulipy
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via build
|
||||
pandas==3.0.3 \
|
||||
--hash=sha256:0383c72c75cdcca61a9e116e611143902dbfd08bff356829c2f6d1cf40a9ca8c \
|
||||
--hash=sha256:05f1f1752b8533ea03f7f39a9c15b1a058d067bb48f4748948e7a8691e0510f2 \
|
||||
@@ -149,6 +163,10 @@ pandas==3.0.3 \
|
||||
# via
|
||||
# -r .github/requirements/bench.in
|
||||
# finta
|
||||
pyproject-hooks==1.2.0 \
|
||||
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||
# via build
|
||||
python-dateutil==2.9.0.post0 \
|
||||
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
|
||||
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
|
||||
@@ -157,10 +175,72 @@ six==1.17.0 \
|
||||
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
|
||||
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
|
||||
# via python-dateutil
|
||||
ta-lib==0.6.8 \
|
||||
--hash=sha256:02388054c059945e5f02625f5075bac20a1803573cb43e7d096091027511961f \
|
||||
--hash=sha256:094677b279a59c3f01c3aca8a889fda3523fd641a3805f69a2d642121b72e55e \
|
||||
--hash=sha256:0a08a29690a922ba92a6cf42902a8a93c6fbda4cfed62c3c5b0471560ef60135 \
|
||||
--hash=sha256:0ccd478ff5735831bf2a61d653466bfda8afadc26ad58ca6b1edb9e7521cc674 \
|
||||
--hash=sha256:0e371d14b49e70caa973a234c8823341dd446f5c5d7acc826868bb42b272bdc0 \
|
||||
--hash=sha256:11a373c9308eae3bac2d56d37017f9ab63968cc074a8b95be879aae3d13133aa \
|
||||
--hash=sha256:128ec92e6a0e9ff7a38edef80e3b74f15bb2ed1c531d5d3252c8dca22677651b \
|
||||
--hash=sha256:1fb4028437201e19014e4e374272b739867c8a3eb655da46675ef4c2ff14b616 \
|
||||
--hash=sha256:282e49c766b5952dd8796f77d7ed3ae412cdd88e31f845b1fbbb86ac6cb7bebf \
|
||||
--hash=sha256:2b369cabb48485fbf444beb3f5a878075367b99c2c86db2f796afeabebc749e0 \
|
||||
--hash=sha256:2bf714333788bf5175f2512b86d2ed129e89ae6f6c2923e8a297a1e3395e13b5 \
|
||||
--hash=sha256:30de46b55873b51be945a09edf486afcc190dc47eff9fb5d2b12c9f7e3d743da \
|
||||
--hash=sha256:34e3b12407ddf99f6627435aa8a165f094339bb7dc33de92e1d7472e9f237304 \
|
||||
--hash=sha256:36b2a516fce57309840f5ef3fa2fd0c4449293fc72536a0400d2e1e26b414da8 \
|
||||
--hash=sha256:3a9195299df9d7d2a6e9d16bebd6b706b0ea99e4b871864c4b034c2577e21a77 \
|
||||
--hash=sha256:3c32fc0f546ceecc47dd45f33d72ab4a1e341b80d9081c2d77b100add5d49104 \
|
||||
--hash=sha256:3d7333e907bff3e3997e54f89733ffa8d619842a3e1cd962bca34bdc11944c28 \
|
||||
--hash=sha256:4795e93d130c9b7fb661f0cead49752ae6a980437df74b99d5918026c212443e \
|
||||
--hash=sha256:490e19a45cd3cdd6dfe6b46019f7ffe1103500750b41b51996a870e7c1c5f066 \
|
||||
--hash=sha256:4aa0fe08383f3e5fc7d2f8cf9b42ac778f4d53fd75bcd2799a858225954eab89 \
|
||||
--hash=sha256:559326d8f3d904cd4aa61f6a392d5626f35eec6a9f6cc83bcddb0abf88c40516 \
|
||||
--hash=sha256:5929c83bd8cb7572d1c17ffdbf0eac235bf3c4d53cde1950cf89d944eaf97525 \
|
||||
--hash=sha256:5bfd21b6acb32e20d4e279c34405a34e63da345be4b2b6eabd683e1a88857406 \
|
||||
--hash=sha256:613cf06313331f49dd7b85a5a24fbddb1156c9723b6921a231906241726e5aee \
|
||||
--hash=sha256:66a8e1c1e899d15a2f7510e43527fba22d895e7f6058d027db3e3837d88a69de \
|
||||
--hash=sha256:691a62926ba09f2653ec0908554b3635497efb7751c5d46b916cd1ebbb1d3c25 \
|
||||
--hash=sha256:6c1fd18e45c39d5a4be4b0d6a20c141e43fe46daeb1b2e2f304ebae7015ab6e6 \
|
||||
--hash=sha256:6c6a1e8f98de92e817491b50aa4d01d69a1b41a4ed3173747e8f16f0d4cf81cc \
|
||||
--hash=sha256:6cf029b886cfb28a2701503b7c602b811f2daa45276bd6459b0c71e051deb497 \
|
||||
--hash=sha256:71506116eac0d3e3598d6325b4b818c3a0f6acb3222b24d30ad726e8c4bf7ea8 \
|
||||
--hash=sha256:7993164e8e9f78ec31d38c47850ca6ba5451788b5b49a8a2dbb3322b36b5693b \
|
||||
--hash=sha256:7a5cc6bf60791d8274edfdfe2dd7cec3f00f656dcc92e2b0a9af06c8b18ce6a6 \
|
||||
--hash=sha256:87c1cc1057d903b78a8257a7c5f497db6fd5284f5080392bd57b66031d7389a3 \
|
||||
--hash=sha256:98376c75bd6c103c74396953084a5e0798ffe476aecbfcc51ec6d100a685ac38 \
|
||||
--hash=sha256:a395524b0fafa10446d11e11acb4742e919523de58aac03b791f26d7a783bcf0 \
|
||||
--hash=sha256:a5100a4be91b7d4b7c8fe16a3600bd0951e10205eb1066b6873afd3996b51ee4 \
|
||||
--hash=sha256:a63a52221f8c73f82f4e00493351d987f594931198589287aee96f8da673cfd5 \
|
||||
--hash=sha256:a89734a7bcb2ea3b6fd600a74d6fbcdb8d3fa3f7917dbd978e039710b5509c9c \
|
||||
--hash=sha256:b165f5e6de1ccc964e863bd2035807a4d3bad3e0481f9db2dc52034d6ad4f9de \
|
||||
--hash=sha256:b3845e4c2fa32963fb7f384ebbaa2761b0e6b96145239bf80e956d4aff4b071c \
|
||||
--hash=sha256:b3b017d9103e7a7372a146773be32b184ff7330bd708d40b1f56f06a686756ed \
|
||||
--hash=sha256:b6c6e4858d8c3f88e19b7aa94b6a7619108f0bee51da9fa67b0785a8b59955f9 \
|
||||
--hash=sha256:bfad1202fb1f9140e3810cc607058395f59032d9128cc0d716900c78bea5f337 \
|
||||
--hash=sha256:c01809fb602e2fefc8cbfb3b603bb59d2a2eaee8708410896d48a835ba00e7c5 \
|
||||
--hash=sha256:cce8de9d48289927ed18aaa420740efd52b2cd9289da32e3799afbb3a02822e8 \
|
||||
--hash=sha256:ce2bc1ea01200b6d8130ab917296d05d77a1a571ec6c1ee25cfca6d55cd5db4a \
|
||||
--hash=sha256:d4601e2a8b46ffbf540601a4926fd6cc5aae8a13b36fdd467f1040f01f9edaed \
|
||||
--hash=sha256:d556d1c256b3700b60b6b061664a667b2e49d599c2772d46a9f2348f2dc4ab5c \
|
||||
--hash=sha256:ddf7453acd03b966624ebefdb38169b5bbbeea1a1a58c90b095667247f9de327 \
|
||||
--hash=sha256:e781eeb65b2007af553389c8a7fb7bc53cb856118b0fcffb2c26b0f49561c686 \
|
||||
--hash=sha256:e920c272cd9e70a6b10eae9203cc96845da142e1dd4482de9343dda3738a9862 \
|
||||
--hash=sha256:f5b6174bf4bf9152e368561dff410203c6921e4dd2afbcda3283a95957158112 \
|
||||
--hash=sha256:f69bd42fd2515060af69b120668213121264bb7976b113954b6f9db327727c65 \
|
||||
--hash=sha256:f823d0f6b04a6797fbe253bcf91666e71a6b63c290683819650c68b2468ebe64 \
|
||||
--hash=sha256:fa7e9f2e80a9535f9692e113d02b4268b5f88675a730d1b0ef0abeb74c9a4e80
|
||||
# via -r .github/requirements/bench.in
|
||||
talipp==2.7.0 \
|
||||
--hash=sha256:567f59ad74366cb59a14a00d350f35fd9d22e6924d6228bad581e6dcf1de2205 \
|
||||
--hash=sha256:f749f22b9ad615605e71faf26457bb7f5e3fe16f04d3287f4ca54fd16bc3d4eb
|
||||
# via -r .github/requirements/bench.in
|
||||
tulipy==0.4.0 \
|
||||
--hash=sha256:540704956b5b940a5f6306aa393a37536a6d7c3cbc07efe47512f3496e5203ab \
|
||||
--hash=sha256:95542e40537afdd345d875baf37485eac993c6a819d00c51432e9de8df21eba8 \
|
||||
--hash=sha256:fbc31727ef7657c93ad910bfdce65fecc6aaa7a5e961fe00240718e7a3fc79d8
|
||||
# via -r .github/requirements/bench.in
|
||||
tzdata==2026.2 \
|
||||
--hash=sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10 \
|
||||
--hash=sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7
|
||||
|
||||
@@ -48,11 +48,11 @@ jobs:
|
||||
name: Cross-library benchmark report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
@@ -117,3 +117,29 @@ jobs:
|
||||
with:
|
||||
name: cross-library-bench
|
||||
path: bindings/python/benchmark.txt
|
||||
|
||||
rust-cross-bench:
|
||||
name: Rust cross-library benchmark report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- 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
|
||||
|
||||
# Wickra vs the other Rust TA crates (kand, ta-rs, yata) on an identical
|
||||
# candle series — the like-for-like engine comparison with no binding
|
||||
# overhead. Streaming + batch, in crates/wickra-bench/benches/cross_lib.rs.
|
||||
- name: Run Rust cross-library benchmark
|
||||
run: cargo bench -p wickra-bench --bench cross_lib | tee rust_cross_bench.txt
|
||||
|
||||
- name: Upload Rust report
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: rust-cross-bench
|
||||
path: rust_cross_bench.txt
|
||||
|
||||
+592
-30
@@ -34,17 +34,18 @@ jobs:
|
||||
rust:
|
||||
name: Rust ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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: rustfmt, clippy
|
||||
|
||||
@@ -53,6 +54,22 @@ jobs:
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# CARGO_NET_RETRY rides out short blips, but a longer runner DNS outage
|
||||
# outlasts cargo's rapid in-process retries: the crates.io index fetch
|
||||
# the first cargo step does ("Could not resolve host: index.crates.io")
|
||||
# then fails the whole job. Pre-fetch the dependency graph here with real
|
||||
# backoff so clippy/build/test resolve from the warmed local cache.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Format check
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
@@ -88,8 +105,9 @@ jobs:
|
||||
examples-smoke:
|
||||
name: Examples (syntax smoke)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -98,7 +116,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- name: Wait before Node retry
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
@@ -111,7 +129,7 @@ jobs:
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- name: Set up Python
|
||||
id: setup_python
|
||||
@@ -177,13 +195,14 @@ jobs:
|
||||
clippy-bindings:
|
||||
name: Clippy bindings
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
@@ -212,7 +231,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- name: Wait before Node retry
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
@@ -225,13 +244,26 @@ jobs:
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- 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
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# See the rust job: pre-fetch with backoff so the clippy index update
|
||||
# can't fail the job on a transient "Could not resolve host" DNS blip.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Clippy (bindings, all targets)
|
||||
run: cargo clippy -p wickra-node -p wickra-python --all-targets -- -D warnings
|
||||
|
||||
@@ -247,6 +279,7 @@ jobs:
|
||||
msrv:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
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:
|
||||
@@ -258,12 +291,12 @@ jobs:
|
||||
toolchain: "1.88"
|
||||
packages: "-p wickra-node"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
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 }}
|
||||
|
||||
@@ -272,6 +305,19 @@ jobs:
|
||||
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Warm cargo registry (retry transient DNS/registry flakes)
|
||||
shell: bash
|
||||
# See the rust job: pre-fetch with backoff so the first cargo step can't
|
||||
# fail the job on a transient "Could not resolve host" DNS blip.
|
||||
run: |
|
||||
for attempt in 1 2 3 4 5; do
|
||||
if cargo fetch; then exit 0; fi
|
||||
echo "::warning::cargo fetch failed (attempt $attempt/5) — likely a registry/DNS flake; retrying in $((attempt * 20))s..."
|
||||
sleep $((attempt * 20))
|
||||
done
|
||||
echo "::error::cargo fetch still failing after 5 attempts"
|
||||
exit 1
|
||||
|
||||
- name: Build on MSRV
|
||||
run: cargo build ${{ matrix.packages }} --verbose
|
||||
|
||||
@@ -282,13 +328,14 @@ jobs:
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
@@ -298,7 +345,7 @@ jobs:
|
||||
timeout-minutes: 6
|
||||
|
||||
- name: Install cargo-llvm-cov
|
||||
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cargo-llvm-cov
|
||||
@@ -323,8 +370,9 @@ jobs:
|
||||
supply-chain:
|
||||
name: Supply-chain (cargo-deny)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -341,13 +389,14 @@ jobs:
|
||||
fuzz-smoke:
|
||||
name: Fuzz (smoke)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
@@ -366,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@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cargo-fuzz
|
||||
@@ -394,18 +443,19 @@ jobs:
|
||||
python:
|
||||
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ["3.9", "3.11", "3.12", "3.13"]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
@@ -475,13 +525,14 @@ jobs:
|
||||
wasm:
|
||||
name: WASM build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
@@ -498,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@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: wasm-pack
|
||||
@@ -515,21 +566,28 @@ 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: 30 # backstop: cap a wedged job instead of GitHub's 6h default (headroom for slow registry/package installs)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node-version: ["18", "20"]
|
||||
node-version: ["22", "24"]
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
@@ -564,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
|
||||
@@ -576,9 +642,505 @@ jobs:
|
||||
# exist yet for win32-x64-msvc).
|
||||
run: npx napi build --platform --release
|
||||
|
||||
# The Node test process has wedged on macOS runners (the step hung for
|
||||
# 1h+ while the same tests passed in ~1.5 min elsewhere). Wrap it so a
|
||||
# hung attempt is killed after 6 min and retried once, rather than
|
||||
# running up to the job-level backstop. A normal run is under a minute.
|
||||
- name: Run Node tests
|
||||
working-directory: bindings/node
|
||||
run: node --test __tests__/
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 6
|
||||
max_attempts: 2
|
||||
command: cd bindings/node && node --test
|
||||
shell: bash
|
||||
|
||||
c-abi:
|
||||
name: C ABI on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
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
|
||||
|
||||
- name: Install cbindgen
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: cbindgen
|
||||
|
||||
- name: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Rust unit tests
|
||||
run: cargo test -p wickra-c
|
||||
|
||||
# The generated header is platform-independent, so checking drift on one OS
|
||||
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
|
||||
- name: Check the committed header is in sync with cbindgen
|
||||
if: runner.os == 'Linux'
|
||||
shell: bash
|
||||
run: |
|
||||
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
|
||||
if ! git diff --quiet -- bindings/c/include/wickra.h; then
|
||||
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
|
||||
git --no-pager diff -- bindings/c/include/wickra.h
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The real cross-language test: a foreign C consumer links the generated
|
||||
# header + the compiled library and runs. If this passes on all three OSes,
|
||||
# every C-capable language can link the same way.
|
||||
- name: Build and run the C smoke example (CMake + ctest)
|
||||
shell: bash
|
||||
run: |
|
||||
cmake -S examples/c -B examples/c/build
|
||||
cmake --build examples/c/build --config Release
|
||||
ctest --test-dir examples/c/build -C Release --output-on-failure
|
||||
|
||||
csharp:
|
||||
name: C# on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
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.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- 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
|
||||
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
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze \
|
||||
fetch_btcusdt live_binance; do
|
||||
dotnet build "examples/csharp/$d" -c Release
|
||||
done
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline C# examples
|
||||
shell: bash
|
||||
run: |
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
dotnet run --project "examples/csharp/$d" -c Release --no-build
|
||||
done
|
||||
|
||||
go:
|
||||
name: Go on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
# Go is not reliably on PATH on every runner image, so install it
|
||||
# explicitly. cache: false — the cargo build dominates and the modules
|
||||
# have no external Go deps worth caching.
|
||||
- name: Set up Go
|
||||
id: setup-go
|
||||
continue-on-error: true
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "stable"
|
||||
cache: false
|
||||
|
||||
- name: Retry Go setup (CDN flake)
|
||||
if: steps.setup-go.outcome == 'failure'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::warning::setup-go failed (likely CDN flake), waiting 30s before retry..."
|
||||
sleep 30
|
||||
|
||||
- name: Set up Go (retry)
|
||||
if: steps.setup-go.outcome == 'failure'
|
||||
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||
with:
|
||||
go-version: "stable"
|
||||
cache: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
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
|
||||
|
||||
# The binding links against the C ABI hub via cgo; build it first and stage
|
||||
# the platform library under bindings/go/lib so cgo's LDFLAGS find it. Go is
|
||||
# preinstalled on the GitHub runners, so no setup-go step is needed.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Vendored header in sync with the C ABI
|
||||
shell: bash
|
||||
# bindings/go/include/wickra.h is a committed copy of the cbindgen header
|
||||
# (the parent ../c/include is outside the Go module, so it must be
|
||||
# vendored). Fail if it drifts from the source of truth.
|
||||
run: |
|
||||
if ! diff -u bindings/c/include/wickra.h bindings/go/include/wickra.h; then
|
||||
echo "::error::bindings/go/include/wickra.h is stale — copy bindings/c/include/wickra.h over it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Stage the native library
|
||||
shell: bash
|
||||
# Stage into lib/<goos>_<goarch>/ to match the per-platform cgo LDFLAGS.
|
||||
# CI builds the host target, so RUNNER_OS/ARCH give the right directory
|
||||
# (note macos-latest is arm64).
|
||||
run: |
|
||||
case "$RUNNER_ARCH" in
|
||||
X64) arch=amd64 ;;
|
||||
ARM64) arch=arm64 ;;
|
||||
*) echo "::error::unsupported RUNNER_ARCH '$RUNNER_ARCH'"; exit 1 ;;
|
||||
esac
|
||||
case "$RUNNER_OS" in
|
||||
Linux) dir="linux_$arch"; lib=target/release/libwickra.so ;;
|
||||
macOS) dir="darwin_$arch"; lib=target/release/libwickra.dylib ;;
|
||||
Windows) dir="windows_$arch"; lib=target/release/wickra.dll ;;
|
||||
esac
|
||||
mkdir -p "bindings/go/lib/$dir"
|
||||
cp "$lib" "bindings/go/lib/$dir/"
|
||||
echo "WICKRA_GO_LIBDIR=$PWD/bindings/go/lib/$dir" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Go info
|
||||
run: go version
|
||||
|
||||
- name: Check gofmt
|
||||
shell: bash
|
||||
run: |
|
||||
unformatted="$(gofmt -l bindings/go examples/go)"
|
||||
if [ -n "$unformatted" ]; then
|
||||
echo "gofmt needed on:"; echo "$unformatted"; exit 1
|
||||
fi
|
||||
|
||||
- name: Vet and test the Go binding
|
||||
shell: bash
|
||||
# On Windows there is no rpath; the loader resolves wickra.dll via PATH
|
||||
# (WICKRA_GO_LIBDIR is the per-platform staged lib dir). Linux/macOS use
|
||||
# the rpath baked by the per-platform cgo LDFLAGS.
|
||||
run: |
|
||||
export PATH="$WICKRA_GO_LIBDIR:$PATH"
|
||||
cd bindings/go
|
||||
go vet ./...
|
||||
go test ./...
|
||||
|
||||
- name: Build the Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$WICKRA_GO_LIBDIR:$PATH"
|
||||
cd examples/go
|
||||
go build ./...
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$WICKRA_GO_LIBDIR:$PATH"
|
||||
cd examples/go
|
||||
for d in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
go run "./$d"
|
||||
done
|
||||
|
||||
r:
|
||||
name: R on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
env:
|
||||
WICKRA_INCLUDE_DIR: ${{ github.workspace }}/bindings/c/include
|
||||
WICKRA_LIB_DIR: ${{ github.workspace }}/target/release
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
uses: ./.github/actions/setup-rust
|
||||
|
||||
- name: Cache cargo
|
||||
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
|
||||
continue-on-error: true
|
||||
timeout-minutes: 6
|
||||
|
||||
# The binding compiles a thin .Call glue layer against the C ABI hub; build
|
||||
# the library first. On Windows configure.win bundles a renamed copy
|
||||
# (wickra_abi.dll) so the package's own wickra.dll does not collide with it.
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Set up R
|
||||
uses: r-lib/actions/setup-r@a51a8012b0aab7c32ef9d19bf54da93f3254335e # v2
|
||||
with:
|
||||
r-version: "release"
|
||||
use-public-rspm: true
|
||||
|
||||
# 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
|
||||
# github.workspace is a backslash path on Windows; configure(.win) (sh) and
|
||||
# mingw need forward slashes. WICKRA_*_DIR makes configure use the locally
|
||||
# built C ABI (dev override) instead of downloading the release asset, so
|
||||
# CI is version-independent. Deliberately do NOT set LD_LIBRARY_PATH /
|
||||
# DYLD_LIBRARY_PATH: the lib is bundled into the package and must resolve
|
||||
# via the rpath ($ORIGIN / @loader_path) baked by configure — that is the
|
||||
# self-contained install path real users (and r-universe) get.
|
||||
run: |
|
||||
export WICKRA_INCLUDE_DIR="${WICKRA_INCLUDE_DIR//\\//}"
|
||||
export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}"
|
||||
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 'knitr::knit("bindings/r/vignettes/getting-started.Rmd", output = tempfile(fileext = ".md"), quiet = TRUE); cat("vignette code OK\n")'
|
||||
|
||||
- name: Run the offline R examples
|
||||
shell: bash
|
||||
# No loader-path exports: the installed package is self-contained (bundled
|
||||
# lib + rpath), so the examples exercise exactly what end users run.
|
||||
run: |
|
||||
cd examples/r
|
||||
for f in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
Rscript "$f.R"
|
||||
done
|
||||
|
||||
# fetch_btcusdt / live_binance need the network (and jsonlite / websocket);
|
||||
# build-check that they parse without running them.
|
||||
- name: Parse the network R examples
|
||||
run: Rscript -e 'invisible(lapply(c("examples/r/fetch_btcusdt.R", "examples/r/live_binance.R"), parse)); cat("network R examples parse OK\n")'
|
||||
|
||||
java:
|
||||
name: Java on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
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:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install Rust toolchain
|
||||
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
|
||||
|
||||
# The binding links the C ABI hub at runtime through the Java FFM API; build
|
||||
# it first so WickraNative's development fallback finds
|
||||
# target/release/wickra.{so,dylib,dll}. The FFM API is final since JDK 22; we
|
||||
# build on the 25 LTS (22 is EOL; the pom pins bytecode to release 22 so the
|
||||
# runtime floor stays Java 22). Installed explicitly (runners ship 17/21).
|
||||
- name: Build the C ABI library
|
||||
run: cargo build -p wickra-c --release
|
||||
|
||||
- name: Set up JDK 25
|
||||
id: setup-java
|
||||
continue-on-error: true
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
cache: maven
|
||||
|
||||
- name: Retry JDK setup (CDN flake)
|
||||
if: steps.setup-java.outcome == 'failure'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::warning::setup-java failed (likely CDN flake), waiting 30s before retry..."
|
||||
sleep 30
|
||||
|
||||
- name: Set up JDK 25 (retry)
|
||||
if: steps.setup-java.outcome == 'failure'
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
cache: maven
|
||||
|
||||
- name: Java info
|
||||
run: java -version
|
||||
|
||||
# `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
|
||||
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
|
||||
|
||||
# Run only the offline examples (fetch_btcusdt / live_binance need network).
|
||||
- name: Run the offline Java examples
|
||||
shell: bash
|
||||
run: |
|
||||
for cls in Streaming Backtest MultiTimeframe ParallelAssets \
|
||||
StrategyRsiMeanReversion StrategyMacdAdx StrategyBollingerSqueeze; do
|
||||
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
|
||||
|
||||
@@ -40,17 +40,17 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
|
||||
- name: Perform CodeQL analysis
|
||||
uses: github/codeql-action/analyze@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
+396
-29
@@ -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
|
||||
@@ -45,10 +46,10 @@ jobs:
|
||||
# Settings -> Environments.
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- 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@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
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:
|
||||
@@ -157,7 +159,7 @@ jobs:
|
||||
- { os: windows-11-arm, target: aarch64, manylinux: auto }
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Set up Python
|
||||
@@ -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,9 +211,10 @@ 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Sync root README into bindings/python so it ships in the sdist
|
||||
@@ -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:
|
||||
@@ -249,7 +268,7 @@ jobs:
|
||||
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -258,7 +277,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
|
||||
- name: Wait before Node retry
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
@@ -271,9 +290,9 @@ jobs:
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
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
|
||||
@@ -310,7 +335,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -319,7 +344,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Wait before Node retry
|
||||
@@ -333,12 +358,17 @@ jobs:
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
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
|
||||
@@ -479,7 +510,7 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -488,7 +519,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
node-version: "22"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Wait before Node retry
|
||||
@@ -502,17 +533,21 @@ jobs:
|
||||
if: steps.setup_node.outcome == 'failure'
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
with:
|
||||
node-version: "20"
|
||||
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@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
|
||||
uses: taiki-e/install-action@59012be0884e296ca2da49b530610e72c49039ad # v2.81.6
|
||||
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
|
||||
with:
|
||||
tool: wasm-pack
|
||||
@@ -536,7 +571,7 @@ jobs:
|
||||
pkg.repository = { type: 'git', url: 'https://github.com/wickra-lib/wickra' };
|
||||
pkg.homepage = 'https://github.com/wickra-lib/wickra';
|
||||
pkg.bugs = { url: 'https://github.com/wickra-lib/wickra/issues' };
|
||||
pkg.license = 'PolyForm-Noncommercial-1.0.0';
|
||||
pkg.license = 'MIT OR Apache-2.0';
|
||||
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2));
|
||||
"
|
||||
|
||||
@@ -569,9 +604,318 @@ jobs:
|
||||
# the old "publish, then upload provenance" order would have the provenance
|
||||
# upload rejected once immutability is enabled.
|
||||
# --------------------------------------------------------------------------
|
||||
# --------------------------------------------------------------------------
|
||||
# C ABI native libraries (bindings/c) — built per target on a native runner
|
||||
# (no cross toolchain needed) and attached to the GitHub Release as the
|
||||
# distribution channel. There is no package registry for the C ABI.
|
||||
# --------------------------------------------------------------------------
|
||||
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:
|
||||
include:
|
||||
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
|
||||
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
|
||||
- { host: macos-latest, target: x86_64-apple-darwin }
|
||||
- { host: macos-latest, target: aarch64-apple-darwin }
|
||||
- { host: windows-latest, target: x86_64-pc-windows-msvc }
|
||||
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
|
||||
runs-on: ${{ matrix.host }}
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- uses: ./.github/actions/setup-rust
|
||||
with:
|
||||
targets: ${{ matrix.target }}
|
||||
|
||||
- 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: Build the C ABI library (cdylib + staticlib)
|
||||
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
|
||||
|
||||
- name: Package header + libraries
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
dir="wickra-c-${{ matrix.target }}"
|
||||
mkdir -p "$dir/include" "$dir/lib"
|
||||
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
|
||||
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
|
||||
src="target/${{ matrix.target }}/release/$f"
|
||||
[ -f "$src" ] && cp "$src" "$dir/lib/"
|
||||
done
|
||||
tar -czf "$dir.tar.gz" "$dir"
|
||||
echo "packaged $dir:"; ls -lR "$dir"
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: c-abi-${{ matrix.target }}
|
||||
path: wickra-c-${{ matrix.target }}.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
# Pack and publish the .NET binding to NuGet. Independent of the GitHub-release
|
||||
# job so a C# hiccup never blocks the C/C++ asset release. Authentication uses
|
||||
# NuGet Trusted Publishing (OIDC) — no long-lived API key. The 'wickra-release'
|
||||
# trusted-publishing policy on nuget.org (owner KingchenC, repo wickra-lib/wickra,
|
||||
# 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
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # request the GitHub OIDC token for trusted publishing
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download the C ABI native libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: c-abi-*
|
||||
path: c-abi-artifacts
|
||||
|
||||
- name: Stage native libraries into runtimes/<rid>/native
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
declare -A RID=(
|
||||
[x86_64-unknown-linux-gnu]=linux-x64
|
||||
[aarch64-unknown-linux-gnu]=linux-arm64
|
||||
[x86_64-apple-darwin]=osx-x64
|
||||
[aarch64-apple-darwin]=osx-arm64
|
||||
[x86_64-pc-windows-msvc]=win-x64
|
||||
[aarch64-pc-windows-msvc]=win-arm64
|
||||
)
|
||||
base=bindings/csharp/Wickra/runtimes
|
||||
for target in "${!RID[@]}"; do
|
||||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||||
if [ -z "$archive" ]; then
|
||||
echo "::error::missing native artifact for $target"; exit 1
|
||||
fi
|
||||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||||
dest="$base/${RID[$target]}/native"; mkdir -p "$dest"
|
||||
for f in libwickra.so libwickra.dylib wickra.dll; do
|
||||
src=$(find "$tmp" -name "$f" | head -1)
|
||||
[ -n "$src" ] && cp "$src" "$dest/"
|
||||
done
|
||||
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
|
||||
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.
|
||||
- name: NuGet login (OIDC -> temporary API key)
|
||||
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
|
||||
id: nuget_login
|
||||
with:
|
||||
user: KingchenC
|
||||
|
||||
# Pass the temporary key through the environment (not string-interpolated
|
||||
# into the script) so it cannot be parsed as shell — avoids template injection.
|
||||
- name: Push to NuGet
|
||||
shell: bash
|
||||
env:
|
||||
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
|
||||
run: |
|
||||
dotnet nuget push "nupkg/*.nupkg" --api-key "$NUGET_API_KEY" \
|
||||
--source https://api.nuget.org/v3/index.json --skip-duplicate
|
||||
|
||||
- name: Upload the NuGet package as a build artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: nuget-package
|
||||
path: nupkg/*.nupkg
|
||||
if-no-files-found: error
|
||||
|
||||
# Publish the Java binding to Maven Central. Independent of the GitHub-release
|
||||
# job so a Java hiccup never blocks the C/C++ asset release. The Maven Central
|
||||
# namespace (org.wickra) is verified; authentication uses the Sonatype Central
|
||||
# Portal token (CENTRAL_USERNAME / CENTRAL_PASSWORD) and artifacts are signed
|
||||
# with the GPG key (GPG_PRIVATE_KEY / GPG_PASSPHRASE). The pom version must
|
||||
# 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
|
||||
steps:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download the C ABI native libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: c-abi-*
|
||||
path: c-abi-artifacts
|
||||
|
||||
- name: Stage native libraries into resources/native/<os>-<arch>
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
declare -A PLAT=(
|
||||
[x86_64-unknown-linux-gnu]=linux-x64
|
||||
[aarch64-unknown-linux-gnu]=linux-arm64
|
||||
[x86_64-apple-darwin]=osx-x64
|
||||
[aarch64-apple-darwin]=osx-arm64
|
||||
[x86_64-pc-windows-msvc]=win-x64
|
||||
[aarch64-pc-windows-msvc]=win-arm64
|
||||
)
|
||||
base=bindings/java/src/main/resources/native
|
||||
for target in "${!PLAT[@]}"; do
|
||||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||||
if [ -z "$archive" ]; then
|
||||
echo "::error::missing native artifact for $target"; exit 1
|
||||
fi
|
||||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||||
dest="$base/${PLAT[$target]}"; mkdir -p "$dest"
|
||||
for f in libwickra.so libwickra.dylib wickra.dll; do
|
||||
src=$(find "$tmp" -name "$f" | head -1)
|
||||
[ -n "$src" ] && cp "$src" "$dest/"
|
||||
done
|
||||
echo "staged ${PLAT[$target]}:"; ls -l "$dest"
|
||||
done
|
||||
|
||||
# setup-java writes a settings.xml with the 'central' server credentials
|
||||
# (mapped from the env vars below) and imports the GPG signing key.
|
||||
- name: Set up JDK 25 + Maven Central credentials + GPG
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
server-id: central
|
||||
server-username: CENTRAL_USERNAME
|
||||
server-password: CENTRAL_PASSWORD
|
||||
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
gpg-passphrase: GPG_PASSPHRASE
|
||||
|
||||
- name: Deploy to Maven Central
|
||||
env:
|
||||
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
|
||||
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
|
||||
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
|
||||
run: mvn -B -f bindings/java -Prelease deploy -DskipTests
|
||||
|
||||
# Mirror the in-repo Go module (bindings/go) to the standalone wickra-go
|
||||
# repository so `go get github.com/wickra-lib/wickra-go` builds with no extra
|
||||
# steps. The in-repo module keeps the prebuilt libraries git-ignored; the
|
||||
# mirror commits them per platform under lib/<goos>_<goarch>/ alongside the
|
||||
# vendored header. Independent of the GitHub-release job so a Go hiccup never
|
||||
# blocks the C/C++ asset release. The push uses a fine-grained PAT
|
||||
# (WICKRA_GO_MIRROR_TOKEN, contents:write on wickra-go); the mirror is a
|
||||
# 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:
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download the C ABI native libraries
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: c-abi-*
|
||||
path: c-abi-artifacts
|
||||
|
||||
- name: Assemble the wickra-go module tree
|
||||
shell: bash
|
||||
run: |
|
||||
set -e
|
||||
out=wickra-go-module
|
||||
mkdir -p "$out/include" "$out/lib"
|
||||
# Single-package Go source + vendored C ABI header.
|
||||
cp bindings/go/*.go "$out/"
|
||||
cp bindings/go/include/wickra.h "$out/include/"
|
||||
cp bindings/go/README.md "$out/"
|
||||
# Ship the dual license so pkg.go.dev detects a redistributable license.
|
||||
cp LICENSE-MIT LICENSE-APACHE "$out/"
|
||||
# Standalone module path (the in-repo go.mod points at the subdir).
|
||||
printf 'module github.com/wickra-lib/wickra-go\n\ngo 1.23\n' > "$out/go.mod"
|
||||
# Point install instructions at the standalone module path.
|
||||
sed -i 's#github.com/wickra-lib/wickra/bindings/go#github.com/wickra-lib/wickra-go#g' \
|
||||
"$out/README.md" "$out"/*.go
|
||||
# Stage each prebuilt library under lib/<goos>_<goarch>/ (committed in
|
||||
# the mirror, unlike the in-repo lib/.gitignore). "<dir> <libfile>".
|
||||
declare -A GO=(
|
||||
[x86_64-unknown-linux-gnu]="linux_amd64 libwickra.so"
|
||||
[aarch64-unknown-linux-gnu]="linux_arm64 libwickra.so"
|
||||
[x86_64-apple-darwin]="darwin_amd64 libwickra.dylib"
|
||||
[aarch64-apple-darwin]="darwin_arm64 libwickra.dylib"
|
||||
[x86_64-pc-windows-msvc]="windows_amd64 wickra.dll"
|
||||
[aarch64-pc-windows-msvc]="windows_arm64 wickra.dll"
|
||||
)
|
||||
for target in "${!GO[@]}"; do
|
||||
read -r dir libfile <<< "${GO[$target]}"
|
||||
archive=$(find c-abi-artifacts -name "wickra-c-$target.tar.gz" | head -1)
|
||||
if [ -z "$archive" ]; then
|
||||
echo "::error::missing native artifact for $target"; exit 1
|
||||
fi
|
||||
tmp=$(mktemp -d); tar -xzf "$archive" -C "$tmp"
|
||||
src=$(find "$tmp" -name "$libfile" | head -1)
|
||||
if [ -z "$src" ]; then
|
||||
echo "::error::missing $libfile for $target"; exit 1
|
||||
fi
|
||||
mkdir -p "$out/lib/$dir"; cp "$src" "$out/lib/$dir/"
|
||||
done
|
||||
echo "assembled module:"; find "$out" -type f | sort
|
||||
|
||||
- name: Push to wickra-go and tag the release
|
||||
shell: bash
|
||||
env:
|
||||
MIRROR_TOKEN: ${{ secrets.WICKRA_GO_MIRROR_TOKEN }}
|
||||
run: |
|
||||
set -e
|
||||
version="${GITHUB_REF_NAME#v}"
|
||||
remote="https://x-access-token:${MIRROR_TOKEN}@github.com/wickra-lib/wickra-go.git"
|
||||
git clone -q "$remote" mirror-repo
|
||||
cd mirror-repo
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "support@wickra.org"
|
||||
git symbolic-ref HEAD refs/heads/main
|
||||
# Replace all tracked content with the freshly assembled tree.
|
||||
find . -mindepth 1 -maxdepth 1 -not -name .git -exec rm -rf {} +
|
||||
cp -r ../wickra-go-module/. .
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "wickra-go already up to date; tagging only"
|
||||
else
|
||||
git -c commit.gpgsign=false commit -q -m "Release v$version"
|
||||
fi
|
||||
git push origin HEAD:refs/heads/main
|
||||
git tag "v$version"
|
||||
git push origin "v$version"
|
||||
|
||||
github-release:
|
||||
name: Attach assets to the draft GitHub Release
|
||||
needs: [cargo-publish, python-publish, node-publish, wasm-publish]
|
||||
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:
|
||||
contents: write
|
||||
@@ -579,8 +923,9 @@ jobs:
|
||||
# bundle to this same release without re-resolving it.
|
||||
outputs:
|
||||
tag: ${{ steps.tag.outputs.tag }}
|
||||
version: ${{ steps.tag.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
@@ -607,6 +952,7 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
echo "version=${tag#v}" >> "$GITHUB_OUTPUT"
|
||||
echo "::notice::attaching assets to release $tag"
|
||||
|
||||
- name: Download all build artifacts
|
||||
@@ -644,7 +990,7 @@ jobs:
|
||||
# the provenance bundle is attached (P24, immutability-ready).
|
||||
draft: true
|
||||
body: |
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries.
|
||||
Wickra ${{ github.ref_name }} — streaming-first technical indicators for 10 languages: native Rust, Python, Node.js, WASM plus a C ABI hub for C, C++, C#, Go, Java, R.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -653,18 +999,37 @@ jobs:
|
||||
pip install wickra
|
||||
npm install wickra
|
||||
npm install wickra-wasm
|
||||
dotnet add package Wickra
|
||||
go get github.com/wickra-lib/wickra-go
|
||||
```
|
||||
|
||||
Java (Gradle, Maven Central):
|
||||
|
||||
```gradle
|
||||
implementation("org.wickra:wickra:${{ steps.tag.outputs.version }}")
|
||||
```
|
||||
|
||||
R (r-universe):
|
||||
|
||||
```r
|
||||
install.packages("wickra", repos = "https://wickra-lib.r-universe.dev")
|
||||
```
|
||||
|
||||
### Attached assets
|
||||
|
||||
Pre-built artefacts for every supported platform — the same files that
|
||||
were uploaded to crates.io, PyPI, and npm by this workflow run.
|
||||
Pre-built artefacts for every supported platform — the same files this
|
||||
workflow run published to crates.io, PyPI, and npm. C# ships to NuGet,
|
||||
Java to Maven Central, Go to the wickra-go module, and R to r-universe
|
||||
via their own release jobs.
|
||||
|
||||
- `*.whl` / `wickra-*.tar.gz` — Python wheels + sdist (5 platforms, ABI3 ≥ 3.9)
|
||||
- `wickra.*.node` — native Node bindings (linux-x64-gnu, darwin-x64,
|
||||
- `wickra.*.node` — native Node.js bindings (linux-x64-gnu, darwin-x64,
|
||||
darwin-arm64, win32-x64-msvc)
|
||||
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
|
||||
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
|
||||
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
|
||||
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
|
||||
the hub for C, C++, C#, Go, Java and R
|
||||
|
||||
### Auto-generated changelog
|
||||
|
||||
@@ -675,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
|
||||
@@ -758,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
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
id-token: write # OIDC token to publish results to the OpenSSF API
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -33,6 +33,13 @@ jobs:
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
# The default GITHUB_TOKEN cannot read classic branch-protection
|
||||
# rules, so the Branch-Protection check fails with an internal error
|
||||
# and scores -1. A read-only fine-grained PAT (Administration: read,
|
||||
# Contents: read, Metadata: read) supplied as SCORECARD_TOKEN lets the
|
||||
# check read the protection settings. See
|
||||
# https://github.com/ossf/scorecard-action/blob/main/docs/authentication/fine-grained-auth-token.md
|
||||
repo_token: ${{ secrets.SCORECARD_TOKEN }}
|
||||
# Publish to the public OpenSSF endpoint that backs the README badge.
|
||||
publish_results: true
|
||||
|
||||
@@ -44,6 +51,6 @@ jobs:
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload SARIF to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@03e4368ac7daa2bd82b3e85262f3bf87ee112f57 # v3.36.0
|
||||
uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
|
||||
@@ -9,7 +9,7 @@ name: Sync indicator count
|
||||
# 2. GitHub repo "About" description — synced on push to main / v* tag
|
||||
# 3. Docs site: index.md / overview.md / Indicators-Overview.md
|
||||
# (wickra-lib/wickra-docs) — synced on push to main / v* tag*
|
||||
# 4. Marketing site count (wickra-lib/webpage: index.md /
|
||||
# 4. Marketing site count (wickra-lib/webpage: index.md / about.md /
|
||||
# .vitepress/config.ts) — push to main / v* tag*
|
||||
# 5. org profile README count (wickra-lib/.github, profile/README.md)
|
||||
# — synced on push to main / v* tag*
|
||||
@@ -41,17 +41,17 @@ name: Sync indicator count
|
||||
# `RollingVwap`, so the mod-count under-reports by one. lib.rs is the
|
||||
# single source of truth for what the bindings reach.
|
||||
#
|
||||
# Design: keep README in sync *before* a PR is merged, by pushing a
|
||||
# fix-up commit to the PR head branch. After squash-merge into main
|
||||
# the bot commit is folded into the single signed merge commit, so
|
||||
# main's history never shows an unsigned "sync indicator count" entry.
|
||||
# Design: on PRs this workflow is a READ-ONLY check. The indicator wiring
|
||||
# bumps both README.md and docs/README.md inside the author's code commit, so
|
||||
# the counter is already correct by the time CI runs. If it is not, the check
|
||||
# below fails loud and asks the author to re-run the wiring — it never pushes a
|
||||
# fix-up commit.
|
||||
#
|
||||
# The push to PR head uses the default `GITHUB_TOKEN`, whose pushes
|
||||
# explicitly do NOT trigger downstream workflows (anti-recursion
|
||||
# policy). So a counter fix-up does not re-trigger ci.yml on the PR
|
||||
# — it does, however, re-trigger sync-about.yml on the next PR
|
||||
# `synchronize` event, which is what we want (a no-op if the counter
|
||||
# is now correct).
|
||||
# (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to
|
||||
# the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit
|
||||
# moved the PR head onto a commit with no CI run, which hid the Codecov patch
|
||||
# status — keyed to the PR head sha — from the PR. Keeping the counter in the
|
||||
# code commit avoids that entirely.)
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
@@ -73,53 +73,31 @@ permissions:
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
# The only GITHUB_TOKEN write in this workflow: pushing the counter fix-up
|
||||
# commit onto a same-repo PR head branch (git push origin HEAD:<ref>).
|
||||
# This workflow never writes to wickra-lib/wickra with GITHUB_TOKEN: the PR
|
||||
# flow is a read-only check, and the main/tag flow writes only to other
|
||||
# repos (About metadata, docs, webpage, wiki, org) through the fine-grained
|
||||
# ABOUT_SYNC_TOKEN PAT. So GITHUB_TOKEN stays read-only (OpenSSF Scorecard:
|
||||
# Token-Permissions).
|
||||
permissions:
|
||||
contents: write
|
||||
contents: read
|
||||
pull-requests: read
|
||||
steps:
|
||||
# On PRs from forks the head ref lives in another repo; pushing
|
||||
# back to it from this workflow is blocked by GitHub. We still
|
||||
# want the PR to surface the missing counter, so the check below
|
||||
# falls back to a hard failure when push isn't possible.
|
||||
- name: Determine if push to PR head is possible
|
||||
id: ctx
|
||||
# Untrusted PR contexts (head.ref / head.repo.full_name are attacker
|
||||
# controlled on fork PRs) are passed through the environment, never
|
||||
# interpolated straight into the shell, so a crafted branch name cannot
|
||||
# inject commands (OpenSSF Scorecard: Dangerous-Workflow).
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
BASE_REPO: ${{ github.repository }}
|
||||
HEAD_REF: ${{ github.event.pull_request.head.ref }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" = "pull_request" ]; then
|
||||
if [ "$HEAD_REPO" = "$BASE_REPO" ]; then
|
||||
echo "can_push=true" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=$HEAD_REF" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "can_push=false" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
else
|
||||
echo "can_push=false" >> "$GITHUB_OUTPUT"
|
||||
echo "head_ref=" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# On PRs we check out the *head* commit (not the merge ref) so
|
||||
# any fix-up commit we make goes onto the PR branch itself. On
|
||||
# push events we check out the default ref. fetch-depth: 0 lets
|
||||
# us push back without "shallow update not allowed".
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
# On PRs we check out the PR *head* commit (the author's code, not the
|
||||
# merge ref) so the counter check validates exactly what will land. On
|
||||
# push events we check out the default ref. No push is made, so a shallow
|
||||
# checkout is enough.
|
||||
#
|
||||
# Check out by head SHA, not head ref (branch name): a fast `gh pr merge
|
||||
# --squash --delete-branch` deletes the head branch the moment the PR
|
||||
# merges, often before this queued read-only check reaches its checkout.
|
||||
# Fetching the now-gone `refs/heads/<branch>` then fails the run (exit 1).
|
||||
# The head SHA stays reachable via `refs/pull/N/head` after the branch is
|
||||
# gone, so the checkout — and the run — survives an instant merge.
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref }}
|
||||
fetch-depth: 1
|
||||
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }}
|
||||
repository: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || github.repository }}
|
||||
# Default GITHUB_TOKEN is fine for the same-repo PR-branch
|
||||
# push; the About / Wiki steps re-authenticate with the PAT
|
||||
# below where needed.
|
||||
|
||||
- name: Count indicators
|
||||
id: count
|
||||
@@ -139,69 +117,33 @@ jobs:
|
||||
|
||||
# ----- PR flow ---------------------------------------------------
|
||||
|
||||
- name: Check README counter (PR)
|
||||
- name: Check README counter (PR, read-only)
|
||||
if: github.event_name == 'pull_request'
|
||||
id: pr_check
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
if grep -qE "^${n} streaming-first indicators" README.md \
|
||||
&& grep -qE "\*\*${n} indicators\*\*" docs/README.md; then
|
||||
echo "matches=true" >> "$GITHUB_OUTPUT"
|
||||
echo "README + docs/README counter already at ${n}; nothing to do."
|
||||
else
|
||||
echo "matches=false" >> "$GITHUB_OUTPUT"
|
||||
echo "README/docs counter does not match ${n}; will fix up."
|
||||
ok=true
|
||||
if ! grep -qE "^${n} streaming-first indicators" README.md; then
|
||||
echo "::error::README.md does not say '${n} streaming-first indicators' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps README.md), then push again."
|
||||
ok=false
|
||||
fi
|
||||
|
||||
- name: Fix counter on fork PR head (read-only, fail loud)
|
||||
if: github.event_name == 'pull_request' && steps.pr_check.outputs.matches == 'false' && steps.ctx.outputs.can_push == 'false'
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
echo "::error::README.md / docs/README.md say a different indicator count than lib.rs (${n}). This PR is from a fork, so the workflow cannot push the fix; please set README.md to '${n} streaming-first indicators' and docs/README.md to '**${n} indicators**', then push again."
|
||||
exit 1
|
||||
|
||||
- name: Patch README on PR head
|
||||
if: github.event_name == 'pull_request' && steps.pr_check.outputs.matches == 'false' && steps.ctx.outputs.can_push == 'true'
|
||||
id: pr_patch
|
||||
run: |
|
||||
n="${{ steps.count.outputs.count }}"
|
||||
# docs/README.md carries the count in its docs.wickra.org pointer prose
|
||||
# ("**N indicators**"); keep it in sync with README's prose count.
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" README.md docs/README.md
|
||||
# Bump the banner cache-buster so GitHub's Camo proxy refetches the org
|
||||
# profile image (regenerated with the new count by .github/banner.yml)
|
||||
# instead of serving a stale cached copy. (README only — docs has no banner.)
|
||||
sed -i -E "s|(wickra-banner\.webp\?v=)[0-9]+|\1${n}|" README.md
|
||||
if git diff --quiet; then
|
||||
echo "No README changes after sed (counter regex did not match anything); skipping push."
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
if ! grep -qE "\*\*${n} indicators\*\*" docs/README.md; then
|
||||
echo "::error::docs/README.md does not say '**${n} indicators**' — lib.rs exports ${n}. Re-run the indicator wiring (it bumps docs/README.md), then push again."
|
||||
ok=false
|
||||
fi
|
||||
if [ "$ok" = "true" ]; then
|
||||
echo "README.md + docs/README.md counter already at ${n}; nothing to do."
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Commit & push counter fix to PR head
|
||||
if: github.event_name == 'pull_request' && steps.pr_patch.outputs.changed == 'true'
|
||||
# head_ref still carries the (untrusted) PR branch name forwarded by the
|
||||
# ctx step; pass it through the environment so the push refspec cannot be
|
||||
# used to inject shell commands (OpenSSF Scorecard: Dangerous-Workflow).
|
||||
env:
|
||||
COUNT: ${{ steps.count.outputs.count }}
|
||||
HEAD_REF: ${{ steps.ctx.outputs.head_ref }}
|
||||
run: |
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add README.md docs/README.md
|
||||
git commit -m "chore: sync indicator count to ${COUNT}"
|
||||
git push origin "HEAD:${HEAD_REF}"
|
||||
|
||||
# ----- main / tag flow ------------------------------------------
|
||||
#
|
||||
# After a PR squash-merges, this workflow runs again on the push
|
||||
# to main. README is already correct (it was fixed on the PR
|
||||
# branch before the merge); the only outward syncs left are the
|
||||
# GitHub About description (repo metadata, not a commit) and the
|
||||
# wiki repo (separate repo, no main history pollution). README is
|
||||
# not touched on main any more.
|
||||
# After a PR squash-merges, this workflow runs again on the push to main.
|
||||
# README.md / docs/README.md are already correct (the indicator wiring
|
||||
# bumped them in the merged code commit); the only outward syncs left are
|
||||
# the GitHub About description (repo metadata, not a commit) and the docs /
|
||||
# webpage / wiki / org repos (separate repos, no main history pollution).
|
||||
# The wickra repo's own README is not touched on main any more.
|
||||
|
||||
- name: Update GitHub About (description + homepage)
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -214,7 +156,7 @@ jobs:
|
||||
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore
|
||||
# gated on the domain resolving, otherwise the About link would 404.
|
||||
homepage="https://docs.wickra.org"
|
||||
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
|
||||
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, C ABI, .NET, Go, Java, and R bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
|
||||
# Enforce the homepage unconditionally — it is a constant, so this both
|
||||
# corrects the stale kingchenc URL and self-heals any future drift.
|
||||
# Same Administration-write permission as --description (no extra scope).
|
||||
@@ -245,14 +187,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd docs-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md overview.md Indicators-Overview.md
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md overview.md Indicators-Overview.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Docs indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md overview.md Indicators-Overview.md
|
||||
git add index.md overview.md Indicators-Overview.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
@@ -390,20 +332,28 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd docs-ver
|
||||
# Published-versions table rows (crates.io / PyPI / npm): replace only the
|
||||
# version number, leaving the trailing padding + pipe intact. The '.' in
|
||||
# the quickstart pattern matches the literal backtick around the version
|
||||
# without needing a backtick in this shell string. Historical "since
|
||||
# X.Y.Z" references contain no such anchor and are never matched.
|
||||
sed -i -E "s/^(\| (crates\.io|PyPI|npm) .*\| )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" overview.md
|
||||
# Published-versions table rows (all registries): replace only the
|
||||
# version number, leaving the trailing padding + pipe intact. The
|
||||
# registry name is matched whether plain or wrapped in a markdown link
|
||||
# (\[?...\]?). The '.' in the quickstart pattern matches the literal
|
||||
# backtick around the version without needing a backtick in this shell
|
||||
# string. Historical "since X.Y.Z" references contain no such anchor and
|
||||
# are never matched.
|
||||
sed -i -E "s/^(\| \[?(crates\.io|PyPI|npm|NuGet|Maven Central|Go|r-universe)\]?.*\| )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" overview.md
|
||||
sed -i -E "s/(published crate is at version .)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" Quickstart-Rust.md
|
||||
# Quickstart-Java is the only quickstart whose install block pins a
|
||||
# version (the Maven `<version>` tag and the `org.wickra:wickra:<v>`
|
||||
# Gradle coordinate). Java publishes on every release, so it tracks the
|
||||
# same ${version}.
|
||||
sed -i -E "s|<version>[0-9]+\.[0-9]+\.[0-9]+</version>|<version>${version}</version>|" Quickstart-Java.md
|
||||
sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" Quickstart-Java.md
|
||||
if git diff --quiet; then
|
||||
echo "Docs version already at ${version}."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add overview.md Quickstart-Rust.md
|
||||
git add overview.md Quickstart-Rust.md Quickstart-Java.md
|
||||
git commit -m "chore: sync published version to ${version}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/wickra-docs failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
@@ -431,14 +381,14 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
cd webpage-count
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts
|
||||
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts
|
||||
if git diff --quiet; then
|
||||
echo "Webpage indicator count unchanged."
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add index.md .vitepress/config.ts
|
||||
git add index.md about.md .vitepress/config.ts
|
||||
git commit -m "chore: sync indicator count to ${n}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
@@ -489,6 +439,14 @@ jobs:
|
||||
sed -i -E "s/(Latest:\*\* \[.wickra(-wasm)? )[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" api/*.md
|
||||
sed -i -E "s/(text: .v)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" .vitepress/config.ts
|
||||
sed -i -E "s/(.wickra-wasm.: .\^)[0-9]+\.[0-9]+\.[0-9]+/\1${version}/" package.json
|
||||
# Java is the only target whose install snippet pins a version: the
|
||||
# Maven `<version>` tag in the api/java.md dependency block and the
|
||||
# home-page install tab in index.md, plus any `org.wickra:wickra:<v>`
|
||||
# Gradle coordinate. Java publishes on every release, so it tracks the
|
||||
# same ${version} as the rest. (Other languages' api/*.md carry a
|
||||
# "Latest:" line handled above; Java uses the XML snippet instead.)
|
||||
sed -i -E "s|<version>[0-9]+\.[0-9]+\.[0-9]+</version>|<version>${version}</version>|" api/java.md index.md
|
||||
sed -i -E "s|(org\.wickra:wickra:)[0-9]+\.[0-9]+\.[0-9]+|\1${version}|" api/java.md index.md
|
||||
# Keep package-lock.json in sync with the package.json bump. The site's
|
||||
# Cloudflare build runs `npm clean-install` (npm ci), which hard-fails
|
||||
# with EUSAGE if the lockfile still pins the previous wickra-wasm —
|
||||
@@ -507,7 +465,7 @@ jobs:
|
||||
fi
|
||||
git config user.name "wickra-bot"
|
||||
git config user.email "wickra-bot@users.noreply.github.com"
|
||||
git add api/*.md .vitepress/config.ts package.json package-lock.json
|
||||
git add api/*.md index.md .vitepress/config.ts package.json package-lock.json
|
||||
git commit -m "chore: sync published version to ${version}"
|
||||
if ! git push 2>/dev/null; then
|
||||
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
|
||||
|
||||
@@ -14,7 +14,7 @@ jobs:
|
||||
name: metadata audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
actions: read # online audits resolve referenced actions
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ bindings/node/npm-debug.log*
|
||||
|
||||
# WASM build output
|
||||
bindings/wasm/pkg/
|
||||
bindings/wasm/pkg-node/
|
||||
|
||||
# Python venv
|
||||
.venv/
|
||||
|
||||
+40
-23
@@ -7,7 +7,7 @@ for the day-to-day workflow.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
Wickra is a Cargo workspace of three Rust crates plus three binding crates.
|
||||
Wickra is a Cargo workspace of three Rust crates plus four binding crates.
|
||||
The split is deliberate: every concern that one user might want to disable
|
||||
or replace lives behind a separate crate boundary.
|
||||
|
||||
@@ -20,23 +20,36 @@ or replace lives behind a separate crate boundary.
|
||||
┌───────────▼──────────┐ ┌──────────▼─────────┐
|
||||
│ wickra-core │ │ wickra-data │
|
||||
│ indicator engine │ │ i/o + aggregation │
|
||||
│ • 214 indicators │ │ • CSV reader │
|
||||
│ • 514 indicators │ │ • CSV reader │
|
||||
│ • Indicator trait │ │ • Tick aggregator │
|
||||
│ • BatchExt impl │ │ • Resampler │
|
||||
│ • OHLCV / Candle │ │ • Live feeds │
|
||||
│ no I/O, no deps │ │ optional features │
|
||||
└──────────────────────┘ └────────────────────┘
|
||||
▲
|
||||
│ (every binding wraps the same core)
|
||||
│
|
||||
┌────────────┴───────────┬─────────────────────┐
|
||||
│ │ │
|
||||
┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐
|
||||
│ Python │ │ Node │ │ WASM │
|
||||
│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │
|
||||
└─────────┘ └──────────────┘ └────────────────┘
|
||||
│ every binding wraps the same core
|
||||
┌────────────┼────────────┬────────────────┐
|
||||
│ │ │ │
|
||||
┌──▼───────┐ ┌──▼───────┐ ┌──▼───────────┐ ┌──▼──────────────────┐
|
||||
│ Python │ │ Node.js │ │ WASM │ │ C ABI (cbindgen) │
|
||||
│ (PyO3) │ │ (napi-rs)│ │(wasm-bindgen)│ │ cdylib + header │
|
||||
└──────────┘ └──────────┘ └──────────────┘ └─────────┬───────────┘
|
||||
│ linked by
|
||||
┌──────────▼──────────┐
|
||||
│ C · C++ · C# · Go │
|
||||
│ · Java · R │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
Python, Node.js and WASM are *native* Rust bindings (PyO3 / napi-rs /
|
||||
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
|
||||
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
|
||||
downstream languages link that one artifact rather than each re-wrapping the
|
||||
core. C and C++ link it directly; the **C#** binding (`bindings/csharp`,
|
||||
on NuGet), the **Go** binding (`bindings/go`, cgo), the **R** binding
|
||||
(`bindings/r`, `.Call`) and the **Java** binding (`bindings/java`, the Java FFM
|
||||
API / Panama, on Maven Central) are all generated from `wickra.h`.
|
||||
|
||||
| Crate | Path | What it owns | Public deps |
|
||||
|---|---|---|---|
|
||||
| `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) |
|
||||
@@ -44,7 +57,8 @@ or replace lives behind a separate crate boundary.
|
||||
| `wickra-data` | `crates/wickra-data` | CSV reader, tick aggregator, resampler, live exchange feeds (feature-gated) | `tokio`, `tokio-tungstenite` (live), `serde_json` |
|
||||
| `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` |
|
||||
| `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` |
|
||||
| `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` |
|
||||
| `wickra-wasm` | `bindings/wasm` | WASM binding | `wasm-bindgen`, depends on `wickra-core` |
|
||||
| `wickra-c` | `bindings/c` | C ABI hub — `cdylib`/`staticlib` + generated `wickra.h` (cbindgen) | depends on `wickra-core` |
|
||||
| `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
|
||||
|
||||
The `fuzz/` directory is **excluded** from the workspace (it has its own
|
||||
@@ -189,14 +203,17 @@ typed object arrays for Node/WASM).
|
||||
|
||||
A handful of indicators need care beyond naive accumulation:
|
||||
|
||||
- **Welford's online variance** is used in `StdDev`, `Variance`, `ZScore`,
|
||||
`BollingerBands`, and several others. Standard sum-of-squares is
|
||||
catastrophically lossy for low-variance inputs; Welford's recurrence
|
||||
keeps O(eps) error.
|
||||
- **Kahan summation** is used wherever rolling sums could span > 1e6
|
||||
elements without resetting — currently only Hurst-exponent's R/S
|
||||
chunks. Most rolling sums are bounded by the window size and don't need
|
||||
it.
|
||||
- **Rolling variance is running-sum, not Welford.** The sliding-window
|
||||
variance family — `StdDev`, `Variance`, `ZScore`, `Bollinger` — keeps
|
||||
running `Σx` and `Σx²` over the window and reports `var = Σx²/n − mean²`,
|
||||
clamping to zero the tiny negative values floating-point cancellation can
|
||||
produce. `Bollinger` periodically reseeds its `Σx²` from the live window
|
||||
so error cannot accumulate over a long stream. Welford's online algorithm
|
||||
(an incremental `M2` accumulator) does **not** transfer cleanly to a
|
||||
sliding window — removing the oldest point from `M2` is numerically
|
||||
unstable — so it is used only where the statistic is *not* a fixed
|
||||
window: `IntradayVolatilityProfile` and `SeasonalZScore` accumulate
|
||||
per-bucket variance that way.
|
||||
- **Logarithm bases** matter for some indicators (Hurst, MFI). Wickra
|
||||
uses natural log everywhere unless the reference math explicitly
|
||||
requires `log10` or `log2` — and then it documents the choice in the
|
||||
@@ -210,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 ───┐
|
||||
@@ -313,9 +330,9 @@ re-discovering them.
|
||||
- **`FAMILIES` (from PR #60) is hand-maintained.** Adding a new
|
||||
indicator requires a separate entry in `FAMILIES`. The
|
||||
`total_count_matches_expected` test will fail if you forget.
|
||||
- **WASM does not have automated tests yet.** Smoke-validated only
|
||||
through the manual examples. Adding `wasm-bindgen-test` coverage is
|
||||
on the roadmap.
|
||||
- **WASM is covered by `wasm-bindgen-test`.** `bindings/wasm/src/lib.rs`
|
||||
carries 21 in-crate tests (run under `wasm-pack test` in CI), in
|
||||
addition to the manual browser examples.
|
||||
|
||||
For the high-level project goals see [`ROADMAP.md`](ROADMAP.md); for
|
||||
day-to-day contribution mechanics see [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
# Benchmarks
|
||||
|
||||
Read these as **relative** speedups on identical input — absolute µs depend on
|
||||
CPU, memory clock and OS scheduler, not a universal contract. **Streaming is the
|
||||
headline**: it is where Wickra's design pays off and where the gap is measured in
|
||||
orders of magnitude, not percent. The batch numbers come second and are shown
|
||||
honestly — the leanest crates edge Wickra out on the simple recurrences, and that
|
||||
is a deliberate trade for warmup/NaN semantics, not a ceiling.
|
||||
|
||||
- **Reproduced on:** Windows 11 Pro 26200, AMD Ryzen 9 9950X, 64 GB DDR5,
|
||||
Rust 1.92 (release: `lto = "fat"`, `codegen-units = 1`), Python 3.12.
|
||||
- **Reproduce yourself:**
|
||||
- Rust core vs Rust crates: `cargo bench -p wickra-bench`
|
||||
- Python vs Python libs: `pip install -e bindings/python[bench]` then
|
||||
`python -m benchmarks.compare_libraries` (auto-detects installed peers).
|
||||
|
||||
## 1. Streaming — the structural win
|
||||
|
||||
Live trading feeds one tick at a time. Wickra updates every indicator in **O(1)**;
|
||||
batch-only libraries (TA-Lib, tulipy, finta, pandas-ta) have no incremental API
|
||||
and must recompute the whole history on every tick. Only `talipp` (Python) and
|
||||
`ta-rs` / `yata` (Rust) carry real per-tick state. This is the gap the library
|
||||
was built to expose.
|
||||
|
||||
**Python — per-tick latency** (seed 5 000 bars, then feed ticks one at a time):
|
||||
|
||||
| Indicator | **★ Wickra** | talipp | TA-Lib (recompute) |
|
||||
|------------------|------------------:|------------------|-----------------------|
|
||||
| SMA(20) | **0.089 µs ★** | 0.96 µs (11×) | 422 µs (4 700×) |
|
||||
| EMA(20) | **0.111 µs ★** | 1.19 µs (11×) | 430 µs (3 900×) |
|
||||
| RSI(14) | **0.061 µs ★** | 0.95 µs (16×) | 298 µs (4 900×) |
|
||||
| MACD(12, 26, 9) | **0.079 µs ★** | 3.30 µs (42×) | 327 µs (4 100×) |
|
||||
| Bollinger(20, 2) | **0.089 µs ★** | 4.97 µs (56×) | 296 µs (3 300×) |
|
||||
|
||||
Against the only other incremental Python peer Wickra is **11–56× faster**;
|
||||
against the recompute-on-every-tick libraries it is **2 800–19 000× faster**
|
||||
(`finta` RSI hits 19 000×). tulipy / pandas-ta land in the same recompute band
|
||||
as TA-Lib.
|
||||
|
||||
**Rust — per-tick latency** (whole 50 000-bar series, lower = faster):
|
||||
|
||||
| Indicator | **★ Wickra** | kand | ta-rs | yata |
|
||||
|------------------|------------------:|-----:|------:|-----:|
|
||||
| SMA(20) | 50 | 38 | 47 | 38 |
|
||||
| EMA(20) | 154 | 69 | 56 | 69 |
|
||||
| RSI(14) | 164 | 216 | 74 | — |
|
||||
| MACD(12, 26, 9) | 275 | 143 | 66 | — |
|
||||
| Bollinger(20, 2) | **128 ★** | 248 | 168 | — |
|
||||
| ATR(14) | 152 | 166 | 61 | — |
|
||||
|
||||
`ta-rs` hands back a bare `f64` from the first tick with no warmup and no
|
||||
validation; it leads several rows by giving those guarantees up. Against `kand`,
|
||||
Wickra wins streaming RSI, Bollinger and ATR. `yata` exposes only SMA/EMA as
|
||||
raw-value methods, so its other rows are omitted rather than faked.
|
||||
|
||||
## 2. Batch — competitive, not the headline
|
||||
|
||||
Whole series in one call. Here hand-tuned C (`tulipy`, TA-Lib) and the leanest
|
||||
Rust crate (`kand`) win the simple recurrences — Wickra trades a few µs per pass
|
||||
for the `None`-warmup, NaN-safety and bit-exact `batch == streaming` guarantees
|
||||
none of them keep. It still wins several rows outright and beats the rest of the
|
||||
field everywhere.
|
||||
|
||||
**Python** (20 000-bar pass, µs/op, lower = faster):
|
||||
|
||||
| Indicator | Wickra | TA-Lib | tulipy | pandas-ta | finta |
|
||||
|------------------|---------:|---------:|---------:|----------:|---------:|
|
||||
| SMA(20) | 22.2 | **15.6** | 15.9 | 32.7 | 290.1 |
|
||||
| EMA(20) | 30.5 | **30.4** | 30.9 | 46.7 | 198.5 |
|
||||
| RSI(14) | 52.3 | 72.0 | **34.2** | 88.8 | 812.3 |
|
||||
| MACD(12, 26, 9) | 129.8 | 111.1 | **38.4** | 286.8 | 716.7 |
|
||||
| Bollinger(20, 2) | 87.2 | 74.6 | **37.9** | 474.3 | 1255.5 |
|
||||
| ATR(14) | 74.7 | 87.3 | **35.5** | — | 3496.4 |
|
||||
|
||||
Wickra beats pandas-ta and finta on every row and TA-Lib on RSI and ATR;
|
||||
tulipy's SIMD C (and TA-Lib on SMA/EMA) lead the remaining rows.
|
||||
|
||||
**Rust** (50 000-bar pass, µs, lower = faster). Only Wickra and `kand` expose a
|
||||
batch API; `ta-rs` and `yata` are streaming-only:
|
||||
|
||||
| Indicator | **★ Wickra** | kand |
|
||||
|------------------|------------------:|-------:|
|
||||
| SMA(20) | 53 | **41** |
|
||||
| EMA(20) | 111 | **71** |
|
||||
| RSI(14) | **221 ★** | 259 |
|
||||
| MACD(12, 26, 9) | 533 | **327** |
|
||||
| Bollinger(20, 2) | **404 ★** | 460 |
|
||||
| ATR(14) | **122 ★** | 169 |
|
||||
|
||||
Run the suite yourself:
|
||||
|
||||
```bash
|
||||
cargo bench -p wickra-bench # Rust core vs kand / ta-rs / yata
|
||||
pip install -e bindings/python[bench] # Python peers
|
||||
python -m benchmarks.compare_libraries
|
||||
```
|
||||
|
||||
## 3. Per-binding throughput — the cost of the boundary
|
||||
|
||||
The sections above compare Wickra against other libraries, which only exists for
|
||||
Python and Rust (there is no comparable streaming TA library for C, C++, C#, Go, Java,
|
||||
R or WASM to benchmark against). Every binding calls the **same** Rust
|
||||
core, so these per-binding benchmarks are **not** a speed claim and **not** a
|
||||
cross-library ratio — they document the raw cost of crossing each language's FFI
|
||||
boundary, in million updates per second (Mupd/s).
|
||||
|
||||
Each binding ships a small `throughput` benchmark that feeds a synthetic OHLCV
|
||||
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 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 bindings — the 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
|
||||
per-call marshalling (cgo, FFM, the R/WASM boundary) costs more per tick.
|
||||
|
||||
The Rust core ships the same benchmark with **no** FFI boundary
|
||||
(`examples/rust/.../throughput.rs`) — it is the ceiling each binding is measured
|
||||
against and the value the batch paths converge towards.
|
||||
|
||||
`SMA(20)`, 200 000 bars, median of 3 runs, on the reference machine (Windows 11,
|
||||
AMD Ryzen 9 9950X):
|
||||
|
||||
| Target | streaming (Mupd/s) | batch (Mupd/s) |
|
||||
|----------------------|-------------------:|---------------:|
|
||||
| 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 (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).
|
||||
|
||||
Run any target's benchmark (build the C ABI library first where it links one):
|
||||
|
||||
```bash
|
||||
cargo run -p wickra-examples --release --bin throughput # Rust core baseline (no FFI)
|
||||
|
||||
node bindings/node/benchmarks/throughput.js # native napi-rs
|
||||
( cd bindings/python && python -m benchmarks.throughput ) # native PyO3
|
||||
( cd bindings/wasm && wasm-pack build --target nodejs --out-dir pkg-node --release ) \
|
||||
&& node bindings/wasm/benchmarks/throughput.mjs # wasm boundary
|
||||
|
||||
cargo build -p wickra-c --release # the C ABI hub
|
||||
cmake -S bindings/c/benchmarks -B build/cbench && cmake --build build/cbench \
|
||||
&& ./build/cbench/throughput # raw C ABI
|
||||
dotnet run -c Release --project bindings/csharp/benchmarks # C# (P/Invoke)
|
||||
( cd bindings/go/benchmarks && go run . ) # Go (cgo)
|
||||
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
|
||||
```
|
||||
+797
-1
@@ -7,6 +7,754 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [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[t−lookback]`; 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
|
||||
- C ABI hub: every indicator now exposes `wickra_<ind>_warmup_period` and
|
||||
`wickra_<ind>_is_ready`, closing the gap with the native bindings (which
|
||||
already had them). The C-ABI languages surface them idiomatically: C# `int
|
||||
WarmupPeriod()` / `bool IsReady()`, Go `WarmupPeriod()` / `IsReady()`, Java
|
||||
`int warmupPeriod()` / `boolean isReady()`, and R `warmup_period()` /
|
||||
`is_ready()` generics. The alt-chart bar builders are excluded by design (a
|
||||
candle can complete 0..n bars, so they have no warmup).
|
||||
- Runnable rustdoc examples for 23 indicators that previously lacked one.
|
||||
- A Requirements reference documenting the minimum supported version per
|
||||
language — a new page in the documentation site plus README, marketing-site
|
||||
and organization-profile sections.
|
||||
|
||||
### Changed
|
||||
- Raised the minimum Node.js version to 20 — Node 18 reached end-of-life. The
|
||||
prebuilt N-API addon is now tested on the active LTS lines (22 and 24).
|
||||
- The Java binding now builds on the JDK 25 LTS in CI (JDK 22 reached
|
||||
end-of-life); the published bytecode still targets Java 22, so the runtime
|
||||
requirement is unchanged.
|
||||
- Standardised programming-language naming and ordering across all docs, READMEs,
|
||||
the documentation site, marketing site, organization profile and GitHub
|
||||
repository descriptions. Canonical list:
|
||||
`Rust, Python, Node.js, WASM, C, C++, C#, Go, Java, R`. Uses C# (not .NET) as
|
||||
the language label, lists C and C++ separately, prefers `Node.js` and `WASM` in
|
||||
prose, and frames the C ABI as a hub (`C ABI hub → …`) rather than a
|
||||
language-list entry. Documentation only — no code or public API changes.
|
||||
- Python binding: upgraded `pyo3` and `rust-numpy` from 0.28 to 0.29. No public
|
||||
API changes; the full test suite passes unchanged.
|
||||
|
||||
### Fixed
|
||||
- Corrected the internal casing of the `RelativeStrengthAB` binding wrappers,
|
||||
which used `...Ab` (`WasmRelativeStrengthAb` in the WASM crate,
|
||||
`RelativeStrengthAbNode` in the Node crate) while every other surface uses the
|
||||
acronym `AB`. The published JS/WASM class name was already `RelativeStrengthAB`
|
||||
(set via `js_name`/`js_class`), so the runtime API is unchanged; the only
|
||||
visible change is the auto-generated TypeScript type alias, renamed
|
||||
`RelativeStrengthAbNode` → `RelativeStrengthABNode` in `index.d.ts`.
|
||||
|
||||
### Security
|
||||
- Resolved the pyo3 advisories RUSTSEC-2026-0176 (out-of-bounds read in
|
||||
`PyList`/`PyTuple` `nth`/`nth_back`) and RUSTSEC-2026-0177 (missing `Sync`
|
||||
bound on `PyCFunction::new_closure`) by upgrading to pyo3 0.29, which fixes
|
||||
both. The upgrade was previously blocked upstream by rust-numpy 0.28 pinning
|
||||
pyo3 `^0.28`; rust-numpy 0.29 lifts that pin. The not-affected exceptions are
|
||||
removed from `deny.toml` and `osv-scanner.toml`.
|
||||
|
||||
## [0.9.0] - 2026-06-13
|
||||
|
||||
Maintenance release: Java build-dependency updates and CI/Dependabot
|
||||
housekeeping only. No library code or public API changes.
|
||||
|
||||
### Changed
|
||||
- Java binding: upgraded the test framework to JUnit Jupiter 6.1.0 (from
|
||||
5.10.2) and bumped the Maven build plugins — `maven-compiler-plugin`
|
||||
3.13.0 → 3.15.0, `maven-surefire-plugin` 3.2.5 → 3.5.6, `maven-jar-plugin`
|
||||
3.4.1 → 3.5.0, `maven-source-plugin` 3.3.1 → 3.4.0, `maven-javadoc-plugin`
|
||||
3.7.0 → 3.12.0, and `maven-gpg-plugin` 3.2.4 → 3.2.8.
|
||||
- Java benchmarks and examples: bumped `maven-compiler-plugin` to 3.15.0 and
|
||||
`exec-maven-plugin` to 3.6.3; examples bumped `jackson-databind` 2.17.1 →
|
||||
2.22.0.
|
||||
- Grouped Dependabot updates per ecosystem into a single pull request and
|
||||
extended tracking to the NuGet (C#) binding and the Node/Go examples.
|
||||
|
||||
|
||||
## [0.8.9] - 2026-06-12
|
||||
|
||||
Maintenance release: supply-chain and CI housekeeping only. No library code or
|
||||
public API changes.
|
||||
|
||||
### Security
|
||||
- Triaged the pyo3 advisories RUSTSEC-2026-0176 (out-of-bounds read in
|
||||
`PyList`/`PyTuple` `nth`/`nth_back`) and RUSTSEC-2026-0177 (missing `Sync`
|
||||
bound on `PyCFunction::new_closure`) as **not affecting Wickra**: neither
|
||||
vulnerable API is reachable from the Python binding. Both are fixed in pyo3
|
||||
0.29, but rust-numpy 0.28 pins pyo3 `^0.28`, so the upgrade is blocked
|
||||
upstream; the advisories are recorded with their not-affected rationale in
|
||||
`deny.toml` and `osv-scanner.toml` and will be cleared once rust-numpy 0.29
|
||||
ships.
|
||||
|
||||
### Changed
|
||||
- Java binding: bumped `central-publishing-maven-plugin` 0.5.0 → 0.10.0 (the
|
||||
Maven Central publishing plugin used at release time).
|
||||
- Bumped the SHA-pinned GitHub Actions used in CI (`actions/checkout`,
|
||||
`actions/setup-go`, `actions/setup-java`, `github/codeql-action`,
|
||||
`taiki-e/install-action`) to their latest releases.
|
||||
- Added a Maven ecosystem to Dependabot so the Java binding's build plugins and
|
||||
dependencies are tracked going forward.
|
||||
|
||||
|
||||
## [0.8.8] - 2026-06-11
|
||||
### Fixed
|
||||
- R binding: declare `Depends: R (>= 2.10)`, clearing the `R CMD check` warning
|
||||
("package needs dependence on R (>= 2.10)") that the bundled, lazy-loaded
|
||||
`sample_ohlcv` dataset triggers on r-universe / CRAN.
|
||||
|
||||
## [0.8.7] - 2026-06-11
|
||||
### Added
|
||||
- R binding: a *Getting started* vignette and a synthetic `sample_ohlcv` example
|
||||
dataset, giving new users a runnable, self-contained walkthrough and populating
|
||||
the R-universe Articles and Datasets tabs. The vignette's code is exercised in
|
||||
CI so a broken example is caught before the published build.
|
||||
|
||||
## [0.8.6] - 2026-06-11
|
||||
### Changed
|
||||
- Package registry metadata for better discoverability:
|
||||
- R (R-universe): added the R-universe URL and `X-schema.org-keywords` to the
|
||||
R `DESCRIPTION`, plus a package logo at `bindings/r/man/figures/logo.png`
|
||||
(pkgdown convention).
|
||||
- Python (PyPI): added a `Documentation` project URL.
|
||||
- C# (NuGet): added a package icon via `PackageIcon`.
|
||||
|
||||
## [0.8.5] - 2026-06-11
|
||||
### Fixed
|
||||
- The R binding's golden-fixture parity test now skips gracefully when the shared
|
||||
`testdata/golden` fixtures are not bundled with the package — standalone
|
||||
r-universe / CRAN builds package only `bindings/r`, so the repo-root fixtures
|
||||
are unreachable there. The parity stays enforced by the repository CI, where
|
||||
the fixtures are present.
|
||||
|
||||
## [0.8.4] - 2026-06-11
|
||||
### Fixed
|
||||
- A single non-finite (NaN/inf) tick no longer poisons indicator state.
|
||||
The 16 pairwise running-sum/buffer indicators fixed first (`Beta`,
|
||||
`BetaNeutralSpread`, `Cointegration`, `HasbrouckInformationShare`,
|
||||
`PearsonCorrelation`, `RollingCorrelation`, `RollingCovariance`,
|
||||
`DistanceSsd`, `GrangerCausality`, `KendallTau`, `LeadLagCrossCorrelation`,
|
||||
`OuHalfLife`, `SpearmanCorrelation`, `SpreadAr1Coefficient`, `SpreadHurst`,
|
||||
`VarianceRatio`) were joined by 38 more scalar/pairwise indicators the new
|
||||
property harness surfaced (the linear-regression family, rolling quantiles
|
||||
and IQR, `Variance`/`StdDev`-derived stats, `Kurtosis`/`Skewness`, the
|
||||
trailing stops, `KalmanHedgeRatio`, `SpreadBollingerBands`, and more). Every
|
||||
`f64` / `(f64, f64)` indicator now rejects non-finite input and returns
|
||||
`None`, matching the streaming-robustness guarantee — and the harness enforces
|
||||
it going forward.
|
||||
|
||||
### Added
|
||||
- Catalogue-wide property-based invariant harness
|
||||
(`crates/wickra-core/tests/invariants.rs`) asserting `batch == streaming`,
|
||||
`reset == fresh`, and non-finite-input rejection for every indicator and
|
||||
bar-builder.
|
||||
|
||||
### Changed
|
||||
- CI: every job now has a runtime cap and the historically flaky Node test step
|
||||
auto-retries, so a wedged runner fails fast instead of hanging for hours.
|
||||
- Documentation accuracy fixes in `SECURITY.md`, `ARCHITECTURE.md`, and
|
||||
`THREAT_MODEL.md` (supported version, indicator count, WASM test coverage,
|
||||
numerical-stability notes, and the C-ABI panic strategy).
|
||||
|
||||
## [0.8.3] - 2026-06-10
|
||||
### Added
|
||||
- **Per-binding throughput benchmarks** — every target now ships a `throughput`
|
||||
benchmark mirroring the Node `throughput.js`: streaming and batch
|
||||
updates-per-second for `SMA(20)`, `ATR(14)` and `MACD(12,26,9)` over a
|
||||
synthetic OHLCV series. New for Python (`bindings/python/benchmarks/`), C
|
||||
(`bindings/c/benchmarks/`), C# (`bindings/csharp/benchmarks/`), Go
|
||||
(`bindings/go/benchmarks/`), Java (`bindings/java/benchmarks/`), R
|
||||
(`bindings/r/benchmarks/`), WebAssembly (`bindings/wasm/benchmarks/`) and the
|
||||
Rust core baseline (`examples/rust/.../throughput.rs`, no FFI). They measure
|
||||
each binding's FFI overhead — the same Rust core runs underneath all of them —
|
||||
and are documented in [BENCHMARKS.md](BENCHMARKS.md) §3, not a cross-library
|
||||
speed claim.
|
||||
- **C ABI archetype test** — `examples/c/archetypes.c` exercises one indicator
|
||||
per FFI archetype (scalar, multi-output, bars, profile, array input) through
|
||||
the C boundary, matching the Go/R/Java suites.
|
||||
|
||||
## [0.8.2] - 2026-06-10
|
||||
### Fixed
|
||||
- **R binding builds for WebAssembly** — `bindings/r/configure` now builds the
|
||||
C ABI from source for the `wasm32-unknown-emscripten` target (r-universe /
|
||||
webR) using the build image's cargo + emscripten, instead of failing with
|
||||
"unsupported OS Emscripten". rayon is dropped on wasm via
|
||||
`--no-default-features`; the indicators are pure computation, so the serial
|
||||
path is functionally identical.
|
||||
|
||||
## [0.8.1] - 2026-06-10
|
||||
### Fixed
|
||||
- **`wickra-go` license** — the release-time Go module mirror now ships the dual
|
||||
`LICENSE-MIT` and `LICENSE-APACHE` files, so pkg.go.dev detects a
|
||||
redistributable license for `github.com/wickra-lib/wickra-go`. The previous
|
||||
mirror shipped no license file.
|
||||
|
||||
## [0.8.0] - 2026-06-09
|
||||
### Added
|
||||
- **Standalone `wickra-go` module** — the Go binding is now mirrored to a
|
||||
dedicated `github.com/wickra-lib/wickra-go` repository on every release, with
|
||||
the prebuilt C ABI libraries committed per platform under
|
||||
`lib/<goos>_<goarch>/` and the C ABI header vendored alongside the source, so
|
||||
`go get github.com/wickra-lib/wickra-go` builds with no extra steps. The
|
||||
in-repo `bindings/go` module is unchanged for repo-clone workflows.
|
||||
|
||||
### Changed
|
||||
- **Go binding (`bindings/go`) is self-contained** — the C ABI header is now
|
||||
vendored inside the module (`bindings/go/include/wickra.h`) instead of being
|
||||
referenced from the parent `bindings/c` directory, and the cgo link flags
|
||||
resolve the prebuilt library per `GOOS`/`GOARCH` under `lib/<goos>_<goarch>/`.
|
||||
This removes the dependency on a full repository checkout for building the
|
||||
module.
|
||||
|
||||
## [0.7.9] - 2026-06-09
|
||||
### Added
|
||||
- **Java binding (`bindings/java`)** — a Java binding reaching the C ABI hub
|
||||
through the Java Foreign Function & Memory API (Panama, `java.lang.foreign`,
|
||||
final in Java 22) rather than JNI or jextract, exposing all 514 indicators as
|
||||
idiomatic `AutoCloseable` classes. The downcall handles, per-indicator
|
||||
wrappers and output records are generated from `wickra.h`; the opaque handle is
|
||||
a `MemorySegment` freed by a `java.lang.ref.Cleaner` action. Ships a full
|
||||
example suite mirroring the C, C#, Go and R examples; published to Maven
|
||||
Central as `org.wickra:wickra`.
|
||||
|
||||
## [0.7.8] - 2026-06-09
|
||||
### Added
|
||||
- **R binding (`bindings/r`)** — an R package reaching the C ABI hub through R's
|
||||
native `.Call` interface, exposing all 514 indicators as constructors that
|
||||
return a `wickra_indicator` object with `update`/`batch`/`reset` methods. The
|
||||
C glue and R wrappers are generated from `wickra.h`; the native handle is freed
|
||||
by a registered finalizer. Ships a full example suite mirroring the C, C# and
|
||||
Go examples; distributed for r-universe / source install.
|
||||
|
||||
## [0.7.7] - 2026-06-09
|
||||
### Added
|
||||
- **Go binding (`bindings/go`)** — a cgo binding over the C ABI hub exposing all
|
||||
514 indicators as idiomatic types with `New<Indicator>` constructors and
|
||||
`Update`/`Batch`/`Reset`/`Close` methods, generated from `wickra.h`. Handles are
|
||||
freed by `Close()` with a `runtime.SetFinalizer` backstop. Ships a full example
|
||||
suite mirroring the C and C# examples; distributed as a subdirectory module
|
||||
(`go get github.com/wickra-lib/wickra/bindings/go`).
|
||||
|
||||
## [0.7.6] - 2026-06-09
|
||||
### Added
|
||||
- **C# / .NET binding (`bindings/csharp`)** — the first language stecker on the
|
||||
C ABI hub. Exposes all 514 indicators as idiomatic `IDisposable` classes via
|
||||
`[LibraryImport]` source-generated P/Invoke, generated from `wickra.h`. Ships
|
||||
on NuGet as `Wickra` with prebuilt native libraries for six target triples
|
||||
(win/linux/osx × x64/arm64), plus a full example suite mirroring the C examples.
|
||||
|
||||
## [0.7.5] - 2026-06-09
|
||||
### Added
|
||||
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
|
||||
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
|
||||
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
|
||||
links against, complementing the native Python/Node/WASM bindings. Ships a
|
||||
full example suite (streaming, backtest, multi-timeframe, OpenMP parallel
|
||||
fan-out, three educational strategies, and Binance fetch/live over `curl`)
|
||||
mirroring the other bindings, plus an optional `wickra.hpp` C++ RAII wrapper.
|
||||
|
||||
## [0.7.4] - 2026-06-08
|
||||
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
|
||||
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
|
||||
- **Imbalance** — Imbalance bars (tick-rule signed imbalance threshold) (`IMBALANCE_BARS`).
|
||||
- **Dollar** — Dollar bars (fixed traded value per bar, Lopez de Prado) (`DOLLAR_BARS`).
|
||||
- **Volume** — Volume bars (fixed traded volume per bar) (`VOLUME_BARS`).
|
||||
- **Tick** — Tick bars (fixed candle count per bar) (`TICK_BARS`).
|
||||
- **Range** — Range bars (fixed price-range bricks) (`RANGE_BARS`).
|
||||
|
||||
## [0.7.3] - 2026-06-08
|
||||
- **M2Measure** — M2 measure (Modigliani; Sharpe expressed in benchmark return units) (`M2Measure`).
|
||||
- **UpsidePotentialRatio** — Upside Potential Ratio (upside mean over downside deviation) (`UpsidePotentialRatio`).
|
||||
- **GainToPainRatio** — Gain-to-Pain Ratio (sum of returns over sum of losses) (`GainToPainRatio`).
|
||||
- **CommonSenseRatio** — Common Sense Ratio (tail ratio times gain-to-pain) (`CommonSenseRatio`).
|
||||
- **KRatio** — K-Ratio (Kestner; equity-curve slope over its standard error) (`KRatio`).
|
||||
- **TailRatio** — Tail Ratio (95th over absolute 5th return percentile) (`TailRatio`).
|
||||
- **MartinRatio** — Martin Ratio (Ulcer Performance Index; return over RMS drawdown) (`MartinRatio`).
|
||||
- **BurkeRatio** — Burke Ratio (return over root-sum-squared drawdowns) (`BurkeRatio`).
|
||||
- **SterlingRatio** — Sterling Ratio (mean return over average drawdown) (`SterlingRatio`).
|
||||
|
||||
## [0.7.2] - 2026-06-08
|
||||
- **Composite Profile** — multi-session composite volume profile exposing POC, VAH and VAL (`CompositeProfile`).
|
||||
- **High/Low Volume Nodes** — highest- and lowest-volume price nodes in the profile (`HighLowVolumeNodes`).
|
||||
- **Profile Shape** — profile shape classification (b/P/D normal) as a numeric code (`ProfileShape`).
|
||||
- **Single Prints** — count of single-print (low-activity) price levels in the profile (`SinglePrints`).
|
||||
- **Naked POC** — most recent untouched (naked) point of control level (`NakedPoc`).
|
||||
|
||||
## [0.7.1] - 2026-06-08
|
||||
- **Open-Interest Momentum** — rate-of-change of open interest over a rolling window (`OpenInterestMomentum`).
|
||||
- **Funding-Implied APR** — annualised funding rate (per-interval funding times intervals per year) (`FundingImpliedApr`).
|
||||
- **Perpetual Premium Index** — relative premium of the mark price over the index price (`PerpetualPremiumIndex`).
|
||||
- **OI-to-Volume Ratio** — open interest divided by taker volume (position turnover proxy) (`OiToVolumeRatio`).
|
||||
- **Estimated Leverage Ratio** — open interest divided by aggregate long+short position size (leverage proxy) (`EstimatedLeverageRatio`).
|
||||
|
||||
## [0.7.0] - 2026-06-08
|
||||
- **Hasbrouck Information Share** — variance-ratio proxy for each venue's share of price discovery (Hasbrouck information share) (`HasbrouckInformationShare`).
|
||||
- **PIN** — probability of informed trading from rolling buy/sell imbalance (EKOP single-window estimator) (`Pin`).
|
||||
- **Trade-Sign Autocorrelation** — lag-1 autocorrelation of the signed trade aggressor (order-flow persistence) (`TradeSignAutocorrelation`).
|
||||
|
||||
## [0.6.9] - 2026-06-08
|
||||
- **Tristar** — a three-doji star reversal: three consecutive dojis with the middle gapped above (bearish) or below (bullish) its neighbours (`Tristar`).
|
||||
- **Harami Cross** — a Harami whose second candle is a contained doji, a stronger reversal than a plain Harami (`HaramiCross`).
|
||||
- **Tower Top/Bottom** — a tall bar, a small pause bar, then a tall opposite bar marking a reversal (`TowerTopBottom`).
|
||||
- **Frying Pan Bottom** — a rounded (U-shaped) accumulation base over the lookback window, confirmed when price recovers above the rim (`FryPanBottom`).
|
||||
- **Dumpling Top** — a rounded (dome-shaped) distribution top over the lookback window, confirmed when price breaks below the start (`DumplingTop`).
|
||||
- **New Price Lines** — flags a run of N consecutive new closing highs (+1) or lows (-1), the eight/ten-new-price-lines exhaustion gauge (`NewPriceLines`).
|
||||
|
||||
## [0.6.8] - 2026-06-08
|
||||
- **Smoothed Heikin-Ashi** — a Heikin-Ashi candle computed from EMA-smoothed OHLC, damping noise into a cleaner trend candle (`SmoothedHeikinAshi`).
|
||||
- **Heikin-Ashi Oscillator** — the Heikin-Ashi candle body (`ha_close − ha_open`), optionally EMA-smoothed, as a zero-line oscillator (`HeikinAshiOscillator`).
|
||||
- **Three Line Break** — the trend direction of a line-break chart, reversing only when the close breaks the extreme of the last N lines (`ThreeLineBreak`).
|
||||
- **Equivolume** — a chart box whose height is the bar range and whose width is volume-relative, fusing price range with activity (`Equivolume`).
|
||||
- **CandleVolume** — a candle whose body is close-minus-open and whose width is volume-relative, a volume-weighted candle chart (`CandleVolume`).
|
||||
|
||||
## [0.6.7] - 2026-06-08
|
||||
- **TD Camouflage** — a DeMark qualifier flagging hidden intrabar strength or weakness against the prior close (`TDCamouflage`).
|
||||
- **TD Clop** — a DeMark two-bar open/close engulfing reversal where the bar opens beyond and closes back across the prior body (`TDClop`).
|
||||
- **TD Clopwin** — the inside-body cousin of TD Clop, marking a compression bar whose direction hints at the next move (`TDClopwin`).
|
||||
- **TD Propulsion** — a DeMark continuation thrust that opens on the trend side and closes beyond the prior bar's extreme (`TDPropulsion`).
|
||||
- **TD Trap** — an inside ("trap") bar followed by a close beyond its range, triggering a directional breakout signal (`TDTrap`).
|
||||
- **TD D-Wave** — a streaming Elliott-style swing-wave counter labelling the market's 1–5 impulse / A–C correction sequence (`TDDWave`).
|
||||
- **TD Moving Averages** — the DeMark ST1 (fast) and ST2 (slow) median-price trend ribbon whose crossover frames the trend (`TDMovingAverage`).
|
||||
|
||||
## [0.6.6] - 2026-06-08
|
||||
- **Pivot Reversal** — a breakout signal when price closes through the most recently confirmed swing pivot (`PIVOT_REVERSAL`).
|
||||
- **Volume-Weighted Support/Resistance** — a band whose edges are the volume-weighted average of recent highs and lows (`VOLUME_WEIGHTED_SR`).
|
||||
- **Andrews Pitchfork** — median line and two parallels projected from the last three swing pivots (`ANDREWS_PITCHFORK`).
|
||||
- **Murrey Math Lines** — T. H. Murrey's eighths grid over the recent trading range, each level acting as support/resistance (`MURREY_MATH_LINES`).
|
||||
- **Central Pivot Range** — the classic pivot flanked by two central levels gauging the day's expected character (`CENTRAL_PIVOT_RANGE`).
|
||||
- **Faster scalar batch paths** — `Ema`, `Rsi`, `BollingerBands`, `MacdIndicator` and `Atr` gained dedicated batch fast paths (used by the Python bindings) that strip per-element `Option`/validation overhead and the intermediate `Vec<Option<_>>` allocation, while staying *bit-for-bit* equal to replaying `update` (including the SMA/Bollinger drift-reseed). Python batch is ~2× faster on EMA/RSI/MACD/ATR; streaming is unchanged.
|
||||
- **Cross-library benchmark refresh** — `benchmarks/compare_libraries.py` now measures the median across timing rounds (`--rounds` / `--streaming-rounds`), adds `--skip-batch` / `--skip-streaming`, and drives every peer through the streaming arena (recompute for batch-only libraries). `wickra-bench` compares the batch fast paths against `kand`.
|
||||
|
||||
## [0.6.5] - 2026-06-07
|
||||
- **Autocorrelation Periodogram** — Ehlers autocorrelation periodogram: dominant cycle period estimate (`AUTOCORRPGRAM`).
|
||||
- **Even Better Sinewave** — Ehlers Even Better Sinewave: normalized cycle-phase oscillator (`EVENBETTERSINE`).
|
||||
- **Bandpass Filter** — Ehlers bandpass filter: isolates a frequency band around the dominant cycle (`BANDPASS`).
|
||||
- **Adaptive CCI** — Adaptive CCI: efficiency-ratio-adaptive CCI on typical price (`ADAPTIVECCI`).
|
||||
- **Universal Oscillator** — Ehlers Universal Oscillator: SuperSmoother-based normalized cycle oscillator (`UNIVERSALOSC`).
|
||||
- **Adaptive RSI** — Adaptive RSI: dominant-cycle-tuned RSI length (Ehlers) (`ADAPTIVERSI`).
|
||||
- **Correlation Trend Indicator** — Ehlers Correlation Trend Indicator: Pearson correlation of price vs time (`CTI`).
|
||||
- **Trendflex** — Ehlers Trendflex: trend-following companion to Reflex (`TRENDFLEX`).
|
||||
- **Reflex** — Ehlers Reflex: trend-cycle oscillator measuring slope-adjusted displacement (`REFLEX`).
|
||||
- **Highpass Filter** — Ehlers highpass filter: removes low-frequency trend, leaving cyclic component (`HIGHPASS`).
|
||||
|
||||
## [0.6.4] - 2026-06-07
|
||||
- **Kendall Tau** — Kendall rank correlation (tau-b) over a rolling window of paired observations (`KENDALLTAU`).
|
||||
- **Sample Entropy** — Sample entropy: regularity/complexity of a rolling series (Richman-Moorman) (`SAMPLEENT`).
|
||||
- **Shannon Entropy** — Shannon entropy of a rolling value distribution over fixed bins (`SHANNONENT`).
|
||||
- **Rolling Min-Max Scaler** — Rolling min-max scaler mapping the latest value to 0..1 over a rolling window (`ROLLINGMINMAX`).
|
||||
- **Jarque-Bera** — Jarque-Bera normality test statistic over a rolling window (`JARQUEBERA`).
|
||||
|
||||
## [0.6.3] - 2026-06-07
|
||||
- **Volume-Weighted MACD** — Volume-Weighted MACD: MACD computed on VWMA instead of EMA, with signal line and histogram (`VWMACD`).
|
||||
- **Better Volume** — Better Volume (VSA): classifies volume against bar spread to surface effort/result imbalance (`BETTERVOL`).
|
||||
- **Intraday Intensity Index** — Intraday Intensity Index: volume weighted by close position within the bar range (`INTRADAYINT`).
|
||||
- **Trade Volume Index** — Trade Volume Index: accumulates volume by tick direction past a min-tick threshold (distinct from TSV) (`TRADEVOLIDX`).
|
||||
- **Twiggs Money Flow** — Twiggs Money Flow: volume-weighted accumulation using true range and Wilder smoothing (distinct from CMF) (`TWIGGSMF`).
|
||||
- **Williams Accumulation/Distribution** — Williams Accumulation/Distribution: cumulative price-direction accumulator (distinct from Chaikin A/D) (`WILLIAMSAD`).
|
||||
- **Volume RSI** — Volume RSI: Wilder-style RSI computed on signed volume flow (`VOLUMERSI`).
|
||||
|
||||
## [0.6.2] - 2026-06-07
|
||||
- **Modified MA Stop** — Modified MA Stop — SMMA-ratcheted trailing stop with directional flip (`MODIFIED_MA_STOP`).
|
||||
- **Time-Based Stop** — Time-Based Stop — bar-count timer that fires after a fixed holding period (`TIME_BASED_STOP`).
|
||||
- **NRTR** — NRTR (Nick Rypock Trailing Reverse) — percentage trailing-reverse stop (`NRTR`).
|
||||
- **ATR Ratchet** — ATR Ratchet — Kaufman per-bar tightening volatility trailing stop (`ATR_RATCHET`).
|
||||
- **Elder SafeZone** — Elder SafeZone Stop — average noise-penetration trailing stop with directional flip (`ELDER_SAFE_ZONE`).
|
||||
- **Kase DevStop** — Kase DevStop volatility trailing stop using standard-deviation of two-bar true range (`KASE_DEV_STOP`).
|
||||
|
||||
## [0.6.1] - 2026-06-07
|
||||
- **Projection Oscillator** — Widner projection oscillator: close position inside the projection bands, scaled 0..100 (`ProjectionOscillator`).
|
||||
- **Projection Bands** — Widner projection bands: forward-projected high/low regression envelope (`ProjectionBands`).
|
||||
- **Median Channel** — robust median +/- multiplier*MAD envelope (`MedianChannel`).
|
||||
- **Bomar Bands** — adaptive percentage bands containing a target coverage fraction of recent closes (`BomarBands`).
|
||||
- **Quartile Bands** — rolling 25th/50th/75th-percentile (Q1/median/Q3) envelope (`QuartileBands`).
|
||||
|
||||
## [0.6.0] - 2026-06-06
|
||||
- **Volatility Cone** — volatility cone: current realized volatility within its historical min/median/max envelope (`VolatilityCone`).
|
||||
- **VolatilityRatio** — Schwager's volatility ratio: true range over the EMA of prior true ranges (`VolatilityRatio`).
|
||||
- **BipowerVariation** — jump-robust realized bipower variation (pi/2 sum of adjacent absolute log-return products) (`BipowerVariation`).
|
||||
- **VolatilityOfVolatility** — vol-of-vol: sample stddev of a rolling realized-volatility series (`VolatilityOfVolatility`).
|
||||
- **Garch11** — GARCH(1,1) conditional volatility with a long-run-variance anchor (`Garch11`).
|
||||
- **EwmaVolatility** — RiskMetrics exponentially-weighted volatility of log returns (lambda decay) (`EwmaVolatility`).
|
||||
|
||||
## [0.5.9] - 2026-06-06
|
||||
|
||||
### Added
|
||||
|
||||
- Internal Rust cross-library benchmark harness (`crates/wickra-bench`, not
|
||||
published) comparing Wickra against `kand`, `ta-rs` and `yata` on an identical
|
||||
candle series in both streaming and batch modes; wired into the nightly
|
||||
`cross-library-bench` workflow.
|
||||
- `tulipy` runners and expanded per-tick streaming coverage (SMA, EMA, RSI,
|
||||
MACD, Bollinger) in the Python `compare_libraries` benchmark.
|
||||
|
||||
### Changed
|
||||
|
||||
- Faster streaming and batch updates for SMA, Bollinger Bands, RSI, EMA and ATR
|
||||
(flat ring buffers replacing `VecDeque`, hoisted reciprocals in the Wilder
|
||||
smoothing, leaner hot state) — indicator outputs are unchanged.
|
||||
- Rewrote the README benchmark section into honest, tiered tables (Rust core vs
|
||||
the other Rust crates, and Python vs the Python ecosystem) that show where
|
||||
Wickra wins and where it loses, not only the favourable comparisons.
|
||||
|
||||
## [0.5.8] - 2026-06-04
|
||||
- **TSF Oscillator** — the percentage gap of the close to the one-bar-ahead time-series forecast, a close-relative companion to CFO (`TsfOscillator`).
|
||||
- **MACD Histogram** — the standalone macd-minus-signal bar of MACD as a scalar series (`MacdHistogram`).
|
||||
- **PPO Histogram** — the Percentage Price Oscillator with its signal EMA and the resulting zero-centered histogram (`PpoHistogram`).
|
||||
|
||||
## [0.5.7] - 2026-06-04
|
||||
- **Qstick** — Qstick (Chande), the SMA of the candle body (close − open) as a net buying/selling pressure gauge (`QSTICK`).
|
||||
- **TTM Trend** — TTM Trend (John Carter), +1/−1 by whether the close sits above the SMA of recent median prices (`TTM_TREND`).
|
||||
- **Trend Strength Index** — trend strength index, the signed r² of a linear regression of price against time (`TREND_STRENGTH_INDEX`).
|
||||
- **Polarized Fractal Efficiency** — polarized fractal efficiency (Hannula), directional trend efficiency over a fractal lookback (`POLARIZED_FRACTAL_EFFICIENCY`).
|
||||
- **Wave PM** — Wave PM (Kase), a variance-normalised peak-momentum statistic (`WAVE_PM`).
|
||||
- **Gator Oscillator** — Gator Oscillator (Bill Williams), the Alligator convergence/divergence histogram (`GATOR_OSCILLATOR`).
|
||||
- **Kase Permission Stochastic** — Kase Permission Stochastic, a double-smoothed stochastic used as a trade-permission filter (`KASE_PERMISSION_STOCHASTIC`).
|
||||
|
||||
## [0.5.6] - 2026-06-04
|
||||
- **QQE** — quantitative qualitative estimation, a smoothed RSI with an ATR-of-RSI trailing line (`QQE`).
|
||||
- **Intraday Momentum Index** — intraday momentum index (Chande), RSI on the open-to-close body (`IMI`).
|
||||
- **Elder Ray** — Elder Ray bull power and bear power around an EMA of close (`ElderRay`).
|
||||
- **Derivative Oscillator** — derivative oscillator (Constance Brown), a double-smoothed RSI histogram (`DerivativeOscillator`).
|
||||
- **RMI** — relative momentum index (RMI), RSI over a multi-bar momentum lookback (`RMI`).
|
||||
- **Stochastic CCI** — stochastic CCI, a stochastic oscillator over the CCI (`StochasticCCI`).
|
||||
- **Dynamic Momentum Index** — dynamic momentum index (Chande), a volatility-adaptive RSI (`DynamicMomentumIndex`).
|
||||
- **RSX** — RSX, a Jurik-style three-stage smoothed RSI (`RSX`).
|
||||
- **Fisher RSI** — Fisher RSI, the Fisher transform of a normalised RSI (`FisherRSI`).
|
||||
- **Disparity Index** — disparity index, the percent gap between price and its moving average (`DisparityIndex`).
|
||||
|
||||
## [0.5.5] - 2026-06-04
|
||||
- **GD** — generalized DEMA (GD), Tillson's volume-factor double EMA and the building block of T3 (`GD`).
|
||||
- **GMA** — geometric moving average (GMA), the rolling geometric mean of prices (`GMA`).
|
||||
- **Holt-Winters** — Holt's linear (double exponential) smoothing with level and trend components (`HoltWinters`).
|
||||
- **Adaptive Laguerre** — Ehlers adaptive Laguerre filter with median-error-adaptive gamma (`AdaptiveLaguerre`).
|
||||
- **Median MA** — median moving average, the rolling median of prices (`MedianMA`).
|
||||
- **EHMA** — exponential Hull moving average (EHMA), the Hull construction built from EMAs (`EHMA`).
|
||||
- **SWMA** — sine-weighted moving average (SWMA), a symmetric half-cycle sine window (`SWMA`).
|
||||
|
||||
## [0.5.4] - 2026-06-04
|
||||
- **Roll Measure** — effective spread implied by the negative serial covariance of trade-price changes (Roll 1984) (`RollMeasure`).
|
||||
- **Amihud Illiquidity** — average absolute log return per unit of traded value (price-impact liquidity proxy, Amihud 2002) (`AmihudIlliquidity`).
|
||||
- **VPIN** — volume-synchronised probability of informed trading (volume-bucketed order-flow toxicity) (`Vpin`).
|
||||
- **Order Flow Imbalance** — rolling sum of best-level order-flow events (Cont-Kukanov-Stoikov OFI) (`OrderFlowImbalance`).
|
||||
- **Expectancy** — expected return per unit of average loss (R-multiple) over a rolling window of returns (`Expectancy`).
|
||||
- **Win Rate** — fraction of strictly-positive returns over a rolling window (`WinRate`).
|
||||
- **Regime Label** — volatility-quantile regime classification: −1 calm / 0 normal / +1 stressed, by where the rolling volatility sits in its own recent distribution (`RegimeLabel`).
|
||||
- **Jump Indicator** — flags return outliers beyond `threshold ×` trailing return volatility (−1 down / 0 / +1 up) (`JumpIndicator`).
|
||||
- **Trend Label** — discrete trend state from the sign of the rolling least-squares slope (−1 / 0 / +1) (`TrendLabel`).
|
||||
- **High-Low Range** — bar high-low range as a fraction of close (scale-free per-bar volatility) (`HighLowRange`).
|
||||
- **Wick Ratio** — signed upper-vs-lower shadow imbalance as a fraction of the range (`WickRatio`).
|
||||
- **Body Size Percent** — absolute candle body as a fraction of the bar range (`BodySizePct`).
|
||||
- **Close vs Open** — signed body as a fraction of the open price, `(close − open) / open` (`CloseVsOpen`).
|
||||
- **Spread AR(1) Coefficient** — first-order autoregression coefficient of the spread `a − b` (direct cointegration / mean-reversion strength) (`SpreadAr1Coefficient`).
|
||||
- **Rolling Quantile** — interpolated q-th quantile over a trailing window (type-7 / NumPy default) (`RollingQuantile`).
|
||||
- **Rolling Percentile Rank** — percentile rank of the latest value within its trailing window (`RollingPercentileRank`).
|
||||
- **Rolling IQR** — interquartile range (Q3 − Q1) over a trailing window (robust dispersion) (`RollingIqr`).
|
||||
- **Realized Volatility** — square root of the summed squared log returns (raw, un-annualised quadratic variation) (`RealizedVolatility`).
|
||||
- **Log Return** — logarithmic return over a fixed lag, `ln(price_t / price_{t−period})` (`LogReturn`).
|
||||
|
||||
## [0.5.3] - 2026-06-04
|
||||
- **Fibonacci Time Zones** — vertical markers at Fibonacci bar-distances (1/2/3/5/8/...) from the latest swing pivot (`FIB_TIME_ZONES`).
|
||||
- **Fibonacci Channel** — a sloped base trendline plus parallel lines at Fibonacci multiples of the channel width (`FIB_CHANNEL`).
|
||||
- **Fibonacci Arcs** — semicircular retracement levels centred on the swing end, normalised by leg bar-width (`FIB_ARCS`).
|
||||
- **Fibonacci Fan** — three trendlines fanning from a swing start through its 38.2/50/61.8% retracement levels (`FIB_FAN`).
|
||||
- **Fibonacci Confluence** — densest cluster of retracement levels across recent swing legs (price + strength) (`FIB_CONFLUENCE`).
|
||||
- **Golden Pocket** — the 0.618-0.65 optimal-trade-entry band of the most recent swing leg (`GOLDEN_POCKET`).
|
||||
- **Auto-Fibonacci** — retracement anchored on the dominant (largest-magnitude) leg among recent swings (`AUTO_FIB`).
|
||||
- **Fibonacci Projection** — measured-move target zone from the last three pivots (A-B-C), projecting A->B from C (`FIB_PROJECTION`).
|
||||
- **Fibonacci Extension** — projects the latest swing leg to the canonical extension ratios (127.2/141.4/161.8/200/261.8%) (`FIB_EXTENSION`).
|
||||
- **Fibonacci Retracement** — seven retracement levels (0/23.6/38.2/50/61.8/78.6/100%) of the most recent confirmed swing leg (`FIB_RETRACEMENT`).
|
||||
|
||||
## [0.5.2] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Three Drives** — three symmetric drives with extension legs; bullish +1, bearish -1 (`THREE_DRIVES`).
|
||||
- **Cypher** — five-point harmonic whose D retraces XC by 0.786; bullish +1, bearish -1 (`CYPHER`).
|
||||
- **Shark** — five-point harmonic with an expansion leg and 0.886-1.13 D; bullish +1, bearish -1 (`SHARK`).
|
||||
- **Crab** — five-point harmonic with the deepest (1.618 XA) D completion; bullish +1, bearish -1 (`CRAB`).
|
||||
- **Bat** — five-point harmonic with a shallow B and 0.886 D completion; bullish +1, bearish -1 (`BAT`).
|
||||
- **Butterfly** — five-point harmonic with an extended (1.27-1.618 XA) D; bullish +1, bearish -1 (`BUTTERFLY`).
|
||||
- **Gartley** — five-point harmonic with a 0.786 D completion; bullish +1, bearish -1 (`GARTLEY`).
|
||||
- **AB=CD** — four-point AB=CD harmonic: BC retraces AB, CD mirrors AB; bullish +1, bearish -1 (`ABCD`).
|
||||
- **Cup and Handle** — rounded base with a shallow handle near the rim; bullish +1, inverse -1 (`CUP_AND_HANDLE`).
|
||||
- **Rectangle / Range** — flat support and resistance; mean-reversion signal off the just-touched boundary; support +1, resistance -1 (`RECTANGLE_RANGE`).
|
||||
- **Flag / Pennant** — shallow consolidation against a sharp pole; continuation in the pole direction; bull +1, bear -1 (`FLAG_PENNANT`).
|
||||
- **Wedge (rising/falling)** — both trendlines slope the same way but converge; rising wedge -1, falling wedge +1 (`WEDGE`).
|
||||
- **Triangle (asc/desc/sym)** — converging trendlines; ascending +1, descending -1, symmetrical follows the last swing (`TRIANGLE`).
|
||||
- **Head and Shoulders** — central head flanked by two matching shoulders over a flat neckline; top -1, inverse +1 (`HEAD_AND_SHOULDERS`).
|
||||
- **Triple Top / Bottom** — three matching peaks / troughs; a stronger reversal than the double; bearish -1, bullish +1 (`TRIPLE_TOP_BOTTOM`).
|
||||
- **Double Top / Bottom** — twin-peak / twin-trough reversal confirmed on the second matching swing extreme; bearish -1, bullish +1 (`DOUBLE_TOP_BOTTOM`).
|
||||
|
||||
## [0.5.1] - 2026-06-03
|
||||
|
||||
### Added — Seasonality & Session family (12 indicators)
|
||||
|
||||
- **Volume-by-Time Profile** — mean traded volume bucketed by intraday time (`VOLUME_BY_TIME_PROFILE`).
|
||||
- **Intraday Volatility Profile** — return standard deviation bucketed by intraday time (`INTRADAY_VOLATILITY_PROFILE`).
|
||||
- **Day-of-Week Profile** — mean bar return bucketed by weekday (`DAY_OF_WEEK_PROFILE`).
|
||||
- **Time-of-Day Return Profile** — mean bar return bucketed by intraday time (`TIME_OF_DAY_RETURN_PROFILE`).
|
||||
- **Seasonal Z-Score** — z-score of the current return versus the same hour-of-day history (`SEASONAL_Z_SCORE`).
|
||||
- **Turn-of-Month** — mean daily return inside the turn-of-month window (`TURN_OF_MONTH`).
|
||||
- **Overnight/Intraday Return** — decomposition of session return into overnight and intraday legs (`OVERNIGHT_INTRADAY_RETURN`).
|
||||
- **Overnight Gap** — close-to-open return across the session boundary (`OVERNIGHT_GAP`).
|
||||
- **Average Daily Range** — mean high-low range of the last N completed sessions (`AVERAGE_DAILY_RANGE`).
|
||||
- **Session Range** — per-session (Asia/EU/US) high-low range (`SESSION_RANGE`).
|
||||
- **Session High/Low** — running high and low of the current session (`SESSION_HIGH_LOW`).
|
||||
- **Session VWAP** — session-anchored volume-weighted average price (`SESSION_VWAP`).
|
||||
|
||||
## [0.5.0] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **TICK Index** — instantaneous net advancing-minus-declining issues (`TICK_INDEX`).
|
||||
- **Absolute Breadth Index** — absolute value of net advancing-minus-declining issues (`ABSOLUTE_BREADTH_INDEX`).
|
||||
- **Cumulative Volume Index** — running total of volume-normalised net advancing volume (`CUMULATIVE_VOLUME_INDEX`).
|
||||
- **Bullish Percent Index** — percentage of the universe on a point-and-figure buy signal (`BULLISH_PERCENT_INDEX`).
|
||||
- **Up/Down Volume Ratio** — advancing volume divided by declining volume (`UP_DOWN_VOLUME_RATIO`).
|
||||
- **Percent Above Moving Average** — percentage of the universe trading above its reference moving average (`PERCENT_ABOVE_MA`).
|
||||
- **High-Low Index** — moving average of the record-high percentage (`HIGH_LOW_INDEX`).
|
||||
- **New Highs - New Lows** — net count of new period highs minus new period lows (`NEW_HIGHS_NEW_LOWS`).
|
||||
- **Breadth Thrust** — moving average of the advancing-issues share (Zweig) (`BREADTH_THRUST`).
|
||||
- **TRIN / Arms Index** — advance-decline ratio divided by the up-down volume ratio (`TRIN`).
|
||||
- **McClellan Summation Index** — running cumulative total of the McClellan Oscillator (`MCCLELLAN_SUMMATION_INDEX`).
|
||||
- **McClellan Oscillator** — spread between a 19- and 39-period EMA of ratio-adjusted net advances (`MCCLELLAN_OSCILLATOR`).
|
||||
- **Advance/Decline Volume Line** — cumulative net advancing-minus-declining volume across the universe (`AD_VOLUME_LINE`).
|
||||
- **Advance/Decline Ratio** — advancing issues divided by declining issues across the universe (`ADVANCE_DECLINE_RATIO`).
|
||||
|
||||
### Changed
|
||||
- **Relicensed** from PolyForm Noncommercial 1.0.0 to dual **MIT OR Apache-2.0**. Wickra is now OSI-approved, permissive open source; commercial use is permitted under either license. See [`LICENSE-MIT`](LICENSE-MIT) and [`LICENSE-APACHE`](LICENSE-APACHE).
|
||||
|
||||
## [0.4.7] - 2026-06-03
|
||||
|
||||
### Added
|
||||
- **Spread Bollinger Bands** — Bollinger bands on the spread of two series for pairs mean-reversion (`SPREAD_BOLLINGER_BANDS`).
|
||||
- **Kalman Hedge Ratio** — Kalman-filter dynamic hedge ratio and spread between two series (`KALMAN_HEDGE_RATIO`).
|
||||
- **Granger Causality** — Granger causality F-statistic measuring whether one series predicts another (`GRANGER_CAUSALITY`).
|
||||
- **Variance Ratio** — Lo-MacKinlay variance-ratio test on the spread of two series (`VARIANCE_RATIO`).
|
||||
- **Beta-Neutral Spread** — beta-neutral spread: the rolling OLS regression residual of two series (`BETA_NEUTRAL_SPREAD`).
|
||||
- **Distance SSD** — Gatev sum-of-squared-deviations distance between two normalised series (`DISTANCE_SSD`).
|
||||
- **Spread Hurst** — Hurst exponent of the spread of two series for regime detection (`SPREAD_HURST`).
|
||||
- **OU Half-Life** — Ornstein-Uhlenbeck half-life of mean reversion for the spread of two series (`OU_HALF_LIFE`).
|
||||
- **Rolling Covariance** — rolling covariance of the period-over-period returns of two series (`ROLLING_COVARIANCE`).
|
||||
- **Rolling Correlation** — rolling Pearson correlation of the period-over-period returns of two series (`ROLLING_CORRELATION`).
|
||||
|
||||
- **Market Breadth family** — a new indicator family built on a new
|
||||
`CrossSection` input type that carries the per-symbol state of an entire
|
||||
universe in one tick (each `Member` holds a signed `change`, a `volume`, and
|
||||
`new_high` / `new_low` flags). `CrossSection::new` validates the universe
|
||||
(non-empty, finite changes, finite non-negative volumes); `new_unchecked`
|
||||
skips validation for hot paths.
|
||||
- `AdvanceDecline` (`ADVANCE_DECLINE`) — the Advance/Decline Line, the running
|
||||
cumulative sum of net advancing-minus-declining issues across the universe.
|
||||
|
||||
## [0.4.6] - 2026-06-03
|
||||
|
||||
### Added
|
||||
@@ -1124,7 +1872,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
optional Binance live feed.
|
||||
- Bindings for Python, Node.js, and WebAssembly.
|
||||
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.4.6...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.9.6...HEAD
|
||||
[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
|
||||
[0.8.8]: https://github.com/wickra-lib/wickra/compare/v0.8.7...v0.8.8
|
||||
[0.8.7]: https://github.com/wickra-lib/wickra/compare/v0.8.6...v0.8.7
|
||||
[0.8.6]: https://github.com/wickra-lib/wickra/compare/v0.8.5...v0.8.6
|
||||
[0.8.5]: https://github.com/wickra-lib/wickra/compare/v0.8.4...v0.8.5
|
||||
[0.8.4]: https://github.com/wickra-lib/wickra/compare/v0.8.3...v0.8.4
|
||||
[0.8.3]: https://github.com/wickra-lib/wickra/compare/v0.8.2...v0.8.3
|
||||
[0.8.2]: https://github.com/wickra-lib/wickra/compare/v0.8.1...v0.8.2
|
||||
[0.8.1]: https://github.com/wickra-lib/wickra/compare/v0.8.0...v0.8.1
|
||||
[0.8.0]: https://github.com/wickra-lib/wickra/compare/v0.7.9...v0.8.0
|
||||
[0.7.9]: https://github.com/wickra-lib/wickra/compare/v0.7.8...v0.7.9
|
||||
[0.7.8]: https://github.com/wickra-lib/wickra/compare/v0.7.7...v0.7.8
|
||||
[0.7.7]: https://github.com/wickra-lib/wickra/compare/v0.7.6...v0.7.7
|
||||
[0.7.6]: https://github.com/wickra-lib/wickra/compare/v0.7.5...v0.7.6
|
||||
[0.7.5]: https://github.com/wickra-lib/wickra/compare/v0.7.4...v0.7.5
|
||||
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
|
||||
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
|
||||
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
|
||||
[0.7.1]: https://github.com/wickra-lib/wickra/compare/v0.7.0...v0.7.1
|
||||
[0.7.0]: https://github.com/wickra-lib/wickra/compare/v0.6.9...v0.7.0
|
||||
[0.6.9]: https://github.com/wickra-lib/wickra/compare/v0.6.8...v0.6.9
|
||||
[0.6.8]: https://github.com/wickra-lib/wickra/compare/v0.6.7...v0.6.8
|
||||
[0.6.7]: https://github.com/wickra-lib/wickra/compare/v0.6.6...v0.6.7
|
||||
[0.6.6]: https://github.com/wickra-lib/wickra/compare/v0.6.5...v0.6.6
|
||||
[0.6.5]: https://github.com/wickra-lib/wickra/compare/v0.6.4...v0.6.5
|
||||
[0.6.4]: https://github.com/wickra-lib/wickra/compare/v0.6.3...v0.6.4
|
||||
[0.6.3]: https://github.com/wickra-lib/wickra/compare/v0.6.2...v0.6.3
|
||||
[0.6.2]: https://github.com/wickra-lib/wickra/compare/v0.6.1...v0.6.2
|
||||
[0.6.1]: https://github.com/wickra-lib/wickra/compare/v0.6.0...v0.6.1
|
||||
[0.6.0]: https://github.com/wickra-lib/wickra/compare/v0.5.9...v0.6.0
|
||||
[0.5.9]: https://github.com/wickra-lib/wickra/compare/v0.5.8...v0.5.9
|
||||
[0.5.8]: https://github.com/wickra-lib/wickra/compare/v0.5.7...v0.5.8
|
||||
[0.5.7]: https://github.com/wickra-lib/wickra/compare/v0.5.6...v0.5.7
|
||||
[0.5.6]: https://github.com/wickra-lib/wickra/compare/v0.5.5...v0.5.6
|
||||
[0.5.5]: https://github.com/wickra-lib/wickra/compare/v0.5.4...v0.5.5
|
||||
[0.5.4]: https://github.com/wickra-lib/wickra/compare/v0.5.3...v0.5.4
|
||||
[0.5.3]: https://github.com/wickra-lib/wickra/compare/v0.5.2...v0.5.3
|
||||
[0.5.2]: https://github.com/wickra-lib/wickra/compare/v0.5.1...v0.5.2
|
||||
[0.5.1]: https://github.com/wickra-lib/wickra/compare/v0.5.0...v0.5.1
|
||||
[0.5.0]: https://github.com/wickra-lib/wickra/compare/v0.4.7...v0.5.0
|
||||
[0.4.7]: https://github.com/wickra-lib/wickra/compare/v0.4.6...v0.4.7
|
||||
[0.4.6]: https://github.com/wickra-lib/wickra/compare/v0.4.5...v0.4.6
|
||||
[0.4.5]: https://github.com/wickra-lib/wickra/compare/v0.4.4...v0.4.5
|
||||
[0.4.4]: https://github.com/wickra-lib/wickra/compare/v0.4.3...v0.4.4
|
||||
|
||||
+3
-1
@@ -26,4 +26,6 @@ keywords:
|
||||
- quantitative-finance
|
||||
- rust
|
||||
- time-series
|
||||
license: PolyForm-Noncommercial-1.0.0
|
||||
license:
|
||||
- MIT
|
||||
- Apache-2.0
|
||||
|
||||
+50
-6
@@ -5,11 +5,11 @@ build the project, the standards a change must meet, and how to get it merged.
|
||||
|
||||
## License of contributions
|
||||
|
||||
Wickra is licensed under the **PolyForm Noncommercial License 1.0.0** (see
|
||||
[`LICENSE`](LICENSE)). By submitting a contribution you agree that it is
|
||||
licensed to the project under those same terms. The Noncommercial license
|
||||
permits use for any purpose **other than** a commercial one; keep that in mind
|
||||
when proposing features or depending on Wickra elsewhere.
|
||||
Wickra is dual-licensed under the [MIT](LICENSE-MIT) and
|
||||
[Apache-2.0](LICENSE-APACHE) licenses; users may choose either. Unless you
|
||||
explicitly state otherwise, any contribution you intentionally submit for
|
||||
inclusion in the work, as defined in the Apache-2.0 license, shall be dual
|
||||
licensed as above, without any additional terms or conditions.
|
||||
|
||||
## Project layout
|
||||
|
||||
@@ -21,6 +21,11 @@ when proposing features or depending on Wickra elsewhere.
|
||||
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
|
||||
| `bindings/node` | napi-rs bindings (`wickra` on npm). |
|
||||
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
|
||||
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
|
||||
| `bindings/csharp` | C# binding over the C ABI (`Wickra` on NuGet) — `[LibraryImport]` P/Invoke generated from `wickra.h`. |
|
||||
| `bindings/go` | Go binding over the C ABI via cgo (module tag `bindings/go/vX.Y.Z`) — wrappers generated from `wickra.h`. |
|
||||
| `bindings/r` | R binding over the C ABI via `.Call` (R package) — C glue + R wrappers generated from `wickra.h`. |
|
||||
| `bindings/java` | Java binding over the C ABI via the FFM API (Panama, Maven Central) — wrappers generated from `wickra.h`. |
|
||||
| `examples/` | Runnable examples. |
|
||||
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
|
||||
|
||||
@@ -102,7 +107,16 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
- **Streaming parity.** An indicator's `batch` output must equal the sequence
|
||||
of `update` calls.
|
||||
- **Bindings.** A change to a public indicator API must be mirrored across the
|
||||
Python, Node, and WASM bindings, including their type stubs / `.d.ts`.
|
||||
Python, Node.js, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
|
||||
(`bindings/c`) is generated from the core, so regenerate it from the core and
|
||||
commit `src/lib.rs` + `include/wickra.h`. The C# binding (`bindings/csharp`) is
|
||||
generated from `wickra.h`, so regenerate and commit its `Generated/*.g.cs` too.
|
||||
The Go binding (`bindings/go`) is likewise generated from `wickra.h`, so
|
||||
regenerate and commit `indicators_gen.go` (`gofmt`-clean). The R binding
|
||||
(`bindings/r`) is generated from `wickra.h` too, so regenerate and commit
|
||||
`src/wickra.c` + `R/indicators.R`. The Java binding (`bindings/java`) is
|
||||
generated from `wickra.h` as well, so regenerate and commit its
|
||||
`src/main/java/org/wickra/*.java`.
|
||||
- **Docs.** Update the relevant page on the
|
||||
[documentation site](https://docs.wickra.org) and the
|
||||
`README.md` when behaviour or the public API changes. The docs live in
|
||||
@@ -122,3 +136,33 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
|
||||
Use the issue templates under
|
||||
[`.github/ISSUE_TEMPLATE`](.github/ISSUE_TEMPLATE). For security-sensitive
|
||||
reports, follow [`SECURITY.md`](SECURITY.md) instead of opening a public issue.
|
||||
|
||||
## Developer Certificate of Origin (DCO)
|
||||
|
||||
All contributions to Wickra are made under the [Developer Certificate of
|
||||
Origin (DCO) 1.1](DCO). By signing off on your commits you certify that you
|
||||
wrote the patch, or otherwise have the right to submit it under the project's
|
||||
`MIT OR Apache-2.0` license.
|
||||
|
||||
Sign off every commit by adding a `Signed-off-by` trailer with your real name
|
||||
and email — Git adds it automatically with the `-s` flag:
|
||||
|
||||
```bash
|
||||
git commit -s -m "your message"
|
||||
```
|
||||
|
||||
This produces a trailer of the form:
|
||||
|
||||
```
|
||||
Signed-off-by: Your Name <you@example.com>
|
||||
```
|
||||
|
||||
The name and email must match the commit author. Commits without a valid
|
||||
sign-off line cannot be merged. To sign off a commit you already made, amend it
|
||||
with `git commit -s --amend`, or sign off a range with an interactive rebase.
|
||||
|
||||
## Governance
|
||||
|
||||
Wickra's decision-making and maintainership are described in
|
||||
[`GOVERNANCE.md`](GOVERNANCE.md); the current maintainers are listed in
|
||||
[`MAINTAINERS.md`](MAINTAINERS.md).
|
||||
|
||||
Generated
+160
-284
@@ -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]]
|
||||
@@ -703,10 +673,14 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
name = "kand"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
checksum = "af1f41590bd014ef6c3dd815b45f07deb4c3198e355a4319bb7521b6a3a6aeb5"
|
||||
dependencies = [
|
||||
"num_enum",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
@@ -748,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"
|
||||
@@ -859,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"
|
||||
@@ -883,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"
|
||||
@@ -912,19 +843,25 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "0.28.0"
|
||||
name = "num_enum"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778da78c64ddc928ebf5ad9df5edf0789410ff3bdbf3619aed51cd789a6af1e2"
|
||||
checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndarray",
|
||||
"num-complex",
|
||||
"num-integer",
|
||||
"num-traits",
|
||||
"pyo3",
|
||||
"pyo3-build-config",
|
||||
"rustc-hash",
|
||||
"num_enum_derive",
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_enum_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||
dependencies = [
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -970,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"
|
||||
@@ -978,6 +924,7 @@ checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
@@ -1044,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"
|
||||
@@ -1072,13 +1010,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1111,9 +1048,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3"
|
||||
version = "0.28.3"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
|
||||
checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
@@ -1125,18 +1062,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-build-config"
|
||||
version = "0.28.3"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
|
||||
checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078"
|
||||
dependencies = [
|
||||
"target-lexicon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-ffi"
|
||||
version = "0.28.3"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
|
||||
checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pyo3-build-config",
|
||||
@@ -1144,9 +1081,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros"
|
||||
version = "0.28.3"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
|
||||
checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"pyo3-macros-backend",
|
||||
@@ -1156,13 +1093,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pyo3-macros-backend"
|
||||
version = "0.28.3"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
|
||||
checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"pyo3-build-config",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
@@ -1188,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"
|
||||
@@ -1220,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]]
|
||||
@@ -1232,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"
|
||||
@@ -1287,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"
|
||||
@@ -1498,6 +1416,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ta"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "609409d472a0a7d8d4dd9e19891bbdef546b9dce670c3057d0e02192dc541226"
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.5"
|
||||
@@ -1511,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",
|
||||
@@ -1607,6 +1531,36 @@ dependencies = [
|
||||
"tungstenite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "1.1.1+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.25.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.1.2+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.29.0"
|
||||
@@ -1649,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"
|
||||
@@ -1715,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]]
|
||||
@@ -1821,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"
|
||||
@@ -1867,7 +1785,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1876,9 +1794,30 @@ dependencies = [
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
"ta",
|
||||
"wickra",
|
||||
"wickra-data",
|
||||
"yata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"tokio",
|
||||
"wickra-core",
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1888,24 +1827,26 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
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.0.0"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -1915,26 +1856,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
"napi-derive",
|
||||
"tokio",
|
||||
"wickra-core",
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"bytemuck",
|
||||
"pyo3",
|
||||
"tokio",
|
||||
"wickra-core",
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
@@ -1943,6 +1888,7 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-test",
|
||||
"wickra-core",
|
||||
"wickra-data",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1992,12 +1938,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
name = "winnow"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2006,91 +1952,21 @@ 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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||
|
||||
[[package]]
|
||||
name = "yata"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b4ef8ddfa3ccd93454262c0e60a43a2bbf403d404174e1815f7581d5028229f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.2"
|
||||
|
||||
+8
-5
@@ -7,16 +7,18 @@ members = [
|
||||
"bindings/python",
|
||||
"bindings/wasm",
|
||||
"bindings/node",
|
||||
"bindings/c",
|
||||
"examples/rust",
|
||||
"crates/wickra-bench",
|
||||
]
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.4.6"
|
||||
version = "0.9.6"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
license-file = "LICENSE"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/wickra-lib/wickra"
|
||||
homepage = "https://github.com/wickra-lib/wickra"
|
||||
readme = "README.md"
|
||||
@@ -24,7 +26,8 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.4.6" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.9.6" }
|
||||
wickra-data = { path = "crates/wickra-data", version = "0.9.6" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
@@ -35,8 +38,8 @@ approx = "0.5"
|
||||
criterion = { version = "0.8", features = ["html_reports"] }
|
||||
|
||||
# Python binding
|
||||
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39"] }
|
||||
numpy = "0.28"
|
||||
pyo3 = { version = "0.29", features = ["extension-module", "abi3-py39"] }
|
||||
numpy = "0.29"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
Developer Certificate of Origin
|
||||
Version 1.1
|
||||
|
||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim copies of this
|
||||
license document, but changing it is not allowed.
|
||||
|
||||
|
||||
Developer's Certificate of Origin 1.1
|
||||
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
have the right to submit it under the open source license
|
||||
indicated in the file; or
|
||||
|
||||
(b) The contribution is based upon previous work that, to the best
|
||||
of my knowledge, is covered under an appropriate open source
|
||||
license and I have the right under that license to submit that
|
||||
work with modifications, whether created in whole or in part
|
||||
by me, under the same open source license (unless I am
|
||||
permitted to submit under a different license), as indicated
|
||||
in the file; or
|
||||
|
||||
(c) The contribution was provided directly to me by some other
|
||||
person who certified (a), (b) or (c) and I have not modified
|
||||
it.
|
||||
|
||||
(d) I understand and agree that this project and the contribution
|
||||
are public and that a record of the contribution (including all
|
||||
personal information I submit with it, including my sign-off) is
|
||||
maintained indefinitely and may be redistributed consistent with
|
||||
this project or the open source license(s) involved.
|
||||
@@ -0,0 +1,71 @@
|
||||
# Governance
|
||||
|
||||
Wickra is an open-source project maintained under a **single-maintainer
|
||||
("BDFL") model**. This document describes how decisions are made and how the
|
||||
project is run, so contributors know what to expect.
|
||||
|
||||
## Roles
|
||||
|
||||
- **Maintainer.** The maintainer (see [`MAINTAINERS.md`](MAINTAINERS.md)) is
|
||||
responsible for the project's direction, reviews and merges changes, cuts
|
||||
releases, and has final say on all technical and project decisions.
|
||||
- **Contributors.** Anyone who proposes changes via pull requests, files
|
||||
issues, improves documentation, or otherwise participates. Contributors do
|
||||
not need any special status to take part.
|
||||
|
||||
## Decision-making
|
||||
|
||||
- Day-to-day technical decisions (APIs, indicator implementations, refactors)
|
||||
are made by the maintainer, informed by discussion on issues and pull
|
||||
requests.
|
||||
- Proposals are raised as GitHub issues or pull requests. Significant or
|
||||
breaking changes should be opened as an issue first to agree on the approach
|
||||
before implementation.
|
||||
- The maintainer aims to act transparently: rationale for non-trivial decisions
|
||||
is recorded in the relevant issue, pull request, or commit message.
|
||||
|
||||
## Contribution flow
|
||||
|
||||
All changes — including the maintainer's own — go through pull requests so that
|
||||
CI (tests, linting, static analysis) runs against them, and so the change
|
||||
history is reviewable. Contribution requirements are documented in
|
||||
[`CONTRIBUTING.md`](CONTRIBUTING.md), including the Developer Certificate of
|
||||
Origin sign-off that every commit must carry.
|
||||
|
||||
## Becoming a maintainer
|
||||
|
||||
The project currently has one maintainer. Maintainership may be extended to
|
||||
contributors who have demonstrated sustained, high-quality involvement, at the
|
||||
current maintainer's discretion. If the project grows to multiple maintainers,
|
||||
this document will be updated to describe shared decision-making.
|
||||
|
||||
## Continuity and succession
|
||||
|
||||
The project is designed to survive the loss of any single individual, so that
|
||||
issues can be triaged, proposed changes accepted, and releases published within
|
||||
one week of confirmed loss of the maintainer:
|
||||
|
||||
- **Credentials.** All credentials required to operate the project — the
|
||||
`wickra-lib` GitHub organization, the publishing tokens for crates.io, PyPI
|
||||
and npm, and the `wickra.org` domain registrar — are stored in a password
|
||||
manager. A trusted contact (a family member) holds **emergency access** to
|
||||
that password manager and can obtain these credentials if the maintainer can
|
||||
no longer continue.
|
||||
- **Continuity actions.** With that access, the trusted contact (or a delegate
|
||||
they appoint) can create and close issues, accept pull requests, and publish
|
||||
releases through the existing CI/CD workflows.
|
||||
- **Account recovery.** The maintainer's GitHub account has recovery configured,
|
||||
and ownership of the `wickra-lib` organization can be transferred to a new
|
||||
maintainer.
|
||||
- **Legal rights.** Legal rights to the project name and DNS are covered by the
|
||||
maintainer's estate arrangements.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
All participants are expected to follow the
|
||||
[Code of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
## Changes to this document
|
||||
|
||||
This governance model may evolve as the project grows. Changes are made via
|
||||
pull request and take effect once merged.
|
||||
@@ -1,161 +0,0 @@
|
||||
# PolyForm Noncommercial License 1.0.0
|
||||
|
||||
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
||||
|
||||
## Acceptance
|
||||
|
||||
In order to get any license under these terms, you must agree
|
||||
to them as both strict obligations and conditions to all
|
||||
your licenses.
|
||||
|
||||
## Copyright License
|
||||
|
||||
The licensor grants you a copyright license for the
|
||||
software to do everything you might do with the software
|
||||
that would otherwise infringe the licensor's copyright
|
||||
in it for any permitted purpose. However, you may
|
||||
only distribute the software according to [Distribution
|
||||
License](#distribution-license) and make changes or new works
|
||||
based on the software according to [Changes and New Works
|
||||
License](#changes-and-new-works-license).
|
||||
|
||||
## Distribution License
|
||||
|
||||
The licensor grants you an additional copyright license
|
||||
to distribute copies of the software. Your license to
|
||||
distribute covers distributing the software with changes
|
||||
and new works permitted by [Changes and New Works
|
||||
License](#changes-and-new-works-license).
|
||||
|
||||
## Notices
|
||||
|
||||
You must ensure that anyone who gets a copy of any part of
|
||||
the software from you also gets a copy of these terms or the
|
||||
URL for them above, as well as copies of any plain-text lines
|
||||
beginning with `Required Notice:` that the licensor provided
|
||||
with the software. For example:
|
||||
|
||||
> Required Notice: Copyright 2026 kingchenc (https://github.com/wickra-lib/wickra)
|
||||
|
||||
## Changes and New Works License
|
||||
|
||||
The licensor grants you an additional copyright license
|
||||
to make changes and new works based on the software for any
|
||||
permitted purpose.
|
||||
|
||||
## Patent License
|
||||
|
||||
The licensor grants you a patent license for the software that
|
||||
covers patent claims the licensor can license, or becomes able
|
||||
to license, that you would infringe by using the software.
|
||||
|
||||
## Noncommercial Purposes
|
||||
|
||||
Any noncommercial purpose is a permitted purpose.
|
||||
|
||||
## Personal Uses
|
||||
|
||||
Personal use for research, experiment, and testing for
|
||||
the benefit of public knowledge, personal study, private
|
||||
entertainment, hobby projects, amateur pursuits, or religious
|
||||
observance, without any anticipated commercial application,
|
||||
is use for a permitted purpose.
|
||||
|
||||
## Noncommercial Organizations
|
||||
|
||||
Use by any charitable organization, educational institution,
|
||||
public research organization, public safety or health
|
||||
organization, environmental protection organization, or
|
||||
government institution is use for a permitted purpose regardless
|
||||
of the source of funding or obligations resulting from the
|
||||
funding.
|
||||
|
||||
## Fair Use
|
||||
|
||||
You may have "fair use" rights for the software under the
|
||||
law. These terms do not limit them.
|
||||
|
||||
## No Other Rights
|
||||
|
||||
These terms do not allow you to sublicense or transfer any of
|
||||
your licenses to anyone else, or prevent the licensor from
|
||||
granting licenses to anyone else. These terms do not imply
|
||||
any other licenses.
|
||||
|
||||
## Patent Defense
|
||||
|
||||
If you make any written claim that the software infringes or
|
||||
contributes to infringement of any patent, your patent license
|
||||
for the software granted under these terms ends immediately. If
|
||||
your company makes such a claim, your patent license ends
|
||||
immediately for work on behalf of your company.
|
||||
|
||||
## Violations
|
||||
|
||||
The first time you are notified in writing that you have
|
||||
violated any of these terms, or done anything with the software
|
||||
not covered by your licenses, your licenses can nonetheless
|
||||
continue if you come into full compliance with these terms,
|
||||
and take practical steps to correct past violations, within 32
|
||||
days of receiving notice. Otherwise, all your licenses end
|
||||
immediately.
|
||||
|
||||
## No Liability
|
||||
|
||||
***As far as the law allows, the software comes as is, without
|
||||
any warranty or condition, and the licensor will not be liable
|
||||
to you for any damages arising out of these terms or the use
|
||||
or nature of the software, under any kind of legal claim.***
|
||||
|
||||
## Definitions
|
||||
|
||||
The **licensor** is the individual or entity offering these
|
||||
terms, and the **software** is the software the licensor makes
|
||||
available under these terms.
|
||||
|
||||
**You** refers to the individual or entity agreeing to these
|
||||
terms.
|
||||
|
||||
**Your company** is any legal entity, sole proprietorship,
|
||||
or other kind of organization that you work for, plus all
|
||||
organizations that have control over, are under the control
|
||||
of, or are under common control with that organization.
|
||||
**Control** means ownership of substantially all the assets
|
||||
of an entity, or the power to direct its management and
|
||||
policies by vote, contract, or otherwise. Control can be
|
||||
direct or indirect.
|
||||
|
||||
**Your licenses** are all the licenses granted to you for the
|
||||
software under these terms.
|
||||
|
||||
**Use** means anything you do with the software requiring one
|
||||
of your licenses.
|
||||
|
||||
## Additional Permissions Granted by the Licensor
|
||||
|
||||
These additional permissions supplement the PolyForm Noncommercial
|
||||
License 1.0.0 above. They only broaden, and never narrow, the
|
||||
licenses granted to you. The text of the PolyForm Noncommercial
|
||||
License 1.0.0 above is unmodified.
|
||||
|
||||
Use by a natural person, acting for their own personal account and
|
||||
not on behalf of any third party, is use for a permitted purpose.
|
||||
This includes operating an automated trading bot or trading strategy
|
||||
on that person's own capital, whether or not it earns that person
|
||||
money.
|
||||
|
||||
For the avoidance of doubt, the licenses above already let you use,
|
||||
fork, modify, and redistribute the software, and file issues and
|
||||
contribute changes, for any permitted purpose. Personal projects,
|
||||
research, education, nonprofit organizations, government use, and
|
||||
hobby trading bots are permitted purposes.
|
||||
|
||||
Any other commercial use — in particular the commercial sale of the
|
||||
software itself, or the commercial sale of services built around it —
|
||||
requires a separate commercial license from the licensor. If you want
|
||||
to use Wickra commercially, get in touch about a license at
|
||||
<https://github.com/wickra-lib/wickra>.
|
||||
|
||||
---
|
||||
|
||||
Required Notice: Copyright 2026 kingchenc (https://github.com/wickra-lib/wickra)
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative
|
||||
Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 kingchenc and the Wickra contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 kingchenc and the Wickra contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative
|
||||
Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2026 kingchenc and the Wickra contributors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 kingchenc and the Wickra contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,17 @@
|
||||
# Maintainers
|
||||
|
||||
This file lists the current maintainers of Wickra. See
|
||||
[`GOVERNANCE.md`](GOVERNANCE.md) for what the role entails and how the project
|
||||
is run.
|
||||
|
||||
| Maintainer | GitHub | Areas |
|
||||
| --- | --- | --- |
|
||||
| kingchenc | [@kingchenc](https://github.com/kingchenc) | All (core, bindings, CI/release, docs) |
|
||||
|
||||
## Contacting the maintainers
|
||||
|
||||
- General questions and support: see [`SUPPORT.md`](SUPPORT.md).
|
||||
- Bug reports and feature requests: open an issue using the
|
||||
[issue templates](.github/ISSUE_TEMPLATE).
|
||||
- Security reports: follow [`SECURITY.md`](SECURITY.md) — do **not** open a
|
||||
public issue.
|
||||
@@ -1,34 +1,41 @@
|
||||
<p align="center">
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=314" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
<a href="https://wickra.org"><img src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/wickra-banner.webp?v=514" alt="Wickra — streaming-first technical indicators" width="100%"></a>
|
||||
</p>
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](https://crates.io/crates/wickra)
|
||||
[](https://pypi.org/project/wickra/)
|
||||
[](https://www.npmjs.com/package/wickra)
|
||||
[](LICENSE)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/attestations)
|
||||
[](https://docs.wickra.org)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/codeql.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](https://crates.io/crates/wickra)
|
||||
[](https://pypi.org/project/wickra/)
|
||||
[](https://www.npmjs.com/package/wickra)
|
||||
[](https://www.nuget.org/packages/Wickra)
|
||||
[](https://central.sonatype.com/artifact/org.wickra/wickra)
|
||||
[](https://pkg.go.dev/github.com/wickra-lib/wickra-go)
|
||||
[](https://wickra-lib.r-universe.dev)
|
||||
[](#license)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/wickra-lib/wickra)
|
||||
[](https://www.bestpractices.dev/projects/13094)
|
||||
[](https://github.com/wickra-lib/wickra/attestations)
|
||||
[](https://docs.wickra.org)
|
||||
[](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
|
||||
bindings for Python, Node.js, and WebAssembly. Every indicator is a state
|
||||
machine that updates in O(1) per new data point, so live trading bots and
|
||||
native bindings for Python, Node.js and WASM, plus a C ABI that C, C++,
|
||||
C#, Go, Java, R and any other C-capable language links against. Every indicator is a
|
||||
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)
|
||||
@@ -45,9 +52,15 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
- **Quickstarts** — [Rust](https://docs.wickra.org/Quickstart-Rust),
|
||||
[Python](https://docs.wickra.org/Quickstart-Python),
|
||||
[Node](https://docs.wickra.org/Quickstart-Node),
|
||||
[WASM](https://docs.wickra.org/Quickstart-WASM).
|
||||
[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),
|
||||
[R](https://docs.wickra.org/Quickstart-R).
|
||||
- **Indicators** — a per-indicator deep dive (formula, parameters, warmup) for
|
||||
every one of the 314 indicators; start at the
|
||||
every one of the 514 indicators; start at the
|
||||
[indicators overview](https://docs.wickra.org/Indicators-Overview).
|
||||
- **Reference** — [warmup periods](https://docs.wickra.org/Warmup-Periods),
|
||||
[streaming vs batch](https://docs.wickra.org/Streaming-vs-Batch),
|
||||
@@ -57,110 +70,144 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[TA-Lib migration](https://docs.wickra.org/TA-Lib-Migration),
|
||||
[FAQ](https://docs.wickra.org/FAQ).
|
||||
|
||||
## Why Wickra
|
||||
|
||||
Most TA libraries are fast, *or* multi-language, *or* broad. Wickra refuses to
|
||||
pick. It's the streaming-first engine built for the workload the others treat as
|
||||
an afterthought — **live, tick-by-tick data** — without giving up the breadth of
|
||||
a full batch library, and without making you reimplement your indicators four
|
||||
times to get there.
|
||||
|
||||
- **The biggest streaming-native catalogue, period.** 514 indicators across 24
|
||||
families — candlesticks, harmonic & chart patterns, market profile, market
|
||||
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
|
||||
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
|
||||
none of them stream.
|
||||
- **One Rust core, five first-class targets.** Native **Rust · Python · Node.js ·
|
||||
WASM** plus a **C ABI** for C, C++, C#, Go, Java, R and any other C-capable language —
|
||||
identical math, identical results, zero per-language reimplementation and zero
|
||||
GIL bottleneck.
|
||||
- **Correct by construction, not by hope.** Every `update` validates its input,
|
||||
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 **11–56×**
|
||||
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
|
||||
and trades the simple recurrences (SMA, EMA, MACD) for its guarantees — and
|
||||
the losses are shown, not hidden.
|
||||
- **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 — 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.
|
||||
|
||||
Every other library forces one of those compromises. Wickra doesn't:
|
||||
|
||||
| Library | Install | Streaming | Languages | Indicators | Active |
|
||||
|------------------|-------------|-------------|-----------------------------|-----------:|--------|
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Rust · Python · Node.js · WASM · C · C++ · C# · Go · Java · R** | **514** | **yes** |
|
||||
| kand | clean | yes | Python · WASM · Rust | ~60 | yes |
|
||||
| ta-rs | clean | yes | Rust only | ~30 | stale |
|
||||
| yata | clean | partial | Rust only | ~35 | yes |
|
||||
| TA-Lib | yes (C deps)| no | many bindings | ~150 | barely |
|
||||
| pandas-ta | clean | no | Python | ~130 | slow |
|
||||
| finta | clean | no | Python | ~80 | stale |
|
||||
| talipp | clean | yes | Python | ~40 | yes |
|
||||
|
||||
Broad, multi-language, streaming-native **and** honest about its trade-offs — at
|
||||
the same time. That's the combination no one else ships.
|
||||
|
||||
## Why Wickra exists
|
||||
|
||||
The Python TA ecosystem has plenty of libraries — TA-Lib, pandas-ta, finta,
|
||||
talipp, tulipy — and every one of them shares the same blind spot:
|
||||
Wickra started as a personal itch. The existing TA libraries never quite fit the
|
||||
projects I was building, so I decided to build one from the ground up — partly to
|
||||
learn, partly because I genuinely enjoy taking something that already exists and
|
||||
trying to do it differently (and, ideally, better). It's open source because the
|
||||
useful version of that itch is the one other people can build on too.
|
||||
|
||||
| Library | Install pain | Streaming | Multi-language | Active |
|
||||
|------------------------|-----------------|-----------|----------------|--------|
|
||||
| **★ Wickra** | **clean** | **yes** | **Python + Node + WASM + Rust** | **yes** |
|
||||
| TA-Lib (Python) | yes (C deps) | no | no | barely |
|
||||
| pandas-ta | clean | no | no | slow |
|
||||
| finta | clean | no | no | stale |
|
||||
| ta-lib-python | yes (C deps) | no | no | barely |
|
||||
| talipp | clean | yes | no | yes |
|
||||
| Tulip Indicators | yes (C deps) | no | partial | stale |
|
||||
| ooples (C#) | clean | no | C# only | yes |
|
||||
## Benchmarks
|
||||
|
||||
Wickra is the only library that combines all of: clean install, streaming,
|
||||
multi-language reach, and active maintenance.
|
||||
Wickra updates every indicator in **O(1)** per tick. In **streaming** — the
|
||||
workload it is built for — it is **11–56× faster** than the only other incremental
|
||||
peer and **thousands of times** faster than recompute-on-every-tick libraries.
|
||||
**Batch** is competitive: it wins several rows outright and trades a few µs
|
||||
elsewhere for `None`-warmup, NaN-safety and bit-exact `batch == streaming`.
|
||||
|
||||
## Benchmark: how much faster is "streaming-first"?
|
||||
Full tables (Rust + Python, streaming + batch) and how to reproduce them live in
|
||||
**[BENCHMARKS.md](BENCHMARKS.md)**.
|
||||
|
||||
The numbers below were measured on a single developer workstation and are not
|
||||
guaranteed to reproduce identically on different hardware — absolute µs values
|
||||
depend on CPU, memory clock and OS scheduler. Read them as **relative
|
||||
speedups** between libraries on identical input, not as a universal
|
||||
performance contract.
|
||||
### Pick your language with eyes open — per-binding throughput
|
||||
|
||||
- **Reproduced on:** Windows 11 Pro 26200, AMD Ryzen 9 9950X, 64 GB DDR5,
|
||||
Rust 1.92 (release profile, `lto = "fat"`, `codegen-units = 1`),
|
||||
Python 3.12, Node 20.
|
||||
- **Reproduce yourself:** `pip install -e bindings/python[bench]` then
|
||||
`python -m benchmarks.compare_libraries`. The script auto-detects every
|
||||
installed peer library and runs them on the same generated inputs as
|
||||
Wickra. The CI job `cross-library-bench` runs the same script on every
|
||||
push and uploads the raw report as a build artefact.
|
||||
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.
|
||||
|
||||
Lower µs/op = faster. Wickra wins every batch category outright, and the
|
||||
streaming gap widens linearly with how much history a batch-only library has
|
||||
to recompute on every tick.
|
||||
| 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 |
|
||||
|
||||
### Batch — single full pass over a 20 000-bar series
|
||||
|
||||
Reading the table: each cell shows that library's runtime, plus how many times
|
||||
slower it is than Wickra in parentheses. **★** marks the winner per row.
|
||||
|
||||
| Indicator | **★ Wickra** | finta | talipp |
|
||||
|---------------------|---------------------|-----------------------------|-------------------------------|
|
||||
| SMA(20) | **95.6 µs ★** | 343.5 µs (3.6× slower) | 7 640.6 µs (79.9× slower) |
|
||||
| EMA(20) | **64.6 µs ★** | 223.1 µs (3.5× slower) | 12 160.9 µs (188.2× slower) |
|
||||
| RSI(14) | **126.2 µs ★** | 1 107.1 µs (8.8× slower) | 15 792.2 µs (125.1× slower) |
|
||||
| MACD(12, 26, 9) | **119.0 µs ★** | 531.8 µs (4.5× slower) | 49 788.1 µs (418.2× slower) |
|
||||
| Bollinger(20, 2.0) | **105.3 µs ★** | 812.0 µs (7.7× slower) | 130 938.3 µs (1 243.7× slower)|
|
||||
| ATR(14) | **123.5 µs ★** | 5 144.8 µs (41.7× slower) | 28 816.0 µs (233.4× slower) |
|
||||
|
||||
### Streaming — per-tick latency after seeding with 5 000 historical bars
|
||||
|
||||
A batch-only library has to re-run its full indicator over the entire history on
|
||||
every new tick; Wickra updates state in O(1).
|
||||
|
||||
| Indicator | **★ Wickra (per tick)** | talipp (per tick) |
|
||||
|-----------|---------------------|---------------------------|
|
||||
| RSI(14) | **0.119 µs ★** | 1.644 µs (13.8× slower) |
|
||||
|
||||
> TA-Lib and pandas-ta are not included here because both fail to install
|
||||
> cleanly on Windows without C build tooling — which is precisely the install
|
||||
> pain Wickra was built to remove. The benchmark script auto-detects every
|
||||
> peer library it can find and runs them on the same inputs as Wickra; install
|
||||
> them in your environment to see those rows light up too.
|
||||
|
||||
Run the suite yourself:
|
||||
|
||||
```bash
|
||||
pip install -e bindings/python[bench]
|
||||
python -m benchmarks.compare_libraries
|
||||
```
|
||||
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
|
||||
|
||||
314 streaming-first indicators across nineteen families. Every one passes the
|
||||
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 |
|
||||
|--------|-----------|
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100) |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| Pivots & S/R | Classic Pivots, Fibonacci Pivots, Camarilla, Woodie Pivots, DeMark Pivots, Williams Fractals, ZigZag |
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns) |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range |
|
||||
| Moving Averages | SMA, EMA, WMA, DEMA, TEMA, HMA, KAMA, SMMA, TRIMA, ZLEMA, T3, VWMA, ALMA, McGinley Dynamic, FRAMA, VIDYA, JMA, Alligator, EVWMA, SWMA, GMA, EHMA, Median MA, Adaptive Laguerre, GD, Holt-Winters |
|
||||
| Momentum Oscillators | RSI (Wilder), Anchored RSI, Stochastic, CCI, ROC, Williams %R, MFI, Awesome Oscillator, MOM, CMO, TSI, PMO, StochRSI, Ultimate Oscillator, RVI, PGO, KST, SMI, Laguerre RSI, Connors RSI, Inertia, ROC Percentage (ROCP), ROC Ratio (ROCR), ROC Ratio 100 (ROCR100), Disparity Index, Fisher RSI, RSX, Dynamic Momentum Index, Stochastic CCI, RMI, Derivative Oscillator, Elder Ray, Intraday Momentum Index, QQE |
|
||||
| Trend & Directional | MACD, MACD Fixed (MACDFIX), MACD Extended (MACDEXT), ADX (+DI/-DI), ADXR, Aroon, TRIX, Aroon Oscillator, Vortex, Random Walk Index, Trend Intensity Index, Wave Trend Oscillator, Mass Index, Choppiness Index, Vertical Horizontal Filter, Plus DM, Minus DM, Plus DI, Minus DI, DX, TTM Trend, Trend Strength Index, Qstick, Polarized Fractal Efficiency, Wave PM, Gator Oscillator, Kase Permission Stochastic |
|
||||
| Price Oscillators | PPO, DPO, Coppock, Accelerator Oscillator, Balance of Power, APO, AO Histogram, CFO, Zero-Lag MACD, Elder Impulse, STC, TSF Oscillator, MACD Histogram, PPO Histogram |
|
||||
| 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 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 |
|
||||
| DeMark | TD Setup, TD Sequential, TD DeMarker, TD REI, TD Pressure, TD Combo, TD Countdown, TD Lines, TD Range Projection, TD Differential, TD Open, TD Risk Level, TD Camouflage, TD Clop, TD Clopwin, TD Propulsion, TD Trap, TD D-Wave, TD Moving Averages |
|
||||
| Ichimoku & Charts | Ichimoku Kinko Hyo (Tenkan, Kijun, Senkou A/B, Chikou), Heikin-Ashi, Heikin-Ashi Oscillator, Three Line Break, Smoothed Heikin-Ashi, Equivolume, CandleVolume |
|
||||
| Alt-Chart Bars | Renko (box-size bricks), Kagi (reversal-amount lines), Point & Figure (X/O columns), Range, Tick, Volume, Dollar, Imbalance, Run, Three-Line Break |
|
||||
| Candlestick Patterns | Doji, Hammer, Inverted Hammer, Hanging Man, Shooting Star, Engulfing, Harami, Morning/Evening Star, Three White Soldiers/Black Crows, Piercing Line/Dark Cloud Cover, Marubozu, Tweezer, Spinning Top, Three Inside Up/Down, Three Outside Up/Down, Two Crows, Upside Gap Two Crows, Identical Three Crows, Three Line Strike, Three Stars in the South, Abandoned Baby, Advance Block, Belt-hold, Breakaway, Counterattack, Doji Star, Dragonfly Doji, Gravestone Doji, Long-Legged Doji, Rickshaw Man, Evening Doji Star, Morning Doji Star, Gap Side-by-Side White, High-Wave, Hikkake, Modified Hikkake, Homing Pigeon, On-Neck, In-Neck, Thrusting, Separating Lines, Kicking, Kicking by Length, Ladder Bottom, Mat Hold, Matching Low, Long Line, Short Line, Rising Three Methods, Falling Three Methods, Upside Gap Three Methods, Downside Gap Three Methods, Stalled Pattern, Stick Sandwich, Takuri, Closing Marubozu, Opening Marubozu, Tasuki Gap, Unique Three River, Concealing Baby Swallow, Tristar, Harami Cross, Tower Top/Bottom, Dumpling Top, New Price Lines, Frying Pan Bottom |
|
||||
| Chart Patterns | Double Top / Bottom, Triple Top / Bottom, Head and Shoulders, Triangle (asc/desc/sym), Wedge (rising/falling), Flag / Pennant, Rectangle / Range, Cup and Handle |
|
||||
| Harmonic Patterns | AB=CD, Gartley, Butterfly, Bat, Crab, Shark, Cypher, Three Drives |
|
||||
| Fibonacci | Fibonacci Retracement, Fibonacci Extension, Fibonacci Projection, Auto-Fibonacci, Golden Pocket, Fibonacci Confluence, Fibonacci Fan, Fibonacci Arcs, Fibonacci Channel, Fibonacci Time Zones |
|
||||
| Microstructure | Order-Book Imbalance (Top-1 / Top-N / Full), Microprice, Quoted Spread, Depth Slope, Signed Volume, Cumulative Volume Delta, Trade Imbalance, Effective Spread, Realized Spread, Kyle's Lambda, Footprint, Order Flow Imbalance, VPIN, Amihud Illiquidity, Roll Measure, Trade-Sign Autocorrelation, Hasbrouck Information Share |
|
||||
| Derivatives | Funding Rate, Funding Rate Mean, Funding Rate Z-Score, Funding Basis, Open-Interest Delta, OI / Price Divergence, OI-Weighted Price, Long/Short Ratio, Taker Buy/Sell Ratio, Liquidation Features, Term-Structure Basis, Calendar Spread, Estimated Leverage Ratio, OI-to-Volume Ratio, Perpetual Premium Index, Funding-Implied APR, Open-Interest Momentum |
|
||||
| Market Profile | Value Area (POC / VAH / VAL), Volume Profile (histogram), TPO Profile, Initial Balance, Opening Range, Naked POC, Single Prints, Profile Shape, High/Low Volume Nodes, Composite Profile |
|
||||
| Market Breadth | Advance/Decline Line, Advance/Decline Ratio, Advance/Decline Volume Line, McClellan Oscillator, McClellan Summation Index, TRIN / Arms Index, Breadth Thrust, New Highs - New Lows, High-Low Index, Percent Above Moving Average, Up/Down Volume Ratio, Bullish Percent Index, Cumulative Volume Index, Absolute Breadth Index, TICK Index |
|
||||
| Risk / Performance | Sharpe Ratio, Sortino Ratio, Calmar Ratio, Omega Ratio, Max Drawdown, Average Drawdown, Drawdown Duration, Pain Index, Value at Risk, Conditional Value at Risk (CVaR), Profit Factor, Gain/Loss Ratio, Recovery Factor, Kelly Criterion, Treynor Ratio, Information Ratio, Alpha (Jensen) |
|
||||
| Seasonality & Session | Session VWAP, Session High/Low, Session Range, Average Daily Range, Overnight Gap, Overnight/Intraday Return, Turn-of-Month, Seasonal Z-Score, Time-of-Day Return Profile, Day-of-Week Profile, Intraday Volatility Profile, Volume-by-Time Profile |
|
||||
|
||||
Every candlestick pattern emits a signed per-bar value — `+1.0` bullish,
|
||||
`−1.0` bearish, `0.0` none — so the family drops straight into a feature matrix
|
||||
@@ -168,8 +215,9 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
|
||||
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
|
||||
`new Doji(true)`) for a dragonfly / gravestone `±1` reading.
|
||||
|
||||
Adding a new indicator means implementing one trait in Rust; all four bindings
|
||||
inherit it automatically.
|
||||
Adding a new indicator means implementing one trait in Rust; every binding
|
||||
inherits it automatically (the C ABI — and the C#, Go, Java and R bindings generated from
|
||||
it — regenerate from the core).
|
||||
|
||||
## Languages
|
||||
|
||||
@@ -179,12 +227,41 @@ inherit it automatically.
|
||||
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
|
||||
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
|
||||
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
|
||||
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
|
||||
| C# (C ABI) | `dotnet add package Wickra`, see [`bindings/csharp`](bindings/csharp) | `examples/csharp/streaming` |
|
||||
| Go (cgo, C ABI) | `go get github.com/wickra-lib/wickra/bindings/go`, see [`bindings/go`](bindings/go) | `examples/go/streaming` |
|
||||
| Java (FFM, C ABI) | Maven Central `org.wickra:wickra`, see [`bindings/java`](bindings/java) | `examples/java` (`Streaming`) |
|
||||
| R (`.Call`, C ABI) | `R CMD INSTALL bindings/r`, see [`bindings/r`](bindings/r) | `examples/r/streaming.R` |
|
||||
|
||||
Each binding ships several runnable examples (streaming, backtest, live feed);
|
||||
[`examples/README.md`](examples/README.md) is the full cross-language index.
|
||||
|
||||
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a
|
||||
memory-safe implementation.
|
||||
The wickra-core crate is `unsafe`-forbidden, so the native bindings are
|
||||
memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
|
||||
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
|
||||
|
||||
## Requirements
|
||||
|
||||
The minimum supported version per language. Prebuilt packages (Rust, Python,
|
||||
Node.js, WASM, C#) need only the runtime; the C-ABI bindings that compile on
|
||||
install — Go (cgo) and R (`.Call`) — also need a C compiler, and Java runs with
|
||||
`--enable-native-access=ALL-UNNAMED`.
|
||||
|
||||
| Language | Package | Minimum supported |
|
||||
|-------------|--------------------------------------|----------------------------|
|
||||
| Rust | crates.io · `wickra` | 1.86 (MSRV) |
|
||||
| Python | PyPI · `wickra` (abi3 wheel) | 3.9 (tested through 3.13) |
|
||||
| Node.js | npm · `wickra` (N-API 8) | 20 (tested on 22 · 24 LTS) |
|
||||
| WASM | npm · `wickra-wasm` | any modern JS engine |
|
||||
| C | `wickra.h` + library (releases) | C99 compiler |
|
||||
| C++ | `wickra.hpp` over the C ABI | C++14 compiler |
|
||||
| C# | NuGet · `Wickra` | .NET 8 (`net8.0`) |
|
||||
| Go | module · `wickra-lib/wickra-go` | Go 1.23 (cgo) |
|
||||
| Java | Maven Central · `org.wickra:wickra` | Java 22 (FFM / Panama) |
|
||||
| R | source package | R ≥ 2.10 (Rtools on Win.) |
|
||||
|
||||
Full per-language detail (runtime vs. build-from-source) is on the
|
||||
[Requirements page](https://docs.wickra.org/Requirements) in the docs.
|
||||
|
||||
## Rust API
|
||||
|
||||
@@ -209,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};
|
||||
@@ -231,33 +313,46 @@ 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
|
||||
|
||||
```
|
||||
wickra/
|
||||
├── crates/
|
||||
│ ├── wickra-core/ core engine + all 314 indicators
|
||||
│ ├── wickra-core/ core engine + all 514 indicators
|
||||
│ ├── wickra/ top-level facade crate (publishes on crates.io) + benches/
|
||||
│ └── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ ├── wickra-data/ CSV reader, tick aggregator, live exchange feeds
|
||||
│ └── wickra-bench/ internal cross-library benchmark harness (not published)
|
||||
├── bindings/
|
||||
│ ├── python/ PyO3 + maturin (publishes on PyPI)
|
||||
│ ├── node/ napi-rs (publishes on npm)
|
||||
│ └── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── wasm/ wasm-bindgen (browsers, bundlers, Node)
|
||||
│ ├── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
|
||||
│ ├── csharp/ C# binding over the C ABI (publishes on NuGet)
|
||||
│ ├── go/ Go binding over the C ABI via cgo (module tag)
|
||||
│ ├── r/ R binding over the C ABI via .Call (R package)
|
||||
│ └── java/ Java binding over the C ABI via the FFM API (Maven Central)
|
||||
├── 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`)
|
||||
│ └── wasm/ browser demo for `wickra-wasm`
|
||||
│ ├── 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`)
|
||||
│ ├── go/ streaming, backtest, strategies (cgo binding)
|
||||
│ ├── r/ streaming, backtest, strategies (.Call binding)
|
||||
│ └── java/ streaming, backtest, strategies (FFM binding)
|
||||
└── .github/workflows/ CI and release pipelines
|
||||
```
|
||||
|
||||
Rust benchmarks live in `crates/wickra/benches/`; runnable Rust examples live
|
||||
in the workspace member crate at `examples/rust/`. There is no top-level
|
||||
`benches/` directory.
|
||||
Wickra's own regression benchmarks live in `crates/wickra/benches/`; the
|
||||
cross-library comparison against kand, ta-rs and yata lives in the internal
|
||||
`crates/wickra-bench/` crate. Runnable Rust examples live in the workspace member
|
||||
crate at `examples/rust/`. There is no top-level `benches/` directory.
|
||||
|
||||
## Building everything from source
|
||||
|
||||
@@ -265,7 +360,8 @@ in the workspace member crate at `examples/rust/`. There is no top-level
|
||||
# Rust core + tests
|
||||
cargo test --workspace
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
cargo bench -p wickra
|
||||
cargo bench -p wickra # Wickra's own regression benchmarks
|
||||
cargo bench -p wickra-bench # cross-library comparison (kand, ta-rs, yata)
|
||||
|
||||
# Python binding (requires Rust toolchain + maturin)
|
||||
cd bindings/python
|
||||
@@ -277,6 +373,25 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
|
||||
|
||||
# Node binding (requires @napi-rs/cli)
|
||||
cd bindings/node && npm install && npm run build && npm test
|
||||
|
||||
# C ABI (cdylib + staticlib + generated header)
|
||||
cargo build -p wickra-c --release
|
||||
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
|
||||
cmake --build examples/c/build && ctest --test-dir examples/c/build --output-on-failure
|
||||
|
||||
# C# binding (requires the .NET 8 SDK; links the C ABI above)
|
||||
dotnet test bindings/csharp/Wickra.Tests/Wickra.Tests.csproj
|
||||
|
||||
# Go binding (requires a C compiler for cgo; links the C ABI above)
|
||||
cp target/release/libwickra.so bindings/go/lib/ # .dylib on macOS, wickra.dll on Windows
|
||||
cd bindings/go && go test ./...
|
||||
|
||||
# R binding (requires a C toolchain / Rtools; links the C ABI above)
|
||||
WICKRA_INCLUDE_DIR="$PWD/bindings/c/include" WICKRA_LIB_DIR="$PWD/target/release" \
|
||||
R CMD INSTALL bindings/r
|
||||
|
||||
# Java binding (requires JDK 22+ and Maven; links the C ABI above)
|
||||
mvn -f bindings/java test
|
||||
```
|
||||
|
||||
## Testing
|
||||
@@ -286,7 +401,10 @@ Every layer is covered; run the suites with the commands in
|
||||
|
||||
- `wickra-core`: unit tests per indicator — textbook reference values
|
||||
(Wilder RSI, Bollinger Bands, MACD, ATR, Stochastic), `batch == streaming`
|
||||
equivalence, `reset` semantics, NaN/Inf handling, and property tests.
|
||||
equivalence, `reset` semantics, NaN/Inf handling, and property tests. A
|
||||
catalogue-wide property harness (`tests/invariants.rs`) additionally asserts
|
||||
`batch == streaming`, `reset == fresh`, and non-finite-input rejection for
|
||||
**every** indicator and bar-builder.
|
||||
- `wickra-data`: unit tests for CSV decoding, the tick aggregator, the
|
||||
resampler, and the Binance payload parser.
|
||||
- `bindings/python`: pytest covering smoke checks, streaming/batch
|
||||
@@ -296,6 +414,26 @@ Every layer is covered; run the suites with the commands in
|
||||
values across all indicators.
|
||||
- `bindings/wasm`: `wasm-bindgen-test` cases for constructors, equivalence,
|
||||
and reference values.
|
||||
- `bindings/c`: Rust unit tests over the FFI boundary, plus C and C++ smoke
|
||||
tests and offline example `ctest`s run on the three OSes.
|
||||
- `bindings/csharp`: `dotnet test` cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input) plus SMA reference values.
|
||||
- `bindings/go`: `go test` cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input), reset, and lifecycle.
|
||||
- `bindings/r`: `testthat` cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input), reset, and validation.
|
||||
- `bindings/java`: JUnit cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input) plus batch equivalence.
|
||||
|
||||
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
|
||||
|
||||
@@ -323,13 +461,20 @@ shape together before you invest the time.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the **PolyForm Noncommercial License 1.0.0**. See [LICENSE](LICENSE).
|
||||
Licensed under either of
|
||||
|
||||
In plain English: use it, fork it, modify it, redistribute it, file issues, send
|
||||
pull requests — all welcome. Personal projects, research, education, non-profits,
|
||||
government, hobby trading bots: all fine. The one thing that's not allowed is
|
||||
commercial sale of the software or of services built around it. If you want to
|
||||
use Wickra commercially, get in touch about a license.
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
<http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option. Use it, fork it, modify it, redistribute it — commercially or
|
||||
not — file issues, send pull requests; all welcome.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
@@ -345,16 +490,23 @@ The library is provided **as is**, without warranty of any kind; see
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/wickra-lib/wickra/stargazers">
|
||||
<img alt="GitHub stars" src="https://img.shields.io/github/stars/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=ffd866">
|
||||
<img alt="GitHub stars" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/stars.svg">
|
||||
</a>
|
||||
<a href="https://github.com/wickra-lib/wickra/network/members">
|
||||
<img alt="GitHub forks" src="https://img.shields.io/github/forks/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=78dce8">
|
||||
<img alt="GitHub forks" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/forks.svg">
|
||||
</a>
|
||||
<a href="https://github.com/wickra-lib/wickra/issues">
|
||||
<img alt="GitHub issues" src="https://img.shields.io/github/issues/wickra-lib/wickra?style=for-the-badge&logo=github&logoColor=white&color=ff6188">
|
||||
<img alt="GitHub issues" src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/issues.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
If Wickra saved you time, the cheapest way to say thanks is to ⭐ the repo.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://star-history.com/#wickra-lib/wickra&Date">
|
||||
<img alt="Wickra star history" width="640"
|
||||
src="https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/star-history.svg">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Roadmap
|
||||
|
||||
This roadmap describes the project's direction at a high level. It is
|
||||
intentionally non-binding: priorities shift with feedback and available time,
|
||||
and the authoritative, up-to-date view of planned work is the
|
||||
[issue tracker](https://github.com/wickra-lib/wickra/issues). Shipped changes
|
||||
are recorded in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
## Status
|
||||
|
||||
Wickra is **pre-1.0**. The public API is largely stable but may still change in
|
||||
minor releases; breaking changes are called out in the changelog.
|
||||
|
||||
## Themes
|
||||
|
||||
- **Indicator coverage.** Continue broadening the indicator catalogue across
|
||||
families (trend, momentum, volatility, volume, statistics, market profile,
|
||||
and more), each with the same streaming/batch parity and test guarantees.
|
||||
- **API stabilization toward 1.0.** Settle the public `Indicator` and
|
||||
`BarBuilder` traits and the binding surfaces, then commit to semantic
|
||||
versioning stability for a 1.0 release.
|
||||
- **Performance.** Keep per-tick updates O(1) and maintain the benchmark suite;
|
||||
investigate further allocation and cache improvements.
|
||||
- **Bindings parity.** Keep the Python, Node.js and WASM bindings — plus
|
||||
the C ABI and the C#, Go, Java and R bindings generated from it — in lockstep with the
|
||||
Rust core, including type stubs and platform coverage.
|
||||
- **Documentation.** Maintain a deep-dive page per indicator on
|
||||
<https://docs.wickra.org>, plus quickstarts and cookbook material.
|
||||
- **Project health.** Maintain test coverage, static and dynamic analysis,
|
||||
signed releases, and supply-chain monitoring.
|
||||
|
||||
## How to influence the roadmap
|
||||
|
||||
Open or comment on an issue, or start with the
|
||||
[feature-request template](.github/ISSUE_TEMPLATE/feature_request.md).
|
||||
Well-scoped proposals and pull requests are the most effective way to move an
|
||||
item forward.
|
||||
+103
-3
@@ -2,13 +2,13 @@
|
||||
|
||||
## Supported versions
|
||||
|
||||
Wickra is pre-1.0. Security fixes are applied to the latest released `0.1.x`
|
||||
Wickra is pre-1.0. Security fixes are applied to the latest released `0.9.6`
|
||||
version only; please upgrade to the newest release before reporting an issue.
|
||||
|
||||
| Version | Supported |
|
||||
| --- | --- |
|
||||
| 0.1.x (latest) | :white_check_mark: |
|
||||
| older 0.1.x | :x: |
|
||||
| 0.9.6 (latest) | :white_check_mark: |
|
||||
| < 0.9.6 | :x: |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
@@ -41,3 +41,103 @@ PyPI/npm packages, and the build/release workflows in `.github/workflows/`.
|
||||
|
||||
Out of scope: vulnerabilities in third-party dependencies (report those
|
||||
upstream; we track them via Dependabot and `cargo-deny`).
|
||||
|
||||
## Security assurance case
|
||||
|
||||
This is a short, evidence-backed argument for why Wickra can be used safely.
|
||||
|
||||
**Security requirements.** Wickra is a computational library: it ingests
|
||||
numeric market data and produces indicator values. It stores no user
|
||||
credentials, authenticates no external users, and implements no cryptography of
|
||||
its own. The requirements are therefore: (1) memory safety and freedom from
|
||||
undefined behaviour, (2) robust handling of untrusted/degenerate numeric input
|
||||
without panics or unbounded resource use, (3) integrity of the published
|
||||
artifacts, and (4) a healthy dependency supply chain.
|
||||
|
||||
**How the requirements are met.**
|
||||
|
||||
- *Memory safety* — the core and all bindings are written in Rust. The crates
|
||||
forbid or minimise `unsafe`, so the compiler guarantees memory and thread
|
||||
safety for the indicator logic. The one exception is the C ABI
|
||||
([`bindings/c`](bindings/c)), whose thin FFI shim is necessarily `unsafe`
|
||||
because it dereferences caller-supplied pointers; it adds no indicator logic,
|
||||
validates every handle for NULL, and never lets a panic cross the boundary, so
|
||||
the safe core's guarantees still cover all computation.
|
||||
- *Input robustness* — every indicator validates its parameters and rejects
|
||||
non-finite inputs at construction; behaviour on edge cases (flat markets,
|
||||
warmup, reset) is pinned by unit tests, and the public update paths are
|
||||
exercised by coverage-guided fuzzing (`cargo-fuzz` / libFuzzer) in CI.
|
||||
- *Static and dynamic analysis* — every push and pull request runs Clippy
|
||||
(`clippy::pedantic`, warnings-as-errors), CodeQL, fuzzing, and the full test
|
||||
suite, with 100% line coverage on the core crate tracked by Codecov.
|
||||
- *Artifact integrity* — releases are built in CI, commits and tags are signed,
|
||||
the `main` branch requires signed commits, and release artifacts carry build
|
||||
provenance attestations.
|
||||
- *Supply chain* — dependencies are pinned and monitored with Dependabot and
|
||||
audited with `cargo-deny` (license + advisory checks) on every change.
|
||||
|
||||
**Residual risk.** The optional `live-binance` feature opens a TLS WebSocket to
|
||||
an exchange using the platform TLS library; transport security therefore
|
||||
depends on that library, not on Wickra. Wickra is not a trading system and is
|
||||
provided "as is" — see the disclaimers in `README.md` and the licenses.
|
||||
|
||||
## Secrets management
|
||||
|
||||
The project stores **no** secrets or credentials in the version control system.
|
||||
Secrets required by automation (publishing tokens, the about-sync PAT) are kept
|
||||
exclusively as **GitHub Actions encrypted secrets** and referenced via the
|
||||
`secrets.*` context; they are never written to the repository, logs, or build
|
||||
artifacts. GitHub **secret scanning with push protection** is enabled to block
|
||||
accidental commits of credentials. Secrets follow least privilege (the narrowest
|
||||
scope that works) and are rotated when a holder changes or on suspected
|
||||
exposure.
|
||||
|
||||
## Verifying releases
|
||||
|
||||
Released artifacts can be verified for integrity and authenticity:
|
||||
|
||||
- **Build provenance.** Release assets carry GitHub build provenance
|
||||
attestations. Verify a downloaded asset with the GitHub CLI:
|
||||
`gh attestation verify <file> --repo wickra-lib/wickra`.
|
||||
- **Signed tags.** Each release corresponds to a signed git tag (`vX.Y.Z`);
|
||||
the tag signature identifies the maintainer who authorised the release.
|
||||
- **Registry integrity.** Packages are distributed over HTTPS from crates.io,
|
||||
PyPI and npm, which serve package checksums that package managers verify on
|
||||
install.
|
||||
|
||||
The release is published only by the maintainer through the tag-triggered
|
||||
release workflow, so a verified tag signature establishes the expected
|
||||
publisher identity.
|
||||
|
||||
## Support timeline and end of support
|
||||
|
||||
Wickra is **pre-1.0**: only the **latest released `0.y.z`** version receives
|
||||
security fixes. When a newer release is published, the previous version
|
||||
**immediately reaches end of support** and will not receive further fixes;
|
||||
users should upgrade to the latest release. The supported-versions table above
|
||||
is authoritative. After the `1.0.0` release this policy will be revised to
|
||||
support a defined window of releases.
|
||||
|
||||
## Remediation policy (dependencies and code scanning)
|
||||
|
||||
- **Severity threshold.** Vulnerabilities of **medium severity or higher** in
|
||||
the project's own code or its dependencies are remediated promptly and before
|
||||
the next release; lower-severity findings are addressed on a best-effort
|
||||
basis.
|
||||
- **Automated enforcement (SCA).** Every change is evaluated by `cargo-deny`
|
||||
(RUSTSEC advisories + license policy) and Dependabot; a known-vulnerable
|
||||
dependency fails CI and **blocks the change** until resolved or explicitly
|
||||
waived with justification.
|
||||
- **Automated enforcement (SAST).** Every change is evaluated by CodeQL and
|
||||
Clippy (`-D warnings`); findings **block the change** in CI until fixed.
|
||||
- **Pre-release gate.** A release is not cut while an unresolved medium-or-higher
|
||||
SCA/SAST finding is outstanding.
|
||||
|
||||
## Vulnerability exploitability (VEX)
|
||||
|
||||
Advisories reported by `cargo-deny`/Dependabot for third-party dependencies that
|
||||
do **not** affect Wickra (e.g. the vulnerable code path is not reachable, or the
|
||||
affected feature is not enabled) are triaged and recorded — with the
|
||||
not-affected justification — in the `cargo-deny` configuration (`deny.toml`) and
|
||||
the relevant pull request, rather than forcing an unnecessary dependency bump.
|
||||
This serves as the project's exploitability (VEX) record.
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Support
|
||||
|
||||
Thanks for using Wickra! Here is where to get help, depending on what you need.
|
||||
|
||||
## Documentation first
|
||||
|
||||
Most questions are answered in the documentation:
|
||||
|
||||
- **Docs site:** <https://docs.wickra.org> — quickstarts for Rust, Python,
|
||||
Node.js, WASM, C, C++, C#, Go, Java and R, a per-indicator reference, warmup periods, the
|
||||
data layer, and an FAQ.
|
||||
- **README:** <https://github.com/wickra-lib/wickra#readme> — installation and a
|
||||
quick overview.
|
||||
- **API docs (Rust):** <https://docs.rs/wickra>.
|
||||
|
||||
## Questions and help
|
||||
|
||||
- Ask a question with the
|
||||
[question issue template](.github/ISSUE_TEMPLATE/question.md).
|
||||
- Browse [existing issues](https://github.com/wickra-lib/wickra/issues) — your
|
||||
question may already be answered.
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
- **Bugs:** use the bug-report issue template.
|
||||
- **Feature requests / new indicators:** use the feature-request template.
|
||||
|
||||
## Security issues
|
||||
|
||||
Please do **not** report security vulnerabilities through public issues. Follow
|
||||
the process in [`SECURITY.md`](SECURITY.md) (private GitHub advisory or email).
|
||||
|
||||
## Support expectations
|
||||
|
||||
Wickra is maintained by a single maintainer on a best-effort basis. Issues are
|
||||
triaged and acknowledged as time allows; there is no commercial support or SLA.
|
||||
Clear, reproducible reports get help fastest.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Threat model
|
||||
|
||||
This document describes Wickra's attack surface and the threats considered,
|
||||
together with their mitigations. It complements the security assurance case in
|
||||
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
|
||||
library (a Rust core with Python, Node.js and WASM bindings plus a C ABI
|
||||
and the C#, Go, Java and R bindings built on it),
|
||||
not a network service or trading system; the attack surface is correspondingly
|
||||
small.
|
||||
|
||||
## Assets
|
||||
|
||||
- **Integrity of computed indicator values** — consumers may use them in
|
||||
automated decisions, so silently wrong output is the primary concern.
|
||||
- **Availability of the calling process** — a library must not crash or hang
|
||||
its host on malformed input.
|
||||
- **Integrity of published artifacts** — the crates, wheels and npm packages
|
||||
users install.
|
||||
- **The build and release pipeline** and its secrets (publishing tokens).
|
||||
|
||||
## Actors / trust boundaries
|
||||
|
||||
- **Library consumer** (trusted) — calls the API with numeric data. Data may
|
||||
originate from untrusted sources (e.g. a market feed), so *input values* are
|
||||
treated as untrusted even though the caller is trusted.
|
||||
- **Optional live feed** — with the `live-binance` feature, data crosses a
|
||||
network boundary from an exchange over TLS.
|
||||
- **Contributors** (semi-trusted) — propose changes via pull requests.
|
||||
- **Supply chain** — upstream dependencies and the CI/CD platform.
|
||||
|
||||
## Threats and mitigations
|
||||
|
||||
| Threat | Mitigation |
|
||||
| --- | --- |
|
||||
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
|
||||
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and is built with `panic = "abort"` so a panic terminates the process deterministically instead of unwinding across the FFI boundary (which would be undefined behaviour). A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
|
||||
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
|
||||
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
|
||||
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
|
||||
| Adversary-in-the-middle on the optional live feed | Connection uses TLS via the platform library; transport security is delegated to that reviewed implementation. |
|
||||
| Compromised dependency (supply chain) | Dependencies pinned (`Cargo.lock`, hash-locked CI requirements), monitored by Dependabot, audited by `cargo-deny` (advisories + licenses) on every change. |
|
||||
| Malicious or accidental change to `main` | Branch protection requires signed commits and blocks force-push and deletion; all changes flow through pull requests with required CI; static analysis (CodeQL, Clippy) and fuzzing run on every change. |
|
||||
| Compromised CI / leaked secrets | Workflows use least-privilege `permissions:`; secrets live only as encrypted GitHub Actions secrets; secret scanning with push protection is enabled; workflows are linted by `zizmor`. |
|
||||
| Tampered release artifact | Releases are built in CI, tags are signed, and assets carry build provenance attestations (verifiable with `gh attestation verify`). |
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Wickra implements no authentication, authorization or cryptography of its own,
|
||||
stores no user data, and exposes no network listener; those threat classes do
|
||||
not apply.
|
||||
- Vulnerabilities in third-party dependencies that do not affect Wickra are
|
||||
tracked as exploitability (VEX) records (see [`SECURITY.md`](SECURITY.md)).
|
||||
|
||||
## Maintenance
|
||||
|
||||
This threat model is reviewed when the architecture changes materially (for
|
||||
example, a new input family, a new network feature, or a new release channel).
|
||||
@@ -0,0 +1,72 @@
|
||||
[package]
|
||||
name = "wickra-c"
|
||||
description = "C ABI (cdylib + staticlib) for the Wickra streaming-first technical indicators library — the hub every C-capable language (C, C++, Go, C#, Java, R) links against."
|
||||
version.workspace = true
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
readme = "README.md"
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
name = "wickra"
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
|
||||
# The C ABI inherently needs `unsafe` (raw pointers across the FFI boundary,
|
||||
# `#[export_name]` symbol control). The workspace forbids `unsafe_code`, so this
|
||||
# crate cannot inherit `workspace = true`; it mirrors every workspace lint and
|
||||
# only relaxes `unsafe_code` to `allow` (parity with how the proc-macro bindings
|
||||
# emit their unsafe). The Rust core stays `unsafe`-forbidden — this is the one
|
||||
# crate where the boundary lives.
|
||||
[lints.rust]
|
||||
unsafe_code = "allow"
|
||||
missing_debug_implementations = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unused_must_use = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
all = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
similar_names = "allow"
|
||||
float_cmp = "allow"
|
||||
|
||||
[features]
|
||||
# `parallel` (rayon-backed batch in wickra-core) is on by default for native
|
||||
# builds. The wasm32-unknown-emscripten target has no threads, so the R
|
||||
# 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.
|
||||
#
|
||||
# `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
|
||||
# `default-features = false` is ignored when inheriting a workspace dep that
|
||||
# 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 }
|
||||
@@ -0,0 +1,101 @@
|
||||
# Wickra — C / C++
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://github.com/wickra-lib/wickra/releases/latest)
|
||||
[](https://github.com/wickra-lib/wickra#license)
|
||||
|
||||
**Streaming-first technical indicators for C and C++. A prebuilt shared/static
|
||||
library plus a generated `wickra.h` — no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js and WASM, plus a C ABI for C, C++, C#, Go, Java, R and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the **C ABI hub**: it compiles the
|
||||
core to a C-compatible shared/static library plus a generated header, so any
|
||||
C-capable language (C, C++, C#, Go, Java, R) links against one artifact instead
|
||||
of re-wrapping every indicator natively.
|
||||
|
||||
## Install
|
||||
|
||||
Grab the prebuilt header + library for your platform from the
|
||||
[GitHub releases](https://github.com/wickra-lib/wickra/releases) — each archive
|
||||
has `wickra.h`, the optional `wickra.hpp` C++ wrapper, and the shared/static
|
||||
library — or build from source:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
# -> target/release/libwickra.{so,dylib} or wickra.dll (+ import lib) + a staticlib
|
||||
```
|
||||
|
||||
Then compile against the header and link the library
|
||||
(`cc app.c -I include -L lib -lwickra -lm -o app`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```c
|
||||
#include "wickra.h"
|
||||
|
||||
struct Rsi *rsi = wickra_rsi_new(14); /* NULL on invalid params */
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
double v = wickra_rsi_update(rsi, prices[i]); /* NaN during warmup */
|
||||
if (v == v && v > 70.0) /* v == v is the NaN check */
|
||||
printf("overbought\n");
|
||||
}
|
||||
wickra_rsi_free(rsi); /* exactly once per _new */
|
||||
```
|
||||
|
||||
Every indicator is an opaque handle with the same five functions —
|
||||
`_new` / `_update` / `_batch` / `_reset` / `_free`. `update` is O(1); there is no
|
||||
RAII across the C boundary, so each `_new` needs exactly one `_free`, and every
|
||||
function is NULL-safe (a NULL handle yields `NaN` or a no-op, never a crash).
|
||||
Multi-output indicators (MACD, Bollinger, ADX, …) take a pointer to a `#[repr(C)]`
|
||||
struct and return a `bool`. The optional `wickra.hpp` wraps any handle in a
|
||||
move-only `wickra::Handle` for exception-safe C++ lifetimes.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/throughput.c` reports streaming and batch updates-per-second for
|
||||
`SMA`, `ATR` and `MACD`. As the thinnest binding it is the floor of the
|
||||
per-binding FFI overhead — not a cross-library ratio (the same Rust core runs
|
||||
under every binding); see the repository
|
||||
[BENCHMARKS.md](https://github.com/wickra-lib/wickra/blob/main/BENCHMARKS.md) §3.
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
cmake -S benchmarks -B build && cmake --build build
|
||||
./build/throughput
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (C quickstart, cookbook, TA-Lib migration): <https://docs.wickra.org/Quickstart-C>
|
||||
- **Runnable examples:** [`examples/c/`](https://github.com/wickra-lib/wickra/tree/main/examples/c)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WASM and Rust, plus this
|
||||
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
|
||||
all exposing the same indicators from the shared Rust core.
|
||||
|
||||
## Security
|
||||
|
||||
Found a security issue? **Please don't open a public issue.** Report it privately
|
||||
via the affected repository's *Security* tab (*"Report a vulnerability"*) or email
|
||||
**support@wickra.org** with a subject line starting `[wickra security]`. Full
|
||||
policy: <https://github.com/wickra-lib/wickra/blob/main/SECURITY.md>.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,40 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(wickra_c_benchmarks C)
|
||||
|
||||
# Directory holding the compiled Wickra C library (cargo output), e.g.
|
||||
# <workspace>/target/release. Override with -DWICKRA_LIB_DIR=/path/to/target/release.
|
||||
if(NOT DEFINED WICKRA_LIB_DIR)
|
||||
set(WICKRA_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../../target/release")
|
||||
endif()
|
||||
set(WICKRA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../include")
|
||||
|
||||
# Pick the right link target per platform/toolchain.
|
||||
# - MSVC links the generated import library (wickra.dll.lib).
|
||||
# - MinGW/gcc on Windows links the DLL directly.
|
||||
# - Unix links the shared object / dylib.
|
||||
if(WIN32)
|
||||
set(WICKRA_RUNTIME "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
if(MSVC)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll.lib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/wickra.dll")
|
||||
endif()
|
||||
elseif(APPLE)
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.dylib")
|
||||
else()
|
||||
set(WICKRA_LINK_LIB "${WICKRA_LIB_DIR}/libwickra.so")
|
||||
endif()
|
||||
|
||||
add_executable(throughput throughput.c)
|
||||
target_include_directories(throughput PRIVATE "${WICKRA_INCLUDE_DIR}")
|
||||
target_link_libraries(throughput PRIVATE "${WICKRA_LINK_LIB}")
|
||||
if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(throughput PRIVATE m)
|
||||
endif()
|
||||
|
||||
# On Windows copy the DLL next to the executable so the loader finds it.
|
||||
if(WIN32)
|
||||
add_custom_command(TARGET throughput POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:throughput>")
|
||||
endif()
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Throughput benchmark for the Wickra C ABI.
|
||||
*
|
||||
* Measures how many indicator updates per second the C ABI sustains, both
|
||||
* per-tick (streaming `_update`) and bulk (`_batch`), over a synthetic OHLCV
|
||||
* series. It is the C counterpart of the Node throughput.js and the Rust
|
||||
* criterion benches: it benchmarks Wickra's own O(1) streaming engine through
|
||||
* the raw C boundary (there is no comparable streaming TA library to compare
|
||||
* against), so the headline number is raw throughput, not a cross-library
|
||||
* ratio. C is the thinnest binding, so these numbers are the floor of the
|
||||
* per-binding FFI overhead the higher-level bindings build on.
|
||||
*
|
||||
* Three indicators are timed, chosen by call-signature archetype rather than
|
||||
* algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
||||
* (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
||||
* single-output SMA and ATR (the C ABI has no MACD batch entry point).
|
||||
*
|
||||
* Build the C ABI library first, then build and run the benchmark:
|
||||
*
|
||||
* cargo build -p wickra-c --release
|
||||
* cmake -S bindings/c/benchmarks -B build/cbench -DCMAKE_BUILD_TYPE=Release
|
||||
* cmake --build build/cbench
|
||||
* ./build/cbench/throughput # 200k bars (default)
|
||||
* ./build/cbench/throughput 1000000
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "wickra.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
static double now_ns(void) {
|
||||
static LARGE_INTEGER freq;
|
||||
static int init = 0;
|
||||
LARGE_INTEGER counter;
|
||||
if (!init) {
|
||||
QueryPerformanceFrequency(&freq);
|
||||
init = 1;
|
||||
}
|
||||
QueryPerformanceCounter(&counter);
|
||||
return (double)counter.QuadPart * 1e9 / (double)freq.QuadPart;
|
||||
}
|
||||
#else
|
||||
#include <time.h>
|
||||
static double now_ns(void) {
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec * 1e9 + (double)ts.tv_nsec;
|
||||
}
|
||||
#endif
|
||||
|
||||
static double median3(double a, double b, double c) {
|
||||
if ((a <= b && b <= c) || (c <= b && b <= a)) return b;
|
||||
if ((b <= a && a <= c) || (c <= a && a <= b)) return a;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* Run `body` once as warmup, then time three repetitions and store the median
|
||||
* elapsed nanoseconds in `dst`. `body` is a brace-enclosed statement block. */
|
||||
#define MEASURE(dst, body) \
|
||||
do { \
|
||||
body; \
|
||||
double s0, s1, s2, t0; \
|
||||
t0 = now_ns(); body; s0 = now_ns() - t0; \
|
||||
t0 = now_ns(); body; s1 = now_ns() - t0; \
|
||||
t0 = now_ns(); body; s2 = now_ns() - t0; \
|
||||
(dst) = median3(s0, s1, s2); \
|
||||
} while (0)
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
size_t bars = 200000;
|
||||
if (argc > 1) {
|
||||
long n = strtol(argv[1], NULL, 10);
|
||||
if (n >= 1000) {
|
||||
bars = (size_t)n;
|
||||
}
|
||||
}
|
||||
const size_t n = bars;
|
||||
|
||||
/* Deterministic synthetic OHLCV (no RNG, so runs are comparable). */
|
||||
double *open = malloc(n * sizeof(double));
|
||||
double *high = malloc(n * sizeof(double));
|
||||
double *low = malloc(n * sizeof(double));
|
||||
double *close = malloc(n * sizeof(double));
|
||||
double *volume = malloc(n * sizeof(double));
|
||||
int64_t *timestamp = malloc(n * sizeof(int64_t));
|
||||
double *out = malloc(n * sizeof(double)); /* reused batch scratch buffer */
|
||||
if (!open || !high || !low || !close || !volume || !timestamp || !out) {
|
||||
fprintf(stderr, "allocation failed\n");
|
||||
return 1;
|
||||
}
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
double mid = 100 + sin((double)i * 0.001) * 20 + (double)i * 1e-4;
|
||||
double c = mid + sin((double)i * 0.05) * 2;
|
||||
close[i] = c;
|
||||
open[i] = mid;
|
||||
high[i] = fmax(c, mid) + 1.5;
|
||||
low[i] = fmin(c, mid) - 1.5;
|
||||
volume[i] = 1000 + (double)(i % 97) * 13;
|
||||
timestamp[i] = (int64_t)i;
|
||||
}
|
||||
|
||||
double ns;
|
||||
double sma_stream, sma_batch, atr_stream, atr_batch, macd_stream;
|
||||
|
||||
MEASURE(ns, {
|
||||
struct Sma *ind = wickra_sma_new(20);
|
||||
for (size_t i = 0; i < n; i++) wickra_sma_update(ind, close[i]);
|
||||
wickra_sma_free(ind);
|
||||
});
|
||||
sma_stream = (double)n / (ns / 1e9) / 1e6;
|
||||
|
||||
MEASURE(ns, {
|
||||
struct Sma *ind = wickra_sma_new(20);
|
||||
wickra_sma_batch(ind, close, out, n);
|
||||
wickra_sma_free(ind);
|
||||
});
|
||||
sma_batch = (double)n / (ns / 1e9) / 1e6;
|
||||
|
||||
MEASURE(ns, {
|
||||
struct Atr *ind = wickra_atr_new(14);
|
||||
for (size_t i = 0; i < n; i++)
|
||||
wickra_atr_update(ind, open[i], high[i], low[i], close[i], volume[i], timestamp[i]);
|
||||
wickra_atr_free(ind);
|
||||
});
|
||||
atr_stream = (double)n / (ns / 1e9) / 1e6;
|
||||
|
||||
MEASURE(ns, {
|
||||
struct Atr *ind = wickra_atr_new(14);
|
||||
wickra_atr_batch(ind, open, high, low, close, volume, timestamp, out, n);
|
||||
wickra_atr_free(ind);
|
||||
});
|
||||
atr_batch = (double)n / (ns / 1e9) / 1e6;
|
||||
|
||||
MEASURE(ns, {
|
||||
struct MacdIndicator *ind = wickra_macd_indicator_new(12, 26, 9);
|
||||
struct WickraMacdOutput value;
|
||||
for (size_t i = 0; i < n; i++) wickra_macd_indicator_update(ind, close[i], &value);
|
||||
wickra_macd_indicator_free(ind);
|
||||
});
|
||||
macd_stream = (double)n / (ns / 1e9) / 1e6;
|
||||
|
||||
printf("Wickra C throughput - %zu bars (median of 3 runs)\n\n", n);
|
||||
printf("%-22s%20s%18s\n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)");
|
||||
printf("------------------------------------------------------------\n");
|
||||
printf("%-22s%20.1f%18.1f\n", "SMA(20)", sma_stream, sma_batch);
|
||||
printf("%-22s%20.1f%18.1f\n", "ATR(14)", atr_stream, atr_batch);
|
||||
printf("%-22s%20.1f%18s\n", "MACD(12,26,9)", macd_stream, "-");
|
||||
|
||||
printf("\nMupd/s = million indicator updates per second. Streaming is the per-tick\n"
|
||||
"`_update` path (one C call per value); batch is the bulk array path (one\n"
|
||||
"C call). Higher is better. Numbers are machine-dependent - use them for\n"
|
||||
"relative comparison, not as a speed claim.\n");
|
||||
|
||||
free(open);
|
||||
free(high);
|
||||
free(low);
|
||||
free(close);
|
||||
free(volume);
|
||||
free(timestamp);
|
||||
free(out);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
language = "C"
|
||||
header = "/* Wickra C ABI — generated by cbindgen. Do not edit by hand. */"
|
||||
include_guard = "WICKRA_H"
|
||||
pragma_once = true
|
||||
# Wrap the declarations in `extern "C"` under __cplusplus so the header is usable
|
||||
# from C++ (the optional wickra.hpp RAII layer and any C++ consumer).
|
||||
cpp_compat = true
|
||||
tab_width = 4
|
||||
# Off: cbindgen copies the Rust struct/fn doc comments verbatim, and some core
|
||||
# indicator docs contain markdown (e.g. `**1/8**/**7/8**`) whose `*/` would close
|
||||
# the C block comment early and break the header. Usage docs live in the crate
|
||||
# README and examples; the header is a pure declaration contract.
|
||||
documentation = false
|
||||
|
||||
[parse]
|
||||
# Parse wickra-core too so the opaque indicator handle types (Sma, Ema, …) are
|
||||
# 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", "wickra-data"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,66 @@
|
||||
// Optional C++ convenience layer over the Wickra C ABI (`wickra.h`).
|
||||
//
|
||||
// The C ABI hands out raw handles that must be released exactly once with the
|
||||
// matching `wickra_<ind>_free`. `wickra::Handle` wraps that in a move-only RAII
|
||||
// owner so the free happens automatically at scope exit:
|
||||
//
|
||||
// #include "wickra.hpp"
|
||||
//
|
||||
// wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(14));
|
||||
// if (sma) {
|
||||
// double v = wickra_sma_update(sma.get(), 42.0); // NaN during warmup
|
||||
// }
|
||||
// // sma is freed here
|
||||
//
|
||||
// This is header-only and adds no runtime cost beyond the C calls themselves.
|
||||
|
||||
#ifndef WICKRA_HPP
|
||||
#define WICKRA_HPP
|
||||
|
||||
#include "wickra.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace wickra {
|
||||
|
||||
/// Move-only RAII owner of a Wickra handle. `T` is the opaque indicator type and
|
||||
/// `Free` its `wickra_<ind>_free` function.
|
||||
template <typename T, void (*Free)(T *)>
|
||||
class Handle {
|
||||
public:
|
||||
explicit Handle(T *ptr) noexcept : ptr_(ptr) {}
|
||||
|
||||
~Handle() {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
Handle(const Handle &) = delete;
|
||||
Handle &operator=(const Handle &) = delete;
|
||||
|
||||
Handle(Handle &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
|
||||
|
||||
Handle &operator=(Handle &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (ptr_ != nullptr) {
|
||||
Free(ptr_);
|
||||
}
|
||||
ptr_ = std::exchange(other.ptr_, nullptr);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// The raw handle, for passing to the `wickra_<ind>_*` functions.
|
||||
T *get() const noexcept { return ptr_; }
|
||||
|
||||
/// True if the handle is non-null (construction succeeded).
|
||||
explicit operator bool() const noexcept { return ptr_ != nullptr; }
|
||||
|
||||
private:
|
||||
T *ptr_;
|
||||
};
|
||||
|
||||
} // namespace wickra
|
||||
|
||||
#endif // WICKRA_HPP
|
||||
+68740
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
|
||||
# NuGet packaging output
|
||||
*.nupkg
|
||||
*.snupkg
|
||||
|
||||
# Native libraries staged for packaging (produced by the release pipeline from
|
||||
# the wickra-c-<triple>.tar.gz assets; never committed to source).
|
||||
Wickra/runtimes/
|
||||
@@ -0,0 +1,97 @@
|
||||
# Wickra — C#
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://www.nuget.org/packages/Wickra)
|
||||
[](https://github.com/wickra-lib/wickra#license)
|
||||
|
||||
**Streaming-first technical indicators for C#. `dotnet add package Wickra` —
|
||||
prebuilt native library, no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js and WASM, plus a C ABI for C, C++, C#, Go, Java, R and any
|
||||
other C-capable language. Every indicator is an O(1)
|
||||
streaming state machine, so live trading bots and historical backtests share
|
||||
the exact same implementation. This package is the C# binding; it consumes the
|
||||
C ABI hub through `[LibraryImport]` P/Invoke and exposes all 514 streaming-first
|
||||
indicators as idiomatic `IDisposable` classes.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
dotnet add package Wickra
|
||||
```
|
||||
|
||||
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
|
||||
arm64) under `runtimes/<rid>/native/`, selected automatically. There is nothing
|
||||
to compile. Targets .NET 8 and later.
|
||||
|
||||
## Quick start
|
||||
|
||||
```csharp
|
||||
using Wickra;
|
||||
|
||||
// Batch: run an indicator over a whole series (NaN at warmup positions).
|
||||
var prices = Enumerable.Range(0, 1000).Select(i => 100.0 + i * 0.1).ToArray();
|
||||
using var sma = new Sma(20);
|
||||
double[] values = sma.Batch(prices);
|
||||
|
||||
// Streaming: the same indicator, fed tick by tick in O(1).
|
||||
using var rsi = new Rsi(14);
|
||||
foreach (var price in liveFeed)
|
||||
{
|
||||
var value = rsi.Update(price); // NaN during warmup, no recomputation
|
||||
if (double.IsFinite(value) && value > 70)
|
||||
{
|
||||
Console.WriteLine("overbought");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`Batch(prices)` and feeding the same prices through `Update()` produce identical
|
||||
values — the equivalence is enforced by the test suite. Multi-output indicators
|
||||
(MACD, Bollinger, ADX, …) return a nullable `record struct`, `null` while warming up.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/` reports streaming and batch updates-per-second for `SMA`, `ATR`
|
||||
and `MACD`. It measures this binding's FFI overhead, not a cross-library ratio
|
||||
(the same Rust core runs under every binding) — see the repository
|
||||
[BENCHMARKS.md](https://github.com/wickra-lib/wickra/blob/main/BENCHMARKS.md) §3.
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
dotnet run -c Release --project benchmarks
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/csharp/`](https://github.com/wickra-lib/wickra/tree/main/examples/csharp)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WASM and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Security
|
||||
|
||||
Found a security issue? **Please don't open a public issue.** Report it privately
|
||||
via the affected repository's *Security* tab (*"Report a vulnerability"*) or email
|
||||
**support@wickra.org** with a subject line starting `[wickra security]`. Full
|
||||
policy: <https://github.com/wickra-lib/wickra/blob/main/SECURITY.md>.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,159 @@
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// One representative per FFI archetype, exercising every marshalling path the
|
||||
/// generator produces (scalar, candle, pairwise, multi-output, bars, profile,
|
||||
/// values-profile, array-input). Garbage marshalling surfaces as NaN, wild
|
||||
/// values, or crashes — so finite/sane assertions are the real check.
|
||||
/// </summary>
|
||||
public class ArchetypeTests
|
||||
{
|
||||
private static (double open, double high, double low, double close, double volume, long ts) Candle(int i)
|
||||
{
|
||||
var close = 100.0 + 10.0 * Math.Sin(i * 0.3);
|
||||
var open = 100.0 + 10.0 * Math.Sin((i - 1) * 0.3);
|
||||
var high = Math.Max(open, close) + 1.0;
|
||||
var low = Math.Min(open, close) - 1.0;
|
||||
return (open, high, low, close, 1_000.0, i * 60_000L);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Scalar_Ema_IsFiniteAfterWarmup()
|
||||
{
|
||||
using var ema = new Ema(3);
|
||||
double last = double.NaN;
|
||||
for (var i = 1; i <= 10; i++)
|
||||
{
|
||||
last = ema.Update(i);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
Assert.InRange(last, 1.0, 10.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Query_WarmupPeriodAndIsReady()
|
||||
{
|
||||
using var sma = new Sma(3);
|
||||
Assert.Equal(3, sma.WarmupPeriod());
|
||||
Assert.False(sma.IsReady());
|
||||
sma.Update(1.0);
|
||||
sma.Update(2.0);
|
||||
Assert.False(sma.IsReady());
|
||||
sma.Update(3.0);
|
||||
Assert.True(sma.IsReady());
|
||||
sma.Reset();
|
||||
Assert.False(sma.IsReady());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Candle_Atr_IsFinitePositive()
|
||||
{
|
||||
using var atr = new Atr(3);
|
||||
double last = double.NaN;
|
||||
for (var i = 0; i < 20; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
last = atr.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
Assert.True(last > 0.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pairwise_Beta_IsFinite()
|
||||
{
|
||||
using var beta = new Beta(5);
|
||||
double last = double.NaN;
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
var market = 100.0 + 10.0 * Math.Sin(i * 0.5);
|
||||
var asset = 50.0 + 6.0 * Math.Sin(i * 0.5 + 0.2);
|
||||
last = beta.Update(market, asset);
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(last));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Adx_ReturnsFiniteStruct()
|
||||
{
|
||||
using var adx = new Adx(5);
|
||||
AdxOutput? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
result = adx.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.True(double.IsFinite(result!.Value.Adx));
|
||||
Assert.True(double.IsFinite(result.Value.PlusDi));
|
||||
Assert.True(double.IsFinite(result.Value.MinusDi));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bars_DollarBars_EmitsBars()
|
||||
{
|
||||
using var bars = new DollarBars(5_000.0);
|
||||
var total = 0;
|
||||
for (var i = 0; i < 200; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
total += bars.Update(o, h, l, c, v, ts).Length;
|
||||
}
|
||||
|
||||
Assert.True(total > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Profile_VolumeProfile_ReturnsValues()
|
||||
{
|
||||
using var profile = new VolumeProfile(20, 8);
|
||||
VolumeProfileOutputScalars? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var (o, h, l, c, v, ts) = Candle(i);
|
||||
result = profile.Update(o, h, l, c, v, ts);
|
||||
}
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.NotNull(result!.Value.Values);
|
||||
Assert.True(result.Value.PriceLow <= result.Value.PriceHigh);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProfileValues_DayOfWeekProfile_NoCrash()
|
||||
{
|
||||
using var profile = new DayOfWeekProfile(0);
|
||||
double[]? result = null;
|
||||
for (var i = 0; i < 60; i++)
|
||||
{
|
||||
var close = 100.0 + 5.0 * Math.Sin(i * 0.2);
|
||||
// one day apart so the day-of-week buckets fill
|
||||
result = profile.Update(close, close + 1, close - 1, close, 1_000.0, i * 86_400_000L);
|
||||
}
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
Assert.All(result, v => Assert.True(double.IsFinite(v)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ArrayInput_DepthSlope_IsFinite()
|
||||
{
|
||||
using var slope = new DepthSlope();
|
||||
ReadOnlySpan<double> bidPrice = stackalloc double[] { 99.0, 98.0, 97.0 };
|
||||
ReadOnlySpan<double> bidSize = stackalloc double[] { 10.0, 20.0, 30.0 };
|
||||
ReadOnlySpan<double> askPrice = stackalloc double[] { 101.0, 102.0, 103.0 };
|
||||
ReadOnlySpan<double> askSize = stackalloc double[] { 12.0, 22.0, 32.0 };
|
||||
|
||||
var result = slope.Update(bidPrice, bidSize, askPrice, askSize);
|
||||
Assert.True(double.IsFinite(result));
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,216 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-fixture parity: replay the shared <c>testdata/golden</c> input series
|
||||
/// through the C# FFI and assert every value matches the Rust reference output.
|
||||
/// Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
/// wiring bug (swapped parameter, wrong multi-output field) is caught.
|
||||
/// Fixtures are generated by <c>cargo run -p wickra-examples --bin gen_golden</c>.
|
||||
/// </summary>
|
||||
public class GoldenTests
|
||||
{
|
||||
private const double Tol = 1e-6;
|
||||
|
||||
private static string GoldenDir([CallerFilePath] string file = "") =>
|
||||
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
|
||||
|
||||
private static List<string[]> ReadCsv(string name)
|
||||
{
|
||||
var path = Path.Combine(GoldenDir(), name + ".csv");
|
||||
return File.ReadAllLines(path)
|
||||
.Skip(1) // header
|
||||
.Where(l => l.Length > 0)
|
||||
.Select(l => l.Split(','))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static double[][] Input()
|
||||
{
|
||||
return ReadCsv("input")
|
||||
.Select(r => r.Select(c => double.Parse(c, CultureInfo.InvariantCulture)).ToArray())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double Cell(string s) =>
|
||||
s == "nan" ? double.NaN : double.Parse(s, CultureInfo.InvariantCulture);
|
||||
|
||||
private static void AssertClose(double got, double want, int row, string field)
|
||||
{
|
||||
if (double.IsNaN(want))
|
||||
{
|
||||
Assert.True(double.IsNaN(got), $"row {row} {field}: expected warmup/NaN, got {got}");
|
||||
return;
|
||||
}
|
||||
var tol = Tol * Math.Max(1.0, Math.Abs(want));
|
||||
Assert.True(Math.Abs(got - want) <= tol, $"row {row} {field}: got {got}, want {want}");
|
||||
}
|
||||
|
||||
// --- scalar (close-driven) ------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData("sma")]
|
||||
[InlineData("ema")]
|
||||
[InlineData("rsi")]
|
||||
public void Scalar_MatchesGolden(string name)
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv(name);
|
||||
using var ind = (IDisposable)(name switch
|
||||
{
|
||||
"sma" => new Sma(14),
|
||||
"ema" => new Ema(14),
|
||||
"rsi" => new Rsi(14),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(name)),
|
||||
});
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var close = input[i][3];
|
||||
double got = ind switch
|
||||
{
|
||||
Sma s => s.Update(close),
|
||||
Ema e => e.Update(close),
|
||||
Rsi r => r.Update(close),
|
||||
_ => double.NaN,
|
||||
};
|
||||
AssertClose(got, Cell(expected[i][0]), i, name);
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle, single output ------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Candle_Atr_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("atr");
|
||||
using var atr = new Atr(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AssertClose(atr.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "atr");
|
||||
}
|
||||
}
|
||||
|
||||
// --- pairwise -------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Pairwise_Beta_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("beta");
|
||||
using var beta = new Beta(20);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
// generator fed (close, open)
|
||||
AssertClose(beta.Update(input[i][3], input[i][0]), Cell(expected[i][0]), i, "beta");
|
||||
}
|
||||
}
|
||||
|
||||
// --- scalar multi-output: MACD -------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Macd_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("macd");
|
||||
using var macd = new MacdIndicator(12, 26, 9);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
MacdOutput? got = macd.Update(input[i][3]);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.Macd, Cell(e[0]), i, "macd.macd");
|
||||
AssertClose(got.Value.Signal, Cell(e[1]), i, "macd.signal");
|
||||
AssertClose(got.Value.Histogram, Cell(e[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle multi-output: ADX --------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Adx_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("adx");
|
||||
using var adx = new Adx(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AdxOutput? got = adx.Update(o, h, l, c, v, i);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.PlusDi, Cell(e[0]), i, "adx.plus_di");
|
||||
AssertClose(got.Value.MinusDi, Cell(e[1]), i, "adx.minus_di");
|
||||
AssertClose(got.Value.Adx, Cell(e[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
public class SmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void StreamingMatchesReference()
|
||||
{
|
||||
using var sma = new Sma(3);
|
||||
Assert.True(double.IsNaN(sma.Update(1)));
|
||||
Assert.True(double.IsNaN(sma.Update(2)));
|
||||
Assert.Equal(2.0, sma.Update(3), 9);
|
||||
Assert.Equal(3.0, sma.Update(4), 9);
|
||||
Assert.Equal(4.0, sma.Update(5), 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchMatchesStreaming()
|
||||
{
|
||||
using var sma = new Sma(3);
|
||||
var output = sma.Batch(new double[] { 1, 2, 3, 4, 5 });
|
||||
|
||||
Assert.True(double.IsNaN(output[0]));
|
||||
Assert.True(double.IsNaN(output[1]));
|
||||
Assert.Equal(2.0, output[2], 9);
|
||||
Assert.Equal(3.0, output[3], 9);
|
||||
Assert.Equal(4.0, output[4], 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResetClearsState()
|
||||
{
|
||||
using var sma = new Sma(3);
|
||||
sma.Update(1);
|
||||
sma.Update(2);
|
||||
sma.Update(3);
|
||||
sma.Reset();
|
||||
Assert.True(double.IsNaN(sma.Update(10)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroPeriodThrows()
|
||||
{
|
||||
// Zero is rejected by the native constructor (returns NULL) -> ArgumentException;
|
||||
// a negative period is caught earlier by the wrapper guard.
|
||||
Assert.Throws<ArgumentException>(() => new Sma(0));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new Sma(-1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
</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" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wickra\Wickra.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<RootNamespace>Wickra</RootNamespace>
|
||||
<AssemblyName>Wickra</AssemblyName>
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>Wickra</PackageId>
|
||||
<Version>0.9.6</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>
|
||||
<PackageProjectUrl>https://github.com/wickra-lib/wickra</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/wickra-lib/wickra</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageTags>technical-analysis;indicators;trading;finance;streaming;ffi;native</PackageTags>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- A managed package carrying per-RID native assets; not built per-RID itself. -->
|
||||
<IncludeBuildOutput>true</IncludeBuildOutput>
|
||||
<!-- NU5128: managed package carrying only per-RID native assets.
|
||||
CS1591: generated members are self-descriptive; hand-written API is documented. -->
|
||||
<NoWarn>$(NoWarn);NU5128;CS1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Supported native runtime identifiers. The release pipeline builds the C ABI
|
||||
per target triple and stages the libraries under
|
||||
Wickra/runtimes/<rid>/native/ before `dotnet pack`:
|
||||
win-x64 win-arm64 linux-x64 linux-arm64 osx-x64 osx-arm64
|
||||
-->
|
||||
<PropertyGroup>
|
||||
<WickraRuntimeIdentifiers>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</WickraRuntimeIdentifiers>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<!--
|
||||
Native libraries are packed under runtimes/<rid>/native/ by the release pipeline,
|
||||
which unpacks the wickra-c-<triple>.tar.gz assets into the matching RID folders.
|
||||
For local development and tests the natives are resolved from the cargo target dir
|
||||
via WickraNative's DllImportResolver.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<None Include="runtimes/**/native/*" Pack="true" PackagePath="runtimes" Condition="Exists('runtimes')" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace Wickra;
|
||||
|
||||
/// <summary>
|
||||
/// Owns an opaque native indicator handle and releases it via the indicator's
|
||||
/// <c>_free</c> function. One generic handle type backs every indicator; the
|
||||
/// correct free routine is captured at construction time.
|
||||
/// </summary>
|
||||
internal sealed class WickraHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
private readonly Action<nint> _free;
|
||||
|
||||
internal WickraHandle(nint handle, Action<nint> free)
|
||||
: base(ownsHandle: true)
|
||||
{
|
||||
_free = free;
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
_free(handle);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Wickra;
|
||||
|
||||
/// <summary>
|
||||
/// Native library resolution for the Wickra C ABI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When consumed as a NuGet package the native library ships under
|
||||
/// <c>runtimes/<rid>/native/</c> and the default runtime resolver finds it
|
||||
/// automatically. For local development (project reference against a cargo build)
|
||||
/// the resolver additionally walks up the directory tree to locate
|
||||
/// <c>target/release</c> or <c>target/debug</c>. Every candidate is validated to
|
||||
/// actually export the Wickra ABI before it is accepted, so an unrelated library
|
||||
/// of the same name cannot shadow the real one.
|
||||
/// </remarks>
|
||||
internal static class WickraNative
|
||||
{
|
||||
/// <summary>The library name passed to <c>[LibraryImport]</c>.</summary>
|
||||
internal const string LibraryName = "wickra";
|
||||
|
||||
// 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()
|
||||
{
|
||||
NativeLibrary.SetDllImportResolver(typeof(WickraNative).Assembly, Resolve);
|
||||
}
|
||||
|
||||
private static nint Resolve(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath)
|
||||
{
|
||||
if (libraryName != LibraryName)
|
||||
{
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
// 1. Default resolution (NuGet runtimes/ layout, app-local copies). Accept
|
||||
// only if it is genuinely the Wickra ABI; otherwise discard and fall through.
|
||||
if (NativeLibrary.TryLoad(libraryName, assembly, searchPath, out var handle))
|
||||
{
|
||||
if (Exports(handle))
|
||||
{
|
||||
return handle;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(handle);
|
||||
}
|
||||
|
||||
// 2. Development fallback: locate the cargo build output.
|
||||
var fileName = NativeFileName();
|
||||
var dir = AppContext.BaseDirectory;
|
||||
for (var i = 0; i < 16 && dir is not null; i++)
|
||||
{
|
||||
foreach (var profile in new[] { "release", "debug" })
|
||||
{
|
||||
var candidate = Path.Combine(dir, "target", profile, fileName);
|
||||
if (File.Exists(candidate) && NativeLibrary.TryLoad(candidate, out var devHandle))
|
||||
{
|
||||
if (Exports(devHandle))
|
||||
{
|
||||
return devHandle;
|
||||
}
|
||||
|
||||
NativeLibrary.Free(devHandle);
|
||||
}
|
||||
}
|
||||
|
||||
dir = Path.GetDirectoryName(dir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar));
|
||||
}
|
||||
|
||||
return nint.Zero;
|
||||
}
|
||||
|
||||
private static bool Exports(nint handle) => NativeLibrary.TryGetExport(handle, SentinelSymbol, out _);
|
||||
|
||||
private static string NativeFileName()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return "wickra.dll";
|
||||
}
|
||||
|
||||
return OperatingSystem.IsMacOS() ? "libwickra.dylib" : "libwickra.so";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Standalone throughput benchmark for the Wickra C# binding.
|
||||
See Program.cs for run instructions. Requires the C ABI library; the
|
||||
binding's dev DllImportResolver falls back to target/release. -->
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<AssemblyName>Wickra.Benchmarks</AssemblyName>
|
||||
<RootNamespace>Wickra.Benchmarks</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wickra\Wickra.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,101 @@
|
||||
// Throughput benchmark for the Wickra C# binding.
|
||||
//
|
||||
// Measures how many indicator updates per second the binding sustains, both
|
||||
// per-tick (streaming Update) and bulk (Batch), over a synthetic OHLCV series.
|
||||
// It is the C# counterpart of the Node throughput.js and the Rust criterion
|
||||
// benches: it benchmarks Wickra's own O(1) streaming engine across the
|
||||
// managed<->C-ABI boundary (there is no comparable streaming TA library on
|
||||
// NuGet to compare against), so the headline number is raw per-binding
|
||||
// throughput / FFI overhead, not a cross-library ratio.
|
||||
//
|
||||
// Three indicators are timed, chosen by FFI call-signature archetype rather
|
||||
// than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
||||
// (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
||||
// single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
||||
//
|
||||
// cargo build -p wickra-c --release
|
||||
// dotnet run -c Release --project bindings/csharp/benchmarks # 200k bars
|
||||
// dotnet run -c Release --project bindings/csharp/benchmarks -- --bars 1000000
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Wickra;
|
||||
|
||||
// Deterministic, locale-independent number formatting for the report.
|
||||
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
|
||||
|
||||
int bars = 200_000;
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (args[i] == "--bars" && int.TryParse(args[i + 1], out var n) && n >= 1000)
|
||||
{
|
||||
bars = n;
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
var open = new double[bars];
|
||||
var high = new double[bars];
|
||||
var low = new double[bars];
|
||||
var close = new double[bars];
|
||||
var volume = new double[bars];
|
||||
var timestamp = new long[bars];
|
||||
for (int i = 0; i < bars; i++)
|
||||
{
|
||||
double mid = 100 + Math.Sin(i * 0.001) * 20 + i * 1e-4;
|
||||
double c = mid + Math.Sin(i * 0.05) * 2;
|
||||
close[i] = c;
|
||||
open[i] = mid;
|
||||
high[i] = Math.Max(c, mid) + 1.5;
|
||||
low[i] = Math.Min(c, mid) - 1.5;
|
||||
volume[i] = 1000 + (i % 97) * 13;
|
||||
timestamp[i] = i;
|
||||
}
|
||||
|
||||
double Mups(double ns) => bars / (ns / 1e9) / 1e6;
|
||||
|
||||
// Median elapsed-ns over a few repetitions, after one warmup pass.
|
||||
double TimeNs(Action fn, int reps = 3)
|
||||
{
|
||||
fn(); // warmup (JIT + cache)
|
||||
var samples = new double[reps];
|
||||
for (int r = 0; r < reps; r++)
|
||||
{
|
||||
long t0 = Stopwatch.GetTimestamp();
|
||||
fn();
|
||||
samples[r] = (Stopwatch.GetTimestamp() - t0) * (1e9 / Stopwatch.Frequency);
|
||||
}
|
||||
Array.Sort(samples);
|
||||
return samples[reps / 2];
|
||||
}
|
||||
|
||||
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
||||
var indicators = new (string Name, Action Stream, Action? Batch)[]
|
||||
{
|
||||
("SMA(20)",
|
||||
() => { using var ind = new Sma(20); for (int i = 0; i < bars; i++) ind.Update(close[i]); },
|
||||
() => { using var ind = new Sma(20); ind.Batch(close); }),
|
||||
("ATR(14)",
|
||||
() => { using var ind = new Atr(14); for (int i = 0; i < bars; i++) ind.Update(open[i], high[i], low[i], close[i], volume[i], timestamp[i]); },
|
||||
() => { using var ind = new Atr(14); ind.Batch(open, high, low, close, volume, timestamp); }),
|
||||
("MACD(12,26,9)",
|
||||
() => { using var ind = new MacdIndicator(12, 26, 9); for (int i = 0; i < bars; i++) ind.Update(close[i]); },
|
||||
null), // multi-output: streaming only
|
||||
};
|
||||
|
||||
Console.WriteLine($"Wickra C# throughput - {bars:N0} bars (median of 3 runs)\n");
|
||||
Console.WriteLine($"{"Indicator",-22}{"streaming (Mupd/s)",20}{"batch (Mupd/s)",18}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
foreach (var (name, stream, batch) in indicators)
|
||||
{
|
||||
string streamMups = Mups(TimeNs(stream)).ToString("F1");
|
||||
string batchMups = batch is null ? "-" : Mups(TimeNs(batch)).ToString("F1");
|
||||
Console.WriteLine($"{name,-22}{streamMups,20}{batchMups,18}");
|
||||
}
|
||||
|
||||
Console.WriteLine(
|
||||
"\nMupd/s = million indicator updates per second. Streaming is the per-tick\n" +
|
||||
"Update path crossing the managed<->C-ABI boundary once per value; batch is\n" +
|
||||
"the bulk array path (one boundary crossing). Higher is better. Numbers are\n" +
|
||||
"machine-dependent - use them for relative comparison, not as a speed claim.");
|
||||
@@ -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")
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,132 @@
|
||||
# Wickra — Go
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://pkg.go.dev/github.com/wickra-lib/wickra/bindings/go)
|
||||
[](https://github.com/wickra-lib/wickra#license)
|
||||
|
||||
**Streaming-first technical indicators for Go, over the Wickra C ABI hub via cgo.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js and WASM, plus a C ABI for C, C++, C#, Go, Java, R and
|
||||
any other C-capable language. Every indicator is an O(1) streaming state machine,
|
||||
so live trading bots and historical backtests share the exact same
|
||||
implementation. This package is the Go binding; it consumes the C ABI hub through
|
||||
cgo and exposes all 514 streaming-first indicators as idiomatic types.
|
||||
|
||||
## Install
|
||||
|
||||
Use the published **`wickra-go`** module, which bundles the prebuilt C ABI
|
||||
library for every platform, so `go get` + `go build` works with no extra steps
|
||||
(a C compiler is still required, as the binding uses cgo):
|
||||
|
||||
```bash
|
||||
go get github.com/wickra-lib/wickra-go
|
||||
```
|
||||
|
||||
```go
|
||||
import wickra "github.com/wickra-lib/wickra-go"
|
||||
```
|
||||
|
||||
`wickra-go` is generated from this directory by the release pipeline: it mirrors
|
||||
the Go sources, the vendored C ABI header (`include/wickra.h`) and the prebuilt
|
||||
libraries under `lib/<goos>_<goarch>/`. On Linux/macOS the library path is baked
|
||||
in via rpath; on Windows the DLL must be discoverable at run time (next to the
|
||||
executable or on `PATH`).
|
||||
|
||||
### Building from this repository (contributors)
|
||||
|
||||
This `bindings/go` directory is the development source. To build it directly,
|
||||
compile the C ABI and stage the library into the per-platform directory cgo
|
||||
links against:
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
mkdir -p bindings/go/lib/linux_amd64 # match your GOOS_GOARCH
|
||||
cp target/release/libwickra.so bindings/go/lib/linux_amd64/ # Linux
|
||||
cp target/release/libwickra.dylib bindings/go/lib/darwin_arm64/ # macOS (arm64)
|
||||
cp target/release/wickra.dll bindings/go/lib/windows_amd64/ # Windows
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Batch: run an indicator over a whole series (NaN at warmup positions).
|
||||
prices := make([]float64, 1000)
|
||||
for i := range prices {
|
||||
prices[i] = 100.0 + float64(i)*0.1
|
||||
}
|
||||
sma, _ := wickra.NewSma(20)
|
||||
defer sma.Close()
|
||||
values := sma.Batch(prices)
|
||||
|
||||
// Streaming: the same indicator, fed tick by tick in O(1).
|
||||
rsi, _ := wickra.NewRsi(14)
|
||||
defer rsi.Close()
|
||||
for _, price := range prices {
|
||||
value := rsi.Update(price) // NaN during warmup, no recomputation
|
||||
if value > 70 {
|
||||
fmt.Println("overbought")
|
||||
}
|
||||
}
|
||||
_ = values
|
||||
}
|
||||
```
|
||||
|
||||
`Batch(prices)` and feeding the same prices through `Update()` produce identical
|
||||
values — the equivalence is enforced by the test suite. Multi-output indicators
|
||||
(MACD, Bollinger, ADX, …) return `(Output, bool)`, with `false` while warming up.
|
||||
Every indicator owns a native handle freed by `Close()`; a finalizer is wired as
|
||||
a backstop, but call `Close()` (e.g. with `defer`) to release memory promptly.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/throughput.go` reports streaming and batch updates-per-second for
|
||||
`SMA`, `ATR` and `MACD`. It measures this binding's FFI overhead, not a
|
||||
cross-library ratio (the same Rust core runs under every binding) — see the
|
||||
repository [BENCHMARKS.md](https://github.com/wickra-lib/wickra/blob/main/BENCHMARKS.md) §3.
|
||||
|
||||
```bash
|
||||
cd benchmarks && go run .
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in the
|
||||
main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/go/`](https://github.com/wickra-lib/wickra/tree/main/examples/go)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WASM and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Security
|
||||
|
||||
Found a security issue? **Please don't open a public issue.** Report it privately
|
||||
via the affected repository's *Security* tab (*"Report a vulnerability"*) or email
|
||||
**support@wickra.org** with a subject line starting `[wickra security]`. Full
|
||||
policy: <https://github.com/wickra-lib/wickra/blob/main/SECURITY.md>.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes are
|
||||
deterministic transforms of the input data — they are not financial advice and
|
||||
do not predict the market. Any use in a live trading context is at your own risk.
|
||||
The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,145 @@
|
||||
// Throughput benchmark for the Wickra Go bindings.
|
||||
//
|
||||
// Measures how many indicator updates per second the cgo binding sustains,
|
||||
// both per-tick (streaming Update) and bulk (Batch), over a synthetic OHLCV
|
||||
// series. It is the Go counterpart of the Node throughput.js and the Rust
|
||||
// criterion benches: it benchmarks Wickra's own O(1) streaming engine across
|
||||
// the Go<->C-ABI boundary (there is no comparable streaming TA library to
|
||||
// compare against), so the headline number is raw per-binding throughput /
|
||||
// FFI overhead, not a cross-library ratio.
|
||||
//
|
||||
// Three indicators are timed, chosen by FFI call-signature archetype rather
|
||||
// than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
||||
// (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
||||
// single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
||||
//
|
||||
// Provision the C ABI library first (see bindings/go/README.md), then run:
|
||||
//
|
||||
// cd bindings/go/benchmarks
|
||||
// go run . # 200k bars (default)
|
||||
// go run . -bars 1000000
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
wickra "github.com/wickra-lib/wickra/bindings/go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bars := flag.Int("bars", 200_000, "number of synthetic bars to feed")
|
||||
flag.Parse()
|
||||
n := *bars
|
||||
if n < 1000 {
|
||||
fmt.Println("-bars must be >= 1000")
|
||||
return
|
||||
}
|
||||
|
||||
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
open := make([]float64, n)
|
||||
high := make([]float64, n)
|
||||
low := make([]float64, n)
|
||||
closeP := make([]float64, n)
|
||||
volume := make([]float64, n)
|
||||
timestamp := make([]int64, n)
|
||||
for i := 0; i < n; i++ {
|
||||
mid := 100 + math.Sin(float64(i)*0.001)*20 + float64(i)*1e-4
|
||||
c := mid + math.Sin(float64(i)*0.05)*2
|
||||
closeP[i] = c
|
||||
open[i] = mid
|
||||
high[i] = math.Max(c, mid) + 1.5
|
||||
low[i] = math.Min(c, mid) - 1.5
|
||||
volume[i] = 1000 + float64(i%97)*13
|
||||
timestamp[i] = int64(i)
|
||||
}
|
||||
|
||||
mups := func(d time.Duration) float64 {
|
||||
return float64(n) / d.Seconds() / 1e6
|
||||
}
|
||||
|
||||
// Median elapsed over a few repetitions, after one warmup pass.
|
||||
timeFn := func(fn func()) time.Duration {
|
||||
fn() // warmup
|
||||
const reps = 3
|
||||
samples := make([]time.Duration, reps)
|
||||
for r := 0; r < reps; r++ {
|
||||
t0 := time.Now()
|
||||
fn()
|
||||
samples[r] = time.Since(t0)
|
||||
}
|
||||
sort.Slice(samples, func(a, b int) bool { return samples[a] < samples[b] })
|
||||
return samples[reps/2]
|
||||
}
|
||||
|
||||
type indicator struct {
|
||||
name string
|
||||
stream func()
|
||||
batch func() // nil -> streaming only
|
||||
}
|
||||
|
||||
indicators := []indicator{
|
||||
{
|
||||
name: "SMA(20)",
|
||||
stream: func() {
|
||||
ind, _ := wickra.NewSma(20)
|
||||
for i := 0; i < n; i++ {
|
||||
ind.Update(closeP[i])
|
||||
}
|
||||
ind.Close()
|
||||
},
|
||||
batch: func() {
|
||||
ind, _ := wickra.NewSma(20)
|
||||
ind.Batch(closeP)
|
||||
ind.Close()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ATR(14)",
|
||||
stream: func() {
|
||||
ind, _ := wickra.NewAtr(14)
|
||||
for i := 0; i < n; i++ {
|
||||
ind.Update(open[i], high[i], low[i], closeP[i], volume[i], timestamp[i])
|
||||
}
|
||||
ind.Close()
|
||||
},
|
||||
batch: func() {
|
||||
ind, _ := wickra.NewAtr(14)
|
||||
ind.Batch(open, high, low, closeP, volume, timestamp)
|
||||
ind.Close()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "MACD(12,26,9)",
|
||||
stream: func() {
|
||||
ind, _ := wickra.NewMacdIndicator(12, 26, 9)
|
||||
for i := 0; i < n; i++ {
|
||||
ind.Update(closeP[i])
|
||||
}
|
||||
ind.Close()
|
||||
},
|
||||
batch: nil, // multi-output: streaming only
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Printf("Wickra Go throughput - %d bars (median of 3 runs)\n\n", n)
|
||||
fmt.Printf("%-22s%20s%18s\n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)")
|
||||
fmt.Println("------------------------------------------------------------")
|
||||
|
||||
for _, ind := range indicators {
|
||||
streamMups := fmt.Sprintf("%.1f", mups(timeFn(ind.stream)))
|
||||
batchMups := "-"
|
||||
if ind.batch != nil {
|
||||
batchMups = fmt.Sprintf("%.1f", mups(timeFn(ind.batch)))
|
||||
}
|
||||
fmt.Printf("%-22s%20s%18s\n", ind.name, streamMups, batchMups)
|
||||
}
|
||||
|
||||
fmt.Print("\nMupd/s = million indicator updates per second. Streaming is the per-tick\n",
|
||||
"Update path crossing the Go<->C-ABI boundary once per value; batch is the\n",
|
||||
"bulk slice path (one boundary crossing). Higher is better. Numbers are\n",
|
||||
"machine-dependent - use them for relative comparison, not as a speed claim.\n")
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -0,0 +1,3 @@
|
||||
module github.com/wickra-lib/wickra/bindings/go
|
||||
|
||||
go 1.23
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,250 @@
|
||||
package wickra
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Golden-fixture parity: replay the shared testdata/golden input series through
|
||||
// the Go FFI and assert every value matches the Rust reference output. Where the
|
||||
// archetype test only checks finiteness, this pins exact values, catching wiring
|
||||
// bugs (swapped params, wrong multi-output field). Fixtures are generated by
|
||||
// `cargo run -p wickra-examples --bin gen_golden`.
|
||||
|
||||
const goldenTol = 1e-6
|
||||
|
||||
func readGolden(t *testing.T, name string) [][]string {
|
||||
t.Helper()
|
||||
f, err := os.Open("../../testdata/golden/" + name + ".csv")
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
var rows [][]string
|
||||
sc := bufio.NewScanner(f)
|
||||
first := true
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if first {
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, strings.Split(line, ","))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func goldenCell(s string) float64 {
|
||||
if s == "nan" {
|
||||
return math.NaN()
|
||||
}
|
||||
v, _ := strconv.ParseFloat(s, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func goldenInput(t *testing.T) [][]float64 {
|
||||
rows := readGolden(t, "input")
|
||||
out := make([][]float64, len(rows))
|
||||
for i, r := range rows {
|
||||
vals := make([]float64, len(r))
|
||||
for j, c := range r {
|
||||
vals[j] = goldenCell(c)
|
||||
}
|
||||
out[i] = vals
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertGoldenClose(t *testing.T, got, want float64, row int, field string) {
|
||||
t.Helper()
|
||||
if math.IsNaN(want) {
|
||||
if !math.IsNaN(got) {
|
||||
t.Errorf("row %d %s: expected warmup/NaN, got %v", row, field, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
tol := goldenTol * math.Max(1.0, math.Abs(want))
|
||||
if math.Abs(got-want) > tol {
|
||||
t.Errorf("row %d %s: got %v want %v", row, field, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenScalar(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
sma, err := NewSma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sma.Close()
|
||||
ema, err := NewEma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ema.Close()
|
||||
rsi, err := NewRsi(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsi.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
upd func(close float64) float64
|
||||
}{
|
||||
{"sma", sma.Update},
|
||||
{"ema", ema.Update},
|
||||
{"rsi", rsi.Update},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
exp := readGolden(t, tc.name)
|
||||
for i := range input {
|
||||
assertGoldenClose(t, tc.upd(input[i][3]), goldenCell(exp[i][0]), i, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAtr(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "atr")
|
||||
atr, err := NewAtr(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer atr.Close()
|
||||
for i := range input {
|
||||
got := atr.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "atr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenBeta(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "beta")
|
||||
beta, err := NewBeta(20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer beta.Close()
|
||||
for i := range input {
|
||||
// generator fed (close, open)
|
||||
assertGoldenClose(t, beta.Update(input[i][3], input[i][0]), goldenCell(exp[i][0]), i, "beta")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenMacd(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "macd")
|
||||
macd, err := NewMacdIndicator(12, 26, 9)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer macd.Close()
|
||||
for i := range input {
|
||||
out, ok := macd.Update(input[i][3])
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d macd: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d macd: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.Macd, goldenCell(exp[i][0]), i, "macd.macd")
|
||||
assertGoldenClose(t, out.Signal, goldenCell(exp[i][1]), i, "macd.signal")
|
||||
assertGoldenClose(t, out.Histogram, goldenCell(exp[i][2]), i, "macd.histogram")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAdx(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "adx")
|
||||
adx, err := NewAdx(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adx.Close()
|
||||
for i := range input {
|
||||
out, ok := adx.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d adx: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d adx: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.PlusDi, goldenCell(exp[i][0]), i, "adx.plus_di")
|
||||
assertGoldenClose(t, out.MinusDi, goldenCell(exp[i][1]), i, "adx.minus_di")
|
||||
assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx")
|
||||
}
|
||||
}
|
||||
|
||||
// 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
@@ -0,0 +1,7 @@
|
||||
# Prebuilt Wickra C ABI libraries are provisioned locally / in CI, not committed.
|
||||
*.so
|
||||
*.dylib
|
||||
*.dll
|
||||
*.a
|
||||
*.lib
|
||||
*.exp
|
||||
@@ -0,0 +1,34 @@
|
||||
// Package wickra provides idiomatic Go bindings for the Wickra
|
||||
// technical-analysis library over its C ABI hub.
|
||||
//
|
||||
// Each indicator is an opaque-handle type with a New<Indicator> constructor and
|
||||
// Update/Batch/Reset/Close methods. Handles are freed by Close and, as a
|
||||
// backstop, by a finalizer; call Close explicitly to release native memory
|
||||
// promptly. The binding links against the prebuilt Wickra C ABI library, staged
|
||||
// per platform under ./lib/<goos>_<goarch>/, with the C ABI header vendored
|
||||
// under ./include. For distribution the libraries are committed alongside the
|
||||
// source in the wickra-go module, so `go get` + `go build` works with no extra
|
||||
// steps — see the package README.
|
||||
package wickra
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -I${SRCDIR}/include
|
||||
#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/lib/linux_amd64 -lwickra -Wl,-rpath,${SRCDIR}/lib/linux_amd64
|
||||
#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/lib/linux_arm64 -lwickra -Wl,-rpath,${SRCDIR}/lib/linux_arm64
|
||||
#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/lib/darwin_amd64 -lwickra -Wl,-rpath,${SRCDIR}/lib/darwin_amd64
|
||||
#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/lib/darwin_arm64 -lwickra -Wl,-rpath,${SRCDIR}/lib/darwin_arm64
|
||||
#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/lib/windows_amd64 -l:wickra.dll
|
||||
#cgo windows,arm64 LDFLAGS: -L${SRCDIR}/lib/windows_arm64 -l:wickra.dll
|
||||
#include "wickra.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
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")
|
||||
@@ -0,0 +1,179 @@
|
||||
package wickra
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// One indicator per FFI archetype, exercising the full New/Update/Batch/Reset/
|
||||
// Close surface against the real native library.
|
||||
|
||||
func TestScalarKnownValue(t *testing.T) {
|
||||
s, err := NewSma(3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSma: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
var last float64
|
||||
for _, v := range []float64{1, 2, 3, 4, 5} {
|
||||
last = s.Update(v)
|
||||
}
|
||||
if math.Abs(last-4.0) > 1e-9 {
|
||||
t.Fatalf("sma(3) last = %v, want 4.0", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupPeriodAndIsReady(t *testing.T) {
|
||||
s, err := NewSma(3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSma: %v", err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
if got := s.WarmupPeriod(); got != 3 {
|
||||
t.Fatalf("sma(3) WarmupPeriod = %d, want 3", got)
|
||||
}
|
||||
if s.IsReady() {
|
||||
t.Fatal("sma is ready before any update")
|
||||
}
|
||||
s.Update(1)
|
||||
s.Update(2)
|
||||
if s.IsReady() {
|
||||
t.Fatal("sma is ready mid-warmup")
|
||||
}
|
||||
s.Update(3)
|
||||
if !s.IsReady() {
|
||||
t.Fatal("sma is not ready after the warmup period")
|
||||
}
|
||||
s.Reset()
|
||||
if s.IsReady() {
|
||||
t.Fatal("sma is ready after reset")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScalarBatchMatchesStreaming(t *testing.T) {
|
||||
input := []float64{1, 2, 3, 4, 5, 6, 7, 8}
|
||||
|
||||
stream, _ := NewSma(3)
|
||||
defer stream.Close()
|
||||
want := make([]float64, len(input))
|
||||
for i, v := range input {
|
||||
want[i] = stream.Update(v)
|
||||
}
|
||||
|
||||
batchInd, _ := NewSma(3)
|
||||
defer batchInd.Close()
|
||||
got := batchInd.Batch(input)
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("batch len = %d, want %d", len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if math.IsNaN(want[i]) && math.IsNaN(got[i]) {
|
||||
continue
|
||||
}
|
||||
if math.Abs(got[i]-want[i]) > 1e-9 {
|
||||
t.Fatalf("batch[%d] = %v, streaming = %v", i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiOutput(t *testing.T) {
|
||||
m, err := NewMacdIndicator(3, 6, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMacdIndicator: %v", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
var ok bool
|
||||
var out MacdOutput
|
||||
for i := 0; i < 30; i++ {
|
||||
out, ok = m.Update(100 + float64(i))
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("macd never produced a value after warmup")
|
||||
}
|
||||
if math.IsNaN(out.Macd) {
|
||||
t.Fatal("macd value is NaN after warmup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBars(t *testing.T) {
|
||||
rb, err := NewRangeBars(2.0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRangeBars: %v", err)
|
||||
}
|
||||
defer rb.Close()
|
||||
|
||||
total := 0
|
||||
for _, p := range []float64{100, 101, 103, 104, 99, 96, 102, 108, 95, 110} {
|
||||
bars := rb.Update(p, p, p, p, 1, 0)
|
||||
total += len(bars)
|
||||
}
|
||||
if total == 0 {
|
||||
t.Fatal("range bars produced no bars over a 15-point move")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfile(t *testing.T) {
|
||||
vp, err := NewVolumeProfile(10, 24)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVolumeProfile: %v", err)
|
||||
}
|
||||
defer vp.Close()
|
||||
|
||||
var ok bool
|
||||
var snap VolumeProfileOutputScalars
|
||||
for i := 0; i < 50; i++ {
|
||||
price := 100 + 5*math.Sin(float64(i)*0.3)
|
||||
snap, ok = vp.Update(price, price+1, price-1, price, 1000, int64(i))
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("volume profile never produced a snapshot")
|
||||
}
|
||||
if len(snap.Values) == 0 {
|
||||
t.Fatal("volume profile returned an empty values buffer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestArrayInput(t *testing.T) {
|
||||
ob, err := NewOrderBookImbalanceFull()
|
||||
if err != nil {
|
||||
t.Fatalf("NewOrderBookImbalanceFull: %v", err)
|
||||
}
|
||||
defer ob.Close()
|
||||
|
||||
bidPrice := []float64{99.9, 99.8, 99.7}
|
||||
bidSize := []float64{5, 3, 2}
|
||||
askPrice := []float64{100.1, 100.2, 100.3}
|
||||
askSize := []float64{1, 1, 1}
|
||||
v := ob.Update(bidPrice, bidSize, askPrice, askSize)
|
||||
if math.IsNaN(v) {
|
||||
t.Fatal("order-book imbalance is NaN on a populated book")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResetReturnsToWarmup(t *testing.T) {
|
||||
s, _ := NewSma(3)
|
||||
defer s.Close()
|
||||
for _, v := range []float64{1, 2, 3} {
|
||||
s.Update(v)
|
||||
}
|
||||
s.Reset()
|
||||
if got := s.Update(10); !math.IsNaN(got) {
|
||||
t.Fatalf("after reset first update = %v, want NaN (warmup)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidParams(t *testing.T) {
|
||||
if _, err := NewSma(0); err == nil {
|
||||
t.Fatal("NewSma(0) should return ErrInvalidParams")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseIsIdempotent(t *testing.T) {
|
||||
s, _ := NewSma(3)
|
||||
s.Close()
|
||||
s.Close() // must not panic or double-free
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
# Maven build output
|
||||
target/
|
||||
|
||||
# Native libraries staged for packaging (produced by the release pipeline from
|
||||
# the wickra-c-<triple>.tar.gz assets; never committed to source).
|
||||
src/main/resources/native/
|
||||
@@ -0,0 +1,122 @@
|
||||
# Wickra — Java
|
||||
|
||||
[](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/wickra-lib/wickra)
|
||||
[](https://central.sonatype.com/artifact/org.wickra/wickra)
|
||||
[](https://github.com/wickra-lib/wickra#license)
|
||||
|
||||
**Streaming-first technical indicators for the JVM, on the Java Foreign Function
|
||||
& Memory API — prebuilt native library, no JNI, no system dependencies.**
|
||||
|
||||
Wickra is a multi-language technical-analysis library with a Rust core and
|
||||
bindings for Python, Node.js and WASM, plus a C ABI for C, C++, C#, Go, Java, R
|
||||
and any other C-capable language. Every indicator is an O(1) streaming state
|
||||
machine, so live trading bots and historical backtests share the exact same
|
||||
implementation. This package is the Java binding; it consumes the C ABI hub
|
||||
through the Panama FFM API (`java.lang.foreign`) and exposes all 514
|
||||
streaming-first indicators as idiomatic `AutoCloseable` classes.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Java 22 or later** (the FFM API is final since Java 22; no preview flag).
|
||||
- The FFM API is *restricted*: pass `--enable-native-access=ALL-UNNAMED` when you
|
||||
run your application to silence the native-access warning.
|
||||
|
||||
## Install
|
||||
|
||||
Maven:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.9.6</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Gradle:
|
||||
|
||||
```kotlin
|
||||
implementation("org.wickra:wickra:0.9.6")
|
||||
```
|
||||
|
||||
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
|
||||
arm64) inside the jar and is extracted automatically on first use. There is
|
||||
nothing to compile.
|
||||
|
||||
## Quick start
|
||||
|
||||
```java
|
||||
import org.wickra.Ema;
|
||||
import org.wickra.Rsi;
|
||||
|
||||
// Batch: run an indicator over a whole series (NaN at warmup positions).
|
||||
double[] prices = new double[1000];
|
||||
for (int i = 0; i < prices.length; i++) {
|
||||
prices[i] = 100.0 + i * 0.1;
|
||||
}
|
||||
try (Ema ema = new Ema(20)) {
|
||||
double[] values = ema.batch(prices);
|
||||
}
|
||||
|
||||
// Streaming: the same indicator, fed tick by tick in O(1).
|
||||
try (Rsi rsi = new Rsi(14)) {
|
||||
for (double price : liveFeed) {
|
||||
double value = rsi.update(price); // NaN during warmup, no recomputation
|
||||
if (Double.isFinite(value) && value > 70) {
|
||||
System.out.println("overbought");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`batch(prices)` and feeding the same prices through `update()` produce identical
|
||||
values — the equivalence is enforced by the test suite. Multi-output indicators
|
||||
(MACD, Bollinger, ADX, …) return a `record`, `null` while warming up. Each
|
||||
indicator owns a native handle freed by a `Cleaner`; `close()` releases it
|
||||
eagerly (use try-with-resources).
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/` reports streaming and batch updates-per-second for `SMA`, `ATR`
|
||||
and `MACD`. It measures this binding's FFI overhead, not a cross-library ratio
|
||||
(the same Rust core runs under every binding) — see the repository
|
||||
[BENCHMARKS.md](https://github.com/wickra-lib/wickra/blob/main/BENCHMARKS.md) §3.
|
||||
|
||||
```bash
|
||||
cargo build -p wickra-c --release
|
||||
mvn -q install -DskipTests
|
||||
mvn -q -f benchmarks exec:exec -Dexec.mainClass=org.wickra.benchmarks.Throughput
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
the main repository and documentation site:
|
||||
|
||||
- **Repository & full indicator list:** <https://github.com/wickra-lib/wickra>
|
||||
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
|
||||
- **Runnable examples:** [`examples/java/`](https://github.com/wickra-lib/wickra/tree/main/examples/java)
|
||||
|
||||
Wickra ships native bindings for Python, Node.js, WASM and Rust, plus a
|
||||
C ABI hub that any C-capable language (C, C++, C#, Go, Java, R) links against —
|
||||
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
|
||||
|
||||
## Security
|
||||
|
||||
Found a security issue? **Please don't open a public issue.** Report it privately
|
||||
via the affected repository's *Security* tab (*"Report a vulnerability"*) or email
|
||||
**support@wickra.org** with a subject line starting `[wickra security]`. Full
|
||||
policy: <https://github.com/wickra-lib/wickra/blob/main/SECURITY.md>.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Wickra is an indicator toolkit, not a trading system. The values it computes
|
||||
are deterministic transforms of the input data — they are not financial advice
|
||||
and do not predict the market. Any use in a live trading context is at your own
|
||||
risk. The library is provided **as is**, without warranty of any kind.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of [Apache-2.0](https://github.com/wickra-lib/wickra/blob/main/LICENSE-APACHE)
|
||||
or [MIT](https://github.com/wickra-lib/wickra/blob/main/LICENSE-MIT) at your option.
|
||||
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.wickra.benchmarks</groupId>
|
||||
<artifactId>wickra-benchmarks</artifactId>
|
||||
<version>0.8.2</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Wickra Java benchmarks</name>
|
||||
<description>Throughput benchmark for the Wickra Java binding.</description>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>22</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.8.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.15.0</version>
|
||||
</plugin>
|
||||
|
||||
<!--
|
||||
Run the benchmark in a forked JVM with native access granted:
|
||||
mvn exec:exec -Dexec.mainClass=org.wickra.benchmarks.Throughput
|
||||
Requires the C ABI library and the installed binding; see Throughput.java.
|
||||
-->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.6.3</version>
|
||||
<configuration>
|
||||
<executable>${java.home}/bin/java</executable>
|
||||
<arguments>
|
||||
<argument>--enable-native-access=ALL-UNNAMED</argument>
|
||||
<argument>-classpath</argument>
|
||||
<classpath/>
|
||||
<argument>${exec.mainClass}</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.wickra.benchmarks;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import org.wickra.Atr;
|
||||
import org.wickra.MacdIndicator;
|
||||
import org.wickra.Sma;
|
||||
|
||||
/**
|
||||
* Throughput benchmark for the Wickra Java binding.
|
||||
*
|
||||
* <p>Measures how many indicator updates per second the binding sustains, both
|
||||
* per-tick (streaming {@code update}) and bulk ({@code batch}), over a synthetic
|
||||
* OHLCV series. It is the Java counterpart of the Node {@code throughput.js} and
|
||||
* the Rust criterion benches: it benchmarks Wickra's own O(1) streaming engine
|
||||
* across the Java FFM <-> C-ABI boundary (there is no comparable streaming
|
||||
* TA library on Maven Central to compare against), so the headline number is raw
|
||||
* per-binding throughput / FFI overhead, not a cross-library ratio.
|
||||
*
|
||||
* <p>Three indicators are timed, chosen by FFI call-signature archetype rather
|
||||
* than algorithm: SMA (1-in -> 1-out), ATR (multi-in -> 1-out), and MACD
|
||||
* (1-in -> multi-out). Streaming is timed for all three; batch only for the
|
||||
* single-output SMA and ATR (multi-output batch is not exposed uniformly).
|
||||
*
|
||||
* <p>Install the binding and build the C ABI library first, then run from the
|
||||
* repo root:
|
||||
*
|
||||
* <pre>
|
||||
* cargo build -p wickra-c --release
|
||||
* mvn -q -f bindings/java install -DskipTests
|
||||
* mvn -q -f bindings/java/benchmarks exec:exec -Dexec.mainClass=org.wickra.benchmarks.Throughput
|
||||
* </pre>
|
||||
*/
|
||||
public final class Throughput {
|
||||
private Throughput() {}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int bars = 200_000;
|
||||
for (int i = 0; i < args.length - 1; i++) {
|
||||
if (args[i].equals("--bars")) {
|
||||
try {
|
||||
int n = Integer.parseInt(args[i + 1]);
|
||||
if (n >= 1000) {
|
||||
bars = n;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {
|
||||
// keep default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
double[] open = new double[bars];
|
||||
double[] high = new double[bars];
|
||||
double[] low = new double[bars];
|
||||
double[] close = new double[bars];
|
||||
double[] volume = new double[bars];
|
||||
double[] timestamp = new double[bars];
|
||||
for (int i = 0; i < bars; i++) {
|
||||
double mid = 100 + Math.sin(i * 0.001) * 20 + i * 1e-4;
|
||||
double c = mid + Math.sin(i * 0.05) * 2;
|
||||
close[i] = c;
|
||||
open[i] = mid;
|
||||
high[i] = Math.max(c, mid) + 1.5;
|
||||
low[i] = Math.min(c, mid) - 1.5;
|
||||
volume[i] = 1000 + (i % 97) * 13;
|
||||
timestamp[i] = i;
|
||||
}
|
||||
|
||||
final int n = bars;
|
||||
|
||||
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
||||
Indicator[] indicators = {
|
||||
new Indicator("SMA(20)",
|
||||
() -> {
|
||||
try (Sma ind = new Sma(20)) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
ind.update(close[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
try (Sma ind = new Sma(20)) {
|
||||
ind.batch(close);
|
||||
}
|
||||
}),
|
||||
new Indicator("ATR(14)",
|
||||
() -> {
|
||||
try (Atr ind = new Atr(14)) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
ind.update(open[i], high[i], low[i], close[i], volume[i], (long) timestamp[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
try (Atr ind = new Atr(14)) {
|
||||
ind.batch(open, high, low, close, volume, timestamp);
|
||||
}
|
||||
}),
|
||||
new Indicator("MACD(12,26,9)",
|
||||
() -> {
|
||||
try (MacdIndicator ind = new MacdIndicator(12, 26, 9)) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
ind.update(close[i]);
|
||||
}
|
||||
}
|
||||
},
|
||||
null), // multi-output: streaming only
|
||||
};
|
||||
|
||||
System.out.printf(Locale.ROOT, "Wickra Java throughput - %,d bars (median of 3 runs)%n%n", bars);
|
||||
System.out.printf(Locale.ROOT, "%-22s%20s%18s%n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)");
|
||||
System.out.println("------------------------------------------------------------");
|
||||
|
||||
for (Indicator ind : indicators) {
|
||||
String streamMups = String.format(Locale.ROOT, "%.1f", mups(bars, timeNs(ind.stream)));
|
||||
String batchMups = ind.batch == null
|
||||
? "-"
|
||||
: String.format(Locale.ROOT, "%.1f", mups(bars, timeNs(ind.batch)));
|
||||
System.out.printf(Locale.ROOT, "%-22s%20s%18s%n", ind.name, streamMups, batchMups);
|
||||
}
|
||||
|
||||
System.out.println(
|
||||
"\nMupd/s = million indicator updates per second. Streaming is the per-tick\n"
|
||||
+ "update path crossing the Java FFM<->C-ABI boundary once per value; batch is\n"
|
||||
+ "the bulk array path (one boundary crossing). Higher is better. Numbers are\n"
|
||||
+ "machine-dependent - use them for relative comparison, not as a speed claim.");
|
||||
}
|
||||
|
||||
private static double mups(int bars, double ns) {
|
||||
return bars / (ns / 1e9) / 1e6;
|
||||
}
|
||||
|
||||
// Median elapsed-ns over a few repetitions, after one warmup pass.
|
||||
private static double timeNs(Runnable fn) {
|
||||
fn.run(); // warmup (JIT + cache)
|
||||
final int reps = 3;
|
||||
double[] samples = new double[reps];
|
||||
for (int r = 0; r < reps; r++) {
|
||||
long t0 = System.nanoTime();
|
||||
fn.run();
|
||||
samples[r] = System.nanoTime() - t0;
|
||||
}
|
||||
Arrays.sort(samples);
|
||||
return samples[reps / 2];
|
||||
}
|
||||
|
||||
private record Indicator(String name, Runnable stream, Runnable batch) {}
|
||||
}
|
||||
@@ -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_<Canonical>.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")
|
||||
@@ -0,0 +1,12 @@
|
||||
# OSV-Scanner suppression for this Maven manifest. OSV-Scanner looks for an
|
||||
# osv-scanner.toml next to each manifest it scans, and the repo-root config does
|
||||
# not cover Maven sub-directory scans — so the jackson finding is suppressed
|
||||
# here as well. See the root osv-scanner.toml and the SECURITY.md VEX section.
|
||||
#
|
||||
# tools.jackson.core:jackson-core 3.x is NOT a dependency of this project. Full
|
||||
# Maven resolution (the publishing plugin tree and the project dependency tree)
|
||||
# resolves only jackson 2.16.1 / 2.17.1; tools.jackson 3.x appears nowhere.
|
||||
# OSV-Scanner's own resolver flags it as a false positive.
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-72hv-8253-57qq"
|
||||
reason = "tools.jackson.core:jackson-core 3.x is not a dependency; only jackson 2.x resolves. Not affected."
|
||||
@@ -0,0 +1,171 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.9.6</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Wickra</name>
|
||||
<description>High-performance streaming technical-analysis indicators (514 indicators) for the
|
||||
JVM, backed by the native Rust core through the Wickra C ABI via the Java FFM API (Panama).</description>
|
||||
<url>https://github.com/wickra-lib/wickra</url>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
<url>https://opensource.org/licenses/MIT</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-2.0</name>
|
||||
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
|
||||
<distribution>repo</distribution>
|
||||
</license>
|
||||
</licenses>
|
||||
|
||||
<developers>
|
||||
<developer>
|
||||
<id>kingchenc</id>
|
||||
<name>kingchenc</name>
|
||||
<url>https://github.com/kingchenc</url>
|
||||
</developer>
|
||||
</developers>
|
||||
|
||||
<scm>
|
||||
<connection>scm:git:https://github.com/wickra-lib/wickra.git</connection>
|
||||
<developerConnection>scm:git:https://github.com/wickra-lib/wickra.git</developerConnection>
|
||||
<url>https://github.com/wickra-lib/wickra</url>
|
||||
</scm>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.release>22</maven.compiler.release>
|
||||
<junit.version>6.1.0</junit.version>
|
||||
<!-- The FFM API is restricted; grant native access to the unnamed module so
|
||||
tests and examples run without warnings. Consumers pass the same flag. -->
|
||||
<native.access.arg>--enable-native-access=ALL-UNNAMED</native.access.arg>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.15.0</version>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.6</version>
|
||||
<configuration>
|
||||
<argLine>${native.access.arg}</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<!-- Grant native access when the jar is run directly. -->
|
||||
<Enable-Native-Access>ALL-UNNAMED</Enable-Native-Access>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<!--
|
||||
Release profile (CI only, gated): attaches source + javadoc jars, GPG-signs
|
||||
every artifact, and publishes to Maven Central via the central-publishing
|
||||
plugin. The native libraries are unpacked from the wickra-c-<triple>.tar.gz
|
||||
release assets into src/main/resources/native/<os>-<arch>/ before `mvn deploy`,
|
||||
so the published jar carries every platform's library.
|
||||
-->
|
||||
<profile>
|
||||
<id>release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-source-plugin</artifactId>
|
||||
<version>3.4.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-sources</id>
|
||||
<goals><goal>jar-no-fork</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-javadoc-plugin</artifactId>
|
||||
<version>3.12.0</version>
|
||||
<configuration>
|
||||
<!-- Generated members are self-descriptive; do not fail on doclint. -->
|
||||
<doclint>none</doclint>
|
||||
<quiet>true</quiet>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-javadocs</id>
|
||||
<goals><goal>jar</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-gpg-plugin</artifactId>
|
||||
<version>3.2.8</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>sign-artifacts</id>
|
||||
<phase>verify</phase>
|
||||
<goals><goal>sign</goal></goals>
|
||||
<configuration>
|
||||
<!-- Non-interactive signing on CI: modern GPG needs loopback
|
||||
pinentry to take the passphrase from the env without a TTY. -->
|
||||
<gpgArguments>
|
||||
<arg>--pinentry-mode</arg>
|
||||
<arg>loopback</arg>
|
||||
</gpgArguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.10.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>central</publishingServerId>
|
||||
<autoPublish>true</autoPublish>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AbandonedBaby indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AbandonedBaby implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AbandonedBaby() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ABANDONED_BABY_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AbandonedBaby parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ABANDONED_BABY_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ABANDONED_BABY_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ABANDONED_BABY_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ABANDONED_BABY_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ABANDONED_BABY_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ABANDONED_BABY_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming Abcd indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class Abcd implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public Abcd() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ABCD_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid Abcd parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ABCD_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ABCD_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ABCD_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ABCD_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ABCD_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ABCD_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AbsoluteBreadthIndex indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AbsoluteBreadthIndex implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AbsoluteBreadthIndex() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AbsoluteBreadthIndex parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double[] change, double[] volume, double[] newHigh, double[] newLow, double[] aboveMa, double[] onBuySignal, long timestamp) {
|
||||
if (volume.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (newHigh.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (newLow.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (aboveMa.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (onBuySignal.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ABSOLUTE_BREADTH_INDEX_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AccelerationBands indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AccelerationBands implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AccelerationBands(int period, double factor) {
|
||||
if (period < 0) {
|
||||
throw new IllegalArgumentException("period must be non-negative");
|
||||
}
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ACCELERATION_BANDS_NEW.invokeExact((long) period, factor);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AccelerationBands parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ACCELERATION_BANDS_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the result, or null during warmup. */
|
||||
public AccelerationBandsOutput update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment out = a.allocate(24L);
|
||||
byte ok = (byte) NativeMethods.WICKRA_ACCELERATION_BANDS_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp, out);
|
||||
if (ok == 0) {
|
||||
return null;
|
||||
}
|
||||
return new AccelerationBandsOutput(
|
||||
out.get(JAVA_DOUBLE, 0L),
|
||||
out.get(JAVA_DOUBLE, 8L),
|
||||
out.get(JAVA_DOUBLE, 16L));
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ACCELERATION_BANDS_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ACCELERATION_BANDS_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ACCELERATION_BANDS_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
|
||||
package org.wickra;
|
||||
|
||||
/** Output record produced by the matching indicator. */
|
||||
public record AccelerationBandsOutput(double upper, double middle, double lower) {}
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AcceleratorOscillator indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AcceleratorOscillator implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AcceleratorOscillator(int aoFast, int aoSlow, int signalPeriod) {
|
||||
if (aoFast < 0) {
|
||||
throw new IllegalArgumentException("aoFast must be non-negative");
|
||||
}
|
||||
if (aoSlow < 0) {
|
||||
throw new IllegalArgumentException("aoSlow must be non-negative");
|
||||
}
|
||||
if (signalPeriod < 0) {
|
||||
throw new IllegalArgumentException("signalPeriod must be non-negative");
|
||||
}
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_NEW.invokeExact((long) aoFast, (long) aoSlow, (long) signalPeriod);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AcceleratorOscillator parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ACCELERATOR_OSCILLATOR_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdOscillator indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdOscillator implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdOscillator() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_AD_OSCILLATOR_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdOscillator parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_AD_OSCILLATOR_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_AD_OSCILLATOR_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_AD_OSCILLATOR_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_AD_OSCILLATOR_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_AD_OSCILLATOR_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_AD_OSCILLATOR_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdVolumeLine indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdVolumeLine implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdVolumeLine() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_AD_VOLUME_LINE_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdVolumeLine parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_AD_VOLUME_LINE_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double[] change, double[] volume, double[] newHigh, double[] newLow, double[] aboveMa, double[] onBuySignal, long timestamp) {
|
||||
if (volume.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (newHigh.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (newLow.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (aboveMa.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
if (onBuySignal.length != change.length) {
|
||||
throw new IllegalArgumentException("input arrays in the same group must have equal length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment changeSeg = a.allocateFrom(JAVA_DOUBLE, change);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_AD_VOLUME_LINE_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_AD_VOLUME_LINE_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_AD_VOLUME_LINE_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdaptiveCci indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdaptiveCci implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdaptiveCci(int period) {
|
||||
if (period < 0) {
|
||||
throw new IllegalArgumentException("period must be non-negative");
|
||||
}
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_CCI_NEW.invokeExact((long) period);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdaptiveCci parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ADAPTIVE_CCI_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ADAPTIVE_CCI_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ADAPTIVE_CCI_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ADAPTIVE_CCI_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ADAPTIVE_CCI_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ADAPTIVE_CCI_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdaptiveCycle indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdaptiveCycle implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdaptiveCycle() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_CYCLE_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdaptiveCycle parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ADAPTIVE_CYCLE_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double value) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ADAPTIVE_CYCLE_UPDATE.invokeExact(handle, value);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] input) {
|
||||
int n = input.length;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment inputSeg = a.allocateFrom(JAVA_DOUBLE, input);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ADAPTIVE_CYCLE_BATCH.invokeExact(handle, inputSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ADAPTIVE_CYCLE_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ADAPTIVE_CYCLE_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ADAPTIVE_CYCLE_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdaptiveLaguerreFilter indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdaptiveLaguerreFilter implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdaptiveLaguerreFilter(int period) {
|
||||
if (period < 0) {
|
||||
throw new IllegalArgumentException("period must be non-negative");
|
||||
}
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_NEW.invokeExact((long) period);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdaptiveLaguerreFilter parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double value) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_UPDATE.invokeExact(handle, value);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] input) {
|
||||
int n = input.length;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment inputSeg = a.allocateFrom(JAVA_DOUBLE, input);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_BATCH.invokeExact(handle, inputSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ADAPTIVE_LAGUERRE_FILTER_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming AdaptiveRsi indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class AdaptiveRsi implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public AdaptiveRsi(int period) {
|
||||
if (period < 0) {
|
||||
throw new IllegalArgumentException("period must be non-negative");
|
||||
}
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ADAPTIVE_RSI_NEW.invokeExact((long) period);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid AdaptiveRsi parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ADAPTIVE_RSI_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double value) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ADAPTIVE_RSI_UPDATE.invokeExact(handle, value);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] input) {
|
||||
int n = input.length;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment inputSeg = a.allocateFrom(JAVA_DOUBLE, input);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ADAPTIVE_RSI_BATCH.invokeExact(handle, inputSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ADAPTIVE_RSI_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ADAPTIVE_RSI_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ADAPTIVE_RSI_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// 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 static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** Streaming Adl indicator over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class Adl implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
public Adl() {
|
||||
MemorySegment h;
|
||||
try {
|
||||
h = (MemorySegment) NativeMethods.WICKRA_ADL_NEW.invokeExact();
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid Adl parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_ADL_FREE);
|
||||
}
|
||||
|
||||
/** Push one observation; returns the indicator value (NaN during warmup). */
|
||||
public double update(double open, double high, double low, double close, double volume, long timestamp) {
|
||||
try {
|
||||
return (double) NativeMethods.WICKRA_ADL_UPDATE.invokeExact(handle, open, high, low, close, volume, timestamp);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Vectorized update over a whole series; NaN at warmup positions. */
|
||||
public double[] batch(double[] open, double[] high, double[] low, double[] close, double[] volume, double[] timestamp) {
|
||||
int n = open.length;
|
||||
if (high.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (low.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (close.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (volume.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
if (timestamp.length != n) {
|
||||
throw new IllegalArgumentException("all input arrays must have the same length");
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment openSeg = a.allocateFrom(JAVA_DOUBLE, open);
|
||||
MemorySegment highSeg = a.allocateFrom(JAVA_DOUBLE, high);
|
||||
MemorySegment lowSeg = a.allocateFrom(JAVA_DOUBLE, low);
|
||||
MemorySegment closeSeg = a.allocateFrom(JAVA_DOUBLE, close);
|
||||
MemorySegment volumeSeg = a.allocateFrom(JAVA_DOUBLE, volume);
|
||||
MemorySegment timestampSeg = a.allocateFrom(JAVA_DOUBLE, timestamp);
|
||||
MemorySegment outSeg = a.allocate(JAVA_DOUBLE.byteSize() * n);
|
||||
NativeMethods.WICKRA_ADL_BATCH.invokeExact(handle, openSeg, highSeg, lowSeg, closeSeg, volumeSeg, timestampSeg, outSeg, (long) n);
|
||||
double[] out = new double[n];
|
||||
MemorySegment.copy(outSeg, JAVA_DOUBLE, 0L, out, 0, n);
|
||||
return out;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Number of updates required before update() yields a value. */
|
||||
public int warmupPeriod() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_ADL_WARMUP_PERIOD.invokeExact(handle);
|
||||
return (int) n;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether the indicator has consumed enough input to emit a value. */
|
||||
public boolean isReady() {
|
||||
try {
|
||||
byte r = (byte) NativeMethods.WICKRA_ADL_IS_READY.invokeExact(handle);
|
||||
return r != 0;
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
NativeMethods.WICKRA_ADL_RESET.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user