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
+12
View File
@@ -128,6 +128,8 @@ typedef struct CalmarRatio CalmarRatio;
typedef struct Camarilla Camarilla;
typedef struct CandleReader CandleReader;
typedef struct CandleVolume CandleVolume;
typedef struct Cci Cci;
@@ -13736,6 +13738,16 @@ bool wickra_resampler_flush(struct Resampler *handle, struct WickraCandle *out);
void wickra_resampler_free(struct Resampler *handle);
struct CandleReader *wickra_candle_reader_new(const uint8_t *data, uintptr_t len);
uintptr_t wickra_candle_reader_count(const struct CandleReader *handle);
uintptr_t wickra_candle_reader_read(struct CandleReader *handle,
struct WickraCandle *out,
uintptr_t cap);
void wickra_candle_reader_free(struct CandleReader *handle);
#ifdef __cplusplus
} // extern "C"
#endif // __cplusplus
+93
View File
@@ -68286,6 +68286,99 @@ pub unsafe extern "C" fn wickra_resampler_free(handle: *mut Resampler) {
}
}
/// Opaque CSV candle reader: parses an entire `timestamp,open,high,low,close,volume`
/// CSV buffer up front and hands the candles out in drain order. Named
/// `CandleReader` (the public C-ABI handle); the inner `wickra-data` reader is
/// reached through its full path to avoid the name clash.
#[derive(Debug)]
pub struct CandleReader {
candles: Vec<Candle>,
pos: usize,
}
/// Parse an OHLCV CSV buffer (`len` bytes at `data`) into candles. The first line
/// must be a header naming `timestamp,open,high,low,close,volume` (a leading UTF-8
/// BOM and field whitespace are tolerated). Returns `NULL` on a `NULL` pointer or a
/// malformed CSV (missing column, unparseable row, or an OHLC relation the core
/// rejects). Read the candles with `wickra_candle_reader_read` and release with
/// `wickra_candle_reader_free`.
///
/// # Safety
/// `data` must point to `len` readable bytes, or be `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_new(
data: *const u8,
len: usize,
) -> *mut CandleReader {
if data.is_null() {
return ptr::null_mut();
}
let bytes = slice::from_raw_parts(data, len);
let Ok(mut reader) = wickra_data::csv::CandleReader::from_reader(bytes) else {
return ptr::null_mut();
};
match reader.read_all() {
Ok(candles) => Box::into_raw(Box::new(CandleReader { candles, pos: 0 })),
Err(_) => ptr::null_mut(),
}
}
/// Number of candles not yet read from the reader. Returns `0` on a `NULL` handle.
///
/// # Safety
/// `handle` must be valid (from `wickra_candle_reader_new`, not freed), or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_count(handle: *const CandleReader) -> usize {
match handle.as_ref() {
Some(reader) => reader.candles.len() - reader.pos,
None => 0,
}
}
/// Copy up to `cap` not-yet-read candles into `out`, advance past them, and return
/// the number written. Returns `0` on a `NULL` handle / `out`. Call with `cap` equal
/// to `wickra_candle_reader_count` to drain every candle in one call.
///
/// # Safety
/// `handle` (from `wickra_candle_reader_new`, not freed) and `out` must be valid or
/// `NULL`; when non-`NULL`, `out` must cover `cap` `WickraCandle` elements.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_read(
handle: *mut CandleReader,
out: *mut WickraCandle,
cap: usize,
) -> usize {
let Some(reader) = handle.as_mut() else {
return 0;
};
if out.is_null() {
return 0;
}
let count = (reader.candles.len() - reader.pos).min(cap);
let slots = slice::from_raw_parts_mut(out, count);
for (slot, candle) in slots
.iter_mut()
.zip(&reader.candles[reader.pos..reader.pos + count])
{
*slot = candle_to_c(*candle);
}
reader.pos += count;
count
}
/// Destroy a candle reader created by `wickra_candle_reader_new`. No-op if `handle`
/// is `NULL`.
///
/// # Safety
/// `handle` must have been returned by `wickra_candle_reader_new` and not previously
/// freed, or `NULL`.
#[no_mangle]
pub unsafe extern "C" fn wickra_candle_reader_free(handle: *mut CandleReader) {
if !handle.is_null() {
drop(Box::from_raw(handle));
}
}
#[cfg(test)]
mod tests {
use super::*;