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.
This commit is contained in:
kingchenc
2026-06-09 02:14:28 +02:00
committed by GitHub
parent 91e05e3c26
commit 12681e4b1b
28 changed files with 1653 additions and 62 deletions
+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
+5 -5
View File
@@ -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).
+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
+4 -2
View File
@@ -10,8 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated - **C ABI (`bindings/c`)** — a `cdylib` + `staticlib` plus a generated
`include/wickra.h` exposing all 514 indicators and 10 bar builders over an `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) 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 C links against, complementing the native Python/Node/WASM bindings. Ships a
smoke + streaming examples and an optional `wickra.hpp` C++ RAII wrapper. 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`).
+2 -2
View File
@@ -104,8 +104,8 @@ installed. Dependabot also keeps the `.github/requirements` pins current.
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`. The C ABI Python, Node, and WASM bindings, including their type stubs / `.d.ts`. The C ABI
(`bindings/c`) is generated from the core, so regenerate it (the ScriptHelpers (`bindings/c`) is generated from the core, so regenerate it from the core and
`capi` wrapper) and commit `src/lib.rs` + `include/wickra.h`. 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
+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. |
+2 -3
View File
@@ -11,9 +11,8 @@
//! - `wickra_<ind>_free(h)` — destroy the handle. Every `_new` must be paired //! - `wickra_<ind>_free(h)` — destroy the handle. Every `_new` must be paired
//! with exactly one `_free`; there is no RAII across the C boundary. //! with exactly one `_free`; there is no RAII across the C boundary.
//! //!
//! GENERATED by the `ScriptHelpers` `capi` wrapper from the Rust core. Do not edit //! GENERATED from the Wickra core. Do not edit by hand — regenerate from the
//! by hand — edit the generator and regenerate, then commit this file and //! core and commit this file together with `include/wickra.h`.
//! `include/wickra.h`.
use core::ptr; use core::ptr;
use core::slice; use core::slice;
+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
+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
+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
+15 -1
View File
@@ -31,9 +31,23 @@ the examples via CMake:
| Example | What it does | CMake target | | Example | What it does | CMake target |
| --- | --- | --- | | --- | --- | --- |
| `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` | | `smoke.c` | Links the generated header + library and asserts SMA streaming / batch values across the boundary. | `smoke` |
| `streaming.c` | Feed a tick stream through an EMA, printing each value (NaN during warmup). | `streaming` | | `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` | | `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 |
+39 -10
View File
@@ -8,6 +8,15 @@ if(NOT DEFINED WICKRA_LIB_DIR)
endif() endif()
set(WICKRA_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../../bindings/c/include") 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. # Pick the right link target per platform/toolchain.
# - MSVC links the generated import library (wickra.dll.lib). # - MSVC links the generated import library (wickra.dll.lib).
# - MinGW/gcc on Windows links the DLL directly. # - MinGW/gcc on Windows links the DLL directly.
@@ -27,12 +36,15 @@ endif()
enable_testing() enable_testing()
# Build one example, link it to the Wickra library, run it as a ctest. On Windows # Build one example and link it to the Wickra library. On Windows the DLL is
# the DLL is copied next to the executable so the loader finds it at run time. # copied next to the executable so the loader finds it at run time. With
function(add_wickra_example name source) # 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}) add_executable(${name} ${source})
target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}") target_include_directories(${name} PRIVATE "${WICKRA_INCLUDE_DIR}")
target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}") target_link_libraries(${name} PRIVATE "${WICKRA_LINK_LIB}")
target_compile_definitions(${name} PRIVATE "WICKRA_DATA_DIR=\"${WICKRA_DATA_DIR}\"")
if(UNIX AND NOT APPLE) if(UNIX AND NOT APPLE)
target_link_libraries(${name} PRIVATE m) target_link_libraries(${name} PRIVATE m)
endif() endif()
@@ -41,13 +53,30 @@ function(add_wickra_example name source)
COMMAND ${CMAKE_COMMAND} -E copy_if_different COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>") "${WICKRA_RUNTIME}" "$<TARGET_FILE_DIR:${name}>")
endif() endif()
add_test(NAME ${name} COMMAND ${name}) if(register_test)
if(NOT WIN32) add_test(NAME ${name} COMMAND ${name})
set_tests_properties(${name} PROPERTIES if(NOT WIN32)
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}") set_tests_properties(${name} PROPERTIES
ENVIRONMENT "LD_LIBRARY_PATH=${WICKRA_LIB_DIR};DYLD_LIBRARY_PATH=${WICKRA_LIB_DIR}")
endif()
endif() endif()
endfunction() endfunction()
add_wickra_example(smoke smoke.c) # links the boundary, asserts values # Offline examples built and run as ctests.
add_wickra_example(streaming streaming.c) # runnable streaming demo add_wickra_example(smoke smoke.c TRUE) # links the boundary, asserts values
add_wickra_example(cpp_smoke smoke.cpp) # C++ RAII wrapper (wickra.hpp) 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
+22
View File
@@ -50,6 +50,28 @@ Expected output:
OK: wickra C ABI smoke passed (SMA streaming + batch + reset + NULL-safety + free) 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 ## Usage shape
Every indicator follows the same five-function pattern over an opaque handle: Every indicator follows the same five-function pattern over an opaque handle:
+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;
}
+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;
}
+91 -17
View File
@@ -1,36 +1,110 @@
/* Streaming usage example for the Wickra C ABI. /* Streaming indicators with the Wickra C ABI.
* *
* The same five-function shape (new / update / batch / reset / free) drives every * Feeds a synthetic price series through several indicators tick by tick the
* scalar indicator. Here an EMA consumes a live tick stream one value at a time; * same O(1)-per-update model a live trading bot would use and prints a status
* `update` is O(1) per tick and returns NaN until the indicator has warmed up. * 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`): * Build (after `cargo build -p wickra-c --release`):
* cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming * cc examples/c/streaming.c -I bindings/c/include -L target/release -lwickra -lm -o streaming
*/ */
#include "wickra.h" #include "wickra.h"
#include <math.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) { #define DEFAULT_TICKS 120
struct Ema *ema = wickra_ema_new(5);
if (ema == NULL) { /* Deterministic synthetic series matching the sibling examples' seeded LCG. */
fprintf(stderr, "failed to create EMA\n"); 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; return 1;
} }
const double prices[] = {10.0, 10.5, 11.0, 10.8, 11.2, 11.5, 11.3, 11.8}; struct Sma *sma = wickra_sma_new(20);
const size_t n = sizeof(prices) / sizeof(prices[0]); 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("EMA(5) streaming:\n"); printf("Wickra streaming indicator demo (C)\n\n");
for (size_t i = 0; i < n; ++i) {
double value = wickra_ema_update(ema, prices[i]); size_t signals = 0;
if (value != value) { /* NaN during warmup */ for (size_t t = 0; t < ticks; ++t) {
printf(" tick %zu price %.2f -> (warming up)\n", i, prices[i]); double price = prices[t];
} else { double sv = wickra_sma_update(sma, price);
printf(" tick %zu price %.2f -> %.4f\n", i, prices[i], value); 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_ema_free(ema);
wickra_rsi_free(rsi);
wickra_macd_indicator_free(macd);
free(prices);
return 0; 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 */