8b9e8e30b9
Continue the per-language `examples/<lang>/` restructure: move the three
Node example files (streaming.js, backtest.js, live_trading.js) out of
bindings/node/examples/ and into a top-level examples/node/ directory.
* `examples/node/package.json` is a `private` package that pulls the
native binding via `file:../../bindings/node` and lists `ws` as a
dev-dependency for the live-trading example. `require('..')` in each
file becomes `require('wickra')` — exactly what a downstream user would
write — and the file-header run instructions are updated to the new
two-step workflow (`npm install` in bindings/node, then in
examples/node).
* `backtest.js`'s default-CSV path becomes the much shorter
`__dirname/../data/btcusdt-1d.csv` from the new location.
* `bindings/node/package.json` drops the now-unused `ws` devDependency.
* `.gitignore` is broadened from `bindings/node/node_modules/` to
`**/node_modules/` so the new `examples/node/node_modules/` directory is
not tracked.
* The README "Languages" table, project-layout block and
`examples/README.md` Node section are updated for the new paths and run
commands.
Verified by running `node backtest.js` (3200 BTCUSDT daily bars, matching
output), `node streaming.js`, and `node --check live_trading.js` from the
new location.
160 lines
5.0 KiB
JavaScript
160 lines
5.0 KiB
JavaScript
// Live trading skeleton for the Wickra Node binding.
|
|
//
|
|
// Connects to Binance's public WebSocket feed (no API key needed) and runs
|
|
// RSI / MACD / Bollinger Bands on the incoming close prices. When RSI crosses
|
|
// the common overbought / oversold thresholds *and* the MACD histogram
|
|
// confirms the direction *and* price pierces the matching Bollinger band, a
|
|
// signal line is printed. No orders are placed. It is the Node counterpart of
|
|
// examples/python/live_trading.py.
|
|
//
|
|
// 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 # pulls `ws` for this example
|
|
// node live_trading.js --symbol BTCUSDT --interval 1m
|
|
//
|
|
// Stop it with Ctrl+C.
|
|
|
|
const wickra = require('wickra');
|
|
|
|
let WebSocket;
|
|
try {
|
|
WebSocket = require('ws');
|
|
} catch (err) {
|
|
console.error('This example needs the `ws` package — run `npm install` in examples/node.');
|
|
process.exit(1);
|
|
}
|
|
|
|
const BINANCE_WS = 'wss://stream.binance.com:9443/stream';
|
|
|
|
// Kline intervals the public Binance WebSocket API recognises.
|
|
const VALID_INTERVALS = new Set([
|
|
'1s', '1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h',
|
|
'1d', '3d', '1w', '1M',
|
|
]);
|
|
|
|
// A Binance symbol is strictly alphanumeric (e.g. BTCUSDT).
|
|
const SYMBOL_RE = /^[A-Za-z0-9]+$/;
|
|
|
|
// Parse `--symbol` / `--interval` flags, defaulting to BTCUSDT 1m.
|
|
function parseArgs(argv) {
|
|
const args = { symbol: 'BTCUSDT', interval: '1m' };
|
|
for (let i = 0; i < argv.length; i++) {
|
|
if (argv[i] === '--symbol') {
|
|
args.symbol = argv[i + 1];
|
|
i += 1;
|
|
} else if (argv[i] === '--interval') {
|
|
args.interval = argv[i + 1];
|
|
i += 1;
|
|
} else {
|
|
throw new Error(`unexpected argument: ${argv[i]}`);
|
|
}
|
|
}
|
|
return args;
|
|
}
|
|
|
|
// Reject a symbol or interval that is not safe to splice into the WS URL.
|
|
// Both are interpolated straight into the stream name, so validating up front
|
|
// keeps the URL well-formed without needing to escape it.
|
|
function validateArgs(symbol, interval) {
|
|
if (typeof symbol !== 'string' || !SYMBOL_RE.test(symbol)) {
|
|
throw new Error(
|
|
`invalid --symbol ${JSON.stringify(symbol)}: expected only letters and digits, e.g. BTCUSDT`,
|
|
);
|
|
}
|
|
if (!VALID_INTERVALS.has(interval)) {
|
|
throw new Error(
|
|
`invalid --interval ${JSON.stringify(interval)}: expected one of ` +
|
|
[...VALID_INTERVALS].join(', '),
|
|
);
|
|
}
|
|
}
|
|
|
|
const fmt = (v) => (v === null || v === undefined || Number.isNaN(v) ? '--' : v.toFixed(2));
|
|
|
|
function main() {
|
|
let args;
|
|
try {
|
|
args = parseArgs(process.argv.slice(2));
|
|
validateArgs(args.symbol, args.interval);
|
|
} catch (err) {
|
|
console.error(`error: ${err.message}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
// One streaming instance of each indicator — the same O(1) update model a
|
|
// real bot would use.
|
|
const rsi = new wickra.RSI(14);
|
|
const macd = new wickra.MACD(12, 26, 9);
|
|
const bb = new wickra.BollingerBands(20, 2.0);
|
|
|
|
const stream = `${args.symbol.toLowerCase()}@kline_${args.interval}`;
|
|
const url = `${BINANCE_WS}?streams=${stream}`;
|
|
console.log(`Connecting to ${url}`);
|
|
|
|
const ws = new WebSocket(url);
|
|
|
|
ws.on('open', () => {
|
|
console.log(`Connected, listening for ${stream} klines (Ctrl+C to stop)`);
|
|
});
|
|
|
|
ws.on('message', (raw) => {
|
|
let envelope;
|
|
try {
|
|
envelope = JSON.parse(raw.toString());
|
|
} catch (err) {
|
|
return; // ignore non-JSON frames
|
|
}
|
|
const k = (envelope.data && envelope.data.k) || null;
|
|
if (!k || k.c === undefined) {
|
|
// Subscription acks, heartbeats and error frames carry no kline payload —
|
|
// skip them instead of crashing on Number(undefined).
|
|
return;
|
|
}
|
|
const close = Number(k.c);
|
|
const isClosed = Boolean(k.x);
|
|
|
|
const rsiV = rsi.update(close);
|
|
const macdV = macd.update(close); // { macd, signal, histogram } or null
|
|
const bbV = bb.update(close); // { upper, middle, lower, stddev } or null
|
|
const hist = macdV ? macdV.histogram : null;
|
|
|
|
console.log(
|
|
`${isClosed ? 'BAR ' : 'tick'} close=${fmt(close)} rsi=${fmt(rsiV)} ` +
|
|
`hist=${fmt(hist)} ` +
|
|
`bb=${bbV ? `${fmt(bbV.lower)}/${fmt(bbV.middle)}/${fmt(bbV.upper)}` : '--'}`,
|
|
);
|
|
|
|
// Only act once every indicator has warmed up.
|
|
if (rsiV !== null && macdV !== null && bbV !== null) {
|
|
if (rsiV > 70 && hist < 0 && close >= bbV.upper) {
|
|
console.log(
|
|
` SELL candidate: rsi=${fmt(rsiV)} hist=${fmt(hist)} close >= bb_upper=${fmt(bbV.upper)}`,
|
|
);
|
|
} else if (rsiV < 30 && hist > 0 && close <= bbV.lower) {
|
|
console.log(
|
|
` BUY candidate: rsi=${fmt(rsiV)} hist=${fmt(hist)} close <= bb_lower=${fmt(bbV.lower)}`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
ws.on('error', (err) => {
|
|
console.error(`websocket error: ${err.message}`);
|
|
process.exitCode = 1;
|
|
});
|
|
|
|
ws.on('close', () => {
|
|
console.log('Connection closed.');
|
|
});
|
|
|
|
// Translate Ctrl+C into a clean shutdown.
|
|
process.on('SIGINT', () => {
|
|
console.log('\nShutting down…');
|
|
ws.close();
|
|
process.exit(0);
|
|
});
|
|
}
|
|
|
|
main();
|