1312 lines
95 KiB
Text
1312 lines
95 KiB
Text
|
|
{
|
||
|
|
"cells": [
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "b0f80098",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"# Integrating MQL5 with Data Processing Packages (Part 10)\n",
|
||
|
|
"## Deploying Python AutoML Pipelines for Strategy Testing\n",
|
||
|
|
"\n",
|
||
|
|
"**Pipeline:** Fetch data → Engineer features → Generate labels → Train one model (FLAML) → Export ONNX → Integrate in EA\n",
|
||
|
|
"\n",
|
||
|
|
"**Strategy under test:** EMA(12/26) crossover + RSI context on XAUUSD H1.\n",
|
||
|
|
"The model's job: given the market context at the moment of a crossover, predict whether the trade will close in profit.\n",
|
||
|
|
"\n",
|
||
|
|
"**Design decisions locked in:**\n",
|
||
|
|
"- **Single ONNX model** — market regime is encoded in the features, not in separate models\n",
|
||
|
|
"- **Binary labels** — 1 = trade closed in profit, 0 = closed in loss\n",
|
||
|
|
"- **Exit rule for labeling** — next opposite crossover, capped at `MAX_HOLD_BARS`\n",
|
||
|
|
"- **All ratio-based features** — no raw price levels e.g, so that the model survives XAUUSD moving from 1500 to 2700+\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "42426bfb",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 0 — Environment & Configuration\n",
|
||
|
|
"\n",
|
||
|
|
"Run the install cell once. Versions are pinned where it matters: the ONNX toolchain is\n",
|
||
|
|
"the most fragile part of this pipeline.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": null,
|
||
|
|
"id": "4d5205e8",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [],
|
||
|
|
"source": [
|
||
|
|
"# Run once in the jupyter terminal, then restart the kernel\n",
|
||
|
|
"# %pip install pandas numpy scikit-learn flaml[automl] lightgbm xgboost skl2onnx onnxmltools onnxruntime onnx"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 9,
|
||
|
|
"id": "628bab46-edb4-49b6-9a31-a6f633a30e5f",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"MetaTrader5 package v5.0.5572 by MetaQuotes Ltd.\n",
|
||
|
|
"[MT5] Connected — build (500, 5836, '28 Apr 2026')\n",
|
||
|
|
"[MT5] Symbol: XAUUSD Point: 0.001 Digits: 3\n",
|
||
|
|
"[MT5] Requesting H1 bars from 2023-01-01 to 2026-01-01 ...\n",
|
||
|
|
"\n",
|
||
|
|
"=======================================================\n",
|
||
|
|
" Data Quality Report\n",
|
||
|
|
"=======================================================\n",
|
||
|
|
" Bars fetched : 13,584\n",
|
||
|
|
" Date range : 2023-09-04 01:00 -> 2025-12-31 23:00\n",
|
||
|
|
" Price range : 1815.03 -> 4546.69\n",
|
||
|
|
" Mean ATR (H-L) : 8.17\n",
|
||
|
|
" Missing bars : 709 (weekend/holiday gaps expected)\n",
|
||
|
|
"=======================================================\n",
|
||
|
|
"\n",
|
||
|
|
"[OK] Saved 13,584 bars to 'XAUUSD_H1.csv'\n",
|
||
|
|
" File size: 873.9 KB\n",
|
||
|
|
"\n",
|
||
|
|
"Next step: set csv_file = 'XAUUSD_H1.csv' in the training pipeline.\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"from datetime import datetime\n",
|
||
|
|
"import sys\n",
|
||
|
|
"import warnings\n",
|
||
|
|
"\n",
|
||
|
|
"import pandas as pd\n",
|
||
|
|
"import pytz\n",
|
||
|
|
"\n",
|
||
|
|
"warnings.filterwarnings(\"ignore\")\n",
|
||
|
|
"\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"# CONFIG — edit these values\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"SYMBOL = \"XAUUSD\" # exact symbol name as shown in MT5 Market Watch\n",
|
||
|
|
"TIMEFRAME = \"H1\" # M1 M5 M15 M30 H1 H4 D1\n",
|
||
|
|
"DATE_FROM = datetime(2023, 1, 1) # start of historical range\n",
|
||
|
|
"DATE_TO = datetime(2026, 1, 1) # end of historical range (exclusive)\n",
|
||
|
|
"OUTPUT_CSV = \"XAUUSD_H1.csv\" # output filename (saved in working directory)\n",
|
||
|
|
"TIMEZONE = \"Etc/UTC\" # keep UTC — pipeline expects UTC timestamps\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"\n",
|
||
|
|
"# Timeframe string -> MT5 constant name\n",
|
||
|
|
"TF_MAP = {\n",
|
||
|
|
" \"M1\": \"TIMEFRAME_M1\",\n",
|
||
|
|
" \"M5\": \"TIMEFRAME_M5\",\n",
|
||
|
|
" \"M15\": \"TIMEFRAME_M15\",\n",
|
||
|
|
" \"M30\": \"TIMEFRAME_M30\",\n",
|
||
|
|
" \"H1\": \"TIMEFRAME_H1\",\n",
|
||
|
|
" \"H4\": \"TIMEFRAME_H4\",\n",
|
||
|
|
" \"D1\": \"TIMEFRAME_D1\",\n",
|
||
|
|
"}\n",
|
||
|
|
"\n",
|
||
|
|
"def fetch():\n",
|
||
|
|
" try:\n",
|
||
|
|
" import MetaTrader5 as mt5\n",
|
||
|
|
" except ImportError:\n",
|
||
|
|
" sys.exit(\"[ERROR] MetaTrader5 package not installed.\\n\"\n",
|
||
|
|
" \" Run: pip install MetaTrader5\")\n",
|
||
|
|
"\n",
|
||
|
|
" print(f\"MetaTrader5 package v{mt5.__version__} by {mt5.__author__}\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Initialise ───────────────────────────────────────────────────────────\n",
|
||
|
|
" if not mt5.initialize():\n",
|
||
|
|
" sys.exit(f\"[ERROR] mt5.initialize() failed: {mt5.last_error()}\")\n",
|
||
|
|
" print(f\"[MT5] Connected — build {mt5.version()}\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Select symbol ────────────────────────────────────────────────────────\n",
|
||
|
|
" if not mt5.symbol_select(SYMBOL, True):\n",
|
||
|
|
" mt5.shutdown()\n",
|
||
|
|
" sys.exit(f\"[ERROR] Symbol '{SYMBOL}' not found in Market Watch.\\n\"\n",
|
||
|
|
" \" Check the exact name (e.g. XAUUSD vs XAUUSD.m vs XAUUSDm)\")\n",
|
||
|
|
"\n",
|
||
|
|
" info = mt5.symbol_info(SYMBOL)\n",
|
||
|
|
" point = info.point if info else 0.01\n",
|
||
|
|
" print(f\"[MT5] Symbol: {SYMBOL} Point: {point} Digits: {info.digits if info else '?'}\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Validate timeframe ───────────────────────────────────────────────────\n",
|
||
|
|
" tf_attr = TF_MAP.get(TIMEFRAME.upper())\n",
|
||
|
|
" if tf_attr is None:\n",
|
||
|
|
" mt5.shutdown()\n",
|
||
|
|
" sys.exit(f\"[ERROR] Unknown timeframe '{TIMEFRAME}'. Choose from: {list(TF_MAP)}\")\n",
|
||
|
|
" tf = getattr(mt5, tf_attr)\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Build UTC-aware date range ───────────────────────────────────────────\n",
|
||
|
|
" tz = pytz.timezone(TIMEZONE)\n",
|
||
|
|
" utc_from = tz.localize(DATE_FROM)\n",
|
||
|
|
" utc_to = tz.localize(DATE_TO)\n",
|
||
|
|
" print(f\"[MT5] Requesting {TIMEFRAME} bars from {utc_from.date()} to {utc_to.date()} ...\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Fetch ────────────────────────────────────────────────────────────────\n",
|
||
|
|
" rates = mt5.copy_rates_range(SYMBOL, tf, utc_from, utc_to)\n",
|
||
|
|
" mt5.shutdown()\n",
|
||
|
|
"\n",
|
||
|
|
" if rates is None or len(rates) == 0:\n",
|
||
|
|
" sys.exit(\"[ERROR] No bars returned. Possible causes:\\n\"\n",
|
||
|
|
" \" - Date range has no data for this symbol on this broker\\n\"\n",
|
||
|
|
" \" - Symbol requires a different name (try without .m suffix)\\n\"\n",
|
||
|
|
" \" - MT5 history for this period not downloaded yet\\n\"\n",
|
||
|
|
" \" (Open the chart in MT5 and scroll back to force download)\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Build DataFrame ──────────────────────────────────────────────────────\n",
|
||
|
|
" df = pd.DataFrame(rates)\n",
|
||
|
|
" df[\"time\"] = pd.to_datetime(df[\"time\"], unit=\"s\", utc=True)\n",
|
||
|
|
" df.set_index(\"time\", inplace=True)\n",
|
||
|
|
"\n",
|
||
|
|
" # Rename tick_volume -> volume, drop spread column if present\n",
|
||
|
|
" df.rename(columns={\"tick_volume\": \"volume\", \"real_volume\": \"real_vol\"}, inplace=True)\n",
|
||
|
|
" keep = [c for c in [\"open\", \"high\", \"low\", \"close\", \"volume\"] if c in df.columns]\n",
|
||
|
|
" df = df[keep].astype(float)\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Quality report ───────────────────────────────────────────────────────\n",
|
||
|
|
" n_bars = len(df)\n",
|
||
|
|
" date_start = df.index[0].strftime(\"%Y-%m-%d %H:%M\")\n",
|
||
|
|
" date_end = df.index[-1].strftime(\"%Y-%m-%d %H:%M\")\n",
|
||
|
|
" price_min = df[\"close\"].min()\n",
|
||
|
|
" price_max = df[\"close\"].max()\n",
|
||
|
|
" atr_proxy = (df[\"high\"] - df[\"low\"]).mean()\n",
|
||
|
|
" gaps = df.index.to_series().diff().dropna()\n",
|
||
|
|
" expected = gaps.mode()[0]\n",
|
||
|
|
" gap_bars = (gaps > expected * 1.5).sum()\n",
|
||
|
|
"\n",
|
||
|
|
" print(f\"\\n{'='*55}\")\n",
|
||
|
|
" print(f\" Data Quality Report\")\n",
|
||
|
|
" print(f\"{'='*55}\")\n",
|
||
|
|
" print(f\" Bars fetched : {n_bars:,}\")\n",
|
||
|
|
" print(f\" Date range : {date_start} -> {date_end}\")\n",
|
||
|
|
" print(f\" Price range : {price_min:.2f} -> {price_max:.2f}\")\n",
|
||
|
|
" print(f\" Mean ATR (H-L) : {atr_proxy:.2f}\")\n",
|
||
|
|
" print(f\" Missing bars : {gap_bars:,} (weekend/holiday gaps expected)\")\n",
|
||
|
|
" print(f\"{'='*55}\\n\")\n",
|
||
|
|
"\n",
|
||
|
|
" if n_bars < 1000:\n",
|
||
|
|
" print(f\"[WARN] Only {n_bars} bars fetched. Consider extending the date range.\\n\"\n",
|
||
|
|
" \" Training needs at least 5,000+ bars for meaningful results.\")\n",
|
||
|
|
"\n",
|
||
|
|
" # ── Save ─────────────────────────────────────────────────────────────────\n",
|
||
|
|
" df.to_csv(OUTPUT_CSV)\n",
|
||
|
|
" print(f\"[OK] Saved {n_bars:,} bars to '{OUTPUT_CSV}'\")\n",
|
||
|
|
" print(f\" File size: {pd.io.common.get_handle(OUTPUT_CSV,'r').handle.seek(0,2) if False else ''}\"\n",
|
||
|
|
" f\"{round(Path(OUTPUT_CSV).stat().st_size / 1024, 1)} KB\")\n",
|
||
|
|
" print(f\"\\nNext step: set csv_file = '{OUTPUT_CSV}' in the training pipeline.\")\n",
|
||
|
|
" return df\n",
|
||
|
|
"\n",
|
||
|
|
"\n",
|
||
|
|
"# ── Entrypoint ────────────────────────────────────────────────────────────────\n",
|
||
|
|
"from pathlib import Path\n",
|
||
|
|
"\n",
|
||
|
|
"if __name__ == \"__main__\":\n",
|
||
|
|
" fetch()\n",
|
||
|
|
"else:\n",
|
||
|
|
" # When imported or run as a Jupyter cell, execute immediately\n",
|
||
|
|
" fetch()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 10,
|
||
|
|
"id": "a544dd20",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Feature contract locked: 9 features\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"import warnings\n",
|
||
|
|
"warnings.filterwarnings(\"ignore\")\n",
|
||
|
|
"\n",
|
||
|
|
"import numpy as np\n",
|
||
|
|
"import pandas as pd\n",
|
||
|
|
"\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"# CONFIG — the single source of truth for the whole pipeline\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"CSV_FILE = \"XAUUSD_H1.csv\" # produced by the fetch script (Step 1)\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Strategy parameters (must match the MQL5 EA inputs exactly) ---\n",
|
||
|
|
"EMA_FAST = 12\n",
|
||
|
|
"EMA_SLOW = 26\n",
|
||
|
|
"RSI_PERIOD = 14\n",
|
||
|
|
"ATR_PERIOD = 14\n",
|
||
|
|
"VOL_FAST = 20 # short std-dev window for volatility_ratio\n",
|
||
|
|
"VOL_SLOW = 100 # long std-dev window for volatility_ratio\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Labeling ---\n",
|
||
|
|
"MAX_HOLD_BARS = 50 # exit at next opposite crossover OR after this many bars\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Training ---\n",
|
||
|
|
"TEST_FRACTION = 0.20 # chronological hold-out (most recent 20% of signals)\n",
|
||
|
|
"FLAML_TIME_SEC = 180 # 3-minute AutoML budget\n",
|
||
|
|
"RANDOM_SEED = 42\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Export ---\n",
|
||
|
|
"ONNX_FILE = \"ema_rsi_model.onnx\"\n",
|
||
|
|
"ONNX_OPSET = 12 # MT5's ONNX runtime safe zone — do NOT raise casually\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Feature contract (ORDER IS LAW — the MQL5 EA must fill its input\n",
|
||
|
|
"# array in exactly this order) ---\n",
|
||
|
|
"FEATURES = [\n",
|
||
|
|
" \"ema_fast_rel\", # 0: EMA(12)/close - 1\n",
|
||
|
|
" \"ema_slow_rel\", # 1: EMA(26)/close - 1\n",
|
||
|
|
" \"ema_distance\", # 2: (EMA fast - EMA slow)/close\n",
|
||
|
|
" \"rsi\", # 3: Wilder RSI(14), 0..100\n",
|
||
|
|
" \"rsi_momentum\", # 4: RSI[0] - RSI[1]\n",
|
||
|
|
" \"atr_rel\", # 5: ATR(14)/close\n",
|
||
|
|
" \"volatility_ratio\", # 6: std(close,20)/std(close,100)\n",
|
||
|
|
" \"close_range_pct\", # 7: (close-low)/(high-low) of the signal bar\n",
|
||
|
|
" \"signal_direction\", # 8: +1 buy crossover, -1 sell crossover\n",
|
||
|
|
"]\n",
|
||
|
|
"N_FEATURES = len(FEATURES)\n",
|
||
|
|
"print(f\"Feature contract locked: {N_FEATURES} features\")"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "1772ddb4",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 1 — Load the Historical Data\n",
|
||
|
|
"\n",
|
||
|
|
"The CSV comes from the MT5 fetch script (`copy_rates_range`). We only verify integrity here —\n",
|
||
|
|
"the fetch script already did the quality report.\n",
|
||
|
|
"\n",
|
||
|
|
"> **Note on `signal_direction`:** during design we planned 8 features, but the model must know\n",
|
||
|
|
"> whether a crossover is a *buy* or a *sell* — a bullish cross at RSI 75 and a bearish cross at\n",
|
||
|
|
"> RSI 75 are opposite situations. Encoding direction as feature #9 lets one model handle both\n",
|
||
|
|
"> sides cleanly. This is exactly the kind of contract change you must propagate to the EA.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 11,
|
||
|
|
"id": "d85bb004",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Bars loaded : 13,584\n",
|
||
|
|
"Date range : 2023-09-04 01:00:00+00:00 -> 2025-12-31 23:00:00+00:00\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"text/html": [
|
||
|
|
"<div>\n",
|
||
|
|
"<style scoped>\n",
|
||
|
|
" .dataframe tbody tr th:only-of-type {\n",
|
||
|
|
" vertical-align: middle;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe tbody tr th {\n",
|
||
|
|
" vertical-align: top;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe thead th {\n",
|
||
|
|
" text-align: right;\n",
|
||
|
|
" }\n",
|
||
|
|
"</style>\n",
|
||
|
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||
|
|
" <thead>\n",
|
||
|
|
" <tr style=\"text-align: right;\">\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th>open</th>\n",
|
||
|
|
" <th>high</th>\n",
|
||
|
|
" <th>low</th>\n",
|
||
|
|
" <th>close</th>\n",
|
||
|
|
" <th>volume</th>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>time</th>\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </thead>\n",
|
||
|
|
" <tbody>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>2025-12-31 21:00:00+00:00</th>\n",
|
||
|
|
" <td>4319.94</td>\n",
|
||
|
|
" <td>4322.98</td>\n",
|
||
|
|
" <td>4307.89</td>\n",
|
||
|
|
" <td>4311.38</td>\n",
|
||
|
|
" <td>11506.0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>2025-12-31 22:00:00+00:00</th>\n",
|
||
|
|
" <td>4311.39</td>\n",
|
||
|
|
" <td>4324.47</td>\n",
|
||
|
|
" <td>4304.65</td>\n",
|
||
|
|
" <td>4313.32</td>\n",
|
||
|
|
" <td>11811.0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>2025-12-31 23:00:00+00:00</th>\n",
|
||
|
|
" <td>4313.37</td>\n",
|
||
|
|
" <td>4316.86</td>\n",
|
||
|
|
" <td>4307.08</td>\n",
|
||
|
|
" <td>4316.86</td>\n",
|
||
|
|
" <td>4196.0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </tbody>\n",
|
||
|
|
"</table>\n",
|
||
|
|
"</div>"
|
||
|
|
],
|
||
|
|
"text/plain": [
|
||
|
|
" open high low close volume\n",
|
||
|
|
"time \n",
|
||
|
|
"2025-12-31 21:00:00+00:00 4319.94 4322.98 4307.89 4311.38 11506.0\n",
|
||
|
|
"2025-12-31 22:00:00+00:00 4311.39 4324.47 4304.65 4313.32 11811.0\n",
|
||
|
|
"2025-12-31 23:00:00+00:00 4313.37 4316.86 4307.08 4316.86 4196.0"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"execution_count": 11,
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "execute_result"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"df = pd.read_csv(CSV_FILE, index_col=\"time\", parse_dates=True)\n",
|
||
|
|
"\n",
|
||
|
|
"required = {\"open\", \"high\", \"low\", \"close\", \"volume\"}\n",
|
||
|
|
"missing = required - set(df.columns)\n",
|
||
|
|
"assert not missing, f\"CSV missing columns: {missing}\"\n",
|
||
|
|
"assert df.index.is_monotonic_increasing, \"Bars are not in chronological order\"\n",
|
||
|
|
"assert not df.index.duplicated().any(), \"Duplicate timestamps found\"\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Bars loaded : {len(df):,}\")\n",
|
||
|
|
"print(f\"Date range : {df.index[0]} -> {df.index[-1]}\")\n",
|
||
|
|
"df.tail(3)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "d6a96f9f",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 2 — Feature Engineering\n",
|
||
|
|
"\n",
|
||
|
|
"Every indicator here is implemented to **match its MT5 counterpart numerically**:\n",
|
||
|
|
"\n",
|
||
|
|
"| Feature source | Python implementation | MT5 equivalent |\n",
|
||
|
|
"|---|---|---|\n",
|
||
|
|
"| EMA | `ewm(span=N, adjust=False)` | `iMA(..., MODE_EMA)` |\n",
|
||
|
|
"| RSI | Wilder smoothing (`ewm(alpha=1/N)`) | `iRSI` |\n",
|
||
|
|
"| ATR | Wilder smoothing of True Range | `iATR` |\n",
|
||
|
|
"\n",
|
||
|
|
"If the Python indicator and the MQL5 indicator disagree, the model is trained on one\n",
|
||
|
|
"distribution and queried on another — the classic *feature alignment drift* failure.\n",
|
||
|
|
"We trim the first 200 bars so the recursive smoothers are fully converged.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 12,
|
||
|
|
"id": "566e2bea",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Bars after warm-up trim: 13,384\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"text/html": [
|
||
|
|
"<div>\n",
|
||
|
|
"<style scoped>\n",
|
||
|
|
" .dataframe tbody tr th:only-of-type {\n",
|
||
|
|
" vertical-align: middle;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe tbody tr th {\n",
|
||
|
|
" vertical-align: top;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe thead th {\n",
|
||
|
|
" text-align: right;\n",
|
||
|
|
" }\n",
|
||
|
|
"</style>\n",
|
||
|
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||
|
|
" <thead>\n",
|
||
|
|
" <tr style=\"text-align: right;\">\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th>close</th>\n",
|
||
|
|
" <th>ema_distance</th>\n",
|
||
|
|
" <th>rsi</th>\n",
|
||
|
|
" <th>atr_rel</th>\n",
|
||
|
|
" <th>volatility_ratio</th>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </thead>\n",
|
||
|
|
" <tbody>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>count</th>\n",
|
||
|
|
" <td>13384.0000</td>\n",
|
||
|
|
" <td>13384.0000</td>\n",
|
||
|
|
" <td>13384.0000</td>\n",
|
||
|
|
" <td>13384.0000</td>\n",
|
||
|
|
" <td>13384.0000</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>mean</th>\n",
|
||
|
|
" <td>2804.1304</td>\n",
|
||
|
|
" <td>0.0004</td>\n",
|
||
|
|
" <td>52.8647</td>\n",
|
||
|
|
" <td>0.0028</td>\n",
|
||
|
|
" <td>0.4766</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>std</th>\n",
|
||
|
|
" <td>682.1967</td>\n",
|
||
|
|
" <td>0.0024</td>\n",
|
||
|
|
" <td>12.8625</td>\n",
|
||
|
|
" <td>0.0011</td>\n",
|
||
|
|
" <td>0.2826</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>min</th>\n",
|
||
|
|
" <td>1815.0300</td>\n",
|
||
|
|
" <td>-0.0139</td>\n",
|
||
|
|
" <td>9.3349</td>\n",
|
||
|
|
" <td>0.0010</td>\n",
|
||
|
|
" <td>0.0501</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>25%</th>\n",
|
||
|
|
" <td>2314.1300</td>\n",
|
||
|
|
" <td>-0.0009</td>\n",
|
||
|
|
" <td>44.2780</td>\n",
|
||
|
|
" <td>0.0021</td>\n",
|
||
|
|
" <td>0.2687</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>50%</th>\n",
|
||
|
|
" <td>2649.6800</td>\n",
|
||
|
|
" <td>0.0004</td>\n",
|
||
|
|
" <td>52.7205</td>\n",
|
||
|
|
" <td>0.0025</td>\n",
|
||
|
|
" <td>0.4054</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>75%</th>\n",
|
||
|
|
" <td>3328.6725</td>\n",
|
||
|
|
" <td>0.0018</td>\n",
|
||
|
|
" <td>61.7292</td>\n",
|
||
|
|
" <td>0.0032</td>\n",
|
||
|
|
" <td>0.6146</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>max</th>\n",
|
||
|
|
" <td>4546.6900</td>\n",
|
||
|
|
" <td>0.0100</td>\n",
|
||
|
|
" <td>89.2458</td>\n",
|
||
|
|
" <td>0.0103</td>\n",
|
||
|
|
" <td>1.8456</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </tbody>\n",
|
||
|
|
"</table>\n",
|
||
|
|
"</div>"
|
||
|
|
],
|
||
|
|
"text/plain": [
|
||
|
|
" close ema_distance rsi atr_rel volatility_ratio\n",
|
||
|
|
"count 13384.0000 13384.0000 13384.0000 13384.0000 13384.0000\n",
|
||
|
|
"mean 2804.1304 0.0004 52.8647 0.0028 0.4766\n",
|
||
|
|
"std 682.1967 0.0024 12.8625 0.0011 0.2826\n",
|
||
|
|
"min 1815.0300 -0.0139 9.3349 0.0010 0.0501\n",
|
||
|
|
"25% 2314.1300 -0.0009 44.2780 0.0021 0.2687\n",
|
||
|
|
"50% 2649.6800 0.0004 52.7205 0.0025 0.4054\n",
|
||
|
|
"75% 3328.6725 0.0018 61.7292 0.0032 0.6146\n",
|
||
|
|
"max 4546.6900 0.0100 89.2458 0.0103 1.8456"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"execution_count": 12,
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "execute_result"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"def ema(s: pd.Series, period: int) -> pd.Series:\n",
|
||
|
|
" # Matches MT5 MODE_EMA\n",
|
||
|
|
" return s.ewm(span=period, adjust=False).mean()\n",
|
||
|
|
"\n",
|
||
|
|
"def rsi_wilder(close: pd.Series, period: int) -> pd.Series:\n",
|
||
|
|
" # Matches MT5 iRSI (Wilder smoothing) after warm-up\n",
|
||
|
|
" delta = close.diff()\n",
|
||
|
|
" gain = delta.clip(lower=0.0)\n",
|
||
|
|
" loss = (-delta).clip(lower=0.0)\n",
|
||
|
|
" avg_gain = gain.ewm(alpha=1.0 / period, adjust=False).mean()\n",
|
||
|
|
" avg_loss = loss.ewm(alpha=1.0 / period, adjust=False).mean()\n",
|
||
|
|
" rs = avg_gain / avg_loss.replace(0.0, np.nan)\n",
|
||
|
|
" return (100.0 - 100.0 / (1.0 + rs)).fillna(50.0)\n",
|
||
|
|
"\n",
|
||
|
|
"def atr_wilder(df: pd.DataFrame, period: int) -> pd.Series:\n",
|
||
|
|
" # Matches MT5 iATR (Wilder smoothing) after warm-up\n",
|
||
|
|
" prev_close = df[\"close\"].shift(1)\n",
|
||
|
|
" tr = pd.concat([\n",
|
||
|
|
" df[\"high\"] - df[\"low\"],\n",
|
||
|
|
" (df[\"high\"] - prev_close).abs(),\n",
|
||
|
|
" (df[\"low\"] - prev_close).abs(),\n",
|
||
|
|
" ], axis=1).max(axis=1)\n",
|
||
|
|
" return tr.ewm(alpha=1.0 / period, adjust=False).mean()\n",
|
||
|
|
"\n",
|
||
|
|
"\n",
|
||
|
|
"feat = df.copy()\n",
|
||
|
|
"\n",
|
||
|
|
"ema_f = ema(feat[\"close\"], EMA_FAST)\n",
|
||
|
|
"ema_s = ema(feat[\"close\"], EMA_SLOW)\n",
|
||
|
|
"\n",
|
||
|
|
"feat[\"ema_fast_rel\"] = ema_f / feat[\"close\"] - 1.0\n",
|
||
|
|
"feat[\"ema_slow_rel\"] = ema_s / feat[\"close\"] - 1.0\n",
|
||
|
|
"feat[\"ema_distance\"] = (ema_f - ema_s) / feat[\"close\"]\n",
|
||
|
|
"feat[\"rsi\"] = rsi_wilder(feat[\"close\"], RSI_PERIOD)\n",
|
||
|
|
"feat[\"rsi_momentum\"] = feat[\"rsi\"].diff()\n",
|
||
|
|
"feat[\"atr_rel\"] = atr_wilder(feat, ATR_PERIOD) / feat[\"close\"]\n",
|
||
|
|
"feat[\"volatility_ratio\"] = (feat[\"close\"].rolling(VOL_FAST).std()\n",
|
||
|
|
" / feat[\"close\"].rolling(VOL_SLOW).std())\n",
|
||
|
|
"rng = (feat[\"high\"] - feat[\"low\"])\n",
|
||
|
|
"feat[\"close_range_pct\"] = ((feat[\"close\"] - feat[\"low\"]) / rng).where(rng > 0, 0.5)\n",
|
||
|
|
"\n",
|
||
|
|
"# Raw EMA columns kept ONLY for crossover detection — they are NOT model features\n",
|
||
|
|
"feat[\"_ema_f\"] = ema_f\n",
|
||
|
|
"feat[\"_ema_s\"] = ema_s\n",
|
||
|
|
"\n",
|
||
|
|
"# Drop the warm-up region (recursive smoothers + 100-bar rolling window)\n",
|
||
|
|
"feat = feat.iloc[200:].dropna(subset=[f for f in FEATURES if f != \"signal_direction\"])\n",
|
||
|
|
"feat = feat.reset_index() # positional indexing from here on\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Bars after warm-up trim: {len(feat):,}\")\n",
|
||
|
|
"feat[[\"close\", \"ema_distance\", \"rsi\", \"atr_rel\", \"volatility_ratio\"]].describe().round(4)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "e20702e2",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 3 — Label Generation (Trade Simulation)\n",
|
||
|
|
"\n",
|
||
|
|
"The most important cell in the notebook. For every EMA crossover we simulate the trade\n",
|
||
|
|
"exactly the way the EA will manage it:\n",
|
||
|
|
"\n",
|
||
|
|
"1. **Signal** — fast EMA crosses the slow EMA at the close of bar *i* (all features are\n",
|
||
|
|
" computed from bar *i*'s close, so nothing peeks into the future)\n",
|
||
|
|
"2. **Entry** — the *open of bar i+1* (the EA acts on the new bar, not on the closed one)\n",
|
||
|
|
"3. **Exit** — the close of the bar where the next *opposite* crossover appears, or after\n",
|
||
|
|
" `MAX_HOLD_BARS`, whichever comes first\n",
|
||
|
|
"4. **Label** — `1` if the trade closed in profit, `0` otherwise\n",
|
||
|
|
"\n",
|
||
|
|
"Signals too close to the end of the dataset are dropped so no trade is truncated by\n",
|
||
|
|
"\"running out of history\".\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 13,
|
||
|
|
"id": "e87576b4",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Signals simulated : 439\n",
|
||
|
|
" Buys / Sells : 219 / 220\n",
|
||
|
|
" Baseline win % : 29.2% <- what the model must beat\n",
|
||
|
|
" Avg hold (bars) : 23.7\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"text/html": [
|
||
|
|
"<div>\n",
|
||
|
|
"<style scoped>\n",
|
||
|
|
" .dataframe tbody tr th:only-of-type {\n",
|
||
|
|
" vertical-align: middle;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe tbody tr th {\n",
|
||
|
|
" vertical-align: top;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe thead th {\n",
|
||
|
|
" text-align: right;\n",
|
||
|
|
" }\n",
|
||
|
|
"</style>\n",
|
||
|
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||
|
|
" <thead>\n",
|
||
|
|
" <tr style=\"text-align: right;\">\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th>bar</th>\n",
|
||
|
|
" <th>direction</th>\n",
|
||
|
|
" <th>hold_bars</th>\n",
|
||
|
|
" <th>pnl</th>\n",
|
||
|
|
" <th>label</th>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </thead>\n",
|
||
|
|
" <tbody>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>0</th>\n",
|
||
|
|
" <td>66</td>\n",
|
||
|
|
" <td>-1</td>\n",
|
||
|
|
" <td>9</td>\n",
|
||
|
|
" <td>-1.62</td>\n",
|
||
|
|
" <td>0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>1</th>\n",
|
||
|
|
" <td>75</td>\n",
|
||
|
|
" <td>1</td>\n",
|
||
|
|
" <td>11</td>\n",
|
||
|
|
" <td>-5.37</td>\n",
|
||
|
|
" <td>0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>2</th>\n",
|
||
|
|
" <td>86</td>\n",
|
||
|
|
" <td>-1</td>\n",
|
||
|
|
" <td>32</td>\n",
|
||
|
|
" <td>1.25</td>\n",
|
||
|
|
" <td>1</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>3</th>\n",
|
||
|
|
" <td>118</td>\n",
|
||
|
|
" <td>1</td>\n",
|
||
|
|
" <td>12</td>\n",
|
||
|
|
" <td>-1.88</td>\n",
|
||
|
|
" <td>0</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>4</th>\n",
|
||
|
|
" <td>130</td>\n",
|
||
|
|
" <td>-1</td>\n",
|
||
|
|
" <td>50</td>\n",
|
||
|
|
" <td>26.99</td>\n",
|
||
|
|
" <td>1</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </tbody>\n",
|
||
|
|
"</table>\n",
|
||
|
|
"</div>"
|
||
|
|
],
|
||
|
|
"text/plain": [
|
||
|
|
" bar direction hold_bars pnl label\n",
|
||
|
|
"0 66 -1 9 -1.62 0\n",
|
||
|
|
"1 75 1 11 -5.37 0\n",
|
||
|
|
"2 86 -1 32 1.25 1\n",
|
||
|
|
"3 118 1 12 -1.88 0\n",
|
||
|
|
"4 130 -1 50 26.99 1"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"execution_count": 13,
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "execute_result"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"f_arr = feat[\"_ema_f\"].to_numpy()\n",
|
||
|
|
"s_arr = feat[\"_ema_s\"].to_numpy()\n",
|
||
|
|
"open_ = feat[\"open\"].to_numpy()\n",
|
||
|
|
"close = feat[\"close\"].to_numpy()\n",
|
||
|
|
"\n",
|
||
|
|
"# Crossover detection at the close of bar i\n",
|
||
|
|
"above = f_arr > s_arr\n",
|
||
|
|
"cross_up = above & ~np.roll(above, 1)\n",
|
||
|
|
"cross_dn = ~above & np.roll(above, 1)\n",
|
||
|
|
"cross_up[0] = cross_dn[0] = False\n",
|
||
|
|
"\n",
|
||
|
|
"signal = np.zeros(len(feat), dtype=int)\n",
|
||
|
|
"signal[cross_up] = 1\n",
|
||
|
|
"signal[cross_dn] = -1\n",
|
||
|
|
"\n",
|
||
|
|
"sig_idx = np.where(signal != 0)[0]\n",
|
||
|
|
"sig_idx = sig_idx[sig_idx < len(feat) - MAX_HOLD_BARS - 1] # no truncated trades\n",
|
||
|
|
"\n",
|
||
|
|
"records = []\n",
|
||
|
|
"for i in sig_idx:\n",
|
||
|
|
" direction = signal[i]\n",
|
||
|
|
" entry = open_[i + 1] # enter at next bar's open\n",
|
||
|
|
"\n",
|
||
|
|
" # Exit: next opposite crossover within the window, else time-stop\n",
|
||
|
|
" window = signal[i + 1 : i + 1 + MAX_HOLD_BARS]\n",
|
||
|
|
" opp = np.where(window == -direction)[0]\n",
|
||
|
|
" exit_i = (i + 1 + opp[0]) if len(opp) else (i + MAX_HOLD_BARS)\n",
|
||
|
|
"\n",
|
||
|
|
" pnl = (close[exit_i] - entry) * direction\n",
|
||
|
|
" records.append({\n",
|
||
|
|
" \"bar\": i,\n",
|
||
|
|
" \"direction\": direction,\n",
|
||
|
|
" \"hold_bars\": exit_i - i,\n",
|
||
|
|
" \"pnl\": pnl,\n",
|
||
|
|
" \"label\": int(pnl > 0),\n",
|
||
|
|
" })\n",
|
||
|
|
"\n",
|
||
|
|
"trades = pd.DataFrame(records)\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Signals simulated : {len(trades):,}\")\n",
|
||
|
|
"print(f\" Buys / Sells : {(trades.direction == 1).sum():,} / {(trades.direction == -1).sum():,}\")\n",
|
||
|
|
"print(f\" Baseline win % : {trades.label.mean() * 100:.1f}% <- what the model must beat\")\n",
|
||
|
|
"print(f\" Avg hold (bars) : {trades.hold_bars.mean():.1f}\")\n",
|
||
|
|
"trades.head()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "4dda107c",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Assemble the training matrix\n",
|
||
|
|
"\n",
|
||
|
|
"Features are read **from the signal bar** (`bar` index) — the exact snapshot the EA will\n",
|
||
|
|
"have when it queries the model live.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 14,
|
||
|
|
"id": "ce7e0c38",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"X shape: (439, 9) | positive labels: 29.2%\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"text/html": [
|
||
|
|
"<div>\n",
|
||
|
|
"<style scoped>\n",
|
||
|
|
" .dataframe tbody tr th:only-of-type {\n",
|
||
|
|
" vertical-align: middle;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe tbody tr th {\n",
|
||
|
|
" vertical-align: top;\n",
|
||
|
|
" }\n",
|
||
|
|
"\n",
|
||
|
|
" .dataframe thead th {\n",
|
||
|
|
" text-align: right;\n",
|
||
|
|
" }\n",
|
||
|
|
"</style>\n",
|
||
|
|
"<table border=\"1\" class=\"dataframe\">\n",
|
||
|
|
" <thead>\n",
|
||
|
|
" <tr style=\"text-align: right;\">\n",
|
||
|
|
" <th></th>\n",
|
||
|
|
" <th>min</th>\n",
|
||
|
|
" <th>mean</th>\n",
|
||
|
|
" <th>max</th>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </thead>\n",
|
||
|
|
" <tbody>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>ema_fast_rel</th>\n",
|
||
|
|
" <td>-0.0135</td>\n",
|
||
|
|
" <td>0.0003</td>\n",
|
||
|
|
" <td>0.0187</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>ema_slow_rel</th>\n",
|
||
|
|
" <td>-0.0146</td>\n",
|
||
|
|
" <td>0.0003</td>\n",
|
||
|
|
" <td>0.0197</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>ema_distance</th>\n",
|
||
|
|
" <td>-0.0010</td>\n",
|
||
|
|
" <td>-0.0000</td>\n",
|
||
|
|
" <td>0.0011</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>rsi</th>\n",
|
||
|
|
" <td>26.4466</td>\n",
|
||
|
|
" <td>49.6795</td>\n",
|
||
|
|
" <td>81.1874</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>rsi_momentum</th>\n",
|
||
|
|
" <td>-39.5084</td>\n",
|
||
|
|
" <td>0.3301</td>\n",
|
||
|
|
" <td>31.4615</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>atr_rel</th>\n",
|
||
|
|
" <td>0.0010</td>\n",
|
||
|
|
" <td>0.0028</td>\n",
|
||
|
|
" <td>0.0079</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>volatility_ratio</th>\n",
|
||
|
|
" <td>0.0968</td>\n",
|
||
|
|
" <td>0.3853</td>\n",
|
||
|
|
" <td>1.1030</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>close_range_pct</th>\n",
|
||
|
|
" <td>0.0000</td>\n",
|
||
|
|
" <td>0.5110</td>\n",
|
||
|
|
" <td>1.0000</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" <tr>\n",
|
||
|
|
" <th>signal_direction</th>\n",
|
||
|
|
" <td>-1.0000</td>\n",
|
||
|
|
" <td>-0.0023</td>\n",
|
||
|
|
" <td>1.0000</td>\n",
|
||
|
|
" </tr>\n",
|
||
|
|
" </tbody>\n",
|
||
|
|
"</table>\n",
|
||
|
|
"</div>"
|
||
|
|
],
|
||
|
|
"text/plain": [
|
||
|
|
" min mean max\n",
|
||
|
|
"ema_fast_rel -0.0135 0.0003 0.0187\n",
|
||
|
|
"ema_slow_rel -0.0146 0.0003 0.0197\n",
|
||
|
|
"ema_distance -0.0010 -0.0000 0.0011\n",
|
||
|
|
"rsi 26.4466 49.6795 81.1874\n",
|
||
|
|
"rsi_momentum -39.5084 0.3301 31.4615\n",
|
||
|
|
"atr_rel 0.0010 0.0028 0.0079\n",
|
||
|
|
"volatility_ratio 0.0968 0.3853 1.1030\n",
|
||
|
|
"close_range_pct 0.0000 0.5110 1.0000\n",
|
||
|
|
"signal_direction -1.0000 -0.0023 1.0000"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"execution_count": 14,
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "execute_result"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"X = feat.loc[trades[\"bar\"], [f for f in FEATURES if f != \"signal_direction\"]].reset_index(drop=True)\n",
|
||
|
|
"X[\"signal_direction\"] = trades[\"direction\"].astype(float)\n",
|
||
|
|
"X = X[FEATURES] # enforce the contract order\n",
|
||
|
|
"y = trades[\"label\"].to_numpy()\n",
|
||
|
|
"\n",
|
||
|
|
"assert list(X.columns) == FEATURES, \"Feature order violated!\"\n",
|
||
|
|
"assert X.isna().sum().sum() == 0, \"NaNs in the feature matrix!\"\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"X shape: {X.shape} | positive labels: {y.mean()*100:.1f}%\")\n",
|
||
|
|
"X.describe().T[[\"min\", \"mean\", \"max\"]].round(4)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "a24d58b2",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 4 — AutoML Training with FLAML\n",
|
||
|
|
"\n",
|
||
|
|
"Two rules keep this cell out of trouble:\n",
|
||
|
|
"\n",
|
||
|
|
"- **Chronological split.** The most recent 20% of *signals* is the hold-out set. Shuffled\n",
|
||
|
|
" splits leak regime information from the future into training and inflate every metric.\n",
|
||
|
|
"- **Explicit `eval_method` + `split_type`.** FLAML's internal validation must also respect\n",
|
||
|
|
" time ordering (`split_type=\"time\"`), and stating `eval_method=\"holdout\"` up front avoids\n",
|
||
|
|
" the auto-selection conflicts.\n",
|
||
|
|
"\n",
|
||
|
|
"`estimator_list` is restricted to `lgbm`, `xgboost`, `rf` — all three convert cleanly to\n",
|
||
|
|
"ONNX opset 12, so whatever FLAML picks, the export phase cannot fail.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 15,
|
||
|
|
"id": "846a58c7",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Train signals: 351 Test signals: 88 (most recent)\n",
|
||
|
|
"\n",
|
||
|
|
"=======================================================\n",
|
||
|
|
" Best estimator : lgbm\n",
|
||
|
|
" Best val AUC : 0.8095\n",
|
||
|
|
" Best config : {'n_estimators': 4, 'num_leaves': 101, 'min_child_samples': 8, 'learning_rate': np.float64(0.2803183062730246), 'log_max_bin': 7, 'colsample_bytree': np.float64(0.701769072892554), 'reg_alpha': np.float64(0.0031534857907903574), 'reg_lambda': np.float64(0.2306579703496806)}\n",
|
||
|
|
"=======================================================\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"from flaml import AutoML\n",
|
||
|
|
"\n",
|
||
|
|
"split = int(len(X) * (1 - TEST_FRACTION))\n",
|
||
|
|
"X_train, X_test = X.iloc[:split], X.iloc[split:]\n",
|
||
|
|
"y_train, y_test = y[:split], y[split:]\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Train signals: {len(X_train):,} Test signals: {len(X_test):,} (most recent)\")\n",
|
||
|
|
"\n",
|
||
|
|
"automl = AutoML()\n",
|
||
|
|
"automl.fit(\n",
|
||
|
|
" X_train=X_train,\n",
|
||
|
|
" y_train=y_train,\n",
|
||
|
|
" task=\"classification\",\n",
|
||
|
|
" metric=\"roc_auc\",\n",
|
||
|
|
" time_budget=FLAML_TIME_SEC,\n",
|
||
|
|
" estimator_list=[\"lgbm\", \"xgboost\", \"rf\"],\n",
|
||
|
|
" eval_method=\"holdout\",\n",
|
||
|
|
" split_type=\"time\",\n",
|
||
|
|
" seed=RANDOM_SEED,\n",
|
||
|
|
" verbose=1,\n",
|
||
|
|
")\n",
|
||
|
|
"\n",
|
||
|
|
"best_model = automl.model.estimator # the underlying sklearn-API model\n",
|
||
|
|
"print(\"\\n\" + \"=\" * 55)\n",
|
||
|
|
"print(f\" Best estimator : {automl.best_estimator}\")\n",
|
||
|
|
"print(f\" Best val AUC : {1 - automl.best_loss:.4f}\")\n",
|
||
|
|
"print(f\" Best config : {automl.best_config}\")\n",
|
||
|
|
"print(\"=\" * 55)"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "d54e0f55",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"### Evaluate on the hold-out set\n",
|
||
|
|
"\n",
|
||
|
|
"The single number that justifies this whole article is the **filtered win-rate table**:\n",
|
||
|
|
"what happens to the win rate when the EA only takes crossovers the model is confident in.\n",
|
||
|
|
"This is also how you will choose the default for the EA's `InpConfidenceThreshold` input.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 16,
|
||
|
|
"id": "57822848",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Hold-out AUC : 0.5222\n",
|
||
|
|
"Hold-out accuracy : 0.7273\n",
|
||
|
|
"Baseline win rate : 28.4% (take every crossover)\n",
|
||
|
|
"\n",
|
||
|
|
"Threshold | Trades kept | Win rate | Lift\n",
|
||
|
|
"---------------------------------------------\n",
|
||
|
|
" 0.50 | 9 | 55.6% | +27.1%\n",
|
||
|
|
" 0.55 | 6 | 66.7% | +38.3%\n",
|
||
|
|
" 0.60 | 5 | 60.0% | +31.6%\n",
|
||
|
|
" 0.65 | 1 | 100.0% | +71.6%\n",
|
||
|
|
" 0.70 | 1 | 100.0% | +71.6%\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"from sklearn.metrics import roc_auc_score, accuracy_score\n",
|
||
|
|
"\n",
|
||
|
|
"proba = best_model.predict_proba(X_test)[:, 1] # P(trade closes in profit)\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Hold-out AUC : {roc_auc_score(y_test, proba):.4f}\")\n",
|
||
|
|
"print(f\"Hold-out accuracy : {accuracy_score(y_test, (proba > 0.5).astype(int)):.4f}\")\n",
|
||
|
|
"print(f\"Baseline win rate : {y_test.mean()*100:.1f}% (take every crossover)\\n\")\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"{'Threshold':>9} | {'Trades kept':>11} | {'Win rate':>8} | {'Lift':>6}\")\n",
|
||
|
|
"print(\"-\" * 45)\n",
|
||
|
|
"for thr in [0.50, 0.55, 0.60, 0.65, 0.70]:\n",
|
||
|
|
" mask = proba > thr\n",
|
||
|
|
" if mask.sum() == 0:\n",
|
||
|
|
" print(f\"{thr:>9.2f} | {'0':>11} | n/a | n/a\"); continue\n",
|
||
|
|
" wr = y_test[mask].mean()\n",
|
||
|
|
" lift = wr - y_test.mean()\n",
|
||
|
|
" print(f\"{thr:>9.2f} | {mask.sum():>11,} | {wr*100:>7.1f}% | {lift*100:>+5.1f}%\")"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 17,
|
||
|
|
"id": "d7af7b19",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"image/png": "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
|
||
|
|
"text/plain": [
|
||
|
|
"<Figure size 800x400 with 1 Axes>"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
"metadata": {},
|
||
|
|
"output_type": "display_data"
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# Which features carry the signal?\n",
|
||
|
|
"import matplotlib.pyplot as plt\n",
|
||
|
|
"\n",
|
||
|
|
"imp = pd.Series(best_model.feature_importances_, index=FEATURES).sort_values()\n",
|
||
|
|
"ax = imp.plot(kind=\"barh\", figsize=(8, 4), title=f\"Feature importance — {automl.best_estimator}\")\n",
|
||
|
|
"ax.set_xlabel(\"importance\")\n",
|
||
|
|
"plt.tight_layout(); plt.show()"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "c9032309",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 5 — Export to ONNX (the MT5-safe way)\n",
|
||
|
|
"\n",
|
||
|
|
"Three settings prevent the three failures:\n",
|
||
|
|
"\n",
|
||
|
|
"| Setting | Prevents |\n",
|
||
|
|
"|---|---|\n",
|
||
|
|
"| `target_opset={\"\": 12, \"ai.onnx.ml\": 2}` | *\"unsupported operator\"* — tree models live in the **`ai.onnx.ml` domain** (`TreeEnsembleClassifier`), which has its own opset counter. Capping only the default domain is not enough; recent converter versions will otherwise emit `ai.onnx.ml` v5, which fails conversion or loading |\n",
|
||
|
|
"| Post-export opset stamp to 12 | MT5 rejecting the model — a pure tree graph uses no default-domain ops, so skl2onnx stamps the default domain at opset **1**; runtimes require ≥ 7 |\n",
|
||
|
|
"| `FloatTensorType([None, 9])` | dtype mismatch — MT5 feeds `float` (float32), never double |\n",
|
||
|
|
"| `zipmap: False` | `ERR_ONNX_INCORRECT_OUTPUT_SHAPE` — without it, sklearn-style converters emit probabilities as a *map* type MT5 can't read; with it, output is a plain `(N, 2)` float tensor |\n",
|
||
|
|
"\n",
|
||
|
|
"LightGBM and XGBoost aren't native sklearn models, so their converters must be registered\n",
|
||
|
|
"with `skl2onnx` first; RandomForest converts natively. One export function covers all three.\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 1,
|
||
|
|
"id": "57ee80bd",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"ename": "KeyboardInterrupt",
|
||
|
|
"evalue": "",
|
||
|
|
"output_type": "error",
|
||
|
|
"traceback": [
|
||
|
|
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
||
|
|
"\u001b[31mKeyboardInterrupt\u001b[39m Traceback (most recent call last)",
|
||
|
|
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mos\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mpathlib\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Path\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mskl2onnx\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m convert_sklearn, update_registered_converter\n\u001b[32m 4\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mskl2onnx\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mcommon\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mdata_types\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m FloatTensorType\n\u001b[32m 5\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mskl2onnx\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mcommon\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mshape_calculator\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m calculate_linear_classifier_output_shapes\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\skl2onnx\\__init__.py:16\u001b[39m\n\u001b[32m 12\u001b[39m __model_version__ = \u001b[32m0\u001b[39m\n\u001b[32m 13\u001b[39m __max_supported_opset__ = \u001b[32m22\u001b[39m \u001b[38;5;66;03m# Converters are tested up to this version.\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m16\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mconvert\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m convert_sklearn, to_onnx, wrap_as_onnx_mixin\n\u001b[32m 17\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_supported_operators\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m update_registered_converter, get_model_alias\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_parse\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m update_registered_parser\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\skl2onnx\\convert.py:8\u001b[39m\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mtyping\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Callable, Dict, List, Optional, Sequence, Set, Tuple, Union\n\u001b[32m 7\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnumpy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnp\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m8\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mbase\u001b[39;00m\n\u001b[32m 9\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mproto\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_latest_tested_opset_version\n\u001b[32m 10\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01mcommon\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_topology\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m convert_topology\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\__init__.py:70\u001b[39m\n\u001b[32m 62\u001b[39m \u001b[38;5;66;03m# `_distributor_init` allows distributors to run custom init code.\u001b[39;00m\n\u001b[32m 63\u001b[39m \u001b[38;5;66;03m# For instance, for the Windows wheel, this is used to pre-load the\u001b[39;00m\n\u001b[32m 64\u001b[39m \u001b[38;5;66;03m# vcomp shared library runtime for OpenMP embedded in the sklearn/.libs\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 67\u001b[39m \u001b[38;5;66;03m# later is linked to the OpenMP runtime to make it possible to introspect\u001b[39;00m\n\u001b[32m 68\u001b[39m \u001b[38;5;66;03m# it and importing it first would fail if the OpenMP dll cannot be found.\u001b[39;00m\n\u001b[32m 69\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m __check_build, _distributor_init \u001b[38;5;66;03m# noqa: E402 F401\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m70\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mbase\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m clone \u001b[38;5;66;03m# noqa: E402\u001b[39;00m\n\u001b[32m 71\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_show_versions\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m show_versions \u001b[38;5;66;03m# noqa: E402\u001b[39;00m\n\u001b[32m 73\u001b[39m _submodules = [\n\u001b[32m 74\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcalibration\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 75\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcallback\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 112\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mcompose\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 113\u001b[39m ]\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\base.py:20\u001b[39m\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_config\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m config_context, get_config\n\u001b[32m 19\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexceptions\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m InconsistentVersionWarning\n\u001b[32m---> \u001b[39m\u001b[32m20\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_metadata_requests\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _MetadataRequester, _routing_enabled\n\u001b[32m 21\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_missing\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m is_pandas_na, is_scalar_nan\n\u001b[32m 22\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_param_validation\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m validate_parameter_constraints\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\utils\\__init__.py:9\u001b[39m\n\u001b[32m 7\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m metadata_routing\n\u001b[32m 8\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_bunch\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Bunch\n\u001b[32m----> \u001b[39m\u001b[32m9\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_chunking\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m gen_batches, gen_even_slices\n\u001b[32m 11\u001b[39m \u001b[38;5;66;03m# Make _safe_indexing importable from here for backward compat as this particular\u001b[39;00m\n\u001b[32m 12\u001b[39m \u001b[38;5;66;03m# helper is considered semi-private and typically very useful for third-party\u001b[39;00m\n\u001b[32m 13\u001b[39m \u001b[38;5;66;03m# libraries that want to comply with scikit-learn's estimator API. In particular,\u001b[39;00m\n\u001b[32m 14\u001b[39m \u001b[38;5;66;03m# _safe_indexing was included in our public API documentation despite the leading\u001b[39;00m\n\u001b[32m 15\u001b[39m \u001b[38;5;66;03m# `_` in its name.\u001b[39;00m\n\u001b[32m 16\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_indexing\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _safe_indexing, resample, shuffle\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\utils\\_chunking.py:11\u001b[39m\n\u001b[32m 8\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnumpy\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnp\u001b[39;00m\n\u001b[32m 10\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_config\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_config\n\u001b[32m---> \u001b[39m\u001b[32m11\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_param_validation\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Interval, validate_params\n\u001b[32m 14\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mchunk_generator\u001b[39m(gen, chunksize):\n\u001b[32m 15\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Chunk generator, ``gen`` into lists of length ``chunksize``. The last\u001b[39;00m\n\u001b[32m 16\u001b[39m \u001b[33;03m chunk may have a length less than ``chunksize``.\"\"\"\u001b[39;00m\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\utils\\_param_validation.py:17\u001b[39m\n\u001b[32m 14\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscipy\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01msparse\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m csr_array, issparse\n\u001b[32m 16\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_config\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m config_context, get_config\n\u001b[32m---> \u001b[39m\u001b[32m17\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvalidation\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _is_arraylike_not_scalar\n\u001b[32m 20\u001b[39m \u001b[38;5;28;01mclass\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mInvalidParameterError\u001b[39;00m(\u001b[38;5;167;01mValueError\u001b[39;00m, \u001b[38;5;167;01mTypeError\u001b[39;00m):\n\u001b[32m 21\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"Custom exception to be raised when the parameter of a class/method/function\u001b[39;00m\n\u001b[32m 22\u001b[39m \u001b[33;03m does not have a valid type or value.\u001b[39;00m\n\u001b[32m 23\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\utils\\validation.py:24\u001b[39m\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_config \u001b[38;5;28;01mas\u001b[39;00m _get_config\n\u001b[32m 19\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexceptions\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m (\n\u001b[32m 20\u001b[39m DataConversionWarning,\n\u001b[32m 21\u001b[39m NotFittedError,\n\u001b[32m 22\u001b[39m PositiveSpectrumWarning,\n\u001b[32m 23\u001b[39m )\n\u001b[32m---> \u001b[39m\u001b[32m24\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_array_api\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m (\n\u001b[32m 25\u001b[39m _asarray_with_order,\n\u001b[32m 26\u001b[39m _is_numpy_namespace,\n\u001b[32m 27\u001b[39m _max_precision_float_dtype,\n\u001b[32m 28\u001b[39m get_namespace,\n\u001b[32m 29\u001b[39m get_namespace_and_device,\n\u001b[32m 30\u001b[39m move_to,\n\u001b[32m 31\u001b[39m )\n\u001b[32m 32\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_dataframe\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m is_pandas_df_or_series\n\u001b[32m 33\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mutils\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_isfinite\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m FiniteStatus, cy_isfinite\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\sklearn\\utils\\_array_api.py:16\u001b[39m\n\u001b[32m 14\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscipy\u001b[39;00m\n\u001b[32m 15\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscipy\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01msparse\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msp\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m16\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mscipy\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mspecial\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mas\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mspecial\u001b[39;00m\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_config\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m get_config\n\u001b[32m 19\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01msklearn\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mexternals\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m array_api_compat\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\scipy\\special\\__init__.py:788\u001b[39m\n\u001b[32m 785\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _ufuncs\n\u001b[32m 786\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_ufuncs\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m *\n\u001b[32m--> \u001b[39m\u001b[32m788\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _basic\n\u001b[32m 789\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_basic\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m *\n\u001b[32m 791\u001b[39m \u001b[38;5;66;03m# Replace some function definitions from _ufuncs and _basic\u001b[39;00m\n\u001b[32m 792\u001b[39m \u001b[38;5;66;03m# to add Array API support\u001b[39;00m\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\Python\\Python314\\site-packages\\scipy\\special\\_basic.py:20\u001b[39m\n\u001b[32m 18\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_gufuncs\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _lqn, _lqmn, _rctj, _rcty\n\u001b[32m 19\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_input_validation\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _nonneg_int_or_fail\n\u001b[32m---> \u001b[39m\u001b[32m20\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _specfun\n\u001b[32m 21\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01m.\u001b[39;00m\u001b[34;01m_comb\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m _comb_int\n\u001b[32m 24\u001b[39m __all__ = [\n\u001b[32m 25\u001b[39m \u001b[33m'\u001b[39m\u001b[33mai_zeros\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 26\u001b[39m \u001b[33m'\u001b[39m\u001b[33massoc_laguerre\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 81\u001b[39m \u001b[33m'\u001b[39m\u001b[33mzeta\u001b[39m\u001b[33m'\u001b[39m\n\u001b[32m 82\u001b[39m ]\n",
|
||
|
|
"\u001b[36mFile \u001b[39m\u001b[32m<frozen importlib._bootstrap>:648\u001b[39m, in \u001b[36mModuleSpec.parent\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 644\u001b[39m \u001b[38;5;129m@cached\u001b[39m.setter\n\u001b[32m 645\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mcached\u001b[39m(\u001b[38;5;28mself\u001b[39m, cached):\n\u001b[32m 646\u001b[39m \u001b[38;5;28mself\u001b[39m._cached = cached\n\u001b[32m--> \u001b[39m\u001b[32m648\u001b[39m \u001b[38;5;129m@property\u001b[39m\n\u001b[32m 649\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mparent\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m 650\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"The name of the module's parent.\"\"\"\u001b[39;00m\n\u001b[32m 651\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.submodule_search_locations \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n",
|
||
|
|
"\u001b[31mKeyboardInterrupt\u001b[39m: "
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"import os\n",
|
||
|
|
"from pathlib import Path\n",
|
||
|
|
"from skl2onnx import convert_sklearn, update_registered_converter\n",
|
||
|
|
"from skl2onnx.common.data_types import FloatTensorType\n",
|
||
|
|
"from skl2onnx.common.shape_calculator import calculate_linear_classifier_output_shapes\n",
|
||
|
|
"from onnxmltools.convert.lightgbm.operator_converters.LightGbm import convert_lightgbm\n",
|
||
|
|
"from onnxmltools.convert.xgboost.operator_converters.XGBoost import convert_xgboost\n",
|
||
|
|
"from lightgbm import LGBMClassifier\n",
|
||
|
|
"from xgboost import XGBClassifier\n",
|
||
|
|
"\n",
|
||
|
|
"# --- Output path (MT5 Files folder for your terminal) ---\n",
|
||
|
|
"MT5_FILES_DIR = Path(r\"C:\\Users\\wyt_coal\\AppData\\Roaming\\MetaQuotes\\Terminal\\...\\MQL5\\Files\\AutoML\")\n",
|
||
|
|
"MT5_FILES_DIR.mkdir(parents=True, exist_ok=True) # creates AutoML folder if it doesn't exist\n",
|
||
|
|
"ONNX_PATH = MT5_FILES_DIR / ONNX_FILE # full save path\n",
|
||
|
|
"\n",
|
||
|
|
"update_registered_converter(\n",
|
||
|
|
" LGBMClassifier, \"LightGbmLGBMClassifier\",\n",
|
||
|
|
" calculate_linear_classifier_output_shapes, convert_lightgbm,\n",
|
||
|
|
" options={\"nocl\": [True, False], \"zipmap\": [True, False]},\n",
|
||
|
|
")\n",
|
||
|
|
"update_registered_converter(\n",
|
||
|
|
" XGBClassifier, \"XGBoostXGBClassifier\",\n",
|
||
|
|
" calculate_linear_classifier_output_shapes, convert_xgboost,\n",
|
||
|
|
" options={\"nocl\": [True, False], \"zipmap\": [True, False]},\n",
|
||
|
|
")\n",
|
||
|
|
"\n",
|
||
|
|
"onnx_model = convert_sklearn(\n",
|
||
|
|
" best_model,\n",
|
||
|
|
" initial_types=[(\"input\", FloatTensorType([None, N_FEATURES]))],\n",
|
||
|
|
" target_opset={\"\": ONNX_OPSET, \"ai.onnx.ml\": 2},\n",
|
||
|
|
" options={id(best_model): {\"zipmap\": False}},\n",
|
||
|
|
")\n",
|
||
|
|
"\n",
|
||
|
|
"for op in onnx_model.opset_import:\n",
|
||
|
|
" if op.domain in (\"\", \"ai.onnx\"):\n",
|
||
|
|
" op.version = ONNX_OPSET\n",
|
||
|
|
"\n",
|
||
|
|
"with open(ONNX_PATH, \"wb\") as f:\n",
|
||
|
|
" f.write(onnx_model.SerializeToString())\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"[OK] Exported to '{ONNX_PATH}' ({os.path.getsize(ONNX_PATH)/1024:.1f} KB, opset {ONNX_OPSET})\")\n",
|
||
|
|
"print(\" Inputs :\", [(i.name, i.type) for i in onnx_model.graph.input])\n",
|
||
|
|
"print(\" Outputs:\", [o.name for o in onnx_model.graph.output])"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "ff6cc17f",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Phase 6 — Validate the ONNX Model Before Touching MQL5\n",
|
||
|
|
"\n",
|
||
|
|
"Never hand an unvalidated ONNX file to the EA. Two checks:\n",
|
||
|
|
"\n",
|
||
|
|
"1. **Numerical parity** — ONNX probabilities must match `predict_proba` to float32 precision\n",
|
||
|
|
"2. **The feature contract printout** — the exact spec the MQL5 code must implement\n"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 19,
|
||
|
|
"id": "40579b24",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"Samples validated : 88\n",
|
||
|
|
"Max |ONNX - sklearn| probability diff: 6.25e-08\n",
|
||
|
|
"[OK] ONNX model is numerically faithful — safe to deploy to MT5\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"import onnxruntime as ort\n",
|
||
|
|
"\n",
|
||
|
|
"sess = ort.InferenceSession(ONNX_PATH, providers=[\"CPUExecutionProvider\"]) # <-- ONNX_PATH not ONNX_FILE\n",
|
||
|
|
"input_name = sess.get_inputs()[0].name\n",
|
||
|
|
"\n",
|
||
|
|
"X32 = X_test.to_numpy().astype(np.float32)\n",
|
||
|
|
"outputs = sess.run(None, {input_name: X32})\n",
|
||
|
|
"\n",
|
||
|
|
"onnx_proba = outputs[1][:, 1]\n",
|
||
|
|
"max_diff = np.abs(onnx_proba - proba).max()\n",
|
||
|
|
"\n",
|
||
|
|
"print(f\"Samples validated : {len(X32):,}\")\n",
|
||
|
|
"print(f\"Max |ONNX - sklearn| probability diff: {max_diff:.2e}\")\n",
|
||
|
|
"assert max_diff < 1e-3, \"ONNX output diverges from the trained model!\"\n",
|
||
|
|
"print(\"[OK] ONNX model is numerically faithful — safe to deploy to MT5\")"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "code",
|
||
|
|
"execution_count": 20,
|
||
|
|
"id": "ce06b682",
|
||
|
|
"metadata": {},
|
||
|
|
"outputs": [
|
||
|
|
{
|
||
|
|
"name": "stdout",
|
||
|
|
"output_type": "stream",
|
||
|
|
"text": [
|
||
|
|
"ONNX file : ema_rsi_model.onnx\n",
|
||
|
|
"Input tensor : 'input' float32 shape (1, 9)\n",
|
||
|
|
"Output tensor #1 : probabilities, float32, shape (1, 2) -> read [0][1] = P(profit)\n",
|
||
|
|
"Confidence gate : EA input parameter (suggested default from the threshold table)\n",
|
||
|
|
"\n",
|
||
|
|
"Idx | Feature | MQL5 computation\n",
|
||
|
|
"------------------------------------------------------------------------------\n",
|
||
|
|
" 0 | ema_fast_rel | iMA(EMA,12) / close[1] - 1.0\n",
|
||
|
|
" 1 | ema_slow_rel | iMA(EMA,26) / close[1] - 1.0\n",
|
||
|
|
" 2 | ema_distance | (emaFast - emaSlow) / close[1]\n",
|
||
|
|
" 3 | rsi | iRSI(14) on bar 1\n",
|
||
|
|
" 4 | rsi_momentum | rsi[1] - rsi[2]\n",
|
||
|
|
" 5 | atr_rel | iATR(14) / close[1]\n",
|
||
|
|
" 6 | volatility_ratio | StdDev(close,20) / StdDev(close,100)\n",
|
||
|
|
" 7 | close_range_pct | (close[1]-low[1]) / (high[1]-low[1]), 0.5 if flat bar\n",
|
||
|
|
" 8 | signal_direction | +1.0 buy crossover, -1.0 sell crossover\n",
|
||
|
|
"\n",
|
||
|
|
"NOTE: 'bar 1' = the just-closed bar. The EA computes features on the closed\n",
|
||
|
|
" signal bar and enters on the current bar — mirroring the simulation.\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"source": [
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"# FEATURE CONTRACT — pin this next to your keyboard while writing the EA\n",
|
||
|
|
"# =============================================================================\n",
|
||
|
|
"print(f\"ONNX file : {ONNX_FILE}\")\n",
|
||
|
|
"print(f\"Input tensor : '{input_name}' float32 shape (1, {N_FEATURES})\")\n",
|
||
|
|
"print(f\"Output tensor #1 : probabilities, float32, shape (1, 2) -> read [0][1] = P(profit)\")\n",
|
||
|
|
"print(f\"Confidence gate : EA input parameter (suggested default from the threshold table)\")\n",
|
||
|
|
"print()\n",
|
||
|
|
"print(f\"{'Idx':>3} | {'Feature':<17} | MQL5 computation\")\n",
|
||
|
|
"print(\"-\" * 78)\n",
|
||
|
|
"contract = [\n",
|
||
|
|
" (\"ema_fast_rel\", f\"iMA(EMA,{EMA_FAST}) / close[1] - 1.0\"),\n",
|
||
|
|
" (\"ema_slow_rel\", f\"iMA(EMA,{EMA_SLOW}) / close[1] - 1.0\"),\n",
|
||
|
|
" (\"ema_distance\", \"(emaFast - emaSlow) / close[1]\"),\n",
|
||
|
|
" (\"rsi\", f\"iRSI({RSI_PERIOD}) on bar 1\"),\n",
|
||
|
|
" (\"rsi_momentum\", \"rsi[1] - rsi[2]\"),\n",
|
||
|
|
" (\"atr_rel\", f\"iATR({ATR_PERIOD}) / close[1]\"),\n",
|
||
|
|
" (\"volatility_ratio\", f\"StdDev(close,{VOL_FAST}) / StdDev(close,{VOL_SLOW})\"),\n",
|
||
|
|
" (\"close_range_pct\", \"(close[1]-low[1]) / (high[1]-low[1]), 0.5 if flat bar\"),\n",
|
||
|
|
" (\"signal_direction\", \"+1.0 buy crossover, -1.0 sell crossover\"),\n",
|
||
|
|
"]\n",
|
||
|
|
"for i, (name, mql) in enumerate(contract):\n",
|
||
|
|
" print(f\"{i:>3} | {name:<17} | {mql}\")\n",
|
||
|
|
"print()\n",
|
||
|
|
"print(\"NOTE: 'bar 1' = the just-closed bar. The EA computes features on the closed\")\n",
|
||
|
|
"print(\" signal bar and enters on the current bar — mirroring the simulation.\")"
|
||
|
|
]
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"cell_type": "markdown",
|
||
|
|
"id": "cdb3cc40",
|
||
|
|
"metadata": {},
|
||
|
|
"source": [
|
||
|
|
"---\n",
|
||
|
|
"## Next Step — MQL5 Integration\n",
|
||
|
|
"\n",
|
||
|
|
"The Python side is done. The EA will:\n",
|
||
|
|
"\n",
|
||
|
|
"1. Embed the model: `#resource \"\\\\Files\\\\ema_rsi_model.onnx\" as uchar ExtModel[]` + `OnnxCreateFromBuffer`\n",
|
||
|
|
"2. Detect the EMA crossover on the closed bar (same 12/26 logic)\n",
|
||
|
|
"3. Fill a `float[9]` array **in contract order** from the closed bar's indicators\n",
|
||
|
|
"4. Run inference, read `P(profit)` from the probability tensor\n",
|
||
|
|
"5. Trade only when `signal && confidence > InpConfidenceThreshold` (optimizable input)\n",
|
||
|
|
"6. Manage the position with the trailing stop / opposite-crossover exit\n"
|
||
|
|
]
|
||
|
|
}
|
||
|
|
],
|
||
|
|
"metadata": {
|
||
|
|
"kernelspec": {
|
||
|
|
"display_name": "Python 3 (ipykernel)",
|
||
|
|
"language": "python",
|
||
|
|
"name": "python3"
|
||
|
|
},
|
||
|
|
"language_info": {
|
||
|
|
"codemirror_mode": {
|
||
|
|
"name": "ipython",
|
||
|
|
"version": 3
|
||
|
|
},
|
||
|
|
"file_extension": ".py",
|
||
|
|
"mimetype": "text/x-python",
|
||
|
|
"name": "python",
|
||
|
|
"nbconvert_exporter": "python",
|
||
|
|
"pygments_lexer": "ipython3",
|
||
|
|
"version": "3.14.3"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"nbformat": 4,
|
||
|
|
"nbformat_minor": 5
|
||
|
|
}
|