Skip to content

Node.js — backtest & live

The same strategy object runs unchanged in backtest, synchronous live, and threaded live modes.

node docs/examples/node_backtest_vs_live.js
/**
 * Backtest and live with the same strategy class — Node.js.
 *
 * The same strategy object works in three modes:
 *   - BacktestRunner:       replay CSV, SimulatedExecutor fills orders
 *   - Runner:               synchronous live, push ticks from your connector
 *   - Runner(reg, cb, true): Disruptor-based, lock-free publish
 *
 * Usage:
 *   node examples/node_backtest_vs_live.js
 */

const path = require('path');
const flox = require('../../node');

const DATA = path.join(__dirname, 'data', 'btcusdt_1m.csv');

// ── Symbol registry (shared across all modes) ──────────────────────────

const registry = new flox.SymbolRegistry();
const btc = registry.addSymbol('binance', 'BTCUSDT', 0.01);

// ── Strategy factory — plain JS object with callbacks ─────────────────
//
// A new instance is created for each mode so state is fresh.
// In backtest mode the emit object is wired to SimulatedExecutor.
// In live modes it routes orders to the exchange.

function makeSMAStrategy() {
  const fast = new flox.SMA(10);
  const slow  = new flox.SMA(30);
  let tradeCount = 0;

  return {
    symbols: [btc],

    onStart() {
      tradeCount = 0;
    },

    onStop() {
      console.log(`  SMAStrategy stopped  (${tradeCount} signals emitted)`);
    },

    onTrade(ctx, trade, emit) {
      const fv = fast.update(trade.price);
      const sv = slow.update(trade.price);
      if (!slow.ready) return;

      if (fv > sv && ctx.position === 0) {
        emit.marketBuy(0.01);
        tradeCount++;
      } else if (fv < sv && ctx.position === 0) {
        emit.marketSell(0.01);
        tradeCount++;
      }
    },
  };
}

// ── 1. BacktestRunner — replay historical CSV ─────────────────────────

console.log('── Backtest ──────────────────────────────────────────────────────');

const bt = new flox.BacktestRunner(registry, 0.0004, 10_000);
bt.setStrategy(makeSMAStrategy());
const stats = bt.runCsv(DATA, 'BTCUSDT');

if (stats) {
  console.log(`  Return   : ${stats.returnPct >= 0 ? '+' : ''}${stats.returnPct.toFixed(4)}%`);
  console.log(`  Trades   : ${stats.totalTrades}  win=${(stats.winRate * 100).toFixed(1)}%`);
  console.log(`  Sharpe   : ${stats.sharpeRatio.toFixed(4)}`);
  console.log(`  Max DD   : ${stats.maxDrawdownPct.toFixed(4)}%`);
  console.log(`  Net PnL  : ${stats.netPnl.toFixed(4)}`);
} else {
  console.log('  backtest failed (CSV not found?)');
}

// ── 2. Runner — synchronous live ──────────────────────────────────────

console.log('\n── Runner (live, sync) ───────────────────────────────────────────');

const liveSigs = [];
const runner = new flox.Runner(registry, sig => liveSigs.push(sig));
runner.addStrategy(makeSMAStrategy());
runner.start();

const prices = Array.from({ length: 40 }, (_, i) => 50000 + i * 50);
let tsNs = BigInt(Date.now()) * 1_000_000n;
for (let i = 0; i < prices.length; i++) {
  runner.onTrade(btc, prices[i], 0.1, i % 2 === 0, Number(tsNs));
  tsNs += 1_000_000_000n;
}
runner.stop();

console.log(`  Signals received: ${liveSigs.length}`);
for (const s of liveSigs.slice(0, 3)) {
  console.log(`    ${s.side.padEnd(4)}  ${s.quantity.toFixed(4)} @ ${s.price.toFixed(2)}  [${s.orderType}]`);
}

// ── 3. Runner(threaded=true) — Disruptor, lock-free publish ───────────

console.log('\n── Runner (threaded=true) ────────────────────────────────────────');

const engineSigs = [];
const threaded = new flox.Runner(registry, sig => engineSigs.push(sig), true);
threaded.addStrategy(makeSMAStrategy());
threaded.start();

tsNs = BigInt(Date.now()) * 1_000_000n;
for (let i = 0; i < prices.length; i++) {
  threaded.onTrade(btc, prices[i], 0.1, i % 2 === 0, Number(tsNs));
  tsNs += 1_000_000_000n;
}

setTimeout(() => {
  threaded.stop();
  console.log(`  Signals received: ${engineSigs.length}`);
  process.exit(0);
}, 50);