"""Generate src/test/java/org/wickra/GoldenAllTest.java: a reflection-driven value-parity test that replays the shared golden input through every one of the 514 Java indicators and checks output bit-for-bit against the Rust reference fixtures g_.csv. The per-indicator spec is embedded so the test has no JSON dependency; a single reflective runner covers all archetypes. Run from repo root: python bindings/java/gen_golden_test.py """ import json import os ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", "..")) MAN = json.load(open(os.path.join(ROOT, "testdata", "golden", "golden_manifest.json"))) def params_lit(ps): if not ps: return "new double[]{}" return "new double[]{" + ", ".join(repr(float(p)) for p in ps) + "}" specs = [] for e in MAN: n = e.get("n", e.get("width", 0)) specs.append(f' new Spec("{e["canonical"]}", "{e["arch"]}", {params_lit(e["params"])}, {n}),') specs_block = "\n".join(specs) # Canonical core Indicator::name() per indicator, shared across every binding. NAMES = json.load(open(os.path.join(ROOT, "testdata", "golden", "names.json"))) names_block = "\n".join( f" NAMES.put({json.dumps(c)}, {json.dumps(NAMES[c])});" for c in sorted(NAMES) ) TEMPLATE = '''// Code generated by gen_golden_test.py. DO NOT EDIT. package org.wickra; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; import java.io.IOException; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.lang.reflect.RecordComponent; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.DynamicTest.dynamicTest; /** * Reflection-driven value parity for the whole 514-indicator catalogue: each * indicator is reconstructed by its class name, fed the synthetic stream derived * from the shared golden input (identical to gen_golden's Rust construction) and * checked bit-for-bit against testdata/golden/g_<Canonical>.csv. One runner * flattens scalar, multi-output records, profiles and bar arrays by reflection. */ class GoldenAllTest { private static final double TOL = 1e-6; private record Spec(String canonical, String arch, double[] params, int width) {} private static final Spec[] SPECS = { %SPECS% }; private static final java.util.Map NAMES = new java.util.HashMap<>(); static { %NAMES% } private static Path goldenDir() { java.io.File d = new java.io.File("").getAbsoluteFile(); while (d != null) { java.io.File g = new java.io.File(d, "testdata/golden"); if (g.isDirectory()) { return g.toPath(); } d = d.getParentFile(); } throw new IllegalStateException("testdata/golden not found"); } private static double cell(String s) { return switch (s) { case "nan" -> Double.NaN; case "inf" -> Double.POSITIVE_INFINITY; case "-inf" -> Double.NEGATIVE_INFINITY; default -> Double.parseDouble(s); }; } private static double[][] input() throws IOException { List lines = Files.readAllLines(goldenDir().resolve("input.csv")); List rows = new ArrayList<>(); for (int i = 1; i < lines.size(); i++) { if (lines.get(i).isEmpty()) continue; String[] p = lines.get(i).split(","); double[] r = new double[p.length]; for (int j = 0; j < p.length; j++) r[j] = Double.parseDouble(p[j]); rows.add(r); } return rows.toArray(new double[0][]); } // Keep blank lines (a candle on which no bar closed) so rows stay aligned. private static double[][] fixture(String name) throws IOException { List lines = Files.readAllLines(goldenDir().resolve("g_" + name + ".csv")); List rows = new ArrayList<>(); for (int i = 1; i < lines.size(); i++) { String ln = lines.get(i); if (ln.isEmpty()) { rows.add(new double[0]); continue; } String[] p = ln.split(","); double[] r = new double[p.length]; for (int j = 0; j < p.length; j++) r[j] = cell(p[j]); rows.add(r); } return rows.toArray(new double[0][]); } private static double[] nanRow(int n) { double[] r = new double[n]; java.util.Arrays.fill(r, Double.NaN); return r; } private static double[] derivFields(double[] r) { double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4]; return new double[]{ (c - o) / c * 0.01, c, c - 0.5, c + 1.0, v * 10.0, v * 0.6, v * 0.4, v * 0.55, v * 0.45, h - c, c - l, }; } private static double[] crossList(double[] r, int which) { double o = r[0], c = r[3], v = r[4]; double[] out = new double[5]; for (int j = 0; j < 5; j++) { out[j] = switch (which) { case 0 -> (c - o) + j; case 1 -> v + j * 10.0; case 2 -> j % 2 == 0 ? 1.0 : 0.0; // newHigh case 3 -> j % 3 == 0 ? 1.0 : 0.0; // newLow case 4 -> j % 2 == 0 ? 1.0 : 0.0; // aboveMa default -> j % 3 == 0 ? 1.0 : 0.0; // onBuySignal }; } return out; } private static double[] obList(double[] r, int which) { double c = r[3], v = r[4]; double[] out = new double[5]; for (int k = 0; k < 5; k++) { double kf = k + 1; out[k] = switch (which) { case 0 -> c - 0.1 * kf; case 1 -> v / kf; case 2 -> c + 0.1 * kf; default -> v * 0.9 / kf; }; } return out; } private static double[] flattenRecord(Object o) throws Exception { RecordComponent[] rc = o.getClass().getRecordComponents(); List list = new ArrayList<>(); for (RecordComponent c : rc) { Object v = c.getAccessor().invoke(o); if (v instanceof double[] arr) { for (double d : arr) list.add(d); } else if (v instanceof Number num) { list.add(num.doubleValue()); } } double[] out = new double[list.size()]; for (int i = 0; i < out.length; i++) out[i] = list.get(i); return out; } private static double[] flattenArray(Object arr) throws Exception { int len = Array.getLength(arr); List list = new ArrayList<>(); for (int i = 0; i < len; i++) { for (double d : flattenRecord(Array.get(arr, i))) list.add(d); } double[] out = new double[list.size()]; for (int i = 0; i < out.length; i++) out[i] = list.get(i); return out; } private static Object construct(Spec s) throws Exception { Class cls = Class.forName("org.wickra." + s.canonical()); Constructor ctor = cls.getConstructors()[0]; Class[] pt = ctor.getParameterTypes(); Object[] args = new Object[pt.length]; for (int i = 0; i < pt.length; i++) { double v = s.params()[i]; if (pt[i] == int.class) args[i] = (int) Math.round(v); else if (pt[i] == long.class) args[i] = (long) Math.round(v); else args[i] = v; } return ctor.newInstance(args); } private static Method updateMethod(Object ind) { for (Method m : ind.getClass().getMethods()) { if (m.getName().equals("update")) return m; } throw new IllegalStateException("no update on " + ind.getClass()); } private static double[] row(Spec s, Object ind, Method upd, double[] r, int i) throws Exception { double o = r[0], h = r[1], l = r[2], c = r[3], v = r[4]; Object res = switch (s.arch()) { case "scalar_f64", "multi_f64" -> upd.invoke(ind, c); case "pairwise", "multi_pairwise" -> upd.invoke(ind, c, o); case "scalar_candle", "multi_candle", "profile_bins", "profile_pricebins" -> upd.invoke(ind, o, h, l, c, v, (long) i); case "trade" -> upd.invoke(ind, c, v, c >= o, (long) i); case "trademid" -> upd.invoke(ind, c, v, c >= o, (long) i, (h + l) / 2); case "ob" -> upd.invoke(ind, obList(r, 0), obList(r, 1), obList(r, 2), obList(r, 3)); case "cross" -> upd.invoke(ind, crossList(r, 0), crossList(r, 1), crossList(r, 2), crossList(r, 3), crossList(r, 4), crossList(r, 5), (long) i); case "deriv", "deriv_multi" -> { double[] d = derivFields(r); yield upd.invoke(ind, d[0], d[1], d[2], d[3], d[4], d[5], d[6], d[7], d[8], d[9], d[10], (long) i); } case "bars_close" -> upd.invoke(ind, c, c, c, c, 1.0, 0L); case "bars_candle4" -> upd.invoke(ind, o, h, l, c, 1.0, 0L); case "bars_candle5" -> upd.invoke(ind, o, h, l, c, v, 0L); case "footprint" -> upd.invoke(ind, c, v, c >= o, (long) i); default -> throw new IllegalStateException("arch " + s.arch()); }; return switch (s.arch()) { case "scalar_f64", "scalar_candle", "pairwise", "trade", "trademid", "ob", "cross", "deriv" -> new double[]{((Number) res).doubleValue()}; case "multi_f64", "multi_candle", "multi_pairwise", "deriv_multi", "profile_pricebins" -> res == null ? nanRow(s.width()) : flattenRecord(res); case "profile_bins" -> res == null ? nanRow(s.width()) : (double[]) res; default -> flattenArray(res); // bars_*, footprint }; } @TestFactory List golden() throws Exception { double[][] rows = input(); List tests = new ArrayList<>(); for (Spec s : SPECS) { tests.add(dynamicTest(s.canonical(), () -> { Object ind = construct(s); String gotName = (String) ind.getClass().getMethod("name").invoke(ind); assertTrue(gotName.equals(NAMES.get(s.canonical())), s.canonical() + ": name() " + gotName + " != " + NAMES.get(s.canonical())); Method upd = updateMethod(ind); double[][] exp = fixture(s.canonical()); assertTrue(exp.length == rows.length, s.canonical() + ": row count " + exp.length + " vs " + rows.length); for (int i = 0; i < rows.length; i++) { double[] got = row(s, ind, upd, rows[i], i); double[] want = exp[i]; assertTrue(got.length == want.length, s.canonical() + " row " + i + ": arity " + got.length + " vs " + want.length); for (int k = 0; k < want.length; k++) { double w = want[k], g = got[k]; if (Double.isNaN(w)) { assertTrue(Double.isNaN(g), s.canonical() + " row " + i + " col " + k + ": want NaN got " + g); } else if (Double.isInfinite(w)) { assertTrue(Double.isInfinite(g) && Math.signum(g) == Math.signum(w), s.canonical() + " row " + i + " col " + k + ": want " + w + " got " + g); } else { double tol = TOL * Math.max(1.0, Math.abs(w)); assertTrue(Math.abs(g - w) <= tol, s.canonical() + " row " + i + " col " + k + ": got " + g + " want " + w); } } } })); } return tests; } } ''' out = TEMPLATE.replace("%SPECS%", specs_block).replace("%NAMES%", names_block) dest = os.path.join(ROOT, "bindings", "java", "src", "test", "java", "org", "wickra", "GoldenAllTest.java") open(dest, "w", encoding="utf-8").write(out) print("generated GoldenAllTest.java with", len(MAN), "indicators")