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
+15 -1
View File
@@ -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 |
+39 -10
View File
@@ -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}" "$<TARGET_FILE_DIR:${name}>")
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
+22
View File
@@ -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:
+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
* 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 <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
+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 */