Compare commits

..

7 Commits

Author SHA1 Message Date
kingchenc 3ee3fb67ec release: bump 0.8.2 -> 0.8.3 (#249)
Version bump 0.8.2 → 0.8.3, releasing the per-binding throughput benchmark work
(merged in #246).

Bumped via `ScriptHelpers/bump_version.py` across all manual touchpoints —
`Cargo.toml`, `pyproject.toml`, the Node `package.json` + 6 platform packages +
both lockfiles, the Java `pom.xml`s + README, the C# `.csproj`, the R
`DESCRIPTION` — plus `Cargo.lock` (via `cargo build`) and the `CHANGELOG.md`
`[0.8.3]` section and compare URLs.

CHANGELOG `[0.8.3]`:
- Per-binding throughput benchmarks for all 9 targets (BENCHMARKS.md §3).
- C ABI archetype test (`examples/c/archetypes.c`).

`cargo fmt`, `cargo test --workspace --all-features` (all green) and
`cargo clippy --workspace --all-targets --all-features -D warnings` pass. The
docs/webpage version strings are bumped by `sync-about.yml` on the `v*` tag —
not touched here.
2026-06-10 03:55:39 +02:00
kingchenc 3ebcb3f758 Per-binding throughput benchmarks + test-coverage gaps (#246)
Adds a `throughput` benchmark to every target and closes two small
test-coverage documentation/QA gaps. One PR, no merge of binding code beyond
the additive benchmarks and one C test.

## 1. Per-binding throughput benchmarks (all 9 targets)

Each benchmark feeds a deterministic synthetic OHLCV series through three
indicators chosen by **FFI call-signature archetype** (not algorithm — the same
Rust core runs underneath all bindings):

- `SMA(20)` — 1-in → 1-out (baseline boundary cost)
- `ATR(14)` — multi-in → 1-out (input marshalling)
- `MACD(12,26,9)` — 1-in → multi-out (output marshalling)

Streaming is timed for all three; batch for the single-output SMA and ATR
(median of 3 runs, after a warmup pass).

New: Python (PyO3), WASM, C (CMake), C# (Stopwatch), Go, Java (FFM), R, and the
Rust core baseline (`examples/rust/.../throughput.rs`, **no FFI** — the ceiling
the bindings are measured against and the value their batch paths converge
towards). Node already had `throughput.js`.

**Not a speed claim:** there is no comparable streaming TA library for C, C#,
Go, Java, R or WASM to compare against, so these are raw per-binding throughput
numbers documenting each language's FFI overhead — see BENCHMARKS.md §3. The
"Wickra is fast" claim still lives in §1/§2 (Rust core + the Python/Rust
cross-library runs).

## 2. README `## Testing`: C# and C bullets

The section listed every layer except C# and C, even though both have suites.
Adds the two missing bullets.

## 3. C archetype ctest

`examples/c/archetypes.c` drives one indicator per FFI archetype through the
real C boundary (scalar + batch==streaming, multi-output, bars, profile, array
input) plus reset, invalid-parameter and NULL-safety — the C counterpart of the
Go/R/Java archetype suites. Runs on three OSes via the existing CMake/ctest.

## Notes

- Benchmarks are not CI-gated (manual-run scripts, like the existing
  `throughput.js`); no `ci.yml`/`release.yml` changes.
- Docs: BENCHMARKS.md §3, a `## Benchmark` section in every binding README, a
  CHANGELOG entry.
- Verified locally by running: Rust, Python, C, C#, Go, Java (real numbers); the
  C archetype ctest with `-Wall -Wextra -Wpedantic -Werror`. WASM and R are
  API-correct and syntax-checked but need their own toolchains to run.
2026-06-10 03:46:38 +02:00
kingchenc b31a9a3624 release: bump 0.8.1 -> 0.8.2 (#245)
Patch release shipping the R WebAssembly build fix (#244): `bindings/r/configure` builds the C ABI from source for `wasm32-unknown-emscripten`, so r-universe's webR build stops failing. Also carries the shared Go badge in `bindings/go/README.md`. Version bumped across all manual touchpoints via `bump_version.py` (incl. DESCRIPTION + csproj, which had drifted before).
2026-06-10 01:57:10 +02:00
kingchenc 6433c9b3de bindings/r: build the C ABI from source for the WebAssembly target (#244)
## Problem
r-universe builds every package to WebAssembly (webR) in addition to the native platforms, calling `./configure --host=wasm32-unknown-emscripten`. `bindings/r/configure` only knew Linux/macOS/Windows and exited with `unsupported OS 'Emscripten'`, so the **WASM job was the single red check** on an otherwise fully-published r-universe build (all native platforms + deploy are green; the package installs fine everywhere).

## Fix
The r-universe wasm build image ships **cargo** (`/usr/local/cargo/bin`) and **emscripten** (`EMSDK` on PATH). So instead of needing a prebuilt wasm lib (which would risk an emscripten ABI/version mismatch), `configure` now detects the Emscripten host and **builds the C ABI staticlib from source** for `wasm32-unknown-emscripten` in-place — compiled with the image's own emscripten, so the ABI always matches. The static `libwickra.a` is linked into the package object (no shared lib, no rpath).

`wickra-core`'s rayon batch needs threads (absent on wasm), so the wasm build drops it via `--no-default-features`. `wickra-c` now takes `wickra-core` as a direct path dep with `default-features = false` plus a default `parallel` feature that re-enables it for native builds (a member-level `default-features = false` is ignored when inheriting a workspace dep — that was the trap). 

## Validated locally
- `cargo build -p wickra-c` (default) → rayon present, builds.
- `cargo build -p wickra-c --no-default-features` (the wasm feature path) → rayon **gone**, builds.
- `cargo build --workspace`, clippy, fmt all clean; `configure` passes `sh -n`.

## Cannot be validated locally
No Rust/emscripten wasm toolchain here. The wasm build only runs in r-universe, and `configure` downloads the matching `v${version}` source tag — so this takes effect from the **first release that includes it** (the tagged source must contain the feature toggle). Open risks: whether the image has the `wasm32-unknown-emscripten` Rust target pre-installed (configure runs `rustup target add` best-effort) and the wasm build time. Worth one r-universe rebuild to confirm.

Not merging — review first.
2026-06-10 01:44:48 +02:00
kingchenc 447bbc8220 bindings/csharp: sync the static csproj version to 0.8.1 (#243)
The static `<Version>` in `bindings/csharp/Wickra/Wickra.csproj` had drifted to 0.7.9 — it was missed in the 0.8.0/0.8.1 bumps (wrongly assumed to be purely tag-injected). The published NuGet package was correct (the release packs with `-p:Version=`), but the static field lagged. Resync to 0.8.1; bump_version.py now covers it.
2026-06-10 01:08:37 +02:00
kingchenc c9ef2fc037 bindings/r: bump DESCRIPTION version to 0.8.1 (#242)
The R package DESCRIPTION was missed in the 0.8.0/0.8.1 version bumps (it stayed at 0.7.9), so r-universe published wickra 0.7.9 while everything else is 0.8.1. Resync it; r-universe rebuilds from the main HEAD on its next ~hourly sync.
2026-06-10 00:47:13 +02:00
kingchenc 49c2872ad4 sync-about: sync the version for every registry row (#241)
The published-versions table sync only matched crates.io/PyPI/npm rows, so the NuGet, Maven Central, Go and r-universe rows added to the docs would stay stale on each release. Extend the regex to all seven registries and match the registry name whether plain or wrapped in a markdown link.
2026-06-10 00:26:49 +02:00
44 changed files with 1656 additions and 68 deletions
+8 -6
View File
@@ -332,12 +332,14 @@ 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
if git diff --quiet; then
echo "Docs version already at ${version}."
+1
View File
@@ -55,6 +55,7 @@ bindings/node/npm-debug.log*
# WASM build output
bindings/wasm/pkg/
bindings/wasm/pkg-node/
# Python venv
.venv/
+72
View File
@@ -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)
```
+29 -1
View File
@@ -5,6 +5,32 @@ 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).
## [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
@@ -1485,7 +1511,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.8.1...HEAD
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.8.3...HEAD
[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
Generated
+9 -9
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]]
name = "wickra"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"approx",
"criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]]
name = "wickra-bench"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"criterion",
"kand",
@@ -1967,14 +1967,14 @@ dependencies = [
[[package]]
name = "wickra-c"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"wickra-core",
]
[[package]]
name = "wickra-core"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"approx",
"proptest",
@@ -1984,7 +1984,7 @@ dependencies = [
[[package]]
name = "wickra-data"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"approx",
"csv",
@@ -2001,7 +2001,7 @@ dependencies = [
[[package]]
name = "wickra-examples"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"serde_json",
"tokio",
@@ -2011,7 +2011,7 @@ dependencies = [
[[package]]
name = "wickra-node"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"napi",
"napi-build",
@@ -2021,7 +2021,7 @@ dependencies = [
[[package]]
name = "wickra-python"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"numpy",
"pyo3",
@@ -2030,7 +2030,7 @@ dependencies = [
[[package]]
name = "wickra-wasm"
version = "0.8.1"
version = "0.8.3"
dependencies = [
"console_error_panic_hook",
"js-sys",
+2 -2
View File
@@ -14,7 +14,7 @@ members = [
exclude = ["fuzz"]
[workspace.package]
version = "0.8.1"
version = "0.8.3"
authors = ["kingchenc <support@wickra.org>"]
edition = "2021"
rust-version = "1.86"
@@ -26,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"]
[workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.8.1" }
wickra-core = { path = "crates/wickra-core", version = "0.8.3" }
thiserror = "2"
rayon = "1.10"
+4
View File
@@ -344,6 +344,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
+14 -1
View File
@@ -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 }
+14
View File
@@ -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
+40
View File
@@ -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()
+167
View File
@@ -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;
}
+12
View File
@@ -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
+1 -1
View File
@@ -11,7 +11,7 @@
<!-- NuGet package metadata -->
<PackageId>Wickra</PackageId>
<Version>0.7.9</Version>
<Version>0.8.3</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>
+101
View File
@@ -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.");
+12 -1
View File
@@ -2,7 +2,7 @@
[![CI](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml/badge.svg)](https://github.com/wickra-lib/wickra/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/wickra-lib/wickra/branch/main/graph/badge.svg)](https://codecov.io/gh/wickra-lib/wickra)
[![Go Reference](https://pkg.go.dev/badge/github.com/wickra-lib/wickra/bindings/go.svg)](https://pkg.go.dev/github.com/wickra-lib/wickra/bindings/go)
[![Go module](https://raw.githubusercontent.com/wickra-lib/.github/main/profile/badges/go.svg)](https://pkg.go.dev/github.com/wickra-lib/wickra/bindings/go)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT_OR_Apache--2.0-blue)](https://github.com/wickra-lib/wickra#license)
**Streaming-first technical indicators for Go, over the Wickra C ABI hub via cgo.**
@@ -88,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
+145
View File
@@ -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")
}
+15 -2
View File
@@ -30,14 +30,14 @@ Maven:
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.1</version>
<version>0.8.3</version>
</dependency>
```
Gradle:
```kotlin
implementation("org.wickra:wickra:0.8.1")
implementation("org.wickra:wickra:0.8.3")
```
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
+57
View File
@@ -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 &lt;-&gt; 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 -&gt; 1-out), ATR (multi-in -&gt; 1-out), and MACD
* (1-in -&gt; 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) {}
}
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.1</version>
<version>0.8.3</version>
<packaging>jar</packaging>
<name>Wickra</name>
+12
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-arm64",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-darwin-x64",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-arm64-gnu",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-arm64-gnu.node",
"files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wickra-linux-x64-gnu",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.linux-x64-gnu.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-arm64-msvc",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-arm64-msvc.node",
"files": [
@@ -1,6 +1,6 @@
{
"name": "wickra-win32-x64-msvc",
"version": "0.8.1",
"version": "0.8.3",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.win32-x64-msvc.node",
"files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{
"name": "wickra",
"version": "0.8.1",
"version": "0.8.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wickra",
"version": "0.8.1",
"version": "0.8.3",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.8.1",
"wickra-darwin-x64": "0.8.1",
"wickra-linux-arm64-gnu": "0.8.1",
"wickra-linux-x64-gnu": "0.8.1",
"wickra-win32-arm64-msvc": "0.8.1",
"wickra-win32-x64-msvc": "0.8.1"
"wickra-darwin-arm64": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-linux-x64-gnu": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3"
}
},
"node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
}
},
"node_modules/wickra-darwin-arm64": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.8.3.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [
"arm64"
@@ -57,8 +57,8 @@
}
},
"node_modules/wickra-darwin-x64": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.8.3.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [
"x64"
@@ -73,8 +73,8 @@
}
},
"node_modules/wickra-linux-arm64-gnu": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.8.3.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [
"arm64"
@@ -89,8 +89,8 @@
}
},
"node_modules/wickra-linux-x64-gnu": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.8.3.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [
"x64"
@@ -105,8 +105,8 @@
}
},
"node_modules/wickra-win32-arm64-msvc": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.8.3.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [
"arm64"
@@ -121,8 +121,8 @@
}
},
"node_modules/wickra-win32-x64-msvc": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.1.tgz",
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.8.3.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [
"x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "wickra",
"version": "0.8.1",
"version": "0.8.3",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>",
"main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-linux-x64-gnu": "0.8.1",
"wickra-linux-arm64-gnu": "0.8.1",
"wickra-darwin-x64": "0.8.1",
"wickra-darwin-arm64": "0.8.1",
"wickra-win32-x64-msvc": "0.8.1",
"wickra-win32-arm64-msvc": "0.8.1"
"wickra-linux-x64-gnu": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-darwin-arm64": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3"
},
"scripts": {
"build": "napi build --platform --release",
+18
View File
@@ -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
+116
View File
@@ -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()
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "wickra"
version = "0.8.1"
version = "0.8.3"
description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md"
license = "MIT OR Apache-2.0"
+1 -1
View File
@@ -1,7 +1,7 @@
Package: wickra
Type: Package
Title: Streaming-First Technical Indicators
Version: 0.7.9
Version: 0.8.3
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
+11
View File
@@ -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
+119
View File
@@ -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"
))
+30
View File
@@ -13,6 +13,36 @@ 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}"
+12
View File
@@ -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
+127
View File
@@ -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.',
);
+1
View File
@@ -64,6 +64,7 @@ endfunction()
# Offline examples built and run as ctests.
add_wickra_example(smoke smoke.c TRUE) # links the boundary, asserts values
add_wickra_example(archetypes archetypes.c TRUE) # one indicator per FFI archetype
add_wickra_example(streaming streaming.c TRUE) # multi-indicator tick stream
add_wickra_example(cpp_smoke smoke.cpp TRUE) # C++ RAII wrapper (wickra.hpp)
add_wickra_example(backtest backtest.c TRUE) # indicator basket over a CSV
+161
View File
@@ -0,0 +1,161 @@
/*
* Archetype coverage test for the Wickra C ABI.
*
* Exercises one indicator per FFI call-signature archetype through the real C
* boundary scalar (+ batch == streaming), multi-output, alt-chart bars,
* market profile and array input plus reset, invalid-parameter and NULL-safety
* behaviour. It is the C counterpart of the Go / R / Java archetype suites; the
* existing smoke test covers symbol/header/link, this one covers the data
* contracts. Run as a ctest (exit 0 on success).
*/
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include "wickra.h"
static int failures = 0;
#define CHECK(cond, msg) \
do { \
if (!(cond)) { \
fprintf(stderr, "FAIL: %s\n", (msg)); \
failures += 1; \
} \
} while (0)
static int is_nan(double value) {
return value != value;
}
int main(void) {
/* 1. Scalar archetype: SMA known value over the FFI boundary. */
{
struct Sma *sma = wickra_sma_new(3);
CHECK(sma != NULL, "sma_new(3) returned NULL");
double last = NAN;
const double xs[] = {1.0, 2.0, 3.0, 4.0, 5.0};
for (size_t i = 0; i < 5; i++) {
last = wickra_sma_update(sma, xs[i]);
}
CHECK(fabs(last - 4.0) < 1e-9, "SMA(3) over [.. 3 4 5] should be 4");
wickra_sma_free(sma);
}
/* 2. Scalar batch must equal streaming. */
{
const double xs[] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0};
const size_t n = 8;
struct Sma *stream = wickra_sma_new(3);
double want[8];
for (size_t i = 0; i < n; i++) {
want[i] = wickra_sma_update(stream, xs[i]);
}
wickra_sma_free(stream);
struct Sma *batched = wickra_sma_new(3);
double got[8];
wickra_sma_batch(batched, xs, got, n);
wickra_sma_free(batched);
int equal = 1;
for (size_t i = 0; i < n; i++) {
if (is_nan(want[i]) != is_nan(got[i])) {
equal = 0;
} else if (!is_nan(want[i]) && fabs(want[i] - got[i]) > 1e-9) {
equal = 0;
}
}
CHECK(equal, "SMA batch must equal streaming");
}
/* 3. Multi-output archetype: MACD writes a struct and returns a bool. */
{
struct MacdIndicator *macd = wickra_macd_indicator_new(3, 6, 3);
CHECK(macd != NULL, "macd_indicator_new returned NULL");
struct WickraMacdOutput out = {0};
int produced = 0;
for (int i = 0; i < 30; i++) {
if (wickra_macd_indicator_update(macd, 100.0 + (double)i, &out)) {
produced = 1;
}
}
CHECK(produced, "MACD produced no value after warmup");
CHECK(!is_nan(out.macd), "MACD value is NaN after warmup");
wickra_macd_indicator_free(macd);
}
/* 4. Alt-chart bars archetype: RangeBars writes 0..n bars per candle. */
{
struct RangeBars *bars = wickra_range_bars_new(2.0);
CHECK(bars != NULL, "range_bars_new returned NULL");
struct WickraRangeBar out[16];
size_t total = 0;
for (int i = 0; i < 15; i++) {
double price = 100.0 + (double)i;
total += wickra_range_bars_update(bars, price, price, price, price, 1.0,
(int64_t)i, out, 16);
}
CHECK(total > 0, "range bars produced no bars over a 15-point move");
wickra_range_bars_free(bars);
}
/* 5. Market-profile archetype: scalars + a caller-owned values buffer. */
{
struct VolumeProfile *profile = wickra_volume_profile_new(10, 24);
CHECK(profile != NULL, "volume_profile_new returned NULL");
struct WickraVolumeProfileOutputScalars scalars = {0};
double values[24];
intptr_t len = -1;
for (int i = 0; i < 20; i++) {
double price = 100.0 + (double)i;
len = wickra_volume_profile_update(profile, price, price + 1.0, price - 1.0,
price, 1000.0, (int64_t)i, &scalars,
values, 24);
}
CHECK(len > 0, "volume profile never produced a snapshot");
wickra_volume_profile_free(profile);
}
/* 6. Array-input archetype: a full order-book snapshot per side. */
{
struct OrderBookImbalanceFull *book = wickra_order_book_imbalance_full_new();
CHECK(book != NULL, "order_book_imbalance_full_new returned NULL");
const double bid_price[] = {99.9, 99.8, 99.7};
const double bid_size[] = {5.0, 3.0, 2.0};
const double ask_price[] = {100.1, 100.2, 100.3};
const double ask_size[] = {1.0, 1.0, 1.0};
double imbalance = wickra_order_book_imbalance_full_update(
book, bid_price, bid_size, 3, ask_price, ask_size, 3);
CHECK(!is_nan(imbalance), "order book imbalance returned NaN");
wickra_order_book_imbalance_full_free(book);
}
/* 7. Reset returns the indicator to its warmup state. */
{
struct Sma *sma = wickra_sma_new(3);
for (int i = 0; i < 3; i++) {
wickra_sma_update(sma, (double)(i + 1));
}
wickra_sma_reset(sma);
CHECK(is_nan(wickra_sma_update(sma, 10.0)), "SMA after reset must be NaN");
wickra_sma_free(sma);
}
/* 8. Invalid parameters return NULL; freeing NULL is a no-op. */
{
CHECK(wickra_sma_new(0) == NULL, "sma_new(0) should return NULL");
wickra_sma_free(NULL);
}
/* 9. A NULL handle update is a no-op returning NaN, never a crash. */
CHECK(is_nan(wickra_sma_update(NULL, 1.0)), "update(NULL) should return NaN");
if (failures == 0) {
printf("all archetypes passed\n");
return 0;
}
fprintf(stderr, "%d archetype check(s) failed\n", failures);
return 1;
}
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>org.wickra.examples</groupId>
<artifactId>wickra-examples</artifactId>
<version>0.8.1</version>
<version>0.8.3</version>
<packaging>jar</packaging>
<name>Wickra Java examples</name>
@@ -21,7 +21,7 @@
<dependency>
<groupId>org.wickra</groupId>
<artifactId>wickra</artifactId>
<version>0.8.1</version>
<version>0.8.3</version>
</dependency>
<!-- Only the network examples (fetch_btcusdt) parse JSON. -->
<dependency>
+7 -7
View File
@@ -17,7 +17,7 @@
},
"../../bindings/node": {
"name": "wickra",
"version": "0.8.1",
"version": "0.8.3",
"license": "MIT OR Apache-2.0",
"devDependencies": {
"@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18"
},
"optionalDependencies": {
"wickra-darwin-arm64": "0.8.1",
"wickra-darwin-x64": "0.8.1",
"wickra-linux-arm64-gnu": "0.8.1",
"wickra-linux-x64-gnu": "0.8.1",
"wickra-win32-arm64-msvc": "0.8.1",
"wickra-win32-x64-msvc": "0.8.1"
"wickra-darwin-arm64": "0.8.3",
"wickra-darwin-x64": "0.8.3",
"wickra-linux-arm64-gnu": "0.8.3",
"wickra-linux-x64-gnu": "0.8.3",
"wickra-win32-arm64-msvc": "0.8.3",
"wickra-win32-x64-msvc": "0.8.3"
}
},
"node_modules/wickra": {
+131
View File
@@ -0,0 +1,131 @@
//! Throughput benchmark for the Wickra Rust core — the zero-FFI baseline.
//!
//! Reports streaming (`update`) and batch updates-per-second over a synthetic
//! OHLCV series, in the same format as every binding's `throughput` benchmark.
//! Rust has no FFI boundary — it calls the core directly — so these numbers are
//! the ceiling the per-binding benchmarks are measured against, and the value
//! their `batch` paths converge towards. See the repository BENCHMARKS.md §3.
//!
//! For per-update latency and the cross-library comparison, use the criterion
//! harnesses instead: `cargo bench -p wickra` and `cargo bench -p wickra-bench`.
//!
//! Run:
//! cargo run -p wickra-examples --release --bin throughput # 200k bars
//! cargo run -p wickra-examples --release --bin throughput -- 1000000
use std::time::Instant;
use wickra::{Atr, Candle, Indicator, MacdIndicator, Sma};
/// Median elapsed-ns over a few repetitions, after one warmup pass.
fn time_ns(mut run: impl FnMut()) -> u128 {
run(); // warmup
let mut samples = [0u128; 3];
for sample in &mut samples {
let start = Instant::now();
run();
*sample = start.elapsed().as_nanos();
}
samples.sort_unstable();
samples[1]
}
fn main() {
let bars: usize = std::env::args()
.nth(1)
.and_then(|arg| arg.parse().ok())
.filter(|&n| n >= 1000)
.unwrap_or(200_000);
// Deterministic synthetic OHLCV (no RNG, so runs are comparable).
let mut open = Vec::with_capacity(bars);
let mut high = Vec::with_capacity(bars);
let mut low = Vec::with_capacity(bars);
let mut close = Vec::with_capacity(bars);
let mut volume = Vec::with_capacity(bars);
for i in 0..bars {
let mid = 100.0 + (i as f64 * 0.001).sin() * 20.0 + i as f64 * 1e-4;
let c = mid + (i as f64 * 0.05).sin() * 2.0;
close.push(c);
open.push(mid);
high.push(c.max(mid) + 1.5);
low.push(c.min(mid) - 1.5);
volume.push(1000.0 + (i % 97) as f64 * 13.0);
}
// ATR streams a Candle per tick; build them once, outside the timed loop.
let candles: Vec<Candle> = (0..bars)
.map(|i| {
Candle::new(
open[i],
high[i],
low[i],
close[i],
volume[i],
i64::try_from(i).unwrap(),
)
.unwrap()
})
.collect();
let mups = |ns: u128| bars as f64 / (ns as f64 / 1e9) / 1e6;
// SMA (scalar 1-in/1-out), ATR (multi-in/1-out), MACD (1-in/multi-out).
let sma_stream = time_ns(|| {
let mut ind = Sma::new(20).unwrap();
for &price in &close {
ind.update(price);
}
});
let sma_batch = time_ns(|| {
let mut ind = Sma::new(20).unwrap();
ind.batch_nan(&close);
});
let atr_stream = time_ns(|| {
let mut ind = Atr::new(14).unwrap();
for &candle in &candles {
ind.update(candle);
}
});
let atr_batch = time_ns(|| {
let mut ind = Atr::new(14).unwrap();
ind.batch_atr(&high, &low, &close);
});
let macd_stream = time_ns(|| {
let mut ind = MacdIndicator::new(12, 26, 9).unwrap();
for &price in &close {
ind.update(price);
}
});
println!("Wickra Rust core throughput - {bars} bars (median of 3 runs)\n");
println!(
"{:<22}{:>20}{:>18}",
"Indicator", "streaming (Mupd/s)", "batch (Mupd/s)"
);
println!("{}", "-".repeat(60));
println!(
"{:<22}{:>20.1}{:>18.1}",
"SMA(20)",
mups(sma_stream),
mups(sma_batch)
);
println!(
"{:<22}{:>20.1}{:>18.1}",
"ATR(14)",
mups(atr_stream),
mups(atr_batch)
);
println!(
"{:<22}{:>20.1}{:>18}",
"MACD(12,26,9)",
mups(macd_stream),
"-"
);
println!(
"\nMupd/s = million indicator updates per second. This is the Rust core with\n\
no FFI boundary, so it is the ceiling for the per-binding benchmarks and\n\
the value their batch paths converge towards. Numbers are machine-dependent\n\
- use them for relative comparison, not as a speed claim."
);
}