Back to skills
extension
Category: Development & EngineeringNo API key required

drawdown-guardrails-pattern

Consistent drawdown control pattern for trading systems - backtests, live trading, and training

personAuthor: jakexiaohubgithub

Drawdown Guardrails Pattern - Research Notes

Experiment Overview

| Item | Details | |------|---------| | Date | 2025-12-16 | | Goal | Implement consistent drawdown controls across all trading system components | | Environment | Python 3.10, PyTorch, custom trading system | | Status | Success |

Context

Trading systems need drawdown protection at multiple layers:

  1. Backtesting - Avoid selecting high-drawdown strategies during model evaluation
  2. Live Trading - Halt or reduce risk when equity drops
  3. Training - Penalize models that learn high-return/high-drawdown behavior

Without consistent guardrails, a model might backtest well but blow up in production.

Verified Workflow

1. Configuration (Single Source of Truth)

# Define limits once, reference everywhere
MAX_DRAWDOWN_PCT = 0.15      # 15% - halt trading
DRAWDOWN_WARNING_PCT = 0.10  # 10% - reduce position sizes
DRAWDOWN_SIZING_SCALE = 0.5  # 50% position size in warning zone

2. Drawdown Tracking State

@dataclass
class DrawdownState:
    peak_equity: float
    current_drawdown_pct: float = 0.0
    drawdown_warning_triggered: bool = False
    drawdown_halt_triggered: bool = False  # Once triggered, stays triggered

def update_drawdown_tracking(self, current_equity: float):
    """Update drawdown state - call after every equity calculation."""
    # Update peak (high water mark)
    if current_equity > self.peak_equity:
        self.peak_equity = current_equity

    # Calculate current drawdown
    if self.peak_equity > 0:
        self.current_drawdown_pct = (self.peak_equity - current_equity) / self.peak_equity

    # Update flags
    self.drawdown_warning_triggered = self.current_drawdown_pct >= DRAWDOWN_WARNING_PCT

    # Halt flag is sticky - once triggered, stays triggered until manual reset
    if self.current_drawdown_pct >= MAX_DRAWDOWN_PCT:
        self.drawdown_halt_triggered = True

3. Guardrail Check Function

def check_drawdown_guardrails(self) -> Tuple[bool, str, float]:
    """
    Returns: (can_trade, reason, position_scale)
    """
    # Check halt condition
    if self.drawdown_halt_triggered:
        return False, f"max_drawdown_{self.current_drawdown_pct:.1%}", 0.0

    # Check warning condition
    if self.drawdown_warning_triggered:
        return True, f"drawdown_warning_{self.current_drawdown_pct:.1%}", DRAWDOWN_SIZING_SCALE

    # Normal operation
    return True, "ok", 1.0

4. Integration Points

Backtest Engine

for bar_idx in range(start_idx, end_idx):
    # Calculate equity BEFORE new trades
    equity = self._calculate_equity(current_price)

    # Update drawdown tracking
    self._update_drawdown_tracking(equity)
    can_trade, reason, position_scale = self._check_drawdown_guardrails()

    # Only process signals if allowed
    if can_trade:
        self._process_signal(..., position_scale=position_scale)
    # Even if halted, existing positions can still exit via stop-loss/TP

Live Trading Loop

while running:
    # Check drawdown before any trading
    can_trade, reason, position_scale = profit_tracker.check_trading_conditions(
        max_drawdown_pct=0.15
    )

    if not can_trade:
        logger.warning(f"TRADING HALTED: {reason}")
        # Allow exits but no new entries
        continue

    # Pass scale to trading function
    new_state = decide_and_trade(..., position_scale=position_scale)

Training Validation

# Compute fitness with drawdown penalty
drawdown_penalty = config.drawdown_penalty_weight * max_drawdown
fitness_score = sharpe_ratio * max(0.0, 1.0 - drawdown_penalty)

# Early stopping on excessive drawdown
if max_drawdown > config.max_drawdown_threshold:
    print(f"DRAWDOWN EARLY STOP: {max_drawdown:.1%}")
    should_stop = True

Failed Attempts (Critical)

| Attempt | Why it Failed | Lesson Learned | |---------|---------------|----------------| | Only checking drawdown on trade entry | Positions can gap down overnight | Check on every bar/loop | | Resetting halt flag when drawdown recovers | Creates "trading whipsaw" behavior | Halt should be sticky until manual reset | | Using absolute dollar drawdown | Not comparable across account sizes | Always use percentage of peak | | Linear position scaling 0-100% | Too aggressive reduction at small drawdowns | Use threshold-based (warning zone) | | Not allowing exits when halted | Positions stuck, can't cut losses | Always allow stop-loss exits | | Drawdown from initial capital | Misses profit drawdowns | Track from peak equity (high water mark) |

Final Parameters

# Recommended guardrail configuration
@dataclass
class DrawdownConfig:
    max_drawdown_pct: float = 0.15      # 15% - matches common hedge fund limits
    drawdown_warning_pct: float = 0.10  # 10% - early warning
    drawdown_sizing_scale: float = 0.5  # 50% in warning zone
    halt_on_max_drawdown: bool = True   # Hard stop at max
    drawdown_penalty_weight: float = 2.0  # Training penalty multiplier

Key Insights

  • Drawdown from peak, not initial - Track high water mark, not starting capital
  • Halt flag should be sticky - Don't auto-resume trading after drawdown recovery
  • Allow exits even when halted - Can still cut losses, just no new entries
  • Position scaling is gradual - Warning zone reduces size, halt zone blocks entirely
  • Consistent across all layers - Same 15% limit in backtest, live, and training
  • Training penalty prevents selection - Models that drawdown heavily get lower fitness
  • Log when triggered - Visibility into when guardrails activate

Testing Checklist

# Verify these scenarios:
1. [ ] Drawdown hits 10% -> position sizes reduced 50%
2. [ ] Drawdown hits 15% -> new entries blocked
3. [ ] Recovery to 5% drawdown -> halt flag STILL active (sticky)
4. [ ] Stop-loss orders execute even when halted
5. [ ] Training early-stops on 15%+ drawdown
6. [ ] Backtest metrics show drawdown correctly

References

  • CLAUDE.md: "Max drawdown trigger: 15%"
  • Hedge fund risk management: 15-20% typical max drawdown limits
  • Kelly criterion: Fractional Kelly (0.25-0.5x) to reduce drawdown