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
+48
View File
@@ -187,9 +187,57 @@ fn main() {
emit_bars(dir, &candles);
emit_data_layer(dir);
emit_resampler(dir, &candles);
emit_candle_reader_csv(dir, &candles);
println!("golden fixtures written to {}", dir.display());
}
/// Data layer: the CSV candle reader. Writes a source CSV in the reader's required
/// `timestamp,open,high,low,close,volume` layout, plus the reference candles the
/// reader parses out of it. Every binding parses the same bytes and checks the
/// candles match — pinning column mapping and numeric round-tripping across the
/// FFI.
fn emit_candle_reader_csv(dir: &Path, candles: &[Candle]) {
let mut src = Vec::with_capacity(candles.len());
for c in candles {
src.push(format!(
"{},{},{},{},{},{}",
c.timestamp, c.open, c.high, c.low, c.close, c.volume
));
}
write_csv(
dir,
"data_csv",
"timestamp,open,high,low,close,volume",
&src,
);
// Reference parse: feed the same bytes back through the reader so the fixture
// is exactly what wickra-data's parser yields, not just the input echoed.
let mut bytes = String::from("timestamp,open,high,low,close,volume\n");
for row in &src {
bytes.push_str(row);
bytes.push('\n');
}
let mut reader =
wickra_data::csv::CandleReader::from_reader(bytes.as_bytes()).expect("valid candle reader");
let parsed = reader.read_all().expect("valid csv parse");
let rows: Vec<String> = parsed
.iter()
.map(|c| {
format!(
"{},{},{},{},{},{}",
c.open, c.high, c.low, c.close, c.volume, c.timestamp
)
})
.collect();
write_csv(
dir,
"data_csv_candles",
"open,high,low,close,volume,timestamp",
&rows,
);
}
/// Data layer: the resampler. Resamples the shared input candles (timestamp =
/// row index) into 5-unit buckets; the final partial bucket comes out of flush.
fn emit_resampler(dir: &Path, candles: &[Candle]) {