fd9f4c8bc6
* feat(bindings): expose name() on every indicator in Node, WASM, and Python Surface the core Indicator::name() / BarBuilder::name() accessor through the three native bindings so every indicator reports its canonical name at runtime, matching the existing reset/isReady/warmupPeriod surface. - Node (napi): name(): string on all 514 classes (regenerated index.d.ts) - WASM (wasm-bindgen): name(): string on all 514 classes - Python (pyo3): name() -> str on all classes * feat(bindings): expose name() across the C ABI and C/C++/Go/C#/Java/R Regenerate the C ABI and the four generated language bindings from the updated ScriptHelpers generators so every indicator and bar builder reports its canonical name at runtime, completing name() coverage across all 10 languages. - C ABI (bindings/c): wickra_<ind>_name() -> *const c_char for all 514, cached in a per-function OnceLock<CString> with ind.name() as the source of truth; cbindgen header regenerated and vendored into bindings/go/include. - Go: Name() string; C#: string Name(); Java: String name(); R: name() S3 generic over the wk_<ind>_name C glue (methods.R + NAMESPACE). The Java regeneration also restores two fixes that had drifted out of the generator (bool* arrays via boolSegment; uint8_t ctor args cast to byte) and C# re-emits '#nullable enable'; these are no-op vs the previous committed output apart from the new name() accessors. * test(golden): pin canonical name() across all 10 language bindings Add a cross-language name() consistency check: every indicator must report the exact core Indicator::name() (which can differ from the registered class name, e.g. ChaikinMoneyFlow -> "CMF", Donchian -> "DonchianChannels"). The 514 core names are committed as testdata/golden/names.json (keyed by Rust canonical) and asserted by each binding's golden replay, which already reconstructs the whole catalogue: - node / wasm: assert against names.json in the existing golden test - python: new test_golden_names.py over the shared node manifest - go / csharp / java / c+c++ / r: the golden-test generators load names.json and emit a name assertion per indicator (regenerated test artifacts committed) All 10 bindings return identical names by construction (each delegates to core), so this pins that contract and guards against a future binding breaking the passthrough. * docs(changelog): record name() across all 10 bindings under Unreleased * fix(r): restore bool* flag marshalling in the regenerated C glue The name() regeneration had reverted the cross-section bool fix: the R glue emitted (bool *)REAL(x) for const bool* inputs, reinterpreting 8-byte doubles as 1-byte bools so every flag read as false (PercentAboveMa, NewHighsNewLows, HighLowIndex, BullishPercentIndex returned 0 instead of the breadth value). The wk_bool_vec() helper is restored in the generator and the glue routes bool arrays through it again.
346 lines
14 KiB
Python
346 lines
14 KiB
Python
"""Generate examples/c/golden_test.c: a value-parity test that replays the shared
|
|
golden input through every one of the 514 indicators via the C ABI (wickra.h)
|
|
and checks output bit-for-bit against the Rust reference fixtures
|
|
g_<Canonical>.csv. The same source compiles under both a C compiler (the C
|
|
binding) and a C++ compiler (the C++ binding) — wickra.h is `extern "C"`.
|
|
|
|
Run from repo root: python examples/c/gen_golden_test.py
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
|
|
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
G = os.path.join(ROOT, "testdata", "golden")
|
|
HDR = open(os.path.join(ROOT, "bindings", "c", "include", "wickra.h"), encoding="utf-8").read()
|
|
|
|
# Canonical core Indicator::name() per indicator, shared across every binding.
|
|
NAMES = json.load(open(os.path.join(G, "names.json")))
|
|
|
|
# canonical -> C prefix, from the R wrappers (.wk_obj first arg == C symbol prefix).
|
|
RSRC = open(os.path.join(ROOT, "bindings", "r", "R", "indicators.R"), encoding="utf-8").read()
|
|
PREFIX = {m.group(1): m.group(2)
|
|
for m in re.finditer(r"^(\w+) <- function\([^)]*\) \{.*?\.wk_obj\(\"([^\"]+)\"", RSRC, re.S | re.M)}
|
|
|
|
# wickra_* function signatures (return type, args), multiline-collapsed.
|
|
SIG = {}
|
|
for m in re.finditer(r"([A-Za-z_][\w ]*\*?)\s*(wickra_\w+)\(([^;]*?)\);", HDR, re.S):
|
|
SIG[m.group(2)] = (re.sub(r"\s+", " ", m.group(1)).strip(), re.sub(r"\s+", " ", m.group(3)).strip())
|
|
|
|
# archetype + n/width per canonical
|
|
spec = {}
|
|
for e in json.load(open(os.path.join(G, "scalar_manifest.json"))):
|
|
spec[e["canonical"]] = {"arch": {"f64": "scalar_f64", "Candle": "scalar_candle", "(f64, f64)": "pairwise"}[e["input"]], "params": e["params"]}
|
|
for e in json.load(open(os.path.join(G, "multi_manifest.json"))):
|
|
spec[e["canonical"]] = {"arch": {"f64": "multi_f64", "Candle": "multi_candle", "(f64, f64)": "multi_pairwise"}[e["input"]], "params": e["params"], "n": e["n"]}
|
|
ex = json.load(open(os.path.join(G, "exotic_manifest.json")))
|
|
for e in ex["deriv"]:
|
|
spec[e["canonical"]] = {"arch": "deriv_multi" if "n" in e else "deriv", "params": e["params"], **({"n": e["n"]} if "n" in e else {})}
|
|
for fam, a in (("cross", "cross"), ("trade", "trade"), ("trademid", "trademid"), ("ob", "ob")):
|
|
for e in ex[fam]:
|
|
spec[e["canonical"]] = {"arch": a, "params": e["params"]}
|
|
for e in json.load(open(os.path.join(G, "profile_manifest.json"))):
|
|
spec[e["canonical"]] = {"arch": "profile_" + e["kind"], "params": e["params"], "width": e["width"]}
|
|
for e in json.load(open(os.path.join(G, "bars_manifest.json"))):
|
|
spec[e["canonical"]] = {"arch": "footprint" if e["canonical"] == "Footprint" else "bars_" + e["feed"], "params": e["params"]}
|
|
|
|
canons = sorted(os.path.basename(f)[2:-4] for f in __import__("glob").glob(os.path.join(G, "g_*.csv")))
|
|
|
|
BAR_FIELDS = {
|
|
"RenkoBars": ["open", "close", "direction"], "KagiBars": ["start", "end", "direction"],
|
|
"PointAndFigureBars": ["direction", "high", "low"], "RangeBars": ["open", "close", "direction"],
|
|
"ThreeLineBreakBars": ["open", "close", "direction"],
|
|
"ImbalanceBars": ["open", "high", "low", "close", "imbalance", "direction"],
|
|
"RunBars": ["open", "high", "low", "close", "length", "direction"],
|
|
"DollarBars": ["open", "high", "low", "close", "volume", "dollar"],
|
|
"TickBars": ["open", "high", "low", "close", "volume"],
|
|
"VolumeBars": ["open", "high", "low", "close", "volume"],
|
|
"Footprint": ["price", "bid_vol", "ask_vol"],
|
|
}
|
|
|
|
|
|
# cbindgen appends '_' to struct fields that collide with C/C++ reserved words.
|
|
_C_RESERVED = {"long", "short", "int", "char", "float", "double", "new", "class",
|
|
"this", "delete", "register", "auto", "const", "void"}
|
|
|
|
|
|
def c_field(name):
|
|
return name + "_" if name in _C_RESERVED else name
|
|
|
|
|
|
def csv_header(canon):
|
|
with open(os.path.join(G, "g_" + canon + ".csv"), encoding="utf-8") as f:
|
|
return f.readline().strip().split(",")
|
|
|
|
|
|
def out_struct(prefix):
|
|
"""The pointer-to-struct out param type of wickra_<prefix>_update, if any."""
|
|
_, args = SIG["wickra_" + prefix + "_update"]
|
|
m = re.search(r"struct (\w+) \*out", args)
|
|
if m:
|
|
return "struct " + m.group(1)
|
|
m = re.search(r"struct (\w+) \*scalars", args)
|
|
if m:
|
|
return "struct " + m.group(1)
|
|
return None
|
|
|
|
|
|
def ctor_casts(prefix, params):
|
|
_, args = SIG["wickra_" + prefix + "_new"]
|
|
if not args:
|
|
return ""
|
|
types = [a.strip().rsplit(" ", 1)[0].strip() for a in args.split(",")]
|
|
out = []
|
|
for t, v in zip(types, params):
|
|
if t in ("uintptr_t", "intptr_t", "size_t"):
|
|
out.append(f"(uintptr_t){int(round(v))}")
|
|
elif t in ("uint8_t",):
|
|
out.append(f"(uint8_t){int(round(v))}")
|
|
elif t in ("int32_t",):
|
|
out.append(f"(int32_t){int(round(v))}")
|
|
elif t in ("int64_t",):
|
|
out.append(f"(int64_t){int(round(v))}")
|
|
else:
|
|
out.append(repr(float(v)))
|
|
return ", ".join(out)
|
|
|
|
|
|
def gen_check(canon):
|
|
s = spec[canon]
|
|
p = PREFIX[canon]
|
|
a = s["arch"]
|
|
new = f"wickra_{p}_new({ctor_casts(p, s['params'])})"
|
|
upd = f"wickra_{p}_update"
|
|
L = [f"static int check_{canon}(void) {{",
|
|
f" struct {struct_name(p)} *h = {new};",
|
|
f' if (!h) {{ printf("FAIL {canon}: new returned NULL\\n"); return 1; }}',
|
|
f" double **exp; int rows = read_fixture(\"g_{canon}\", &exp);",
|
|
" int fails = 0;",
|
|
f' {{ const char *nm = wickra_{p}_name(h);',
|
|
f' if (!nm || strcmp(nm, {json.dumps(NAMES[canon])}) != 0) {{ printf("FAIL {canon}: name %s\\n", nm ? nm : "(null)"); fails++; }} }}',
|
|
" for (int i = 0; i < N_INPUT; i++) {",
|
|
" double o = IN[i][0], hi = IN[i][1], lo = IN[i][2], c = IN[i][3], v = IN[i][4];",
|
|
" (void)o; (void)hi; (void)lo; (void)c; (void)v;",
|
|
" double got[128]; int gn = 0;"]
|
|
if a in ("scalar_f64", "multi_f64"):
|
|
call_args = "h, c"
|
|
elif a in ("pairwise", "multi_pairwise"):
|
|
call_args = "h, c, o"
|
|
elif a in ("scalar_candle", "multi_candle") or a.startswith("profile"):
|
|
call_args = "h, o, hi, lo, c, v, (int64_t)i"
|
|
elif a == "trade":
|
|
call_args = "h, c, v, c >= o, (int64_t)i"
|
|
elif a == "trademid":
|
|
call_args = "h, c, v, c >= o, (int64_t)i, (hi + lo) / 2.0"
|
|
elif a == "ob":
|
|
L.append(" double bp[5], bs[5], ap[5], asz[5];")
|
|
L.append(" for (int k = 0; k < 5; k++) { double kf = k + 1; bp[k] = c - 0.1*kf; bs[k] = v/kf; ap[k] = c + 0.1*kf; asz[k] = v*0.9/kf; }")
|
|
call_args = "h, bp, bs, 5, ap, asz, 5"
|
|
elif a == "cross":
|
|
L.append(" double chg[5], vol[5]; bool nh[5], nl[5], am[5], ob[5];")
|
|
L.append(" for (int j = 0; j < 5; j++) { chg[j] = (c-o)+j; vol[j] = v + j*10.0; nh[j] = (j%2==0); nl[j] = (j%3==0); am[j] = (j%2==0); ob[j] = (j%3==0); }")
|
|
call_args = "h, chg, vol, nh, nl, am, ob, 5, (int64_t)i"
|
|
elif a in ("deriv", "deriv_multi"):
|
|
L.append(" double fr=(c-o)/c*0.01, mp=c, ip=c-0.5, fp=c+1.0, oi=v*10.0, ls=v*0.6, ss=v*0.4, tbv=v*0.55, tsv=v*0.45, ll=hi-c, sl=c-lo;")
|
|
call_args = "h, fr, mp, ip, fp, oi, ls, ss, tbv, tsv, ll, sl, (int64_t)i"
|
|
elif a == "bars_close":
|
|
call_args = "h, c, c, c, c, 1.0, 0"
|
|
elif a == "bars_candle4":
|
|
call_args = "h, o, hi, lo, c, 1.0, 0"
|
|
elif a == "bars_candle5":
|
|
call_args = "h, o, hi, lo, c, v, 0"
|
|
elif a == "footprint":
|
|
call_args = "h, c, v, c >= o, (int64_t)i"
|
|
else:
|
|
raise SystemExit("arch " + a)
|
|
|
|
# output handling
|
|
if a in ("scalar_f64", "scalar_candle", "pairwise", "trade", "trademid", "ob", "cross", "deriv"):
|
|
L.append(f" got[gn++] = {upd}({call_args});")
|
|
elif a in ("multi_f64", "multi_candle", "multi_pairwise", "deriv_multi"):
|
|
st = out_struct(p)
|
|
fields = csv_header(canon)
|
|
L.append(f" {st} out;")
|
|
L.append(f" if ({upd}({call_args}, &out)) {{")
|
|
for f in fields:
|
|
L.append(f" got[gn++] = out.{c_field(f)};")
|
|
L.append(" } else {")
|
|
L.append(f" for (int z = 0; z < {len(fields)}; z++) got[gn++] = NANV;")
|
|
L.append(" }")
|
|
elif a == "profile_bins":
|
|
w = s["width"]
|
|
L.append(" double vbuf[256];")
|
|
L.append(f" intptr_t k = {upd}({call_args}, vbuf, 256);")
|
|
L.append(f" if (k < 0) {{ for (int z = 0; z < {w}; z++) got[gn++] = NANV; }}")
|
|
L.append(f" else {{ for (int z = 0; z < {w}; z++) got[gn++] = vbuf[z]; }}")
|
|
elif a == "profile_pricebins":
|
|
w = s["width"]
|
|
st = out_struct(p)
|
|
L.append(" double vbuf[256];")
|
|
L.append(f" {st} sc;")
|
|
L.append(f" intptr_t k = {upd}({call_args}, &sc, vbuf, 256);")
|
|
L.append(f" if (k < 0) {{ for (int z = 0; z < {w}; z++) got[gn++] = NANV; }}")
|
|
L.append(" else { got[gn++] = sc.price_low; got[gn++] = sc.price_high;")
|
|
L.append(f" for (int z = 0; z < {w - 2}; z++) got[gn++] = vbuf[z]; }}")
|
|
else: # bars_* / footprint
|
|
elem = out_struct(p)
|
|
fields = BAR_FIELDS[canon]
|
|
cap = 256
|
|
L.append(f" {elem} bbuf[{cap}];")
|
|
if a == "footprint":
|
|
L.append(f" intptr_t k = {upd}({call_args}, bbuf, {cap});")
|
|
L.append(" if (k < 0) k = 0;")
|
|
else:
|
|
L.append(f" uintptr_t k = {upd}({call_args}, bbuf, {cap});")
|
|
L.append(" for (uintptr_t b = 0; b < (uintptr_t)k; b++) {")
|
|
for f in fields:
|
|
L.append(f" got[gn++] = (double)bbuf[b].{f};")
|
|
L.append(" }")
|
|
|
|
L.append(" fails += cmp_row(\"" + canon + "\", i, exp[i], EXPLEN[i], got, gn);")
|
|
L.append(" }")
|
|
L.append(" free_fixture(exp, rows);")
|
|
L.append(f" wickra_{p}_free(h);")
|
|
L.append(" return fails;")
|
|
L.append("}")
|
|
return "\n".join(L)
|
|
|
|
|
|
def struct_name(prefix):
|
|
ret, _ = SIG["wickra_" + prefix + "_new"]
|
|
m = re.search(r"struct (\w+)", ret)
|
|
return m.group(1)
|
|
|
|
|
|
HEADER = r'''/* Generated by gen_golden_test.py. DO NOT EDIT.
|
|
*
|
|
* Value-parity for the whole 514-indicator catalogue through the Wickra C ABI.
|
|
* The same source compiles as C (gcc) and C++ (g++) since wickra.h is extern "C".
|
|
* Each indicator replays the shared golden input and is checked bit-for-bit
|
|
* against the Rust reference fixtures testdata/golden/g_<Canonical>.csv. */
|
|
#include <math.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "wickra.h"
|
|
|
|
#define NANV (nan(""))
|
|
#define MAXROWS 512
|
|
#define MAXCOLS 256
|
|
|
|
static double IN[MAXROWS][8];
|
|
static int N_INPUT = 0;
|
|
static int EXPLEN[MAXROWS];
|
|
|
|
static const char *GDIR = NULL;
|
|
|
|
static double parse_cell(const char *s) {
|
|
if (strcmp(s, "nan") == 0) return NANV;
|
|
if (strcmp(s, "inf") == 0) return INFINITY;
|
|
if (strcmp(s, "-inf") == 0) return -INFINITY;
|
|
return atof(s);
|
|
}
|
|
|
|
static int split_line(char *line, double *out, int max) {
|
|
int n = 0;
|
|
char *p = line;
|
|
while (*p && n < max) {
|
|
char *comma = strchr(p, ',');
|
|
if (comma) *comma = '\0';
|
|
out[n++] = parse_cell(p);
|
|
if (!comma) break;
|
|
p = comma + 1;
|
|
}
|
|
return n;
|
|
}
|
|
|
|
static FILE *open_fixture(const char *name) {
|
|
char path[1024];
|
|
snprintf(path, sizeof(path), "%s/%s.csv", GDIR, name);
|
|
return fopen(path, "r");
|
|
}
|
|
|
|
static void load_input(void) {
|
|
FILE *f = open_fixture("input");
|
|
if (!f) { fprintf(stderr, "cannot open input.csv in %s\n", GDIR); exit(2); }
|
|
char line[8192];
|
|
int first = 1;
|
|
while (fgets(line, sizeof(line), f)) {
|
|
line[strcspn(line, "\r\n")] = '\0';
|
|
if (first) { first = 0; continue; }
|
|
if (line[0] == '\0') continue;
|
|
N_INPUT += (split_line(line, IN[N_INPUT], 8) > 0);
|
|
}
|
|
fclose(f);
|
|
}
|
|
|
|
/* Read a fixture, keeping blank rows (a candle on which no bar closed). */
|
|
static int read_fixture(const char *name, double ***outp) {
|
|
FILE *f = open_fixture(name);
|
|
if (!f) { fprintf(stderr, "cannot open %s.csv\n", name); exit(2); }
|
|
double **rows = (double **)malloc(sizeof(double *) * MAXROWS);
|
|
int n = 0, first = 1;
|
|
char line[8192];
|
|
while (fgets(line, sizeof(line), f) && n < MAXROWS) {
|
|
line[strcspn(line, "\r\n")] = '\0';
|
|
if (first) { first = 0; continue; }
|
|
double *vals = (double *)malloc(sizeof(double) * MAXCOLS);
|
|
int c = (line[0] == '\0') ? 0 : split_line(line, vals, MAXCOLS);
|
|
EXPLEN[n] = c;
|
|
rows[n++] = vals;
|
|
}
|
|
fclose(f);
|
|
*outp = rows;
|
|
return n;
|
|
}
|
|
|
|
static void free_fixture(double **rows, int n) {
|
|
for (int i = 0; i < n; i++) free(rows[i]);
|
|
free(rows);
|
|
}
|
|
|
|
static int close_to(double g, double w) {
|
|
if (isnan(w)) return isnan(g);
|
|
if (isinf(w)) return isinf(g) && ((g > 0) == (w > 0));
|
|
double tol = 1e-6 * (fabs(w) > 1.0 ? fabs(w) : 1.0);
|
|
return fabs(g - w) <= tol;
|
|
}
|
|
|
|
static int cmp_row(const char *name, int i, const double *want, int wn, const double *got, int gn) {
|
|
if (wn != gn) {
|
|
printf("FAIL %s row %d: arity %d vs %d\n", name, i, gn, wn);
|
|
return 1;
|
|
}
|
|
for (int k = 0; k < wn; k++) {
|
|
if (!close_to(got[k], want[k])) {
|
|
printf("FAIL %s row %d col %d: got %g want %g\n", name, i, k, got[k], want[k]);
|
|
return 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
'''
|
|
|
|
MAIN_HEAD = r'''
|
|
int main(int argc, char **argv) {
|
|
GDIR = (argc > 1) ? argv[1] : "testdata/golden";
|
|
load_input();
|
|
int total = 0, failed = 0;
|
|
'''
|
|
|
|
out = [HEADER]
|
|
for canon in canons:
|
|
out.append(gen_check(canon))
|
|
out.append(MAIN_HEAD)
|
|
for canon in canons:
|
|
out.append(f" total++; if (check_{canon}()) failed++;")
|
|
out.append(r''' printf("\nC/C++ golden: %d passed, %d failed (of %d)\n", total - failed, failed, total);
|
|
return failed ? 1 : 0;
|
|
}''')
|
|
|
|
dest = os.path.join(ROOT, "examples", "c", "golden_test.c")
|
|
open(dest, "w", encoding="utf-8").write("\n".join(out) + "\n")
|
|
print("generated golden_test.c with", len(canons), "indicators")
|