feat(data): native live Binance kline feed in 9 languages (+ 3d/1M intervals) (#313)
* feat(data): C-ABI Binance feed + Go binding (F4 wip)
C ABI exposes the existing async BinanceKlineStream (tokio + TLS, auto-reconnect,
mock-server-tested in wickra-data) through a blocking poll: wickra_binance_connect
/ _next(out, timeout_ms) -> {1 event, 0 timeout, -1 closed} / _close / _free over
an opaque BinanceStream that owns a current-thread runtime. WickraKlineEvent
carries OHLCV + open_time + is_closed + a 16-byte symbol buffer. `live-binance`
is now a default feature of wickra-c (the published DLL ships the feed; the wasm
build drops it via --no-default-features).
Go: NewBinanceFeed(symbols, interval, baseURL) + Next(timeout) + Close, with a
deterministic error-path smoke (the connect->event pipeline is covered by the
Rust mock-WS-server tests).
* feat(data): Binance feed C# + Java bindings (F4 wip)
C#: BinanceFeed(symbols, interval, baseUrl?) + Next(timeout) -> KlineEvent? +
Dispose; bespoke WickraKlineEvent native struct (fixed symbol buffer + byte
is_closed) since the scalar struct parser can't model it. `char` maps to `byte`
for the const char* params.
Java: BinanceFeed + KlineEvent record + BinanceInterval enum over Panama FFM;
the event is read at hand-computed offsets (symbol@0, doubles@16..48,
open_time@56, is_closed@64; 72-byte struct). Both with deterministic error-path
smokes (pipeline covered by the Rust mock-WS-server tests).
* feat(data): Binance feed R binding (F4 wip)
R: BinanceFeed(symbols, interval, base_url) + binance_next(feed, timeout_ms) ->
named list | NULL + binance_close, via bespoke .Call glue (wk_binance_*). The
glue + its registration entries are gated out of the Emscripten/wasm build
(#ifndef __EMSCRIPTEN__) since r-universe/webR has no raw sockets. NAMESPACE
exports added by hand (roxygen2 not installed locally). Deterministic error-path
smoke; pipeline covered by the Rust mock-WS-server tests.
* feat(data): native Binance feed for Node + Python; CHANGELOG (F4 complete)
Node (napi) BinanceFeed: new(symbols, interval, baseUrl?) + next(timeoutMs) ->
KlineEvent | null + close. Python (pyo3) BinanceFeed: same, with next releasing
the GIL (py.detach) while it waits. Both drive the mock-server-tested async
BinanceKlineStream on a single-thread tokio runtime (blocking poll); wickra-data
gains the live-binance feature + tokio in each binding.
Completes F4: the live Binance kline feed is now native in all 9 languages
(WASM excluded), with no third-party WebSocket client in any of them.
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// 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 java.nio.charset.StandardCharsets;
|
||||
import static java.lang.foreign.ValueLayout.*;
|
||||
|
||||
/** A live Binance kline stream over the Wickra C ABI. Not thread-safe; close when done. */
|
||||
public final class BinanceFeed implements AutoCloseable {
|
||||
private final MemorySegment handle;
|
||||
private final Cleaner.Cleanable cleanable;
|
||||
|
||||
/** Connect to Binance's live kline stream for the given comma-separated
|
||||
* symbols (case-insensitive) at interval. baseUrl overrides the endpoint
|
||||
* (null = production; pass a ws:// URL to target a test server). */
|
||||
public BinanceFeed(String symbols, BinanceInterval interval, String baseUrl) {
|
||||
if (symbols == null) {
|
||||
throw new NullPointerException("symbols");
|
||||
}
|
||||
MemorySegment h;
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
byte[] sb = symbols.getBytes(StandardCharsets.UTF_8);
|
||||
MemorySegment sym = a.allocate(sb.length + 1L);
|
||||
MemorySegment.copy(sb, 0, sym, JAVA_BYTE, 0L, sb.length);
|
||||
MemorySegment url = MemorySegment.NULL;
|
||||
if (baseUrl != null) {
|
||||
byte[] ub = baseUrl.getBytes(StandardCharsets.UTF_8);
|
||||
url = a.allocate(ub.length + 1L);
|
||||
MemorySegment.copy(ub, 0, url, JAVA_BYTE, 0L, ub.length);
|
||||
}
|
||||
h = (MemorySegment) NativeMethods.WICKRA_BINANCE_CONNECT.invokeExact(
|
||||
sym, (byte) interval.ordinal(), url);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
if (h.address() == 0L) {
|
||||
throw new IllegalArgumentException("invalid BinanceFeed parameters");
|
||||
}
|
||||
this.handle = h;
|
||||
this.cleanable = WickraNative.register(this, h, NativeMethods.WICKRA_BINANCE_FREE);
|
||||
}
|
||||
|
||||
/** Connect with the production endpoint. */
|
||||
public BinanceFeed(String symbols, BinanceInterval interval) {
|
||||
this(symbols, interval, null);
|
||||
}
|
||||
|
||||
/** Poll for the next kline event, waiting up to timeoutMillis. Returns the
|
||||
* event, or null on timeout (call again). Throws once the stream is closed. */
|
||||
public KlineEvent next(long timeoutMillis) {
|
||||
try (Arena a = Arena.ofConfined()) {
|
||||
MemorySegment out = a.allocate(72L);
|
||||
int code = (int) NativeMethods.WICKRA_BINANCE_NEXT.invokeExact(handle, out, timeoutMillis);
|
||||
if (code == 0) {
|
||||
return null;
|
||||
}
|
||||
if (code != 1) {
|
||||
throw new IllegalStateException("binance feed closed");
|
||||
}
|
||||
byte[] symBytes = new byte[16];
|
||||
MemorySegment.copy(out, JAVA_BYTE, 0L, symBytes, 0, 16);
|
||||
int n = 0;
|
||||
while (n < 16 && symBytes[n] != 0) {
|
||||
n++;
|
||||
}
|
||||
return new KlineEvent(
|
||||
new String(symBytes, 0, n, StandardCharsets.UTF_8),
|
||||
out.get(JAVA_DOUBLE, 16L),
|
||||
out.get(JAVA_DOUBLE, 24L),
|
||||
out.get(JAVA_DOUBLE, 32L),
|
||||
out.get(JAVA_DOUBLE, 40L),
|
||||
out.get(JAVA_DOUBLE, 48L),
|
||||
out.get(JAVA_LONG, 56L),
|
||||
out.get(JAVA_BYTE, 64L) != 0);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
try {
|
||||
NativeMethods.WICKRA_BINANCE_CLOSE.invokeExact(handle);
|
||||
} catch (Throwable t) {
|
||||
throw WickraNative.rethrow(t);
|
||||
}
|
||||
cleanable.clean();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
|
||||
package org.wickra;
|
||||
|
||||
/** Kline interval for the live Binance feed (ordinal = the C ABI code). */
|
||||
public enum BinanceInterval {
|
||||
ONE_SECOND,
|
||||
ONE_MINUTE,
|
||||
THREE_MINUTES,
|
||||
FIVE_MINUTES,
|
||||
FIFTEEN_MINUTES,
|
||||
THIRTY_MINUTES,
|
||||
ONE_HOUR,
|
||||
TWO_HOURS,
|
||||
FOUR_HOURS,
|
||||
SIX_HOURS,
|
||||
EIGHT_HOURS,
|
||||
TWELVE_HOURS,
|
||||
ONE_DAY,
|
||||
THREE_DAYS,
|
||||
ONE_WEEK,
|
||||
ONE_MONTH
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Generated from bindings/c/include/wickra.h. Do not edit by hand.
|
||||
package org.wickra;
|
||||
|
||||
/** One event from the live Binance feed. */
|
||||
public record KlineEvent(String symbol, double open, double high, double low,
|
||||
double close, double volume, long openTime, boolean isClosed) {}
|
||||
@@ -3959,6 +3959,10 @@ public final class NativeMethods {
|
||||
public static MethodHandle WICKRA_CANDLE_READER_COUNT;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_READ;
|
||||
public static MethodHandle WICKRA_CANDLE_READER_FREE;
|
||||
public static MethodHandle WICKRA_BINANCE_CONNECT;
|
||||
public static MethodHandle WICKRA_BINANCE_NEXT;
|
||||
public static MethodHandle WICKRA_BINANCE_CLOSE;
|
||||
public static MethodHandle WICKRA_BINANCE_FREE;
|
||||
|
||||
static {
|
||||
init0();
|
||||
@@ -8039,6 +8043,10 @@ public final class NativeMethods {
|
||||
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));
|
||||
WICKRA_BINANCE_CONNECT = h("wickra_binance_connect", FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_BYTE, ADDRESS));
|
||||
WICKRA_BINANCE_NEXT = h("wickra_binance_next", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, JAVA_LONG));
|
||||
WICKRA_BINANCE_CLOSE = h("wickra_binance_close", FunctionDescriptor.ofVoid(ADDRESS));
|
||||
WICKRA_BINANCE_FREE = h("wickra_binance_free", FunctionDescriptor.ofVoid(ADDRESS));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.wickra;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/**
|
||||
* The live Binance feed's connect → read → reconnect pipeline is covered
|
||||
* deterministically by the Rust mock-WS-server tests in wickra-data. Here we
|
||||
* only assert the binding's error paths, which need no network.
|
||||
*/
|
||||
class BinanceFeedTest {
|
||||
@Test
|
||||
void rejectsEmptySymbols() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new BinanceFeed("", BinanceInterval.ONE_MINUTE));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsUnreachableEndpoint() {
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> new BinanceFeed("BTCUSDT", BinanceInterval.ONE_MINUTE, "ws://127.0.0.1:1"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user