feat(data): native Binance REST kline fetcher in 9 languages (#315)
Adds `BinanceRest::fetch_klines` to `wickra-data`: a blocking historical kline downloader (`GET /api/v3/klines`) and the historical counterpart to the F4 live `BinanceFeed`. It is the last native data-layer primitive needed to drop third-party HTTP/JSON download helpers (`jackson`, `jsonlite`, `urllib`, …) from the examples. ## What - **Core** (`wickra-data`): `fetch_klines(symbol, interval, limit, start?, end?)` built on `ureq` with native-tls — sharing the exact same TLS backend (native-tls 0.2 / SChannel) as the existing tokio-tungstenite live feed, so the two pull one TLS stack, not two. Parses Binance's 12-element array rows via the existing serde infrastructure into validated `Candle`s. Blocking by design (a one-shot request needs no async runtime; the FFI boundary is synchronous anyway). Nine mock-HTTP-server tests cover parse / empty / limit / transport / JSON / invariant-violation paths. - **C ABI**: `wickra_binance_fetch_klines(...)` (blocking drain into a caller buffer, `-1` on error) + regenerated cbindgen header and its vendored Go copy. - **Bindings**: native Node `fetchBinanceKlines` / Python `fetch_binance_klines`; generated Go `FetchBinanceKlines` / C# `BinanceFeed.FetchKlines` / Java `BinanceFeed.fetchKlines` / R `fetch_binance_klines`. C / C++ call the C ABI directly. **WASM is excluded** (browsers use the host `fetch`). The four C-ABI bindings are regenerated from the ScriptHelpers generators (not hand-edited); the regen diff is exactly the new wrapper in each. ## Verification All ten toolchains green locally: Rust (`cargo test`/`clippy`/`fmt`), Node, Python, Go, C#, Java, R (`R CMD INSTALL` + smoke), WASM (`cargo check`, confirmed `ureq` is not pulled). Each binding has an error-path smoke test; the parse/HTTP success path is covered by the Rust mock-server tests. No release in this PR — ships with the data-layer + numpy bundle later.
This commit is contained in:
@@ -27,4 +27,27 @@ public class BinanceFeedTests
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
new BinanceFeed("BTCUSDT", BinanceInterval.OneMinute, "ws://127.0.0.1:1"));
|
||||
}
|
||||
|
||||
// The REST fetcher's parse/HTTP success path is covered by the Rust
|
||||
// mock-HTTP-server tests; here we only assert the binding's error paths.
|
||||
[Fact]
|
||||
public void FetchKlinesRejectsUnknownInterval()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BinanceFeed.FetchKlines("BTCUSDT", (BinanceInterval)99, 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FetchKlinesRejectsZeroLimit()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BinanceFeed.FetchKlines("BTCUSDT", BinanceInterval.OneHour, 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FetchKlinesSurfacesUnreachableEndpoint()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
BinanceFeed.FetchKlines("BTCUSDT", BinanceInterval.OneHour, 1, baseUrl: "http://127.0.0.1:1"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41589,6 +41589,43 @@ public sealed class BinanceFeed : IDisposable
|
||||
return new KlineEvent(symbol, ev.open, ev.high, ev.low, ev.close, ev.volume, ev.open_time, ev.is_closed != 0);
|
||||
}
|
||||
|
||||
/// <summary>Fetch historical klines from Binance's REST endpoint. <paramref name="symbol"/>
|
||||
/// is the trading pair (case-insensitive), <paramref name="limit"/> the number of
|
||||
/// candles (1..=1000). <paramref name="startMs"/>/<paramref name="endMs"/> are inclusive
|
||||
/// Unix-millisecond bounds (negative = unset); <paramref name="baseUrl"/> overrides the
|
||||
/// host (null = production). Blocks until the response arrives.</summary>
|
||||
public static Candle[] FetchKlines(string symbol, BinanceInterval interval, uint limit, long startMs = -1, long endMs = -1, string? baseUrl = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(symbol);
|
||||
if (limit == 0)
|
||||
{
|
||||
throw new ArgumentException("limit must be in 1..=1000");
|
||||
}
|
||||
var symBytes = System.Text.Encoding.UTF8.GetBytes(symbol + '\0');
|
||||
var urlBytes = baseUrl is null ? null : System.Text.Encoding.UTF8.GetBytes(baseUrl + '\0');
|
||||
var buffer = new WickraCandle[limit];
|
||||
long count;
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* sp = symBytes)
|
||||
fixed (byte* up = urlBytes)
|
||||
fixed (WickraCandle* ptr = buffer)
|
||||
{
|
||||
count = (long)NativeMethods.wickra_binance_fetch_klines(sp, (byte)interval, limit, startMs, endMs, up, ptr, (nuint)limit);
|
||||
}
|
||||
}
|
||||
if (count < 0)
|
||||
{
|
||||
throw new ArgumentException("invalid FetchKlines parameters or transport error");
|
||||
}
|
||||
var result = new Candle[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
result[i] = new Candle(buffer[i].open, buffer[i].high, buffer[i].low, buffer[i].close, buffer[i].volume, buffer[i].timestamp);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
unsafe
|
||||
|
||||
@@ -12454,6 +12454,9 @@ internal static partial class NativeMethods
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static partial void wickra_binance_free(nint handle);
|
||||
|
||||
[LibraryImport(WickraNative.LibraryName)]
|
||||
internal static unsafe partial nint wickra_binance_fetch_klines(byte* symbol, byte interval, uint limit, long startMs, long endMs, byte* baseUrl, WickraCandle* @out, nuint cap);
|
||||
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
|
||||
Reference in New Issue
Block a user