Algorithmic & Financial Disclaimer: This article documents quantitative software architecture, backtesting methodology, and mathematical modeling. It is published strictly for computer science and financial engineering educational purposes and does not constitute financial, investment, or trading advice.

The Trap of Naive Pandas Backtests

The most common introductory quantitative script involves loading daily closing prices into pandas, computing a 20-period moving average, and executing:

df['signal'] = np.where(df['close'] > df['sma_20'], 1, 0)
df['strategy_returns'] = df['signal'] * df['returns']

This code produces glowing Sharpe ratios on historical data. When deployed to a live exchange, however, it loses capital almost immediately. Why?

  1. Look-Ahead Bias (Future Leakage): The moving average and signal at index t are calculated using the close price of candle t. In reality, you cannot execute at the close of candle t because the close is not known until the candle has ended. You must execute at the open of candle t+1.
  2. Execution Friction Neglect: Real-world execution suffers from bid-ask spread crossing, broker commissions, exchange fees, and market impact slippage. In high-frequency or short-timeframe strategies, friction wipes out theoretical alpha entirely.

Vectorized vs. Event-Driven Architecture

When building backtesters, quants must choose between two distinct engineering paradigms:

Attribute Vectorized Backtester Event-Driven Backtester
Execution Speed Ultra-Fast (Millions of bars in milliseconds via NumPy/Polars) Slower (Object-oriented event loop tick-by-tick)
Order Types Primarily Market on Open / Close Complex Limit Orders, Stop Orders, Icebergs, Partial Fills
Ideal Use Case Rapid hypothesis screening & parameter sweeps Final production pre-flight testing matching broker API

Mathematical Modeling of Slippage and Commission

In our custom engine, we model transaction friction deterministically:

import numpy as np

def run_vectorized_backtest(
    prices: np.ndarray,
    signals: np.ndarray,
    commission_bps: float = 5.0,  # 5 basis points (0.05%)
    slippage_bps: float = 2.0      # 2 basis points
) -> dict:
    # Vectorized simulation doc
    Vectorized simulation incorporating lagged execution and transaction costs.
    Signals: 1 (Long), 0 (Flat), -1 (Short)
    # Signals: 1 (Long), 0 (Flat), -1 (Short)
    n_bars = len(prices)
    # 1. Enforce 1-bar execution lag to eliminate look-ahead bias
    executed_positions = np.zeros(n_bars)
    executed_positions[1:] = signals[:-1]

    # 2. Calculate raw price returns
    price_returns = np.zeros(n_bars)
    price_returns[1:] = (prices[1:] / prices[:-1]) - 1.0

    # 3. Strategy raw returns
    strategy_returns = executed_positions * price_returns

    # 4. Detect position transitions (trades executed)
    trades = np.zeros(n_bars)
    trades[1:] = np.abs(executed_positions[1:] - executed_positions[:-1])

    # 5. Apply total friction costs (Commission + Slippage)
    total_friction_rate = (commission_bps + slippage_bps) / 10000.0
    friction_penalties = trades * total_friction_rate

    # 6. Net strategy returns
    net_returns = strategy_returns - friction_penalties
    cumulative_curve = np.cumprod(1.0 + net_returns)

    # 7. Compute Peak-to-Trough Drawdown
    running_max = np.maximum.accumulate(cumulative_curve)
    drawdowns = (cumulative_curve - running_max) / running_max
    max_drawdown = np.min(drawdowns)

    # 8. Annualized Sharpe Ratio (Assuming 252 daily bars)
    mean_ret = np.mean(net_returns)
    std_ret = np.std(net_returns) + 1e-9
    sharpe = (mean_ret / std_ret) * np.sqrt(252)

    return {
        "total_return": float(cumulative_curve[-1] - 1.0),
        "max_drawdown": float(max_drawdown),
        "sharpe_ratio": float(sharpe),
        "trades_count": int(np.sum(trades > 0))
    }