Claude-skill-registry drawdown-guardrails-pattern

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

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/drawdown-guardrails-pattern" ~/.claude/skills/majiayu000-claude-skill-registry-drawdown-guardrails-pattern && rm -rf "$T"
manifest: skills/data/drawdown-guardrails-pattern/SKILL.md
source content

Drawdown Guardrails Pattern - Research Notes

Experiment Overview

ItemDetails
Date2025-12-16
GoalImplement consistent drawdown controls across all trading system components
EnvironmentPython 3.10, PyTorch, custom trading system
StatusSuccess

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)

AttemptWhy it FailedLesson Learned
Only checking drawdown on trade entryPositions can gap down overnightCheck on every bar/loop
Resetting halt flag when drawdown recoversCreates "trading whipsaw" behaviorHalt should be sticky until manual reset
Using absolute dollar drawdownNot comparable across account sizesAlways use percentage of peak
Linear position scaling 0-100%Too aggressive reduction at small drawdownsUse threshold-based (warning zone)
Not allowing exits when haltedPositions stuck, can't cut lossesAlways allow stop-loss exits
Drawdown from initial capitalMisses profit drawdownsTrack 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