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:
@@ -147,6 +147,49 @@ static int check_resample(void) {
|
||||
return fails;
|
||||
}
|
||||
|
||||
static int check_reader(void) {
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/data_csv.csv", GDIR);
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
printf("FAIL reader: cannot open %s\n", path);
|
||||
return 1;
|
||||
}
|
||||
static char buf[1 << 16];
|
||||
size_t len = fread(buf, 1, sizeof(buf), f);
|
||||
fclose(f);
|
||||
|
||||
struct CandleReader *r = wickra_candle_reader_new((const uint8_t *)buf, len);
|
||||
if (!r) {
|
||||
printf("FAIL reader: new returned NULL\n");
|
||||
return 1;
|
||||
}
|
||||
double want[MAXROWS * 6];
|
||||
int nw = read_csv("data_csv_candles", want, 6);
|
||||
uintptr_t n = wickra_candle_reader_count(r);
|
||||
struct WickraCandle cands[MAXROWS];
|
||||
uintptr_t got = wickra_candle_reader_read(r, cands, n);
|
||||
wickra_candle_reader_free(r);
|
||||
if ((int)got != nw) {
|
||||
printf("FAIL reader: %d candles vs %d\n", (int)got, nw);
|
||||
return 1;
|
||||
}
|
||||
int fails = 0;
|
||||
for (uintptr_t i = 0; i < got; i++) {
|
||||
double row[6] = {cands[i].open, cands[i].high, cands[i].low,
|
||||
cands[i].close, cands[i].volume, (double)cands[i].timestamp};
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double w = want[i * 6 + j];
|
||||
double tol = 1e-9 * fmax(1.0, fabs(w));
|
||||
if (fabs(row[j] - w) > tol) {
|
||||
printf("FAIL reader row %d col %d: %g vs %g\n", (int)i, j, row[j], w);
|
||||
fails++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return fails;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
GDIR = (argc > 1) ? argv[1] : "testdata/golden";
|
||||
double ticks[MAXROWS * 3];
|
||||
@@ -154,6 +197,7 @@ int main(int argc, char **argv) {
|
||||
int fails = check("data_candles", false, ticks, nt);
|
||||
fails += check("data_candles_gap", true, ticks, nt);
|
||||
fails += check_resample();
|
||||
fails += check_reader();
|
||||
if (fails == 0) {
|
||||
printf("C/C++ data layer: OK (%d ticks)\n", nt);
|
||||
}
|
||||
|
||||
@@ -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]) {
|
||||
|
||||
Reference in New Issue
Block a user