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:
- Slippage: In backtests, you buy at the exact price you see. In live markets, you get filled at the next price — or worse.
- Liquidity gaps: Historical data might show a price, but if there's no liquidity at that price, your order doesn't execute.
- Market regime changes: Strategies that work in bull markets often crash in bear markets.
- 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;
}
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;
}
}
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}%`);
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)]
};
}
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
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:
- Clean your data — validate for gaps and outliers
- Simulate execution precisely — use limit orders, not market orders
- Walk-forward validate — test on out-of-sample data
- Run Monte Carlo — check 5th percentile returns
- 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