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