From 12681e4b1b88b8809a71471116f6a19c2ba7f47e Mon Sep 17 00:00:00 2001 From: kingchenc Date: Tue, 9 Jun 2026 02:14:28 +0200 Subject: [PATCH] C ABI: full example suite + docs & About coverage (#224) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .../ISSUE_TEMPLATE/performance_regression.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .github/workflows/sync-about.yml | 10 +- ARCHITECTURE.md | 19 +- CHANGELOG.md | 6 +- CONTRIBUTING.md | 4 +- SECURITY.md | 6 +- THREAT_MODEL.md | 6 +- bindings/c/src/lib.rs | 5 +- bindings/node/README.md | 8 +- bindings/python/README.md | 8 +- bindings/wasm/README.md | 8 +- examples/README.md | 16 +- examples/c/CMakeLists.txt | 49 ++- examples/c/README.md | 22 ++ examples/c/backtest.c | 139 ++++++++ examples/c/fetch_btcusdt.c | 311 ++++++++++++++++++ examples/c/live_binance.c | 154 +++++++++ examples/c/multi_timeframe.c | 151 +++++++++ examples/c/parallel_assets.c | 156 +++++++++ examples/c/strategy_bollinger_squeeze.c | 137 ++++++++ examples/c/strategy_macd_adx.c | 107 ++++++ examples/c/strategy_rsi_mean_reversion.c | 95 ++++++ examples/c/streaming.c | 108 +++++- examples/c/wickra_csv.h | 90 +++++ examples/c/wickra_strategy.h | 92 ++++++ 28 files changed, 1653 insertions(+), 62 deletions(-) create mode 100644 examples/c/backtest.c create mode 100644 examples/c/fetch_btcusdt.c create mode 100644 examples/c/live_binance.c create mode 100644 examples/c/multi_timeframe.c create mode 100644 examples/c/parallel_assets.c create mode 100644 examples/c/strategy_bollinger_squeeze.c create mode 100644 examples/c/strategy_macd_adx.c create mode 100644 examples/c/strategy_rsi_mean_reversion.c create mode 100644 examples/c/wickra_csv.h create mode 100644 examples/c/wickra_strategy.h diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 13b9a6d4..06ec110f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -30,7 +30,7 @@ assignees: "" ## Environment - Wickra version: -- Language / binding: +- Language / binding: - OS and architecture: - Rust / Python / Node version (If relevant): diff --git a/.github/ISSUE_TEMPLATE/performance_regression.md b/.github/ISSUE_TEMPLATE/performance_regression.md index d501d5ad..971d3761 100644 --- a/.github/ISSUE_TEMPLATE/performance_regression.md +++ b/.github/ISSUE_TEMPLATE/performance_regression.md @@ -13,7 +13,7 @@ assignees: [] ## Affected code path - Indicator / API: `e.g. EMA.update` -- Binding: `Rust / Python / Node / Wasm` +- Binding: `Rust / Python / Node / Wasm / C ABI` - Hot loop or one-shot call? ## Versions compared diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 1cfba2e2..d5e5cedb 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -33,4 +33,4 @@ import wickra as ta ## Environment (Only if relevant) - Wickra version: `e.g. 0.4.2` -- Binding: `Rust / Python / Node / Wasm` +- Binding: `Rust / Python / Node / Wasm / C ABI` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 078a43ea..ec52060c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,7 +22,7 @@ - [ ] `cargo clippy --workspace --all-targets -- -D warnings` is clean. - [ ] `cargo test --workspace` passes. - [ ] 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). - [ ] The relevant page on the [documentation site](https://docs.wickra.org) and the `README.md` are updated (If applicable). Docs edits go to a diff --git a/.github/workflows/sync-about.yml b/.github/workflows/sync-about.yml index 8b3fae55..7b507ad7 100644 --- a/.github/workflows/sync-about.yml +++ b/.github/workflows/sync-about.yml @@ -42,10 +42,10 @@ name: Sync indicator count # single source of truth for what the bindings reach. # # Design: on PRs this workflow is a READ-ONLY check. The indicator wiring -# (ScriptHelpers/_common.py wire_readme_counter) bumps both README.md and -# docs/README.md inside the author's code commit, so the counter is already -# correct by the time CI runs. If it is not, the check below fails loud and -# asks the author to re-run the wiring — it never pushes a fix-up commit. +# bumps both README.md and docs/README.md inside the author's code commit, so +# the counter is already correct by the time CI runs. If it is not, the check +# below fails loud and asks the author to re-run the wiring — it never pushes a +# fix-up commit. # # (An earlier version pushed a GITHUB_TOKEN "sync indicator count" commit to # 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 # gated on the domain resolving, otherwise the About link would 404. 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 # corrects the stale kingchenc URL and self-heals any future drift. # Same Administration-write permission as --description (no extra scope). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index aa5a7217..12e3d173 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -29,14 +29,20 @@ or replace lives behind a separate crate boundary. ▲ │ (every binding wraps the same core) │ - ┌────────────┴───────────┬─────────────────────┐ - │ │ │ -┌──▼──────┐ ┌───────▼──────┐ ┌───────▼────────┐ -│ Python │ │ Node │ │ WASM │ -│ (PyO3) │ │ (napi-rs) │ │ (wasm-bindgen) │ -└─────────┘ └──────────────┘ └────────────────┘ + ┌───────────┬────────────┴┬──────────────┬──────────────────────┐ + │ │ │ │ │ +┌──▼─────┐ ┌───▼────┐ ┌──────▼──────┐ ┌─────▼──────────────┐ +│ Python │ │ Node │ │ WASM │ │ C ABI (cbindgen) │ +│ (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 | |---|---|---|---| | `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-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-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` | The `fuzz/` directory is **excluded** from the workspace (it has its own diff --git a/CHANGELOG.md b/CHANGELOG.md index f0701125..3975a12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `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 C - smoke + streaming examples and an optional `wickra.hpp` C++ RAII wrapper. + 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 - **Three-Line Break** — Three-line-break bars (reversal needs N-line break) (`THREE_LINE_BREAK_BARS`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c72a0c6..d8de3de2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -104,8 +104,8 @@ installed. Dependabot also keeps the `.github/requirements` pins current. of `update` calls. - **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 - (`bindings/c`) is generated from the core, so regenerate it (the ScriptHelpers - `capi` wrapper) and commit `src/lib.rs` + `include/wickra.h`. + (`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 [documentation site](https://docs.wickra.org) and the `README.md` when behaviour or the public API changes. The docs live in diff --git a/SECURITY.md b/SECURITY.md index f3e03595..fb66291b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 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 non-finite inputs at construction; behaviour on edge cases (flat markets, warmup, reset) is pinned by unit tests, and the public update paths are diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index ab90e051..654fab6a 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -3,8 +3,9 @@ This document describes Wickra's attack surface and the threats considered, together with their mitigations. It complements the security assurance case in [`SECURITY.md`](SECURITY.md). Wickra is a computational technical-analysis -library (a Rust core with Python, Node.js and WebAssembly bindings), not a -network service or trading system; the attack surface is correspondingly small. +library (a Rust core with Python, Node.js and WebAssembly bindings plus a C ABI), +not a network service or trading system; the attack surface is correspondingly +small. ## Assets @@ -31,6 +32,7 @@ network service or trading system; the attack surface is correspondingly small. | Threat | Mitigation | | --- | --- | | Memory-safety exploit (buffer overflow, UAF) via crafted input | Pure safe Rust; `unsafe` is forbidden/minimised, so the compiler precludes these classes. | +| Misuse of the C ABI FFI boundary (invalid/dangling handle, undersized batch buffer) | The C ABI (`bindings/c`) is the sole `unsafe` surface. Its shim adds no logic, NULL-checks every handle (returning `NaN`/no-op), writes only into caller-sized buffers, and catches panics so none cross the boundary. A caller passing a non-NULL but dangling pointer is undefined behaviour by C's own contract — out of scope, the same as any C library. | | Denial of service via malformed/degenerate input (NaN, infinities, extreme magnitudes) | Indicators reject non-finite inputs and validate parameters at construction; update paths are exercised by coverage-guided fuzzing and unit tests for edge cases. | | Silently incorrect results | 100% line coverage on the core crate; reference-value tests against known-good sources; streaming/batch parity tests. | | Integer overflow / panics | `clippy::pedantic` with `-D warnings`; debug assertions and overflow checks enabled in test/fuzz builds. | diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 8e29d4ac..4c143377 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -11,9 +11,8 @@ //! - `wickra__free(h)` — destroy the handle. Every `_new` must be paired //! 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 -//! by hand — edit the generator and regenerate, then commit this file and -//! `include/wickra.h`. +//! GENERATED from the Wickra core. Do not edit by hand — regenerate from the +//! core and commit this file together with `include/wickra.h`. use core::ptr; use core::slice; diff --git a/bindings/node/README.md b/bindings/node/README.md index 5420c761..9ace5185 100644 --- a/bindings/node/README.md +++ b/bindings/node/README.md @@ -9,7 +9,8 @@ prebuilt native binary, no system dependencies.** 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 the exact same implementation. This package is the Node.js binding (napi-rs); 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): - **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 -expose the same indicators from the shared, `unsafe`-forbidden Rust core. +Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a +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 diff --git a/bindings/python/README.md b/bindings/python/README.md index 549d24bc..7a6f614e 100644 --- a/bindings/python/README.md +++ b/bindings/python/README.md @@ -9,7 +9,8 @@ system dependencies, no C build tooling.** 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 the exact same implementation. This package is the Python binding (PyO3); it exposes 200+ streaming-first indicators across sixteen families. @@ -54,8 +55,9 @@ the main repository and documentation site: - **Docs** (quickstarts, cookbook, TA-Lib migration): - **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 -expose the same indicators from the shared, `unsafe`-forbidden Rust core. +Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a +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 diff --git a/bindings/wasm/README.md b/bindings/wasm/README.md index 7021854d..7e1bd3e6 100644 --- a/bindings/wasm/README.md +++ b/bindings/wasm/README.md @@ -9,7 +9,8 @@ wickra-wasm` — pure WebAssembly, runs anywhere a modern JS engine does.** 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 share the exact same implementation. This package is the WebAssembly binding (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): - **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 -expose the same indicators from the shared, `unsafe`-forbidden Rust core. +Wickra ships native bindings for Python, Node.js, WebAssembly and Rust, plus a +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 diff --git a/examples/README.md b/examples/README.md index 59d208e7..c862fbb0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -31,9 +31,23 @@ the examples via CMake: | 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 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` | +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/` | Example | What it does | Run | diff --git a/examples/c/CMakeLists.txt b/examples/c/CMakeLists.txt index ea17980d..8a57b879 100644 --- a/examples/c/CMakeLists.txt +++ b/examples/c/CMakeLists.txt @@ -8,6 +8,15 @@ if(NOT DEFINED WICKRA_LIB_DIR) 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. @@ -27,12 +36,15 @@ endif() enable_testing() -# Build one example, link it to the Wickra library, run it as a ctest. On Windows -# the DLL is copied next to the executable so the loader finds it at run time. -function(add_wickra_example name source) +# 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() @@ -41,13 +53,30 @@ function(add_wickra_example name source) COMMAND ${CMAKE_COMMAND} -E copy_if_different "${WICKRA_RUNTIME}" "$") endif() - 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}") + 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() -add_wickra_example(smoke smoke.c) # links the boundary, asserts values -add_wickra_example(streaming streaming.c) # runnable streaming demo -add_wickra_example(cpp_smoke smoke.cpp) # C++ RAII wrapper (wickra.hpp) +# 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 diff --git a/examples/c/README.md b/examples/c/README.md index 631f141a..44ab2cd9 100644 --- a/examples/c/README.md +++ b/examples/c/README.md @@ -50,6 +50,28 @@ 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: diff --git a/examples/c/backtest.c b/examples/c/backtest.c new file mode 100644 index 00000000..975084fb --- /dev/null +++ b/examples/c/backtest.c @@ -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 +#include +#include + +#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; +} diff --git a/examples/c/fetch_btcusdt.c b/examples/c/fetch_btcusdt.c new file mode 100644 index 00000000..6fcc4638 --- /dev/null +++ b/examples/c/fetch_btcusdt.c @@ -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 +#include +#include +#include +#include + +#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 ` 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; +} diff --git a/examples/c/live_binance.c b/examples/c/live_binance.c new file mode 100644 index 00000000..daa06494 --- /dev/null +++ b/examples/c/live_binance.c @@ -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 +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#define SLEEP_MS(ms) Sleep(ms) +#define POPEN _popen +#define PCLOSE _pclose +#else +#include +#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 ` 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). */ +} diff --git a/examples/c/multi_timeframe.c b/examples/c/multi_timeframe.c new file mode 100644 index 00000000..f61ebe4f --- /dev/null +++ b/examples/c/multi_timeframe.c @@ -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 +#include +#include + +#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; +} diff --git a/examples/c/parallel_assets.c b/examples/c/parallel_assets.c new file mode 100644 index 00000000..3d64c34a --- /dev/null +++ b/examples/c/parallel_assets.c @@ -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__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 +#include +#include +#include +#include + +#ifdef _OPENMP +#include +#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], ¶llel[(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; +} diff --git a/examples/c/strategy_bollinger_squeeze.c b/examples/c/strategy_bollinger_squeeze.c new file mode 100644 index 00000000..5e0b0f78 --- /dev/null +++ b/examples/c/strategy_bollinger_squeeze.c @@ -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 +#include +#include + +#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; +} diff --git a/examples/c/strategy_macd_adx.c b/examples/c/strategy_macd_adx.c new file mode 100644 index 00000000..4ab80d6a --- /dev/null +++ b/examples/c/strategy_macd_adx.c @@ -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 +#include +#include + +#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; +} diff --git a/examples/c/strategy_rsi_mean_reversion.c b/examples/c/strategy_rsi_mean_reversion.c new file mode 100644 index 00000000..0a10babe --- /dev/null +++ b/examples/c/strategy_rsi_mean_reversion.c @@ -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 +#include +#include + +#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; +} diff --git a/examples/c/streaming.c b/examples/c/streaming.c index fae45e1b..f4b3ddc4 100644 --- a/examples/c/streaming.c +++ b/examples/c/streaming.c @@ -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 - * scalar indicator. Here an EMA consumes a live tick stream one value at a time; - * `update` is O(1) per tick and returns NaN until the indicator has warmed up. + * 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 #include +#include +#include -int main(void) { - struct Ema *ema = wickra_ema_new(5); - if (ema == NULL) { - fprintf(stderr, "failed to create EMA\n"); +#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; } - const double prices[] = {10.0, 10.5, 11.0, 10.8, 11.2, 11.5, 11.3, 11.8}; - const size_t n = sizeof(prices) / sizeof(prices[0]); + 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("EMA(5) streaming:\n"); - for (size_t i = 0; i < n; ++i) { - double value = wickra_ema_update(ema, prices[i]); - if (value != value) { /* NaN during warmup */ - printf(" tick %zu price %.2f -> (warming up)\n", i, prices[i]); - } else { - printf(" tick %zu price %.2f -> %.4f\n", i, prices[i], value); + 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; } diff --git a/examples/c/wickra_csv.h b/examples/c/wickra_csv.h new file mode 100644 index 00000000..893b700a --- /dev/null +++ b/examples/c/wickra_csv.h @@ -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 +#include + +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 +#include + +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 */ diff --git a/examples/c/wickra_strategy.h b/examples/c/wickra_strategy.h new file mode 100644 index 00000000..a8e4d0e6 --- /dev/null +++ b/examples/c/wickra_strategy.h @@ -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 + +/* 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 +#include + +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 */