← Back to Blog
TUTORIALS

Backtesting Your LMEX Trading Bot in Python: A Practical Guide

May 25, 2026 · 9 min read · LMEX.AI

Most backtest results are fiction. Sharpe 3.0 strategies that turn into Sharpe 0.3 in production. Backtests showing 200% annualised returns that produce 5% live. The gap between backtest and reality is usually called "overfitting" but it's actually a whole family of mistakes that compound on each other.


This article walks through how to build a Python backtest that produces results you can actually trust, the common mistakes that make backtests overly optimistic, and a complete working framework you can adapt.


What a backtest is supposed to do


A backtest simulates running your strategy on historical data to estimate how it would have performed. The output is an equity curve plus statistics — Sharpe ratio, max drawdown, win rate, etc.


Done right, a backtest gives you a reasonable estimate of forward performance. Done wrong, it gives you false confidence that destroys real capital.


The line between right and wrong is rarely about the algorithm. It's about subtle errors in how the simulation handles execution, data, and parameter selection.


The mistakes that ruin backtests


**Lookahead bias.** Your code accidentally uses information that wasn't available at the time of the trade. Example: calculating a 20-period moving average and using "current price" includes today's close, but today's close didn't exist when the trade decision was being made. Subtle versions of this are everywhere. They produce backtests that look incredible and fail completely live.


**Survivorship bias.** Testing on assets that exist today ignores assets that delisted. If you backtest a crypto trading strategy on the current top 20 coins, you've implicitly selected the survivors. The strategy works in backtest because those coins didn't fail; live, you might trade coins that will fail.


**Optimistic execution assumptions.** Backtests typically assume orders fill at the price you expected. Real execution involves slippage, missed fills, partial fills, and price movements between signal and execution. Backtest 1% slippage assumptions; realistic is 3-5x that for retail-sized orders, more for larger orders.


**Ignoring transaction costs.** Fees and spreads. A strategy with positive backtest returns before costs might be negative after costs, particularly for higher-frequency strategies.


**Parameter optimization on the full dataset.** Finding the parameters that "would have worked best" on historical data and reporting those results. This is data mining, not strategy validation. The parameters fitted to the past rarely work on the future.


A backtest framework that handles these


import pandas as pd
import numpy as np
import ccxt
from dataclasses import dataclass, field
from typing import Callable

@dataclass
class Trade:
    entry_time: pd.Timestamp
    exit_time: pd.Timestamp
    entry_price: float
    exit_price: float
    size: float
    side: str  # 'long' or 'short'
    pnl: float = 0
    commission: float = 0

@dataclass
class Backtest:
    commission_rate: float = 0.0006  # 6 bps per side (maker)
    slippage_bps: float = 5  # 5 bps slippage
    initial_capital: float = 10000
    
    def __post_init__(self):
        self.trades: list[Trade] = []
        self.equity_curve: list[float] = []
        self.capital = self.initial_capital
    
    def execute_trade(self, signal_time, signal_price, side, size, exit_time, exit_price):
        # Apply slippage to entry and exit
        slip_factor = 1 + (self.slippage_bps / 10000) if side == 'long' else 1 - (self.slippage_bps / 10000)
        actual_entry = signal_price * slip_factor
        exit_slip = 1 - (self.slippage_bps / 10000) if side == 'long' else 1 + (self.slippage_bps / 10000)
        actual_exit = exit_price * exit_slip
        
        # Calculate P&L
        if side == 'long':
            gross_pnl = (actual_exit - actual_entry) * size
        else:
            gross_pnl = (actual_entry - actual_exit) * size
        
        # Apply commissions on both sides
        commission = (actual_entry + actual_exit) * size * self.commission_rate
        net_pnl = gross_pnl - commission
        
        self.capital += net_pnl
        self.trades.append(Trade(
            entry_time=signal_time,
            exit_time=exit_time,
            entry_price=actual_entry,
            exit_price=actual_exit,
            size=size,
            side=side,
            pnl=net_pnl,
            commission=commission
        ))
    
    def metrics(self):
        if not self.trades:
            return {}
        
        returns = [t.pnl / self.initial_capital for t in self.trades]
        equity = self.initial_capital + np.cumsum([t.pnl for t in self.trades])
        peak = np.maximum.accumulate(equity)
        drawdown = (equity - peak) / peak
        
        return {
            'total_return': (self.capital - self.initial_capital) / self.initial_capital,
            'num_trades': len(self.trades),
            'win_rate': sum(1 for t in self.trades if t.pnl > 0) / len(self.trades),
            'avg_win': np.mean([t.pnl for t in self.trades if t.pnl > 0]) if any(t.pnl > 0 for t in self.trades) else 0,
            'avg_loss': np.mean([t.pnl for t in self.trades if t.pnl < 0]) if any(t.pnl < 0 for t in self.trades) else 0,
            'max_drawdown': drawdown.min(),
            'sharpe': np.mean(returns) / np.std(returns) * np.sqrt(252) if np.std(returns) > 0 else 0,
        }

This framework includes:

  • Realistic commission (0.06% per side, matches LMEX maker rate)
  • Slippage (5 basis points per side, conservative for retail size)
  • Per-trade tracking for accurate analytics
  • Correct drawdown calculation (from peak, not from start)

  • You add your strategy on top by generating entry/exit signals and calling `execute_trade` for each.


    Out-of-sample testing


    The single most important practice: split your data and never optimize on the test set.


    A typical workflow:

    1. Take 3 years of data

    2. Use first 2 years for parameter optimization (in-sample)

    3. Test the final parameters on year 3 (out-of-sample)

    4. If out-of-sample Sharpe is dramatically worse than in-sample, you overfitted


    If you can't accept the out-of-sample result, the strategy doesn't work. Don't go back and tweak parameters until out-of-sample looks better — that just adds another round of overfitting.


    For the gold-standard test, do walk-forward analysis (see our walk-forward optimization article). It's more work but produces backtests you can actually trust.


    Sanity checks that catch common mistakes


    Before trusting any backtest result, verify:


    **Reasonable trade frequency.** If your strategy claims to make 500 trades per month on hourly data, that's roughly 1 trade every 1.4 hours — which exceeds the timeframe. You probably have lookahead or your stop logic is firing on the same candle.


    **Reasonable win rate.** Strategies claiming 80%+ win rates with positive risk-reward are almost always overfit. Real strategies tend to have win rates between 35% and 65% with average win > average loss.


    **Reasonable drawdown.** A backtest showing 50%+ return with 5% max drawdown is suspicious. Real strategies have drawdowns roughly proportional to their volatility. Sharpe over 3 with low drawdown is almost certainly fitted.


    **Performance consistent across regimes.** If your strategy made all its money in 2021 and broke even from 2022-2024, it might work in some specific regime that may not recur. Look for strategies that work across regimes.


    What still won't match live performance


    Even with perfect backtesting practice, your live results will likely be worse than backtest. Some reasons:


  • Capacity: strategies that work in backtest may move the market when actually traded
  • Conditions: live markets have flash crashes and unusual events that historical data doesn't fully capture
  • Execution: real fills can be delayed, partially filled, or rejected
  • Discipline: humans deviate from strategies during drawdowns, hurting realised returns

  • Plan for live Sharpe to be 30-50% of backtest Sharpe. Anything closer is a happy surprise.


    Frequently Asked Questions


    Q: What data should I use for backtesting?

    For crypto, 1-2 years minimum, 3-5 years preferred. Include data from both bull and bear markets. CCXT can fetch historical OHLCV data from LMEX going back several years.


    Q: How much data is enough?

    At least 100 trades for any meaningful statistics. More is better. With fewer than 50 trades, results are mostly noise regardless of how good they look.


    Q: Should I backtest with different commission rates?

    Yes — test sensitivity. If your strategy is profitable at 5 bps commission but loses at 10 bps, it's not robust. Real fees vary with volume tier; assume the worst case until you know yours.


    Q: Can I trust open-source backtesting libraries?

    They're convenient but have varied quality. Backtrader, vectorbt, and Zipline are the most popular Python options. Each has quirks and assumptions. Read the docs carefully and verify they handle slippage and lookahead correctly.


    Related Articles


    → Walk-Forward Optimization: The Only Backtest Method That Survives Reality
    → Why Most Trading Bots Fail (And What the Survivors Get Right)
    → Building a Crypto Perpetuals Trading Bot in Python: Complete Guide
    ← All ArticlesBuild a Bot →