{ "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": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
openhighlowclosevolume
time
2025-12-31 21:00:00+00:004319.944322.984307.894311.3811506.0
2025-12-31 22:00:00+00:004311.394324.474304.654313.3211811.0
2025-12-31 23:00:00+00:004313.374316.864307.084316.864196.0
\n", "
" ], "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": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
closeema_distancersiatr_relvolatility_ratio
count13384.000013384.000013384.000013384.000013384.0000
mean2804.13040.000452.86470.00280.4766
std682.19670.002412.86250.00110.2826
min1815.0300-0.01399.33490.00100.0501
25%2314.1300-0.000944.27800.00210.2687
50%2649.68000.000452.72050.00250.4054
75%3328.67250.001861.72920.00320.6146
max4546.69000.010089.24580.01031.8456
\n", "
" ], "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": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
bardirectionhold_barspnllabel
066-19-1.620
175111-5.370
286-1321.251
3118112-1.880
4130-15026.991
\n", "
" ], "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": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
minmeanmax
ema_fast_rel-0.01350.00030.0187
ema_slow_rel-0.01460.00030.0197
ema_distance-0.0010-0.00000.0011
rsi26.446649.679581.1874
rsi_momentum-39.50840.330131.4615
atr_rel0.00100.00280.0079
volatility_ratio0.09680.38531.1030
close_range_pct0.00000.51101.0000
signal_direction-1.0000-0.00231.0000
\n", "
" ], "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": [ "
" ] }, "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: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 }