"""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_.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__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_.csv. */ #include #include #include #include #include #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")