Claude Code for Trading System (2026)

Why Claude Code for Trading System Backtesting

Strategy backtesting frameworks require event-driven architecture that mirrors live trading: market data events trigger signal generation, signals pass through risk checks, risk-approved orders hit a fill simulator that models slippage and partial fills, and fills update portfolio state. Vectorized backtests (apply signals to return series) miss the execution reality that kills strategies in production – queue priority, market impact, latency, and the feedback loop between your orders and the market.

Claude Code generates event-driven backtesting engines with realistic fill models, proper commission handling (maker/taker, exchange fees, clearing), and the risk controls (position limits, drawdown stops, correlation breaks) that production systems require. It produces code that transitions cleanly from backtest to paper trading to live execution.

The Workflow

Step 1: Framework Setup

pip install numpy pandas
pip install sortedcontainers  # for order book simulation
pip install pyarrow           # for fast data serialization
mkdir -p src/engine src/strategies src/fill_models src/risk data/

Step 2: Build Event-Driven Backtesting Engine

# src/engine/backtest_engine.py
"""Event-driven backtesting engine with fill simulation and risk controls."""
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Optional
class Side(Enum):
    BUY = 1
    SELL = -1
class OrderType(Enum):
    MARKET = 'market'
    LIMIT = 'limit'
class OrderStatus(Enum):
    PENDING = 'pending'
    FILLED = 'filled'
    PARTIALLY_FILLED = 'partial'
    CANCELLED = 'cancelled'
    REJECTED = 'rejected'
@dataclass
class Order:
    order_id: int
    symbol: str
    side: Side
    quantity: int
    order_type: OrderType
    limit_price: Optional[float] = None
    timestamp: Optional[pd.Timestamp] = None
    status: OrderStatus = OrderStatus.PENDING
    filled_qty: int = 0
    avg_fill_price: float = 0.0
@dataclass
class Fill:
    order_id: int
    symbol: str
    side: Side
    quantity: int
    price: float
    commission: float
    timestamp: pd.Timestamp
@dataclass
class Position:
    symbol: str
    quantity: int = 0
    avg_cost: float = 0.0
    realized_pnl: float = 0.0
    @property
    def notional(self) -> float:
        return abs(self.quantity) * self.avg_cost
    def update(self, fill: Fill) -> None:
        """Update position with a new fill."""
        fill_sign = 1 if fill.side == Side.BUY else -1
        fill_qty = fill_sign * fill.quantity
        if self.quantity == 0:
            self.avg_cost = fill.price
            self.quantity = fill_qty
        elif np.sign(self.quantity) == np.sign(fill_qty):
            # Adding to position: update average cost
            total_cost = self.avg_cost * abs(self.quantity) + \
                         fill.price * fill.quantity
            self.quantity += fill_qty
            self.avg_cost = total_cost / abs(self.quantity)
        else:
            # Reducing or flipping position: realize P&L
            close_qty = min(abs(self.quantity), fill.quantity)
            if self.quantity > 0:
                self.realized_pnl += close_qty * (fill.price - self.avg_cost)
            else:
                self.realized_pnl += close_qty * (self.avg_cost - fill.price)
            self.quantity += fill_qty
            if abs(fill_qty) > abs(self.quantity - fill_qty):
                self.avg_cost = fill.price
class FillSimulator:
    """Simulate order fills with slippage and partial fills."""
    def __init__(self, slippage_bps: float = 5.0,
                 fill_rate: float = 1.0,
                 commission_per_share: float = 0.005):
        self.slippage_bps = slippage_bps
        self.fill_rate = fill_rate
        self.commission_per_share = commission_per_share
    def simulate_fill(self, order: Order, bar: pd.Series) -> Optional[Fill]:
        """Simulate fill against OHLCV bar data."""
        if order.order_type == OrderType.MARKET:
            # Market order: fill at open + slippage
            base_price = bar['open']
            slippage = base_price * self.slippage_bps / 10000
            if order.side == Side.BUY:
                fill_price = base_price + slippage
            else:
                fill_price = base_price - slippage
        elif order.order_type == OrderType.LIMIT:
            # Limit order: check if price was touched
            if order.side == Side.BUY and bar['low'] <= order.limit_price:
                fill_price = order.limit_price
            elif order.side == Side.SELL and bar['high'] >= order.limit_price:
                fill_price = order.limit_price
            else:
                return None  # limit not reached

        fill_qty = int(order.quantity * self.fill_rate)
        if fill_qty <= 0:
            return None
        commission = fill_qty * self.commission_per_share
        return Fill(
            order_id=order.order_id,
            symbol=order.symbol,
            side=order.side,
            quantity=fill_qty,
            price=fill_price,
            commission=commission,
            timestamp=bar.name if hasattr(bar, 'name') else pd.Timestamp.now(),
        )
class RiskManager:
    """Pre-trade risk checks."""
    def __init__(self, max_position_pct: float = 0.10,
                 max_drawdown_pct: float = 0.15,
                 max_correlation: float = 0.80):
        self.max_position_pct = max_position_pct
        self.max_drawdown_pct = max_drawdown_pct
        self.peak_equity = 0.0
    def check_order(self, order: Order, portfolio_value: float,
                     current_position: Position) -> tuple:
        """Returns (approved: bool, reason: str)."""
        # Position size check
        order_notional = order.quantity * (order.limit_price or 0)
        if order_notional / (portfolio_value + 1e-10) > self.max_position_pct:
            return False, f"Position size {order_notional} exceeds {self.max_position_pct*100}% limit"
        # Drawdown check
        self.peak_equity = max(self.peak_equity, portfolio_value)
        drawdown = 1 - portfolio_value / (self.peak_equity + 1e-10)
        if drawdown > self.max_drawdown_pct:
            return False, f"Drawdown {drawdown:.1%} exceeds {self.max_drawdown_pct:.0%} limit"
        return True, "approved"
class BacktestEngine:
    """Main backtesting engine orchestrator."""
    def __init__(self, initial_capital: float = 1_000_000,
                 fill_sim: FillSimulator = None,
                 risk_mgr: RiskManager = None):
        self.capital = initial_capital
        self.fill_sim = fill_sim or FillSimulator()
        self.risk_mgr = risk_mgr or RiskManager()
        self.positions = {}
        self.fills = []
        self.equity_curve = []
        self._order_counter = 0
    def run(self, data: pd.DataFrame,
            strategy_fn: Callable) -> pd.DataFrame:
        """Run backtest: iterate bars, generate signals, fill orders."""
        for timestamp, bar in data.iterrows():
            # Strategy generates orders
            orders = strategy_fn(bar, self.positions, self.capital)
            for order in orders:
                self._order_counter += 1
                order.order_id = self._order_counter
                order.timestamp = timestamp
                # Risk check
                pos = self.positions.get(order.symbol, Position(order.symbol))
                approved, reason = self.risk_mgr.check_order(
                    order, self._portfolio_value(bar), pos
                )
                if not approved:
                    order.status = OrderStatus.REJECTED
                    continue
                # Fill simulation
                fill = self.fill_sim.simulate_fill(order, bar)
                if fill:
                    order.status = OrderStatus.FILLED
                    order.filled_qty = fill.quantity
                    order.avg_fill_price = fill.price
                    if order.symbol not in self.positions:
                        self.positions[order.symbol] = Position(order.symbol)
                    self.positions[order.symbol].update(fill)
                    self.capital -= fill.commission
                    self.fills.append(fill)
            self.equity_curve.append({
                'timestamp': timestamp,
                'equity': self._portfolio_value(bar),
            })
        return pd.DataFrame(self.equity_curve).set_index('timestamp')
    def _portfolio_value(self, bar: pd.Series) -> float:
        """Mark-to-market portfolio value."""
        mtm = self.capital
        for sym, pos in self.positions.items():
            if f'{sym}_close' in bar:
                mtm += pos.quantity * bar[f'{sym}_close']
            elif 'close' in bar:
                mtm += pos.quantity * bar['close']
        return mtm

Step 3: Example Strategy

# src/strategies/mean_reversion.py
"""Simple mean reversion strategy for backtesting demonstration."""
from src.engine.backtest_engine import Order, Side, OrderType, Position
def mean_reversion_strategy(bar, positions: dict, capital: float,
                             lookback: int = 20,
                             z_entry: float = 2.0,
                             z_exit: float = 0.5) -> list:
    """Mean reversion: buy when z-score < -2, sell when z-score > 2."""
    orders = []
    symbol = 'SPY'
    pos = positions.get(symbol, Position(symbol))
    position_size = int(capital * 0.05 / (bar['close'] + 1e-10))
    # Simplified: use bar data directly
    if 'z_score' in bar:
        z = bar['z_score']
        if z < -z_entry and pos.quantity <= 0:
            orders.append(Order(0, symbol, Side.BUY, position_size, OrderType.MARKET))
        elif z > z_entry and pos.quantity >= 0:
            orders.append(Order(0, symbol, Side.SELL, position_size, OrderType.MARKET))
        elif abs(z) < z_exit and pos.quantity != 0:
            # Exit position
            side = Side.SELL if pos.quantity > 0 else Side.BUY
            orders.append(Order(0, symbol, side, abs(pos.quantity), OrderType.MARKET))
    return orders

Step 4: Verify

python3 -c "
import numpy as np
import pandas as pd
from src.engine.backtest_engine import BacktestEngine, FillSimulator, Order, Side, OrderType, Position
np.random.seed(42)
dates = pd.bdate_range('2025-01-01', periods=252)
prices = 100 * np.exp(np.cumsum(np.random.randn(252) * 0.01))
data = pd.DataFrame({
    'open': prices * (1 + np.random.randn(252)*0.001),
    'high': prices * 1.01,
    'low': prices * 0.99,
    'close': prices,
    'volume': np.random.randint(1e6, 1e7, 252),
    'z_score': np.random.randn(252),
}, index=dates)
def simple_strategy(bar, positions, capital):
    orders = []
    if bar['z_score'] < -2:
        orders.append(Order(0, 'SPY', Side.BUY, 100, OrderType.MARKET))
    elif bar['z_score'] > 2:
        orders.append(Order(0, 'SPY', Side.SELL, 100, OrderType.MARKET))
    return orders
engine = BacktestEngine(initial_capital=1_000_000,
                         fill_sim=FillSimulator(slippage_bps=5))
equity = engine.run(data, simple_strategy)
print(f'Final equity: \${equity[\"equity\"].iloc[-1]:,.0f}')
print(f'Total fills: {len(engine.fills)}')
print(f'Equity points: {len(equity)}')
assert len(equity) == 252, 'Missing equity points'
assert len(engine.fills) > 0, 'No fills generated'
print('Backtest engine: PASS')
"

CLAUDE.md for Trading System Backtesting

# Trading System Backtesting Standards
## Architecture
- Event-driven (not vectorized) for realistic simulation
- Separate signal generation from execution logic
- Risk manager sits between strategy and fill simulator
- Fill simulator models slippage, partial fills, commissions
## Bias Prevention
- No look-ahead: strategy sees only data available at decision time
- No survivorship: include delisted instruments in universe
- Commission and slippage modeling: 5-10 bps minimum
- Walk-forward validation: never report in-sample results
## Performance Metrics
- Sharpe ratio (annualized, excess of risk-free)
- Maximum drawdown (peak to trough)
- Calmar ratio (return / max drawdown)
- Turnover (annual, one-way)
- Net return after all costs
## Common Commands
- python3 src/engine/backtest_engine.py — run backtest
- python3 -c "import pyfolio; pyfolio.create_full_tear_sheet(...)" — performance report

Common Pitfalls

  • Look-ahead bias in bar data: Using the close price for entry when the signal depends on the close creates a fill at a price the strategy could not have known. Claude Code fills market orders at the next bar’s open with slippage, never the current bar’s close.
  • Missing commission modeling: A strategy that trades 50 times per day at $0.005 per share with 1000-share orders loses $250/day in commissions alone. Claude Code tracks cumulative commission impact and reports it separately from gross P&L.
  • Overfitting to parameter optimization: Grid-searching 100 parameter combinations guarantees one with a great backtest. Claude Code implements walk-forward optimization with out-of-sample windows and reports the distribution of out-of-sample Sharpe ratios across folds.

Frequently Asked Questions

Do I need a paid Anthropic plan to use this?

Claude Code works with any Anthropic API plan, including the free tier. However, the free tier has lower rate limits (requests per minute and tokens per minute) that may slow down multi-step workflows. For professional use, the Build or Scale plan provides higher limits and priority access during peak hours.

How does this affect token usage and cost?

The token cost depends on the size of your prompts and Claude’s responses. Typical development tasks consume 10K-50K tokens per interaction. Using a CLAUDE.md file and skills reduces exploration tokens by 50-80%, which directly lowers costs. Monitor your usage at console.anthropic.com/settings/billing.

Can I customize this for my specific project?

Yes. All Claude Code behavior can be customized through CLAUDE.md (project rules), .claude/settings.json (permissions), and .claude/skills/ (domain knowledge). The most impactful customization is adding your project’s specific patterns, conventions, and common commands to CLAUDE.md so Claude Code follows your standards from the start.

What happens when Claude Code makes a mistake?

Claude Code creates files and edits through standard filesystem operations, so all changes are visible in git diff. If a change is wrong, revert it with git checkout -- <file> for a single file or git stash for all changes. Claude Code does not make irreversible changes unless you explicitly allow destructive commands in settings.json.

Build yours → Create a custom CLAUDE.md with our Generator Tool.

Estimate tokens → Calculate your usage with our Token Estimator.

Try it: Estimate your monthly spend with our Cost Calculator.