Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ee3fb67ec | |||
| 3ebcb3f758 | |||
| b31a9a3624 | |||
| 6433c9b3de | |||
| 447bbc8220 | |||
| c9ef2fc037 | |||
| 49c2872ad4 |
@@ -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}."
|
||||
|
||||
@@ -55,6 +55,7 @@ bindings/node/npm-debug.log*
|
||||
|
||||
# WASM build output
|
||||
bindings/wasm/pkg/
|
||||
bindings/wasm/pkg-node/
|
||||
|
||||
# Python venv
|
||||
.venv/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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.**
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.8.1</version>
|
||||
<version>0.8.3</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Wickra</name>
|
||||
|
||||
@@ -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.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,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,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": [
|
||||
|
||||
Generated
+20
-20
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.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,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
|
||||
|
||||
@@ -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"
|
||||
))
|
||||
Vendored
+30
@@ -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}"
|
||||
|
||||
@@ -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.',
|
||||
);
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Generated
+7
-7
@@ -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": {
|
||||
|
||||
@@ -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."
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user