using System; using System.Collections.Generic; using System.Globalization; using System.IO; using Wickra; using Xunit; // Cross-language data-layer parity: replay the shared golden tick stream through // the TickAggregator and check the candles against the Rust reference, with and // without gap filling. public class DataLayerTests { private static string GoldenDir([System.Runtime.CompilerServices.CallerFilePath] string file = "") => Path.GetFullPath(Path.Combine(Path.GetDirectoryName(file)!, "..", "..", "..", "testdata", "golden")); private static double[][] Read(string name) { var lines = File.ReadAllLines(Path.Combine(GoldenDir(), name + ".csv")); var rows = new List(); for (var i = 1; i < lines.Length; i++) { if (lines[i].Length == 0) { continue; } rows.Add(Array.ConvertAll(lines[i].Split(','), s => double.Parse(s, CultureInfo.InvariantCulture))); } return rows.ToArray(); } [Theory] [InlineData(false, "data_candles")] [InlineData(true, "data_candles_gap")] public void TickAggregatorMatchesGolden(bool gapFill, string fixture) { var ticks = Read("data_ticks"); using var agg = new TickAggregator(1000, gapFill); var got = new List(); foreach (var t in ticks) { foreach (var c in agg.Push(t[0], t[1], (long)t[2])) { got.Add(new[] { c.Open, c.High, c.Low, c.Close, c.Volume, (double)c.Timestamp }); } } var want = Read(fixture); Assert.Equal(want.Length, got.Count); for (var i = 0; i < got.Count; i++) { for (var j = 0; j < 6; j++) { var tol = 1e-9 * Math.Max(1, Math.Abs(want[i][j])); Assert.True( Math.Abs(got[i][j] - want[i][j]) <= tol, $"{fixture} row {i} col {j}: {got[i][j]} vs {want[i][j]}"); } } } }