Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d7cb771a28 | |||
| 3e5a19c94a | |||
| 8bfe24ac1d | |||
| 395a2289f4 | |||
| 9973d1a6bf | |||
| cb216668ee | |||
| 20c0002f8e | |||
| 99497eb062 | |||
| 7e51f31f02 | |||
| 3ee3fb67ec | |||
| 3ebcb3f758 | |||
| b31a9a3624 | |||
| 6433c9b3de | |||
| 447bbc8220 | |||
| c9ef2fc037 | |||
| 49c2872ad4 | |||
| 0c4bbaf314 | |||
| 99ad2ab107 | |||
| 8e225a5872 | |||
| 87bb008aa1 | |||
| b06ce2678a | |||
| 5b7523265c |
+6
-2
@@ -11,10 +11,13 @@ bindings/c/include/wickra.h text eol=lf
|
||||
*.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.
|
||||
# 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.
|
||||
@@ -23,8 +26,9 @@ go.sum 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 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
|
||||
|
||||
|
||||
+66
-19
@@ -34,6 +34,7 @@ jobs:
|
||||
rust:
|
||||
name: Rust ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -104,6 +105,7 @@ jobs:
|
||||
examples-smoke:
|
||||
name: Examples (syntax smoke)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -193,6 +195,7 @@ jobs:
|
||||
clippy-bindings:
|
||||
name: Clippy bindings
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -276,6 +279,7 @@ jobs:
|
||||
msrv:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -324,6 +328,7 @@ jobs:
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -365,6 +370,7 @@ jobs:
|
||||
supply-chain:
|
||||
name: Supply-chain (cargo-deny)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -383,6 +389,7 @@ jobs:
|
||||
fuzz-smoke:
|
||||
name: Fuzz (smoke)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -436,6 +443,7 @@ jobs:
|
||||
python:
|
||||
name: Python ${{ matrix.python-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -517,6 +525,7 @@ jobs:
|
||||
wasm:
|
||||
name: WASM build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
@@ -560,6 +569,7 @@ jobs:
|
||||
node:
|
||||
name: Node ${{ matrix.node-version }} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -618,13 +628,22 @@ jobs:
|
||||
# exist yet for win32-x64-msvc).
|
||||
run: npx napi build --platform --release
|
||||
|
||||
# The Node test process has wedged on macOS runners (the step hung for
|
||||
# 1h+ while the same tests passed in ~1.5 min elsewhere). Wrap it so a
|
||||
# hung attempt is killed after 6 min and retried once, rather than
|
||||
# running up to the job-level backstop. A normal run is under a minute.
|
||||
- name: Run Node tests
|
||||
working-directory: bindings/node
|
||||
run: node --test __tests__/
|
||||
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||
with:
|
||||
timeout_minutes: 6
|
||||
max_attempts: 2
|
||||
command: cd bindings/node && node --test __tests__/
|
||||
shell: bash
|
||||
|
||||
c-abi:
|
||||
name: C ABI on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -680,6 +699,7 @@ jobs:
|
||||
csharp:
|
||||
name: C# on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -730,6 +750,7 @@ jobs:
|
||||
go:
|
||||
name: Go on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -778,15 +799,36 @@ jobs:
|
||||
- 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: |
|
||||
mkdir -p bindings/go/lib
|
||||
case "$RUNNER_OS" in
|
||||
Linux) cp target/release/libwickra.so bindings/go/lib/ ;;
|
||||
macOS) cp target/release/libwickra.dylib bindings/go/lib/ ;;
|
||||
Windows) cp target/release/wickra.dll bindings/go/lib/ ;;
|
||||
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
|
||||
@@ -801,9 +843,11 @@ jobs:
|
||||
|
||||
- name: Vet and test the Go binding
|
||||
shell: bash
|
||||
# On Windows there is no rpath; the loader resolves wickra.dll via PATH.
|
||||
# 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="$PWD/bindings/go/lib:$PATH"
|
||||
export PATH="$WICKRA_GO_LIBDIR:$PATH"
|
||||
cd bindings/go
|
||||
go vet ./...
|
||||
go test ./...
|
||||
@@ -811,7 +855,7 @@ jobs:
|
||||
- name: Build the Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$PWD/bindings/go/lib:$PATH"
|
||||
export PATH="$WICKRA_GO_LIBDIR:$PATH"
|
||||
cd examples/go
|
||||
go build ./...
|
||||
|
||||
@@ -819,7 +863,7 @@ jobs:
|
||||
- name: Run the offline Go examples
|
||||
shell: bash
|
||||
run: |
|
||||
export PATH="$PWD/bindings/go/lib:$PATH"
|
||||
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
|
||||
@@ -829,6 +873,7 @@ jobs:
|
||||
r:
|
||||
name: R on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -868,23 +913,24 @@ jobs:
|
||||
|
||||
- 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. On Linux/macOS the rpath points at
|
||||
# WICKRA_LIB_DIR; export the loader path too as a belt-and-suspenders.
|
||||
# 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//\\//}"
|
||||
export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH"
|
||||
export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH"
|
||||
R CMD INSTALL bindings/r
|
||||
Rscript -e 'library(testthat); library(wickra); test_dir("bindings/r/tests/testthat", stop_on_failure = TRUE)'
|
||||
|
||||
- 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: |
|
||||
export WICKRA_LIB_DIR="${WICKRA_LIB_DIR//\\//}"
|
||||
export LD_LIBRARY_PATH="$WICKRA_LIB_DIR:$LD_LIBRARY_PATH"
|
||||
export DYLD_LIBRARY_PATH="$WICKRA_LIB_DIR:$DYLD_LIBRARY_PATH"
|
||||
cd examples/r
|
||||
for f in streaming backtest multi_timeframe parallel_assets \
|
||||
strategy_rsi_mean_reversion strategy_macd_adx strategy_bollinger_squeeze; do
|
||||
@@ -899,6 +945,7 @@ jobs:
|
||||
java:
|
||||
name: Java on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 20 # backstop: cap a wedged job instead of GitHub's 6h default (slowest real job ~5 min)
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -775,6 +775,97 @@ jobs:
|
||||
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
|
||||
needs: c-abi-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
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, c-abi-build]
|
||||
|
||||
@@ -332,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,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 —
|
||||
@@ -449,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)."
|
||||
|
||||
@@ -55,6 +55,7 @@ bindings/node/npm-debug.log*
|
||||
|
||||
# WASM build output
|
||||
bindings/wasm/pkg/
|
||||
bindings/wasm/pkg-node/
|
||||
|
||||
# Python venv
|
||||
.venv/
|
||||
|
||||
+16
-13
@@ -7,7 +7,7 @@ for the day-to-day workflow.
|
||||
|
||||
## Workspace layout
|
||||
|
||||
Wickra is a Cargo workspace of three Rust crates plus three binding crates.
|
||||
Wickra is a Cargo workspace of three Rust crates plus four binding crates.
|
||||
The split is deliberate: every concern that one user might want to disable
|
||||
or replace lives behind a separate crate boundary.
|
||||
|
||||
@@ -20,7 +20,7 @@ or replace lives behind a separate crate boundary.
|
||||
┌───────────▼──────────┐ ┌──────────▼─────────┐
|
||||
│ wickra-core │ │ wickra-data │
|
||||
│ indicator engine │ │ i/o + aggregation │
|
||||
│ • 214 indicators │ │ • CSV reader │
|
||||
│ • 514 indicators │ │ • CSV reader │
|
||||
│ • Indicator trait │ │ • Tick aggregator │
|
||||
│ • BatchExt impl │ │ • Resampler │
|
||||
│ • OHLCV / Candle │ │ • Live feeds │
|
||||
@@ -203,14 +203,17 @@ typed object arrays for Node/WASM).
|
||||
|
||||
A handful of indicators need care beyond naive accumulation:
|
||||
|
||||
- **Welford's online variance** is used in `StdDev`, `Variance`, `ZScore`,
|
||||
`BollingerBands`, and several others. Standard sum-of-squares is
|
||||
catastrophically lossy for low-variance inputs; Welford's recurrence
|
||||
keeps O(eps) error.
|
||||
- **Kahan summation** is used wherever rolling sums could span > 1e6
|
||||
elements without resetting — currently only Hurst-exponent's R/S
|
||||
chunks. Most rolling sums are bounded by the window size and don't need
|
||||
it.
|
||||
- **Rolling variance is running-sum, not Welford.** The sliding-window
|
||||
variance family — `StdDev`, `Variance`, `ZScore`, `Bollinger` — keeps
|
||||
running `Σx` and `Σx²` over the window and reports `var = Σx²/n − mean²`,
|
||||
clamping to zero the tiny negative values floating-point cancellation can
|
||||
produce. `Bollinger` periodically reseeds its `Σx²` from the live window
|
||||
so error cannot accumulate over a long stream. Welford's online algorithm
|
||||
(an incremental `M2` accumulator) does **not** transfer cleanly to a
|
||||
sliding window — removing the oldest point from `M2` is numerically
|
||||
unstable — so it is used only where the statistic is *not* a fixed
|
||||
window: `IntradayVolatilityProfile` and `SeasonalZScore` accumulate
|
||||
per-bucket variance that way.
|
||||
- **Logarithm bases** matter for some indicators (Hurst, MFI). Wickra
|
||||
uses natural log everywhere unless the reference math explicitly
|
||||
requires `log10` or `log2` — and then it documents the choice in the
|
||||
@@ -327,9 +330,9 @@ re-discovering them.
|
||||
- **`FAMILIES` (from PR #60) is hand-maintained.** Adding a new
|
||||
indicator requires a separate entry in `FAMILIES`. The
|
||||
`total_count_matches_expected` test will fail if you forget.
|
||||
- **WASM does not have automated tests yet.** Smoke-validated only
|
||||
through the manual examples. Adding `wasm-bindgen-test` coverage is
|
||||
on the roadmap.
|
||||
- **WASM is covered by `wasm-bindgen-test`.** `bindings/wasm/src/lib.rs`
|
||||
carries 21 in-crate tests (run under `wasm-pack test` in CI), in
|
||||
addition to the manual browser examples.
|
||||
|
||||
For the high-level project goals see [`ROADMAP.md`](ROADMAP.md); for
|
||||
day-to-day contribution mechanics see [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
||||
|
||||
@@ -94,3 +94,75 @@ 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#, Go, Java,
|
||||
R or WebAssembly 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 converges.** A `batch` call crosses the boundary once and the Rust core
|
||||
computes the whole series internally, so batch throughput is roughly the same
|
||||
in every binding — close to the core speed.
|
||||
- **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) | 391 | 500 |
|
||||
| C | 383 | 330 |
|
||||
| C# / .NET | 337 | 244 |
|
||||
| Python | 33 | 488 |
|
||||
| Java | 28 | 175 |
|
||||
| Go | 24 | 400 |
|
||||
| WebAssembly | 19 | 167 |
|
||||
| Node.js | 17 | 10 |
|
||||
| R | 0.1 | 193 |
|
||||
|
||||
Streaming spans more than three orders of magnitude — the raw C ABI (383) is
|
||||
nearly the FFI-free Rust ceiling (391), while R's per-call interpreter overhead
|
||||
(0.1) makes streaming ~2000× slower than its own batch. Batch converges near the
|
||||
core speed for the zero-copy bindings (numpy, slices, typed arrays); the two
|
||||
outliers are Node — whose napi `batch` boxes every element into a JS `Array` —
|
||||
and R. These are machine-dependent and reflect FFI overhead, not algorithm
|
||||
speed.
|
||||
|
||||
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# / .NET (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)
|
||||
```
|
||||
|
||||
+96
-1
@@ -5,6 +5,95 @@ All notable changes to Wickra are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.8.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
|
||||
@@ -1461,7 +1550,13 @@ 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.7.9...HEAD
|
||||
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.8.5...HEAD
|
||||
[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
|
||||
|
||||
Generated
+9
-9
@@ -1944,7 +1944,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"criterion",
|
||||
@@ -1955,7 +1955,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-bench"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"criterion",
|
||||
"kand",
|
||||
@@ -1967,14 +1967,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-c"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"wickra-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wickra-core"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"proptest",
|
||||
@@ -1984,7 +1984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-data"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"csv",
|
||||
@@ -2001,7 +2001,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-examples"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
@@ -2011,7 +2011,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-node"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"napi",
|
||||
"napi-build",
|
||||
@@ -2021,7 +2021,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-python"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"numpy",
|
||||
"pyo3",
|
||||
@@ -2030,7 +2030,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wickra-wasm"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"js-sys",
|
||||
|
||||
+2
-2
@@ -14,7 +14,7 @@ members = [
|
||||
exclude = ["fuzz"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
authors = ["kingchenc <support@wickra.org>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.86"
|
||||
@@ -26,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
|
||||
categories = ["finance", "mathematics", "science"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.7.9" }
|
||||
wickra-core = { path = "crates/wickra-core", version = "0.8.5" }
|
||||
|
||||
thiserror = "2"
|
||||
rayon = "1.10"
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
[](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)
|
||||
@@ -52,6 +55,7 @@ Full documentation lives at **[docs.wickra.org](https://docs.wickra.org)**:
|
||||
[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 514 indicators; start at the
|
||||
@@ -102,8 +106,7 @@ 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 · WASM** | **514** | **yes** |
|
||||
| | | | **C · C# · Go · Java · R** | | |
|
||||
| **★ Wickra**| **clean** | **yes, O(1)** | **Rust · Python · Node · WASM · 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 |
|
||||
@@ -331,7 +334,10 @@ Every layer is covered; run the suites with the commands in
|
||||
|
||||
- `wickra-core`: unit tests per indicator — textbook reference values
|
||||
(Wilder RSI, Bollinger Bands, MACD, ATR, Stochastic), `batch == streaming`
|
||||
equivalence, `reset` semantics, NaN/Inf handling, and property tests.
|
||||
equivalence, `reset` semantics, NaN/Inf handling, and property tests. A
|
||||
catalogue-wide property harness (`tests/invariants.rs`) additionally asserts
|
||||
`batch == streaming`, `reset == fresh`, and non-finite-input rejection for
|
||||
**every** indicator and bar-builder.
|
||||
- `wickra-data`: unit tests for CSV decoding, the tick aggregator, the
|
||||
resampler, and the Binance payload parser.
|
||||
- `bindings/python`: pytest covering smoke checks, streaming/batch
|
||||
@@ -341,6 +347,10 @@ 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
|
||||
@@ -348,6 +358,12 @@ Every layer is covered; run the suites with the commands in
|
||||
- `bindings/java`: JUnit cases covering one indicator per FFI archetype
|
||||
(scalar/batch, multi-output, bars, profile, array input) plus batch equivalence.
|
||||
|
||||
The four C-ABI bindings (C#, Go, Java, R) additionally replay a shared,
|
||||
language-neutral golden fixture (`testdata/golden/*.csv`, generated by
|
||||
`cargo run -p wickra-examples --bin gen_golden`) and assert exact parity with the
|
||||
Rust reference outputs across every archetype (SMA, EMA, RSI, ATR, MACD, ADX,
|
||||
Beta), catching FFI wiring bugs the math-only core tests cannot see.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are very welcome — issues, bug reports, ideas, and pull requests
|
||||
|
||||
+3
-3
@@ -2,13 +2,13 @@
|
||||
|
||||
## Supported versions
|
||||
|
||||
Wickra is pre-1.0. Security fixes are applied to the latest released `0.5.x`
|
||||
Wickra is pre-1.0. Security fixes are applied to the latest released `0.8.5`
|
||||
version only; please upgrade to the newest release before reporting an issue.
|
||||
|
||||
| Version | Supported |
|
||||
| --- | --- |
|
||||
| 0.5.x (latest) | :white_check_mark: |
|
||||
| older 0.5.x | :x: |
|
||||
| 0.8.5 (latest) | :white_check_mark: |
|
||||
| < 0.8.5 | :x: |
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ small.
|
||||
| Threat | Mitigation |
|
||||
| --- | --- |
|
||||
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
|
||||
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
|
||||
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and is built with `panic = "abort"` so a panic terminates the process deterministically instead of unwinding across the FFI boundary (which would be undefined behaviour). A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
|
||||
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
|
||||
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
|
||||
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
|
||||
|
||||
+14
-1
@@ -42,5 +42,18 @@ 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.
|
||||
default = ["parallel"]
|
||||
parallel = ["wickra-core/parallel"]
|
||||
|
||||
[dependencies]
|
||||
wickra-core = { workspace = true }
|
||||
# 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 }
|
||||
|
||||
@@ -54,6 +54,20 @@ Multi-output indicators (MACD, Bollinger, ADX, …) take a pointer to a `#[repr(
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -52,6 +52,18 @@ foreach (var price in liveFeed)
|
||||
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
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
using System.Globalization;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Wickra;
|
||||
using Xunit;
|
||||
|
||||
namespace Wickra.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Golden-fixture parity: replay the shared <c>testdata/golden</c> input series
|
||||
/// through the C# FFI and assert every value matches the Rust reference output.
|
||||
/// Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
/// wiring bug (swapped parameter, wrong multi-output field) is caught.
|
||||
/// Fixtures are generated by <c>cargo run -p wickra-examples --bin gen_golden</c>.
|
||||
/// </summary>
|
||||
public class GoldenTests
|
||||
{
|
||||
private const double Tol = 1e-6;
|
||||
|
||||
private static string GoldenDir([CallerFilePath] string file = "") =>
|
||||
Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden"));
|
||||
|
||||
private static List<string[]> ReadCsv(string name)
|
||||
{
|
||||
var path = Path.Combine(GoldenDir(), name + ".csv");
|
||||
return File.ReadAllLines(path)
|
||||
.Skip(1) // header
|
||||
.Where(l => l.Length > 0)
|
||||
.Select(l => l.Split(','))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static double[][] Input()
|
||||
{
|
||||
return ReadCsv("input")
|
||||
.Select(r => r.Select(c => double.Parse(c, CultureInfo.InvariantCulture)).ToArray())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static double Cell(string s) =>
|
||||
s == "nan" ? double.NaN : double.Parse(s, CultureInfo.InvariantCulture);
|
||||
|
||||
private static void AssertClose(double got, double want, int row, string field)
|
||||
{
|
||||
if (double.IsNaN(want))
|
||||
{
|
||||
Assert.True(double.IsNaN(got), $"row {row} {field}: expected warmup/NaN, got {got}");
|
||||
return;
|
||||
}
|
||||
var tol = Tol * Math.Max(1.0, Math.Abs(want));
|
||||
Assert.True(Math.Abs(got - want) <= tol, $"row {row} {field}: got {got}, want {want}");
|
||||
}
|
||||
|
||||
// --- scalar (close-driven) ------------------------------------------------
|
||||
|
||||
[Theory]
|
||||
[InlineData("sma")]
|
||||
[InlineData("ema")]
|
||||
[InlineData("rsi")]
|
||||
public void Scalar_MatchesGolden(string name)
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv(name);
|
||||
using var ind = (IDisposable)(name switch
|
||||
{
|
||||
"sma" => new Sma(14),
|
||||
"ema" => new Ema(14),
|
||||
"rsi" => new Rsi(14),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(name)),
|
||||
});
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var close = input[i][3];
|
||||
double got = ind switch
|
||||
{
|
||||
Sma s => s.Update(close),
|
||||
Ema e => e.Update(close),
|
||||
Rsi r => r.Update(close),
|
||||
_ => double.NaN,
|
||||
};
|
||||
AssertClose(got, Cell(expected[i][0]), i, name);
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle, single output ------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Candle_Atr_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("atr");
|
||||
using var atr = new Atr(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AssertClose(atr.Update(o, h, l, c, v, i), Cell(expected[i][0]), i, "atr");
|
||||
}
|
||||
}
|
||||
|
||||
// --- pairwise -------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Pairwise_Beta_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("beta");
|
||||
using var beta = new Beta(20);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
// generator fed (close, open)
|
||||
AssertClose(beta.Update(input[i][3], input[i][0]), Cell(expected[i][0]), i, "beta");
|
||||
}
|
||||
}
|
||||
|
||||
// --- scalar multi-output: MACD -------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Macd_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("macd");
|
||||
using var macd = new MacdIndicator(12, 26, 9);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
MacdOutput? got = macd.Update(input[i][3]);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.Macd, Cell(e[0]), i, "macd.macd");
|
||||
AssertClose(got.Value.Signal, Cell(e[1]), i, "macd.signal");
|
||||
AssertClose(got.Value.Histogram, Cell(e[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
|
||||
// --- candle multi-output: ADX --------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void MultiOutput_Adx_MatchesGolden()
|
||||
{
|
||||
var input = Input();
|
||||
var expected = ReadCsv("adx");
|
||||
using var adx = new Adx(14);
|
||||
for (var i = 0; i < input.Length; i++)
|
||||
{
|
||||
var (o, h, l, c, v) = (input[i][0], input[i][1], input[i][2], input[i][3], input[i][4]);
|
||||
AdxOutput? got = adx.Update(o, h, l, c, v, i);
|
||||
var e = expected[i];
|
||||
if (e[0] == "nan")
|
||||
{
|
||||
Assert.Null(got);
|
||||
continue;
|
||||
}
|
||||
Assert.NotNull(got);
|
||||
AssertClose(got!.Value.PlusDi, Cell(e[0]), i, "adx.plus_di");
|
||||
AssertClose(got.Value.MinusDi, Cell(e[1]), i, "adx.minus_di");
|
||||
AssertClose(got.Value.Adx, Cell(e[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- NuGet package metadata -->
|
||||
<PackageId>Wickra</PackageId>
|
||||
<Version>0.7.9</Version>
|
||||
<Version>0.8.5</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>
|
||||
|
||||
@@ -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.");
|
||||
+36
-11
@@ -2,7 +2,7 @@
|
||||
|
||||
[](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://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.**
|
||||
@@ -16,24 +16,38 @@ 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/bindings/go
|
||||
go get github.com/wickra-lib/wickra-go
|
||||
```
|
||||
|
||||
The binding uses cgo, so a C compiler is required, and it links against the
|
||||
prebuilt Wickra C ABI library. Build that library from the workspace and stage
|
||||
it under this package's `lib/` directory:
|
||||
```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
|
||||
cp target/release/libwickra.so bindings/go/lib/ # Linux
|
||||
cp target/release/libwickra.dylib bindings/go/lib/ # macOS
|
||||
cp target/release/wickra.dll bindings/go/lib/ # Windows (also on PATH at run time)
|
||||
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
|
||||
```
|
||||
|
||||
On Linux and 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`).
|
||||
|
||||
## Quick start
|
||||
|
||||
```go
|
||||
@@ -74,6 +88,17 @@ values — the equivalence is enforced by the test suite. Multi-output indicator
|
||||
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
|
||||
|
||||
@@ -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,191 @@
|
||||
package wickra
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Golden-fixture parity: replay the shared testdata/golden input series through
|
||||
// the Go FFI and assert every value matches the Rust reference output. Where the
|
||||
// archetype test only checks finiteness, this pins exact values, catching wiring
|
||||
// bugs (swapped params, wrong multi-output field). Fixtures are generated by
|
||||
// `cargo run -p wickra-examples --bin gen_golden`.
|
||||
|
||||
const goldenTol = 1e-6
|
||||
|
||||
func readGolden(t *testing.T, name string) [][]string {
|
||||
t.Helper()
|
||||
f, err := os.Open("../../testdata/golden/" + name + ".csv")
|
||||
if err != nil {
|
||||
t.Fatalf("open %s: %v", name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
var rows [][]string
|
||||
sc := bufio.NewScanner(f)
|
||||
first := true
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if first {
|
||||
first = false
|
||||
continue
|
||||
}
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
rows = append(rows, strings.Split(line, ","))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
func goldenCell(s string) float64 {
|
||||
if s == "nan" {
|
||||
return math.NaN()
|
||||
}
|
||||
v, _ := strconv.ParseFloat(s, 64)
|
||||
return v
|
||||
}
|
||||
|
||||
func goldenInput(t *testing.T) [][]float64 {
|
||||
rows := readGolden(t, "input")
|
||||
out := make([][]float64, len(rows))
|
||||
for i, r := range rows {
|
||||
vals := make([]float64, len(r))
|
||||
for j, c := range r {
|
||||
vals[j] = goldenCell(c)
|
||||
}
|
||||
out[i] = vals
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func assertGoldenClose(t *testing.T, got, want float64, row int, field string) {
|
||||
t.Helper()
|
||||
if math.IsNaN(want) {
|
||||
if !math.IsNaN(got) {
|
||||
t.Errorf("row %d %s: expected warmup/NaN, got %v", row, field, got)
|
||||
}
|
||||
return
|
||||
}
|
||||
tol := goldenTol * math.Max(1.0, math.Abs(want))
|
||||
if math.Abs(got-want) > tol {
|
||||
t.Errorf("row %d %s: got %v want %v", row, field, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenScalar(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
sma, err := NewSma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sma.Close()
|
||||
ema, err := NewEma(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ema.Close()
|
||||
rsi, err := NewRsi(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rsi.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
upd func(close float64) float64
|
||||
}{
|
||||
{"sma", sma.Update},
|
||||
{"ema", ema.Update},
|
||||
{"rsi", rsi.Update},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
exp := readGolden(t, tc.name)
|
||||
for i := range input {
|
||||
assertGoldenClose(t, tc.upd(input[i][3]), goldenCell(exp[i][0]), i, tc.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAtr(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "atr")
|
||||
atr, err := NewAtr(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer atr.Close()
|
||||
for i := range input {
|
||||
got := atr.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "atr")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenBeta(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "beta")
|
||||
beta, err := NewBeta(20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer beta.Close()
|
||||
for i := range input {
|
||||
// generator fed (close, open)
|
||||
assertGoldenClose(t, beta.Update(input[i][3], input[i][0]), goldenCell(exp[i][0]), i, "beta")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenMacd(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "macd")
|
||||
macd, err := NewMacdIndicator(12, 26, 9)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer macd.Close()
|
||||
for i := range input {
|
||||
out, ok := macd.Update(input[i][3])
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d macd: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d macd: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.Macd, goldenCell(exp[i][0]), i, "macd.macd")
|
||||
assertGoldenClose(t, out.Signal, goldenCell(exp[i][1]), i, "macd.signal")
|
||||
assertGoldenClose(t, out.Histogram, goldenCell(exp[i][2]), i, "macd.histogram")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGoldenAdx(t *testing.T) {
|
||||
input := goldenInput(t)
|
||||
exp := readGolden(t, "adx")
|
||||
adx, err := NewAdx(14)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer adx.Close()
|
||||
for i := range input {
|
||||
out, ok := adx.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i))
|
||||
if exp[i][0] == "nan" {
|
||||
if ok {
|
||||
t.Errorf("row %d adx: expected warmup, got %+v", i, out)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("row %d adx: expected value, got warmup", i)
|
||||
continue
|
||||
}
|
||||
assertGoldenClose(t, out.PlusDi, goldenCell(exp[i][0]), i, "adx.plus_di")
|
||||
assertGoldenClose(t, out.MinusDi, goldenCell(exp[i][1]), i, "adx.minus_di")
|
||||
assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+12
-7
@@ -4,16 +4,21 @@
|
||||
// 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
|
||||
// (libwickra.so/.dylib or wickra.dll) staged under ./lib — see the package
|
||||
// README for how to provision it.
|
||||
// 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}/../c/include
|
||||
#cgo linux LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
|
||||
#cgo darwin LDFLAGS: -L${SRCDIR}/lib -lwickra -Wl,-rpath,${SRCDIR}/lib
|
||||
#cgo windows LDFLAGS: -L${SRCDIR}/lib -l:wickra.dll
|
||||
#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"
|
||||
|
||||
+15
-2
@@ -30,14 +30,14 @@ Maven:
|
||||
<dependency>
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.7.9</version>
|
||||
<version>0.8.5</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Gradle:
|
||||
|
||||
```kotlin
|
||||
implementation("org.wickra:wickra:0.7.9")
|
||||
implementation("org.wickra:wickra:0.8.5")
|
||||
```
|
||||
|
||||
The native library ships prebuilt per platform (Linux, macOS, Windows — x64 and
|
||||
@@ -76,6 +76,19 @@ values — the equivalence is enforced by the test suite. Multi-output indicator
|
||||
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
|
||||
|
||||
@@ -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.13.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.2.0</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) {}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>org.wickra</groupId>
|
||||
<artifactId>wickra</artifactId>
|
||||
<version>0.7.9</version>
|
||||
<version>0.8.5</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Wickra</name>
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
package org.wickra;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Golden-fixture parity: replay the shared {@code testdata/golden} input series
|
||||
* through the Java FFI and assert every value matches the Rust reference output.
|
||||
* Where the archetype tests only check finiteness, this pins exact values, so a
|
||||
* wiring bug (swapped parameter, wrong multi-output field) is caught. Fixtures
|
||||
* are generated by {@code cargo run -p wickra-examples --bin gen_golden}.
|
||||
*/
|
||||
class GoldenTests {
|
||||
private static final double TOL = 1e-6;
|
||||
|
||||
private static Path goldenDir() {
|
||||
File d = new File("").getAbsoluteFile();
|
||||
while (d != null) {
|
||||
File g = new File(d, "testdata/golden");
|
||||
if (g.isDirectory()) {
|
||||
return g.toPath();
|
||||
}
|
||||
d = d.getParentFile();
|
||||
}
|
||||
throw new IllegalStateException("testdata/golden not found from " + new File("").getAbsolutePath());
|
||||
}
|
||||
|
||||
private static List<String[]> readCsv(String name) throws Exception {
|
||||
List<String> lines = Files.readAllLines(goldenDir().resolve(name + ".csv"));
|
||||
List<String[]> rows = new ArrayList<>();
|
||||
for (int i = 1; i < lines.size(); i++) { // skip header
|
||||
if (!lines.get(i).isEmpty()) {
|
||||
rows.add(lines.get(i).split(","));
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static double cell(String s) {
|
||||
return s.equals("nan") ? Double.NaN : Double.parseDouble(s);
|
||||
}
|
||||
|
||||
private static double[][] input() throws Exception {
|
||||
List<String[]> rows = readCsv("input");
|
||||
double[][] out = new double[rows.size()][];
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
String[] r = rows.get(i);
|
||||
double[] v = new double[r.length];
|
||||
for (int j = 0; j < r.length; j++) {
|
||||
v[j] = Double.parseDouble(r[j]);
|
||||
}
|
||||
out[i] = v;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void close(double got, double want, int row, String field) {
|
||||
if (Double.isNaN(want)) {
|
||||
assertTrue(Double.isNaN(got), "row " + row + " " + field + ": expected warmup/NaN, got " + got);
|
||||
return;
|
||||
}
|
||||
double tol = TOL * Math.max(1.0, Math.abs(want));
|
||||
assertTrue(Math.abs(got - want) <= tol, "row " + row + " " + field + ": got " + got + " want " + want);
|
||||
}
|
||||
|
||||
@Test
|
||||
void scalarMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
try (Sma sma = new Sma(14)) {
|
||||
List<String[]> e = readCsv("sma");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(sma.update(in[i][3]), cell(e.get(i)[0]), i, "sma");
|
||||
}
|
||||
}
|
||||
try (Ema ema = new Ema(14)) {
|
||||
List<String[]> e = readCsv("ema");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(ema.update(in[i][3]), cell(e.get(i)[0]), i, "ema");
|
||||
}
|
||||
}
|
||||
try (Rsi rsi = new Rsi(14)) {
|
||||
List<String[]> e = readCsv("rsi");
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(rsi.update(in[i][3]), cell(e.get(i)[0]), i, "rsi");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candleAtrMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("atr");
|
||||
try (Atr atr = new Atr(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
close(atr.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i), cell(e.get(i)[0]), i, "atr");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pairwiseBetaMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("beta");
|
||||
try (Beta beta = new Beta(20)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
// generator fed (close, open)
|
||||
close(beta.update(in[i][3], in[i][0]), cell(e.get(i)[0]), i, "beta");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void macdMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("macd");
|
||||
try (MacdIndicator macd = new MacdIndicator(12, 26, 9)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
MacdOutput o = macd.update(in[i][3]);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " macd: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " macd: expected value");
|
||||
close(o.macd(), cell(row[0]), i, "macd.macd");
|
||||
close(o.signal(), cell(row[1]), i, "macd.signal");
|
||||
close(o.histogram(), cell(row[2]), i, "macd.histogram");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void adxMatchesGolden() throws Exception {
|
||||
double[][] in = input();
|
||||
List<String[]> e = readCsv("adx");
|
||||
try (Adx adx = new Adx(14)) {
|
||||
for (int i = 0; i < in.length; i++) {
|
||||
AdxOutput o = adx.update(in[i][0], in[i][1], in[i][2], in[i][3], in[i][4], i);
|
||||
String[] row = e.get(i);
|
||||
if (row[0].equals("nan")) {
|
||||
assertNull(o, "row " + i + " adx: expected warmup");
|
||||
continue;
|
||||
}
|
||||
assertNotNull(o, "row " + i + " adx: expected value");
|
||||
close(o.plusDi(), cell(row[0]), i, "adx.plus_di");
|
||||
close(o.minusDi(), cell(row[1]), i, "adx.minus_di");
|
||||
close(o.adx(), cell(row[2]), i, "adx.adx");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,18 @@ for (const price of liveFeed) {
|
||||
`batch(prices)` and feeding the same prices through `update()` produce
|
||||
identical values — the equivalence is enforced by the test suite.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/throughput.js` 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
|
||||
npx napi build --platform --release
|
||||
node benchmarks/throughput.js
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-arm64",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-arm64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-darwin-x64",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.darwin-x64.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-arm64-gnu",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-arm64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-linux-x64-gnu",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.linux-x64-gnu.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-arm64-msvc",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-arm64-msvc.node",
|
||||
"files": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra-win32-x64-msvc",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
|
||||
"main": "wickra.win32-x64-msvc.node",
|
||||
"files": [
|
||||
|
||||
Generated
+20
-20
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "wickra",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@napi-rs/cli": "^2.18.0"
|
||||
@@ -15,12 +15,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-darwin-arm64": "0.7.9",
|
||||
"wickra-darwin-x64": "0.7.9",
|
||||
"wickra-linux-arm64-gnu": "0.7.9",
|
||||
"wickra-linux-x64-gnu": "0.7.9",
|
||||
"wickra-win32-arm64-msvc": "0.7.9",
|
||||
"wickra-win32-x64-msvc": "0.7.9"
|
||||
"wickra-darwin-arm64": "0.8.5",
|
||||
"wickra-darwin-x64": "0.8.5",
|
||||
"wickra-linux-arm64-gnu": "0.8.5",
|
||||
"wickra-linux-x64-gnu": "0.8.5",
|
||||
"wickra-win32-arm64-msvc": "0.8.5",
|
||||
"wickra-win32-x64-msvc": "0.8.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/cli": {
|
||||
@@ -41,8 +41,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-arm64": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.5.tgz",
|
||||
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -57,8 +57,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-darwin-x64": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.5.tgz",
|
||||
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -73,8 +73,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-arm64-gnu": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.5.tgz",
|
||||
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -89,8 +89,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-linux-x64-gnu": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.5.tgz",
|
||||
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
@@ -105,8 +105,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-arm64-msvc": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.5.tgz",
|
||||
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
@@ -121,8 +121,8 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wickra-win32-x64-msvc": {
|
||||
"version": "0.7.9",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.9.tgz",
|
||||
"version": "0.8.5",
|
||||
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.5.tgz",
|
||||
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "wickra",
|
||||
"version": "0.7.9",
|
||||
"version": "0.8.5",
|
||||
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
|
||||
"author": "kingchenc <support@wickra.org>",
|
||||
"main": "index.js",
|
||||
@@ -47,12 +47,12 @@
|
||||
"node": ">= 18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"wickra-linux-x64-gnu": "0.7.9",
|
||||
"wickra-linux-arm64-gnu": "0.7.9",
|
||||
"wickra-darwin-x64": "0.7.9",
|
||||
"wickra-darwin-arm64": "0.7.9",
|
||||
"wickra-win32-x64-msvc": "0.7.9",
|
||||
"wickra-win32-arm64-msvc": "0.7.9"
|
||||
"wickra-linux-x64-gnu": "0.8.5",
|
||||
"wickra-linux-arm64-gnu": "0.8.5",
|
||||
"wickra-darwin-x64": "0.8.5",
|
||||
"wickra-darwin-arm64": "0.8.5",
|
||||
"wickra-win32-x64-msvc": "0.8.5",
|
||||
"wickra-win32-arm64-msvc": "0.8.5"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "napi build --platform --release",
|
||||
|
||||
@@ -46,6 +46,24 @@ for price in live_feed:
|
||||
`batch(prices)` and feeding the same prices through `update()` produce
|
||||
identical values — the equivalence is enforced by the test suite.
|
||||
|
||||
## Benchmark
|
||||
|
||||
Two benchmarks ship with the binding:
|
||||
|
||||
- `benchmarks/throughput.py` — streaming and batch updates-per-second for `SMA`,
|
||||
`ATR` and `MACD`. This is per-binding FFI overhead (the same Rust core runs
|
||||
under every binding), not a cross-library ratio.
|
||||
- `benchmarks/compare_libraries.py` — the cross-library comparison against
|
||||
TA-Lib, pandas-ta, tulipy and finta that backs the headline speedups.
|
||||
|
||||
```bash
|
||||
maturin develop --release
|
||||
python -m benchmarks.throughput
|
||||
python -m benchmarks.compare_libraries # cross-library; auto-detects installed peers
|
||||
```
|
||||
|
||||
See the repository [BENCHMARKS.md](https://github.com/wickra-lib/wickra/blob/main/BENCHMARKS.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Throughput benchmark for the Wickra Python 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 Python counterpart of the Node ``throughput.js`` and the Rust
|
||||
criterion benches: it benchmarks Wickra's own O(1) streaming engine across the
|
||||
Python<->Rust boundary, so the headline number is raw per-binding throughput /
|
||||
FFI overhead, not a cross-library ratio.
|
||||
|
||||
For the cross-library comparison against TA-Lib, pandas-ta, tulipy and finta,
|
||||
see ``benchmarks/compare_libraries.py`` instead.
|
||||
|
||||
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 (multi-output batch returns a 2-D array and is not compared here).
|
||||
|
||||
Install the binding first (``maturin develop --release`` in bindings/python),
|
||||
then run from bindings/python::
|
||||
|
||||
python -m benchmarks.throughput # 200k bars (default)
|
||||
python -m benchmarks.throughput --bars 1000000
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import wickra as ta
|
||||
|
||||
|
||||
def _time_ns(fn, reps: int = 3) -> float:
|
||||
"""Median elapsed-ns over a few repetitions, after one warmup pass."""
|
||||
fn() # warmup
|
||||
samples = []
|
||||
for _ in range(reps):
|
||||
t0 = time.perf_counter_ns()
|
||||
fn()
|
||||
samples.append(time.perf_counter_ns() - t0)
|
||||
samples.sort()
|
||||
return samples[len(samples) // 2]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Wickra Python throughput benchmark")
|
||||
parser.add_argument("--bars", type=int, default=200_000, help="number of synthetic bars")
|
||||
args = parser.parse_args()
|
||||
bars = args.bars if args.bars >= 1000 else 200_000
|
||||
|
||||
# Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
idx = np.arange(bars, dtype=np.float64)
|
||||
mid = 100 + np.sin(idx * 0.001) * 20 + idx * 1e-4
|
||||
close = mid + np.sin(idx * 0.05) * 2
|
||||
high = np.maximum(close, mid) + 1.5
|
||||
low = np.minimum(close, mid) - 1.5
|
||||
open_ = mid
|
||||
volume = 1000 + (idx % 97) * 13
|
||||
|
||||
close_list = close.tolist()
|
||||
# ATR streams a 6-tuple (open, high, low, close, volume, timestamp) per tick.
|
||||
candles = list(
|
||||
zip(open_.tolist(), high.tolist(), low.tolist(), close_list, volume.tolist(), range(bars))
|
||||
)
|
||||
|
||||
def mups(ns: float) -> float:
|
||||
return bars / (ns / 1e9) / 1e6
|
||||
|
||||
def sma_stream() -> None:
|
||||
ind = ta.SMA(20)
|
||||
for value in close_list:
|
||||
ind.update(value)
|
||||
|
||||
def sma_batch() -> None:
|
||||
ta.SMA(20).batch(close)
|
||||
|
||||
def atr_stream() -> None:
|
||||
ind = ta.ATR(14)
|
||||
for candle in candles:
|
||||
ind.update(candle)
|
||||
|
||||
def atr_batch() -> None:
|
||||
ta.ATR(14).batch(high, low, close)
|
||||
|
||||
def macd_stream() -> None:
|
||||
ind = ta.MACD(12, 26, 9)
|
||||
for value in close_list:
|
||||
ind.update(value)
|
||||
|
||||
# SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
||||
indicators = [
|
||||
("SMA(20)", sma_stream, sma_batch),
|
||||
("ATR(14)", atr_stream, atr_batch),
|
||||
("MACD(12,26,9)", macd_stream, None), # multi-output: streaming only
|
||||
]
|
||||
|
||||
print(f"Wickra Python throughput - {bars:,} bars (median of 3 runs)\n")
|
||||
print(f"{'Indicator':<22}{'streaming (Mupd/s)':>20}{'batch (Mupd/s)':>18}")
|
||||
print("-" * 60)
|
||||
for name, stream, batch in indicators:
|
||||
stream_mups = f"{mups(_time_ns(stream)):.1f}"
|
||||
batch_mups = "-" if batch is None else f"{mups(_time_ns(batch)):.1f}"
|
||||
print(f"{name:<22}{stream_mups:>20}{batch_mups:>18}")
|
||||
|
||||
print(
|
||||
"\nMupd/s = million indicator updates per second. Streaming is the per-tick\n"
|
||||
"`update` path crossing the Python<->Rust boundary once per value; batch is\n"
|
||||
"the bulk numpy path (one boundary crossing). Higher is better. Numbers are\n"
|
||||
"machine-dependent - use them for relative comparison, not as a speed claim."
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "wickra"
|
||||
version = "0.7.9"
|
||||
version = "0.8.5"
|
||||
description = "Streaming-first technical indicators: incremental, fast, install-free."
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
^src/wickra\.dll$
|
||||
^src/wickra\.so$
|
||||
^src/symbols\.rds$
|
||||
^src/wickra\.h$
|
||||
^src/libwickra\.(so|dylib)$
|
||||
^src/wickra-c\.tar\.gz$
|
||||
^src/wickra-c$
|
||||
^src/Makevars$
|
||||
^\.gitignore$
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Package: wickra
|
||||
Type: Package
|
||||
Title: Streaming-First Technical Indicators
|
||||
Version: 0.7.9
|
||||
Version: 0.8.5
|
||||
Authors@R: person("Wickra contributors", role = c("aut", "cre"), email = "support@wickra.org")
|
||||
Description: R bindings for the Wickra technical-analysis library over its C ABI
|
||||
hub. Exposes 514 indicators, each an O(1) streaming state machine shared with
|
||||
@@ -12,8 +12,10 @@ URL: https://github.com/wickra-lib/wickra, https://docs.wickra.org
|
||||
BugReports: https://github.com/wickra-lib/wickra/issues
|
||||
Encoding: UTF-8
|
||||
NeedsCompilation: yes
|
||||
SystemRequirements: the Wickra C ABI library (libwickra); set WICKRA_INCLUDE_DIR
|
||||
and WICKRA_LIB_DIR when installing from source.
|
||||
SystemRequirements: the Wickra C ABI shared library, downloaded automatically at
|
||||
install time from the matching GitHub release and bundled into the package.
|
||||
Set WICKRA_INCLUDE_DIR and WICKRA_LIB_DIR to build against a locally built C
|
||||
ABI instead (e.g. after `cargo build -p wickra-c --release`).
|
||||
Roxygen: list(markdown = TRUE)
|
||||
Suggests: testthat (>= 3.0.0)
|
||||
Config/testthat/edition: 3
|
||||
|
||||
@@ -59,6 +59,17 @@ indicators take the OHLCV fields plus a timestamp, e.g.
|
||||
`update(atr, open, high, low, close, volume, timestamp)`. The native handle is
|
||||
freed automatically when the object is garbage-collected.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/throughput.R` 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
|
||||
Rscript benchmarks/throughput.R
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in the
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env Rscript
|
||||
#
|
||||
# Throughput benchmark for the Wickra R bindings.
|
||||
#
|
||||
# Measures how many indicator updates per second the R binding sustains, both
|
||||
# per-tick (streaming `update`) and bulk (`batch`), over a synthetic OHLCV
|
||||
# series. It is the R counterpart of the Node `throughput.js` and the Rust
|
||||
# criterion benches: it benchmarks Wickra's own O(1) streaming engine across
|
||||
# the R<->C-ABI boundary (there is no comparable streaming TA library on CRAN
|
||||
# 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).
|
||||
#
|
||||
# Install the package first (it links the C ABI; see bindings/r/README.md),
|
||||
# then run:
|
||||
#
|
||||
# Rscript bindings/r/benchmarks/throughput.R # 200k bars (default)
|
||||
# Rscript bindings/r/benchmarks/throughput.R --bars 1000000
|
||||
|
||||
suppressMessages(library(wickra))
|
||||
|
||||
parse_bars <- function() {
|
||||
args <- commandArgs(trailingOnly = TRUE)
|
||||
i <- match("--bars", args)
|
||||
if (!is.na(i) && length(args) >= i + 1L) {
|
||||
n <- suppressWarnings(as.integer(args[i + 1L]))
|
||||
if (!is.na(n) && n >= 1000L) {
|
||||
return(n)
|
||||
}
|
||||
stop("--bars must be an integer >= 1000")
|
||||
}
|
||||
200000L
|
||||
}
|
||||
|
||||
bars <- parse_bars()
|
||||
|
||||
# Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
idx <- seq.int(0L, bars - 1L)
|
||||
mid <- 100 + sin(idx * 0.001) * 20 + idx * 1e-4
|
||||
close <- mid + sin(idx * 0.05) * 2
|
||||
high <- pmax(close, mid) + 1.5
|
||||
low <- pmin(close, mid) - 1.5
|
||||
open <- mid
|
||||
volume <- 1000 + (idx %% 97L) * 13
|
||||
# `numeric` (double), not integer: the candle batch path coerces the timestamp
|
||||
# column with REAL(), which rejects an integer vector.
|
||||
timestamp <- as.numeric(idx)
|
||||
|
||||
# Median elapsed-ns over a few repetitions, after one warmup pass.
|
||||
time_ns <- function(fn, reps = 3L) {
|
||||
fn() # warmup
|
||||
samples <- numeric(reps)
|
||||
for (r in seq_len(reps)) {
|
||||
t0 <- Sys.time()
|
||||
fn()
|
||||
samples[r] <- as.numeric(Sys.time() - t0, units = "secs") * 1e9
|
||||
}
|
||||
median(samples)
|
||||
}
|
||||
|
||||
mups_from_ns <- function(ns) bars / (ns / 1e9) / 1e6
|
||||
|
||||
# SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
||||
indicators <- list(
|
||||
list(
|
||||
name = "SMA(20)",
|
||||
stream = function() {
|
||||
ind <- Sma(20)
|
||||
for (i in seq_len(bars)) update(ind, close[i])
|
||||
},
|
||||
batch = function() {
|
||||
batch(Sma(20), close)
|
||||
}
|
||||
),
|
||||
list(
|
||||
name = "ATR(14)",
|
||||
stream = function() {
|
||||
ind <- Atr(14)
|
||||
for (i in seq_len(bars)) {
|
||||
update(ind, open[i], high[i], low[i], close[i], volume[i], timestamp[i])
|
||||
}
|
||||
},
|
||||
batch = function() {
|
||||
batch(Atr(14), open, high, low, close, volume, timestamp)
|
||||
}
|
||||
),
|
||||
list(
|
||||
name = "MACD(12,26,9)",
|
||||
stream = function() {
|
||||
ind <- MacdIndicator(12, 26, 9)
|
||||
for (i in seq_len(bars)) update(ind, close[i])
|
||||
},
|
||||
batch = NULL # multi-output: streaming only
|
||||
)
|
||||
)
|
||||
|
||||
cat(sprintf(
|
||||
"Wickra R throughput - %s bars (median of 3 runs)\n\n",
|
||||
format(bars, big.mark = ",")
|
||||
))
|
||||
cat(sprintf("%-22s%20s%18s\n", "Indicator", "streaming (Mupd/s)", "batch (Mupd/s)"))
|
||||
cat(strrep("-", 60), "\n", sep = "")
|
||||
|
||||
for (ind in indicators) {
|
||||
stream_mups <- sprintf("%.1f", mups_from_ns(time_ns(ind$stream)))
|
||||
batch_mups <- if (is.null(ind$batch)) "-" else sprintf("%.1f", mups_from_ns(time_ns(ind$batch)))
|
||||
cat(sprintf("%-22s%20s%18s\n", ind$name, stream_mups, batch_mups))
|
||||
}
|
||||
|
||||
cat(paste0(
|
||||
"\nMupd/s = million indicator updates per second. Streaming is the per-tick\n",
|
||||
"`update` path crossing the R<->C-ABI boundary once per value; batch is the\n",
|
||||
"bulk vector path (one boundary crossing). Higher is better. Numbers are\n",
|
||||
"machine-dependent - use them for relative comparison, not as a speed claim.\n"
|
||||
))
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/sh
|
||||
# Resolve the Wickra C ABI (header + shared library) for the Unix build.
|
||||
#
|
||||
# Self-contained by default: download the prebuilt wickra-c-<triple>.tar.gz
|
||||
# release asset that matches this package's version and stage the shared library
|
||||
# into src/ so the compiled wickra.so links against it and bundles it (via
|
||||
# install.libs.R), found post-install through an rpath ($ORIGIN on Linux,
|
||||
# @loader_path on macOS). Set WICKRA_INCLUDE_DIR + WICKRA_LIB_DIR to build
|
||||
# against a locally built C ABI instead (dev override; e.g.
|
||||
# `cargo build -p wickra-c --release`). Download/extract uses base R (always
|
||||
# present at build time), so there is no curl/wget system dependency.
|
||||
set -e
|
||||
|
||||
inc=""
|
||||
lib=""
|
||||
|
||||
# WebAssembly (r-universe / webR): there is no prebuilt wasm C ABI to download,
|
||||
# but the build image ships cargo (/usr/local/cargo/bin) and emscripten
|
||||
# (EMSDK on PATH), so build the C ABI staticlib from source for
|
||||
# wasm32-unknown-emscripten right here. Building it in the same image with the
|
||||
# same emscripten avoids any ABI/version mismatch a prebuilt lib would risk.
|
||||
# rayon (threads) is dropped via --no-default-features; the indicators are pure
|
||||
# computation, so the serial path is functionally identical.
|
||||
if [ "$(uname -s)" = "Emscripten" ]; then
|
||||
version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION)
|
||||
echo "wickra: building C ABI from source for wasm32-unknown-emscripten (v${version})"
|
||||
build=$(mktemp -d)
|
||||
url="https://github.com/wickra-lib/wickra/archive/refs/tags/v${version}.tar.gz"
|
||||
"${R_HOME}/bin/Rscript" -e "download.file('${url}', '${build}/src.tar.gz', mode = 'wb', quiet = TRUE)" \
|
||||
|| { echo "wickra: failed to download source ${url}"; exit 1; }
|
||||
"${R_HOME}/bin/Rscript" -e "untar('${build}/src.tar.gz', exdir = '${build}')"
|
||||
root="${build}/wickra-${version}"
|
||||
rustup target add wasm32-unknown-emscripten 2>/dev/null || true
|
||||
( cd "${root}" && cargo build -p wickra-c --release \
|
||||
--target wasm32-unknown-emscripten --no-default-features ) \
|
||||
|| { echo "wickra: cargo wasm build failed"; exit 1; }
|
||||
cp "${root}/bindings/c/include/wickra.h" src/wickra.h
|
||||
cp "${root}/target/wasm32-unknown-emscripten/release/libwickra.a" src/libwickra.a
|
||||
rm -rf "${build}"
|
||||
# The static archive is linked into the package object; no shared lib to
|
||||
# bundle and no rpath needed.
|
||||
sed "s|@WICKRA_RPATH@||" src/Makevars.in > src/Makevars
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -n "${WICKRA_INCLUDE_DIR}" ] && [ -n "${WICKRA_LIB_DIR}" ]; then
|
||||
echo "wickra: using C ABI from WICKRA_INCLUDE_DIR / WICKRA_LIB_DIR (dev override)"
|
||||
inc="${WICKRA_INCLUDE_DIR}"
|
||||
lib="${WICKRA_LIB_DIR}"
|
||||
else
|
||||
version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION)
|
||||
os=$(uname -s)
|
||||
arch=$(uname -m)
|
||||
case "${os}" in
|
||||
Linux)
|
||||
case "${arch}" in
|
||||
x86_64) triple="x86_64-unknown-linux-gnu" ;;
|
||||
aarch64 | arm64) triple="aarch64-unknown-linux-gnu" ;;
|
||||
*) echo "wickra: unsupported Linux arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
Darwin)
|
||||
case "${arch}" in
|
||||
x86_64) triple="x86_64-apple-darwin" ;;
|
||||
arm64 | aarch64) triple="aarch64-apple-darwin" ;;
|
||||
*) echo "wickra: unsupported macOS arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
echo "wickra: unsupported OS '${os}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
url="https://github.com/wickra-lib/wickra/releases/download/v${version}/wickra-c-${triple}.tar.gz"
|
||||
echo "wickra: downloading C ABI ${triple} for v${version}"
|
||||
"${R_HOME}/bin/Rscript" -e "download.file('${url}', 'src/wickra-c.tar.gz', mode = 'wb', quiet = TRUE)" \
|
||||
|| { echo "wickra: failed to download ${url}"; exit 1; }
|
||||
"${R_HOME}/bin/Rscript" -e "untar('src/wickra-c.tar.gz', exdir = 'src/wickra-c')"
|
||||
inc="src/wickra-c/wickra-c-${triple}/include"
|
||||
lib="src/wickra-c/wickra-c-${triple}/lib"
|
||||
fi
|
||||
|
||||
# Stage the header and the platform shared library into src/ so the package
|
||||
# object links with -L. -lwickra and install.libs.R bundles the lib.
|
||||
cp "${inc}/wickra.h" src/wickra.h
|
||||
staged=""
|
||||
for f in libwickra.so libwickra.dylib; do
|
||||
if [ -f "${lib}/${f}" ]; then
|
||||
cp "${lib}/${f}" "src/${f}"
|
||||
staged="${f}"
|
||||
fi
|
||||
done
|
||||
if [ -z "${staged}" ]; then
|
||||
echo "wickra: no libwickra.so/.dylib found under ${lib}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# rpath so the bundled lib is found next to wickra.so after install.
|
||||
case "$(uname -s)" in
|
||||
Darwin)
|
||||
rpath="-Wl,-rpath,@loader_path"
|
||||
# Normalise the dylib id to @rpath so @loader_path resolution applies.
|
||||
install_name_tool -id "@rpath/${staged}" "src/${staged}" 2>/dev/null || true
|
||||
;;
|
||||
*)
|
||||
rpath="-Wl,-rpath,'\$\$ORIGIN'"
|
||||
;;
|
||||
esac
|
||||
|
||||
sed "s|@WICKRA_RPATH@|${rpath}|" src/Makevars.in > src/Makevars
|
||||
echo "wickra: configured (bundled ${staged})"
|
||||
exit 0
|
||||
@@ -4,9 +4,35 @@
|
||||
# itself). Stage a renamed copy, wickra_abi.dll, into src/ and build a mingw
|
||||
# import library that references it by that name (objdump + dlltool, both shipped
|
||||
# with Rtools — no gendef/pexports needed). install.libs.R then bundles the DLL.
|
||||
#
|
||||
# Self-contained by default: download the prebuilt wickra-c-<triple>.tar.gz
|
||||
# release asset matching this package's version. Set WICKRA_INCLUDE_DIR +
|
||||
# WICKRA_LIB_DIR to build against a locally built C ABI instead (dev override).
|
||||
set -e
|
||||
: "${WICKRA_LIB_DIR:?set WICKRA_LIB_DIR to the directory containing wickra.dll}"
|
||||
cp "${WICKRA_LIB_DIR}/wickra.dll" src/wickra_abi.dll
|
||||
|
||||
if [ -n "${WICKRA_INCLUDE_DIR}" ] && [ -n "${WICKRA_LIB_DIR}" ]; then
|
||||
echo "wickra: using C ABI from WICKRA_INCLUDE_DIR / WICKRA_LIB_DIR (dev override)"
|
||||
inc="${WICKRA_INCLUDE_DIR}"
|
||||
lib="${WICKRA_LIB_DIR}"
|
||||
else
|
||||
version=$(sed -n 's/^Version:[[:space:]]*//p' DESCRIPTION)
|
||||
arch=$(uname -m)
|
||||
case "${arch}" in
|
||||
x86_64) triple="x86_64-pc-windows-msvc" ;;
|
||||
aarch64 | arm64) triple="aarch64-pc-windows-msvc" ;;
|
||||
*) echo "wickra: unsupported Windows arch '${arch}' — set WICKRA_INCLUDE_DIR/WICKRA_LIB_DIR"; exit 1 ;;
|
||||
esac
|
||||
url="https://github.com/wickra-lib/wickra/releases/download/v${version}/wickra-c-${triple}.tar.gz"
|
||||
echo "wickra: downloading C ABI ${triple} for v${version}"
|
||||
"${R_HOME}/bin/Rscript" -e "download.file('${url}', 'src/wickra-c.tar.gz', mode = 'wb', quiet = TRUE)" \
|
||||
|| { echo "wickra: failed to download ${url}"; exit 1; }
|
||||
"${R_HOME}/bin/Rscript" -e "untar('src/wickra-c.tar.gz', exdir = 'src/wickra-c')"
|
||||
inc="src/wickra-c/wickra-c-${triple}/include"
|
||||
lib="src/wickra-c/wickra-c-${triple}/lib"
|
||||
fi
|
||||
|
||||
cp "${inc}/wickra.h" src/wickra.h
|
||||
cp "${lib}/wickra.dll" src/wickra_abi.dll
|
||||
{
|
||||
echo 'LIBRARY wickra_abi.dll'
|
||||
echo 'EXPORTS'
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
# Build artifacts and the configure.win-generated C ABI import shims.
|
||||
# Build artifacts and the configure-staged C ABI header / library / downloads.
|
||||
*.o
|
||||
*.so
|
||||
*.dll
|
||||
*.dll.a
|
||||
*.dylib
|
||||
wickra.h
|
||||
wickra_abi.def
|
||||
symbols.rds
|
||||
wickra-c.tar.gz
|
||||
wickra-c/
|
||||
# configure generates Makevars from Makevars.in (Makevars.in + Makevars.win stay tracked).
|
||||
/Makevars
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# The Wickra C ABI header and library are not vendored; point the build at them
|
||||
# via WICKRA_INCLUDE_DIR (containing wickra.h) and WICKRA_LIB_DIR (containing
|
||||
# libwickra). Build the library first with: cargo build -p wickra-c --release.
|
||||
PKG_CPPFLAGS = -I$(WICKRA_INCLUDE_DIR)
|
||||
PKG_LIBS = -L$(WICKRA_LIB_DIR) -lwickra -Wl,-rpath,$(WICKRA_LIB_DIR)
|
||||
@@ -0,0 +1,6 @@
|
||||
# Generated into src/Makevars by ../configure (the @WICKRA_RPATH@ token is
|
||||
# substituted per-OS: $ORIGIN on Linux, @loader_path on macOS). The C ABI header
|
||||
# (wickra.h) and shared library (libwickra.so/.dylib) are staged into src/ by
|
||||
# configure, so the package object links against the bundled lib.
|
||||
PKG_CPPFLAGS = -I.
|
||||
PKG_LIBS = -L. -lwickra @WICKRA_RPATH@
|
||||
@@ -1,5 +1,5 @@
|
||||
# Link the import library built by configure.win (references wickra_abi.dll, not
|
||||
# the package's own wickra.dll). The C ABI header dir is passed via
|
||||
# WICKRA_INCLUDE_DIR at build time.
|
||||
PKG_CPPFLAGS = -I$(WICKRA_INCLUDE_DIR)
|
||||
# the package's own wickra.dll). The C ABI header (wickra.h) is staged into src/
|
||||
# by configure.win, so the include path is the source dir itself.
|
||||
PKG_CPPFLAGS = -I.
|
||||
PKG_LIBS = libwickra_abi.dll.a
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
# Install the compiled package shared object plus, on Windows, the bundled
|
||||
# wickra_abi.dll (matched by the *.dll glob) so the loader can resolve the import
|
||||
# from the package's own libs directory (see .onLoad, which puts it on PATH).
|
||||
files <- Sys.glob(paste0("*", SHLIB_EXT))
|
||||
# Install the compiled package shared object plus the bundled C ABI library so
|
||||
# the package is self-contained: on Windows the wickra_abi.dll (matched by the
|
||||
# *.dll glob, loaded via PATH in .onLoad); on Linux the libwickra.so (matched by
|
||||
# the *.so SHLIB_EXT glob); on macOS the libwickra.dylib (added explicitly, since
|
||||
# R package objects use the .so extension there too). The Unix rpath baked by
|
||||
# configure ($ORIGIN / @loader_path) resolves it from this libs directory.
|
||||
files <- unique(c(Sys.glob(paste0("*", SHLIB_EXT)), Sys.glob("libwickra.dylib")))
|
||||
dest <- file.path(R_PACKAGE_DIR, paste0("libs", R_ARCH))
|
||||
dir.create(dest, recursive = TRUE, showWarnings = FALSE)
|
||||
file.copy(files, dest, overwrite = TRUE)
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Golden-fixture parity: replay the shared testdata/golden input series through
|
||||
# the R FFI and assert every value matches the Rust reference output. Where the
|
||||
# archetype test only checks finiteness, this pins exact values, catching wiring
|
||||
# bugs (swapped params, wrong multi-output field). Fixtures are generated by
|
||||
# `cargo run -p wickra-examples --bin gen_golden`.
|
||||
#
|
||||
# The fixtures live at the repository root (testdata/golden) and are present
|
||||
# during the monorepo test run, but they are NOT bundled into the standalone R
|
||||
# package. A packaged check (r-universe / CRAN) therefore cannot find them, so
|
||||
# these tests skip there; the parity is already enforced by the repository CI.
|
||||
|
||||
find_golden_dir <- function() {
|
||||
d <- normalizePath(getwd(), winslash = "/", mustWork = FALSE)
|
||||
repeat {
|
||||
g <- file.path(d, "testdata", "golden")
|
||||
if (dir.exists(g)) return(g)
|
||||
parent <- dirname(d)
|
||||
if (identical(parent, d)) return(NULL)
|
||||
d <- parent
|
||||
}
|
||||
}
|
||||
|
||||
golden_dir <- find_golden_dir()
|
||||
|
||||
skip_if_no_golden <- function() {
|
||||
skip_if(is.null(golden_dir), "golden fixtures not bundled with the package")
|
||||
}
|
||||
|
||||
read_golden <- function(name) {
|
||||
read.csv(file.path(golden_dir, paste0(name, ".csv")),
|
||||
colClasses = "character", check.names = FALSE)
|
||||
}
|
||||
|
||||
read_golden_input <- function() read.csv(file.path(golden_dir, "input.csv"))
|
||||
|
||||
gcell <- function(s) if (identical(s, "nan")) NA_real_ else as.numeric(s)
|
||||
|
||||
expect_close <- function(got, want, row, field) {
|
||||
if (is.na(want)) {
|
||||
expect_true(is.na(got), info = paste("row", row, field, "expected warmup/NA"))
|
||||
} else {
|
||||
tol <- 1e-6 * max(1, abs(want))
|
||||
expect_lte(abs(got - want), tol, label = paste("row", row, field, "got", got, "want", want))
|
||||
}
|
||||
}
|
||||
|
||||
test_that("scalar indicators match golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
specs <- list(c("sma", 14), c("ema", 14), c("rsi", 14))
|
||||
for (spec in specs) {
|
||||
name <- spec[[1]]
|
||||
ind <- switch(name, sma = Sma(14), ema = Ema(14), rsi = Rsi(14))
|
||||
exp <- read_golden(name)
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
got <- update(ind, golden_input$close[i])
|
||||
expect_close(got, gcell(exp[i, 1]), i, name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test_that("candle Atr matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
atr <- Atr(14)
|
||||
exp <- read_golden("atr")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
got <- update(atr, golden_input$open[i], golden_input$high[i], golden_input$low[i],
|
||||
golden_input$close[i], golden_input$volume[i], i - 1)
|
||||
expect_close(got, gcell(exp[i, 1]), i, "atr")
|
||||
}
|
||||
})
|
||||
|
||||
test_that("pairwise Beta matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
beta <- Beta(20)
|
||||
exp <- read_golden("beta")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
# generator fed (close, open)
|
||||
got <- update(beta, golden_input$close[i], golden_input$open[i])
|
||||
expect_close(got, gcell(exp[i, 1]), i, "beta")
|
||||
}
|
||||
})
|
||||
|
||||
test_that("multi-output MACD matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
macd <- MacdIndicator(12, 26, 9)
|
||||
exp <- read_golden("macd")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
out <- update(macd, golden_input$close[i])
|
||||
if (identical(exp[i, "macd"], "nan")) {
|
||||
expect_true(all(is.na(out)), info = paste("row", i, "macd warmup"))
|
||||
} else {
|
||||
expect_close(out[["macd"]], gcell(exp[i, "macd"]), i, "macd.macd")
|
||||
expect_close(out[["signal"]], gcell(exp[i, "signal"]), i, "macd.signal")
|
||||
expect_close(out[["histogram"]], gcell(exp[i, "histogram"]), i, "macd.histogram")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test_that("multi-output ADX matches golden", {
|
||||
skip_if_no_golden()
|
||||
golden_input <- read_golden_input()
|
||||
adx <- Adx(14)
|
||||
exp <- read_golden("adx")
|
||||
for (i in seq_len(nrow(golden_input))) {
|
||||
out <- update(adx, golden_input$open[i], golden_input$high[i], golden_input$low[i],
|
||||
golden_input$close[i], golden_input$volume[i], i - 1)
|
||||
if (identical(exp[i, "plus_di"], "nan")) {
|
||||
expect_true(all(is.na(out)), info = paste("row", i, "adx warmup"))
|
||||
} else {
|
||||
expect_close(out[["plus_di"]], gcell(exp[i, "plus_di"]), i, "adx.plus_di")
|
||||
expect_close(out[["minus_di"]], gcell(exp[i, "minus_di"]), i, "adx.minus_di")
|
||||
expect_close(out[["adx"]], gcell(exp[i, "adx"]), i, "adx.adx")
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -46,6 +46,18 @@ Constructors mirror the other bindings (`new SMA(20)`, `new MACD(12, 26, 9)`,
|
||||
`new BollingerBands(20, 2.0)`, …); `update()` returns the latest value or
|
||||
`null` while the indicator is still warming up.
|
||||
|
||||
## Benchmark
|
||||
|
||||
`benchmarks/throughput.mjs` 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
|
||||
wasm-pack build --target nodejs --out-dir pkg-node --release
|
||||
node benchmarks/throughput.mjs
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The full indicator catalogue, guides, quickstarts, and API reference live in
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
// Throughput benchmark for the Wickra WebAssembly bindings.
|
||||
//
|
||||
// Measures how many indicator updates per second the wasm binding sustains,
|
||||
// both per-tick (streaming `update`) and bulk (`batch`), over a synthetic
|
||||
// OHLCV series. It is the wasm counterpart of the Node `throughput.js` and the
|
||||
// Rust criterion benches: it benchmarks Wickra's own O(1) streaming engine
|
||||
// across the JS<->wasm boundary (there is no install-free TA library with a
|
||||
// comparable surface 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 (the algorithm is identical to the Rust core; only the
|
||||
// boundary cost differs): 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).
|
||||
//
|
||||
// Build the nodejs-target package first (needs the wasm32-unknown-unknown
|
||||
// target, i.e. a rustup toolchain), then run:
|
||||
//
|
||||
// cd bindings/wasm && wasm-pack build --target nodejs --out-dir pkg-node --release
|
||||
// node benchmarks/throughput.mjs # 200k bars (default)
|
||||
// node benchmarks/throughput.mjs --bars 1000000
|
||||
|
||||
import { createRequire } from 'node:module';
|
||||
import { hrtime } from 'node:process';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
// wasm-pack --target nodejs emits a CommonJS module named after the crate.
|
||||
const wasm = require('../pkg-node/wickra_wasm.js');
|
||||
const { SMA, ATR, MACD } = wasm;
|
||||
|
||||
function parseBars() {
|
||||
const idx = process.argv.indexOf('--bars');
|
||||
if (idx !== -1 && process.argv[idx + 1]) {
|
||||
const n = Number(process.argv[idx + 1]);
|
||||
if (Number.isFinite(n) && n >= 1000) return Math.floor(n);
|
||||
console.error('--bars must be a number >= 1000');
|
||||
process.exit(1);
|
||||
}
|
||||
return 200_000;
|
||||
}
|
||||
|
||||
const BARS = parseBars();
|
||||
|
||||
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
|
||||
const close = new Float64Array(BARS);
|
||||
const high = new Float64Array(BARS);
|
||||
const low = new Float64Array(BARS);
|
||||
for (let i = 0; i < BARS; i++) {
|
||||
const mid = 100 + Math.sin(i * 0.001) * 20 + i * 1e-4;
|
||||
close[i] = mid + Math.sin(i * 0.05) * 2;
|
||||
high[i] = Math.max(close[i], mid) + 1.5;
|
||||
low[i] = Math.min(close[i], mid) - 1.5;
|
||||
}
|
||||
|
||||
// Median elapsed-ns over a few repetitions, after one warmup pass.
|
||||
function timeNs(fn, reps = 3) {
|
||||
fn(); // warmup (JIT + cache)
|
||||
const samples = [];
|
||||
for (let r = 0; r < reps; r++) {
|
||||
const t0 = hrtime.bigint();
|
||||
fn();
|
||||
samples.push(Number(hrtime.bigint() - t0));
|
||||
}
|
||||
samples.sort((a, b) => a - b);
|
||||
return samples[Math.floor(samples.length / 2)];
|
||||
}
|
||||
|
||||
function mupsFromNs(ns) {
|
||||
return BARS / (ns / 1e9) / 1e6; // million updates per second
|
||||
}
|
||||
|
||||
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
|
||||
const indicators = [
|
||||
{
|
||||
name: 'SMA(20)',
|
||||
stream: () => {
|
||||
const ind = new SMA(20);
|
||||
for (let i = 0; i < BARS; i++) ind.update(close[i]);
|
||||
ind.free();
|
||||
},
|
||||
batch: () => {
|
||||
const ind = new SMA(20);
|
||||
ind.batch(close);
|
||||
ind.free();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ATR(14)',
|
||||
stream: () => {
|
||||
const ind = new ATR(14);
|
||||
for (let i = 0; i < BARS; i++) ind.update(high[i], low[i], close[i]);
|
||||
ind.free();
|
||||
},
|
||||
batch: () => {
|
||||
const ind = new ATR(14);
|
||||
ind.batch(high, low, close);
|
||||
ind.free();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'MACD(12,26,9)',
|
||||
stream: () => {
|
||||
const ind = new MACD(12, 26, 9);
|
||||
for (let i = 0; i < BARS; i++) ind.update(close[i]);
|
||||
ind.free();
|
||||
},
|
||||
batch: null, // multi-output: streaming only
|
||||
},
|
||||
];
|
||||
|
||||
console.log(`Wickra WASM throughput — ${BARS.toLocaleString('en-US')} bars (median of 3 runs)\n`);
|
||||
console.log(`${'Indicator'.padEnd(22)}${'streaming (Mupd/s)'.padStart(20)}${'batch (Mupd/s)'.padStart(18)}`);
|
||||
console.log('-'.repeat(60));
|
||||
|
||||
for (const ind of indicators) {
|
||||
const streamMups = mupsFromNs(timeNs(ind.stream)).toFixed(1);
|
||||
const batchMups = ind.batch ? mupsFromNs(timeNs(ind.batch)).toFixed(1) : '—';
|
||||
console.log(`${ind.name.padEnd(22)}${streamMups.padStart(20)}${batchMups.padStart(18)}`);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'\nMupd/s = million indicator updates per second. Streaming is the per-tick\n' +
|
||||
'`update` path crossing the JS<->wasm boundary once per value; batch is the\n' +
|
||||
'bulk array path (one boundary crossing). Higher is better. Numbers are\n' +
|
||||
'machine-dependent — use them for relative comparison, not as a speed claim.',
|
||||
);
|
||||
@@ -84,6 +84,9 @@ impl Indicator for Autocorrelation {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -92,6 +92,9 @@ impl Indicator for Beta {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let (oa, ob) = self.window.pop_front().expect("non-empty");
|
||||
self.sum_a -= oa;
|
||||
@@ -225,4 +228,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut b = Beta::new(3).unwrap();
|
||||
assert_eq!(b.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(b.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(b.update((1.0, 2.0)), None);
|
||||
assert_eq!(b.update((2.0, 5.0)), None);
|
||||
assert!(b.update((3.0, 7.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,9 @@ impl Indicator for BetaNeutralSpread {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let (oa, ob) = self.window.pop_front().expect("non-empty");
|
||||
self.sum_a -= oa;
|
||||
@@ -244,4 +247,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| s.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut s = BetaNeutralSpread::new(3).unwrap();
|
||||
assert_eq!(s.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(s.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(s.update((1.0, 2.0)), None);
|
||||
assert_eq!(s.update((2.0, 5.0)), None);
|
||||
assert!(s.update((3.0, 7.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +106,9 @@ impl Indicator for BomarBands {
|
||||
type Output = BomarBandsOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<BomarBandsOutput> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ impl Indicator for Cfo {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: f64) -> Option<f64> {
|
||||
if !input.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let forecast = self.linreg.update(input)?;
|
||||
// Hold the previous value if the close is zero — the percentage form
|
||||
// is undefined and a return of inf would propagate badly.
|
||||
|
||||
@@ -71,6 +71,9 @@ impl Indicator for CoefficientOfVariation {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
self.sum -= old;
|
||||
|
||||
@@ -116,6 +116,9 @@ impl Indicator for Cointegration {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<CointegrationOutput> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let (oa, ob) = self.window.pop_front().expect("non-empty");
|
||||
self.sum_a -= oa;
|
||||
@@ -443,4 +446,16 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| c.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut c = Cointegration::new(4, 0).unwrap();
|
||||
assert_eq!(c.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(c.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(c.update((1.0, 2.0)), None);
|
||||
assert_eq!(c.update((2.0, 5.0)), None);
|
||||
assert_eq!(c.update((3.0, 7.0)), None);
|
||||
assert!(c.update((4.0, 11.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ impl Indicator for DetrendedStdDev {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let y0 = self.window.pop_front().expect("non-empty");
|
||||
self.sum_xy = self.sum_xy - self.sum_y + y0;
|
||||
|
||||
@@ -76,6 +76,9 @@ impl Indicator for DistanceSsd {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
if !input.0.is_finite() || !input.1.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -232,4 +235,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| d.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut d = DistanceSsd::new(3).unwrap();
|
||||
assert_eq!(d.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(d.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(d.update((1.0, 1.0)), None);
|
||||
assert_eq!(d.update((2.0, 4.0)), None);
|
||||
assert!(d.update((3.0, 9.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ impl Indicator for Expectancy {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, ret: f64) -> Option<f64> {
|
||||
if !ret.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("window is non-empty");
|
||||
self.sum -= old;
|
||||
|
||||
@@ -96,6 +96,9 @@ impl Indicator for GrangerCausality {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
if !input.0.is_finite() || !input.1.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -332,4 +335,16 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| g.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut g = GrangerCausality::new(5, 1).unwrap();
|
||||
assert_eq!(g.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(g.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
for t in 0..4 {
|
||||
assert_eq!(g.update((f64::from(t), f64::from(t) * 0.5)), None);
|
||||
}
|
||||
assert!(g.update((4.0, 2.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,9 @@ impl Indicator for HasbrouckInformationShare {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (x, y) = input;
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let Some((px, py)) = self.prev else {
|
||||
self.prev = Some((x, y));
|
||||
return None;
|
||||
@@ -248,4 +251,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut h = HasbrouckInformationShare::new(2).unwrap();
|
||||
assert_eq!(h.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(h.update((1.0, f64::INFINITY)), None);
|
||||
// First finite tick seeds prev; two more returns fill the window.
|
||||
assert_eq!(h.update((1.0, 1.0)), None);
|
||||
assert_eq!(h.update((2.0, 3.0)), None);
|
||||
assert!(h.update((3.0, 4.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +138,9 @@ impl Indicator for HurstExponent {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ impl Indicator for KalmanHedgeRatio {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<KalmanHedgeRatioOutput> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
// Predicted state covariance: add the transition noise to the diagonal
|
||||
// (the very first observation starts from a zero prior).
|
||||
let mut cov_pred = self.cov;
|
||||
|
||||
@@ -130,6 +130,9 @@ impl Indicator for KendallTau {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
if !input.0.is_finite() || !input.1.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -293,4 +296,14 @@ mod tests {
|
||||
let v = k.update((3.0, 3.0)).unwrap();
|
||||
assert!((-1.0..=1.0).contains(&v), "got {v}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut k = KendallTau::new(2).unwrap();
|
||||
assert_eq!(k.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(k.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(k.update((1.0, 2.0)), None);
|
||||
assert!(k.update((2.0, 5.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ impl Indicator for Kurtosis {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
let sq = old * old;
|
||||
|
||||
@@ -152,6 +152,9 @@ impl Indicator for LeadLagCrossCorrelation {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<LeadLagCrossCorrelationOutput> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.a_buf.len() == self.len {
|
||||
self.a_buf.pop_front();
|
||||
self.b_buf.pop_front();
|
||||
@@ -321,4 +324,17 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| ll.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
// len = window + 2*max_lag = 2 + 2 = 4 finite ticks fill the buffers.
|
||||
let mut ll = LeadLagCrossCorrelation::new(2, 1).unwrap();
|
||||
assert_eq!(ll.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(ll.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(ll.update((1.0, 2.0)), None);
|
||||
assert_eq!(ll.update((2.0, 1.0)), None);
|
||||
assert_eq!(ll.update((3.0, 4.0)), None);
|
||||
assert!(ll.update((4.0, 2.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,9 @@ impl Indicator for LinearRegression {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
// Sliding phase: pop the oldest, then shift every remaining index
|
||||
// down by 1 in the running `sum_xy`. The identity
|
||||
|
||||
@@ -57,6 +57,9 @@ impl Indicator for LinRegAngle {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
self.slope.update(value).map(|s| s.atan().to_degrees())
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,9 @@ impl Indicator for LinRegChannel {
|
||||
type Output = LinRegChannelOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<LinRegChannelOutput> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -77,6 +77,9 @@ impl Indicator for LinRegIntercept {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let y0 = self.window.pop_front().expect("non-empty");
|
||||
self.sum_xy = self.sum_xy - self.sum_y + y0;
|
||||
|
||||
@@ -87,6 +87,9 @@ impl Indicator for LinRegSlope {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
// Sliding-window identity: when the window slides one step forward
|
||||
// the indices `x` for every kept entry shift down by 1, so
|
||||
|
||||
@@ -89,6 +89,9 @@ impl Indicator for MedianAbsoluteDeviation {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -96,6 +96,9 @@ impl Indicator for MedianChannel {
|
||||
type Output = MedianChannelOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<MedianChannelOutput> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ impl Indicator for MidPoint {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -80,6 +80,9 @@ impl Indicator for OuHalfLife {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -242,4 +245,16 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| hl.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut hl = OuHalfLife::new(4).unwrap();
|
||||
assert_eq!(hl.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(hl.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(hl.update((1.0, 0.0)), None);
|
||||
assert_eq!(hl.update((2.0, 0.0)), None);
|
||||
assert_eq!(hl.update((3.0, 0.0)), None);
|
||||
assert!(hl.update((4.0, 0.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ impl Indicator for PearsonCorrelation {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (x, y) = input;
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let (ox, oy) = self.window.pop_front().expect("non-empty");
|
||||
self.sum_x -= ox;
|
||||
@@ -243,4 +246,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut p = PearsonCorrelation::new(3).unwrap();
|
||||
assert_eq!(p.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(p.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(p.update((1.0, 2.0)), None);
|
||||
assert_eq!(p.update((2.0, 5.0)), None);
|
||||
assert!(p.update((3.0, 7.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,9 @@ impl Indicator for PercentageTrailingStop {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
if !close.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let step = close.abs() * self.percent / 100.0;
|
||||
let stop = match self.prev_stop {
|
||||
Some(prev) => {
|
||||
|
||||
@@ -82,6 +82,9 @@ impl Indicator for PolarizedFractalEfficiency {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
if !close.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if let Some(prev) = self.prev_close {
|
||||
let diff = close - prev;
|
||||
let segment = diff.mul_add(diff, 1.0).sqrt();
|
||||
|
||||
@@ -80,6 +80,9 @@ impl Indicator for QuartileBands {
|
||||
type Output = QuartileBandsOutput;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<QuartileBandsOutput> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -95,6 +95,9 @@ impl Indicator for RSquared {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let y0 = self.window.pop_front().expect("non-empty");
|
||||
self.sum_xy = self.sum_xy - self.sum_y + y0;
|
||||
|
||||
@@ -74,6 +74,9 @@ impl Indicator for RenkoTrailingStop {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, close: f64) -> Option<f64> {
|
||||
if !close.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let anchor = match self.anchor {
|
||||
Some(prev) => {
|
||||
if self.long {
|
||||
|
||||
@@ -90,6 +90,9 @@ impl Indicator for RollingCorrelation {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (x, y) = input;
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let Some((px, py)) = self.prev else {
|
||||
// First level in each channel: store it, no return yet.
|
||||
self.prev = Some((x, y));
|
||||
@@ -272,4 +275,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut rc = RollingCorrelation::new(2).unwrap();
|
||||
assert_eq!(rc.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(rc.update((1.0, f64::INFINITY)), None);
|
||||
// First finite tick seeds prev; two more returns fill the window.
|
||||
assert_eq!(rc.update((1.0, 1.0)), None);
|
||||
assert_eq!(rc.update((2.0, 3.0)), None);
|
||||
assert!(rc.update((3.0, 5.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,9 @@ impl Indicator for RollingCovariance {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (x, y) = input;
|
||||
if !x.is_finite() || !y.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let Some((px, py)) = self.prev else {
|
||||
self.prev = Some((x, y));
|
||||
return None;
|
||||
@@ -240,4 +243,15 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| rc.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut rc = RollingCovariance::new(2).unwrap();
|
||||
assert_eq!(rc.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(rc.update((1.0, f64::INFINITY)), None);
|
||||
// First finite tick seeds prev; two more returns fill the window.
|
||||
assert_eq!(rc.update((1.0, 1.0)), None);
|
||||
assert_eq!(rc.update((2.0, 3.0)), None);
|
||||
assert!(rc.update((3.0, 5.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ impl Indicator for RollingIqr {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ impl Indicator for RollingPercentileRank {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -104,6 +104,9 @@ impl Indicator for RollingQuantile {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
|
||||
@@ -79,6 +79,9 @@ impl Indicator for Skewness {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, value: f64) -> Option<f64> {
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
self.sum -= old;
|
||||
|
||||
@@ -123,6 +123,9 @@ impl Indicator for SpearmanCorrelation {
|
||||
type Output = f64;
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
if !input.0.is_finite() || !input.1.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -311,4 +314,14 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| b.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut s = SpearmanCorrelation::new(2).unwrap();
|
||||
assert_eq!(s.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(s.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(s.update((1.0, 2.0)), None);
|
||||
assert!(s.update((2.0, 5.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,9 @@ impl Indicator for SpreadAr1Coefficient {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -248,4 +251,16 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| ar1.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut ar1 = SpreadAr1Coefficient::new(4).unwrap();
|
||||
assert_eq!(ar1.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(ar1.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
assert_eq!(ar1.update((1.0, 0.0)), None);
|
||||
assert_eq!(ar1.update((2.0, 0.0)), None);
|
||||
assert_eq!(ar1.update((3.0, 0.0)), None);
|
||||
assert!(ar1.update((4.0, 0.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,9 @@ impl Indicator for SpreadBollingerBands {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<SpreadBollingerBandsOutput> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
let spread = a - b;
|
||||
if self.window.len() == self.period {
|
||||
let old = self.window.pop_front().expect("non-empty");
|
||||
|
||||
@@ -84,6 +84,9 @@ impl Indicator for SpreadHurst {
|
||||
|
||||
fn update(&mut self, input: (f64, f64)) -> Option<f64> {
|
||||
let (a, b) = input;
|
||||
if !a.is_finite() || !b.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if self.window.len() == self.period {
|
||||
self.window.pop_front();
|
||||
}
|
||||
@@ -268,4 +271,16 @@ mod tests {
|
||||
let streamed: Vec<_> = pairs.iter().map(|p| h.update(*p)).collect();
|
||||
assert_eq!(batch, streamed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_finite_input_returns_none() {
|
||||
let mut h = SpreadHurst::new(8).unwrap();
|
||||
assert_eq!(h.update((f64::NAN, 1.0)), None);
|
||||
assert_eq!(h.update((1.0, f64::INFINITY)), None);
|
||||
// The rejected ticks leave no trace: a fresh window still warms up.
|
||||
for t in 0..7 {
|
||||
assert_eq!(h.update((f64::from(t), 0.0)), None);
|
||||
}
|
||||
assert!(h.update((7.0, 0.0)).is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user