Integrating AI into 3 Smart Money Concepts (SMC): OB, BOS, and FVG
Table of Contents
Introduction
Trading with Smart Money Concepts provides a structured approach to market analysis, but structure alone does not guarantee consistency. Even with clearly defined Order Blocks, Fair Value Gaps, and Break of Structure signals, traders still face the challenge of deciding which setups to trust and which to ignore. Not every OB retrace leads to continuation, not every FVG fill produces reversal, and not every BOS reflects a true market shift. This continuous filtering process introduces emotional bias, hesitation, and inconsistency, especially in fast-moving instruments like XAUUSD where conditions can change rapidly within a single candle.
This article addresses that limitation by embedding a machine-learning layer into the SMC EA from the previous topic. Using Python, we train an XGBoost binary classifier on historical XAUUSD H1 data. We then export it to ONNX and compile it into the EA as a binary resource. When a signal appears, the EA generates a 12-feature vector describing market structure, momentum, session timing, and trend context, then evaluates it through the AI model before execution. The system also provides real-time panel feedback showing signal origin, AI confidence, and trend strength, while a volatility-aware trailing stop dynamically manages active positions.
System Overview
The system is built on two separate but tightly connected layers. The first layer is a Python training pipeline. It fetches OHLCV data from MetaTrader 5, detects SMC signals, labels each event as a win/loss based on TP/SL, and trains an XGBoost classifier on the resulting dataset. Once training is complete, the model is exported to ONNX format and saved directly into the MetaTrader 5 Files folder. From there, MetaEditor compiles it into the EA as a binary resource, meaning the model is baked into the executable and requires no file path at runtime.

The model is trained offline in Python, then baked directly into the EA executable. At runtime, no file path is required—the brain lives inside the robot.
The second layer is the MQL5 EA itself, which handles everything that happens on the chart. On each new bar, it scans for valid OB, FVG, and BOS setups using the same detection logic that was used during training. When price enters a zone, the EA builds a feature vector from the current market context and passes it to the embedded ONNX model for scoring. If the score meets the minimum confidence threshold, the trade is executed with a volatility-aware stop-loss and take-profit. A trailing stop then manages the open position tick by tick, locking in profit as price moves forward. The chart panel displays the active signal, its origin, the AI confidence score, and the current trend strength. This provides visibility into the system's decisions.

The EA acts as the eyes, brain, and hands—detecting setups, scoring them with the embedded ONNX model, executing trades, and showing the trader exactly why every decision was made.
Getting Started
from datetime import datetime import sys import warnings import pandas as pd import pytz warnings.filterwarnings("ignore") # ============================================================================= # CONFIG — edit these values # ============================================================================= SYMBOL = "XAUUSD" # exact symbol name as shown in MT5 Market Watch TIMEFRAME = "H1" # M1 M5 M15 M30 H1 H4 D1 DATE_FROM = datetime(2020, 1, 1) # start of historical range DATE_TO = datetime(2026, 1, 1) # end of historical range (exclusive) OUTPUT_CSV = "XAUUSD_H1.csv" # output filename (saved in working directory) TIMEZONE = "Etc/UTC" # keep UTC — pipeline expects UTC timestamps # ============================================================================= # Timeframe string -> MT5 constant name TF_MAP = { "M1": "TIMEFRAME_M1", "M5": "TIMEFRAME_M5", "M15": "TIMEFRAME_M15", "M30": "TIMEFRAME_M30", "H1": "TIMEFRAME_H1", "H4": "TIMEFRAME_H4", "D1": "TIMEFRAME_D1", } def fetch(): try: import MetaTrader5 as mt5 except ImportError: sys.exit("[ERROR] MetaTrader5 package not installed.\n" " Run: pip install MetaTrader5") print(f"MetaTrader5 package v{mt5.__version__} by {mt5.__author__}") # ── Initialise ─────────────────────────────────────────────────────────── if not mt5.initialize(): sys.exit(f"[ERROR] mt5.initialize() failed: {mt5.last_error()}") print(f"[MT5] Connected — build {mt5.version()}") # ── Select symbol ──────────────────────────────────────────────────────── if not mt5.symbol_select(SYMBOL, True): mt5.shutdown() sys.exit(f"[ERROR] Symbol '{SYMBOL}' not found in Market Watch.\n" " Check the exact name (e.g. XAUUSD vs XAUUSD.m vs XAUUSDm)") info = mt5.symbol_info(SYMBOL) point = info.point if info else 0.01 print(f"[MT5] Symbol: {SYMBOL} Point: {point} Digits: {info.digits if info else '?'}") # ── Validate timeframe ─────────────────────────────────────────────────── tf_attr = TF_MAP.get(TIMEFRAME.upper()) if tf_attr is None: mt5.shutdown() sys.exit(f"[ERROR] Unknown timeframe '{TIMEFRAME}'. Choose from: {list(TF_MAP)}") tf = getattr(mt5, tf_attr) # ── Build UTC-aware date range ─────────────────────────────────────────── tz = pytz.timezone(TIMEZONE) utc_from = tz.localize(DATE_FROM) utc_to = tz.localize(DATE_TO) print(f"[MT5] Requesting {TIMEFRAME} bars from {utc_from.date()} to {utc_to.date()} ...") # ── Fetch ──────────────────────────────────────────────────────────────── rates = mt5.copy_rates_range(SYMBOL, tf, utc_from, utc_to) mt5.shutdown() if rates is None or len(rates) == 0: sys.exit("[ERROR] No bars returned. Possible causes:\n" " - Date range has no data for this symbol on this broker\n" " - Symbol requires a different name (try without .m suffix)\n" " - MT5 history for this period not downloaded yet\n" " (Open the chart in MT5 and scroll back to force download)") # ── Build DataFrame ────────────────────────────────────────────────────── df = pd.DataFrame(rates) df["time"] = pd.to_datetime(df["time"], unit="s", utc=True) df.set_index("time", inplace=True) # Rename tick_volume -> volume, drop spread column if present df.rename(columns={"tick_volume": "volume", "real_volume": "real_vol"}, inplace=True) keep = [c for c in ["open", "high", "low", "close", "volume"] if c in df.columns] df = df[keep].astype(float) # ── Quality report ─────────────────────────────────────────────────────── n_bars = len(df) date_start = df.index[0].strftime("%Y-%m-%d %H:%M") date_end = df.index[-1].strftime("%Y-%m-%d %H:%M") price_min = df["close"].min() price_max = df["close"].max() atr_proxy = (df["high"] - df["low"]).mean() gaps = df.index.to_series().diff().dropna() expected = gaps.mode()[0] gap_bars = (gaps > expected * 1.5).sum() print(f"\n{'='*55}") print(f" Data Quality Report") print(f"{'='*55}") print(f" Bars fetched : {n_bars:,}") print(f" Date range : {date_start} -> {date_end}") print(f" Price range : {price_min:.2f} -> {price_max:.2f}") print(f" Mean ATR (H-L) : {atr_proxy:.2f}") print(f" Missing bars : {gap_bars:,} (weekend/holiday gaps expected)") print(f"{'='*55}\n") if n_bars < 1000: print(f"[WARN] Only {n_bars} bars fetched. Consider extending the date range.\n" " Training needs at least 5,000+ bars for meaningful results.") # ── Save ───────────────────────────────────────────────────────────────── df.to_csv(OUTPUT_CSV) print(f"[OK] Saved {n_bars:,} bars to '{OUTPUT_CSV}'") print(f" File size: {pd.io.common.get_handle(OUTPUT_CSV,'r').handle.seek(0,2) if False else ''}" f"{round(Path(OUTPUT_CSV).stat().st_size / 1024, 1)} KB") print(f"\nNext step: set csv_file = '{OUTPUT_CSV}' in the training pipeline.") return df # ── Entrypoint ──────────────────────────────────────────────────────────────── from pathlib import Path if __name__ == "__main__": fetch() else: # When imported or run as a Jupyter cell, execute immediately fetch()
This script builds the historical data extraction pipeline used for training the AI trading model. It connects directly to MetaTrader 5 using Python, selects the specified symbol and timeframe, and downloads historical OHLCV market data within a configurable UTC date range. The retrieved data is converted into a clean Pandas DataFrame, indexed by timestamp, and standardized into a consistent format suitable for machine learning. The script also performs a lightweight quality analysis by reporting the total number of bars, price range, average candle volatility, and potential missing-bar gaps caused by weekends or unavailable history.
Output:

import argparse import os import sys import warnings from pathlib import Path import numpy as np import pandas as pd warnings.filterwarnings("ignore") # ============================================================================= # NOTEBOOK CONFIG — edit these values (ignored when run from CLI) # ============================================================================= NOTEBOOK_CONFIG = dict( # ── Data source ────────────────────────────────────────────────────────── # "csv" : load from CSV_FILE produced by fetch_historical_data.py ✅ recommended # "mt5" : pull bars live from MT5 (MT5 must be open) # "synthetic": random-walk data for quick pipeline tests data_source = "csv", csv_file = "XAUUSD_H1.csv", # path to the CSV file # ── MT5 live-pull settings (used only when data_source="mt5") ──────────── symbol = "XAUUSD.m", timeframe = "H1", bars = 50000, # ── Trade parameters — MUST match EA inputs exactly ────────────────────── # XAUUSD point = 0.01, so: # sl_pts=3500 -> SL distance = 3500 × 0.01 = $35.00 # tp_pts=7500 -> TP distance = 7500 × 0.01 = $75.00 sl_pts = 3500, tp_pts = 7500, # ── SMC detection parameters — MUST match EA inputs exactly ───────────── swing_len = 5, # SwingPeriod in EA fvg_min = 3, # FVG_MinPoints in EA fib_lvl = 61.8, # Fib_Trade_lvls in EA # ── Output ─────────────────────────────────────────────────────────────── out_dir = ".", # where to save smc_features.csv (audit log) # Direct path to MT5 Files folder -- model is saved here automatically. # MT5 -> File -> Open Data Folder -> MQL5 -> Files mt5_files_path = r'C:\Users\...\AppData\Roaming\MetaQuotes\Terminal\....\MQL5\Files', ) # ============================================================================= # --------------------------------------------------------------------------- # Symbol point-size lookup (mirrors MQL5 _Point behaviour) # --------------------------------------------------------------------------- SYMBOL_POINT = { "XAUUSD": 0.01, "XAGUSD": 0.001, "USDJPY": 0.001, "EURJPY": 0.001, "GBPJPY": 0.001, "US30": 0.01, "NAS100": 0.01, "SPX500": 0.01, "USTEC": 0.01, } def get_point(symbol: str) -> float: sym = symbol.upper() for key, val in SYMBOL_POINT.items(): if key in sym: return val return 0.00001 # default: standard 5-digit FX
This section establishes the core configuration layer for the entire AI training pipeline. We define how market data will be sourced, whether from a previously exported CSV file, a live MetaTrader 5 connection, or synthetic random-walk data for testing purposes. The configuration also centralizes all strategy parameters that must remain synchronized with the Expert Advisor. These include stop-loss and take-profit distances, swing detection settings, Fair Value Gap sensitivity, and Fibonacci retracement levels. By keeping these values in one structured configuration block, we ensure that the Python training environment and the MQL5 execution logic operate under identical market assumptions.
We also implement a symbol point-size mapping system that mirrors the _Point behavior used inside MQL5. This allows the pipeline to correctly interpret price distances across different instruments such as XAUUSD, indices, and JPY currency pairs. The get_point() helper function automatically determines the correct point precision for the selected symbol. This ensures that SL, TP, feature engineering, and signal reconstruction calculations remain fully consistent with the EA during both training and live execution.
# --------------------------------------------------------------------------- # Config resolution (Jupyter-safe argparse) # --------------------------------------------------------------------------- def get_config(): """ Returns a plain Namespace from NOTEBOOK_CONFIG when inside Jupyter, or from CLI arguments when run from a terminal. """ running_in_jupyter = ( "ipykernel_launcher" in sys.argv[0] or any(a.startswith("--f=") or a == "-f" for a in sys.argv[1:]) ) if running_in_jupyter: print("[Config] Jupyter detected -- using NOTEBOOK_CONFIG.") print("[Config] Edit the NOTEBOOK_CONFIG dict at the top of this file.\n") return argparse.Namespace(**NOTEBOOK_CONFIG) p = argparse.ArgumentParser(description="SMC XGBoost -> ONNX trainer") p.add_argument("--data_source", default="csv", choices=["csv", "mt5", "synthetic"]) p.add_argument("--csv_file", default="XAUUSD_H1.csv") p.add_argument("--symbol", default="XAUUSD.m") p.add_argument("--timeframe", default="H1") p.add_argument("--bars", type=int, default=50000) p.add_argument("--sl_pts", type=int, default=3500) p.add_argument("--tp_pts", type=int, default=7500) p.add_argument("--swing_len", type=int, default=5) p.add_argument("--fvg_min", type=int, default=3) p.add_argument("--fib_lvl", type=float, default=61.8) p.add_argument("--out_dir", default=".") p.add_argument("--mt5_files_path", default="", help="Direct path to MQL5/Files folder") return p.parse_args() # --------------------------------------------------------------------------- # Data loaders # --------------------------------------------------------------------------- def load_csv(csv_file: str) -> pd.DataFrame: """ Load the CSV produced by fetch_historical_data.py. Handles both UTC-aware and naive timestamps. Expected columns: time (index), open, high, low, close, volume """ if not Path(csv_file).exists(): sys.exit( f"[ERROR] CSV file not found: '{csv_file}'\n" " Run fetch_historical_data.py first to generate it." ) df = pd.read_csv(csv_file, index_col=0, parse_dates=True) # Normalise index to UTC-aware if df.index.tz is None: df.index = pd.to_datetime(df.index, utc=True) else: df.index = df.index.tz_convert("UTC") df.index.name = "time" # Keep only OHLCV; rename tick_volume if present df.rename(columns={"tick_volume": "volume"}, inplace=True) required = ["open", "high", "low", "close"] missing = [c for c in required if c not in df.columns] if missing: sys.exit(f"[ERROR] CSV is missing columns: {missing}\n" f" Found columns: {list(df.columns)}") if "volume" not in df.columns: df["volume"] = 1.0 # placeholder if volume absent df = df[["open", "high", "low", "close", "volume"]].astype(float) df.dropna(inplace=True) df.sort_index(inplace=True) date_start = df.index[0].strftime("%Y-%m-%d") date_end = df.index[-1].strftime("%Y-%m-%d") atr_proxy = (df["high"] - df["low"]).mean() print(f"[CSV] Loaded {len(df):,} bars from '{csv_file}'") print(f" Date range : {date_start} -> {date_end}") print(f" Price range: {df['close'].min():.2f} -> {df['close'].max():.2f}") print(f" Mean H-L : {atr_proxy:.4f} ({atr_proxy:.2f} for XAUUSD-style)\n") return df def load_mt5_bars(symbol: str, tf_str: str, n: int) -> pd.DataFrame: try: import MetaTrader5 as mt5 except ImportError: sys.exit("MetaTrader5 package not installed. pip install MetaTrader5") if not mt5.initialize(): sys.exit(f"mt5.initialize() failed: {mt5.last_error()}") tf = getattr(mt5, f"TIMEFRAME_{tf_str}", None) if tf is None: mt5.shutdown() sys.exit(f"Unknown timeframe '{tf_str}'") rates = mt5.copy_rates_from_pos(symbol, tf, 0, n) mt5.shutdown() if rates is None or len(rates) == 0: sys.exit("No data returned from MT5. Check symbol name and date availability.") df = pd.DataFrame(rates) df["time"] = pd.to_datetime(df["time"], unit="s", utc=True) df.rename(columns={"tick_volume": "volume"}, inplace=True) df.set_index("time", inplace=True) df = df[["open", "high", "low", "close", "volume"]].astype(float) print(f"[MT5] Loaded {len(df):,} bars of {symbol} {tf_str}") return df def make_synthetic_bars(n: int = 20000, symbol: str = "XAUUSD", timeframe: str = "H1") -> pd.DataFrame: """Realistic random-walk data — for quick pipeline tests only.""" np.random.seed(42) sym = symbol.upper() freq_map = {"M1":"1min","M5":"5min","M15":"15min","M30":"30min", "H1":"1h","H4":"4h","D1":"1D"} freq = freq_map.get(timeframe, "1h") dates = pd.date_range("2020-01-01", periods=n, freq=freq, tz="UTC") if "XAU" in sym: base, step, noise, wick = 1900.0, 0.80, 2.50, 4.00 elif "XAG" in sym: base, step, noise, wick = 24.0, 0.05, 0.15, 0.25 elif "JPY" in sym: base, step, noise, wick = 130.0, 0.05, 0.15, 0.25 elif "US30" in sym: base, step, noise, wick = 34000., 50., 120., 200. else: base, step, noise, wick = 1.1, 3e-4, 5e-4, 8e-4 close = base + np.cumsum(np.random.normal(0, step, n)) close = np.maximum(close, base * 0.5) body = np.abs(np.random.normal(0, noise, n)) wu = np.abs(np.random.normal(0, wick, n)) wd = np.abs(np.random.normal(0, wick, n)) direction = np.sign(np.random.normal(0, 1, n)) open_ = close - direction * body high = np.maximum(close, open_) + wu low = np.minimum(close, open_) - wd volume = np.random.randint(500, 8000, n).astype(float) df = pd.DataFrame({"open": open_, "high": high, "low": low, "close": close, "volume": volume}, index=dates) print(f"[Synth] Generated {n:,} synthetic {symbol} {timeframe} bars " f"({close.min():.2f} – {close.max():.2f})") return df
This section builds the configuration and data-loading infrastructure for the AI training pipeline. We first implement a Jupyter-safe configuration resolver that automatically detects whether the script is running inside a notebook environment or from the command line. When executed in Jupyter, the pipeline loads all parameters directly from the NOTEBOOK_CONFIG dictionary, making experimentation and debugging easier. When launched from a terminal, the system switches to argparse and accepts runtime parameters such as symbol selection, timeframe, stop-loss settings, Fibonacci levels, output paths, and data source configuration. This dual-mode design allows the same training pipeline to operate consistently across notebooks, scripts, and automated workflows without requiring code modifications.
We then implement multiple market-data loaders to support flexible dataset generation. The load_csv() function imports previously exported historical OHLCV data, validates the required columns, normalizes timestamps to UTC, cleans missing values, and produces a standardized DataFrame ready for feature engineering. The load_mt5_bars() function connects directly to MetaTrader 5 and retrieves live historical bars from the terminal, enabling rapid retraining without relying on external CSV files. Finally, the make_synthetic_bars() function generates realistic random-walk market data for quick pipeline testing and debugging when real market data is unavailable. Together, these loaders create a unified data ingestion layer that ensures the training environment remains stable, consistent, and adaptable across different development scenarios.
# --------------------------------------------------------------------------- # Technical indicators (no TA-Lib dependency) # --------------------------------------------------------------------------- def rsi(series: pd.Series, period: int = 14) -> pd.Series: delta = series.diff() gain = delta.clip(lower=0).ewm(com=period-1, adjust=False).mean() loss = (-delta.clip(upper=0)).ewm(com=period-1, adjust=False).mean() return (100 - 100 / (1 + gain / loss.replace(0, np.nan))).fillna(50) def atr(df: pd.DataFrame, period: int = 14) -> pd.Series: tr = pd.concat([ df["high"] - df["low"], (df["high"] - df["close"].shift(1)).abs(), (df["low"] - df["close"].shift(1)).abs(), ], axis=1).max(axis=1) return tr.ewm(com=period-1, adjust=False).mean() def adx(df: pd.DataFrame, period: int = 14) -> pd.Series: up, down = df["high"].diff(), -df["low"].diff() pdm = up.where((up > down) & (up > 0), 0.0) ndm = down.where((down > up) & (down > 0), 0.0) tr_s = atr(df, period) pdi = 100 * pdm.ewm(com=period-1, adjust=False).mean() / tr_s.replace(0, np.nan) ndi = 100 * ndm.ewm(com=period-1, adjust=False).mean() / tr_s.replace(0, np.nan) dx = (100 * (pdi - ndi).abs() / (pdi + ndi).replace(0, np.nan)).fillna(0) return dx.ewm(com=period-1, adjust=False).mean().fillna(20) # --------------------------------------------------------------------------- # SMC signal detection (mirrors EA logic exactly) # --------------------------------------------------------------------------- SIGNAL_OB = 0 SIGNAL_FVG = 1 SIGNAL_BOS = 2 def _is_swing_high(high: np.ndarray, idx: int, length: int) -> bool: n = len(high) for i in range(1, length + 1): l, r = idx + i, idx - i if r < 0: return False if high[idx] <= high[r]: return False if l < n and high[idx] < high[l]: return False return True def _is_swing_low(low: np.ndarray, idx: int, length: int) -> bool: n = len(low) for i in range(1, length + 1): l, r = idx + i, idx - i if r < 0: return False if low[idx] >= low[r]: return False if l < n and low[idx] > low[l]: return False return True def detect_signals(df: pd.DataFrame, args) -> pd.DataFrame: point = get_point(args.symbol) sw_len = args.swing_len fvg_min = args.fvg_min * point H = df["high"].values L = df["low"].values O = df["open"].values C = df["close"].values n = len(df) events = [] # ── Order Blocks ────────────────────────────────────────────────────────── for i in range(4, n - 4): # Bullish OB: bearish candle at [i+3] before bullish impulse if (O[i+3] > C[i+3] and O[i+2] < C[i+2] and O[i] < C[i] and O[i+3] < C[i+2]): events.append(dict(bar_idx=i, signal_type=SIGNAL_OB, direction=1, zone_high=H[i+3], zone_low=L[i+3], entry_price=(H[i+3]+L[i+3])/2)) # Bearish OB: bullish candle at [i+3] before bearish impulse if (O[i+3] < C[i+3] and O[i+2] > C[i+2] and O[i] > C[i] and O[i+3] > C[i+2]): events.append(dict(bar_idx=i, signal_type=SIGNAL_OB, direction=-1, zone_high=H[i+3], zone_low=L[i+3], entry_price=(H[i+3]+L[i+3])/2)) # ── Fair Value Gaps ─────────────────────────────────────────────────────── for i in range(2, n - 2): la, ha = L[i+2], H[i+2] hc, lc = H[i], L[i] if la > hc and (la - hc) >= fvg_min: events.append(dict(bar_idx=i, signal_type=SIGNAL_FVG, direction=1, zone_high=la, zone_low=hc, entry_price=(la+hc)/2)) elif ha < lc and (lc - ha) >= fvg_min: events.append(dict(bar_idx=i, signal_type=SIGNAL_FVG, direction=-1, zone_high=lc, zone_low=ha, entry_price=(lc+ha)/2)) # ── Break of Structure ──────────────────────────────────────────────────── sh_list = [i for i in range(sw_len, n-sw_len) if _is_swing_high(H, i, sw_len)] sl_list = [i for i in range(sw_len, n-sw_len) if _is_swing_low(L, i, sw_len)] for sl_idx in sl_list: # BOS Buy: break below swing low for j in range(sl_idx+1, min(sl_idx+50, n)): if L[j] < L[sl_idx]: events.append(dict(bar_idx=j, signal_type=SIGNAL_BOS, direction=1, zone_high=L[sl_idx]+fvg_min, zone_low =L[sl_idx]-fvg_min, entry_price=L[sl_idx])) break for sh_idx in sh_list: # BOS Sell: break above swing high for j in range(sh_idx+1, min(sh_idx+50, n)): if H[j] > H[sh_idx]: events.append(dict(bar_idx=j, signal_type=SIGNAL_BOS, direction=-1, zone_high=H[sh_idx]+fvg_min, zone_low =H[sh_idx]-fvg_min, entry_price=H[sh_idx])) break sig_df = (pd.DataFrame(events) .drop_duplicates(subset=["bar_idx","signal_type","direction"]) .sort_values("bar_idx") .reset_index(drop=True)) print(f"[SMC] Detected {len(sig_df):,} raw signal events " f"(OB={(sig_df.signal_type==SIGNAL_OB).sum()}, " f"FVG={(sig_df.signal_type==SIGNAL_FVG).sum()}, " f"BOS={(sig_df.signal_type==SIGNAL_BOS).sum()})") return sig_df # --------------------------------------------------------------------------- # Labeller: walk-forward TP/SL simulation # --------------------------------------------------------------------------- def label_events(df: pd.DataFrame, sig_df: pd.DataFrame, args) -> pd.DataFrame: """ Simulate each detected signal forward bar-by-bar. Label = 1 if TP is hit first, 0 if SL is hit first. Events where neither is hit within 2000 bars are dropped (expired). SL / TP are in EA 'points' — converted to price distance via get_point(). """ point = get_point(args.symbol) sl_dist = args.sl_pts * point tp_dist = args.tp_pts * point H = df["high"].values L = df["low"].values n_bars = len(df) LOOKFWD = 2000 print(f"[Label] Symbol: {args.symbol} Point: {point}") print(f"[Label] SL: {args.sl_pts} pts = {sl_dist:.4f} | " f"TP: {args.tp_pts} pts = {tp_dist:.4f} | Lookahead: {LOOKFWD} bars") # Auto-scale safety net: if SL > 10× mean bar range, trades will rarely resolve mean_hl = float((df["high"] - df["low"]).mean()) if sl_dist > 10 * mean_hl: sl_dist = round(2.0 * mean_hl, 5) tp_dist = round(4.0 * mean_hl, 5) print(f"[Label] WARN: SL/TP too large for data volatility.") print(f"[Label] Auto-scaled -> SL={sl_dist:.4f} TP={tp_dist:.4f} (2x/4x mean H-L)") print(f"[Label] NOTE: This only applies to synthetic data. " "Real CSV data will use your exact sl_pts/tp_pts.") labels = [] for _, row in sig_df.iterrows(): entry = float(row["entry_price"]) start = int(row["bar_idx"]) d = int(row["direction"]) sl_price = entry - d * sl_dist tp_price = entry + d * tp_dist outcome = np.nan for j in range(start + 1, min(start + LOOKFWD, n_bars)): if d == 1: # buy if L[j] <= sl_price: outcome = 0; break if H[j] >= tp_price: outcome = 1; break else: # sell if H[j] >= sl_price: outcome = 0; break if L[j] <= tp_price: outcome = 1; break labels.append(outcome) result = sig_df.copy() result["label"] = labels result.dropna(subset=["label"], inplace=True) result["label"] = result["label"].astype(int) expired = len(sig_df) - len(result) win_rate = result["label"].mean() rr = args.tp_pts / args.sl_pts print(f"\n[Label] Results:") print(f" Total signals : {len(sig_df):,}") print(f" Labelled : {len(result):,} ({100*len(result)/len(sig_df):.1f}% resolved)") print(f" Expired (NaN) : {expired:,}") print(f" Win rate : {win_rate:.1%}") print(f" TP hits : {result['label'].sum():,}") print(f" SL hits : {(result['label']==0).sum():,}") print(f" Risk:Reward : 1:{rr:.2f}") print(f" Expected win% : {100/(1+rr):.1f}% (random baseline)\n") return result
We implement a full, feature-rich market analysis layer that first calculates core technical indicators—RSI, ATR, and ADX—using pure NumPy/Pandas formulas without external dependencies. These indicators help quantify momentum, volatility, and trend strength in a way that is consistent with how the trading system interprets market structure. On top of this, we build an SMC signal engine that mirrors the EA logic, detecting OB, FVG, and BOS events by scanning historical price action and identifying swing highs and lows based on configurable structure length.
We then simulate how each detected signal would perform in real market conditions through a forward-looking labeling engine. For every event, we project price movement bar-by-bar to determine whether take-profit or stop-loss is reached first, using EA-aligned point-based distance conversions. Signals that do not resolve within a fixed lookahead window are discarded as expired, ensuring only meaningful outcomes are kept. Finally, we output a clean, labeled dataset along with diagnostic statistics such as win rate, resolution rate, and risk-to-reward ratio, forming the foundation for training the machine learning model.
# --------------------------------------------------------------------------- # Feature engineering (12 features — must match MQL5 RunONNX exactly) # --------------------------------------------------------------------------- FEATURE_COLS = [ "f_signal_ob", # one-hot: is OB signal? "f_signal_fvg", # one-hot: is FVG signal? "f_signal_bos", # one-hot: is BOS signal? "f_direction", # +1 bullish / -1 bearish "f_zone_width_atr", # zone width normalised by ATR(14) "f_dist_to_zone_atr", # |entry - zone midpoint| / ATR(14) "f_fib_pct", # retrace depth inside zone [0,1] "f_session_sin", # hour-of-day sin encoding "f_session_cos", # hour-of-day cos encoding "f_rsi14", # RSI(14) / 100 "f_adx14", # ADX(14) / 100 "f_spread_norm", # H-L range / ATR(14) (spread proxy) ] def build_features(df: pd.DataFrame, sig_df: pd.DataFrame) -> pd.DataFrame: atr14 = atr(df, 14) rsi14 = rsi(df["close"], 14) adx14 = adx(df, 14) hours = df.index.hour rows = [] for _, row in sig_df.iterrows(): i = max(0, min(int(row["bar_idx"]), len(df)-1)) atr_v = max(float(atr14.iloc[i]), 1e-8) zw = row["zone_high"] - row["zone_low"] zmid = (row["zone_high"] + row["zone_low"]) / 2 hour = int(hours[i]) rows.append({ "f_signal_ob": int(row["signal_type"] == SIGNAL_OB), "f_signal_fvg": int(row["signal_type"] == SIGNAL_FVG), "f_signal_bos": int(row["signal_type"] == SIGNAL_BOS), "f_direction": float(row["direction"]), "f_zone_width_atr": float(np.clip(zw / atr_v, 0, 10)), "f_dist_to_zone_atr": float(np.clip(abs(row["entry_price"] - zmid) / atr_v, 0, 10)), "f_fib_pct": float(np.clip(abs(row["entry_price"] - row["zone_low"]) / max(zw, 1e-8), 0, 1)), "f_session_sin": float(np.sin(2 * np.pi * hour / 24)), "f_session_cos": float(np.cos(2 * np.pi * hour / 24)), "f_rsi14": float(rsi14.iloc[i]) / 100.0, "f_adx14": float(np.clip(adx14.iloc[i], 0, 100)) / 100.0, "f_spread_norm": float(np.clip((df["high"].iloc[i] - df["low"].iloc[i]) / atr_v, 0, 5)), }) feat_df = pd.DataFrame(rows, columns=FEATURE_COLS, index=sig_df.index) feat_df["label"] = sig_df["label"].values feat_df.dropna(inplace=True) print(f"[Feat] Feature matrix: {feat_df.shape[0]:,} rows x {len(FEATURE_COLS)} features") return feat_df # --------------------------------------------------------------------------- # Training # --------------------------------------------------------------------------- def train(feat_df: pd.DataFrame, args): from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.metrics import classification_report, roc_auc_score from xgboost import XGBClassifier X = feat_df[FEATURE_COLS].values.astype(np.float32) y = feat_df["label"].values.astype(int) pos = int(y.sum()) neg = int(len(y) - pos) w = neg / max(pos, 1) print(f"[Train] TP hits: {pos:,} SL hits: {neg:,} " f"scale_pos_weight: {w:.2f}") if len(np.unique(y)) < 2: print("[Train] WARNING: only one outcome class — model will be trivial.") print("[Train] Fitting anyway for ONNX export. Re-check sl_pts / tp_pts.") pipe = Pipeline([("scaler", StandardScaler()), ("xgb", XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False, eval_metric="logloss"))]) pipe.fit(X, y) return pipe xgb = XGBClassifier( n_estimators = 400, max_depth = 5, learning_rate = 0.04, subsample = 0.8, colsample_bytree = 0.8, min_child_weight = 3, gamma = 0.1, scale_pos_weight = w, eval_metric = "logloss", use_label_encoder = False, random_state = 42, n_jobs = -1, ) pipeline = Pipeline([("scaler", StandardScaler()), ("xgb", xgb)]) n_splits = min(5, pos, neg) cv = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=42) auc = cross_val_score(pipeline, X, y, cv=cv, scoring="roc_auc", n_jobs=-1) print(f"\n[Train] {n_splits}-Fold CV ROC-AUC: {auc.mean():.4f} +/- {auc.std():.4f}") # Interpretation guide printed alongside results if auc.mean() >= 0.70: auc_comment = "Good — model adds real edge" elif auc.mean() >= 0.62: auc_comment = "Fair — some predictive signal" elif auc.mean() >= 0.55: auc_comment = "Weak — borderline useful" else: auc_comment = "Poor — near random; check data quality" print(f" Interpretation : {auc_comment}") pipeline.fit(X, y) y_pred = pipeline.predict(X) y_prob = pipeline.predict_proba(X)[:, 1] labels_present = sorted(np.unique(y)) tgt = ["SL/Expire","TP Hit"] if len(labels_present)==2 else [str(l) for l in labels_present] print("\n[Train] In-sample classification report:") print(classification_report(y, y_pred, target_names=tgt, labels=labels_present)) print(f"[Train] In-sample ROC-AUC : {roc_auc_score(y, y_prob):.4f} " "(will be higher than CV — expected due to overfitting)") imp = (pd.DataFrame({"feature": FEATURE_COLS, "importance": xgb.feature_importances_}) .sort_values("importance", ascending=False)) print("\n[Train] Feature importances (higher = more useful to the model):") print(imp.to_string(index=False)) return pipeline
We construct a structured feature engineering layer that converts raw SMC signals into a fixed 12-dimensional numerical representation. This format is fully aligned with the MQL5 ONNX inference structure. Each signal is encoded using a mix of structural and contextual information. This includes signal type (OB, FVG, BOS), direction, zone geometry normalized by ATR, Fibonacci retracement position, and distance-to-zone metrics.
We also include broader market context features to improve model awareness. Session timing is encoded using sine and cosine transformations. RSI and ADX values are normalized, and volatility is captured through a spread-to-ATR ratio. We then train an XGBoost classifier to predict whether each signal results in TP or SL. The process includes class balancing, cross-validation, and ROC-AUC evaluation. Finally, we produce a trained pipeline with clear feature importance and export readiness for ONNX deployment in the MQL5 Expert Advisor.
# --------------------------------------------------------------------------- # ONNX export + verification # --------------------------------------------------------------------------- def export_onnx(pipeline, out_dir: str, mt5_files_path: str = '') -> str: from skl2onnx import convert_sklearn, update_registered_converter from skl2onnx.common.data_types import FloatTensorType from skl2onnx.common.shape_calculator import calculate_linear_classifier_output_shapes import onnxruntime as rt try: from xgboost import XGBClassifier as _XGB from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost update_registered_converter( _XGB, "XGBoostXGBClassifier", calculate_linear_classifier_output_shapes, convert_xgboost, options={"nocl": [True, False], "zipmap": [True, False, "columns"]}, ) except ImportError: sys.exit("[ERROR] onnxmltools required.\n pip install onnxmltools") n_feat = len(FEATURE_COLS) onnx_model = convert_sklearn( pipeline, initial_types=[("float_input", FloatTensorType([None, n_feat]))], options={"zipmap": False}, target_opset={"": 15, "ai.onnx.ml": 3}, ) model_bytes = onnx_model.SerializeToString() # Always save a local copy out_path = os.path.join(out_dir, "smc_filter.onnx") with open(out_path, "wb") as f: f.write(model_bytes) print(f"\n[ONNX] Local copy saved -> {out_path}") print(f" File size : {Path(out_path).stat().st_size / 1024:.1f} KB") # Save directly into MT5 Files folder if mt5_files_path and mt5_files_path.strip(): mt5_dir = Path(mt5_files_path.strip()) if mt5_dir.exists(): mt5_dest = mt5_dir / "smc_filter.onnx" with open(mt5_dest, "wb") as f: f.write(model_bytes) print(f"[ONNX] MT5 copy saved -> {mt5_dest}") else: print(f"[ONNX] WARNING: mt5_files_path not found: {mt5_dir}") print(f" Copy \"{out_path}\" to MQL5/Files/ manually.") else: print("[ONNX] mt5_files_path not set -- copy to MQL5/Files/ manually.") # Verify inference works sess = rt.InferenceSession(out_path, providers=["CPUExecutionProvider"]) dummy = np.random.rand(1, n_feat).astype(np.float32) in_name = sess.get_inputs()[0].name lbl_name = sess.get_outputs()[0].name prb_name = sess.get_outputs()[1].name lbl, prb = sess.run([lbl_name, prb_name], {in_name: dummy}) print(f"[ONNX] Verification -> label: {lbl} prob: {prb}") print(f"[ONNX] Input shape : {sess.get_inputs()[0].shape}") print(f"[ONNX] Output names : {[o.name for o in sess.get_outputs()]}") mt5_path = r"%APPDATA%\MetaQuotes\Terminal\<ID>\MQL5\Files\smc_filter.onnx" print(f"\n{'='*55}") print(f" Next steps:") print(f" 1. Copy '{out_path}'") print(f" -> {mt5_path}") print(f" (Open MT5: File -> Open Data Folder to find <ID>)") print(f" 2. Compile SMC_AI_Filter.mq5 in MetaEditor") print(f" 3. Attach EA to XAUUSD H1 chart") print(f" 4. Run Strategy Tester on the same date range as your CSV") print(f"{'='*55}\n") return out_path # --------------------------------------------------------------------------- # Audit CSV # --------------------------------------------------------------------------- def save_report(feat_df: pd.DataFrame, out_dir: str): path = os.path.join(out_dir, "smc_features.csv") feat_df.to_csv(path, index=False) print(f"[Report] Labelled feature matrix saved -> {path}") print(f" ({len(feat_df):,} rows — open in Excel to inspect signal quality)") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main(): cfg = get_config() Path(cfg.out_dir).mkdir(parents=True, exist_ok=True) print("=" * 55) print(" SMC AI Filter -- XGBoost Training Pipeline") print(f" Data source : {cfg.data_source.upper()}") print(f" Symbol : {cfg.symbol} | TF: {cfg.timeframe}") print(f" Point size : {get_point(cfg.symbol)}") print(f" SL: {cfg.sl_pts} pts | TP: {cfg.tp_pts} pts") print("=" * 55 + "\n") # 1. Load data src = cfg.data_source if src == "csv": df = load_csv(cfg.csv_file) elif src == "mt5": df = load_mt5_bars(cfg.symbol, cfg.timeframe, cfg.bars) elif src == "synthetic": df = make_synthetic_bars( getattr(cfg, "bars", 20000), cfg.symbol, cfg.timeframe) else: sys.exit(f"[ERROR] Unknown data_source '{src}'. Choose: csv | mt5 | synthetic") if len(df) < 500: sys.exit(f"[ERROR] Only {len(df)} bars loaded — need at least 500.") # 2. Detect SMC signals sig_df = detect_signals(df, cfg) if len(sig_df) == 0: sys.exit("[ERROR] No signals detected. Check symbol/data settings.") # 3. Label TP / SL outcomes sig_df = label_events(df, sig_df, cfg) if len(sig_df) < 100: sys.exit( f"[ERROR] Only {len(sig_df)} labelled events (need >= 100).\n" " - Extend the date range in fetch_historical_data.py\n" " - Check that sl_pts / tp_pts match the actual price scale" ) # 4. Feature engineering feat_df = build_features(df, sig_df) # 5. Train pipeline = train(feat_df, cfg) # 6. Export ONNX export_onnx(pipeline, cfg.out_dir, getattr(cfg, 'mt5_files_path', '')) # 7. Audit report save_report(feat_df, cfg.out_dir) print("Pipeline complete.") if __name__ == "__main__": main()
Here, we implement an ONNX export and verification stage that converts the trained XGBoost pipeline into a format compatible with MetaTrader 5. First, we register the XGBoost converter and transform the Scikit-learn pipeline into an ONNX graph. The input shape is fixed to match the 12 engineered features. The model is then serialized and saved locally. It can also be copied directly into the MetaTrader 5 MQL5/Files directory for immediate use by the Expert Advisor.
After export, we verify the model using ONNX Runtime. A dummy input is passed through the model to confirm that the input shape, output labels, and probability scores are correct. This ensures the model is fully functional before deployment. We then define the main pipeline that controls the full workflow. We load the dataset, detect SMC signals, and label each event using TP and SL simulation. Next, we convert signals into feature vectors and train the XGBoost model. Finally, we export the model to ONNX and generate an audit report, completing a full end-to-end AI trading system ready for MetaTrader 5 integration.
Output:


MQL5 Integration
//+------------------------------------------------------------------+ //| SMC AI.mq5 | //| SMC EA with XGBoost ONNX inference, zone visualisation | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/johnhlomohang/" #property version "1.00" #resource "\\Files\\SMConnx\\smc_filter.onnx" as uchar ExtModel[] #include <Trade/Trade.mqh> #include <Trade/PositionInfo.mqh> CTrade trade; CPositionInfo pos; //+------------------------------------------------------------------+ //| ENUMS | //+------------------------------------------------------------------+ enum ENUM_STRATEGY { STRAT_OB, STRAT_FVG, STRAT_BOS, STRAT_AUTO }; enum ENUM_SWING_TYPE { SWING_OB, SWING_BOS }; enum ENUM_TREND_STR { TS_WEAK, TS_MEDIUM, TS_STRONG }; //+------------------------------------------------------------------+ //| INPUTS | //+------------------------------------------------------------------+ input group "═══ Strategy ═══" input ENUM_STRATEGY TradeStrategy = STRAT_AUTO; input double In_Lot = 0.02; input long MagicNumber = 76543; input group "═══ SL / TP ═══" enum ENUM_SLTP_MODE { PRICE_DIST, ATR_MULT }; input ENUM_SLTP_MODE SL_Mode = PRICE_DIST; input double StopLoss_Dist = 20.0; // SL in price units ($) input double TakeProfit_Dist = 50.0; // TP in price units ($) //--- ATR multiplier (used when SL_Mode = ATR_MULT) --- input double SL_ATR_Mult = 1.5; // SL = ATR(14) × this input double TP_ATR_Mult = 3.0; // TP = ATR(14) × this //--- Trailing stop --- input bool Trail_SL = true; input double Trail_TriggerDist = 10.0; // profit ($) needed to start trailing input double Trail_StepDist = 5.0; // trail step ($) — SL moves in this increment input group "═══ SMC Parameters ═══" input int SwingPeriod = 5; // bars each side for swing input double Fib_Trade_lvls = 61.8; // OB fib retrace % input bool DrawBOSLines = true; input int FVG_MinPoints = 3; input int FVG_ScanBars = 20; input bool FVG_TradeAtEQ = true; input bool OneTradePerBar = true; input group "═══ ONNX AI Filter ═══" input bool UseAI = true; input double AI_MinScore = 0.60; // min probability to allow trade input double AI_StrongScore = 0.80; // threshold for "Strong" trend rating input double AI_MediumScore = 0.65; // threshold for "Medium" trend rating input group "═══ Panel ═══" input int PanelX = 20; input int PanelY = 30; input color PanelBG = C'30,30,40'; input color PanelText = clrSilver; input color PanelAccent = clrDodgerBlue; //+------------------------------------------------------------------+ //| CONSTANTS | //+------------------------------------------------------------------+ #define CLR_BULL_OB clrLimeGreen #define CLR_BEAR_OB clrTomato #define CLR_BULL_FVG clrDodgerBlue #define CLR_BEAR_FVG clrOrange #define CLR_BOS_BULL clrDodgerBlue #define CLR_BOS_BEAR clrTomato #define N_FEATURES 12 //+------------------------------------------------------------------+ //| GLOBALS | //+------------------------------------------------------------------+ double Bid, Ask; datetime g_lastBarTime = 0; //--- ONNX long g_onnx_model = INVALID_HANDLE; float g_ai_score = 0.0f; //--- Panel state string g_panel_signal = "—"; string g_panel_origin = "—"; ENUM_TREND_STR g_trend_str = TS_WEAK; //--- OB state struct SOrderBlock { int direction; datetime time; double high, low; }; SOrderBlock g_OB; bool g_OB_valid = false; datetime g_lastOBTradeTime = 0; //--- OB fib scratch double fib_high, fib_low; datetime fib_t1, fib_t2; //--- BOS state double swng_High = -1.0, swng_Low = -1.0; datetime bos_tH = 0, bos_tL = 0; bool Bull_BOS_traded = false, Bear_BOS_traded = false; datetime lastBOSTradeTime = 0; int lastBOSTradeDirection = 0; //--- FVG trade guard datetime lastFVGTradeBar = 0;
We begin by defining the enums and inputs that control how the EA behaves. The ENUM_STRATEGY enum lets the trader choose between running OB, FVG, BOS, or all three at once through the STRAT_AUTO option. The ENUM_SLTP_MODE enum gives the choice between a fixed dollar-based stop and take-profit or one that scales automatically with ATR. We also declare all global variables here, including the ONNX model handle, the panel display state, and the individual trackers for each SMC concept.
The #resource directive at the very top is what embeds the trained ONNX model into the compiled EA. Once compiled, the model travels inside the .ex5 file and requires no external file at runtime. This is what makes the EA portable across machines and compatible with the Strategy Tester without any path errors.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { if(!InitONNX()) return INIT_FAILED; trade.SetExpertMagicNumber(MagicNumber); CreatePanel(); UpdatePanel(); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { DeinitONNX(); DestroyPanel(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Trail SL runs on every tick so it reacts immediately to price movement. ManageTrailingSL(); //--- Signal detection and trade entry only on new bar open. if(!IsNewBar()) return; if(TradeStrategy == STRAT_FVG || TradeStrategy == STRAT_AUTO) DetectAndDrawFVGs(); if(TradeStrategy == STRAT_OB || TradeStrategy == STRAT_AUTO) DetectAndDrawOrderBlocks(); if(TradeStrategy == STRAT_BOS || TradeStrategy == STRAT_AUTO) DetectAndDrawBOS(); }
We set up the EA in OnInit by loading the ONNX model, registering the magic number, and drawing the trade panel on the chart. If the model fails to load, we return INIT_FAILED immediately so the EA does not run in a broken state. On removal, OnDeinit releases the model handle and cleans up all panel objects. In OnTick, we separate two concerns deliberately. The trailing stop runs on every tick because it needs to react to price movement in real time. Signal detection and trade entry, however, only run on a new bar open. This prevents the EA from firing multiple entries within the same candle and keeps the logic aligned with how SMC signals are structured.
//+------------------------------------------------------------------+ //| HELPERS — price access | //+------------------------------------------------------------------+ double getHigh(int i) { return iHigh(_Symbol, _Period, i); } double getLow(int i) { return iLow(_Symbol, _Period, i); } double getOpen(int i) { return iOpen(_Symbol, _Period, i); } double getClose(int i) { return iClose(_Symbol, _Period, i); } datetime getTime(int i) { return iTime(_Symbol, _Period, i); } bool IsNewBar() { datetime t = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE); if(g_lastBarTime == 0) { g_lastBarTime = t; return false; } if(g_lastBarTime != t) { g_lastBarTime = t; return true; } return false; }
We define a set of small helper functions to access OHLC values by bar index. These wrap the standard MQL5 series functions and keep the rest of the code readable. Rather than repeating iHigh(_Symbol, _Period, i) throughout the EA, we call getHigh(i) instead. The IsNewBar function compares the last known bar time against the current one. When they differ, it updates the stored time and returns true. This gives us a clean and reliable way to gate all signal logic to bar open without relying on tick counters or timers.
//+------------------------------------------------------------------+ //| POSITION HELPERS | //+------------------------------------------------------------------+ bool HasOpenPosition() { for(int i = PositionsTotal() - 1; i >= 0; i--) { if(pos.SelectByIndex(i)) if(pos.Symbol() == _Symbol && pos.Magic() == MagicNumber) return true; } return false; } //+------------------------------------------------------------------+ //| MANAGE TRAILING SL | //+------------------------------------------------------------------+ void ManageTrailingSL() { if(!Trail_SL) return; for(int i = PositionsTotal() - 1; i >= 0; i--) { if(!pos.SelectByIndex(i)) continue; if(pos.Symbol() != _Symbol || pos.Magic() != MagicNumber) continue; double entry = pos.PriceOpen(); double current_sl = pos.StopLoss(); double tp = pos.TakeProfit(); ulong ticket = pos.Ticket(); Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); double new_sl = 0; if(pos.PositionType() == POSITION_TYPE_BUY) { double profit_dist = Bid - entry; if(profit_dist < Trail_TriggerDist) continue; // not in profit enough yet //--- Trail: SL follows Bid minus one step new_sl = NormalizeDouble(Bid - Trail_StepDist, _Digits); //--- Only move SL if it improves (moves up) if(new_sl <= current_sl) continue; } else // SELL { double profit_dist = entry - Ask; if(profit_dist < Trail_TriggerDist) continue; //--- Trail: SL follows Ask plus one step new_sl = NormalizeDouble(Ask + Trail_StepDist, _Digits); //--- Only move SL if it improves (moves down) if(new_sl >= current_sl) continue; } //--- Validate new SL respects broker minimum stop distance double min_stop = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; double price_ref = (pos.PositionType() == POSITION_TYPE_BUY) ? Bid : Ask; if(MathAbs(price_ref - new_sl) < min_stop) continue; trade.PositionModify(ticket, new_sl, tp); Print("[Trail] SL moved to ", DoubleToString(new_sl, _Digits), " (profit dist: ", DoubleToString((pos.PositionType()==POSITION_TYPE_BUY) ? Bid - entry : entry - Ask, 2), ")"); } }
We use HasOpenPosition to check whether the EA already has a live trade on this symbol before entering any new one. It loops through all open positions and matches by both symbol and magic number. This is what enforces the one-trade-per-signal rule across all three SMC strategies.
The ManageTrailingSL function runs on every tick and manages the open position independently of signal logic. Once the trade's profit distance exceeds Trail_TriggerDist, the stop-loss begins following price by Trail_StepDist increments. The SL is moved only in the direction of profit and is never widened. We also validate the new SL against the broker's minimum stop level before sending the modify request.
//+------------------------------------------------------------------+ //| SL / TP CALCULATOR | //+------------------------------------------------------------------+ void CalcSLTP(double &sl_dist, double &tp_dist) { if(SL_Mode == ATR_MULT) { //--- ATR(14) — simple average of last 14 true ranges double atr = 0; for(int k = 1; k <= 14; k++) { double hl = getHigh(k) - getLow(k); double hpc = MathAbs(getHigh(k) - getClose(k+1)); double lpc = MathAbs(getLow(k) - getClose(k+1)); atr += MathMax(hl, MathMax(hpc, lpc)); } atr /= 14.0; if(atr <= 0) atr = 10.0 * _Point; // fallback sl_dist = atr * SL_ATR_Mult; tp_dist = atr * TP_ATR_Mult; } else // PRICE_DIST — direct dollar/price distance { sl_dist = StopLoss_Dist; tp_dist = TakeProfit_Dist; } //--- Safety floor: SL must be at least 5× the spread / minimum stop level double min_stop = (double)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL) * _Point; double spread = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID); double floor = MathMax(min_stop, spread * 3.0); if(sl_dist < floor) sl_dist = floor; if(tp_dist < floor) tp_dist = floor; }
We centralize all stop and take-profit sizing inside CalcSLTP. When SL_Mode is set to PRICE_DIST, the function returns the fixed dollar values set in the inputs. When set to ATR_MULT, it calculates the 14-period ATR and multiplies it by the input factors. Both outputs are then checked against a safety floor derived from the broker's minimum stop level and the current spread. Having a single function handle this means that any change to SL or TP sizing automatically applies to OB, FVG, and BOS trades equally. Nothing is hardcoded in the execution function itself.
//+------------------------------------------------------------------+ //| ONNX INIT / DEINIT | //+------------------------------------------------------------------+ bool InitONNX() { if(!UseAI) return true; //--- Load from compiled resource (embedded at compile time via #resource) g_onnx_model = OnnxCreateFromBuffer(ExtModel, ONNX_DEFAULT); if(g_onnx_model == INVALID_HANDLE) { Print("[AI] ONNX load failed (", GetLastError(), "). " "Ensure smc_filter.onnx was in MQL5/Files/ when you compiled."); return false; } //--- Dynamic shapes: input [1 × N_FEATURES], output [1 × 2] (class label + proba) ulong in_shape[] = {1, N_FEATURES}; ulong out_shape[] = {1}; // predicted label ulong prb_shape[] = {1, 2}; // [prob_0, prob_1] if(!OnnxSetInputShape(g_onnx_model, 0, in_shape)) { Print("[AI] OnnxSetInputShape failed"); return false; } if(!OnnxSetOutputShape(g_onnx_model, 0, out_shape)) { Print("[AI] OnnxSetOutputShape[0] failed"); return false; } if(!OnnxSetOutputShape(g_onnx_model, 1, prb_shape)) { Print("[AI] OnnxSetOutputShape[1] failed"); return false; } Print("[AI] ONNX model loaded from compiled resource. Features: ", N_FEATURES); return true; } void DeinitONNX() { if(g_onnx_model != INVALID_HANDLE) { OnnxRelease(g_onnx_model); g_onnx_model = INVALID_HANDLE; } } //+------------------------------------------------------------------+ //| ONNX INFERENCE | //+------------------------------------------------------------------+ float RunONNX(int signal_type, // SIGNAL_OB=0 FVG=1 BOS=2 int direction, // +1 bull / -1 bear double zone_high, double zone_low, double entry_price) { if(!UseAI || g_onnx_model == INVALID_HANDLE) return 1.0f; // pass-through double atr14 = 0.0; { //--- Compute ATR(14) as price distance (same units as SL/TP dollars) double sumTR = 0; for(int k = 1; k <= 14; k++) { double hl = getHigh(k) - getLow(k); double hpc = MathAbs(getHigh(k) - getClose(k+1)); double lpc = MathAbs(getLow(k) - getClose(k+1)); sumTR += MathMax(hl, MathMax(hpc, lpc)); } atr14 = sumTR / 14.0; } if(atr14 <= 0) atr14 = _Point; double zone_mid = (zone_high + zone_low) / 2.0; double zone_width = zone_high - zone_low; int hour = datetime(TimeCurrent()); //--- RSI(14) — Wilder EMA approximation double rsi_val = 50.0; { double gain = 0, loss = 0; for(int k = 1; k <= 14; k++) { double d = getClose(k) - getClose(k+1); if(d > 0) gain += d; else loss -= d; } gain /= 14; loss /= 14; if(loss > 0) rsi_val = 100.0 - 100.0 / (1.0 + gain / loss); } //--- ADX(14) approximation double adx_val = 20.0; { double pdm = 0, ndm = 0, tr_sum = 0; for(int k = 1; k <= 14; k++) { double up = getHigh(k) - getHigh(k+1); double down = getLow(k+1) - getLow(k); pdm += (up > down && up > 0) ? up : 0; ndm += (down > up && down > 0) ? down : 0; double hl = getHigh(k) - getLow(k); double hpc = MathAbs(getHigh(k) - getClose(k+1)); double lpc = MathAbs(getLow(k) - getClose(k+1)); tr_sum += MathMax(hl, MathMax(hpc, lpc)); } if(tr_sum > 0) { double pdi = 100 * pdm / tr_sum; double ndi = 100 * ndm / tr_sum; if(pdi + ndi > 0) adx_val = 100 * MathAbs(pdi - ndi) / (pdi + ndi); } } //--- Build float feature vector — order must match Python FEATURE_COLS vectorf features; features.Init(N_FEATURES); features[0] = (float)(signal_type == 0 ? 1 : 0); // f_signal_ob features[1] = (float)(signal_type == 1 ? 1 : 0); // f_signal_fvg features[2] = (float)(signal_type == 2 ? 1 : 0); // f_signal_bos features[3] = (float)direction; // f_direction features[4] = (float)MathMin(zone_width / atr14, 10.0); // f_zone_width_atr features[5] = (float)MathMin(MathAbs(entry_price - zone_mid) / atr14, 10.0); // f_dist_to_zone_atr features[6] = (float)MathMin(MathAbs(entry_price - zone_low) / MathMax(zone_width, _Point), 1.0); // f_fib_pct features[7] = (float)MathSin(2.0 * M_PI * hour / 24.0); // f_session_sin features[8] = (float)MathCos(2.0 * M_PI * hour / 24.0); // f_session_cos features[9] = (float)(rsi_val / 100.0); // f_rsi14 features[10] = (float)MathMin(adx_val / 100.0, 1.0); // f_adx14 features[11] = (float)MathMin((getHigh(0) - getLow(0)) / atr14, 5.0); // f_spread_norm //--- Run inference vectorf label_out(1), prob_out(2); if(!OnnxRun(g_onnx_model, ONNX_DEFAULT, features, label_out, prob_out)) { Print("[AI] OnnxRun failed: ", GetLastError()); return -1.0f; } return prob_out[1]; // probability of TP-hit }
Here, we initialize the model in InitONNX using OnnxCreateFromBuffer, which loads directly from the compiled resource rather than from a file path. We then set the input and output shapes explicitly, since the model expects a fixed vector of 12 float features and returns a predicted label alongside a probability array.
The RunONNX function builds the feature vector presently a signal fires. It computes ATR, RSI, and ADX from raw price data, encodes the session hour using sine and cosine transformations, and normalizes zone geometry relative to ATR. The function returns prob_out[1], which is the model's estimated probability that the trade will hit take-profit. If the score falls below AI_MinScore, the trade is blocked.
//+------------------------------------------------------------------+ //| TRADE EXECUTION | //+------------------------------------------------------------------+ bool ExecuteTrade(ENUM_ORDER_TYPE type, string origin) { double price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get SL/TP as price distances (dollars for XAUUSD) double sl_dist, tp_dist; CalcSLTP(sl_dist, tp_dist); double sl = (type == ORDER_TYPE_BUY) ? price - sl_dist : price + sl_dist; double tp = (type == ORDER_TYPE_BUY) ? price + tp_dist : price - tp_dist; sl = NormalizeDouble(sl, _Digits); tp = NormalizeDouble(tp, _Digits); Print("[Trade] ", (type==ORDER_TYPE_BUY?"BUY":"SELL"), " Entry:", DoubleToString(price,_Digits), " SL:", DoubleToString(sl,_Digits), " (", DoubleToString(sl_dist,2), ")", " TP:", DoubleToString(tp,_Digits), " (", DoubleToString(tp_dist,2), ")", " Origin:", origin); trade.SetExpertMagicNumber(MagicNumber); bool ok = trade.PositionOpen(_Symbol, type, In_Lot, price, sl, tp, "SMC-AI"); if(ok) { g_panel_signal = (type == ORDER_TYPE_BUY) ? "Buy" : "Sell"; g_panel_origin = origin; g_trend_str = ScoreToStrength(g_ai_score); UpdatePanel(); } return ok; }
The ExecuteTrade function handles all order submission. It fetches the current market price, calls CalcSLTP to get the distances, computes the SL and TP price levels, normalizes them to the symbol's digit precision, and then opens the position. Every trade prints a journal line showing the direction, entry price, SL, TP, and the signal origin. After a successful open, we update the panel to reflect the new signal, origin, and trend strength. The trend strength label—Strong, Medium, or Weak—is derived directly from the AI score using the ScoreToStrength function.
//+------------------------------------------------------------------+ //| SWING DETECTION (unified) | //+------------------------------------------------------------------+ void DetectSwingForBar(int barIndex, ENUM_SWING_TYPE type) { const int len = SwingPeriod; bool isSwingH = true, isSwingL = true; int totalBars = Bars(_Symbol, _Period); for(int i = 1; i <= len; i++) { int right = barIndex - i; int left = barIndex + i; if(right < 0) { isSwingH = false; isSwingL = false; break; } if(getHigh(barIndex) <= getHigh(right)) isSwingH = false; if(left < totalBars && getHigh(barIndex) < getHigh(left)) isSwingH = false; if(getLow(barIndex) >= getLow(right)) isSwingL = false; if(left < totalBars && getLow(barIndex) > getLow(left)) isSwingL = false; } if(type == SWING_OB) { if(isSwingH) { fib_high = getHigh(barIndex); fib_t1 = getTime(barIndex); } if(isSwingL) { fib_low = getLow(barIndex); fib_t2 = getTime(barIndex); } } else { if(isSwingH) { swng_High = getHigh(barIndex); bos_tH = getTime(barIndex); } if(isSwingL) { swng_Low = getLow(barIndex); bos_tL = getTime(barIndex); } } }
We use a single unified function, DetectSwingForBar, to identify swing highs and lows for both the OB and BOS strategies. It checks a configurable number of bars to the left and right of the candidate bar. If the bar's high is the highest across that range, it qualifies as a swing high. The same logic applies in reverse for swing lows. The SWING_TYPE parameter determines where the result is stored. When called with SWING_OB, the function writes to the Fibonacci scratch variables used by the OB strategy. When called with SWING_BOS, it writes to the BOS swing globals. This keeps the two strategies from interfering with each other's reference points.
//+------------------------------------------------------------------+ //| VISUALISATION HELPERS | //+------------------------------------------------------------------+ void DrawZoneRect(string name, datetime t1, datetime t2, double high, double low, color clr, string label) { //--- Rectangle if(ObjectFind(0, name) == -1) ObjectCreate(0, name, OBJ_RECTANGLE, 0, t1, high, t2, low); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FILL, true); ObjectSetInteger(0, name, OBJPROP_BACK, true); ObjectSetInteger(0, name, OBJPROP_WIDTH, 1); ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID); //--- Centred label — placed at the midpoint time (approximate) and price string lbl_name = name + "_lbl"; datetime t_mid = t1 + (datetime)((t2 - t1) / 2); double p_mid = (high + low) / 2.0; if(ObjectFind(0, lbl_name) == -1) ObjectCreate(0, lbl_name, OBJ_TEXT, 0, t_mid, p_mid); ObjectSetString(0, lbl_name, OBJPROP_TEXT, label); ObjectSetInteger(0, lbl_name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(0, lbl_name, OBJPROP_FONTSIZE, 9); ObjectSetInteger(0, lbl_name, OBJPROP_ANCHOR, ANCHOR_CENTER); } //--- Update the right-side time anchor of a zone rec void ExtendZoneRect(string name) { if(ObjectFind(0, name) == -1) return; datetime tNow = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE); ObjectSetInteger(0, name, OBJPROP_TIME, 1, tNow); string lbl_name = name + "_lbl"; if(ObjectFind(0, lbl_name) == -1) return; //--- Recentre label horizontally datetime t1 = (datetime)ObjectGetInteger(0, name, OBJPROP_TIME, 0); datetime t_mid = t1 + (datetime)((tNow - t1) / 2); ObjectSetInteger(0, lbl_name, OBJPROP_TIME, 0, t_mid); } void DeleteZone(string name) { ObjectDelete(0, name); ObjectDelete(0, name + "_lbl"); } //--- Draw BOS trend line with direction label void DrawBOS(const string name, datetime t1, double p1, datetime t2, double p2, color col, int dir) { if(ObjectFind(0, name) != -1) return; ObjectCreate(0, name, OBJ_TREND, 0, t1, p1, t2, p2); ObjectSetInteger(0, name, OBJPROP_COLOR, col); ObjectSetInteger(0, name, OBJPROP_WIDTH, 2); ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT, false); string lbl = name + "_lbl"; ObjectCreate(0, lbl, OBJ_TEXT, 0, t2, p2); ObjectSetInteger(0, lbl, OBJPROP_COLOR, col); ObjectSetInteger(0, lbl, OBJPROP_FONTSIZE, 9); ObjectSetString(0, lbl, OBJPROP_TEXT, (dir > 0) ? "BOS ↑" : "BOS ↓"); ObjectSetInteger(0, lbl, OBJPROP_ANCHOR, (dir > 0) ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER); }
In this code section, we draw all SMC zones using DrawZoneRect, which creates a filled rectangle between two time and price coordinates. A centered text label is placed inside the rectangle using an OBJ_TEXT object anchored to the midpoint. The label text and color differ per concept—green with "Bullish OB," red with "Bearish OB," blue with "Bullish FVG," and orange with "Bearish FVG."
BOS levels are drawn as trend lines using DrawBOS, with a directional label anchored at the breakout point. All drawing functions check for object existence before creating anything, preventing duplicate objects from accumulating on the chart across multiple bar events.
//+------------------------------------------------------------------+ //| CREATE PANEL | //+------------------------------------------------------------------+ void CreatePanel() { string bg = "SMC_Panel_BG"; if(ObjectFind(0, bg) == -1) { ObjectCreate(0, bg, OBJ_RECTANGLE_LABEL, 0, 0, 0); ObjectSetInteger(0, bg, OBJPROP_XDISTANCE, PanelX); ObjectSetInteger(0, bg, OBJPROP_YDISTANCE, PanelY); ObjectSetInteger(0, bg, OBJPROP_XSIZE, PANEL_W); ObjectSetInteger(0, bg, OBJPROP_YSIZE, PANEL_H); ObjectSetInteger(0, bg, OBJPROP_BGCOLOR, PanelBG); ObjectSetInteger(0, bg, OBJPROP_BORDER_TYPE,BORDER_FLAT); ObjectSetInteger(0, bg, OBJPROP_COLOR, PanelAccent); ObjectSetInteger(0, bg, OBJPROP_WIDTH, 1); ObjectSetInteger(0, bg, OBJPROP_BACK, false); ObjectSetInteger(0, bg, OBJPROP_CORNER, CORNER_LEFT_UPPER); } } void PanelLabel(string name, int line, string text, color clr, int fontsize = 9) { if(ObjectFind(0, name) == -1) { ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0, name, OBJPROP_XDISTANCE, PanelX + 10); } ObjectSetInteger(0, name, OBJPROP_YDISTANCE, PanelY + 8 + line * PANEL_LN); ObjectSetString(0, name, OBJPROP_TEXT, text); ObjectSetInteger(0, name, OBJPROP_COLOR, clr); ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontsize); ObjectSetString(0, name, OBJPROP_FONT, "Consolas"); } //+------------------------------------------------------------------+ //| UPDATE PANEL | //+------------------------------------------------------------------+ void UpdatePanel() { CreatePanel(); color accent = PanelAccent; color txtClr = PanelText; color valClr = clrWhite; color strengthColor = (g_trend_str == TS_STRONG) ? clrLimeGreen : (g_trend_str == TS_MEDIUM) ? clrYellow : clrTomato; color signalColor = (g_panel_signal == "Buy") ? clrLimeGreen : (g_panel_signal == "Sell") ? clrTomato : txtClr; PanelLabel("SMC_P_title", 0, " SMC AI Filter v2.1", accent, 10); PanelLabel("SMC_P_sep", 1, StringFormat("%s", "─────────────────────"), txtClr, 8); PanelLabel("SMC_P_sig_l", 2, "Current Signal :", txtClr); string sigText = g_panel_signal + " from " + g_panel_origin; PanelLabel("SMC_P_sig_v", 3, " " + sigText, signalColor); PanelLabel("SMC_P_str_l", 4, "Trend Strength :", txtClr); PanelLabel("SMC_P_str_v", 5, " " + StrengthToString(g_trend_str), strengthColor); string aiTxt = UseAI ? StringFormat("%.2f", g_ai_score) : "Disabled"; PanelLabel("SMC_P_ai_l", 6, "AI Score :", txtClr); PanelLabel("SMC_P_ai_v", 7, " " + aiTxt, valClr); double _sl, _tp; CalcSLTP(_sl, _tp); string sltp_mode = (SL_Mode == ATR_MULT) ? "ATR" : "Fixed"; PanelLabel("SMC_P_sl_l", 8, StringFormat("SL $%.1f TP $%.1f [%s]", _sl, _tp, sltp_mode), txtClr); string trailTxt = Trail_SL ? StringFormat("ON trig:$%.1f step:$%.1f", Trail_TriggerDist, Trail_StepDist) : "OFF"; color trailClr = Trail_SL ? clrLimeGreen : clrTomato; PanelLabel("SMC_P_trail_l", 9, "Trail SL :", txtClr); PanelLabel("SMC_P_trail_v", 10, " " + trailTxt, trailClr); PanelLabel("SMC_P_sep2", 11, StringFormat("%s", "─────────────────────"), txtClr, 8); PanelLabel("SMC_P_sym", 12, StringFormat("%-10s Magic %-6I64d", _Symbol, MagicNumber), txtClr, 8); ChartRedraw(0); } void DestroyPanel() { string names[] = { "SMC_Panel_BG", "SMC_P_title","SMC_P_sep","SMC_P_sig_l","SMC_P_sig_v", "SMC_P_str_l","SMC_P_str_v","SMC_P_ai_l","SMC_P_ai_v", "SMC_P_sl_l","SMC_P_trail_l","SMC_P_trail_v","SMC_P_sep2","SMC_P_sym" }; for(int i = 0; i < ArraySize(names); i++) ObjectDelete(0, names[i]); }
The panel is built from a single OBJ_RECTANGLE_LABEL background and a series of OBJ_LABEL objects stacked at fixed vertical intervals. We use PanelLabel to create or update each row, which handles both first-time creation and subsequent updates in the same call. The panel displays the current signal direction and origin, trend strength with color coding, the AI confidence score, the active SL and TP values with their mode, and the trailing stop status. Green indicates active or positive states, and red indicates off or weak states. On OnDeinit, DestroyPanel removes every named object cleanly.
//+------------------------------------------------------------------+ //| ORDER BLOCK STRATEGY | //+------------------------------------------------------------------+ static datetime s_OB_lastDetect = 0; void DetectAndDrawOrderBlocks() { datetime lastBar = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE); //--- Reset OB on new bar if(s_OB_lastDetect != lastBar) { g_OB_valid = false; s_OB_lastDetect = lastBar; } //--- Scan for a fresh OB candidate if(!g_OB_valid) { for(int i = 1; i < 100; i++) { //--- Bullish OB if(getOpen(i) < getClose(i) && getOpen(i+2) < getClose(i+2) && getOpen(i+3) > getClose(i+3) && getOpen(i+3) < getClose(i+2)) { g_OB.direction = 1; g_OB.time = getTime(i+3); g_OB.high = getHigh(i+3); g_OB.low = getLow(i+3); g_OB_valid = true; break; } //--- Bearish OB if(getOpen(i) > getClose(i) && getOpen(i+2) > getClose(i+2) && getOpen(i+3) < getClose(i+3) && getOpen(i+3) > getClose(i+2)) { g_OB.direction = -1; g_OB.time = getTime(i+3); g_OB.high = getHigh(i+3); g_OB.low = getLow(i+3); g_OB_valid = true; break; } } } if(!g_OB_valid) return; if(g_lastOBTradeTime == g_OB.time) return; // already traded this signal if(HasOpenPosition()) return; // wait for current trade to close Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); bool inBull = (g_OB.direction > 0 && Ask <= g_OB.high && Ask >= g_OB.low); bool inBear = (g_OB.direction < 0 && Bid >= g_OB.low && Bid <= g_OB.high); if(!inBull && !inBear) return; //--- Draw zone rectangle NOW (price within the zone) datetime tNow = lastBar; string rectName = "OB_ZONE_" + TimeToString(g_OB.time); if(inBull) { DrawZoneRect(rectName, g_OB.time, tNow, g_OB.high, g_OB.low, CLR_BULL_OB, "Bullish OB"); } else { DrawZoneRect(rectName, g_OB.time, tNow, g_OB.high, g_OB.low, CLR_BEAR_OB, "Bearish OB"); } //--- Find most-recent swing H and L double bestH = 0, bestL = 0; datetime bestHT = 0, bestLT = 0; for(int i = 0; i < 50; i++) { fib_high = 0; fib_low = 0; fib_t1 = 0; fib_t2 = 0; DetectSwingForBar(i, SWING_OB); if(fib_high > 0 && (bestHT == 0 || fib_t1 > bestHT)) { bestH = fib_high; bestHT = fib_t1; } if(fib_low > 0 && (bestLT == 0 || fib_t2 > bestLT)) { bestL = fib_low; bestLT = fib_t2; } } if(bestHT == 0 || bestLT == 0) return; //--- Fibonacci validation if(inBull) { double entLvl = bestH - (bestH - bestL) * (Fib_Trade_lvls / 100.0); if(Ask > entLvl) return; // not deep enough //--- AI filter g_ai_score = RunONNX(0, 1, g_OB.high, g_OB.low, Ask); if(UseAI && g_ai_score < AI_MinScore) { Print("[AI] OB Bull signal rejected — score: ", DoubleToString(g_ai_score, 3)); return; } g_trend_str = ScoreToStrength(g_ai_score); ExecuteTrade(ORDER_TYPE_BUY, "OB"); g_lastOBTradeTime = g_OB.time; g_OB_valid = false; } else // inBear { double entLvl = bestL + (bestH - bestL) * (Fib_Trade_lvls / 100.0); if(Bid < entLvl) return; g_ai_score = RunONNX(0, -1, g_OB.high, g_OB.low, Bid); if(UseAI && g_ai_score < AI_MinScore) { Print("[AI] OB Bear signal rejected — score: ", DoubleToString(g_ai_score, 3)); return; } g_trend_str = ScoreToStrength(g_ai_score); ExecuteTrade(ORDER_TYPE_SELL, "OB"); g_lastOBTradeTime = g_OB.time; g_OB_valid = false; } }
The OB strategy scans back through recent bars looking for the classic three-candle pattern that defines a bullish or bearish order block. A bullish OB is a bearish candle followed by a bullish impulse. A bearish OB is the inverse. Once a valid OB is found, it is stored and reused across bar events until it is traded or invalidated. When price retraces into the OB zone, we draw the colored rectangle, validate the depth of the retrace using a Fibonacci level, and then run the AI filter. If the score passes, the trade is opened. The OB time is recorded in g_lastOBTradeTime to ensure the same zone is never traded twice, even if price returns to it on a later bar.
void DetectAndDrawFVGs() { double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT); int maxBars = MathMin(FVG_ScanBars, Bars(_Symbol, _Period)) - 2; Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); datetime barNow = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE); if(OneTradePerBar && lastFVGTradeBar == barNow) return; if(HasOpenPosition()) return; // wait for current trade to close for(int i = 2; i < maxBars; i++) { double lowA = getLow(i+2), highA = getHigh(i+2); double highC = getHigh(i), lowC = getLow(i); SFVG z; bool found = false; if(lowA > highC && (lowA - highC) >= FVG_MinPoints * point) { z.dir = 1; z.tLeft = getTime(i+2); z.top = lowA; z.bot = highC; found = true; } else if(highA < lowC && (lowC - highA) >= FVG_MinPoints * point) { z.dir = -1; z.tLeft = getTime(i+2); z.top = lowC; z.bot = highA; found = true; } if(!found) continue; double mid = (z.top + z.bot) * 0.5; bool inGap = false; if(z.dir == 1) inGap = (Ask <= z.top && Ask >= z.bot && (!FVG_TradeAtEQ || Ask <= mid)); else inGap = (Bid <= z.top && Bid >= z.bot && (!FVG_TradeAtEQ || Bid >= mid)); if(!inGap) continue; //--- Draw coloured zone only when price enters DrawFVGZone(z); //--- AI filter double entry = (z.dir == 1) ? Ask : Bid; g_ai_score = RunONNX(1, z.dir, z.top, z.bot, entry); if(UseAI && g_ai_score < AI_MinScore) { Print("[AI] FVG signal rejected — score: ", DoubleToString(g_ai_score, 3)); continue; } g_trend_str = ScoreToStrength(g_ai_score); if(z.dir == 1) ExecuteTrade(ORDER_TYPE_BUY, "FVG"); else ExecuteTrade(ORDER_TYPE_SELL, "FVG"); lastFVGTradeBar = barNow; break; } }
For a FVG, we scan the most recent bars looking for a three-candle imbalance. A bullish FVG exists when the low of the first candle is above the high of the third. A bearish FVG is the mirror condition. The gap boundaries define the zone, and the midpoint defines the equilibrium level used when FVG_TradeAtEQ is enabled. When the price enters the gap and meets the EQ condition, we draw the colored zone rectangle and run the AI inference. A bullish FVG draws a blue rectangle labeled "Bullish FVG," and a bearish FVG draws an orange rectangle labeled "Bearish FVG." We then record the bar time in lastFVGTradeBar to prevent multiple entries within the same bar.
//+------------------------------------------------------------------+ //| BOS STRATEGY | //+------------------------------------------------------------------+ void DetectAndDrawBOS() { double bestH = -1, bestL = -1; datetime bestHT = 0, bestLT = 0; for(int i = 0; i < 50; i++) { swng_High = 0; swng_Low = 0; bos_tH = 0; bos_tL = 0; DetectSwingForBar(i, SWING_BOS); if(swng_High > 0 && (bestHT == 0 || bos_tH > bestHT)) { bestH = swng_High; bestHT = bos_tH; } if(swng_Low > 0 && (bestLT == 0 || bos_tL > bestLT)) { bestL = swng_Low; bestLT = bos_tL; } } if(bestHT > 0) { if(bos_tH != bestHT) Bull_BOS_traded = false; swng_High = bestH; bos_tH = bestHT; } if(bestLT > 0) { if(bos_tL != bestLT) Bear_BOS_traded = false; swng_Low = bestL; bos_tL = bestLT; } Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); datetime currentBar = getTime(0); if(HasOpenPosition()) return; // wait for current trade to close //--- BUY on break above swing high if(swng_High > 0 && Ask > swng_High && !Bull_BOS_traded) { if(lastBOSTradeTime != currentBar || lastBOSTradeDirection != 1) { g_ai_score = RunONNX(2, 1, swng_High + 5 * _Point, swng_High - 5 * _Point, Ask); if(!UseAI || g_ai_score >= AI_MinScore) { if(DrawBOSLines) DrawBOS("BOS_H_" + TimeToString(bos_tH), bos_tH, swng_High, TimeCurrent(), swng_High, CLR_BOS_BULL, 1); g_trend_str = ScoreToStrength(g_ai_score); ExecuteTrade(ORDER_TYPE_BUY, "BOS"); lastBOSTradeTime = currentBar; lastBOSTradeDirection = 1; Bull_BOS_traded = true; swng_High = -1.0; } else Print("[AI] BOS Bull rejected — score: ", DoubleToString(g_ai_score, 3)); } } //--- SELL on break below swing low if(swng_Low > 0 && Bid < swng_Low && !Bear_BOS_traded) { if(lastBOSTradeTime != currentBar || lastBOSTradeDirection != -1) { g_ai_score = RunONNX(2, -1, swng_Low + 5 * _Point, swng_Low - 5 * _Point, Bid); if(!UseAI || g_ai_score >= AI_MinScore) { if(DrawBOSLines) DrawBOS("BOS_L_" + TimeToString(bos_tL), bos_tL, swng_Low, TimeCurrent(), swng_Low, CLR_BOS_BEAR, -1); g_trend_str = ScoreToStrength(g_ai_score); ExecuteTrade(ORDER_TYPE_SELL, "BOS"); lastBOSTradeTime = currentBar; lastBOSTradeDirection = -1; Bear_BOS_traded = true; swng_Low = -1.0; } else Print("[AI] BOS Bear rejected — score: ", DoubleToString(g_ai_score, 3)); } } } //+------------------------------------------------------------------+Finally, we identify the most recent swing high and swing low by scanning the last fifty bars using DetectSwingForBar. When price breaks above the swing high, we treat that as a bullish break of structure and open a buy. When price breaks below the swing low, we treat that as a bearish break of structure and open a sell. Each breakout is drawn as a horizontal trend line at the broken level with a "BOS ↑" or "BOS ↓" label. The Bull_BOS_traded and Bear_BOS_traded flags prevent the same structural break from triggering more than one trade. Once a swing level is traded, it is reset so the strategy can identify the next fresh structure as price continues to develop.
Backtest Results
The backtest was conducted across roughly a 2-month testing window from 02 February 2026 to 01 April 2026, with the following settings:

Below are the equity curve and the backtest results:


Conclusion
Throughout this article, we built a complete AI-augmented trading system from the ground up. We built a complete AI-augmented trading system from the ground up. We started with a Python pipeline that extracts historical XAUUSD H1 data from MetaTrader 5 and reconstructs OB, FVG, and BOS events using the EA's logic. Each event was labeled based on its trade outcome. This allowed us to engineer twelve normalized features for zone geometry, momentum, and session timing. These features were then used to train an XGBoost classifier with cross-validation, after which the model was exported to ONNX and compiled into the EA as an embedded resource. On the MQL5 side, the EA was redesigned to compute the feature vector at signal time, evaluate it through the AI model, and filter every trade using the returned confidence score.
We also implemented volatility-aware SL and TP sizing using dollar-based risk management instead of fixed points. A one-trade-per-signal safeguard prevents duplicate entries on the same setup, while a tick-level trailing stop progressively locks in profit as price moves. The chart panel displays the signal origin, AI confidence score, and trend strength in real time, giving the trader clear feedback during execution.
After reading this article, the trader walks away with a complete Python training pipeline and an AI-enabled MQL5 EA ready for testing or live deployment. The system also provides a strong foundation for future optimization and experimentation. Most importantly, the article shows how machine learning can strengthen Smart Money Concepts through objective signal qualification and a more disciplined, data-driven decision process.
Below is the brief description of what is contained inside 'MQL5 zip':
| File Name | File Description |
|---|---|
| SMC_AI_NoteBK.ipynb | A Jupyter notebook for training an ONNX model, based on the original logic of the EA. |
| smc_filter.onnx | A trained ONNX model. |
| SMC AI.mq5 | Is the MetaTrader 5 Expert Advisor that works with the trained ONNX model. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Building a Dynamic STF Liquidity Sweep Indicator in MQL5
News Filtering with MetaTrader 5 Economic Calendar and CSV Fallback
Covariance Matrix Adaptation Evolution Strategy (CMA-ES)
Building the Market Structure Sentinel Indicator in MQL5
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use