feat(data): expose CandleReader (CSV) natively in all 10 languages (#311)

Add the data-layer CSV candle reader to every binding so loading OHLCV
candles from a CSV no longer needs a per-language CSV/dataframe dependency.

- C ABI: wickra_candle_reader_new(bytes, len) / _count / _read / _free over
  an opaque CandleReader handle (parse the whole buffer up front, then drain).
- Native: Node/WASM CandleReader.read() -> Candle[], Python read() -> list[tuple].
- C-ABI languages: Go Read() []Candle, C# Candle[] Read(), Java Candle[] read(),
  R read() S3 generic (n x 6 matrix); C / C++ call the C ABI directly.
- Cross-language golden testdata/golden/data_csv*.csv pins the parsed candles
  bit-for-bit across every binding.

Verified locally across Rust (test+clippy+fmt), Node, WASM, Python, C#, Go,
Java, R, and the C/C++ cmake parity suite.
This commit is contained in:
kingchenc
2026-06-16 00:10:58 +02:00
committed by GitHub
parent cb6da4d737
commit d362ae26a3
31 changed files with 867 additions and 6 deletions
@@ -4981,6 +4981,60 @@ public sealed class Camarilla : IDisposable
public void Dispose() => _handle.Dispose();
}
public sealed class CandleReader : IDisposable
{
private readonly WickraHandle _handle;
/// <summary>Parse a timestamp,open,high,low,close,volume CSV string (a
/// leading UTF-8 BOM and field whitespace are tolerated).</summary>
public CandleReader(string csv)
{
ArgumentNullException.ThrowIfNull(csv);
var bytes = System.Text.Encoding.UTF8.GetBytes(csv);
nint ptr;
unsafe
{
fixed (byte* data = bytes)
{
ptr = NativeMethods.wickra_candle_reader_new(data, (nuint)bytes.Length);
}
}
if (ptr == nint.Zero)
{
throw new ArgumentException("invalid CandleReader CSV");
}
_handle = new WickraHandle(ptr, NativeMethods.wickra_candle_reader_free);
}
/// <summary>Every candle parsed from the CSV, in file order.</summary>
public Candle[] Read()
{
var count = (long)NativeMethods.wickra_candle_reader_count(_handle.DangerousGetHandle());
GC.KeepAlive(_handle);
if (count <= 0)
{
return Array.Empty<Candle>();
}
var buffer = new WickraCandle[count];
unsafe
{
fixed (WickraCandle* ptr = buffer)
{
NativeMethods.wickra_candle_reader_read(_handle.DangerousGetHandle(), ptr, (nuint)count);
}
}
GC.KeepAlive(_handle);
var result = new Candle[count];
for (var i = 0; i < count; i++)
{
result[i] = new Candle(buffer[i].open, buffer[i].high, buffer[i].low, buffer[i].close, buffer[i].volume, buffer[i].timestamp);
}
return result;
}
public void Dispose() => _handle.Dispose();
}
public sealed class CandleVolume : IDisposable
{
private readonly WickraHandle _handle;
@@ -12430,6 +12430,18 @@ internal static partial class NativeMethods
[LibraryImport(WickraNative.LibraryName)]
internal static partial void wickra_resampler_free(nint handle);
[LibraryImport(WickraNative.LibraryName)]
internal static unsafe partial nint wickra_candle_reader_new(byte* data, nuint len);
[LibraryImport(WickraNative.LibraryName)]
internal static partial nuint wickra_candle_reader_count(nint handle);
[LibraryImport(WickraNative.LibraryName)]
internal static unsafe partial nuint wickra_candle_reader_read(nint handle, WickraCandle* @out, nuint cap);
[LibraryImport(WickraNative.LibraryName)]
internal static partial void wickra_candle_reader_free(nint handle);
}
[StructLayout(LayoutKind.Sequential)]