Agent skill

backtest-capital-accounting

Critical fix for backtest capital accounting when equity curves are inflated or drawdown metrics are meaningless

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/testing/backtest-capital-accounting-smith6jt-cop-skills-registry

SKILL.md

Backtest Capital Accounting - Research Notes

Experiment Overview

Item Details
Date 2025-12-16
Goal Fix inflated equity curves and meaningless drawdown in backtests
Environment Python 3.10, pandas, custom BacktestEngine
Status Success

Context

Backtest engines often fail to properly track cash flows, leading to:

  • Capital not deducted when positions open (inflated buying power)
  • Sale proceeds not properly credited on close
  • Equity calculation ignoring open position market values
  • Meaningless max_drawdown metrics that can't trigger guardrails

The bug is subtle because the backtest "runs" without errors but produces unrealistic results.

Verified Workflow

1. Correct _open_position() Implementation

python
def _open_position(self, symbol, side, price, time, bar_idx, confidence):
    """Open position with proper capital accounting."""
    # Apply slippage first
    slippage = price * self.config.slippage_pct
    entry_price = price + (slippage if side == 1 else -slippage)

    # Calculate position size from AVAILABLE capital
    position_value = self.capital * self.config.position_size_pct
    shares = position_value / entry_price

    # CRITICAL: Deduct full position value + commission from capital
    total_cost = (shares * entry_price) + self.config.commission_per_trade
    self.capital -= total_cost  # <-- This line is often missing!

    # Store position
    self.positions[symbol] = Position(...)

2. Correct _close_position() Implementation

python
def _close_position(self, symbol, price, time, bar_idx, reason):
    """Close position with proper capital accounting."""
    position = self.positions[symbol]

    # Apply slippage
    slippage = price * self.config.slippage_pct
    exit_price = price - (slippage if position.side == 1 else -slippage)

    commission = self.config.commission_per_trade

    # CRITICAL: Add sale proceeds (exit value - commission) to capital
    if position.side == 1:  # Long
        sale_proceeds = position.shares * exit_price - commission
    else:  # Short
        # Short P&L: entry - exit (profit when price falls)
        sale_proceeds = (2 * position.shares * position.entry_price) - \
                       (position.shares * exit_price) - commission

    self.capital += sale_proceeds  # <-- This line is often wrong!

    del self.positions[symbol]

3. Correct _calculate_equity() Implementation

python
def _calculate_equity(self, current_price: float) -> float:
    """Calculate total equity = cash + market value of positions."""
    equity = self.capital  # Cash on hand

    for position in self.positions.values():
        if position.side == 1:  # Long
            market_value = position.shares * current_price
        else:  # Short
            # Short market value: 2*entry - current (profit when price falls)
            market_value = (2 * position.shares * position.entry_price) - \
                          (position.shares * current_price)
        equity += market_value

    return equity

Failed Attempts (Critical)

Attempt Why it Failed Lesson Learned
Only tracking P&L on close Capital shows full value even with open positions Must deduct on entry, add on exit
Using capital = initial + sum(pnl) Ignores unrealized gains/losses Equity must include position market value
Not applying slippage to entry/exit Overstated returns Apply slippage before calculating shares/proceeds
Adding position value to capital on close Double-counts - position value already in equity Add sale PROCEEDS, not position value
Short position P&L = (entry - exit) * shares Wrong sign when price rises Use 2*entry - exit formula for short market value

Final Parameters

python
# BacktestConfig with proper defaults
@dataclass
class BacktestConfig:
    initial_capital: float = 100_000.0
    position_size_pct: float = 0.10      # 10% per position
    commission_per_trade: float = 1.0    # $1 per trade
    slippage_pct: float = 0.0005         # 0.05% slippage
    stop_loss_pct: float = 0.02          # 2% stop
    take_profit_pct: float = 0.04        # 4% TP

Key Insights

  • The capital accounting bug is silent - backtests run but produce garbage metrics
  • Equity must equal cash + market value - at all times, not just on close
  • Short positions need special handling - profit when price falls, loss when rises
  • Commission affects both entry AND exit - double the impact of what you might expect
  • Slippage compounds with position size - large positions have more slippage impact
  • Test with round trips - open then close same position, capital should reflect P&L exactly

Validation Check

After fixing, verify with this test:

python
# After a round trip (buy then sell same shares):
# capital_after = capital_before + realized_pnl - 2*commission - slippage_cost
# If capital_after > capital_before + realistic_pnl, accounting is broken

References

  • CLAUDE.md guardrails: "Realistic backtests: Do not alter cash-flow logic without full audit"
  • Standard brokerage margin accounting rules
  • Walk-forward validation best practices

Didn't find tool you were looking for?

Be as detailed as possible for better results