examples: migrate to the native data layer (drop ws/coder-websocket/jackson/jsonlite) (#316)

Stacked on #315 (the native Binance REST fetcher). Retarget to `main` once #315 merges.

Migrates the runnable examples off third-party data-I/O packages onto Wickra's
native data layer (`CandleReader`, `Resampler`, `BinanceFeed`, `fetch_*klines`).

## Third-party packages removed (the zero-dep selling point)
- **Node**: `ws` (live feed → BinanceFeed) — dropped from package.json + lockfile
- **Go**: `github.com/coder/websocket` — dropped from go.mod / go.sum (`go mod tidy`)
- **Java**: `jackson-databind` (live feed + REST fetch) — dropped from pom.xml
- **R**: `jsonlite` + `websocket` + `later` — dropped from the README notes

Each language's CSV loading now goes through `CandleReader`, manual resampling
through `Resampler`, the live feed through `BinanceFeed`, and (Java/R) the REST
download through the native fetcher.

## Verification
Ran the offline examples per language against the bundled data — backtest and
multi_timeframe produce identical output across Python / Node / Go / Java / R
(e.g. ATR(14) last 345.1010; 1h→5m resamples to 240 bars, →15m to 80 bars).

C# / C / WASM (stdlib-only, no third-party deps to remove) follow in this branch.

Note: the streaming `strategy_*` examples have pre-existing candle-indicator
runtime bugs (CI only syntax-smokes them); the CSV migration preserves their
shape and leaves those bugs for a separate fix.
This commit is contained in:
kingchenc
2026-06-17 01:49:11 +02:00
committed by GitHub
parent 2ae76bb90e
commit 677ea37402
40 changed files with 576 additions and 1102 deletions
-6
View File
@@ -23,12 +23,6 @@
<artifactId>wickra</artifactId>
<version>0.9.2</version>
</dependency>
<!-- Only the network examples (fetch_btcusdt) parse JSON. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.22.0</version>
</dependency>
</dependencies>
<build>
@@ -1,48 +1,38 @@
package org.wickra.examples;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.wickra.BinanceFeed;
import org.wickra.BinanceInterval;
import org.wickra.Candle;
import java.io.BufferedWriter;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
/**
* Download real BTCUSDT hourly klines from the Binance REST API into a CSV that the
* other examples can consume. Requires network access (build-only in CI).
* other examples can consume, using Wickra's native fetcher — no third-party HTTP or
* JSON library. Requires network access (build-only in CI).
*/
public final class FetchBtcusdt {
public static void main(String[] args) throws Exception {
String url = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=1h&limit=500";
System.out.println("Fetching " + url);
System.out.println("Fetching 500 BTCUSDT 1h klines from Binance...");
HttpClient http = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).GET().build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
Candle[] klines = BinanceFeed.fetchKlines("BTCUSDT", BinanceInterval.ONE_HOUR, 500, -1L, -1L, null);
JsonNode klines = new ObjectMapper().readTree(response.body());
Path dir = Path.of("data");
Files.createDirectories(dir);
Path path = dir.resolve("btcusdt_1h.csv");
int count = 0;
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write("timestamp,open,high,low,close,volume");
writer.newLine();
for (JsonNode kline : klines) {
// Binance kline array: [openTime, open, high, low, close, volume, ...]
writer.write(kline.get(0).asLong() + "," + kline.get(1).asText() + ","
+ kline.get(2).asText() + "," + kline.get(3).asText() + ","
+ kline.get(4).asText() + "," + kline.get(5).asText());
for (Candle k : klines) {
writer.write((long) k.timestamp() + "," + k.open() + "," + k.high() + ","
+ k.low() + "," + k.close() + "," + k.volume());
writer.newLine();
count++;
}
}
System.out.printf("Wrote %d klines to %s%n", count, path.toAbsolutePath());
System.out.printf("Wrote %d klines to %s%n", klines.length, path.toAbsolutePath());
}
}
@@ -1,73 +1,32 @@
package org.wickra.examples;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.wickra.BinanceFeed;
import org.wickra.BinanceInterval;
import org.wickra.Ema;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.wickra.KlineEvent;
/**
* Stream live BTCUSDT 1-minute klines from Binance and feed each close through EMA(20).
* Stream live BTCUSDT 1-minute klines from Binance and feed each close through EMA(20),
* using Wickra's native BinanceFeed — no third-party WebSocket or JSON library.
* Requires network access (build-only in CI). Runs for up to 60 seconds.
*/
public final class LiveBinance {
public static void main(String[] args) throws Exception {
URI uri = URI.create("wss://stream.binance.com:9443/ws/btcusdt@kline_1m");
System.out.println("Connecting to " + uri + " (up to 60s)...");
public static void main(String[] args) {
System.out.println("Streaming live BTCUSDT 1-minute klines from Binance (up to 60s)...");
ObjectMapper mapper = new ObjectMapper();
CountDownLatch done = new CountDownLatch(1);
try (Ema ema = new Ema(20)) {
WebSocket.Listener listener = new WebSocket.Listener() {
private final StringBuilder buffer = new StringBuilder();
@Override
public void onOpen(WebSocket webSocket) {
webSocket.request(1);
// Native feed: a blocking poll over the same tested stream as the Rust core.
// next(timeoutMillis) returns the event, or null on timeout (poll again).
try (BinanceFeed feed = new BinanceFeed("BTCUSDT", BinanceInterval.ONE_MINUTE);
Ema ema = new Ema(20)) {
long deadline = System.currentTimeMillis() + 60_000L;
while (System.currentTimeMillis() < deadline) {
KlineEvent event = feed.next(1000);
if (event == null) {
continue;
}
@Override
public java.util.concurrent.CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
buffer.append(data);
if (last) {
try {
JsonNode root = mapper.readTree(buffer.toString());
JsonNode k = root.get("k");
if (k != null) {
double close = Double.parseDouble(k.get("c").asText());
double value = ema.update(close);
System.out.printf("close=%.2f EMA(20)=%.2f%n", close, value);
}
} catch (Exception e) {
System.err.println("parse error: " + e.getMessage());
}
buffer.setLength(0);
}
webSocket.request(1);
return null;
}
@Override
public void onError(WebSocket webSocket, Throwable error) {
System.err.println("websocket error: " + error.getMessage());
done.countDown();
}
};
WebSocket ws = HttpClient.newHttpClient()
.newWebSocketBuilder()
.buildAsync(uri, listener)
.join();
if (!done.await(60, TimeUnit.SECONDS)) {
System.out.println("Done (time limit reached).");
System.out.printf("close=%.2f EMA(20)=%.2f%n", event.close(), ema.update(event.close()));
}
ws.sendClose(WebSocket.NORMAL_CLOSURE, "done");
}
System.out.println("Done (time limit reached).");
}
}
@@ -3,8 +3,9 @@ package org.wickra.examples;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.wickra.Candle;
import org.wickra.CandleReader;
/**
* Deterministic synthetic market data plus a small OHLCV CSV loader, shared by
@@ -51,41 +52,20 @@ public final class MarketData {
}
/**
* Loads an OHLCV CSV. Accepts rows of {@code timestamp,open,high,low,close,volume}
* or {@code open,high,low,close,volume}; a non-numeric first row is treated as a header.
* Loads a {@code timestamp,open,high,low,close,volume} OHLCV CSV with Wickra's
* native CandleReader (header validation, BOM and field-whitespace tolerance) —
* no manual CSV parsing.
*/
public static Bar[] loadOhlcvCsv(String path) throws IOException {
List<Bar> bars = new ArrayList<>();
for (String rawLine : Files.readAllLines(Path.of(path))) {
String line = rawLine.trim();
if (line.isEmpty()) {
continue;
String csv = Files.readString(Path.of(path));
try (CandleReader reader = new CandleReader(csv)) {
Candle[] candles = reader.read();
Bar[] bars = new Bar[candles.length];
for (int i = 0; i < candles.length; i++) {
Candle c = candles[i];
bars[i] = new Bar(c.open(), c.high(), c.low(), c.close(), c.volume(), (long) c.timestamp());
}
String[] cols = line.split(",");
if (!isNumeric(cols[0])) {
continue; // header row
}
if (cols.length >= 6) {
bars.add(new Bar(
Double.parseDouble(cols[1]), Double.parseDouble(cols[2]),
Double.parseDouble(cols[3]), Double.parseDouble(cols[4]),
Double.parseDouble(cols[5]), Long.parseLong(cols[0])));
} else {
bars.add(new Bar(
Double.parseDouble(cols[0]), Double.parseDouble(cols[1]),
Double.parseDouble(cols[2]), Double.parseDouble(cols[3]),
Double.parseDouble(cols[4]), bars.size()));
}
}
return bars.toArray(new Bar[0]);
}
private static boolean isNumeric(String s) {
try {
Double.parseDouble(s);
return true;
} catch (NumberFormatException e) {
return false;
return bars;
}
}
}
@@ -1,6 +1,8 @@
package org.wickra.examples;
import org.wickra.Candle;
import org.wickra.Ema;
import org.wickra.Resampler;
import org.wickra.examples.MarketData.Bar;
import java.util.ArrayList;
@@ -28,19 +30,25 @@ public final class MultiTimeframe {
if (factor <= 1) {
return source;
}
// Native Resampler: bucket by an absolute timeframe (the synthetic bars step
// 60_000 ms, so factor minutes == factor*60_000 ms). No hand-written bucketing.
List<Bar> output = new ArrayList<>();
for (int i = 0; i < source.length; i += factor) {
int end = Math.min(i + factor, source.length);
double high = Double.MIN_VALUE;
double low = Double.MAX_VALUE;
double volume = 0;
for (int j = i; j < end; j++) {
high = Math.max(high, source[j].high());
low = Math.min(low, source[j].low());
volume += source[j].volume();
try (Resampler r = new Resampler((long) factor * 60_000L)) {
for (Bar b : source) {
Candle c = r.update(b.open(), b.high(), b.low(), b.close(), b.volume(), b.timestamp());
if (c != null) {
output.add(toBar(c));
}
}
Candle last = r.flush();
if (last != null) {
output.add(toBar(last));
}
output.add(new Bar(source[i].open(), high, low, source[end - 1].close(), volume, source[i].timestamp()));
}
return output.toArray(new Bar[0]);
}
private static Bar toBar(Candle c) {
return new Bar(c.open(), c.high(), c.low(), c.close(), c.volume(), (long) c.timestamp());
}
}