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:
+15
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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). */
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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], ¶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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
@@ -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 */
|
||||
Reference in New Issue
Block a user