Files
kingchenc 677ea37402 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.
2026-06-17 01:49:11 +02:00

156 lines
4.7 KiB
JavaScript

// Offline backtest example for the Wickra Node binding.
//
// Reads an OHLCV CSV, streams every candle through a basket of indicators
// (SMA, EMA, RSI, MACD, Bollinger Bands, ATR, ADX, OBV) with the O(1)
// `update` call, and prints a summary of each resulting series. It is the
// Node counterpart of `examples/python/backtest.py` and the Rust
// `examples/rust/src/bin/backtest.rs`.
//
// Run it from the repository after building the native binding:
//
// cd bindings/node && npm install && npx napi build --platform --release
// cd ../../examples/node && npm install
// node backtest.js # uses the bundled BTCUSDT 1d data
// node backtest.js path/to/ohlcv.csv # or any OHLCV CSV
const fs = require('node:fs');
const path = require('node:path');
const wickra = require('wickra');
// Default dataset: the checked-in BTCUSDT daily candles under the workspace
// `examples/data/` directory, resolved relative to this file.
const DEFAULT_CSV = path.join(__dirname, '..', 'data', 'btcusdt-1d.csv');
// Parse an OHLCV CSV into column arrays of numbers.
//
// The Wickra CSV layout is plain numeric — no quoted fields, no embedded
// commas — so splitting on `,` is a complete and correct parse for it.
function readHistory(csvPath) {
// Native CandleReader: validates the header (timestamp,open,high,low,close,
// volume), tolerates a UTF-8 BOM and field whitespace, and throws on a
// malformed row. No manual CSV parsing.
const text = fs.readFileSync(csvPath, 'utf8');
const candles = new wickra.CandleReader(text).read();
if (candles.length === 0) {
throw new Error(`${csvPath}: CSV has a header but no data rows`);
}
const cols = { timestamp: [], open: [], high: [], low: [], close: [], volume: [] };
for (const k of candles) {
cols.timestamp.push(k.timestamp);
cols.open.push(k.open);
cols.high.push(k.high);
cols.low.push(k.low);
cols.close.push(k.close);
cols.volume.push(k.volume);
}
return cols;
}
// Running min / max / mean / last of one indicator output series. Null and
// non-finite values (the warmup phase) are skipped.
class Series {
constructor(name) {
this.name = name;
this.count = 0;
this.sum = 0;
this.min = Infinity;
this.max = -Infinity;
this.last = NaN;
}
add(value) {
if (value === null || value === undefined || !Number.isFinite(value)) {
return;
}
this.count += 1;
this.sum += value;
if (value < this.min) this.min = value;
if (value > this.max) this.max = value;
this.last = value;
}
print() {
if (this.count === 0) {
console.log(` ${this.name.padEnd(12)} (no valid samples — series too short)`);
return;
}
const cell = (v) => v.toFixed(4).padStart(14);
console.log(
` ${this.name.padEnd(12)} mean=${cell(this.sum / this.count)} ` +
`min=${cell(this.min)} max=${cell(this.max)} last=${cell(this.last)}`,
);
}
}
function main() {
const csvPath = process.argv[2] || DEFAULT_CSV;
let history;
try {
history = readHistory(csvPath);
} catch (err) {
console.error(`error: ${err.message}`);
process.exit(1);
}
const bars = history.close.length;
const sma = new wickra.SMA(20);
const ema = new wickra.EMA(20);
const rsi = new wickra.RSI(14);
const macd = new wickra.MACD(12, 26, 9);
const bb = new wickra.BollingerBands(20, 2.0);
const atr = new wickra.ATR(14);
const adx = new wickra.ADX(14);
const obv = new wickra.OBV();
const series = {
sma: new Series('SMA(20)'),
ema: new Series('EMA(20)'),
rsi: new Series('RSI(14)'),
macdLine: new Series('MACD line'),
macdHist: new Series('MACD hist'),
bbUpper: new Series('BB upper'),
bbLower: new Series('BB lower'),
atr: new Series('ATR(14)'),
adx: new Series('ADX(14)'),
obv: new Series('OBV'),
};
for (let i = 0; i < bars; i++) {
const close = history.close[i];
series.sma.add(sma.update(close));
series.ema.add(ema.update(close));
series.rsi.add(rsi.update(close));
const m = macd.update(close); // { macd, signal, histogram } or null
if (m) {
series.macdLine.add(m.macd);
series.macdHist.add(m.histogram);
}
const b = bb.update(close); // { upper, middle, lower, stddev } or null
if (b) {
series.bbUpper.add(b.upper);
series.bbLower.add(b.lower);
}
series.atr.add(atr.update(history.high[i], history.low[i], close));
const a = adx.update(history.high[i], history.low[i], close); // { plusDi, minusDi, adx } or null
if (a) {
series.adx.add(a.adx);
}
series.obv.add(obv.update(close, history.volume[i]));
}
console.log(`Backtest summary for ${csvPath} (${bars} bars)`);
for (const s of Object.values(series)) {
s.print();
}
}
main();