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:
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
Reference in New Issue
Block a user