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:

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:

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

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:

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:

Blog | Tebotechsignals
Skip to main content
XAU/USD2342.50
US3039245.00
NAS10017854.00
XAU/USD2342.50
US3039245.00
NAS10017854.00
Monte Carlo Forex Signal Simulator: 10,000-Path Python Engine (Agent 7)
tutorials
September 25, 2026

TebotechSignals Research Team

Institutional FX Analysts · ICT Smart Money Concepts Specialists

Monte Carlo Forex Signal Simulator: 10,000-Path Python Engine (Agent 7)

Full Python implementation of Agent 7 from the 7-agent signal validation framework. Runs 10,000 Monte Carlo simulations per signal using Geometric Brownian Motion with volatility scaling, session weighting, and SMC confluence adjustment. Signals only pass if EV is positive and TP1 probability ≥ 50%.

#Monte Carlo
#Python
#signal validation
#probability
#simulation
#quantum computing
#Agent 7
#trading technology

📘 Reading Price Like the Big Players

Liquidity, order blocks and market structure — explained in plain English. Just $10.00.

Get It

📘 Trading Without the Drama

Fixing risk management, discipline and misinformation — explained in plain English. Just $10.00.

Get It
✨
Sponsored · Keller, TX

Turn Your Profits Into Memories 🎉

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

"""
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:

✨
Sponsored · Keller, TX

Turn Your Profits Into Memories 🎉

📲 Get Free Daily Signals on WhatsApp

Follow our free channel for daily market briefs, ICT signals, and SMC education — delivered straight to your WhatsApp.

Join Free →

Disclaimer: This site is an educational platform. All content — including courses, articles, and any market commentary — is for educational and informational purposes only and does not constitute financial, investment, or professional advice, a recommendation, or a solicitation to trade. It is general in nature and does not take into account your individual financial situation, objectives, or needs. All trading and investing involve risk of loss; past performance is not indicative of future results. Always conduct your own research and consult a licensed professional before making any financial decisions. Client and testimonial experiences may not be typical — individual results will vary. Full fee structure is disclosed on our Pricing page.

We use cookies for essential site functionality and, with your consent, for analytics and advertising (Google Analytics, Facebook, Bing) to understand traffic and improve TebotechSignals. See our Privacy Policy for details.