12681e4b1b
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.
312 lines
9.9 KiB
C
312 lines
9.9 KiB
C
/* 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;
|
|
}
|