154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
import random
|
|
|
|
def _calculate_percentile(data, percentile):
|
|
if not data: return 0.0
|
|
sorted_data = sorted(data)
|
|
index = (len(sorted_data) - 1) * (percentile / 100.0)
|
|
lower = int(index)
|
|
upper = lower + 1
|
|
if upper >= len(sorted_data): return sorted_data[-1]
|
|
return sorted_data[lower] + (index - lower) * (sorted_data[upper] - sorted_data[lower])
|
|
|
|
def _run_metrics(pnls, starting_balance):
|
|
if not pnls:
|
|
return {"net_pnl": 0.0, "win_rate": 0.0, "profit_factor": 0.0, "max_drawdown_pct": 0.0}
|
|
|
|
winners = [p for p in pnls if p > 0]
|
|
losers = [p for p in pnls if p < 0]
|
|
|
|
net_pnl = sum(pnls)
|
|
trade_count = len(pnls)
|
|
win_rate = (len(winners) / trade_count) * 100 if trade_count > 0 else 0.0
|
|
|
|
gross_profit = sum(winners)
|
|
gross_loss = abs(sum(losers))
|
|
profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else (999.0 if gross_profit > 0 else 0.0)
|
|
|
|
equity = starting_balance
|
|
peak = starting_balance
|
|
max_drawdown_pct = 0.0
|
|
|
|
for pnl in pnls:
|
|
equity += pnl
|
|
if equity > peak:
|
|
peak = equity
|
|
drawdown_pct = ((peak - equity) / peak) * 100 if peak > 0 else 0.0
|
|
if drawdown_pct > max_drawdown_pct:
|
|
max_drawdown_pct = drawdown_pct
|
|
|
|
return {
|
|
"net_pnl": net_pnl,
|
|
"win_rate": win_rate,
|
|
"profit_factor": profit_factor,
|
|
"max_drawdown_pct": max_drawdown_pct,
|
|
}
|
|
|
|
def run_monte_carlo(
|
|
trade_r_multiples,
|
|
runs=1000,
|
|
starting_balance=10000.0,
|
|
risk_per_trade_pct=1.0,
|
|
sampling_method="bootstrap",
|
|
missed_trade_pct=5.0,
|
|
pnl_variation_pct=10.0,
|
|
price_noise_pct=0.0,
|
|
slippage_per_trade=0.0,
|
|
spread_per_trade=0.0,
|
|
per_trade_cost=None,
|
|
ruin_drawdown_pct=20.0,
|
|
seed=None,
|
|
):
|
|
if not trade_r_multiples:
|
|
return {"summary": {"runs": 0}, "distribution": [], "sample_runs": []}
|
|
|
|
run_count = max(1, int(runs))
|
|
risk_pct = max(0.0, float(risk_per_trade_pct)) / 100.0
|
|
pnl_var = max(0.0, float(pnl_variation_pct)) / 100.0
|
|
price_var = max(0.0, float(price_noise_pct)) / 100.0
|
|
miss_pct = max(0.0, float(missed_trade_pct)) / 100.0
|
|
ruin_threshold = max(0.0, float(ruin_drawdown_pct))
|
|
fixed_cost = float(per_trade_cost) if per_trade_cost is not None else (max(0.0, float(slippage_per_trade)) + max(0.0, float(spread_per_trade)))
|
|
effective_var = max(pnl_var, price_var)
|
|
|
|
rng = random.Random(seed)
|
|
run_results = []
|
|
base_trades = list(trade_r_multiples)
|
|
trade_count = len(base_trades)
|
|
|
|
for run_idx in range(run_count):
|
|
# 1. Generate the Trade Sequence
|
|
if sampling_method == "bootstrap":
|
|
path = [rng.choice(base_trades) for _ in range(trade_count)]
|
|
elif sampling_method == "shuffle":
|
|
path = base_trades[:]
|
|
rng.shuffle(path)
|
|
else:
|
|
path = base_trades[:]
|
|
|
|
adjusted_pnls = []
|
|
equity = float(starting_balance)
|
|
peak = float(starting_balance)
|
|
ruin_hit = False
|
|
|
|
# 2. Execute the trades sequentially
|
|
for base_r in path:
|
|
# Execution Risk: Did the broker drop our connection?
|
|
if rng.random() < miss_pct:
|
|
continue
|
|
|
|
r_multiple = float(base_r)
|
|
|
|
# Add volatility noise to the outcome (Slippage)
|
|
if effective_var > 0:
|
|
r_multiple *= rng.uniform(1 - effective_var, 1 + effective_var)
|
|
|
|
# Calculate PnL in dollars based on CURRENT equity (Compounding)
|
|
pnl = (equity * risk_pct * r_multiple) - fixed_cost
|
|
|
|
adjusted_pnls.append(pnl)
|
|
equity += pnl
|
|
|
|
# 3. Live Drawdown & Ruin Check (Prevents Zombie Trading)
|
|
if equity > peak:
|
|
peak = equity
|
|
|
|
current_dd = ((peak - equity) / peak) * 100 if peak > 0 else 0.0
|
|
|
|
if current_dd >= ruin_threshold or equity <= 0:
|
|
ruin_hit = True
|
|
break # Account blown or max DD hit. STOP trading.
|
|
|
|
# Calculate metrics for the surviving trades
|
|
metrics = _run_metrics(adjusted_pnls, starting_balance)
|
|
metrics["run"] = run_idx + 1
|
|
metrics["ruin"] = ruin_hit
|
|
metrics["trades_taken"] = len(adjusted_pnls)
|
|
run_results.append(metrics)
|
|
|
|
# --- Aggregate Statistics ---
|
|
pnls = [r["net_pnl"] for r in run_results]
|
|
dds = [r["max_drawdown_pct"] for r in run_results]
|
|
|
|
profitable_runs = sum(1 for p in pnls if p > 0)
|
|
ruin_count = sum(1 for r in run_results if r["ruin"])
|
|
|
|
summary = {
|
|
"runs": run_count,
|
|
"avg_pnl": round(sum(pnls) / run_count, 2),
|
|
"worst_case_pnl_5th_pct": round(_calculate_percentile(pnls, 5), 2), # 95% Confidence you make at least this much
|
|
"profitable_run_pct": round((profitable_runs / run_count) * 100, 2),
|
|
|
|
"avg_max_drawdown_pct": round(sum(dds) / run_count, 2),
|
|
"worst_case_dd_95th_pct": round(_calculate_percentile(dds, 95), 2), # 95% Confidence your DD won't exceed this
|
|
"worst_max_drawdown_pct": round(max(dds), 2),
|
|
|
|
"avg_win_rate": round(sum(r["win_rate"] for r in run_results) / run_count, 2),
|
|
"avg_profit_factor": round(sum(r["profit_factor"] for r in run_results) / run_count, 2),
|
|
"probability_of_ruin": round((ruin_count / run_count) * 100, 2),
|
|
}
|
|
|
|
return {
|
|
"summary": summary,
|
|
"distribution": run_results,
|
|
"sample_runs": run_results,
|
|
} |