Monte Carlo Forex Signal Simulator: 10,000-Path Python Engine (Agent 7)
TL;DR:
- Agent 7 of the TebotechSignals 7-agent consensus framework runs 10,000 Monte Carlo simulations per signal to calculate TP1/TP2/TP3/SL probabilities and expected value
- Full Python implementation provided using Geometric Brownian Motion with volatility scaling, session weighting, and SMC confluence adjustment
- A signal only passes Agent 7 if EV is positive AND TP1 probability ≥ 50%
- This prevents overfitting by using statistical probability rather than single backtest results
What Is Monte Carlo Simulation in Forex?
Monte Carlo simulation is a computational technique that uses repeated random sampling to estimate the probability of different outcomes. Instead of looking at one historical backtest result, it simulates 10,000 possible futures from the current entry point.
Each simulation generates a random price path using:
- Historical volatility (ATR) for the specific pair
- Drift (directional bias from SMC analysis)
- Session weighting (London/NY kill zones have higher volatility)
- Confluence adjustment (higher confluence = stronger drift)
The engine then checks: did this path hit TP1, TP2, TP3, or the stop loss first?
Full Python Implementation
python"""
TeboTechSignals Monte Carlo Signal Simulator (Agent 7)
Runs 10,000 simulations per signal to validate expected value and probability.
"""
import numpy as np
from dataclasses import dataclass
from typing import Optional
from datetime import datetime, timedelta
@dataclass
class SignalInput:
"""Input signal data for Monte Carlo simulation."""
pair: str
direction: str # "BUY" or "SELL"
entry_price: float
stop_loss: float
take_profit_1: float
take_profit_2: Optional[float] = None
take_profit_3: Optional[float] = None
timeframe: str = "H4"
smc_confluence_score: int = 8 # 1-10
volu_smart_score: int = 7 # 1-10
fakeout_risk_score: int = 4 # 1-10
htf_bias: str = "bullish" # "bullish", "bearish", "neutral"
smc_session: str = "london_ny_overlap"
@dataclass
class MonteCarloResult:
"""Output of the Monte Carlo simulation."""
tp1_probability: float
tp2_probability: float
tp3_probability: float
sl_probability: float
expected_value_pips: float
profit_factor: float
max_drawdown_avg_pips: float
approved: bool
veto_reason: Optional[str]
simulations_run: int
─────────────────────────────────────────────────────────────────────────────
PAIR-SPECIFIC PARAMETERS
Historical volatility (ATR in pips) and win rates per pair
─────────────────────────────────────────────────────────────────────────────
PAIR_PARAMS = {
"EUR/USD": {"atr_pips": 60, "pip_size": 0.0001, "historical_win_rate": 0.62},
"GBP/USD": {"atr_pips": 80, "pip_size": 0.0001, "historical_win_rate": 0.58},
"USD/JPY": {"atr_pips": 70, "pip_size": 0.01, "historical_win_rate": 0.65},
"GBP/JPY": {"atr_pips": 120, "pip_size": 0.01, "historical_win_rate": 0.60},
"EUR/JPY": {"atr_pips": 100, "pip_size": 0.01, "historical_win_rate": 0.61},
"AUD/USD": {"atr_pips": 55, "pip_size": 0.0001, "historical_win_rate": 0.59},
"NZD/USD": {"atr_pips": 50, "pip_size": 0.0001, "historical_win_rate": 0.57},
"USD/CHF": {"atr_pips": 55, "pip_size": 0.0001, "historical_win_rate": 0.63},
"USD/CAD": {"atr_pips": 60, "pip_size": 0.0001, "historical_win_rate": 0.60},
"EUR/GBP": {"atr_pips": 35, "pip_size": 0.0001, "historical_win_rate": 0.64},
"XAU/USD": {"atr_pips": 300, "pip_size": 0.01, "historical_win_rate": 0.55},
"BTC/USD": {"atr_pips": 2000, "pip_size": 1.0, "historical_win_rate": 0.52},
"NAS100": {"atr_pips": 150, "pip_size": 1.0, "historical_win_rate": 0.56},
}
SESSION_VOLATILITY_MULTIPLIER = {
"london_ny_overlap": 1.3, # Highest volatility
"london": 1.1,
"new_york": 1.0,
"asian": 0.7, # Lowest volatility
"any": 1.0,
}
NUM_SIMULATIONS = 10_000
MAX_BARS_PER_SIM = 240 # 240 H4 bars = 40 days max hold
─────────────────────────────────────────────────────────────────────────────
CORE SIMULATION ENGINE
─────────────────────────────────────────────────────────────────────────────
def run_monte_carlo(signal: SignalInput) -> MonteCarloResult:
"""
Run 10,000 Monte Carlo simulations for a trading signal.
Uses Geometric Brownian Motion (GBM) with:
- Pair-specific volatility (ATR)
- Session-adjusted volatility multiplier
- Drift based on SMC confluence + HTF bias + volume confirmation
- Fakeout risk adjustment (higher fakeout = more noise)
Returns probability of hitting each TP level and stop loss,
expected value in pips, profit factor, and approval decision.
"""
# Get pair parameters (default to EUR/USD if not found)
params = PAIR_PARAMS.get(signal.pair, PAIR_PARAMS["EUR/USD"])
atr = params["atr_pips"]
pip_size = params["pip_size"]
hist_win_rate = params["historical_win_rate"]
# Session volatility multiplier
session_mult = SESSION_VOLATILITY_MULTIPLIER.get(
signal.smc_session, 1.0
)
# Calculate drift (directional bias)
# Higher confluence = stronger drift in signal direction
confluence_factor = signal.smc_confluence_score / 10.0 # 0.8 to 1.0
volume_factor = signal.volu_smart_score / 10.0 # 0.7 to 1.0
# Base drift from historical win rate
base_drift = (hist_win_rate - 0.5) * 2 # -1 to +1
# Adjust drift by confluence and volume
drift_strength = base_drift * confluence_factor * volume_factor
# Fakeout risk reduces effective drift (more noise)
fakeout_penalty = signal.fakeout_risk_score / 20.0 # 0.15 to 0.35
drift_strength *= (1.0 - fakeout_penalty)
# Direction multiplier
direction_mult = 1.0 if signal.direction == "BUY" else -1.0
# HTF bias alignment check
htf_aligned = (
(signal.htf_bias == "bullish" and signal.direction == "BUY") or
(signal.htf_bias == "bearish" and signal.direction == "SELL")
)
if not htf_aligned:
drift_strength *= 0.5 # HTF not aligned = weaker drift
# Calculate price levels in pips from entry
entry = signal.entry_price
sl_distance = abs(signal.stop_loss - entry) / pip_size
tp1_distance = abs(signal.take_profit_1 - entry) / pip_size
tp2_distance = (
abs(signal.take_profit_2 - entry) / pip_size
if signal.take_profit_2 else None
)
tp3_distance = (
abs(signal.take_profit_3 - entry) / pip_size
if signal.take_profit_3 else None
)
# Volatility per bar (adjusted by session)
vol_per_bar = atr * session_mult * np.sqrt(4 / 24) # H4 bars
# Run simulations
tp1_hits = 0
tp2_hits = 0
tp3_hits = 0
sl_hits = 0
max_drawdowns = []
final_pips = []
np.random.seed(42) # Reproducible results
for _ in range(NUM_SIMULATIONS):
price = entry
max_dd = 0.0
hit_tp1 = False
hit_tp2 = False
hit_tp3 = False
hit_sl = False
for bar in range(MAX_BARS_PER_SIM):
# Geometric Brownian Motion step
# dS = drift * S * dt + vol * S * dW
dt = 1.0 / MAX_BARS_PER_SIM
# Random normal with slight skew from drift
random_shock = np.random.normal(
loc=drift_strength * direction_mult * 0.001,
scale=vol_per_bar * pip_size
)
price += random_shock
# Track max drawdown (for BUY: how far below entry)
if signal.direction == "BUY":
dd = (entry - price) / pip_size
else:
dd = (price - entry) / pip_size
if dd > max_dd:
max_dd = dd
# Check if SL hit
if signal.direction == "BUY" and price <= signal.stop_loss:
hit_sl = True
break
elif signal.direction == "SELL" and price >= signal.stop_loss:
hit_sl = True
break
# Check TP levels
if signal.direction == "BUY":
if not hit_tp1 and price >= signal.take_profit_1:
hit_tp1 = True
if signal.take_profit_2 and not hit_tp2 and price >= signal.take_profit_2:
hit_tp2 = True
if signal.take_profit_3 and not hit_tp3 and price >= signal.take_profit_3:
hit_tp3 = True
break # Full target reached
else: # SELL
if not hit_tp1 and price <= signal.take_profit_1:
hit_tp1 = True
if signal.take_profit_2 and not hit_tp2 and price <= signal.take_profit_2:
hit_tp2 = True
if signal.take_profit_3 and not hit_tp3 and price <= signal.take_profit_3:
hit_tp3 = True
break
# Record results
if hit_tp3:
tp1_hits += 1
tp2_hits += 1
tp3_hits += 1
# Partial close: 50% at TP1, 30% at TP2, 20% at TP3
final_pips.append(
tp1_distance * 0.5 + tp2_distance * 0.3 + tp3_distance * 0.2
)
elif hit_tp2:
tp1_hits += 1
tp2_hits += 1
final_pips.append(tp1_distance * 0.5 + tp2_distance * 0.3)
elif hit_tp1:
tp1_hits += 1
final_pips.append(tp1_distance * 0.5)
elif hit_sl:
sl_hits += 1
final_pips.append(-sl_distance)
else:
# Neither TP nor SL hit within time limit — close at current
remaining_pips = (
(price - entry) / pip_size if signal.direction == "BUY"
else (entry - price) / pip_size
)
final_pips.append(remaining_pips)
max_drawdowns.append(max_dd)
# Calculate probabilities
tp1_prob = tp1_hits / NUM_SIMULATIONS
tp2_prob = tp2_hits / NUM_SIMULATIONS
tp3_prob = tp3_hits / NUM_SIMULATIONS
sl_prob = sl_hits / NUM_SIMULATIONS
# Calculate expected value (average pips across all simulations)
ev_pips = np.mean(final_pips)
# Calculate profit factor
wins = [p for p in final_pips if p > 0]
losses = [abs(p) for p in final_pips if p < 0]
gross_profit = sum(wins) if wins else 0
gross_loss = sum(losses) if losses else 1
profit_factor = gross_profit / gross_loss if gross_loss > 0 else float('inf')
# Average max drawdown
avg_max_dd = np.mean(max_drawdowns)
# ── APPROVAL DECISION ──────────────────────────────────────────────────
approved = True
veto_reason = None
if ev_pips <= 0:
approved = False
veto_reason = f"HARD VETO: Expected value negative ({ev_pips:.1f} pips)"
elif tp1_prob < 0.50:
approved = False
veto_reason = f"HARD VETO: TP1 probability {tp1_prob:.1%} < 50% threshold"
elif profit_factor < 1.5:
approved = False
veto_reason = f"REJECT: Profit factor {profit_factor:.2f} < 1.5 threshold"
return MonteCarloResult(
tp1_probability=tp1_prob,
tp2_probability=tp2_prob,
tp3_probability=tp3_prob,
sl_probability=sl_prob,
expected_value_pips=ev_pips,
profit_factor=profit_factor,
max_drawdown_avg_pips=avg_max_dd,
approved=approved,
veto_reason=veto_reason,
simulations_run=NUM_SIMULATIONS,
)
─────────────────────────────────────────────────────────────────────────────
BATCH VALIDATION: Run Monte Carlo on all active signals
─────────────────────────────────────────────────────────────────────────────
def validate_signal_board(signals: list[SignalInput]) -> dict:
"""
Run Monte Carlo validation on the entire signal board.
Returns a summary with approval/rejection for each signal.
"""
results = {}
for signal in signals:
result = run_monte_carlo(signal)
results[signal.pair] = {
"direction": signal.direction,
"tp1_prob": f"{result.tp1_probability:.1%}",
"tp2_prob": f"{result.tp2_probability:.1%}",
"tp3_prob": f"{result.tp3_probability:.1%}",
"sl_prob": f"{result.sl_probability:.1%}",
"ev_pips": f"{result.expected_value_pips:+.1f}",
"profit_factor": f"{result.profit_factor:.2f}",
"max_dd_avg": f"{result.max_drawdown_avg_pips:.0f} pips",
"approved": result.approved,
"veto": result.veto_reason or "PASS",
}
approved_count = sum(1 for r in results.values() if r["approved"])
rejected_count = len(results) - approved_count
return {
"summary": {
"total_signals": len(signals),
"approved": approved_count,
"rejected": rejected_count,
"approval_rate": f"{approved_count / len(signals):.1%}",
},
"signals": results,
}
─────────────────────────────────────────────────────────────────────────────
EXAMPLE: Validate a sample signal
─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
# Example: USD/JPY BUY (our 10/10 confluence signal)
signal = SignalInput(
pair="USD/JPY",
direction="BUY",
entry_price=156.50,
stop_loss=155.70,
take_profit_1=157.50,
take_profit_2=158.50,
take_profit_3=159.50,
timeframe="H4",
smc_confluence_score=10,
volu_smart_score=8,
fakeout_risk_score=3,
htf_bias="bullish",
smc_session="london_ny_overlap",
)
result = run_monte_carlo(signal)
print(f"{'='*60}")
print(f" MONTE CARLO SIMULATION: {signal.pair} {signal.direction}")
print(f" Entry: {signal.entry_price} | SL: {signal.stop_loss}")
print(f" TP1: {signal.take_profit_1} | TP2: {signal.take_profit_2} | TP3: {signal.take_profit_3}")
print(f"{'='*60}")
print(f" Simulations Run: {result.simulations_run:,}")
print(f" TP1 Probability: {result.tp1_probability:.1%}")
print(f" TP2 Probability: {result.tp2_probability:.1%}")
print(f" TP3 Probability: {result.tp3_probability:.1%}")
print(f" SL Probability: {result.sl_probability:.1%}")
print(f" Expected Value: {result.expected_value_pips:+.1f} pips")
print(f" Profit Factor: {result.profit_factor:.2f}")
print(f" Avg Max Drawdown: {result.max_drawdown_avg_pips:.0f} pips")
print(f" APPROVED: {'✅ YES' if result.approved else '❌ NO'}")
if result.veto_reason:
print(f" Veto: {result.veto_reason}")
print(f"{'='*60}")
How the Simulation Works
Step 1: Gather Signal Inputs
The engine takes all signal parameters: pair, direction, entry, SL, TPs, confluence score, volume score, fakeout risk, HTF bias, and session timing.
Step 2: Calculate Pair-Specific Parameters
Each pair has unique volatility (ATR), pip size, and historical win rate. GBP/JPY has 120-pip ATR (high volatility) while EUR/GBP has 35-pip ATR (low volatility). The engine uses these to calibrate the random walk.
Step 3: Calculate Drift
Drift is the directional bias. It's calculated as:
drift = (historical_win_rate - 0.5) × 2 × confluence_factor × volume_factor × (1 - fakeout_penalty)
A 10/10 confluence signal with 8/10 Volu-Smart and 3/10 fakeout risk gets maximum drift. A 6/10 confluence signal with 4/10 volume and 8/10 fakeout risk gets minimal drift.
Step 4: Simulate 10,000 Price Paths
Each path is a Geometric Brownian Motion random walk. The engine steps through up to 240 H4 bars (40 days), checking at each step whether the price hit TP1, TP2, TP3, or the stop loss.
Step 5: Calculate Probabilities
After all 10,000 simulations, the engine calculates:
- What % of paths hit TP1? (TP1 probability)
- What % hit TP2? (TP2 probability)
- What % hit TP3? (TP3 probability)
- What % hit the stop loss? (SL probability)
Step 6: Calculate Expected Value
Using partial close logic (50% at TP1, 30% at TP2, 20% at TP3), the engine calculates the average profit/loss across all 10,000 simulations.
Step 7: Approval Decision
- EV must be positive (hard veto)
- TP1 probability must be ≥ 50% (hard veto)
- Profit factor must be ≥ 1.5 (soft check)
Why This Prevents Overfitting
Traditional backtesting runs a strategy on historical data and reports the results. The problem: the strategy was designed on that same data, so it's optimized for past conditions. This is called overfitting.
Monte Carlo simulation solves this by generating 10,000 new, synthetic price paths that never actually happened. If a signal is profitable across 10,000 random scenarios (not just the one historical path that occurred), it's more likely to be robust.
Key Takeaways:
- Monte Carlo generates synthetic futures, not historical replays
- A signal profitable across 10,000 random paths is more robust than one profitable in one backtest
- The 50% TP1 probability threshold filters out weak setups
- Positive expected value ensures long-term profitability even with individual losses
Partial Close Logic
The engine uses a partial close strategy that mirrors professional trading:
| Level | Close % | Rationale |
|-------|---------|-----------|
| TP1 | 50% | Lock in profit, move SL to breakeven |
| TP2 | 30% | Scale out, let runner work |
| TP3 | 20% | Full target, ride the trend |
This means even if only TP1 is hit, the trader captures 50% of the TP1 distance. If all three TPs are hit, the total profit is: `TP1 × 0.5 + TP2 × 0.3 + TP3 × 0.2`.
Frequently Asked Questions
Q: How many Monte Carlo simulations should I run?
10,000 is the industry standard. More simulations (100,000+) give smoother results but take longer. 10,000 provides a good balance of accuracy and speed.
Q: What volatility should I use?
Use the pair's Average True Range (ATR) on the same timeframe as your signal. For H4 signals, use the H4 ATR. For M5 scalps, use the M5 ATR.
Q: What if the Monte Carlo rejects my signal?
If EV is negative or TP1 probability is below 50%, the signal lacks a statistical edge. Either improve the entry (better OB, tighter SL) or skip the trade. Not every setup is worth taking.
Q: Can I use this for any trading strategy?
Yes. The Monte Carlo engine works with any strategy — SMC, price action, indicators, fundamentals. Just input the entry, SL, and TP levels.
⚠️ Risk Disclaimer: Monte Carlo simulation improves signal quality assessment but does not guarantee profits. The synthetic price paths are statistical approximations, not predictions. Trading involves significant risk. Never risk more than 1-2% of your account per trade.
Related Resources:
- 📊 Live Trading Signals — All validated through the 7-agent framework
- 📈 Performance Track Record — Verified results
- 📚 SMC Education Hub — Learn the methodology
- 📝 Agentic Quantum Framework — Full 7-agent architecture
- 📝 All Blog Articles