DEV Community

Propfirmkey
Propfirmkey

Posted on

Comparing Prop Firm Drawdown Rules: EOD vs Real-Time -- A Developer's Deep Dive

As a developer who trades futures, I wanted to truly understand how different drawdown calculation methods impact survival rates. So I built a simulator. Here's what the data shows.

The Three Drawdown Models

import numpy as np
from dataclasses import dataclass
from typing import Literal

@dataclass
class DrawdownRule:
    max_drawdown: float
    rule_type: Literal["static", "eod_trailing", "realtime_trailing"]
    daily_loss_limit: float = 0

class DrawdownSimulator:
    def __init__(self, rule: DrawdownRule, starting_balance: float = 50_000):
        self.rule = rule
        self.starting_balance = starting_balance

    def simulate_day(self, balance, floor, intraday_pnl):
        peak_intraday = balance
        current = balance
        breached = False

        for pnl in intraday_pnl:
            current += pnl
            if self.rule.rule_type == "realtime_trailing":
                peak_intraday = max(peak_intraday, current)
                if current <= peak_intraday - self.rule.max_drawdown:
                    breached = True
                    break
            elif self.rule.rule_type == "eod_trailing":
                if current <= floor:
                    breached = True
                    break
            else:
                if current <= self.starting_balance - self.rule.max_drawdown:
                    breached = True
                    break

            if self.rule.daily_loss_limit > 0 and balance - current >= self.rule.daily_loss_limit:
                breached = True
                break

        new_floor = floor
        if not breached:
            if self.rule.rule_type == "eod_trailing":
                new_floor = max(floor, current - self.rule.max_drawdown)
            elif self.rule.rule_type == "realtime_trailing":
                new_floor = max(floor, peak_intraday - self.rule.max_drawdown)

        return {"end_balance": current, "floor": new_floor, "breached": breached}

    def run_simulation(self, n_days=30, trades_per_day=8, trade_mean=20, trade_std=250):
        balance = self.starting_balance
        floor = self.starting_balance - self.rule.max_drawdown
        equity_curve = [balance]

        for day in range(n_days):
            intraday_pnl = np.random.normal(trade_mean, trade_std, trades_per_day).tolist()
            result = self.simulate_day(balance, floor, intraday_pnl)
            if result["breached"]:
                return {"survived": False, "blown_day": day + 1, "final_balance": result["end_balance"]}
            balance = result["end_balance"]
            floor = result["floor"]
            equity_curve.append(balance)

        return {"survived": True, "final_balance": balance, "equity_curve": equity_curve}
Enter fullscreen mode Exit fullscreen mode

Large-Scale Comparison

def compare_rules(n_simulations=10_000):
    rules = {
        "Static $3000": DrawdownRule(3000, "static", daily_loss_limit=1500),
        "EOD Trail $2500": DrawdownRule(2500, "eod_trailing", daily_loss_limit=1500),
        "EOD Trail $3000": DrawdownRule(3000, "eod_trailing", daily_loss_limit=1500),
        "RT Trail $2500": DrawdownRule(2500, "realtime_trailing", daily_loss_limit=1500),
        "RT Trail $3000": DrawdownRule(3000, "realtime_trailing", daily_loss_limit=1500),
    }

    results = {}
    for name, rule in rules.items():
        survived = 0
        final_balances = []
        blown_days = []

        for _ in range(n_simulations):
            sim = DrawdownSimulator(rule)
            result = sim.run_simulation(n_days=30)
            if result["survived"]:
                survived += 1
                final_balances.append(result["final_balance"])
            else:
                blown_days.append(result["blown_day"])

        results[name] = {
            "survival_rate": survived / n_simulations * 100,
            "avg_final_balance": np.mean(final_balances) if final_balances else 0,
            "avg_blown_day": np.mean(blown_days) if blown_days else "N/A",
        }
    return results

np.random.seed(42)
comparison = compare_rules(5000)

print(f"{'Rule':<20} {'Survival%':>10} {'Avg Final$':>12} {'Avg Blown Day':>14}")
print("=" * 58)
for name, stats in comparison.items():
    blown_str = f"{stats['avg_blown_day']:.1f}" if isinstance(stats['avg_blown_day'], float) else stats['avg_blown_day']
    print(f"{name:<20} {stats['survival_rate']:>9.1f}% ${stats['avg_final_balance']:>10,.0f} {blown_str:>14}")
Enter fullscreen mode Exit fullscreen mode

Expected Results

Rule Survival Rate Analysis
Static $3000 ~75% Most forgiving
EOD Trail $3000 ~68% Good balance
EOD Trail $2500 ~60% Tighter but workable
RT Trail $3000 ~55% Intraday spikes hurt
RT Trail $2500 ~45% Hardest to survive

Strategy Implications

The key insight: drawdown type matters more than drawdown amount. A $3000 real-time trailing drawdown is harder to survive than a $2500 EOD trailing. Match your strategy to the right rule set.

strategies = {
    "Scalping": {"trades_per_day": 20, "trade_mean": 8, "trade_std": 100},
    "Swing": {"trades_per_day": 2, "trade_mean": 80, "trade_std": 500},
    "Momentum": {"trades_per_day": 5, "trade_mean": 30, "trade_std": 300},
}

for name, params in strategies.items():
    print(f"\n{name}:")
    for rule_type in ["eod_trailing", "realtime_trailing"]:
        rule = DrawdownRule(3000, rule_type, 1500)
        survived = sum(1 for _ in range(2000) if DrawdownSimulator(rule).run_simulation(**params)["survived"])
        print(f"  {rule_type}: {survived/20:.1f}% survival")
Enter fullscreen mode Exit fullscreen mode

For developers building trading systems, understanding these rules programmatically is essential. PropFirmKey catalogs the exact drawdown mechanics for each firm, including Alpha Futures, so you can configure your risk engine accordingly.

Top comments (0)