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:
@@ -0,0 +1,69 @@
|
||||
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
|
||||
package org.wickra;
|
||||
|
||||
import org.wickra.internal.NativeMethods;
|
||||
import org.wickra.internal.WickraNative;
|
||||
import java.lang.foreign.Arena;
|
||||
import java.lang.foreign.MemorySegment;
|
||||
import java.lang.ref.Cleaner;
|
||||
import static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** CSV candle reader over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class CandleReader implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
/** Parse a timestamp,open,high,low,close,volume CSV string (a leading
|
||||
* UTF-8 BOM and field whitespace are tolerated). */
|
||||
public CandleReader(String csv) {
|
||||
if (csv == null) {
|
||||
throw new NullPointerException("csv");
|
||||
}
|
||||
byte[] bytes = csv.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
MemorySegment h;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment data = a.allocate(Math.max(1L, bytes.length));
|
||||
MemorySegment.copy(bytes, 0, data, JAVA_BYTE, 0L, bytes.length);
|
||||
h = (MemorySegment) NativeMethods.WICKRA_CANDLE_READER_NEW.invokeExact(data, (long) bytes.length);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid CandleReader CSV");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_CANDLE_READER_FREE);
|
||||
}
|
||||
|
||||
/** Every candle parsed from the CSV, in file order. */
|
||||
public Candle[] read() {
|
||||
try {
|
||||
long n = (long) NativeMethods.WICKRA_CANDLE_READER_COUNT.invokeExact(handle);
|
||||
if (n <= 0) {
|
||||
return new Candle[0];
|
||||
}
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment out = a.allocate(48L * n);
|
||||
long w = (long) NativeMethods.WICKRA_CANDLE_READER_READ.invokeExact(handle, out, n);
|
||||
Candle[] result = new Candle[(int) w];
|
||||
for (int i = 0; i < w; i++) {
|
||||
long b = (long) i * 48L;
|
||||
result[i] = new Candle(
|
||||
out.get(JAVA_DOUBLE, b + 0L),
|
||||
out.get(JAVA_DOUBLE, b + 8L),
|
||||
out.get(JAVA_DOUBLE, b + 16L),
|
||||
out.get(JAVA_DOUBLE, b + 24L),
|
||||
out.get(JAVA_DOUBLE, b + 32L),
|
||||
(double) out.get(JAVA_LONG, b + 40L));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -3955,6 +3955,10 @@ public final class NativeMethods {
|
||||
public static MethodHandle WICKRA_RESAMPLER_UPDATE;
|
||||
public static MethodHandle WICKRA_RESAMPLER_FLUSH;
|
||||
public static MethodHandle WICKRA_RESAMPLER_FREE;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_NEW;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_COUNT;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_READ;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_FREE;
|
||||
|
||||
static {
|
||||
init0();
|
||||
@@ -8031,6 +8035,10 @@ public final class NativeMethods {
|
||||
WICKRA_RESAMPLER_UPDATE = h("wickra_resampler_update", FunctionDescriptor.of(JAVA_BYTE, ADDRESS, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_DOUBLE, JAVA_LONG, ADDRESS));
|
||||
WICKRA_RESAMPLER_FLUSH = h("wickra_resampler_flush", FunctionDescriptor.of(JAVA_BYTE, ADDRESS, ADDRESS));
|
||||
WICKRA_RESAMPLER_FREE = h("wickra_resampler_free", FunctionDescriptor.ofVoid(ADDRESS));
|
||||
WICKRA_CANDLE_READER_NEW = h("wickra_candle_reader_new", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG));
|
||||
WICKRA_CANDLE_READER_COUNT = h("wickra_candle_reader_count", FunctionDescriptor.of(JAVA_LONG, ADDRESS));
|
||||
WICKRA_CANDLE_READER_READ = h("wickra_candle_reader_read", FunctionDescriptor.of(JAVA_LONG, ADDRESS, ADDRESS, JAVA_LONG));
|
||||
WICKRA_CANDLE_READER_FREE = h("wickra_candle_reader_free", FunctionDescriptor.ofVoid(ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -73,6 +73,25 @@ class DataLayerTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void candleReaderMatchesGolden() throws IOException {
|
||||
String csv = Files.readString(goldenDir().resolve("data_csv.csv"));
|
||||
try (CandleReader reader = new CandleReader(csv)) {
|
||||
Candle[] candles = reader.read();
|
||||
double[][] want = read("data_csv_candles");
|
||||
assertEquals(want.length, candles.length, "candle reader count");
|
||||
for (int i = 0; i < candles.length; i++) {
|
||||
Candle k = candles[i];
|
||||
double[] got = {k.open(), k.high(), k.low(), k.close(), k.volume(), (double) k.timestamp()};
|
||||
for (int j = 0; j < 6; j++) {
|
||||
double w = want[i][j];
|
||||
assertTrue(Math.abs(got[j] - w) <= 1e-9 * Math.max(1, Math.abs(w)),
|
||||
"candle reader row " + i + " col " + j + ": " + got[j] + " vs " + w);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void tickAggregatorMatchesGolden() throws IOException {
|
||||
double[][] ticks = read("data_ticks");
|
||||
|
||||
Reference in New Issue
Block a user