package wickra import ( "math" "os" "strconv" "testing" ) // Cross-language data-layer parity: replay the shared golden tick stream through // the TickAggregator and assert the candles match the Rust reference, with and // without gap filling. Fixtures are generated by // `cargo run -p wickra-examples --bin gen_golden`. func dataParseF(t *testing.T, s string) float64 { t.Helper() v, err := strconv.ParseFloat(s, 64) if err != nil { t.Fatalf("parse %q: %v", s, err) } return v } func TestResamplerGolden(t *testing.T) { input := readGolden(t, "input") // open,high,low,close,volume (timestamp = row index) r, err := NewResampler(5) if err != nil { t.Fatalf("new: %v", err) } var got [][6]float64 for i, row := range input { o, h, l, c, v := dataParseF(t, row[0]), dataParseF(t, row[1]), dataParseF(t, row[2]), dataParseF(t, row[3]), dataParseF(t, row[4]) if k, ok := r.Update(o, h, l, c, v, int64(i)); ok { got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)}) } } if k, ok := r.Flush(); ok { got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)}) } r.Close() want := readGolden(t, "data_resampled") if len(got) != len(want) { t.Fatalf("resample: %d candles vs %d", len(got), len(want)) } for i := range got { for j := 0; j < 6; j++ { w := dataParseF(t, want[i][j]) if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) { t.Errorf("resample row %d col %d: %v vs %v", i, j, got[i][j], w) } } } } func TestCandleReaderGolden(t *testing.T) { csv, err := os.ReadFile("../../testdata/golden/data_csv.csv") if err != nil { t.Fatalf("read data_csv.csv: %v", err) } r, err := NewCandleReader(string(csv)) if err != nil { t.Fatalf("new: %v", err) } defer r.Close() candles := r.Read() got := make([][6]float64, len(candles)) for i, c := range candles { got[i] = [6]float64{c.Open, c.High, c.Low, c.Close, c.Volume, float64(c.Timestamp)} } want := readGolden(t, "data_csv_candles") if len(got) != len(want) { t.Fatalf("candle reader: %d candles vs %d", len(got), len(want)) } for i := range got { for j := 0; j < 6; j++ { w := dataParseF(t, want[i][j]) if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) { t.Errorf("candle reader row %d col %d: %v vs %v", i, j, got[i][j], w) } } } } func TestTickAggregatorGolden(t *testing.T) { ticks := readGolden(t, "data_ticks") cases := []struct { gap bool fixture string }{ {false, "data_candles"}, {true, "data_candles_gap"}, } for _, c := range cases { agg, err := NewTickAggregator(1000, c.gap) if err != nil { t.Fatalf("new: %v", err) } var got [][6]float64 for _, r := range ticks { price, size, ts := dataParseF(t, r[0]), dataParseF(t, r[1]), int64(dataParseF(t, r[2])) for _, k := range agg.Push(price, size, ts) { got = append(got, [6]float64{k.Open, k.High, k.Low, k.Close, k.Volume, float64(k.Timestamp)}) } } agg.Close() want := readGolden(t, c.fixture) if len(got) != len(want) { t.Fatalf("%s: %d candles vs %d", c.fixture, len(got), len(want)) } for i := range got { for j := 0; j < 6; j++ { w := dataParseF(t, want[i][j]) if math.Abs(got[i][j]-w) > 1e-9*math.Max(1, math.Abs(w)) { t.Errorf("%s row %d col %d: %v vs %v", c.fixture, i, j, got[i][j], w) } } } } }