How to Backtest Your Trading Strategy Before Going Live: A Developer's Guide

javascript dev.to

How to Backtest Your Trading Strategy Before Going Live: A Developer's Guide

(And why I lost $2,400 before learning this the hard way)


Every trading bot developer faces this moment: you've built your strategy, the backtests look gorgeous, and you're ready to print money. Then live trading starts — and your account bleeds.

I've been there. In January, my grid trading bot on Binance lost $2,400 in three days. The backtest showed 340% APY. Something was very wrong.

The culprit? I trusted backtests without questioning the data, the execution, or the market conditions. Here's how to avoid my mistakes.

The Problem with Backtests

A backtest is a simulation of your strategy on historical data. It tells you what would have happened if you'd run the bot yesterday. The issue is: past performance ≠ future results.

Here's why:

  1. Slippage: In backtests, you buy at the exact price you see. In live markets, you get filled at the next price — or worse.
  2. Liquidity gaps: Historical data might show a price, but if there's no liquidity at that price, your order doesn't execute.
  3. Market regime changes: Strategies that work in bull markets often crash in bear markets.
  4. Overfitting: If your strategy is too optimized for historical data, it won't generalize.

The Solution: Rigorous Backtesting

Here's my backtesting workflow (tested on 6 strategies, $18K deployed capital):

Step 1: Clean Your Data

Don't trust exchange APIs for historical candles. Download and validate:

const axios = require('axios');

async function fetchHistoricalCandles(symbol, interval, startTime, endTime) {
  const candles = [];
  let currentTime = startTime;

  while (currentTime < endTime) {
    const response = await axios.get(
      `https://api.binance.com/api/v3/klines?symbol=${symbol}&interval=${interval}&startTime=${currentTime}&limit=1000`
    );

    if (response.data.length === 0) break;

    candles.push(...response.data.map(c => ({
      time: c[0],
      open: parseFloat(c[1]),
      high: parseFloat(c[2]),
      low: parseFloat(c[3]),
      close: parseFloat(c[4]),
      volume: parseFloat(c[5])
    })));

    currentTime = candles[candles.length - 1].time + 1;
  }

  return candles;
}
Enter fullscreen mode Exit fullscreen mode

Validate: Check for gaps, outliers (prices 10x from median), and timestamp overlaps.

Step 2: Simulate Execution Precisely

Don't just calculate signals. Simulate fills:

class BacktestEngine {
  constructor(candles, initialBalance) {
    this.candles = candles;
    this.balance = initialBalance;
    this.position = 0;
    this.trades = [];
    this.fees = 0.001; // 0.1% Binance fee
  }

  executeBuy(price, quantity) {
    const cost = price * quantity * (1 + this.fees);
    if (cost > this.balance) {
      quantity = this.balance / price / (1 + this.fees); // Adjust for available balance
    }
    this.balance -= price * quantity * (1 + this.fees);
    this.position += quantity;
    this.trades.push({ type: 'buy', price, quantity, fee: price * quantity * this.fees });
    return quantity;
  }

  executeSell(price, quantity) {
    const revenue = price * quantity * (1 - this.fees);
    this.balance += revenue;
    this.position -= quantity;
    this.trades.push({ type: 'sell', price, quantity, fee: price * quantity * this.fees });
    return revenue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Key detail: Use limit orders, not market orders. In backtests, assume you get filled at the next candle's open (conservative) or mid-price (aggressive).

Step 3: Walk-Forward Analysis

Don't optimize on all data. Split:

  • In-sample (70%): Optimize parameters
  • Out-of-sample (30%): Validate
const splitIndex = Math.floor(candles.length * 0.7);
const inSample = candles.slice(0, splitIndex);
const outOfSample = candles.slice(splitIndex);

// Optimize on in-sample
const optimalParams = optimize(strategy, inSample);

// Validate on out-of-sample
const validationResult = runBacktest(strategy, optimalParams, outOfSample);

console.log(`In-sample: ${validationResult.inSampleReturn}%`);
console.log(`Out-of-sample: ${validationResult.outOfSampleReturn}%`);
console.log(`Drawdown: ${validationResult.maxDrawdown}%`);
Enter fullscreen mode Exit fullscreen mode

If out-of-sample returns are negative or drawdown exceeds 30%, reject the strategy.

Step 4: Monte Carlo Simulation

Run your strategy 100+ times with random data subsets:

function monteCarlo(strategy, candles, runs = 100) {
  const results = [];

  for (let i = 0; i < runs; i++) {
    const sampleSize = Math.floor(candles.length * 0.8);
    const startIdx = Math.floor(Math.random() * (candles.length - sampleSize));
    const sample = candles.slice(startIdx, startIdx + sampleSize);

    const result = runBacktest(strategy, sample);
    results.push(result);
  }

  return {
    meanReturn: results.reduce((a, b) => a + b.return, 0) / results.length,
    stdDev: Math.sqrt(results.reduce((a, b) => a + Math.pow(b.return - meanReturn, 2), 0) / results.length),
    percentile5: results.sort((a, b) => a.return - b.return)[Math.floor(runs * 0.05)],
    percentile95: results.sort((a, b) => a.return - b.return)[Math.floor(runs * 0.95)]
  };
}
Enter fullscreen mode Exit fullscreen mode

Accept only strategies with positive returns at the 5th percentile.

Step 5: Paper Trading

Before live trading, run your bot on paper trading (Binance Testnet or simulated):

// Paper trading mode
const config = {
  mode: 'paper',
  paperBalance: 10000, // Simulated $10K
  slippage: 0.0005 // 0.05% slippage buffer
};

// Test for at least 2 weeks
// Monitor: execution time, fill prices vs signals, drawdown
// Only go live if: drawdown < 15%, execution lag < 500ms
Enter fullscreen mode Exit fullscreen mode

Results

After implementing this workflow, I backtested my grid strategy properly:

Metric Before (Naive) After (Rigorous)
In-sample return 340% 127%
Out-of-sample return -48% 8%
Max drawdown 72% 14%
Win rate 89% 67%

The "340% APY" was overfitted garbage. The real strategy returns 8% in unseen data with 14% max drawdown — and it survived live trading.

Conclusion

Backtests are hypotheses, not guarantees. Before going live:

  1. Clean your data — validate for gaps and outliers
  2. Simulate execution precisely — use limit orders, not market orders
  3. Walk-forward validate — test on out-of-sample data
  4. Run Monte Carlo — check 5th percentile returns
  5. Paper trade first — test in live market conditions without real money

Your backtest should underpromise and overdeliver. If it looks too good to be true, it is.


I run Lucromatic — a self-hosted crypto trading bot for Binance. Get started for free: try.lucromatic.com

Source: dev.to

arrow_back Back to Tutorials