4f708d410d
* test: golden-pin the four de-duplicated indicators across all C-ABI bindings Extend gen_golden to emit reference fixtures for AdOscillator (ADOSC), IntradayIntensity, AwesomeOscillatorHistogram and AverageDrawdown, and replay them through the Go / C# / Java / R golden harnesses so their corrected definitions stay bit-identical to the Rust core in every binding. Go suite verified locally (gcc 13 + cgo): all 9 golden tests pass; C#/Java/R use the same fixtures and harness pattern (CI-verified). First step of extending the golden coverage beyond the seven archetype representatives. * test: golden-pin the scalar-output tranche (308 indicators) against Rust Extend gen_golden with a generated emit_scalar that writes reference fixtures for every single-f64-output indicator (scalar / candle / pairwise input) using valid constructor params, and add a manifest-driven generic Python golden replay that reconstructs each by its native name and checks it bit-for-bit against the Rust output. 308 indicators now value-tied to the Rust core in Python (pytest: 308/308). Takes golden coverage from the 7 archetype representatives to 308+ of the catalogue. 22 scalar indicators with non-default constructor constraints are skipped by gen_golden for now (logged), as are non-f64-output ones; multi-output, exotic inputs and the per-indicator arg arities of the C-ABI/Node replays follow. Generated + verified locally with the full toolchain. * test: golden-pin the multi-output tranche (70 indicators) in Python Add a generated emit_multi to gen_golden (per-indicator Output-field access, one CSV column per field) and a manifest-driven generic Python replay that checks every field of each multi-output indicator against the Rust reference. 70 multi-output indicators now value-tied to Rust in Python; combined with the scalar tranche, 378 indicators are golden-pinned. 8 multi with non-default param constraints and 5 with non-f64 Output fields (Option/Vec/i64) are deferred. pytest green. * test(golden): add 30 constraint-tuned indicators to scalar/multi golden suite Emit golden fixtures for 22 scalar-output and 8 multi-output indicators whose constructors need non-default parameters (Alma, Jma, Psar, T3, Mama, DoubleBollinger, ZigZag, ...). All 408 fixtures replay bit-for-bit through the Python binding. * test(golden): cover 36 missed scalar/multi indicators Add 26 single-output (LinearRegression family, HT cycle, Candle volatility estimators, DrawdownDuration) and 10 multi-output (BollingerBands, MACD/MACDEXT/MACDFIX, Camarilla, VWAP bands, ...) indicators to the golden suite. 444 fixtures replay bit-for-bit through the Python binding. * test(golden): cover 50 exotic-input indicators Add deterministic synthetic feeders for the DerivativesTick (17), CrossSection (15), Trade (8), TradeQuote (3) and OrderBook (7) families, derived from the shared OHLCV input series in both gen_golden and a new Python replay harness (test_golden_exotic). All 494 fixtures replay bit-for-bit through the Python binding. * test(golden): complete 514-indicator golden coverage Add the final tranches: 3 mixed multi-output indicators (Ichimoku, WilliamsFractals, LeadLagCrossCorrelation), 6 histogram profiles (time/volume seasonality + TPO/volume price profiles), 10 alt-chart bar builders and the footprint. Every one of the 514 distinct indicators now has a Rust-generated g_<Canonical>.csv fixture and a generic Python replay (scalar/multi/exotic/profile/bars), all passing bit-for-bit. * test(golden): add generic Node replay for all 514 indicators A manifest-driven node:test harness reconstructs every indicator by its native class, feeds the same synthetic stream derived from the shared golden input, and checks output bit-for-bit against the Rust reference fixtures (scalar/multi/exotic/profile/bars). node_manifest.json is generated from index.d.ts plus the Python-side manifests. 514/514 pass. * test(golden): add generated Go replay for all 514 indicators golden_all_test.go (generated by gen_golden_test.py) reconstructs every Go indicator, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures. A reflection-based comparator flattens multi-output structs, profiles and bar slices so one path covers all archetypes. This is the first C-ABI binding verified across the full catalogue. 514/514 pass. * test(golden): add generated C# replay for all 514 indicators GoldenAllTests.g.cs (generated by gen_golden_test.py) reconstructs every C# indicator, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures via a reflection-based flatten covering scalar/multi/profile/bar archetypes. 514/514 pass. Also add the '#nullable enable' directive the compiler requires to the generated Indicators.g.cs, clearing the four CS8669 warnings on the nullable double[] profile return types. * fix(java): marshal C ABI bool params correctly; add 514 golden replay The Java FFM binding marshalled the cross-section state flags (newHigh, newLow, aboveMa, onBuySignal) as JAVA_DOUBLE arrays, but the C ABI takes them as const bool* (one byte each), so the native side read the low byte of each 8-byte double and saw every flag as false. Add WickraNative. boolSegment and use it across the 15 cross-section indicators. Also pass the MacdExt MaType arguments as byte to match the uint8_t downcall descriptor (was int, throwing WrongMethodTypeException). Add GoldenAllTest.java (generated by gen_golden_test.py): a reflection runner replaying all 514 indicators against the Rust reference fixtures. The bugs above were found by this test; 514/514 now pass. * fix(r): marshal C ABI bool flags correctly; add 514 golden replay The R wrapper passed the cross-section state flags as (bool *)REAL(x), reinterpreting the 8-byte doubles as 1-byte bools so the native side read every flag as false. Add wk_bool_vec to convert each flag vector into a real C bool buffer and use it for all 15 cross-section update wrappers. Add test-golden-all.R + generated golden_specs.R: a reflective runner replaying all 514 indicators against the Rust reference fixtures. The bug above was found by this test; verified 514/514 pass locally. * test(golden): add WASM replay for all 514 indicators A manifest-driven node:test harness loads the nodejs-target wasm-pack build, reconstructs every indicator by its JS class, feeds the shared synthetic stream and checks output bit-for-bit against the Rust reference fixtures. wasm_manifest.json is generated from the wasm .d.ts plus the shared manifests; a recursive flattener covers scalar, multi (Reflect objects), profile and bar shapes. 514/514 pass locally (wasm-pack build --target nodejs, then node --test). * test(golden): add C and C++ replay for all 514 indicators golden_test.c (generated by gen_golden_test.py) drives every indicator through the C ABI (wickra.h) and checks output bit-for-bit against the Rust reference fixtures. golden_test.cpp #includes the same source so the identical runner is compiled and run under both gcc (C) and g++ (C++) via the CMake targets golden_test / golden_test_cpp — proving the extern "C" header is consumable from each language. Both 514/514 (verified via ctest). * test(golden): gofmt the generated Go golden replay * test(golden): make the Node fixture reader CRLF-safe and pin fixtures to LF
280 lines
11 KiB
Python
280 lines
11 KiB
Python
"""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_<Canonical>.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)
|
|
|
|
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 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<String> lines = Files.readAllLines(goldenDir().resolve("input.csv"));
|
|
List<double[]> 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<String> lines = Files.readAllLines(goldenDir().resolve("g_" + name + ".csv"));
|
|
List<double[]> 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<Double> 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<Double> 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<DynamicTest> golden() throws Exception {
|
|
double[][] rows = input();
|
|
List<DynamicTest> tests = new ArrayList<>();
|
|
for (Spec s : SPECS) {
|
|
tests.add(dynamicTest(s.canonical(), () -> {
|
|
Object ind = construct(s);
|
|
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)
|
|
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")
|