package wickra import ( "bufio" "math" "os" "strconv" "strings" "testing" ) // Golden-fixture parity: replay the shared testdata/golden input series through // the Go FFI and assert every value matches the Rust reference output. Where the // archetype test only checks finiteness, this pins exact values, catching wiring // bugs (swapped params, wrong multi-output field). Fixtures are generated by // `cargo run -p wickra-examples --bin gen_golden`. const goldenTol = 1e-6 func readGolden(t *testing.T, name string) [][]string { t.Helper() f, err := os.Open("../../testdata/golden/" + name + ".csv") if err != nil { t.Fatalf("open %s: %v", name, err) } defer f.Close() var rows [][]string sc := bufio.NewScanner(f) first := true for sc.Scan() { line := sc.Text() if first { first = false continue } if line == "" { continue } rows = append(rows, strings.Split(line, ",")) } return rows } func goldenCell(s string) float64 { if s == "nan" { return math.NaN() } v, _ := strconv.ParseFloat(s, 64) return v } func goldenInput(t *testing.T) [][]float64 { rows := readGolden(t, "input") out := make([][]float64, len(rows)) for i, r := range rows { vals := make([]float64, len(r)) for j, c := range r { vals[j] = goldenCell(c) } out[i] = vals } return out } func assertGoldenClose(t *testing.T, got, want float64, row int, field string) { t.Helper() if math.IsNaN(want) { if !math.IsNaN(got) { t.Errorf("row %d %s: expected warmup/NaN, got %v", row, field, got) } return } tol := goldenTol * math.Max(1.0, math.Abs(want)) if math.Abs(got-want) > tol { t.Errorf("row %d %s: got %v want %v", row, field, got, want) } } func TestGoldenScalar(t *testing.T) { input := goldenInput(t) sma, err := NewSma(14) if err != nil { t.Fatal(err) } defer sma.Close() ema, err := NewEma(14) if err != nil { t.Fatal(err) } defer ema.Close() rsi, err := NewRsi(14) if err != nil { t.Fatal(err) } defer rsi.Close() cases := []struct { name string upd func(close float64) float64 }{ {"sma", sma.Update}, {"ema", ema.Update}, {"rsi", rsi.Update}, } for _, tc := range cases { exp := readGolden(t, tc.name) for i := range input { assertGoldenClose(t, tc.upd(input[i][3]), goldenCell(exp[i][0]), i, tc.name) } } } func TestGoldenAtr(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "atr") atr, err := NewAtr(14) if err != nil { t.Fatal(err) } defer atr.Close() for i := range input { got := atr.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i)) assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "atr") } } func TestGoldenBeta(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "beta") beta, err := NewBeta(20) if err != nil { t.Fatal(err) } defer beta.Close() for i := range input { // generator fed (close, open) assertGoldenClose(t, beta.Update(input[i][3], input[i][0]), goldenCell(exp[i][0]), i, "beta") } } func TestGoldenMacd(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "macd") macd, err := NewMacdIndicator(12, 26, 9) if err != nil { t.Fatal(err) } defer macd.Close() for i := range input { out, ok := macd.Update(input[i][3]) if exp[i][0] == "nan" { if ok { t.Errorf("row %d macd: expected warmup, got %+v", i, out) } continue } if !ok { t.Errorf("row %d macd: expected value, got warmup", i) continue } assertGoldenClose(t, out.Macd, goldenCell(exp[i][0]), i, "macd.macd") assertGoldenClose(t, out.Signal, goldenCell(exp[i][1]), i, "macd.signal") assertGoldenClose(t, out.Histogram, goldenCell(exp[i][2]), i, "macd.histogram") } } func TestGoldenAdx(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "adx") adx, err := NewAdx(14) if err != nil { t.Fatal(err) } defer adx.Close() for i := range input { out, ok := adx.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i)) if exp[i][0] == "nan" { if ok { t.Errorf("row %d adx: expected warmup, got %+v", i, out) } continue } if !ok { t.Errorf("row %d adx: expected value, got warmup", i) continue } assertGoldenClose(t, out.PlusDi, goldenCell(exp[i][0]), i, "adx.plus_di") assertGoldenClose(t, out.MinusDi, goldenCell(exp[i][1]), i, "adx.minus_di") assertGoldenClose(t, out.Adx, goldenCell(exp[i][2]), i, "adx.adx") } } // The four de-duplicated indicators: pin their corrected definitions against // the Rust reference so the Go FFI stays bit-identical. func TestGoldenAdOscillator(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "ad_oscillator") ad, err := NewAdOscillator() if err != nil { t.Fatal(err) } defer ad.Close() for i := range input { got := ad.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i)) assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "ad_oscillator") } } func TestGoldenIntradayIntensity(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "intraday_intensity") ii, err := NewIntradayIntensity() if err != nil { t.Fatal(err) } defer ii.Close() for i := range input { got := ii.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i)) assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "intraday_intensity") } } func TestGoldenAwesomeOscillatorHistogram(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "awesome_oscillator_histogram") aoh, err := NewAwesomeOscillatorHistogram(5, 34, 1) if err != nil { t.Fatal(err) } defer aoh.Close() for i := range input { got := aoh.Update(input[i][0], input[i][1], input[i][2], input[i][3], input[i][4], int64(i)) assertGoldenClose(t, got, goldenCell(exp[i][0]), i, "awesome_oscillator_histogram") } } func TestGoldenAverageDrawdown(t *testing.T) { input := goldenInput(t) exp := readGolden(t, "average_drawdown") avg, err := NewAverageDrawdown(20) if err != nil { t.Fatal(err) } defer avg.Close() for i := range input { // generator fed the close column as the equity-curve sample. assertGoldenClose(t, avg.Update(input[i][3]), goldenCell(exp[i][0]), i, "average_drawdown") } }