// 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();
