Compare commits

...
4 Commits
Author SHA1 Message Date
kingchencandGitHub 4caaa1db97 release: bump 0.7.4 -> 0.7.5 (#225)
Version bump for the C ABI hub release (0.7.4 -> 0.7.5). See #222 + #224.
2026-06-09 02:26:27 +02:00
kingchencandGitHub 12681e4b1b C ABI: full example suite + docs & About coverage (#224)
Stacked on #222 (base `feat/c-abi-hub`), so the diff is just the additions on top of the hub foundation — no merge of #222 required.

## What this adds

**Examples — full parity with rust/python/node (`examples/c/`)**
- `streaming.c` upgraded to the multi-indicator (SMA/EMA/RSI/MACD + signals) demo
- `backtest.c`, `multi_timeframe.c` (manual time-bucket resampling), `parallel_assets.c` (serial vs OpenMP fan-out, one handle per asset)
- three educational strategies: `strategy_rsi_mean_reversion.c`, `strategy_macd_adx.c`, `strategy_bollinger_squeeze.c`
- two network examples shelling out to `curl`: `fetch_btcusdt.c`, `live_binance.c` (REST poll)
- two header-only helpers (`wickra_csv.h`, `wickra_strategy.h`) since the C ABI ships no IO layer
- CMake builds all 11; the 9 offline ones run under `ctest` on 3 OS; the network two are built-only

**Docs & metadata — surface the C ABI everywhere it was missing**
- ARCHITECTURE diagram + crate table, SECURITY + THREAT_MODEL (the C ABI as the sole `unsafe` FFI surface), the three binding package READMEs, issue/PR templates, CHANGELOG, and the GitHub About template (live About + org description updated too)

**Cleanup**
- removed all references to the private generator tooling from public files (`bindings/c/src/lib.rs` header, `CONTRIBUTING.md`, `sync-about.yml`)

Verified locally: `cargo build -p wickra-c --release`, `cmake + ctest` (9/9 pass), and `-Wall -Wextra -Wpedantic` clean on gcc 13.
2026-06-09 02:14:28 +02:00
kingchencandGitHub 91e05e3c26 C ABI hub crate (bindings/c) foundation (#222)
## What

Introduces `wickra-c` — a `cdylib` + `staticlib` that exposes the Rust core over a **C ABI**. This is the hub every C-capable language (C, C++, Go, C#, Java, R) links against, instead of re-wiring each indicator natively. The native Python/Node/WASM bindings are untouched; this is purely additive, for ecosystems without first-class Rust tooling.

## Scope (foundation slice)

This PR deliberately validates the **whole pipeline end to end with one indicator (SMA)** before scaling to all 514, so the CI / cross-OS / header-drift mechanics are proven green first.

- Opaque `*mut T` handles; `wickra_<ind>_{new,update,batch,reset,free}`.
- NaN sentinel for warmup / NULL handles; caller-owned batch buffers; every function NULL-safe.
- cbindgen generates and commits `bindings/c/include/wickra.h` with opaque handle typedefs.
- A C smoke example (`examples/c/`) links the header + compiled library and runs (CMake + ctest).
- A `c-abi` CI job builds the library and runs the smoke test on **Linux, macOS and Windows**, plus a header drift check on Linux.

## Notes

- The per-indicator FFI blocks are plain `#[no_mangle]` functions, **not** a macro: cbindgen cannot see macro-generated functions on stable Rust (macro expansion needs nightly), so the blocks are written literally and will be generated mechanically by the ScriptHelpers `capi` wrapper in a follow-up (same model as the committed-but-generated Node `index.js`).
- `bindings/c` cannot inherit the workspace `forbid(unsafe_code)` lint (the C boundary needs raw pointers), so it mirrors every workspace lint and only relaxes `unsafe_code`. The Rust core stays `unsafe`-forbidden.

## Follow-ups (separate PRs)

- ScriptHelpers `capi` generator + wire the scalar family (~235).
- Hand-written blocks for multi-output / custom-input / bars (~279).
- Docs consistency wave (README / docs / webpage: Python·Node·WASM·Rust → +C).
- Release wiring (native-lib matrix + header/lib GH-release assets) — gated.
2026-06-09 02:07:03 +02:00
kingchencandGitHub 9d0983b666 ci(sync-about): sync indicator count into the webpage About page (#223)
The `wickra.org/about` page carries the indicator count in the bot-syncable `<N> indicators` token, but `sync-about.yml` only rewrote `index.md` + `.vitepress/config.ts` on the webpage. Add `about.md` to the webpage count step's `sed` file list and its `git add`, so the About page's count self-heals on every push-to-main / tag like the rest of the marketing site.

No-op for the Rust build — workflow file only.
2026-06-08 21:38:42 +02:00
51 changed files with 57247 additions and 97 deletions
+5
View File
@@ -1,3 +1,8 @@
# Shell scripts must keep LF line endings so they run on Linux/macOS CI and # Shell scripts must keep LF line endings so they run on Linux/macOS CI and
# local shells regardless of the committer's platform autocrlf setting. # local shells regardless of the committer's platform autocrlf setting.
*.sh text eol=lf *.sh text eol=lf
# The cbindgen-generated C header is committed; pin it to LF so its CI drift
# check (regenerate + `git diff`) never trips on a CRLF normalization.
bindings/c/include/wickra.h text eol=lf
+1 -1
View File
@@ -30,7 +30,7 @@ assignees: ""
## Environment ## Environment
- Wickra version: - Wickra version:
- Language / binding: <!-- Rust crate / Python / Node / WASM --> - Language / binding: <!-- Rust crate / Python / Node / WASM / C ABI -->
- OS and architecture: - OS and architecture:
- Rust / Python / Node version (If relevant): - Rust / Python / Node version (If relevant):
@@ -13,7 +13,7 @@ assignees: []
## Affected code path ## Affected code path
- Indicator / API: `e.g. EMA.update` - Indicator / API: `e.g. EMA.update`
- Binding: `Rust / Python / Node / Wasm` - Binding: `Rust / Python / Node / Wasm / C ABI`
- Hot loop or one-shot call? - Hot loop or one-shot call?
## Versions compared ## Versions compared
+1 -1
View File
@@ -33,4 +33,4 @@ import wickra as ta
## Environment (Only if relevant) ## Environment (Only if relevant)
- Wickra version: `e.g. 0.4.2` - Wickra version: `e.g. 0.4.2`
- Binding: `Rust / Python / Node / Wasm` - Binding: `Rust / Python / Node / Wasm / C ABI`
+1 -1
View File
@@ -22,7 +22,7 @@
- [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean. - [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean.
- [ ] `cargo test --workspace` passes. - [ ] `cargo test --workspace` passes.
- [ ] New behaviour has tests; bug fixes have a regression test. - [ ] New behaviour has tests; bug fixes have a regression test.
- [ ] Public API changes are mirrored in the Python / Node / WASM bindings - [ ] Public API changes are mirrored in the Python / Node / WASM bindings and the C ABI is regenerated
and their type stubs (If applicable). and their type stubs (If applicable).
- [ ] The relevant page on the [documentation site](https://docs.wickra.org) - [ ] The relevant page on the [documentation site](https://docs.wickra.org)
and the `README.md` are updated (If applicable). Docs edits go to a and the `README.md` are updated (If applicable). Docs edits go to a
+55
View File
@@ -622,6 +622,61 @@ jobs:
working-directory: bindings/node working-directory: bindings/node
run: node --test __tests__/ run: node --test __tests__/
c-abi:
name: C ABI on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
- name: Cache cargo
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Install cbindgen
uses: taiki-e/install-action@0fd46367812ee04360509b4169d9f659d6892bb2 # v2.79.15
timeout-minutes: 10 # fail fast on a stuck download instead of hanging the job
with:
tool: cbindgen
- name: Build the C ABI library (cdylib + staticlib)
run: cargo build -p wickra-c --release
- name: Rust unit tests
run: cargo test -p wickra-c
# The generated header is platform-independent, so checking drift on one OS
# is enough — and avoids a spurious CRLF/LF diff on the Windows runner.
- name: Check the committed header is in sync with cbindgen
if: runner.os == 'Linux'
shell: bash
run: |
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
if ! git diff --quiet -- bindings/c/include/wickra.h; then
echo "::error::bindings/c/include/wickra.h is out of sync — run cbindgen and commit the result"
git --no-pager diff -- bindings/c/include/wickra.h
exit 1
fi
# The real cross-language test: a foreign C consumer links the generated
# header + the compiled library and runs. If this passes on all three OSes,
# every C-capable language can link the same way.
- name: Build and run the C smoke example (CMake + ctest)
shell: bash
run: |
cmake -S examples/c -B examples/c/build
cmake --build examples/c/build --config Release
ctest --test-dir examples/c/build -C Release --output-on-failure
# The cross-library benchmark has moved to a dedicated scheduled workflow # The cross-library benchmark has moved to a dedicated scheduled workflow
# (.github/workflows/bench.yml) — see audit finding R10. It runs nightly # (.github/workflows/bench.yml) — see audit finding R10. It runs nightly
# at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on # at 03:00 UTC and on-demand via `workflow_dispatch`, and is no longer on
+60 -2
View File
@@ -569,9 +569,64 @@ jobs:
# the old "publish, then upload provenance" order would have the provenance # the old "publish, then upload provenance" order would have the provenance
# upload rejected once immutability is enabled. # upload rejected once immutability is enabled.
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# --------------------------------------------------------------------------
# C ABI native libraries (bindings/c) — built per target on a native runner
# (no cross toolchain needed) and attached to the GitHub Release as the
# distribution channel. There is no package registry for the C ABI.
# --------------------------------------------------------------------------
c-abi-build:
name: C ABI library (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- { host: ubuntu-latest, target: x86_64-unknown-linux-gnu }
- { host: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu }
- { host: macos-latest, target: x86_64-apple-darwin }
- { host: macos-latest, target: aarch64-apple-darwin }
- { host: windows-latest, target: x86_64-pc-windows-msvc }
- { host: windows-11-arm, target: aarch64-pc-windows-msvc }
runs-on: ${{ matrix.host }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-03-27
with:
targets: ${{ matrix.target }}
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
continue-on-error: true # cache is an optimisation; never block on a stuck/slow restore
timeout-minutes: 6
- name: Build the C ABI library (cdylib + staticlib)
run: cargo build -p wickra-c --release --target ${{ matrix.target }}
- name: Package header + libraries
shell: bash
run: |
set -e
dir="wickra-c-${{ matrix.target }}"
mkdir -p "$dir/include" "$dir/lib"
cp bindings/c/include/wickra.h bindings/c/include/wickra.hpp "$dir/include/"
for f in libwickra.so libwickra.a libwickra.dylib wickra.dll wickra.dll.lib wickra.lib; do
src="target/${{ matrix.target }}/release/$f"
[ -f "$src" ] && cp "$src" "$dir/lib/"
done
tar -czf "$dir.tar.gz" "$dir"
echo "packaged $dir:"; ls -lR "$dir"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: c-abi-${{ matrix.target }}
path: wickra-c-${{ matrix.target }}.tar.gz
if-no-files-found: error
github-release: github-release:
name: Attach assets to the draft GitHub Release name: Attach assets to the draft GitHub Release
needs: [cargo-publish, python-publish, node-publish, wasm-publish] needs: [cargo-publish, python-publish, node-publish, wasm-publish, c-abi-build]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
contents: write contents: write
@@ -644,7 +699,7 @@ jobs:
# the provenance bundle is attached (P24, immutability-ready). # the provenance bundle is attached (P24, immutability-ready).
draft: true draft: true
body: | body: |
Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries. Wickra ${{ github.ref_name }} — streaming-first technical indicators across 4 language registries plus a C ABI.
### Install ### Install
@@ -665,6 +720,9 @@ jobs:
darwin-arm64, win32-x64-msvc) darwin-arm64, win32-x64-msvc)
- `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM) - `wickra-*.tgz` — npm-pack tarballs (main package + per-platform subpackages + WASM)
- `*.crate` — cargo source crates (wickra-core, wickra-data, wickra) - `*.crate` — cargo source crates (wickra-core, wickra-data, wickra)
- `wickra-c-<target>.tar.gz` — C ABI: `include/wickra.h` + `wickra.hpp`
and the cdylib/staticlib per target (linux/macos/windows × x64/arm64),
the hub for C / C++ / Go / C# / Java / R
### Auto-generated changelog ### Auto-generated changelog
+8 -8
View File
@@ -9,7 +9,7 @@ name: Sync indicator count
# 2. GitHub repo "About" description — synced on push to main / v* tag # 2. GitHub repo "About" description — synced on push to main / v* tag
# 3. Docs site: index.md / overview.md / Indicators-Overview.md # 3. Docs site: index.md / overview.md / Indicators-Overview.md
# (wickra-lib/wickra-docs) — synced on push to main / v* tag* # (wickra-lib/wickra-docs) — synced on push to main / v* tag*
# 4. Marketing site count (wickra-lib/webpage: index.md / # 4. Marketing site count (wickra-lib/webpage: index.md / about.md /
# .vitepress/config.ts) — push to main / v* tag* # .vitepress/config.ts) — push to main / v* tag*
# 5. org profile README count (wickra-lib/.github, profile/README.md) # 5. org profile README count (wickra-lib/.github, profile/README.md)
# — synced on push to main / v* tag* # — synced on push to main / v* tag*
@@ -42,10 +42,10 @@ name: Sync indicator count
# single source of truth for what the bindings reach. # single source of truth for what the bindings reach.
# #
# Design: on PRs this workflow is a READ-ONLY check. The indicator wiring # Design: on PRs this workflow is a READ-ONLY check. The indicator wiring
# (ScriptHelpers/_common.py wire_readme_counter) bumps both README.md and # bumps both README.md and docs/README.md inside the author's code commit, so
# docs/README.md inside the author's code commit, so the counter is already # the counter is already correct by the time CI runs. If it is not, the check
# correct by the time CI runs. If it is not, the check below fails loud and # below fails loud and asks the author to re-run the wiring — it never pushes a
# asks the author to re-run the wiring — it never pushes a fix-up commit. # fix-up commit.
# #
# (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to # (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to
# the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit # the PR head. Because GITHUB_TOKEN pushes trigger no workflows, that commit
@@ -149,7 +149,7 @@ jobs:
# actually live (Cloudflare Pages, P8.1); merging this PR is therefore # actually live (Cloudflare Pages, P8.1); merging this PR is therefore
# gated on the domain resolving, otherwise the About link would 404. # gated on the domain resolving, otherwise the About link would 404.
homepage="https://docs.wickra.org" homepage="https://docs.wickra.org"
desc="Streaming-first technical indicators with a Rust core and Python, Node.js, and WebAssembly bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement." desc="Streaming-first technical indicators with a Rust core and Python, Node.js, WebAssembly, and C ABI bindings. ${n} indicators, O(1) per-tick updates, no system dependencies. Drop-in TA-Lib replacement."
# Enforce the homepage unconditionally — it is a constant, so this both # Enforce the homepage unconditionally — it is a constant, so this both
# corrects the stale kingchenc URL and self-heals any future drift. # corrects the stale kingchenc URL and self-heals any future drift.
# Same Administration-write permission as --description (no extra scope). # Same Administration-write permission as --description (no extra scope).
@@ -366,14 +366,14 @@ jobs:
exit 0 exit 0
fi fi
cd webpage-count cd webpage-count
sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md .vitepress/config.ts sed -i -E "s/[0-9]+ (streaming-first )?indicators/${n} \1indicators/g" index.md about.md .vitepress/config.ts
if git diff --quiet; then if git diff --quiet; then
echo "Webpage indicator count unchanged." echo "Webpage indicator count unchanged."
exit 0 exit 0
fi fi
git config user.name "wickra-bot" git config user.name "wickra-bot"
git config user.email "wickra-bot@users.noreply.github.com" git config user.email "wickra-bot@users.noreply.github.com"
git add index.md .vitepress/config.ts git add index.md about.md .vitepress/config.ts
git commit -m "chore: sync indicator count to ${n}" git commit -m "chore: sync indicator count to ${n}"
if ! git push 2>/dev/null; then if ! git push 2>/dev/null; then
echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)." echo "::warning::push to wickra-lib/webpage failed — ABOUT_SYNC_TOKEN likely lacks write (findings P10.0a)."
+13 -6
View File
@@ -29,14 +29,20 @@ or replace lives behind a separate crate boundary.
│ (every binding wraps the same core) │ (every binding wraps the same core)
┌───────────────────────┬─────────────────────┐ ┌───────────┬────────────┴┬──────────────┬─────────────────────
│ │ │
┌──▼───── ┌───────▼──────┐ ┌───────────────┐ ┌──▼─────┐ ┌───▼────┐ ┌──────▼──────┐ ┌─────▼──────────────┐
│ Python │ Node │ │ WASM │ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen)
│ (PyO3) │ (napi-rs) │ │ (wasm-bindgen) │ (PyO3) │ │(napi-rs)│ │(wasm-bindgen)│ │ cdylib + header
└──────── └──────────────┘ └────────────────┘ └────────┘ └────────┘ └─────────────┘ └────────────────────┘
``` ```
Python, Node and WASM are *native* Rust bindings (PyO3 / napi-rs /
wasm-bindgen). The C ABI is the *hub* every other C-capable language links
against: it builds to a `cdylib`/`staticlib` plus a generated `wickra.h`, and
C / C++ / Go / C# / Java / R consume that one artifact rather than each
re-wrapping the core.
| Crate | Path | What it owns | Public deps | | Crate | Path | What it owns | Public deps |
|---|---|---|---| |---|---|---|---|
| `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) | | `wickra-core` | `crates/wickra-core` | every indicator, the `Indicator` trait, `BatchExt`, `Candle`/`Tick` types, `Error` | `thiserror`, `rayon` (parallel batch) |
@@ -45,6 +51,7 @@ or replace lives behind a separate crate boundary.
| `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` | | `wickra-python` | `bindings/python` | `_wickra` PyO3 module + Python package | `pyo3`, `numpy`, depends on `wickra-core` |
| `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` | | `wickra-node` | `bindings/node` | NAPI-RS native binding | `napi`, depends on `wickra-core` |
| `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` | | `wickra-wasm` | `bindings/wasm` | WebAssembly binding | `wasm-bindgen`, depends on `wickra-core` |
| `wickra-c` | `bindings/c` | C ABI hub — `cdylib`/`staticlib` + generated `wickra.h` (cbindgen) | depends on `wickra-core` |
| `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` | | `wickra-examples` | `examples/rust` | runnable binary examples | depends on `wickra`, `wickra-data` |
The `fuzz/` directory is **excluded** from the workspace (it has its own The `fuzz/` directory is **excluded** from the workspace (it has its own
+12 -1
View File
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.7.5] - 2026-06-09
### Added
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an
opaque-handle C ABI: the hub any C-capable language (C, C++, Go, C#, Java, R)
links against, complementing the native Python/Node/WASM bindings. Ships a
full example suite (streaming, backtest, multi-timeframe, OpenMP parallel
fan-out, three educational strategies, and Binance fetch/live over `curl`)
mirroring the other bindings, plus an optional `wickra.hpp` C++ RAII wrapper.
## [0.7.4] - 2026-06-08 ## [0.7.4] - 2026-06-08
- **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`). - **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`).
- **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`). - **Run** — Run bars (consecutive same-direction tick runs) (`RUN_BARS`).
@@ -1416,7 +1426,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
optional Binance live feed. optional Binance live feed.
- Bindings for Python, Node.js, and WebAssembly. - Bindings for Python, Node.js, and WebAssembly.
[Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.4...HEAD [Unreleased]: https://github.com/wickra-lib/wickra/compare/v0.7.5...HEAD
[0.7.5]: https://github.com/wickra-lib/wickra/compare/v0.7.4...v0.7.5
[0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4 [0.7.4]: https://github.com/wickra-lib/wickra/compare/v0.7.3...v0.7.4
[0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3 [0.7.3]: https://github.com/wickra-lib/wickra/compare/v0.7.2...v0.7.3
[0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2 [0.7.2]: https://github.com/wickra-lib/wickra/compare/v0.7.1...v0.7.2
+4 -1
View File
@@ -21,6 +21,7 @@ licensed as above, without any additional terms or conditions.
| `bindings/python` | PyO3 bindings (`wickra` on PyPI). | | `bindings/python` | PyO3 bindings (`wickra` on PyPI). |
| `bindings/node` | napi-rs bindings (`wickra` on npm). | | `bindings/node` | napi-rs bindings (`wickra` on npm). |
| `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). | | `bindings/wasm` | wasm-bindgen bindings (`wickra-wasm` on npm). |
| `bindings/c` | C ABI — `cdylib` + `staticlib` + generated `include/wickra.h`. The hub for C / C++ and any C-capable language. |
| `examples/` | Runnable examples. | | `examples/` | Runnable examples. |
| `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. | | `docs/` | Pointer to the documentation site (docs.wickra.org); the docs live in the `wickra-lib/wickra-docs` repo. |
@@ -102,7 +103,9 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
- **Streaming parity.** An indicator's `batch` output must equal the sequence - **Streaming parity.** An indicator's `batch` output must equal the sequence
of `update` calls. of `update` calls.
- **Bindings.** A change to a public indicator API must be mirrored across the - **Bindings.** A change to a public indicator API must be mirrored across the
Python, Node, and WASM bindings, including their type stubs / `.d.ts`. Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
(`bindings/c`) is generated from the core, so regenerate it from the core and
commit `src/lib.rs` + `include/wickra.h`.
- **Docs.** Update the relevant page on the - **Docs.** Update the relevant page on the
[documentation site](https://docs.wickra.org) and the [documentation site](https://docs.wickra.org) and the
`README.md` when behaviour or the public API changes. The docs live in `README.md` when behaviour or the public API changes. The docs live in
Generated
+15 -8
View File
@@ -1944,7 +1944,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra" name = "wickra"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"approx", "approx",
"criterion", "criterion",
@@ -1955,7 +1955,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-bench" name = "wickra-bench"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"criterion", "criterion",
"kand", "kand",
@@ -1965,9 +1965,16 @@ dependencies = [
"yata", "yata",
] ]
[[package]]
name = "wickra-c"
version = "0.7.5"
dependencies = [
"wickra-core",
]
[[package]] [[package]]
name = "wickra-core" name = "wickra-core"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"approx", "approx",
"proptest", "proptest",
@@ -1977,7 +1984,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-data" name = "wickra-data"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"approx", "approx",
"csv", "csv",
@@ -1994,7 +2001,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-examples" name = "wickra-examples"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"serde_json", "serde_json",
"tokio", "tokio",
@@ -2004,7 +2011,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-node" name = "wickra-node"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"napi", "napi",
"napi-build", "napi-build",
@@ -2014,7 +2021,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-python" name = "wickra-python"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"numpy", "numpy",
"pyo3", "pyo3",
@@ -2023,7 +2030,7 @@ dependencies = [
[[package]] [[package]]
name = "wickra-wasm" name = "wickra-wasm"
version = "0.7.4" version = "0.7.5"
dependencies = [ dependencies = [
"console_error_panic_hook", "console_error_panic_hook",
"js-sys", "js-sys",
+3 -2
View File
@@ -7,13 +7,14 @@ members = [
"bindings/python", "bindings/python",
"bindings/wasm", "bindings/wasm",
"bindings/node", "bindings/node",
"bindings/c",
"examples/rust", "examples/rust",
"crates/wickra-bench", "crates/wickra-bench",
] ]
exclude = ["fuzz"] exclude = ["fuzz"]
[workspace.package] [workspace.package]
version = "0.7.4" version = "0.7.5"
authors = ["kingchenc <support@wickra.org>"] authors = ["kingchenc <support@wickra.org>"]
edition = "2021" edition = "2021"
rust-version = "1.86" rust-version = "1.86"
@@ -25,7 +26,7 @@ keywords = ["finance", "trading", "indicators", "technical-analysis", "ta"]
categories = ["finance", "mathematics", "science"] categories = ["finance", "mathematics", "science"]
[workspace.dependencies] [workspace.dependencies]
wickra-core = { path = "crates/wickra-core", version = "0.7.4" } wickra-core = { path = "crates/wickra-core", version = "0.7.5" }
thiserror = "2" thiserror = "2"
rayon = "1.10" rayon = "1.10"
+23 -12
View File
@@ -18,8 +18,9 @@
**Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.** **Streaming-first technical indicators. Install with `pip install wickra` — no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is a state native bindings for Python, Node.js and WebAssembly, plus a C ABI that any
machine that updates in O(1) per new data point, so live trading bots and C-capable language (C, C++, and beyond) links against. Every indicator is a
state machine that updates in O(1) per new data point, so live trading bots and
historical backtests share the exact same implementation. historical backtests share the exact same implementation.
```python ```python
@@ -71,9 +72,10 @@ times to get there.
breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance breadth, Renko/Kagi/Point&Figure bars, Ehlers DSP cycles, risk/performance
metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and metrics — every single one updating in **O(1) per tick**. TA-Lib ships ~150 and
none of them stream. none of them stream.
- **One Rust core, four first-class targets.** Native **Python · Node.js · - **One Rust core, five first-class targets.** Native **Python · Node.js ·
WebAssembly · Rust** — identical math, identical results, zero per-language WebAssembly · Rust** plus a **C ABI** for C / C++ and any C-capable language
reimplementation and zero GIL bottleneck. identical math, identical results, zero per-language reimplementation and zero
GIL bottleneck.
- **Correct by construction, not by hope.** Every `update` validates its input, - **Correct by construction, not by hope.** Every `update` validates its input,
runs a real warmup, and returns an `Option` so a single bad tick can't silently runs a real warmup, and returns an `Option` so a single bad tick can't silently
poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered poison state. `batch == streaming` is **bit-exact, fuzzed and 100 %-line-covered
@@ -95,7 +97,7 @@ Every other library forces one of those compromises. Wickra doesn't:
| Library | Install | Streaming | Languages | Indicators | Active | | Library | Install | Streaming | Languages | Indicators | Active |
|------------------|-------------|-------------|-----------------------------|-----------:|--------| |------------------|-------------|-------------|-----------------------------|-----------:|--------|
| **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust** | **514** | **yes** | | **★&nbsp;Wickra**| **clean** | **yes, O(1)** | **Python · Node · WASM · Rust · C** | **514** | **yes** |
| kand | clean | yes | Python · WASM · Rust | ~60 | yes | | kand | clean | yes | Python · WASM · Rust | ~60 | yes |
| ta-rs | clean | yes | Rust only | ~30 | stale | | ta-rs | clean | yes | Rust only | ~30 | stale |
| yata | clean | partial | Rust only | ~35 | yes | | yata | clean | partial | Rust only | ~35 | yes |
@@ -166,8 +168,8 @@ as one column each. `Doji` is direction-less by default (`+1.0` / `0.0`);
construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`, construct it in signed mode (`Doji::new().signed()`, `Doji(signed=True)`,
`new Doji(true)`) for a dragonfly / gravestone `±1` reading. `new Doji(true)`) for a dragonfly / gravestone `±1` reading.
Adding a new indicator means implementing one trait in Rust; all four bindings Adding a new indicator means implementing one trait in Rust; all five bindings
inherit it automatically. inherit it automatically (the C ABI is generated from the core).
## Languages ## Languages
@@ -177,12 +179,14 @@ inherit it automatically.
| Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` | | Node.js (napi-rs) | `npm install wickra` | `examples/node/backtest.js` |
| Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` | | Browser / WASM | `npm install wickra-wasm` | `examples/wasm/index.html` |
| Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` | | Rust | `cargo add wickra` | `examples/rust/src/bin/backtest.rs` |
| C / C++ (C ABI) | header + library, see [`bindings/c`](bindings/c) | `examples/c/streaming.c` |
Each binding ships several runnable examples (streaming, backtest, live feed); Each binding ships several runnable examples (streaming, backtest, live feed);
[`examples/README.md`](examples/README.md) is the full cross-language index. [`examples/README.md`](examples/README.md) is the full cross-language index.
The wickra-core crate is `unsafe`-forbidden, so every binding inherits a The wickra-core crate is `unsafe`-forbidden, so the native bindings are
memory-safe implementation. memory-safe end to end. The C ABI runs the same safe core; only its thin FFI
boundary uses `unsafe`, and the caller owns handle lifetimes (`_new` / `_free`).
## Rust API ## Rust API
@@ -244,13 +248,15 @@ wickra/
├── bindings/ ├── bindings/
│ ├── python/ PyO3 + maturin (publishes on PyPI) │ ├── python/ PyO3 + maturin (publishes on PyPI)
│ ├── node/ napi-rs (publishes on npm) │ ├── node/ napi-rs (publishes on npm)
── wasm/ wasm-bindgen (browsers, bundlers, Node) ── wasm/ wasm-bindgen (browsers, bundlers, Node)
│ └── c/ C ABI (cdylib + staticlib) + generated include/wickra.h
├── examples/ examples/README.md indexes every language ├── examples/ examples/README.md indexes every language
│ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe │ ├── data/ real BTCUSDT OHLCV datasets, one per timeframe
│ ├── rust/ Rust workspace member (`wickra-examples`) │ ├── rust/ Rust workspace member (`wickra-examples`)
│ ├── python/ backtest, live trading, parallel assets, multi-tf │ ├── python/ backtest, live trading, parallel assets, multi-tf
│ ├── node/ streaming, backtest, live trading (load `wickra`) │ ├── node/ streaming, backtest, live trading (load `wickra`)
── wasm/ browser demo for `wickra-wasm` ── wasm/ browser demo for `wickra-wasm`
│ └── c/ C smoke + streaming, C++ RAII wrapper
└── .github/workflows/ CI and release pipelines └── .github/workflows/ CI and release pipelines
``` ```
@@ -278,6 +284,11 @@ wasm-pack build bindings/wasm --target web --release --features panic-hook
# Node binding (requires @napi-rs/cli) # Node binding (requires @napi-rs/cli)
cd bindings/node && npm install && npm run build && npm test cd bindings/node && npm install && npm run build && npm test
# C ABI (cdylib + staticlib + generated header)
cargo build -p wickra-c --release
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
cmake --build examples/c/build && ctest --test-dir examples/c/build --output-on-failure
``` ```
## Testing ## Testing
+5 -1
View File
@@ -58,7 +58,11 @@ artifacts, and (4) a healthy dependency supply chain.
- *Memory safety* — the core and all bindings are written in Rust. The crates - *Memory safety* — the core and all bindings are written in Rust. The crates
forbid or minimise `unsafe`, so the compiler guarantees memory and thread forbid or minimise `unsafe`, so the compiler guarantees memory and thread
safety for the indicator logic. safety for the indicator logic. The one exception is the C ABI
([`bindings/c`](bindings/c)), whose thin FFI shim is necessarily `unsafe`
because it dereferences caller-supplied pointers; it adds no indicator logic,
validates every handle for NULL, and never lets a panic cross the boundary, so
the safe core's guarantees still cover all computation.
- *Input robustness* — every indicator validates its parameters and rejects - *Input robustness* — every indicator validates its parameters and rejects
non-finite inputs at construction; behaviour on edge cases (flat markets, non-finite inputs at construction; behaviour on edge cases (flat markets,
warmup, reset) is pinned by unit tests, and the public update paths are warmup, reset) is pinned by unit tests, and the public update paths are
+4 -2
View File
@@ -3,8 +3,9 @@
This document describes Wickra's attack surface and the threats considered, This document describes Wickra's attack surface and the threats considered,
together with their mitigations. It complements the security assurance case in together with their mitigations. It complements the security assurance case in
[`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis [`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis
library (a Rust core with Python, Node.js and WebAssembly bindings), not a library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI),
network service or trading system; the attack surface is correspondingly small. not a network service or trading system; the attack surface is correspondingly
small.
## Assets ## Assets
@@ -31,6 +32,7 @@ network service or trading system; the attack surface is correspondingly small.
| Threat | Mitigation | | Threat | Mitigation |
| --- | --- | | --- | --- |
| Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. | | Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. |
| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. |
| Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. | | Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. |
| Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. | | Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. |
| Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. | | Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. |
+46
View File
@@ -0,0 +1,46 @@
[package]
name = "wickra-c"
description = "C ABI (cdylib + staticlib) for the Wickra streaming-first technical indicators library — the hub every C-capable language (C, C++, Go, C#, Java, R) links against."
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
readme = "README.md"
keywords.workspace = true
categories.workspace = true
publish = false
[lib]
name = "wickra"
crate-type = ["cdylib", "staticlib"]
# The C ABI inherently needs `unsafe` (raw pointers across the FFI boundary,
# `#[export_name]` symbol control). The workspace forbids `unsafe_code`, so this
# crate cannot inherit `workspace = true`; it mirrors every workspace lint and
# only relaxes `unsafe_code` to `allow` (parity with how the proc-macro bindings
# emit their unsafe). The Rust core stays `unsafe`-forbidden — this is the one
# crate where the boundary lives.
[lints.rust]
unsafe_code = "allow"
missing_debug_implementations = "warn"
unreachable_pub = "warn"
unused_must_use = "deny"
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
module_name_repetitions = "allow"
must_use_candidate = "allow"
missing_errors_doc = "allow"
missing_panics_doc = "allow"
cast_precision_loss = "allow"
cast_possible_truncation = "allow"
cast_sign_loss = "allow"
similar_names = "allow"
float_cmp = "allow"
[dependencies]
wickra-core = { workspace = true }
+59
View File
@@ -0,0 +1,59 @@
# wickra-c
C ABI for [Wickra](https://github.com/wickra-lib/wickra) — streaming-first
technical indicators with a Rust core. This crate is the **hub**: it compiles the
core to a C-compatible shared/static library plus a generated header, so any
C-capable language (C, C++, Go, C#, Java, R) links against one artifact instead
of re-wiring every indicator natively.
The native Python, Node, and WebAssembly bindings are unaffected — this is
additive, for the ecosystems without first-class Rust tooling.
## Artifacts
```sh
cargo build -p wickra-c --release
```
- `target/release/libwickra.{so,dylib}` / `wickra.dll` (+ `wickra.dll.lib` import lib on Windows)
- `target/release/libwickra.a` / `wickra.lib` (static)
- [`include/wickra.h`](include/wickra.h) — generated by cbindgen, committed.
## API shape
Each indicator is exposed as five `extern "C"` functions over an opaque handle:
```c
struct Sma *wickra_sma_new(uintptr_t period); /* NULL on bad params */
double wickra_sma_update(struct Sma *h, double value); /* NaN during warmup */
void wickra_sma_batch(struct Sma *h, const double *in, double *out, uintptr_t n);
void wickra_sma_reset(struct Sma *h);
void wickra_sma_free(struct Sma *h); /* exactly once per _new */
```
Conventions:
- **Opaque handles.** `wickra_<ind>_new` returns a `T *` you must release with
exactly one `wickra_<ind>_free`. There is no RAII across the boundary.
- **NaN sentinel.** Scalar outputs return `NaN` while warming up or on a `NULL`
handle, mirroring the other bindings — no error codes for the common path.
- **Caller-owned batch buffers.** `wickra_<ind>_batch` writes one output per
input into a buffer you provide; nothing is allocated across the boundary.
- **NULL-safe.** Every function tolerates a `NULL` handle without crashing.
## Header regeneration
The header is generated and committed; CI checks it is in sync:
```sh
cbindgen --config bindings/c/cbindgen.toml --crate wickra-c --output bindings/c/include/wickra.h
```
## Examples
Runnable C examples (build via CMake or a direct compiler invocation) live in
[`examples/c`](../../examples/c).
## License
`MIT OR Apache-2.0`, the same as the rest of Wickra.
+20
View File
@@ -0,0 +1,20 @@
language = "C"
header = "/* Wickra C ABI — generated by cbindgen. Do not edit by hand. */"
include_guard = "WICKRA_H"
pragma_once = true
# Wrap the declarations in `extern "C"` under __cplusplus so the header is usable
# from C++ (the optional wickra.hpp RAII layer and any C++ consumer).
cpp_compat = true
tab_width = 4
# Off: cbindgen copies the Rust struct/fn doc comments verbatim, and some core
# indicator docs contain markdown (e.g. `**1/8**/**7/8**`) whose `*/` would close
# the C block comment early and break the header. Usage docs live in the crate
# README and examples; the header is a pure declaration contract.
documentation = false
[parse]
# Parse wickra-core too so the opaque indicator handle types (Sma, Ema, …) are
# discovered and emitted as forward-declared opaque structs. Their fields are
# never exposed — only `T *` handles cross the boundary.
parse_deps = true
include = ["wickra-core"]
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
// Optional C++ convenience layer over the Wickra C ABI (`wickra.h`).
//
// The C ABI hands out raw handles that must be released exactly once with the
// matching `wickra_<ind>_free`. `wickra::Handle` wraps that in a move-only RAII
// owner so the free happens automatically at scope exit:
//
// #include "wickra.hpp"
//
// wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(14));
// if (sma) {
// double v = wickra_sma_update(sma.get(), 42.0); // NaN during warmup
// }
// // sma is freed here
//
// This is header-only and adds no runtime cost beyond the C calls themselves.
#ifndef WICKRA_HPP
#define WICKRA_HPP
#include "wickra.h"
#include <utility>
namespace wickra {
/// Move-only RAII owner of a Wickra handle. `T` is the opaque indicator type and
/// `Free` its `wickra_<ind>_free` function.
template <typename T, void (*Free)(T *)>
class Handle {
public:
explicit Handle(T *ptr) noexcept : ptr_(ptr) {}
~Handle() {
if (ptr_ != nullptr) {
Free(ptr_);
}
}
Handle(const Handle &) = delete;
Handle &operator=(const Handle &) = delete;
Handle(Handle &&other) noexcept : ptr_(std::exchange(other.ptr_, nullptr)) {}
Handle &operator=(Handle &&other) noexcept {
if (this != &other) {
if (ptr_ != nullptr) {
Free(ptr_);
}
ptr_ = std::exchange(other.ptr_, nullptr);
}
return *this;
}
/// The raw handle, for passing to the `wickra_<ind>_*` functions.
T *get() const noexcept { return ptr_; }
/// True if the handle is non-null (construction succeeded).
explicit operator bool() const noexcept { return ptr_ != nullptr; }
private:
T *ptr_;
};
} // namespace wickra
#endif // WICKRA_HPP
+44290
View File
File diff suppressed because it is too large Load Diff
+5 -3
View File
@@ -9,7 +9,8 @@
prebuilt native binary, no system dependencies.** prebuilt native binary, no system dependencies.**
Wickra is a multi-language technical-analysis library with a Rust core and Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1) bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Node.js binding (napi-rs); the exact same implementation. This package is the Node.js binding (napi-rs);
it exposes 200+ streaming-first indicators across sixteen families. it exposes 200+ streaming-first indicators across sixteen families.
@@ -55,8 +56,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org> - **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node) - **Runnable examples:** [`examples/node/`](https://github.com/wickra-lib/wickra/tree/main/examples/node)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer ## Disclaimer
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-darwin-arm64", "name": "wickra-darwin-arm64",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.", "description": "Native binding for wickra (macOS Apple Silicon). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-arm64.node", "main": "wickra.darwin-arm64.node",
"files": [ "files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-darwin-x64", "name": "wickra-darwin-x64",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.", "description": "Native binding for wickra (macOS Intel). Installed automatically as an optional dependency of wickra on matching platforms.",
"main": "wickra.darwin-x64.node", "main": "wickra.darwin-x64.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-linux-arm64-gnu", "name": "wickra-linux-arm64-gnu",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (linux arm64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.", "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", "main": "wickra.linux-arm64-gnu.node",
"files": [ "files": [
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra-linux-x64-gnu", "name": "wickra-linux-x64-gnu",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (linux x64 GNU). Installed automatically as an optional dependency of wickra on matching platforms.", "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", "main": "wickra.linux-x64-gnu.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-win32-arm64-msvc", "name": "wickra-win32-arm64-msvc",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (Windows arm64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.", "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", "main": "wickra.win32-arm64-msvc.node",
"files": [ "files": [
@@ -1,6 +1,6 @@
{ {
"name": "wickra-win32-x64-msvc", "name": "wickra-win32-x64-msvc",
"version": "0.7.4", "version": "0.7.5",
"description": "Native binding for wickra (Windows x64 MSVC). Installed automatically as an optional dependency of wickra on matching platforms.", "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", "main": "wickra.win32-x64-msvc.node",
"files": [ "files": [
+20 -20
View File
@@ -1,12 +1,12 @@
{ {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.5",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "^2.18.0" "@napi-rs/cli": "^2.18.0"
@@ -15,12 +15,12 @@
"node": ">= 18" "node": ">= 18"
}, },
"optionalDependencies": { "optionalDependencies": {
"wickra-darwin-arm64": "0.7.4", "wickra-darwin-arm64": "0.7.5",
"wickra-darwin-x64": "0.7.4", "wickra-darwin-x64": "0.7.5",
"wickra-linux-arm64-gnu": "0.7.4", "wickra-linux-arm64-gnu": "0.7.5",
"wickra-linux-x64-gnu": "0.7.4", "wickra-linux-x64-gnu": "0.7.5",
"wickra-win32-arm64-msvc": "0.7.4", "wickra-win32-arm64-msvc": "0.7.5",
"wickra-win32-x64-msvc": "0.7.4" "wickra-win32-x64-msvc": "0.7.5"
} }
}, },
"node_modules/@napi-rs/cli": { "node_modules/@napi-rs/cli": {
@@ -41,8 +41,8 @@
} }
}, },
"node_modules/wickra-darwin-arm64": { "node_modules/wickra-darwin-arm64": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-darwin-arm64/-/wickra-darwin-arm64-0.7.5.tgz",
"integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==", "integrity": "sha512-4eZiBR/yGUdr4nzhEUFy2i69XgNx64iI2ax/LPamsThgylC0KpHOZKK19QzJ2d9KbK4C8nMjME5FLuR+4GNEwQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -57,8 +57,8 @@
} }
}, },
"node_modules/wickra-darwin-x64": { "node_modules/wickra-darwin-x64": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-darwin-x64/-/wickra-darwin-x64-0.7.5.tgz",
"integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==", "integrity": "sha512-6hf8zI3QPjTFp4zCpmgUwDvNtu6jHqNUHKD5e55POo0CgA52HkpyxSPtVm8TGTIZDI7kPjlbOdBM8CJ76mmXwA==",
"cpu": [ "cpu": [
"x64" "x64"
@@ -73,8 +73,8 @@
} }
}, },
"node_modules/wickra-linux-arm64-gnu": { "node_modules/wickra-linux-arm64-gnu": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-linux-arm64-gnu/-/wickra-linux-arm64-gnu-0.7.5.tgz",
"integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==", "integrity": "sha512-kSe6y0xBMSiqdPLXNjwop5WZdHtvdBNKSEBCwZ4hFq33p4apW25/wrlzv9/oDuyD4kuPabJEhCCnFOplh58CUg==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -89,8 +89,8 @@
} }
}, },
"node_modules/wickra-linux-x64-gnu": { "node_modules/wickra-linux-x64-gnu": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-linux-x64-gnu/-/wickra-linux-x64-gnu-0.7.5.tgz",
"integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==", "integrity": "sha512-tWBWS4qz7hxM4xnpFb59bhf6TaLwXq0Z3jEa/2l7r8PiHA94g8r8S53NRMiT+4yiL5hSWe/nUiC/YXdRrhEZ4g==",
"cpu": [ "cpu": [
"x64" "x64"
@@ -105,8 +105,8 @@
} }
}, },
"node_modules/wickra-win32-arm64-msvc": { "node_modules/wickra-win32-arm64-msvc": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-win32-arm64-msvc/-/wickra-win32-arm64-msvc-0.7.5.tgz",
"integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==", "integrity": "sha512-EXIckHxAtF75PUGDKRzXyqMe9ldP0JjSdu68WFN6iJfp+McYrGu6h40TEJlQ/oUEIoPqiZB/xhVyo/el5Lg7zw==",
"cpu": [ "cpu": [
"arm64" "arm64"
@@ -121,8 +121,8 @@
} }
}, },
"node_modules/wickra-win32-x64-msvc": { "node_modules/wickra-win32-x64-msvc": {
"version": "0.7.4", "version": "0.7.5",
"resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.4.tgz", "resolved": "https://registry.npmjs.org/wickra-win32-x64-msvc/-/wickra-win32-x64-msvc-0.7.5.tgz",
"integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==", "integrity": "sha512-Yfsqq1Xwp6hdxMyLze411vNdo7BDwI6+lPSe7A9XdqyPecNDbtKwYLpsal2r8EHbNzqM+R8XnuRtUaEQS5VlUQ==",
"cpu": [ "cpu": [
"x64" "x64"
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.5",
"description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.", "description": "Streaming-first technical indicators: incremental, fast, install-free. Node bindings powered by Rust.",
"author": "kingchenc <support@wickra.org>", "author": "kingchenc <support@wickra.org>",
"main": "index.js", "main": "index.js",
@@ -47,12 +47,12 @@
"node": ">= 18" "node": ">= 18"
}, },
"optionalDependencies": { "optionalDependencies": {
"wickra-linux-x64-gnu": "0.7.4", "wickra-linux-x64-gnu": "0.7.5",
"wickra-linux-arm64-gnu": "0.7.4", "wickra-linux-arm64-gnu": "0.7.5",
"wickra-darwin-x64": "0.7.4", "wickra-darwin-x64": "0.7.5",
"wickra-darwin-arm64": "0.7.4", "wickra-darwin-arm64": "0.7.5",
"wickra-win32-x64-msvc": "0.7.4", "wickra-win32-x64-msvc": "0.7.5",
"wickra-win32-arm64-msvc": "0.7.4" "wickra-win32-arm64-msvc": "0.7.5"
}, },
"scripts": { "scripts": {
"build": "napi build --platform --release", "build": "napi build --platform --release",
+5 -3
View File
@@ -9,7 +9,8 @@
system dependencies, no C build tooling.** system dependencies, no C build tooling.**
Wickra is a multi-language technical-analysis library with a Rust core and Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1) bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading bots and historical backtests share streaming state machine, so live trading bots and historical backtests share
the exact same implementation. This package is the Python binding (PyO3); it the exact same implementation. This package is the Python binding (PyO3); it
exposes 200+ streaming-first indicators across sixteen families. exposes 200+ streaming-first indicators across sixteen families.
@@ -54,8 +55,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org> - **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python) - **Runnable examples:** [`examples/python/`](https://github.com/wickra-lib/wickra/tree/main/examples/python)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer ## Disclaimer
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project] [project]
name = "wickra" name = "wickra"
version = "0.7.4" version = "0.7.5"
description = "Streaming-first technical indicators: incremental, fast, install-free." description = "Streaming-first technical indicators: incremental, fast, install-free."
readme = "README.md" readme = "README.md"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
+5 -3
View File
@@ -9,7 +9,8 @@
wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.** wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.**
Wickra is a multi-language technical-analysis library with a Rust core and Wickra is a multi-language technical-analysis library with a Rust core and
bindings for Python, Node.js, and WebAssembly. Every indicator is an O(1) bindings for Python, Node.js and WebAssembly, plus a C ABI for C/C++ and any
other C-capable language. Every indicator is an O(1)
streaming state machine, so live trading dashboards and historical backtests streaming state machine, so live trading dashboards and historical backtests
share the exact same implementation. This package is the WebAssembly binding share the exact same implementation. This package is the WebAssembly binding
(wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first (wasm-bindgen, built for the `web` target); it exposes 200+ streaming-first
@@ -54,8 +55,9 @@ the main repository and documentation site:
- **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org> - **Docs** (quickstarts, cookbook, TA-Lib migration): <https://docs.wickra.org>
- **Runnable browser examples:** [`examples/wasm/`](https://github.com/wickra-lib/wickra/tree/main/examples/wasm) - **Runnable browser examples:** [`examples/wasm/`](https://github.com/wickra-lib/wickra/tree/main/examples/wasm)
Wickra ships four bindings Python, Node.js, WebAssembly, and Rust — that all Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a
expose the same indicators from the shared, `unsafe`-forbidden Rust core. C ABI hub that any C-capable language (C, C++, Go, C#, Java, R) links against —
all exposing the same indicators from the shared, `unsafe`-forbidden Rust core.
## Disclaimer ## Disclaimer
+27
View File
@@ -21,6 +21,33 @@ The Rust examples live in the `wickra-examples` workspace member crate.
| `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` | | `strategy_macd_adx.rs` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `cargo run --release -p wickra-examples --bin strategy_macd_adx` |
| `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` | | `strategy_bollinger_squeeze.rs` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) trailing stop. | `cargo run --release -p wickra-examples --bin strategy_bollinger_squeeze` |
## C / C++ — `examples/c/`
Build the library first (`cargo build -p wickra-c --release`), then build and run
the examples via CMake:
`cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"`
`cmake --build examples/c/build``ctest --test-dir examples/c/build`.
| Example | What it does | CMake target |
| --- | --- | --- |
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
| `streaming.c` | Feed a synthetic price series through SMA / EMA / RSI / MACD tick by tick. | `streaming` |
| `backtest.c` | Basket of indicators over an OHLCV CSV; defaults to the bundled BTCUSDT daily dataset. | `backtest` |
| `multi_timeframe.c` | Resample the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and print indicators per timeframe. | `multi_timeframe` |
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. | `parallel_assets` |
| `strategy_rsi_mean_reversion.c` | Hourly BTCUSDT mean-reversion using RSI(14) thresholds, with PnL / Sharpe / max-DD summary. | `strategy_rsi_mean_reversion` |
| `strategy_macd_adx.c` | Hourly BTCUSDT trend-follower: MACD crossover entries gated by ADX(14) > 20. | `strategy_macd_adx` |
| `strategy_bollinger_squeeze.c` | Daily BTCUSDT Bollinger-squeeze breakout with ATR(14) stop. | `strategy_bollinger_squeeze` |
| `fetch_btcusdt.c` | Download real BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). | `fetch_btcusdt` |
| `live_binance.c` | Poll the Binance REST klines endpoint via `curl` and stream closed candles through RSI(14). | `live_binance` |
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../bindings/c/include/wickra.hpp): construct, move, auto-free. | `cpp_smoke` |
The data-driven examples (`backtest`, `multi_timeframe`, `parallel_assets`, the
three `strategy_*`) build against the bundled datasets and run under `ctest`.
`fetch_btcusdt` and `live_binance` reach the network, so they are built but not
run in CI; run them by hand. `parallel_assets` links OpenMP when the toolchain
provides it and falls back to a single-threaded run otherwise.
## Python — `examples/python/` ## Python — `examples/python/`
| Example | What it does | Run | | Example | What it does | Run |
+82
View File
@@ -0,0 +1,82 @@
cmake_minimum_required(VERSION 3.15)
project(wickra_c_examples C CXX)
# 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}/../../bindings/c/include")
# Absolute path to the bundled OHLCV datasets, baked into the data-driven
# examples as a compile definition so they run from any working directory (the
# C counterpart of the Rust examples' CARGO_MANIFEST_DIR).
get_filename_component(WICKRA_DATA_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../data" ABSOLUTE)
# OpenMP is optional: parallel_assets links it when present and falls back to a
# single-threaded run otherwise.
find_package(OpenMP QUIET)
# 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()
enable_testing()
# Build one example and link it to the Wickra library. On Windows the DLL is
# copied next to the executable so the loader finds it at run time. With
# register_test=TRUE the example is also run as a ctest; network examples pass
# FALSE (they are built only, never run in CI).
function(add_wickra_example name source register_test)
add_executable(${name} ${source})
target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}")
target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}")
target_compile_definitions(${name} PRIVATE "WICKRA_DATA_DIR=\"${WICKRA_DATA_DIR}\"")
if(UNIX AND NOT APPLE)
target_link_libraries(${name} PRIVATE m)
endif()
if(WIN32)
add_custom_command(TARGET ${name} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>")
endif()
if(register_test)
add_test(NAME ${name} COMMAND ${name})
if(NOT WIN32)
set_tests_properties(${name} PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
endif()
endif()
endfunction()
# Offline examples built and run as ctests.
add_wickra_example(smoke smoke.c TRUE) # links the boundary, asserts values
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
add_wickra_example(multi_timeframe multi_timeframe.c TRUE) # resample + per-TF indicators
add_wickra_example(parallel_assets parallel_assets.c TRUE) # serial vs OpenMP fan-out
add_wickra_example(strategy_rsi_mean_reversion strategy_rsi_mean_reversion.c TRUE)
add_wickra_example(strategy_macd_adx strategy_macd_adx.c TRUE)
add_wickra_example(strategy_bollinger_squeeze strategy_bollinger_squeeze.c TRUE)
if(OpenMP_C_FOUND)
target_link_libraries(parallel_assets PRIVATE OpenMP::OpenMP_C)
endif()
# Network examples built (so they stay compilable) but not run in CI.
add_wickra_example(fetch_btcusdt fetch_btcusdt.c FALSE) # downloads CSVs via curl
add_wickra_example(live_binance live_binance.c FALSE) # polls Binance REST via curl
+90
View File
@@ -0,0 +1,90 @@
# Wickra — C / C++ examples
The Wickra C ABI is a single shared/static library plus a generated header
([`bindings/c/include/wickra.h`](../../bindings/c/include/wickra.h)). Any
C-capable language links against the same artifact; these examples show the
plain-C path.
## Build the library
From the workspace root:
```sh
cargo build -p wickra-c --release
```
This produces, in `target/release/`:
| Platform | Shared library | Link target |
|----------|----------------|-------------|
| Linux | `libwickra.so` | `-lwickra` |
| macOS | `libwickra.dylib` | `-lwickra` |
| Windows (MSVC) | `wickra.dll` | `wickra.dll.lib` (import lib) |
A static library (`libwickra.a` / `wickra.lib`) is emitted alongside.
## Build and run the smoke example
### With CMake (portable, used by CI)
```sh
cmake -S examples/c -B examples/c/build -DWICKRA_LIB_DIR="$PWD/target/release"
cmake --build examples/c/build
ctest --test-dir examples/c/build --output-on-failure
```
### Directly with a compiler
```sh
# Linux / macOS
cc examples/c/smoke.c -I bindings/c/include -L target/release -lwickra -lm -o smoke
LD_LIBRARY_PATH=target/release ./smoke # macOS: DYLD_LIBRARY_PATH
# Windows (MinGW gcc, linking the DLL directly)
gcc examples/c/smoke.c -I bindings/c/include target/release/wickra.dll -lm -o smoke.exe
```
Expected output:
```
OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)
```
## The examples
| Example | What it does |
|---------|--------------|
| `smoke.c` | Links the boundary and asserts SMA streaming / batch / reset / NULL-safety values. |
| `streaming.c` | Feeds a synthetic price series through SMA / EMA / RSI / MACD tick by tick. |
| `backtest.c` | Runs an indicator basket over an OHLCV CSV (defaults to the bundled daily dataset). |
| `multi_timeframe.c` | Resamples the bundled 1-minute CSV to 5m / 15m / 1h / 4h / 1d and prints indicators per timeframe. |
| `parallel_assets.c` | Serial vs OpenMP fan-out over a synthetic panel (one handle per asset), with speedup. |
| `strategy_rsi_mean_reversion.c` | Hourly RSI(14) mean-reversion with a PnL / Sharpe / max-drawdown summary. |
| `strategy_macd_adx.c` | Hourly MACD crossover gated by ADX(14) > 20. |
| `strategy_bollinger_squeeze.c` | Daily Bollinger-squeeze breakout with an ATR(14) stop. |
| `fetch_btcusdt.c` | Downloads BTCUSDT klines from the Binance REST API into `examples/data/` (shells out to `curl`). |
| `live_binance.c` | Polls the Binance REST klines endpoint via `curl` and streams closed candles through RSI(14). |
| `smoke.cpp` | C++ RAII via `wickra::Handle` from [`wickra.hpp`](../../bindings/c/include/wickra.hpp). |
`ctest` builds and runs every example except `fetch_btcusdt` and `live_binance`,
which reach the network and are built only — run those two by hand. The C ABI
exposes only the indicators, not the `wickra-data` IO layer, so the examples read
CSV ([`wickra_csv.h`](wickra_csv.h)) and resample themselves; the network ones
shell out to the system `curl` rather than adding an HTTP/TLS dependency.
## Usage shape
Every indicator follows the same five-function pattern over an opaque handle:
```c
#include "wickra.h"
struct Sma *sma = wickra_sma_new(14); /* NULL on invalid params */
double v = wickra_sma_update(sma, 42.0); /* NaN during warmup */
wickra_sma_reset(sma); /* back to fresh state */
wickra_sma_free(sma); /* exactly once per _new */
```
There is no RAII across the C boundary: every `wickra_<ind>_new` must be paired
with exactly one `wickra_<ind>_free`. All functions are NULL-safe (a NULL handle
yields `NaN` / a no-op, never a crash).
+139
View File
@@ -0,0 +1,139 @@
/* Backtest a basket of indicators against an OHLCV CSV with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/backtest.rs` and
* `examples/node/backtest.js`: load an OHLCV file, run a basket of indicators
* (scalar ones via `_batch`, candle ones streamed bar by bar), and print the
* most recent value of each. Defaults to the bundled BTCUSDT daily dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/backtest.c -I bindings/c/include -L target/release -lwickra -lm -o backtest
* ./backtest [path/to/ohlcv.csv]
*/
#define WICKRA_CSV_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
/* Last finite value of a scalar batch result, or NaN if none warmed up. */
static double last_finite(const double *v, size_t n) {
for (size_t i = n; i-- > 0;) {
if (isfinite(v[i])) {
return v[i];
}
}
return NAN;
}
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "backtest: no candles read from %s\n", path);
return 1;
}
double *closes = (double *)malloc(n * sizeof(*closes));
double *rsi_out = (double *)malloc(n * sizeof(*rsi_out));
double *ema_out = (double *)malloc(n * sizeof(*ema_out));
struct Rsi *rsi = wickra_rsi_new(14);
struct Ema *ema = wickra_ema_new(20);
struct BollingerBands *bb = wickra_bollinger_bands_new(20, 2.0);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Atr *atr = wickra_atr_new(14);
struct Adx *adx = wickra_adx_new(14);
struct Obv *obv = wickra_obv_new();
if (closes == NULL || rsi_out == NULL || ema_out == NULL || rsi == NULL ||
ema == NULL || bb == NULL || macd == NULL || atr == NULL || adx == NULL ||
obv == NULL) {
fprintf(stderr, "backtest: allocation failed\n");
return 1;
}
for (size_t i = 0; i < n; ++i) {
closes[i] = candles[i].close;
}
/* Scalar indicators: one batch call over the close series. */
wickra_rsi_batch(rsi, closes, rsi_out, n);
wickra_ema_batch(ema, closes, ema_out, n);
/* Multi-output and candle indicators: streamed bar by bar, keeping the
* last value each one produced. */
WickraBollingerOutput last_bb = {0};
int have_bb = 0;
WickraMacdOutput last_macd = {0};
int have_macd = 0;
WickraAdxOutput last_adx = {0};
int have_adx = 0;
double last_atr = NAN;
double last_obv = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
WickraBollingerOutput bo;
if (wickra_bollinger_bands_update(bb, c->close, &bo)) {
last_bb = bo;
have_bb = 1;
}
WickraMacdOutput mo;
if (wickra_macd_indicator_update(macd, c->close, &mo)) {
last_macd = mo;
have_macd = 1;
}
double av = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
if (isfinite(av)) {
last_atr = av;
}
WickraAdxOutput ao;
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
c->timestamp, &ao)) {
last_adx = ao;
have_adx = 1;
}
double ov = wickra_obv_update(obv, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
if (isfinite(ov)) {
last_obv = ov;
}
}
printf("backtest summary for %s (%llu bars)\n", path, (unsigned long long)n);
printf(" RSI(14) = %9.4f\n", last_finite(rsi_out, n));
printf(" EMA(20) = %9.4f\n", last_finite(ema_out, n));
if (have_bb) {
printf(" BB(20,2) upper=%9.4f middle=%9.4f lower=%9.4f sd=%8.4f\n",
last_bb.upper, last_bb.middle, last_bb.lower, last_bb.stddev);
}
if (have_macd) {
printf(" MACD macd=%9.4f signal=%9.4f hist=%9.4f\n", last_macd.macd,
last_macd.signal, last_macd.histogram);
}
printf(" ATR(14) = %9.4f\n", last_atr);
if (have_adx) {
printf(" ADX(14) +DI=%6.2f -DI=%6.2f ADX=%6.2f\n", last_adx.plus_di,
last_adx.minus_di, last_adx.adx);
}
printf(" OBV = %14.2f\n", last_obv);
wickra_rsi_free(rsi);
wickra_ema_free(ema);
wickra_bollinger_bands_free(bb);
wickra_macd_indicator_free(macd);
wickra_atr_free(atr);
wickra_adx_free(adx);
wickra_obv_free(obv);
free(closes);
free(rsi_out);
free(ema_out);
free(candles);
return 0;
}
+311
View File
@@ -0,0 +1,311 @@
/* Download real BTCUSDT spot candles from the Binance REST API and write them as
* CSV datasets under examples/data/ the C counterpart of
* `examples/rust/src/bin/fetch_btcusdt.rs` and `examples/node/fetch_btcusdt.js`.
*
* Like the Rust example, HTTPS is handled by shelling out to the system `curl`
* (shipped with Windows 10+, macOS and every Linux distro), so this example adds
* no HTTP/TLS dependency. Each klines response is capped at 1000 rows, so larger
* datasets are paginated backwards through `endTime`. Only fully closed candles
* are kept.
*
* This example talks to the network, so it is built but NOT run as a ctest.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/fetch_btcusdt.c -I bindings/c/include -L target/release -lwickra -lm -o fetch_btcusdt
* ./fetch_btcusdt
*/
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _WIN32
#define POPEN _popen
#define PCLOSE _pclose
#else
#define POPEN popen
#define PCLOSE pclose
#endif
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define SYMBOL "BTCUSDT"
#define PAGE_LIMIT 1000
typedef struct {
int64_t open_time;
int64_t close_time;
double open, high, low, close, volume;
} Kline;
typedef struct {
const char *interval;
const char *file;
size_t target;
} Dataset;
/* One dataset per timeframe. The monthly file is `btcusdt-1month.csv`, not
* `-1M`, so it does not collide with `-1m` on case-insensitive filesystems. */
static const Dataset DATASETS[] = {
{"1m", "btcusdt-1m.csv", 50000}, {"5m", "btcusdt-5m.csv", 10000},
{"15m", "btcusdt-15m.csv", 10000}, {"1h", "btcusdt-1h.csv", 10000},
{"12h", "btcusdt-12h.csv", 5000}, {"1d", "btcusdt-1d.csv", 5000},
{"1M", "btcusdt-1month.csv", 5000},
};
/* Run `curl <url>` and return its stdout as a malloc'd, NUL-terminated buffer
* (caller frees), or NULL on failure. */
static char *curl_get(const char *url) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"curl --silent --show-error --fail --max-time 30 \"%s\"", url);
FILE *p = POPEN(cmd, "r");
if (p == NULL) {
fprintf(stderr, "could not run curl (install it / put it on PATH)\n");
return NULL;
}
size_t cap = 1 << 16, len = 0;
char *buf = (char *)malloc(cap);
if (buf == NULL) {
PCLOSE(p);
return NULL;
}
size_t got;
char tmp[8192];
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
if (len + got + 1 > cap) {
cap *= 2;
char *grown = (char *)realloc(buf, cap);
if (grown == NULL) {
free(buf);
PCLOSE(p);
return NULL;
}
buf = grown;
}
memcpy(buf + len, tmp, got);
len += got;
}
int rc = PCLOSE(p);
buf[len] = '\0';
if (rc != 0 || len == 0) {
fprintf(stderr, "curl failed for %s\n", url);
free(buf);
return NULL;
}
return buf;
}
/* Parse a Binance klines JSON array. Each row is
* [openTime, "open", "high", "low", "close", "volume", closeTime, ...].
* Appends parsed rows to *rows (grown via realloc) and returns the new count. */
static size_t parse_klines(const char *body, Kline **rows, size_t count, size_t *cap) {
int depth = 0;
int in_string = 0;
int field = -1; /* -1 = not inside a row yet */
char tok[64];
size_t tok_len = 0;
Kline cur = {0};
for (const char *s = body; *s; ++s) {
char ch = *s;
if (in_string) {
if (ch == '"') {
in_string = 0;
} else if (tok_len + 1 < sizeof(tok)) {
tok[tok_len++] = ch;
}
continue;
}
switch (ch) {
case '[':
depth++;
if (depth == 2) {
field = 0;
tok_len = 0;
memset(&cur, 0, sizeof(cur));
}
break;
case ']':
if (depth == 2) {
/* close the final field of this row, then emit it */
tok[tok_len] = '\0';
if (field == 6) {
cur.close_time = strtoll(tok, NULL, 10);
}
if (*cap == count) {
*cap = *cap ? *cap * 2 : 1024;
Kline *grown = (Kline *)realloc(*rows, *cap * sizeof(Kline));
if (grown == NULL) {
return count;
}
*rows = grown;
}
(*rows)[count++] = cur;
field = -1;
}
depth--;
break;
case '"':
in_string = 1;
break;
case ',':
if (depth == 2) {
tok[tok_len] = '\0';
switch (field) {
case 0: cur.open_time = strtoll(tok, NULL, 10); break;
case 1: cur.open = strtod(tok, NULL); break;
case 2: cur.high = strtod(tok, NULL); break;
case 3: cur.low = strtod(tok, NULL); break;
case 4: cur.close = strtod(tok, NULL); break;
case 5: cur.volume = strtod(tok, NULL); break;
case 6: cur.close_time = strtoll(tok, NULL, 10); break;
default: break;
}
field++;
tok_len = 0;
}
break;
default:
if (depth == 2 && tok_len + 1 < sizeof(tok)) {
tok[tok_len++] = ch;
}
break;
}
}
return count;
}
static int cmp_open(const void *a, const void *b) {
int64_t x = ((const Kline *)a)->open_time;
int64_t y = ((const Kline *)b)->open_time;
return (x > y) - (x < y);
}
/* Paginate backwards until `target` closed candles are collected. */
static size_t collect(const char *interval, size_t target, int64_t now_ms,
Kline **out) {
Kline *rows = NULL;
size_t count = 0, cap = 0;
int64_t end_time = 0; /* 0 = no endTime cap (most recent page) */
int pages = 0;
for (;;) {
char url[256];
if (end_time > 0) {
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
"&limit=%d&endTime=%lld",
SYMBOL, interval, PAGE_LIMIT, (long long)end_time);
} else {
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=%s"
"&limit=%d",
SYMBOL, interval, PAGE_LIMIT);
}
char *body = curl_get(url);
if (body == NULL) {
free(rows);
return 0;
}
Kline *page = NULL;
size_t page_cap = 0;
size_t page_n = parse_klines(body, &page, 0, &page_cap);
free(body);
pages++;
int64_t oldest_open = INT64_MAX;
for (size_t i = 0; i < page_n; ++i) {
if (page[i].open_time < oldest_open) {
oldest_open = page[i].open_time;
}
if (page[i].close_time < now_ms) { /* keep only closed candles */
if (count == cap) {
cap = cap ? cap * 2 : 1024;
Kline *grown = (Kline *)realloc(rows, cap * sizeof(Kline));
if (grown == NULL) {
free(page);
free(rows);
return 0;
}
rows = grown;
}
rows[count++] = page[i];
}
}
fprintf(stderr, "\r %s: collected %llu candles over %d page(s)...",
interval, (unsigned long long)count, pages);
int page_full = page_n >= PAGE_LIMIT;
free(page);
if (count >= target || !page_full || oldest_open == INT64_MAX) {
break;
}
end_time = oldest_open - 1;
}
fprintf(stderr, "\n");
/* Sort ascending, drop duplicate open times, keep the most recent target. */
qsort(rows, count, sizeof(Kline), cmp_open);
size_t uniq = 0;
for (size_t i = 0; i < count; ++i) {
if (uniq == 0 || rows[i].open_time != rows[uniq - 1].open_time) {
rows[uniq++] = rows[i];
}
}
if (uniq > target) {
memmove(rows, rows + (uniq - target), target * sizeof(Kline));
uniq = target;
}
*out = rows;
return uniq;
}
static int write_csv(const char *path, const Kline *rows, size_t n) {
FILE *f = fopen(path, "w");
if (f == NULL) {
return 0;
}
fputs("timestamp,open,high,low,close,volume\n", f);
for (size_t i = 0; i < n; ++i) {
fprintf(f, "%lld,%g,%g,%g,%g,%g\n", (long long)rows[i].open_time,
rows[i].open, rows[i].high, rows[i].low, rows[i].close,
rows[i].volume);
}
fclose(f);
return 1;
}
int main(void) {
int64_t now_ms = (int64_t)time(NULL) * 1000;
printf("Fetching %s klines from Binance into %s\n", SYMBOL, WICKRA_DATA_DIR);
size_t n_datasets = sizeof(DATASETS) / sizeof(DATASETS[0]);
for (size_t d = 0; d < n_datasets; ++d) {
const Dataset *ds = &DATASETS[d];
Kline *rows = NULL;
size_t n = collect(ds->interval, ds->target, now_ms, &rows);
if (n == 0) {
fprintf(stderr, "Binance returned no closed candles for %s\n",
ds->interval);
free(rows);
return 1;
}
char path[256];
snprintf(path, sizeof(path), "%s/%s", WICKRA_DATA_DIR, ds->file);
if (!write_csv(path, rows, n)) {
fprintf(stderr, "could not write %s\n", path);
free(rows);
return 1;
}
printf(" %3s %6llu candles -> %s\n", ds->interval, (unsigned long long)n,
path);
free(rows);
}
printf("Done — %llu datasets written.\n", (unsigned long long)n_datasets);
return 0;
}
+154
View File
@@ -0,0 +1,154 @@
/* Live BTCUSDT indicator with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/live_binance.rs`,
* `examples/python/live_trading.py` and `examples/node/live_trading.js`. Those
* stream Binance over a WebSocket; the C ABI ships only the indicators and no
* socket layer, so this example polls the Binance REST klines endpoint via the
* system `curl` once per interval and feeds each newly *closed* candle into a
* streaming RSI(14). Same "live feed -> incremental indicator" shape, no extra
* dependency.
*
* This example talks to the network and runs until interrupted (Ctrl+C), so it
* is built but NOT run as a ctest.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/live_binance.c -I bindings/c/include -L target/release -lwickra -lm -o live_binance
* ./live_binance [SYMBOL]
*/
#include "wickra.h"
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef _WIN32
#include <windows.h>
#define SLEEP_MS(ms) Sleep(ms)
#define POPEN _popen
#define PCLOSE _pclose
#else
#include <time.h>
#define POPEN popen
#define PCLOSE pclose
static void SLEEP_MS(long ms) {
struct timespec ts = {ms / 1000, (ms % 1000) * 1000000L};
nanosleep(&ts, NULL);
}
#endif
/* Run `curl <url>` and return stdout as a malloc'd, NUL-terminated buffer. */
static char *curl_get(const char *url) {
char cmd[512];
snprintf(cmd, sizeof(cmd),
"curl --silent --show-error --fail --max-time 15 \"%s\"", url);
FILE *p = POPEN(cmd, "r");
if (p == NULL) {
return NULL;
}
size_t cap = 1 << 15, len = 0;
char *buf = (char *)malloc(cap);
if (buf == NULL) {
PCLOSE(p);
return NULL;
}
size_t got;
char tmp[4096];
while ((got = fread(tmp, 1, sizeof(tmp), p)) > 0) {
if (len + got + 1 > cap) {
cap *= 2;
char *grown = (char *)realloc(buf, cap);
if (grown == NULL) {
free(buf);
PCLOSE(p);
return NULL;
}
buf = grown;
}
memcpy(buf + len, tmp, got);
len += got;
}
int rc = PCLOSE(p);
buf[len] = '\0';
if (rc != 0 || len == 0) {
free(buf);
return NULL;
}
return buf;
}
/* Extract the first kline's open time and close price from a klines response of
* the form [[openTime,"open","high","low","close",...],...]. Returns 1 on
* success. The first row (limit=2) is the most recent fully closed candle. */
static int first_kline(const char *body, int64_t *open_time, double *close) {
const char *s = strchr(body, '[');
if (s == NULL) {
return 0;
}
s = strchr(s + 1, '['); /* into the first row */
if (s == NULL) {
return 0;
}
s++;
char tok[64];
int field = 0;
while (*s && *s != ']') {
size_t tl = 0;
while (*s && *s != ',' && *s != ']') {
if (*s != '"' && tl + 1 < sizeof(tok)) {
tok[tl++] = *s;
}
s++;
}
tok[tl] = '\0';
if (field == 0) {
*open_time = strtoll(tok, NULL, 10);
} else if (field == 4) {
*close = strtod(tok, NULL);
return 1;
}
field++;
if (*s == ',') {
s++;
}
}
return 0;
}
int main(int argc, char **argv) {
const char *symbol = (argc > 1) ? argv[1] : "BTCUSDT";
char url[256];
snprintf(url, sizeof(url),
"https://api.binance.com/api/v3/klines?symbol=%s&interval=1m&limit=2",
symbol);
struct Rsi *rsi = wickra_rsi_new(14);
if (rsi == NULL) {
fprintf(stderr, "failed to create RSI\n");
return 1;
}
printf("Listening for %s 1m closes (REST poll, Ctrl+C to stop)...\n", symbol);
int64_t last_open = 0;
for (;;) {
char *body = curl_get(url);
if (body != NULL) {
int64_t open_time = 0;
double close = 0.0;
if (first_kline(body, &open_time, &close) && open_time != last_open) {
last_open = open_time;
double v = wickra_rsi_update(rsi, close);
if (isfinite(v)) {
printf("%s close=%.4f rsi=%.2f\n", symbol, close, v);
} else {
printf("%s close=%.4f rsi=...warmup\n", symbol, close);
}
fflush(stdout);
}
free(body);
}
SLEEP_MS(2000);
}
/* Unreachable in normal use (interrupted by Ctrl+C). */
}
+151
View File
@@ -0,0 +1,151 @@
/* Multi-timeframe indicators with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/multi_timeframe.rs` and
* `examples/python/multi_timeframe.py`: read the bundled 1-minute BTCUSDT CSV
* (or a path on the command line), resample it to 5m / 15m / 1h / 4h / 1d, and
* print the last RSI(14), MACD(12,26,9) histogram and ADX(14) at each timeframe.
*
* The Rust/Python stack resamples through `wickra-data`; the C ABI ships only
* the indicators, so the time-bucket aggregation is done here (open = first,
* high = max, low = min, close = last, volume = sum per bucket).
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/multi_timeframe.c -I bindings/c/include -L target/release -lwickra -lm -o multi_timeframe
* ./multi_timeframe [path/to/1m.csv]
*/
#define WICKRA_CSV_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define ONE_MINUTE_MS 60000LL
/* Aggregate `in` into fixed-width time buckets of `tf_ms`. Writes the bucketed
* candles into the caller-owned `out` (capacity >= count) and returns how many
* buckets were produced. */
static size_t resample(const WickraCandle *in, size_t count, int64_t tf_ms,
WickraCandle *out) {
size_t produced = 0;
int open_bucket = 0;
int64_t bucket_start = 0;
WickraCandle cur = {0};
for (size_t i = 0; i < count; ++i) {
int64_t b = in[i].timestamp - (in[i].timestamp % tf_ms);
if (!open_bucket || b != bucket_start) {
if (open_bucket) {
out[produced++] = cur;
}
bucket_start = b;
cur = in[i];
cur.timestamp = b;
open_bucket = 1;
} else {
if (in[i].high > cur.high) {
cur.high = in[i].high;
}
if (in[i].low < cur.low) {
cur.low = in[i].low;
}
cur.close = in[i].close;
cur.volume += in[i].volume;
}
}
if (open_bucket) {
out[produced++] = cur;
}
return produced;
}
static void summarize(const char *label, const WickraCandle *candles, size_t n) {
if (n == 0) {
printf(" %-5s (empty)\n", label);
return;
}
struct Rsi *rsi = wickra_rsi_new(14);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Adx *adx = wickra_adx_new(14);
double last_rsi = NAN;
double last_hist = NAN;
double last_adx = NAN;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double r = wickra_rsi_update(rsi, c->close);
if (isfinite(r)) {
last_rsi = r;
}
WickraMacdOutput m;
if (wickra_macd_indicator_update(macd, c->close, &m)) {
last_hist = m.histogram;
}
WickraAdxOutput a;
if (wickra_adx_update(adx, c->open, c->high, c->low, c->close, c->volume,
c->timestamp, &a)) {
last_adx = a.adx;
}
}
char b_rsi[16], b_hist[16], b_adx[16];
if (isfinite(last_rsi)) {
snprintf(b_rsi, sizeof(b_rsi), "%6.2f", last_rsi);
} else {
snprintf(b_rsi, sizeof(b_rsi), " --");
}
if (isfinite(last_hist)) {
snprintf(b_hist, sizeof(b_hist), "%+6.2f", last_hist);
} else {
snprintf(b_hist, sizeof(b_hist), " -- ");
}
if (isfinite(last_adx)) {
snprintf(b_adx, sizeof(b_adx), "%6.2f", last_adx);
} else {
snprintf(b_adx, sizeof(b_adx), " --");
}
printf(" %-5s bars=%5llu last_close=%10.2f rsi=%s macd_hist=%s adx=%s\n",
label, (unsigned long long)n, candles[n - 1].close, b_rsi, b_hist, b_adx);
wickra_rsi_free(rsi);
wickra_macd_indicator_free(macd);
wickra_adx_free(adx);
}
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1m.csv";
WickraCandle *ones = NULL;
size_t n = wickra_load_csv(path, &ones);
if (n == 0) {
fprintf(stderr, "multi_timeframe: no candles read from %s\n", path);
return 1;
}
/* Resampling only ever produces fewer candles than the 1m source. */
WickraCandle *buf = (WickraCandle *)malloc(n * sizeof(*buf));
if (buf == NULL) {
fprintf(stderr, "multi_timeframe: allocation failed\n");
free(ones);
return 1;
}
printf("Multi-timeframe view of %s\n", path);
summarize("1m", ones, n);
const struct {
const char *label;
int64_t minutes;
} frames[] = {{"5m", 5}, {"15m", 15}, {"1h", 60}, {"4h", 240}, {"1d", 1440}};
for (size_t f = 0; f < sizeof(frames) / sizeof(frames[0]); ++f) {
size_t m = resample(ones, n, frames[f].minutes * ONE_MINUTE_MS, buf);
summarize(frames[f].label, buf, m);
}
free(buf);
free(ones);
return 0;
}
+156
View File
@@ -0,0 +1,156 @@
/* Parallel multi-asset indicator computation with the Wickra C ABI.
*
* The C counterpart of `examples/rust/src/bin/parallel_assets.rs` (rayon) and
* `examples/python/parallel_assets.py` (the Rust extension drops the GIL). The C
* ABI is the parallelization primitive itself: each `wickra_<ind>_new` handle is
* independent, so the caller fans assets out across threads, one fresh handle per
* asset. Here a serial baseline is compared against an OpenMP `parallel for`.
*
* If the compiler has no OpenMP support the "parallel" pass simply runs the same
* single-threaded loop (speedup ~1x) the result is honest either way.
*
* Build (after `cargo build -p wickra-c --release`):
* cc -fopenmp examples/c/parallel_assets.c -I bindings/c/include -L target/release -lwickra -lm -o parallel_assets
* ./parallel_assets --assets 200 --bars 5000 --indicator sma
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef _OPENMP
#include <omp.h>
#endif
/* Wall-clock seconds (not CPU time, so threaded speedup is measured correctly). */
static double now_seconds(void) {
#ifdef _OPENMP
return omp_get_wtime();
#else
struct timespec ts;
timespec_get(&ts, TIME_UTC);
return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9;
#endif
}
typedef enum { IND_SMA, IND_RSI } Which;
/* Deterministic synthetic (assets, bars) panel, flat row-major. Each asset uses
* an independent LCG seed so the series are uncorrelated but reproducible. */
static void synthesize_panel(double *panel, size_t assets, size_t bars) {
for (size_t a = 0; a < assets; ++a) {
double price = 100.0;
uint32_t state = (uint32_t)(1234567u + a) * 2654435761u;
for (size_t b = 0; b < bars; ++b) {
state = (state * 1103515245u + 12345u) & 0x7FFFFFFFu;
double r = (double)state / (double)0x7FFFFFFFu;
price += (r - 0.5) * 0.4;
panel[a * bars + b] = price;
}
}
}
/* Run one asset's series through a fresh handle into its output slice. */
static void run_one(Which which, const double *prices, double *out, size_t bars) {
if (which == IND_SMA) {
struct Sma *h = wickra_sma_new(14);
wickra_sma_batch(h, prices, out, bars);
wickra_sma_free(h);
} else {
struct Rsi *h = wickra_rsi_new(14);
wickra_rsi_batch(h, prices, out, bars);
wickra_rsi_free(h);
}
}
int main(int argc, char **argv) {
size_t assets = 200;
size_t bars = 5000;
Which which = IND_SMA;
for (int i = 1; i < argc; ++i) {
if (strcmp(argv[i], "--assets") == 0 && i + 1 < argc) {
assets = (size_t)strtoul(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--bars") == 0 && i + 1 < argc) {
bars = (size_t)strtoul(argv[++i], NULL, 10);
} else if (strcmp(argv[i], "--indicator") == 0 && i + 1 < argc) {
++i;
if (strcmp(argv[i], "sma") == 0) {
which = IND_SMA;
} else if (strcmp(argv[i], "rsi") == 0) {
which = IND_RSI;
} else {
fprintf(stderr, "--indicator: expected 'sma' or 'rsi'\n");
return 1;
}
} else {
fprintf(stderr, "usage: parallel_assets [--assets N] [--bars N] "
"[--indicator sma|rsi]\n");
return 1;
}
}
if (assets == 0 || bars == 0) {
fprintf(stderr, "--assets and --bars must be positive\n");
return 1;
}
const char *ind_name = (which == IND_SMA) ? "sma" : "rsi";
printf("Generating %llux%llu synthetic panel...\n", (unsigned long long)assets,
(unsigned long long)bars);
double *panel = (double *)malloc(assets * bars * sizeof(*panel));
double *serial = (double *)malloc(assets * bars * sizeof(*serial));
double *parallel = (double *)malloc(assets * bars * sizeof(*parallel));
if (panel == NULL || serial == NULL || parallel == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
synthesize_panel(panel, assets, bars);
double t0 = now_seconds();
for (size_t a = 0; a < assets; ++a) {
run_one(which, &panel[a * bars], &serial[a * bars], bars);
}
double t_serial = now_seconds() - t0;
printf("Serial: %8.3f s (%llu assets, indicator=%s)\n", t_serial,
(unsigned long long)assets, ind_name);
/* The loop variable is declared outside the `for` and the bound is a plain
* variable: MSVC's OpenMP 2.0 rejects an in-init declaration or a cast in
* the condition (error C3015). */
long a;
long asset_count = (long)assets;
t0 = now_seconds();
#ifdef _OPENMP
#pragma omp parallel for schedule(static)
#endif
for (a = 0; a < asset_count; ++a) {
run_one(which, &panel[(size_t)a * bars], &parallel[(size_t)a * bars], bars);
}
double t_parallel = now_seconds() - t0;
double denom = t_parallel > 1e-9 ? t_parallel : 1e-9;
#ifdef _OPENMP
printf("Parallel: %8.3f s (OpenMP, %d threads, speedup ~%.2fx)\n", t_parallel,
omp_get_max_threads(), t_serial / denom);
#else
printf("Parallel: %8.3f s (no OpenMP at build time — serial, speedup ~%.2fx)\n",
t_parallel, t_serial / denom);
#endif
/* The parallel run must reproduce the serial results exactly. */
for (size_t i = 0; i < assets * bars; ++i) {
int both_nan = isnan(serial[i]) && isnan(parallel[i]);
if (!both_nan && serial[i] != parallel[i]) {
fprintf(stderr, "mismatch at %llu: serial=%g parallel=%g\n",
(unsigned long long)i, serial[i], parallel[i]);
return 1;
}
}
printf("Parallel results match serial results — OK.\n");
free(panel);
free(serial);
free(parallel);
return 0;
}
+64
View File
@@ -0,0 +1,64 @@
/* Smoke test for the Wickra C ABI.
*
* This is the one test the Rust unit tests structurally cannot do: it links a
* foreign C consumer against the generated `wickra.h` + the compiled library and
* exercises the real FFI boundary (symbol export, header correctness, opaque
* handle, pointer ownership, `_free`). If this passes, every C-capable language
* (C, C++, Go, C#, Java, R) can link the same way.
*
* Build (from the workspace root, after `cargo build -p wickra-c --release`):
* cc examples/c/smoke.c -I bindings/c/include target/release/<lib> -lm -o smoke
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
static int near(double a, double b) { return fabs(a - b) < 1e-9; }
int main(void) {
struct Sma *sma = wickra_sma_new(3);
if (sma == NULL) {
printf("FAIL: wickra_sma_new returned NULL\n");
return 1;
}
/* SMA(3): first two outputs are warmup (NaN), then the trailing mean. */
double in[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
double r0 = wickra_sma_update(sma, in[0]); /* NaN (1/3) */
double r1 = wickra_sma_update(sma, in[1]); /* NaN (2/3) */
double r2 = wickra_sma_update(sma, in[2]); /* 2.0 (1+2+3)/3 */
double r3 = wickra_sma_update(sma, in[3]); /* 3.0 (2+3+4)/3 */
if (!isnan(r0) || !isnan(r1)) {
printf("FAIL: warmup not NaN (%f %f)\n", r0, r1);
return 1;
}
if (!near(r2, 2.0) || !near(r3, 3.0)) {
printf("FAIL: streaming values (%f %f), expected (2.0 3.0)\n", r2, r3);
return 1;
}
/* Batch over a reset instance must reproduce the streaming result. */
wickra_sma_reset(sma);
double out[5];
wickra_sma_batch(sma, in, out, 5);
if (!isnan(out[0]) || !isnan(out[1]) ||
!near(out[2], 2.0) || !near(out[3], 3.0) || !near(out[4], 4.0)) {
printf("FAIL: batch mismatch (%f %f %f %f %f)\n",
out[0], out[1], out[2], out[3], out[4]);
return 1;
}
/* NULL handle is a defined no-op / NaN, never a crash. */
if (!isnan(wickra_sma_update(NULL, 1.0))) {
printf("FAIL: NULL update did not return NaN\n");
return 1;
}
wickra_sma_reset(NULL);
wickra_sma_free(NULL);
wickra_sma_free(sma);
printf("OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free)\n");
return 0;
}
+36
View File
@@ -0,0 +1,36 @@
// C++ smoke test for the Wickra C ABI via the optional RAII wrapper (`wickra.hpp`).
//
// Validates that the header compiles as C++ and that `wickra::Handle` constructs,
// moves, and frees correctly across the boundary.
#include "wickra.hpp"
#include <cmath>
#include <cstdio>
#include <utility>
int main() {
wickra::Handle<Sma, wickra_sma_free> sma(wickra_sma_new(3));
if (!sma) {
std::puts("FAIL: wickra_sma_new returned null");
return 1;
}
(void)wickra_sma_update(sma.get(), 1.0);
(void)wickra_sma_update(sma.get(), 2.0);
double value = wickra_sma_update(sma.get(), 3.0);
if (std::fabs(value - 2.0) > 1e-9) {
std::printf("FAIL: SMA(3) value %.6f, expected 2.0\n", value);
return 1;
}
// Move transfers ownership; the moved-from handle must not double-free.
wickra::Handle<Sma, wickra_sma_free> moved(std::move(sma));
if (static_cast<bool>(sma) || !static_cast<bool>(moved)) {
std::puts("FAIL: move semantics");
return 1;
}
std::puts("OK: wickra C++ RAII smoke passed (Handle construct + move + auto-free)");
return 0;
}
+137
View File
@@ -0,0 +1,137 @@
/* Strategy example: Bollinger-Squeeze breakout with ATR stop (Wickra C ABI).
*
* Enters long when the Bollinger Bandwidth has just printed a fresh 6-month low
* (the squeeze) and price closes above the upper band (the release). Exits when
* price closes below entry minus 2*ATR(14), or when the upper band trails back
* below the entry price (the squeeze has played out). 0.1% fees per trade. The
* C counterpart of `examples/rust/src/bin/strategy_bollinger_squeeze.rs`.
*
* Educational example. NOT a live trading recommendation. Uses the checked-in
* `examples/data/btcusdt-1d.csv` dataset because daily bars give an
* interpretable "6-month low" lookback (~180 bars).
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_bollinger_squeeze.c -I bindings/c/include -L target/release -lwickra -lm -o strat_bb
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define BB_PERIOD 20
#define BB_K 2.0
#define ATR_PERIOD 14
#define ATR_STOP_MULT 2.0
#define SQUEEZE_LOOKBACK 180 /* ~6 months of daily bars */
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1d.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < SQUEEZE_LOOKBACK + BB_PERIOD) {
fprintf(stderr, "dataset has only %llu bars; need at least %d\n",
(unsigned long long)n, SQUEEZE_LOOKBACK + BB_PERIOD);
free(candles);
return 1;
}
struct BollingerBands *bb = wickra_bollinger_bands_new(BB_PERIOD, BB_K);
struct Atr *atr = wickra_atr_new(ATR_PERIOD);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
/* Circular buffer of recent bandwidth values for the squeeze lookback. */
double bw_window[SQUEEZE_LOOKBACK];
size_t bw_len = 0, bw_head = 0;
if (bb == NULL || atr == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0, stop_level = 0.0;
size_t n_trades = 0;
double equity = 1.0;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double price = c->close;
WickraBollingerOutput b;
int bb_ready = wickra_bollinger_bands_update(bb, price, &b);
double a = wickra_atr_update(atr, c->open, c->high, c->low, c->close,
c->volume, c->timestamp);
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!bb_ready || !isfinite(a)) {
continue;
}
double bandwidth =
fabs(b.middle) > 1e-15 ? (b.upper - b.lower) / b.middle : NAN;
if (isfinite(bandwidth)) {
if (bw_len == SQUEEZE_LOOKBACK) {
bw_window[bw_head] = bandwidth;
bw_head = (bw_head + 1) % SQUEEZE_LOOKBACK;
} else {
bw_window[bw_len++] = bandwidth;
}
}
if (bw_len < SQUEEZE_LOOKBACK || !isfinite(bandwidth)) {
continue;
}
double min_bw = INFINITY;
for (size_t k = 0; k < bw_len; ++k) {
if (bw_window[k] < min_bw) {
min_bw = bw_window[k];
}
}
if (in_position) {
int stop_hit = price < stop_level;
int upper_collapse = b.upper < entry_price;
if (stop_hit || upper_collapse) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
} else {
int is_new_low = fabs(bandwidth - min_bw) < 1e-12;
int breakout = price > b.upper;
if (is_new_low && breakout) {
entry_price = price;
stop_level = price - ATR_STOP_MULT * a;
equity *= 1.0 - FEE;
in_position = 1;
}
}
}
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("Bollinger Squeeze Breakout (1d, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_bollinger_bands_free(bb);
wickra_atr_free(atr);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+107
View File
@@ -0,0 +1,107 @@
/* Strategy example: MACD crossover with ADX trend-strength filter (Wickra C ABI).
*
* Long-only trend follower. Entries fire when the MACD line crosses above the
* signal line while ADX(14) > 20 (a market with at least mild directional
* strength); exits on the opposite MACD crossover regardless of ADX. 0.1% fees
* per trade. The C counterpart of `examples/rust/src/bin/strategy_macd_adx.rs`.
*
* The ADX filter is the point: pure MACD on sideways markets chops in and out;
* gating entries on directional strength cuts the worst losing streak.
*
* Educational example. NOT a live trading recommendation. Uses the checked-in
* `examples/data/btcusdt-1h.csv` dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_macd_adx.c -I bindings/c/include -L target/release -lwickra -lm -o strat_macd
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define ADX_FLOOR 20.0
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n == 0) {
fprintf(stderr, "CSV is empty: %s\n", path);
return 1;
}
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
struct Adx *adx = wickra_adx_new(14);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
if (macd == NULL || adx == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0;
size_t n_trades = 0;
double equity = 1.0;
/* Previous histogram sign to detect MACD-line crossovers: -1 unset, 0/1 sign. */
int prev_hist_sign = -1;
for (size_t i = 0; i < n; ++i) {
const WickraCandle *c = &candles[i];
double price = c->close;
WickraMacdOutput m;
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
WickraAdxOutput a;
int adx_ready = wickra_adx_update(adx, c->open, c->high, c->low, c->close,
c->volume, c->timestamp, &a);
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!macd_ready || !adx_ready) {
continue;
}
int hist_sign = m.histogram > 0.0 ? 1 : 0;
int cross_up = prev_hist_sign == 0 && hist_sign == 1;
int cross_down = prev_hist_sign == 1 && hist_sign == 0;
prev_hist_sign = hist_sign;
if (!in_position && cross_up && a.adx > ADX_FLOOR) {
entry_price = price;
equity *= 1.0 - FEE;
in_position = 1;
} else if (in_position && cross_down) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
}
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("MACD + ADX Trend Filter (1h, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_macd_indicator_free(macd);
wickra_adx_free(adx);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+95
View File
@@ -0,0 +1,95 @@
/* Strategy example: RSI mean-reversion on hourly BTCUSDT data (Wickra C ABI).
*
* Goes long when RSI(14) crosses below 30 (oversold), exits when RSI crosses
* above 70 (overbought). Position is binary (full-in / full-out), fees are 0.1%
* per trade (Binance maker tier), no stop-loss. The C counterpart of
* `examples/rust/src/bin/strategy_rsi_mean_reversion.rs`.
*
* Educational example. NOT a recommended trading strategy the point is to
* show how a Wickra streaming indicator wires into a signal -> fill -> PnL ->
* equity loop. Uses the checked-in `examples/data/btcusdt-1h.csv` dataset.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/strategy_rsi_mean_reversion.c -I bindings/c/include -L target/release -lwickra -lm -o strat_rsi
*/
#define WICKRA_CSV_IMPL
#define WICKRA_STRATEGY_IMPL
#include "wickra.h"
#include "wickra_csv.h"
#include "wickra_strategy.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#ifndef WICKRA_DATA_DIR
#define WICKRA_DATA_DIR "../data"
#endif
#define FEE 0.001
#define RSI_PERIOD 14
#define OVERSOLD 30.0
#define OVERBOUGHT 70.0
int main(int argc, char **argv) {
const char *path = (argc > 1) ? argv[1] : WICKRA_DATA_DIR "/btcusdt-1h.csv";
WickraCandle *candles = NULL;
size_t n = wickra_load_csv(path, &candles);
if (n < RSI_PERIOD * 4) {
fprintf(stderr, "dataset too small: %llu\n", (unsigned long long)n);
free(candles);
return 1;
}
struct Rsi *rsi = wickra_rsi_new(RSI_PERIOD);
double *trades = (double *)malloc(n * sizeof(*trades));
double *equity_curve = (double *)malloc(n * sizeof(*equity_curve));
if (rsi == NULL || trades == NULL || equity_curve == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
int in_position = 0;
double entry_price = 0.0;
size_t n_trades = 0;
double equity = 1.0;
for (size_t i = 0; i < n; ++i) {
double price = candles[i].close;
double r = wickra_rsi_update(rsi, price);
/* Mark-to-market so the equity curve moves bar-by-bar between trades. */
equity_curve[i] = in_position ? equity * (price / entry_price) : equity;
if (!isfinite(r)) {
continue;
}
if (!in_position && r < OVERSOLD) {
entry_price = price;
equity *= 1.0 - FEE;
in_position = 1;
} else if (in_position && r > OVERBOUGHT) {
double trade_ret = price / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
in_position = 0;
}
}
/* Close any still-open trade at the last bar so metrics include it. */
if (in_position) {
double trade_ret = candles[n - 1].close / entry_price - 1.0;
trades[n_trades++] = trade_ret;
equity *= (1.0 + trade_ret) * (1.0 - FEE);
}
wickra_print_summary("RSI Mean-Reversion (1h, BTCUSDT)", candles[0].close,
candles[n - 1].close, n, trades, n_trades, equity,
equity_curve, n);
wickra_rsi_free(rsi);
free(trades);
free(equity_curve);
free(candles);
return 0;
}
+110
View File
@@ -0,0 +1,110 @@
/* Streaming indicators with the Wickra C ABI.
*
* Feeds a synthetic price series through several indicators tick by tick the
* same O(1)-per-update model a live trading bot would use and prints a status
* line once every indicator has warmed up. The C counterpart of
* `examples/rust/src/bin/streaming.rs`, `examples/python/streaming.py` and
* `examples/node/streaming.js`, using the same seeded LCG so a side-by-side run
* produces visibly comparable streams.
*
* Build (after `cargo build -p wickra-c --release`):
* cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming
*/
#include "wickra.h"
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEFAULT_TICKS 120
/* Deterministic synthetic series matching the sibling examples' seeded LCG. */
static void make_series(double *prices, size_t n) {
uint64_t seed = 1234567u;
for (size_t t = 0; t < n; ++t) {
seed = (seed * 1103515245u + 12345u) & 0x7FFFFFFFu;
double rnd = (double)seed / (double)0x7FFFFFFFu;
double tf = (double)t;
prices[t] = 100.0 + tf * 0.05 + sin(tf * 0.07) * 8.0 + cos(tf * 0.21) * 3.0 +
(rnd - 0.5);
}
}
/* Format an indicator value, rendering warmup (NaN) as a dashed placeholder. */
static void fmt(char *buf, size_t buflen, double v) {
if (isfinite(v)) {
snprintf(buf, buflen, "%7.2f", v);
} else {
snprintf(buf, buflen, " -- ");
}
}
int main(int argc, char **argv) {
size_t ticks = DEFAULT_TICKS;
if (argc == 3 && strcmp(argv[1], "--ticks") == 0) {
long parsed = strtol(argv[2], NULL, 10);
if (parsed <= 0) {
fprintf(stderr, "--ticks must be positive\n");
return 1;
}
ticks = (size_t)parsed;
} else if (argc != 1) {
fprintf(stderr, "usage: streaming [--ticks N]\n");
return 1;
}
struct Sma *sma = wickra_sma_new(20);
struct Ema *ema = wickra_ema_new(20);
struct Rsi *rsi = wickra_rsi_new(14);
struct MacdIndicator *macd = wickra_macd_indicator_new(12, 26, 9);
double *prices = (double *)malloc(ticks * sizeof(*prices));
if (sma == NULL || ema == NULL || rsi == NULL || macd == NULL || prices == NULL) {
fprintf(stderr, "allocation failed\n");
return 1;
}
make_series(prices, ticks);
printf("Wickra streaming indicator demo (C)\n\n");
size_t signals = 0;
for (size_t t = 0; t < ticks; ++t) {
double price = prices[t];
double sv = wickra_sma_update(sma, price);
double ev = wickra_ema_update(ema, price);
double rv = wickra_rsi_update(rsi, price);
WickraMacdOutput m;
int macd_ready = wickra_macd_indicator_update(macd, price, &m);
/* Only act once every indicator has produced a value. */
if (!isfinite(sv) || !isfinite(ev) || !isfinite(rv) || !macd_ready) {
continue;
}
int overbought = rv > 70.0 && m.histogram < 0.0;
int oversold = rv < 30.0 && m.histogram > 0.0;
const char *tag = overbought ? "SELL?" : (oversold ? "BUY? " : " ");
if (overbought || oversold) {
signals++;
}
char b_price[16], b_sma[16], b_ema[16], b_rsi[16], b_hist[16];
fmt(b_price, sizeof(b_price), price);
fmt(b_sma, sizeof(b_sma), sv);
fmt(b_ema, sizeof(b_ema), ev);
fmt(b_rsi, sizeof(b_rsi), rv);
fmt(b_hist, sizeof(b_hist), m.histogram);
printf("t=%3llu price=%s sma=%s ema=%s rsi=%s macd_hist=%s %s\n",
(unsigned long long)t, b_price, b_sma, b_ema, b_rsi, b_hist, tag);
}
printf("\nDone — %llu candidate signal(s) over %llu ticks.\n",
(unsigned long long)signals, (unsigned long long)ticks);
wickra_sma_free(sma);
wickra_ema_free(ema);
wickra_rsi_free(rsi);
wickra_macd_indicator_free(macd);
free(prices);
return 0;
}
+90
View File
@@ -0,0 +1,90 @@
/* Shared OHLCV CSV loader for the Wickra C examples.
*
* The C ABI exposes only the indicators, not the wickra-data IO layer, so the
* examples read CSV themselves. This header-only helper is the C counterpart of
* `wickra_data::csv::CandleReader` used by the Rust examples: it parses the
* standard `timestamp,open,high,low,close,volume` files shipped under
* `examples/data/`.
*
* Header-only: define WICKRA_CSV_IMPL in exactly one translation unit (each
* example is a single .c file, so it just defines it before including this).
*/
#ifndef WICKRA_CSV_H
#define WICKRA_CSV_H
#include <stddef.h>
#include <stdint.h>
typedef struct WickraCandle {
int64_t timestamp;
double open;
double high;
double low;
double close;
double volume;
} WickraCandle;
/* Load an OHLCV CSV into a malloc'd array. Returns the candle count and stores
* the array in *out (caller frees with free()). Returns 0 and leaves *out NULL
* on any error (missing file, no parseable rows). A leading header line whose
* first field is non-numeric is skipped. */
size_t wickra_load_csv(const char *path, WickraCandle **out);
#ifdef WICKRA_CSV_IMPL
#include <stdio.h>
#include <stdlib.h>
size_t wickra_load_csv(const char *path, WickraCandle **out) {
*out = NULL;
FILE *f = fopen(path, "r");
if (f == NULL) {
fprintf(stderr, "wickra_load_csv: cannot open %s\n", path);
return 0;
}
size_t cap = 1024;
size_t n = 0;
WickraCandle *rows = (WickraCandle *)malloc(cap * sizeof(*rows));
if (rows == NULL) {
fclose(f);
return 0;
}
char line[512];
while (fgets(line, (int)sizeof(line), f) != NULL) {
WickraCandle c;
long long ts = 0;
/* sscanf returns the number of fields successfully matched. A header
* row ("timestamp,...") matches 0 and is skipped. */
int matched = sscanf(line, "%lld,%lf,%lf,%lf,%lf,%lf", &ts, &c.open,
&c.high, &c.low, &c.close, &c.volume);
if (matched != 6) {
continue;
}
c.timestamp = (int64_t)ts;
if (n == cap) {
cap *= 2;
WickraCandle *grown = (WickraCandle *)realloc(rows, cap * sizeof(*rows));
if (grown == NULL) {
free(rows);
fclose(f);
return 0;
}
rows = grown;
}
rows[n++] = c;
}
fclose(f);
if (n == 0) {
free(rows);
return 0;
}
*out = rows;
return n;
}
#endif /* WICKRA_CSV_IMPL */
#endif /* WICKRA_CSV_H */
+92
View File
@@ -0,0 +1,92 @@
/* Shared equity-curve summary for the Wickra C strategy examples.
*
* The Rust strategy examples repeat their `print_summary` per file; in C the
* presentation is factored into this header so each strategy .c file stays
* focused on its signal logic. Pure reporting no indicator state.
*
* Header-only: define WICKRA_STRATEGY_IMPL in exactly one translation unit.
*/
#ifndef WICKRA_STRATEGY_H
#define WICKRA_STRATEGY_H
#include <stddef.h>
/* Print a one-screen summary of a strategy run: returns vs buy & hold, trade
* win/loss counts, max drawdown, per-trade Sharpe, best/worst trade. */
void wickra_print_summary(const char *name, double first_price, double last_price,
size_t bars, const double *closed_trades, size_t n_trades,
double final_equity, const double *equity_curve,
size_t n_curve);
#ifdef WICKRA_STRATEGY_IMPL
#include <math.h>
#include <stdio.h>
void wickra_print_summary(const char *name, double first_price, double last_price,
size_t bars, const double *closed_trades, size_t n_trades,
double final_equity, const double *equity_curve,
size_t n_curve) {
double buy_hold = last_price / first_price;
double strat_return = final_equity - 1.0;
double bh_return = buy_hold - 1.0;
size_t wins = 0, losses = 0;
double best = -INFINITY, worst = INFINITY;
double sum_ret = 0.0, sum_sq = 0.0;
for (size_t i = 0; i < n_trades; ++i) {
double r = closed_trades[i];
if (r > 0.0) {
wins++;
} else if (r < 0.0) {
losses++;
}
if (r > best) {
best = r;
}
if (r < worst) {
worst = r;
}
sum_ret += r;
sum_sq += r * r;
}
double n = (double)n_trades;
double mean_ret = n > 0.0 ? sum_ret / n : 0.0;
double var_ret = n > 1.0 ? (sum_sq - n * mean_ret * mean_ret) / (n - 1.0) : 0.0;
double sharpe = var_ret > 0.0 ? mean_ret / sqrt(var_ret) : 0.0;
if (n_trades == 0) {
best = 0.0;
worst = 0.0;
}
double peak = n_curve > 0 ? equity_curve[0] : 1.0;
double max_dd = 0.0;
for (size_t i = 0; i < n_curve; ++i) {
if (equity_curve[i] > peak) {
peak = equity_curve[i];
}
double dd = (peak - equity_curve[i]) / peak;
if (dd > max_dd) {
max_dd = dd;
}
}
printf("=== %s ===\n", name);
printf("Bars: %llu\n", (unsigned long long)bars);
printf("Trades: %llu (W%llu / L%llu)\n", (unsigned long long)n_trades,
(unsigned long long)wins, (unsigned long long)losses);
printf("Strategy return: %+.2f%%\n", strat_return * 100.0);
printf("Buy & Hold return: %+.2f%%\n", bh_return * 100.0);
printf("Excess over BH: %+.2f%%\n", (strat_return - bh_return) * 100.0);
printf("Max drawdown: %.2f%%\n", max_dd * 100.0);
printf("Per-trade Sharpe: %.2f (mean %+.4f, stddev %.4f)\n", sharpe,
mean_ret, sqrt(var_ret));
printf("Best / worst trade: %+.2f%% / %+.2f%%\n", best * 100.0, worst * 100.0);
printf("\n");
printf("NOTE: Educational example — fees, slippage, funding costs and tax "
"effects are simplified or omitted. Past performance is not indicative "
"of future results.\n");
}
#endif /* WICKRA_STRATEGY_IMPL */
#endif /* WICKRA_STRATEGY_H */
+7 -7
View File
@@ -17,7 +17,7 @@
}, },
"../../bindings/node": { "../../bindings/node": {
"name": "wickra", "name": "wickra",
"version": "0.7.4", "version": "0.7.5",
"license": "MIT OR Apache-2.0", "license": "MIT OR Apache-2.0",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "^2.18.0" "@napi-rs/cli": "^2.18.0"
@@ -26,12 +26,12 @@
"node": ">= 18" "node": ">= 18"
}, },
"optionalDependencies": { "optionalDependencies": {
"wickra-darwin-arm64": "0.7.4", "wickra-darwin-arm64": "0.7.5",
"wickra-darwin-x64": "0.7.4", "wickra-darwin-x64": "0.7.5",
"wickra-linux-arm64-gnu": "0.7.4", "wickra-linux-arm64-gnu": "0.7.5",
"wickra-linux-x64-gnu": "0.7.4", "wickra-linux-x64-gnu": "0.7.5",
"wickra-win32-arm64-msvc": "0.7.4", "wickra-win32-arm64-msvc": "0.7.5",
"wickra-win32-x64-msvc": "0.7.4" "wickra-win32-x64-msvc": "0.7.5"
} }
}, },
"node_modules/wickra": { "node_modules/wickra": {