{
"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",
" open | \n",
" high | \n",
" low | \n",
" close | \n",
" volume | \n",
"
\n",
" \n",
" | time | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 2025-12-31 21:00:00+00:00 | \n",
" 4319.94 | \n",
" 4322.98 | \n",
" 4307.89 | \n",
" 4311.38 | \n",
" 11506.0 | \n",
"
\n",
" \n",
" | 2025-12-31 22:00:00+00:00 | \n",
" 4311.39 | \n",
" 4324.47 | \n",
" 4304.65 | \n",
" 4313.32 | \n",
" 11811.0 | \n",
"
\n",
" \n",
" | 2025-12-31 23:00:00+00:00 | \n",
" 4313.37 | \n",
" 4316.86 | \n",
" 4307.08 | \n",
" 4316.86 | \n",
" 4196.0 | \n",
"
\n",
" \n",
"
\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",
" close | \n",
" ema_distance | \n",
" rsi | \n",
" atr_rel | \n",
" volatility_ratio | \n",
"
\n",
" \n",
" \n",
" \n",
" | count | \n",
" 13384.0000 | \n",
" 13384.0000 | \n",
" 13384.0000 | \n",
" 13384.0000 | \n",
" 13384.0000 | \n",
"
\n",
" \n",
" | mean | \n",
" 2804.1304 | \n",
" 0.0004 | \n",
" 52.8647 | \n",
" 0.0028 | \n",
" 0.4766 | \n",
"
\n",
" \n",
" | std | \n",
" 682.1967 | \n",
" 0.0024 | \n",
" 12.8625 | \n",
" 0.0011 | \n",
" 0.2826 | \n",
"
\n",
" \n",
" | min | \n",
" 1815.0300 | \n",
" -0.0139 | \n",
" 9.3349 | \n",
" 0.0010 | \n",
" 0.0501 | \n",
"
\n",
" \n",
" | 25% | \n",
" 2314.1300 | \n",
" -0.0009 | \n",
" 44.2780 | \n",
" 0.0021 | \n",
" 0.2687 | \n",
"
\n",
" \n",
" | 50% | \n",
" 2649.6800 | \n",
" 0.0004 | \n",
" 52.7205 | \n",
" 0.0025 | \n",
" 0.4054 | \n",
"
\n",
" \n",
" | 75% | \n",
" 3328.6725 | \n",
" 0.0018 | \n",
" 61.7292 | \n",
" 0.0032 | \n",
" 0.6146 | \n",
"
\n",
" \n",
" | max | \n",
" 4546.6900 | \n",
" 0.0100 | \n",
" 89.2458 | \n",
" 0.0103 | \n",
" 1.8456 | \n",
"
\n",
" \n",
"
\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",
" bar | \n",
" direction | \n",
" hold_bars | \n",
" pnl | \n",
" label | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 66 | \n",
" -1 | \n",
" 9 | \n",
" -1.62 | \n",
" 0 | \n",
"
\n",
" \n",
" | 1 | \n",
" 75 | \n",
" 1 | \n",
" 11 | \n",
" -5.37 | \n",
" 0 | \n",
"
\n",
" \n",
" | 2 | \n",
" 86 | \n",
" -1 | \n",
" 32 | \n",
" 1.25 | \n",
" 1 | \n",
"
\n",
" \n",
" | 3 | \n",
" 118 | \n",
" 1 | \n",
" 12 | \n",
" -1.88 | \n",
" 0 | \n",
"
\n",
" \n",
" | 4 | \n",
" 130 | \n",
" -1 | \n",
" 50 | \n",
" 26.99 | \n",
" 1 | \n",
"
\n",
" \n",
"
\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",
" min | \n",
" mean | \n",
" max | \n",
"
\n",
" \n",
" \n",
" \n",
" | ema_fast_rel | \n",
" -0.0135 | \n",
" 0.0003 | \n",
" 0.0187 | \n",
"
\n",
" \n",
" | ema_slow_rel | \n",
" -0.0146 | \n",
" 0.0003 | \n",
" 0.0197 | \n",
"
\n",
" \n",
" | ema_distance | \n",
" -0.0010 | \n",
" -0.0000 | \n",
" 0.0011 | \n",
"
\n",
" \n",
" | rsi | \n",
" 26.4466 | \n",
" 49.6795 | \n",
" 81.1874 | \n",
"
\n",
" \n",
" | rsi_momentum | \n",
" -39.5084 | \n",
" 0.3301 | \n",
" 31.4615 | \n",
"
\n",
" \n",
" | atr_rel | \n",
" 0.0010 | \n",
" 0.0028 | \n",
" 0.0079 | \n",
"
\n",
" \n",
" | volatility_ratio | \n",
" 0.0968 | \n",
" 0.3853 | \n",
" 1.1030 | \n",
"
\n",
" \n",
" | close_range_pct | \n",
" 0.0000 | \n",
" 0.5110 | \n",
" 1.0000 | \n",
"
\n",
" \n",
" | signal_direction | \n",
" -1.0000 | \n",
" -0.0023 | \n",
" 1.0000 | \n",
"
\n",
" \n",
"
\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": "iVBORw0KGgoAAAANSUhEUgAAAxYAAAGGCAYAAADmRxfNAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAS0NJREFUeJzt3Qd8FOX2+P+TUEIChN6F0HuvIiBV6QKiKEWKVJESmoBKrxfw0rxS74WrIkWkKCpVehOQJk1AIuhFQUpCr/t/nef72/3vhpRNJsluks/79ZpLdnZ29pmZ5Pqcec55xsdms9kEAAAAACzwtfJhAAAAACCwAAAAABAnGLEAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAIB4s3jxYvHx8ZGQkBDOchJm5TrbP3vw4MF4aRuAhENgAQBxyN5JimgZNmxYvJzrPXv2yOjRo+XmzZvxsv/k7O7du+bcbtu2zdNNAQCvl9LTDQCApGjs2LFSoEABl3WlS5eOt8BizJgx0rlzZ8mYMaN4k7feekvefPNN8fPzk8QaWOi5VXXq1PF0cwDAqxFYAEA8aNy4sVSuXDlRn9s7d+5I2rRpLe0jRYoUZklsnj59Kg8fPvR0MwAgUSEVCgA84Pvvv5datWqZjnv69OmladOmcuLECZdtjh07ZkYhChYsKGnSpJGcOXPK22+/LdeuXXNso2k6Q4YMMT/rCIk97Upz3XXRnzU9Kzxdr5913o+uO3nypLRr104yZcokNWvWdLz/+eefS6VKlcTf318yZ85sRiEuXboUq9z7/PnzS7NmzUx6kQZfus8yZco40o1WrVplXusx63cePnzYZZ96TtKlSye//vqrNGzY0JzD3Llzm1Eim832THA0aNAgyZs3rxk1KVasmEybNu2Z7bSNffr0kSVLlkipUqXMtnPnzpVs2bKZ93XUwn5u7efNnevjfG7PnTvnGFXKkCGDdOnSxYyIhKfnumrVqhIQEGCuw4svvigbN26M8e+PNwRneux6bfRY6tata36/9PrreQhPz0XPnj0lS5YsEhgYKB07dpQbN264bGP1dwdA/GLEAgDiQWhoqPz9998u67JmzWr+/eyzz6RTp06mU/yPf/zDdKjmzJljOvLaEdLOk9q0aZPpPGsHVDut2nGcP3+++Xffvn2ms/rqq6/KL7/8IkuXLpXp06c7vkM7xFevXo1xu19//XUpUqSITJw40dH5njBhgowYMULatGkj3bp1M/udPXu26fBqe2OTfqWdbA1gtCPZoUMH09lv3ry56cy///770rt3b7PdpEmTzPeeOXNGfH3//3thT548kUaNGsnzzz8vU6ZMkfXr18uoUaPk8ePHJsBQ2v5XXnlFtm7dKl27dpXy5cvLhg0bTCD2xx9/mPPl7IcffpAVK1aYAEPPY7ly5cx1eeedd6RVq1bmXKuyZcu6fX2c6XFo8KfH9NNPP8nChQsle/bs5nfATgMY7Yy/8MIL5jhSp04t+/fvN217+eWXY/T742nDhw8310avq7b16NGj5t/79+9HuL2ed/1d0uPX663H9Ntvv5mgwflcWv3dARCPbACAOLNo0SLtjUe4qFu3btkyZsxo6969u8vn/vzzT1uGDBlc1t+9e/eZ/S9dutTsa8eOHY51U6dONesuXLjgsq2+1vXapvB0/ahRoxyv9Wdd17ZtW5ftQkJCbClSpLBNmDDBZf3x48dtKVOmfGZ9ZOfDuW1BQUFm3Z49exzrNmzYYNb5+/vbfvvtN8f6efPmmfVbt251rOvUqZNZ17dvX8e6p0+f2po2bWpLnTq17erVq2bdmjVrzHbjx493adNrr71m8/HxsZ07d87lfPj6+tpOnDjhsq3uK/y5iun1sZ/bt99+22XbVq1a2bJkyeJ4ffbsWdMGXf/kyROXbfX4Yvr7k5DCX2dtj/5+tGzZ0mW70aNHm+30Gob/bKVKlWwPHz50rJ8yZYpZv3bt2jj73QEQvwjhASAe/Otf/zJ3tJ0Xpf/q7E1t27Y1Ixr2ResQqlWrZu6u22mah53e5dXt9A690jve8aFXr14urzW1RFNa9M6vc3v1Dr2ObDi3NyZKliwp1atXd7zWY1f16tWTfPnyPbNeRwYiusMdPpVJ6yI2b95s1n333XfmvPbr18/lc5oapbGEphM5q127tmmXu2J6fcKfW01l0rSpsLAw83rNmjXmXI8cOfKZO+z2O/Yx+f3xpC1btpjRI/vogV3fvn0j/UyPHj0kVapUjtc6UpQyZUpzHeP6dwdA/CAVCgDigebIR1S8ffbsWUcnKCKaW253/fp1kxqzbNkyuXLlyjOpVvEh/ExW2l7thGsQERHnjmBMOHcAldYcKK2FiGh9+Fx77XhrbYOzokWLmn/t9RyaRqP5/VqD4KxEiRKO96M69ujE9PqEP2atn7Afm1738+fPm+OKKriJye9PeJo+Fpv0OKWBi73exB32c1u4cGGX9VqfYz/u8ML/jmkdTa5cuZ55NobV3x0A8YfAAgASkN6RtufJ613/8PQOrZ2OEuhUsloToPUB2tHSz2ttgX0/UQmf4+/cwXTnLry9vbofvbsf0exO2qbYiGymqMjWhy+2jg/hjz06Mb0+cXFsMfn9CU+L7WMaPNkFBQV5zUMOvfF3B8D/IbAAgARUqFAh868W7TZo0CDS7fQuq6aT6B1xTY0Jf8fanQDCfmc4/IPzwt+pj6692jHTDql9RMAbaAdbU1yc26RF7MpevKydYU2LunXrlsuoxenTpx3vRyeycxuT6xOTc63HpTMnaaAS2Tbu/P5ERAMRe0peTMU06LKfWy20dg5mNPUrshEEPXc6c5Td7du35fLly9KkSZNYtRlAwqPGAgASkM6Ko+kqOuvSo0ePnnnfnqpiv/sa/m7rjBkznvmM/VkT4QMI/R6d3WjHjh0u6z/55BO326szIWlbtAMdvi36OvzUqgnp448/dmmLvtbUrPr165t12iHV0Rnn7ZTOBqUBgz5rJDo6TWpE5zYm18ddLVu2NKlQOhtU+BEP+/e4+/sTEZ2CVYOR2Cw1atSI0bHoNdDRE53ZyVn4a+FMZ9RyPib9rNZpuHOdAHgHRiwAIAFpp1A7TPpE6ooVK5rnQWju+sWLF+Xbb781HTjtfOl2Op2rTtepna08efKYZxlcuHDhmX3qfP3qgw8+MPvTzrVOv6kBh04PO3nyZPOv1nxokGG/s+8OvUM+fvx4M3WopsJo51fv/ms7Vq9ebQpuBw8eLAlNO8k6xaxOu6pFupqqpedPpxu11wLoOdA74HpetO06fayew7Vr10pwcLDj7n90d+q15mH58uVmdERrBPQJ6rq4e33cpfUI2tZx48aZwm4N6vR5GgcOHDC1Ijp9qru/P56WI0cO6d+/v3z00Udmyl9ND9PpZvU6abAb0UiQFt5rQGKfIlYDYJ1CVz8PIHEgsACABKZz8GtHUTv8U6dOlQcPHpiOqXYm9ZkIdl988YWZRUdnmNI71vocA+2Y6WedValSxXRGdR5/7Wzr3W7t4GpgoWk6ehd75cqV5hkNevdX96GpNO4aNmyY6VTrnX4dubAXymp7PNXp0xEDPVadOUhrHDTY0edYOKcl6d3/r7/+2qzTwGDRokUmTUrPuc4M5S593oRehwEDBpjOr36PBhbuXp+Y0NEKTR3S54RokKEjJvrcDA0kYvr742n6jA1t/4IFC0xKms7kpMGXBgsaGIanAZE+oFCvlwZrOvPVrFmzIk1HA+B9fHTOWU83AgAAd+lTmzVQ0hx8JC6aUqa1PzoKpoETgKSFGgsAABDn7t2798w6ew1KnTp1OONAEkQqFAAAiHOafrZ48WJTRK9T8e7atUuWLl1qUsZiWgwOIHEgsAAAAHFOa0N0ZigtcNeni9sLujUNCkDSRI0FAAAAAMuosQAAAABgGYEFAAAAAMuosYCDzn3/v//9z8wHz7zhAAAAsNlscuvWLfP8HH0+EIEF3KJBhT70CgAAAHB26dIlee655yQqjFjAQUcq7L84gYGBnBkAAIBkLiwszNx4tvcTo0JgAQd7+pMGFQQWAAAAsHMnTZ7ibQAAAACWEVgAAAAAsIzAAgAAAIBlBBYAAAAALCOwAAAAAGAZgQUAAAAAy5huFs8oPWqD+PoFcGYAAAC8RMjkpuLtGLEAAAAAYBmBBQAAAIDEHVjkz59fZsyY4ckmAAAAAEjsNRYHDhyQtGnTSnK0bds2qVu3rty4cUMyZszo6eYAAAAA3htYPHz4UFKnTh3p+9myZYvPrwcAAACQGFOh6tSpI3369JHg4GDJmjWrNGzYUEaPHi358uUTPz8/yZ07t/Tr1y9WqVA+Pj4yb948adasmQQEBEiJEiVk7969cu7cOfO9OvLxwgsvyPnz510+N2fOHClUqJAJcIoVKyafffZZnOx37dq1UrFiRUmTJo0ULFhQxowZI48fP3bZ78KFC6VVq1Zmv0WKFJGvv/7avBcSEmJGK1SmTJnMtp07d470nJQvX96cR6ttBgAAABJNjcV///tf04nfvXu3NGrUSKZPn246wWfPnpU1a9ZImTJlYr3vcePGSceOHeXIkSNSvHhxadeunfTs2VOGDx8uBw8eFJvNZgIbu9WrV0v//v1l0KBB8vPPP5ttu3TpIlu3brW03507d5rtdd8nT540x7d48WKZMGGCy3412GjTpo0cO3ZMmjRpIu3bt5fr169L3rx55auvvjLbnDlzRi5fviwzZ86M13MRkQcPHkhYWJjLAgAAAHhFYKF35qdMmWJGB1KlSiU5c+aUBg0amFGLqlWrSvfu3WO9bw0KtKNetGhRGTp0qLnzr511HRnRu/ba0dfaBbtp06aZkYDevXubzwwcOFBeffVVs97KfjVgGDZsmHTq1MmMVrz00kumo68BhjP97rZt20rhwoVl4sSJcvv2bfnxxx8lRYoUkjlzZrNN9uzZzTnKkCFDvJ6LiEyaNMl8r33RgAcAAADwisCiUqVKjp9ff/11uXfvnul8a0ChIwjO6UIxVbZsWcfPOXLkMP86j4Douvv37zvuvJ86dUpq1Kjhsg99reut7Pfo0aMyduxYSZcunWPR49ORh7t370a4X01PCgwMlCtXrsT6+K20OSI6uhEaGupYLl26FCdtAwAAQPIT58XbzrM86R1wTfXZvHmzbNq0yYwcTJ06VbZv325GM2LK+TNaZxDZuqdPn8brfnXkQUctdPQjPK25iGi/9v1E1zZfX1+TxuTs0aNHltscEa170QUAAADw+ulm/f39pXnz5mZ59913TT3A8ePHTeFzfNOUIK310JQlO31dsmRJS/vVtmvApClOsWWfLevJkyfPzJSlIx92OuJw4cIFC60FAAAAEnlgoQXN2nGuVq2amb3o888/N4FGUFCQJIQhQ4aYOoQKFSqYOo9vvvlGVq1aZUZQrBg5cqSZkUnrRl577TUzyqDpUVogPn78eLf2oedARxXWrVtnCrv1vGhKVb169cx500BMn2+h36U1GQAAAECyffK2dowXLFhg6hq0JkA79Nq5z5IliySEli1bmtmWtFi7VKlSprh60aJFZkpWK7RAWgOCjRs3SpUqVeT55583s1/FJGDKkyePowhc6yHsMzhp3UPt2rVN4NK0aVNzDDpdLgAAAODNfGzhE/qRbGnalZkdKniF+PoFeLo5AAAA+H9CJjcVT/YPdaIfnYjIYyMWAAAAAJIHrwgslixZ4jJ1q/OiKUwAAAAAvJtXpELdunVL/vrrrwjf0ylUE6rYO7mLyVAXAAAAkr6wGPQP4326WXekT5/eLAAAAAASJ69IhQIAAACQuBFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAsIzAAgAAAIBlBBYAAAAALCOwAAAAAGAZgQUAAAAAywgsAAAAAFiW0voukNSUHrVBfP0CPN0MAADiTcjkppxdII4xYgEAAADAMgILAAAAAJYRWCRh+fPnlxkzZni6GQAAAEgGqLFIwg4cOCBp06b1dDMAAACQDBBYJGIPHz6U1KlTR/p+tmzZErQ9AAAASL5IhUpE6tSpI3369JHg4GDJmjWrNGzYUEaPHi358uUTPz8/yZ07t/Tr18+xPalQAAAASCgEFonMf//7XzNKsXv3bmnUqJFMnz5d5s2bJ2fPnpU1a9ZImTJl3N7XgwcPJCwszGUBAAAAYoNUqESmSJEiMmXKFPNzqlSpJGfOnNKgQQPzs45cVK1a1e19TZo0ScaMGROPrQUAAEBywYhFIlOpUiXHz6+//rrcu3dPChYsKN27d5fVq1fL48eP3d7X8OHDJTQ01LFcunQpnloNAACApI7AIpFxnuUpb968cubMGfnkk0/E399fevfuLS+++KI8evTIrX1pXUZgYKDLAgAAAMQGgUUipwFF8+bNZdasWbJt2zbZu3evHD9+3NPNAgAAQDJDjUUitnjxYnny5IlUq1ZNAgIC5PPPPzeBRlBQkKebBgAAgGSGEYtELGPGjLJgwQKpUaOGlC1bVjZv3izffPONZMmSxdNNAwAAQDLjY7PZbJ5uBLyDTjebIUMGyRu8Qnz9AjzdHAAA4k3I5KacXSAG/UOd6Ce6elxGLAAAAABYRmABAAAAwDKKt/GMn8c0ZOpZAAAAxAgjFgAAAAAsI7AAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAsCyl9V0gqSk9aoP4+gV4uhkAAMSbkMlNObtAHGPEAgAAAIBlBBYAAAAALCOwAAAAAJD0AwsfHx9Zs2ZNnO4nJCTEvD5y5Ih5vW3bNvP65s2bkljUqVNHgoODPd0MAAAAIHEEFjE1evRoKV++/DPrL1++LI0bN47wMy+88IJ5P0OGDOb14sWLJWPGjOINIgt6Vq1aJePGjfNYuwAAAIBkOStUzpw5I30vderUUb4fHx4+fGi+N7YyZ84cp+0BAAAAvHbEYv78+ZI7d255+vSpy/oWLVrI22+/bX6eM2eOFCpUyHSyixUrJp999lmU+xw6dKgULVpUAgICpGDBgjJixAh59OiRY6RhzJgxcvToUXOXXxddF11KlfOogP7cpUsXCQ0NdexDR0HGjh0rpUuXfuazOjqibYhO586dpWXLljJhwgRzTvRYlR5v5cqVJX369Ca4adeunVy5csWRslW3bl3zc6ZMmUxbdD8RpULduHFDOnbsaLbTc6OjM2fPno22XQAAAIDXBxavv/66XLt2TbZu3epYd/36dVm/fr20b99eVq9eLf3795dBgwbJzz//LD179jSdeuftw9MOuAYLJ0+elJkzZ8qCBQtk+vTp5r033njD7KtUqVImtUkXXRcTmhY1Y8YMCQwMdOxj8ODBJhA6deqUHDhwwLHt4cOH5dixY6bN7tiyZYucOXNGNm3aJOvWrTPrNCjSlCYNhjTw0WDCHjzkzZtXvvrqK/Ozfk7bosccEf3MwYMH5euvv5a9e/eKzWaTJk2aOIKuiDx48EDCwsJcFgAAAMDrUqH07rneOf/iiy+kfv36Zt3KlSsla9as5k58rVq1TIe4d+/e5r2BAwfKvn37ZNq0aY479eF9+OGHjp/z589vOv3Lli2T9957T/z9/SVdunSSMmXKWKc26ciJ1lro6IDzPnS/DRs2lEWLFkmVKlXMOv25du3aZuTEHWnTppWFCxe6pEDZR26U7mfWrFlm/7dv3zbfaU95yp49e6R1HzoyoQHF7t27TWCklixZYgITDVY0wIvIpEmTzAgPAAAA4PXF2zoyoXfd9e64vcP75ptviq+vrxkBqFGjhsv2+lrXR2b58uVmG+30a8dbA42LFy9KQujevbssXbpU7t+/b2okNGByDgyiU6ZMmWfqKg4dOiTNmzeXfPnymdEYDVRUTI5Jz5cGU9WqVXOsy5Ili0m3iupcDh8+3KR82ZdLly65/Z0AAABAggYW2mnWtJxvv/3WdFx37txpgo3Y0BQf/aym+GgqkaYiffDBB6aTnxD0WPz8/EwK1zfffGPSjF577TW3P68jFs7u3LljRkE07UoDLk2z0n2rhDgmPRb9bucFAAAA8MpZodKkSSOvvvqq6TifO3fO3EWvWLGiea9EiRImfadTp06O7fV1yZIlI9zXnj17JCgoyAQTdr/99pvLNjoi8OTJE0ttjmwfOiqgbdUUKN1GR140/Sq2Tp8+bWpQJk+ebNKWlNZJhG+LiuqY9Dw+fvxY9u/f70iF0v1qXUZk5xIAAABIdNPN6ihDs2bN5MSJE9KhQwfH+iFDhkibNm2kQoUK0qBBAzMKoM9n2Lx5c4T7KVKkiEkR0poKrUPQURD7HX7nuosLFy6Yh98999xzJr1I78zHhO5Daxy02LpcuXJmliVdVLdu3UxH3h4EWaHpTxo4zJ49W3r16mUK2MM/m0IDKa330BEaHamx15GEPy8605amas2bN88c87BhwyRPnjxmPQAAAJAkHpBXr149U4Ssd9B1OlU7nX5VZznSYm2dyUk7xToaoFOpRuSVV16RAQMGSJ8+fcw0rzqCEX6q19atW0ujRo1M8Xe2bNlMTURM6V1/7ejrjFK6jylTprh04vX94sWLu9Q0xIbuW2e4+vLLL83Igo5c6LlwpsGBFlhroJAjRw5z7BHR81apUiUTwFWvXt2kn3333XeSKlUqS20EAAAA3OFj0x4o3KanS4MLnclKZ7FKSnS6WZ0RK2/wCvH1+78RGgAAkqKQyU093QQgUfUPdaKf6Opxk82Tt+PC1atXTRrWn3/+6fazKwAAAIDkgMAiBvRZEvoMDn2iuD6jw1n4ugdn33//vXlmR2Lx85j/m6kKAAAAcBeBRQxElTWmxeKR0ToJAAAAICkjsIgjhQsXjqtdAQAAAIlOgswKBQAAACBpI7AAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWpbS+CyQ1pUdtEF+/AE83AwDgpUImN/V0EwB4IUYsAAAAAFhGYAEAAADAMgKLaJw+fVqef/55SZMmjZQvX14Sk/z588uMGTM83QwAAAAkAwQW0Rg1apSkTZtWzpw5I1u2bLF8whcvXiwZM2a0vB8AAADAmxBYROP8+fNSs2ZNCQoKkixZsog3ePjwoaebAAAAAHhvYPH06VOZNGmSFChQQPz9/aVcuXKycuVK8962bdvEx8dHNmzYIBUqVDDv16tXT65cuSLff/+9lChRQgIDA6Vdu3Zy9+5dxz7Xr19vAgMdJdDAoFmzZiZYcId+36FDh2Ts2LHm59GjR5v1Q4cOlaJFi0pAQIAULFhQRowYIY8ePXJ87ujRo1K3bl1Jnz69aVOlSpXk4MGD5hi6dOkioaGhZn/O+4wupWncuHHSsWNHs78ePXqY9bt27ZJatWqZc5E3b17p16+f3LlzJ8bnHQAAAEhSgYUGFZ9++qnMnTtXTpw4IQMGDJAOHTrI9u3bHdtoR/zjjz+WPXv2yKVLl6RNmzamjuCLL76Qb7/9VjZu3CizZ892bK8d7YEDB5qOvaYy+fr6SqtWrUwQE53Lly9LqVKlZNCgQebnwYMHm/UaMGhK08mTJ2XmzJmyYMECmT59uuNz7du3l+eee04OHDhgApNhw4ZJqlSp5IUXXjBt1eBA9+e8z+hMmzbNBFqHDx82gYwGR40aNZLWrVvLsWPHZPny5SbQ6NOnTwzPOgAAAGCdj81ms4kXePDggWTOnFk2b94s1atXd6zv1q2bGYHQu/Q6CqDv169f37w3efJkGT58uOlk68iB6tWrl4SEhJiRioj8/fffki1bNjl+/LiULl062nZpwXbLli2jHFnQTv+yZctM8KI0cNDgplOnTs9sqwFJcHCw3Lx5U9ylIxY6SrN69WqX85IiRQqZN2+eY50GFrVr1zbBlBab6+f0u3SJ7JzrYhcWFmZGPvIGr+A5FgCASPEcCyD5CAsLkwwZMpiMG+3jJooRi3PnzpkA4qWXXpJ06dI5Fh3BcE5dKlu2rOPnHDlyONKRnNdpepTd2bNnpW3btmYbPRna2VYXL16MdVt1dKBGjRqSM2dO08YPP/zQZX86QqId/wYNGpjgx93Uq6hUrlzZ5bWmW2mQ4nyuGjZsaEZiLly44PYIkf6i2BcNKgAAAIDY8JrA4vbt2+ZfTWc6cuSIY9F0I3udhdKUIjutUXB+bV/nnObUvHlzuX79uklX2r9/v1msFEDv3bvXpDo1adJE1q1bZ1KTPvjgA5f96eiGpnI1bdpUfvjhBylZsqTLaENs6MxU4c9Xz549Xc6VBhsaSBUqVMitfepoj0af9kVTywAAAIDYSCleQjvffn5+5s6/pvOEF5u7/teuXTPTxGpQoUXO9nQhK7S2Q2eI0mDC7rfffntmOy3u1kXrRHTEZNGiRaa2I3Xq1PLkyROxqmLFiiboKly4cKz3oedbFwAAACDJBBZaEK2FzNoR1xEHnclJ76Lv3r3bpDBpZz6mMmXKZGaCmj9/vuTKlcsELVpIbUWRIkXMfrSmokqVKmaExXk04t69ezJkyBB57bXXzOxWv//+uyni1iJrpalYOtqgheRajK2pXLrElM5MpQ/u02JtTbvSEQ0NNDZt2mSK2wEAAIBkmQqldEpVnfFIc/91+lid9Ug77tpBjw2dAUoDAJ2ZSQu1NWiZOnWqpTa+8sorZj/aodfCbh3B0DbbaUG1jpTo1LA6YqGzVjVu3FjGjBlj3teZobTA/I033jBF5FOmTIlVO7TWRGfL+uWXX8xojBZ3jxw5UnLnzm3p+AAAAIBEPSsUvKfqn1mhAABRYVYoIPkIS4yzQgEAAABIvJJ1YDFx4kSX6VqdF01fSgg7d+6MtA26AAAAAIlBsk6F0mlodYmIv7+/5MmTJ97boMXef/zxR6TvW5n1KT6HugAAAJD0hcWgf+g1s0J5gj7pWxdP0gAmIYMHAAAAID4k61QoAAAAAHGDwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAsIzAAgAAAIBlBBYAAAAALCOwAAAAAGAZgQUAAAAAywgsAAAAAFhGYAEAAADAspTWd4GkpvSoDeLrF+DpZgBAgguZ3JSzDgCxxIgFAAAAAMsILAAAAABYRmCRhOXPn19mzJjh6WYAAAAgGSCwiCchISHi4+MjR44cia+vAAAAALwGgYWHPXz4MEE+AwAAAMQnAgsL1q9fLzVr1pSMGTNKlixZpFmzZnL+/HnzXoECBcy/FSpUMCMXderUMa87d+4sLVu2lAkTJkju3LmlWLFibqU0jRs3Tjp27CiBgYHSo0cPs37Xrl1Sq1Yt8ff3l7x580q/fv3kzp07Vg4JAAAAiBUCCwu0Ez9w4EA5ePCgbNmyRXx9faVVq1by9OlT+fHHH802mzdvlsuXL8uqVascn9Ntz5w5I5s2bZJ169a59V3Tpk2TcuXKyeHDh2XEiBEmgGnUqJG0bt1ajh07JsuXLzeBRp8+fawcEgAAABArPMfCAu3UO/vPf/4j2bJlk5MnT5p/lY5k5MyZ02W7tGnTysKFCyV16tRuf1e9evVk0KBBjtfdunWT9u3bS3BwsHldpEgRmTVrltSuXVvmzJkjadKkiXafDx48MItdWFiY2+0BAAAAnDFiYcHZs2elbdu2UrBgQZOipClL6uLFi1F+rkyZMjEKKlTlypVdXh89elQWL14s6dKlcywNGzY0oyUXLlxwa5+TJk2SDBkyOBZNpwIAAABigxELC5o3by5BQUGyYMECUy+hnfrSpUtHW1ytIxYxFf4zt2/flp49e5q6ivDy5cvn1j6HDx9uUrmcRywILgAAABAbBBaxdO3aNVMnoUGFFlArrXGws49IPHnyROJDxYoVTcpV4cKFY70PPz8/swAAAABWkQoVS5kyZTL1E/Pnz5dz587JDz/84HL3P3v27Ga2Jp056q+//pLQ0FCJS0OHDpU9e/aYYm19VoamZa1du5bibQAAAHgEgUVsT5yvryxbtkwOHTpk0p8GDBggU6dOdbyfMmVKU0w9b948kybVokULiUtly5aV7du3yy+//GJGTHRa25EjR5rvAgAAABKaj81msyX4t8IraY2FKeIOXiG+fgGebg4AJLiQyU056wAQQf9Qs290sqKoMGIBAAAAwDICCw/buXOny5Sx4RcAAAAgMSAVysPu3bsnf/zxR6TvW5n1KT6HugAAAJD0hcWgf8h0sx6mM0clZPAAAAAAxAdSoQAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAsIzAAgAAAIBlBBYAAAAALCOwAAAAAGAZgQUAAAAAywgsAAAAAFhGYAEAAADAMgILAAAAAJYRWAAAAACwjMACAAAAgGUEFgAAAAAsI7AAAAAAYFlK67tAUlN61Abx9QvwdDMAwEXI5KacEQDwYoxYAAAAALCMwAIAAACAZQQW4YSEhIiPj48cOXLEvN62bZt5ffPmTetnGwAAAEiiCCyi8cILL8jly5clQ4YM0Z5MghAAAAAkVxRvRyN16tSSM2fOhLkaAAAAQCKVoCMWT58+lUmTJkmBAgXE399fypUrJytXrnS5279hwwapUKGCeb9evXpy5coV+f7776VEiRISGBgo7dq1k7t37zr2uX79eqlZs6ZkzJhRsmTJIs2aNZPz58+73aYff/zRfF+aNGmkcuXKcvjw4ShHIX777Tdp3ry5ZMqUSdKmTSulSpWS7777zqRQ1a1b12yj7+lnOnfu7FYb7elXq1atMvsICAgw52bv3r0ubdm9e7fUqVPHvK/f0bBhQ7lx40a05xYAAABIUiMW2vH9/PPPZe7cuVKkSBHZsWOHdOjQQbJly+bYZvTo0fLxxx+bznObNm3M4ufnJ1988YXcvn1bWrVqJbNnz5ahQ4ea7e/cuSMDBw6UsmXLmvdHjhxpttEaCV/fqOMm3V47+S+99JJp14ULF6R///5Rfubdd9+Vhw8fmrZrYHHy5ElJly6d5M2bV7766itp3bq1nDlzxgRB2sGPSRs/+OADmTZtmjk3+nPbtm3l3LlzkjJlSrNt/fr15e2335aZM2eadVu3bpUnT55Ee25r164d4bE8ePDALHZhYWFuXUcAAAAgPB+bzWaTBKAd2MyZM8vmzZulevXqjvXdunUzIxA9evQwd+v1fe1Aq8mTJ8vw4cPN3f2CBQuadb169TJ3+HUUICJ///236UwfP35cSpcuHWWb5s+fL++//778/vvvZsRCacf8nXfeMSMX5cuXNyMW2i4dGdARBw0ONHgYNWrUM/sLv21kwrdRj0dHGhYuXChdu3Y122jAoqMhp06dkuLFi5uRmosXL8quXbtifG41KIuIBnFjxox5Zn3e4BU8xwKA1+E5FgCQ8PTGs9Yah4aGmhvnXpEKpXfetZOrowN6h9++fPrppy5pQdpxt8uRI4cZubAHFfZ1mh5ld/bsWXNnX7fRg82fP79Zr53w6GinXb/PHlQo5455RPr16yfjx4+XGjVqmODi2LFj0X6Pu210PvZcuXKZf+3Hah+xsHJuw9OgTX9J7MulS5eiPRYAAADAo6lQmgKkvv32W8mTJ4/Le5rqZO8Ap0qVyrFe6w6cX9vXaT2BndY7BAUFyYIFCyR37tzmPR0F0HSl+KCjAFrboMexceNGk4L00UcfSd++fSP9jLttDH/syn6s9rSq2JzbyOh7Ub0PAAAAuCvBRixKlixpOrF6l75w4cIui9YnxMa1a9dMPcOHH35o7uZrgbe9mNkdur2OONy/f9+xbt++fdF+TturKVlabD1o0CATMNhnkFL2uoe4aKPzaMaWLVsS7NwCAAAAXjlikT59ehk8eLAMGDDA3IXXWZI0/UZnOtL0IL2jH1M6M5LOsqS1Epo6pB3rYcOGuf15rVvQIunu3bubtCCtddDi6agEBwdL48aNpWjRoiZA0AJqDRaUHoOONKxbt06aNGliRhmsttFO21emTBnp3bu3CWo0iNHvfv311yVr1qxRnttOnTrF+PsAAAAAr51udty4cTJixAiTPqSd8UaNGpn0HS1cjg2dUWnZsmVy6NAhk1qkHeupU6e6/XmtQ/jmm29MEbVOOatBxj/+8Y8oP6OjETozlL39GmB88skn5j1NQ9JiaA0ctBakT58+lttop9+jqVdHjx6VqlWrmlqQtWvXmtmh4uPcAgAAAF45KxQST9U/s0IB8EbMCgUACc8rZ4UCAAAAkHQl6cBi4sSJLtOvOi9aJwEAAAAgbiTpVKjr16+bJSJaWB1+atbkLiZDXQAAAEj6YtI/TLBZoTxBn0atCwAAAID4laRToQAAAAAkDAILAAAAAJYRWAAAAACwjMACAAAAgGUEFgAAAAAsI7AAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMtSWt8FkprSozaIr1+Ap5sBIA6FTG7K+QQAxCtGLAAAAABYRmABAAAAwHsCi5CQEPHx8ZEjR47E1S4BAAAAJBKMWCRBBHkAAABIaMkmsHj06JGnmwAAAAAkWTEOLJ4+fSpTpkyRwoULi5+fn+TLl08mTJgQ4bbbt2+XqlWrmu1y5colw4YNk8ePHzveX7lypZQpU0b8/f0lS5Ys0qBBA7lz547j/YULF0qJEiUkTZo0Urx4cfnkk09idMd++fLlUrt2bfP5JUuWyLVr16Rt27aSJ08eCQgIMN+9dOlSl8/WqVNH+vXrJ++9955kzpxZcubMKaNHj3bZ5vTp01KzZk2z35IlS8rmzZvN961Zs8axzaVLl6RNmzaSMWNGs58WLVqYdrmjc+fO0rJlSxkzZoxky5ZNAgMDpVevXvLw4UO3rkOBAgXMvxUqVDDt0mMCAAAAvGq62eHDh8uCBQtk+vTppnN9+fJl09EO748//pAmTZqYTvKnn35qtunevbvpjGtHXT+nnXztHLdq1Upu3bolO3fuFJvNZj6vgcDIkSPl448/Nh3kw4cPm8+nTZtWOnXq5FZbNZD56KOPzOf1e+/fvy+VKlWSoUOHms76t99+K2+99ZYUKlTIBEB2//3vf2XgwIGyf/9+2bt3rzmGGjVqyEsvvSRPnjwxnX7tyOv72u5BgwY9MzrSsGFDqV69ujmmlClTyvjx46VRo0Zy7NgxSZ06dbRt37Jli2nztm3bTEDSpUsXE3zZg4eorsOPP/5ojkcDnlKlSkX6fQ8ePDCLXVhYmFvnFQAAAAjPx2bvybtBO9F6B107+926dXN5Tzu/eqdcA4Dy5cvLBx98IF999ZWcOnXK3DVXOuKgnfrQ0FBT5K2dfP1cUFDQM9+ld+LHjRtngg877Zx/9913smfPnijbaW/LjBkzpH///lFu26xZMzMaMm3aNPNa7+5r8KABgZ120uvVqyeTJ0+W9evXS/Pmzc2IhI5mKO3Aa9CxevVqE3R8/vnnpq3Ox66jDTp6oaMaL7/8cpRt0kDmm2++Md+hIytq7ty5MmTIEHPudFQnsusQ0bWIjAZ4OioSXt7gFTzHAkhieI4FACA29MZzhgwZTB9Ub8zH2YiFdpT1Dnf9+vXd2lbv2Ns71krv+t++fVt+//13KVeunNmPpiPp3X3tbL/22muSKVMm03E+f/68dO3a1YxS2GkalR6YuypXruzyWgOGiRMnyooVK8yIinb29XjsnXe7smXLurzWNK4rV66Yn8+cOSN58+Z1BBXKebRDHT16VM6dOyfp06d3Wa8jJnpc7tDz49wuPZd67jTY+Ouvv9y+DlHRUQ8dmXH+xdFjAwAAAGIqRoGF1kLElRQpUsimTZvM6MPGjRtl9uzZZpRD04vsHWpN9alWrdozn3OXpk05mzp1qsycOdOMZGhAo+8HBwe71C6oVKlSubzW4EhrGtylAYCOxmg6V3g60mBVXF0Hrc3QBQAAAEjQ4u0iRYqYTq3m/0dHi661PsE502r37t3mLv5zzz3n6LDrKIam42jajtYCaDpRjhw5JHfu3PLrr7+alCjnxV6YHBv6/VpE3aFDBzMiULBgQfnll19itI9ixYo5Rg3sDhw44LJNxYoV5ezZs5I9e/Zn2u/uiIuOety7d8/xet++fZIuXTozohDddbDXVOgIDQAAAOB1gYUWE2uNhM6YpAXZmtajHd5///vfz2zbu3dv0wHv27evKSpeu3atjBo1yqTe+Pr6mpEJTUs6ePCgXLx4UVatWiVXr141AYnSYGPSpEkya9Ys0/k/fvy4LFq0SP75z3/G+mC1Q24fJdFUrZ49e7oECO7QWgot9tYCci3E1mDlww8/NO/Z077at28vWbNmNUGM1mpcuHDBFGHrbFOaBuYOHUXRVLCTJ0+auhI9d3369DHnLrrroAGNBh5aD6LHpzlxAAAAgFfNCjVixAgzy5HO2PS///3P1B/oVKjh6ZSu2iHWgmMdHdApV7WjbO+Ea/HHjh07TFqS5vZrAbfO4NS4cWPzvhYla0qUpi/pPjRtSdOXNHUptvS7dRREazp03z169DDF1jHpeGsqlhZga/uqVKliRj20jVrQrR1+pfvWY9PO/6uvvmqK3vV8aE1EdEUvdrqtBkIvvviiqafQInbnaW+jug66XgOysWPHmvdr1aplAhsAAADAK2aFQsR01EKnfNWCbR3NsEpnhbp586bLczESsuqfWaGApIdZoQAAXjUrFP6P1oFovYOOKGgwoVPaaq1IXAQVAAAAQLJ48rY30NoM7dhHtNhTqeKTpja9++675vkXOrqgKVFaQ+KuyNqui/PzMwAAAIDEIlGmQl2/ft0sEdGiZa1n8GY6yhEZbXtcTusbX0NdAAAASPrCknoqlBaC65JY6bSzAAAAQFKSKFOhAAAAAHgXAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAsIzAAgAAAIBlBBYAAAAALCOwAAAAAGAZgQUAAAAAy1Ja3wWSmtKjNoivX4Cnm5HshUxumuzPAQAASDwYsQAAAABgGYEFAAAAAMsILGLAx8dH1qxZI4nFtm3bTJtv3rzp6aYAAAAgiSOwAAAAAGAZgUUiZLPZ5PHjx55uBgAAAOAdgcXTp09l0qRJUqBAAfH395dy5crJypUrXdJ4NmzYIBUqVDDv16tXT65cuSLff/+9lChRQgIDA6Vdu3Zy9+5dxz7Xr18vNWvWlIwZM0qWLFmkWbNmcv78ebfa8/DhQ+nTp4/kypVL0qRJI0FBQaZ9kTl+/Lhpk7ZNv6tHjx5y+/Zt897PP/8svr6+cvXqVfP6+vXr5vWbb77p+Pz48eNNW6NjPxd63JUqVRI/Pz/ZtWtXlOcPAAAASDbTzWqn+PPPP5e5c+dKkSJFZMeOHdKhQwfJli2bY5vRo0fLxx9/LAEBAdKmTRuzaMf6iy++MJ34Vq1ayezZs2Xo0KFm+zt37sjAgQOlbNmy5v2RI0eabY4cOWI69lGZNWuWfP3117JixQrJly+fXLp0ySwR0e9p2LChVK9eXQ4cOGACnm7dupnAZPHixVKqVCkTbGzfvl1ee+012blzp+O1nf5cp04dt8/XsGHDZNq0aVKwYEHJlClTlOevdu3a0e7vwYMHZrELCwtzuy0AAACAVwQW2qGdOHGibN682XTOlXaY9U78vHnzzN1/+139GjVqmJ+7du0qw4cPNyMQuq3STvvWrVsdgUXr1q1dvuc///mP6WifPHlSSpcuHWWbLl68aDroOoqgIwQ6YhEZDWzu378vn376qaRNm9as0wCoefPm8o9//ENy5MghL774ohlt0Dbqv126dJGFCxfK6dOnpVChQrJnzx5577333D5nY8eOlZdeesmt8+dOYKGByZgxY9z+fgAAAMDrUqHOnTtnUpi0o5wuXTrHoh1159QlHXmw0866jlzYgwr7Oh0tsDt79qy0bdvWbKOpUvnz53cEDdHp3LmzGdkoVqyY9OvXTzZu3BjptqdOnTKpR/agQmkApOlJZ86cMa+1c68BhX10QtOm7MGGjnI8evTIETS5o3LlyjE+f1HRIC00NNSxRDY6AwAAAHjtiIW9FuHbb7+VPHnyuLynqU72znGqVKkc63UUwfm1fZ125u10xEBHGhYsWCC5c+c27+lIhdZPRKdixYpy4cIFU8ugIwGadtWgQYNY1y1omlNwcLAJdnTEREdCdLRCA4sbN26YQEEDJXc5BzHRnT936HbubgsAAAB4ZWBRsmRJ06nVkYSI0nbcvevu7Nq1a2a0QIOKWrVqmXWaGhQTOsrxxhtvmEVTmBo1amQKrzNnzuyynRaPay2F1lrYO/y7d+82dRw64qHKlCljaiE0nat8+fJmREGDDU2V0sAiJvUVMT1/AAAAQLIILNKnTy+DBw+WAQMGmFEFvZuv6TjaOdfOfVT1DZHRTrwWSM+fP9/M7KSdbi14dtc///lP8zmdhUoDhC+//FJy5sxpZpgKr3379jJq1Cjp1KmTKTDX2Z/69u0rb731lknPso+maOrTkiVLzLHaU7u0PmLLli2myDy+zp+2CwAAAEgWs0KNGzfOFFZrEfGvv/5qOvCajvT++++7pDe5S4OBZcuWmfoITX/SkQOd6cndkQHtrE+ZMsWkLqVIkUKqVKki3333XYSzSWkKk06F279/f7OdvtbCcQ1OnOlogj6t294G3ZcGG5rCFJP6ipiePwAAACAh+dj0aWvA/5tuNkOGDJI3eIX4+rlf+4H4ETK5KacWAAB4Rf9QM2M0KyYqPHkbAAAAQOJOhUpo+twHXSKixd46G5Qn9OrVyzzoLiL6wDt9AF5C+nlMw2gjUgAAACDZpkLp7E66RMTf3/+ZaVsTij6HI7KnXmsHP3v27F431AUAAICkLywG/cNkNWKhU8aGnzbWG2jgkFDBAwAAABAfqLEAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAABgGYEFAAAAAMsILAAAAABYRmABAAAAwDICCwAAAACWEVgAAAAAILAAAAAA4HmMWAAAAACwjMACAAAAgHcGFp07d5aWLVtKQhs9erSUL18+ztpdp04dCQ4OFk/w5HcDAAAAMZVS4sHMmTPFZrNJYrdq1SpJlSpVvH7Htm3bpG7dunLjxg3JmDFjgn43AAAA4NWBRYYMGSQpyJw5c5TvP3z4UFKnTu2R7wYAAACSTCrUypUrpUyZMuLv7y9ZsmSRBg0ayJ07d55JKbp165a0b99e0qZNK7ly5ZLp06c/k+qTP39+mThxorz99tuSPn16yZcvn8yfP9/l+4YOHSpFixaVgIAAKViwoIwYMUIePXoUq7Y/efJEBg4caEYJtO3vvffeM6MsEbVx3Lhx0rFjRwkMDJQePXqY9bt27ZJatWqZ85A3b17p16+fOQ92Dx48MG3X9/z8/KRw4cLy73//W0JCQsxohcqUKZP4+PiYcxfRd+uIhn6vbqfH37hxYzl79qzj/cWLF5tj2bBhg5QoUULSpUsnjRo1ksuXL8fq/AAAAAAJElhoh7Vt27YmEDh16pRJ6Xn11VcjTIHSDvzu3bvl66+/lk2bNsnOnTvlp59+ema7jz76SCpXriyHDx+W3r17yzvvvCNnzpxxvK8Bh3agT548adKtFixYYIKU2NDv0n395z//MYHB9evXZfXq1dF+btq0aVKuXDnTRg1szp8/bzrwrVu3lmPHjsny5cvN/vr06eP4jAYES5culVmzZplzNW/ePNPx10Djq6++Mtvoceo51eOKiAYcBw8eNOdw79695jw3adLEJbC6e/euad9nn30mO3bskIsXL8rgwYNjdX4AAACAGLHF0qFDhzSCsIWEhDzzXqdOnWwtWrQwP4eFhdlSpUpl+/LLLx3v37x50xYQEGDr37+/Y11QUJCtQ4cOjtdPnz61Zc+e3TZnzpxI2zB16lRbpUqVHK9HjRplK1eunFvtz5Url23KlCmO148ePbI999xzjnar2rVrP9PGli1buuyna9euth49eris27lzp83X19d2794925kzZ8x52rRpU4Tt2Lp1q3n/xo0bLuudv/uXX34x2+zevdvx/t9//23z9/e3rVixwrxetGiR2ebcuXOObf71r3/ZcuTIEek5uH//vi00NNSxXLp0yexDfwYAAABCQ0Pd7h/GusZC79rXr1/fpEI1bNhQXn75ZXnttddMqo6zX3/91dxVr1q1qksNRrFixZ7ZZ9myZR0/a1pQzpw55cqVK451Ohqgd/11lOD27dvy+PFjk5IUU6GhoWZ0oFq1ao51KVOmNKMl0RWd6zbOjh49akYqlixZ4lin+3j69KlcuHBBjh8/LilSpJDatWtLbOkoh7bPub2avqXnUN+z0xSpQoUKOV5r2pnz+Qtv0qRJMmbMmFi3CwAAALCcCqWdZU1r+v7776VkyZIye/Zs09HVznRshZ8FSYML7aArTf/ROg1N/1m3bp1JRfrggw9MAXVC0joRZxrg9OzZU44cOeJYNNjQ+gft5GvdRUKJ6PxFFSgNHz7cBFn25dKlSwnQSgAAACRFloq3teNao0YNc9dbO/o6Q1L4OgUtstYO74EDBxzrtBP7yy+/xOi79uzZI0FBQSaY0FGDIkWKyG+//RarduuIid7N379/v2Odjn4cOnQoxvuqWLGiqfnQguzwi54PHdHR4Gj79u0Rft4+q5QWk0dGi7G1fc7tvXbtmqnL0KAutrSQXEd8nBcAAAAgQQML7eTqLE5aUKxFwvrchatXr5pOsDMtuO7UqZMMGTJEtm7dKidOnJCuXbuKr6+vCUzcpYGEfs+yZctMKpSmRLlTbB2Z/v37y+TJk2XNmjVy+vRpUyx+8+bNGO9HZ3vSoEeLtXW0Qkcq1q5d6yje1pmk9Pi1yF2/S0d0tNB9xYoV5n0NlvQ86CiMnj8dAYno2Fu0aCHdu3c3heE6ItKhQwfJkyePWQ8AAAAk2sBC727rzEOamqRTwH744YdmpiWdBjW8f/7zn1K9enVp1qyZmZJWRzk0AEmTJo3b3/fKK6/IgAEDTIddn66tnXmdlSm2Bg0aJG+99Zbp9GvbNABq1apVjPejdSE6GqEjMDrlbIUKFWTkyJGSO3duxzZz5swx9ScavBQvXtwECPbpaDU40BGfYcOGSY4cOVxmk3K2aNEiqVSpkjmH2l5Ncfruu+94iB4AAAC8go9WcCf0l2qnWjvUGojo6AW8Q1hYmEkT01Q10qIAAAAQFoP+Ybw8eTs8rb/QdCOdGUobNXbsWLOeNB4AAAAgaUiQwELpg9u02FiLlTWlRx+SlzVr1nj7Pn0AXWR0JitNWwIAAACQiAILrTuIzYxLVmghdWQ0DQsAAABAIhyxSGg63SsAAACARPAcCwAAAAAgsAAAAAAQJxixAAAAAGAZgQUAAAAAywgsAAAAAFhGYAEAAADAsiQ73SxizmazOR7dDgAAAIT9v36hvZ8YFQILOFy7ds38mzdvXs4KAAAAHG7duiUZMmSQqBBYwCFz5szm34sXL0b7i4PEf/dBA8hLly5JYGCgp5uDeMb1Tj641skL1zv5CPPgf7d1pEKDity5c0e7LYEFHHx9/6/kRoMKOpvJg15nrnXywfVOPrjWyQvXO/kI9NB/t9294UzxNgAAAADLCCwAAAAAWEZgAQc/Pz8ZNWqU+RdJG9c6eeF6Jx9c6+SF6518+CWSPpqPzZ25owAAAAAgCoxYAAAAALCMwAIAAACAZQQWAAAAACwjsIDxr3/9S/Lnzy9p0qSRatWqyY8//siZSYJGjx4tPj4+Lkvx4sU93SzEkR07dkjz5s3NQ4z02q5Zs8blfS2pGzlypOTKlUv8/f2lQYMGcvbsWc5/ErzWnTt3fuZvvVGjRh5rL2Jv0qRJUqVKFUmfPr1kz55dWrZsKWfOnHHZ5v79+/Luu+9KlixZJF26dNK6dWv566+/OO1J8FrXqVPnmb/tXr16ibcgsIAsX75cBg4caGYb+Omnn6RcuXLSsGFDuXLlCmcnCSpVqpRcvnzZsezatcvTTUIcuXPnjvn71RsFEZkyZYrMmjVL5s6dK/v375e0adOav3XtlCBpXWulgYTz3/rSpUsTtI2IG9u3bzdBw759+2TTpk3y6NEjefnll83vgN2AAQPkm2++kS+//NJs/7///U9effVVLkESvNaqe/fuLn/b+v/tXkNnhULyVrVqVdu7777reP3kyRNb7ty5bZMmTfJouxD3Ro0aZStXrhynNhnQ/3tfvXq14/XTp09tOXPmtE2dOtWx7ubNmzY/Pz/b0qVLPdRKxMe1Vp06dbK1aNGCE5wEXblyxVzz7du3O/6OU6VKZfvyyy8d25w6dcpss3fvXg+2FHF9rVXt2rVt/fv3t3krRiySuYcPH8qhQ4dMSoSdr6+veb13716Ptg3xQ1NfNH2iYMGC0r59e7l48SKnOhm4cOGC/Pnnny5/6xkyZDCpj/ytJ03btm0z6RTFihWTd955R65du+bpJiEOhIaGmn8zZ85s/tX/huudbee/bU1xzZcvH3/bSexa2y1ZskSyZs0qpUuXluHDh8vdu3fFW6T0dAPgWX///bc8efJEcuTI4bJeX58+fdpj7UL80E7k4sWLTUdDh0/HjBkjtWrVkp9//tnkdCLp0qBCRfS3bn8PSYemQWkqTIECBeT8+fPy/vvvS+PGjU1HM0WKFJ5uHmLp6dOnEhwcLDVq1DCdSqV/v6lTp5aMGTO6bMvfdtK71qpdu3YSFBRkbhAeO3ZMhg4dauowVq1aJd6AwAJIRrRjYVe2bFkTaOj/Qa1YsUK6du3q0bYBiDtvvvmm4+cyZcqYv/dChQqZUYz69etzqhMpzb/XG0HUxiXfa92jRw+Xv22djEP/pvUGgv6NexqpUMmcDqXp3avws0fo65w5c3qsXUgYeoeraNGicu7cOU55Emf/e+ZvPXnS1Ef9/3v+1hOvPn36yLp162Tr1q3y3HPPufxta1rzzZs3Xbbnv+NJ71pHRG8QKm/52yawSOZ0+LRSpUqyZcsWl+E3fV29enWPtg3x7/bt2+Yuh97xQNKmKTHaAXH+Ww8LCzOzQ/G3nvT9/vvvpsaCv/XER+vztaO5evVq+eGHH8zfsjP9b3iqVKlc/rY1NUbr5/jbTlrXOiJHjhwx/3rL3zapUDBTzXbq1EkqV64sVatWlRkzZpipzbp06cLZSWIGDx5s5r7X9CedjlCnGNYRq7Zt23q6aYijQNH5rpUWbOt/dLTwTws5NV93/PjxUqRIEfMfrBEjRpg8XZ0rHUnnWuui9VP6LAMNJvXmwXvvvSeFCxc20wsj8aXEfPHFF7J27VpTC2evidLJF/R5NPqvprLqf8v12gcGBkrfvn1NUPH88897uvmIw2utf8v6fpMmTcwzS7TGQqcafvHFF026o1fw9LRU8A6zZ8+25cuXz5Y6dWoz/ey+ffs83STEgzfeeMOWK1cuc53z5MljXp87d45znURs3brVTE0YftGpR+1Tzo4YMcKWI0cOM81s/fr1bWfOnPF0sxHH1/ru3bu2l19+2ZYtWzYzDWlQUJCte/futj///JNznQhFdJ11WbRokWObe/fu2Xr37m3LlCmTLSAgwNaqVSvb5cuXPdpuxP21vnjxou3FF1+0Zc6c2fx/eOHChW1DhgyxhYaG2ryFj/6Pp4MbAAAAAIkbNRYAAAAALCOwAAAAAGAZgQUAAAAAywgsAAAAAFhGYAEAAADAMgILAAAAAJYRWAAAAACwjMACAAAAgGUEFgAAj6lTp44EBwdzBQAgCeDJ2wAAj7l+/bqkSpVK0qdP73VXYdu2bVK3bl25ceOGZMyY0dPNAQCvl9LTDQAAJF+ZM2cWb/To0SNPNwEAEh1SoQAAXpEKlT9/fhk/frx07NhR0qVLJ0FBQfL111/L1atXpUWLFmZd2bJl5eDBg47PL1682IwmrFmzRooUKSJp0qSRhg0byqVLl1y+Z86cOVKoUCFJnTq1FCtWTD777DOX9318fMw2r7zyiqRNm1a6d+9uRitUpkyZzPudO3c2r9evXy81a9Y035slSxZp1qyZnD9/3rGvkJAQs/2qVavMPgICAqRcuXKyd+9el+/cvXu3OX59X79D262jI+rp06cyadIkKVCggPj7+5vPr1y5Ms7PPwDEJQILAIDXmD59utSoUUMOHz4sTZs2lbfeessEGh06dJCffvrJBAf62mazOT5z9+5dmTBhgnz66aems37z5k158803He+vXr1a+vfvL4MGDZKff/5ZevbsKV26dJGtW7e6fPfo0aOlVatWcvz4cRkzZox89dVXZv2ZM2fk8uXLMnPmTPP6zp07MnDgQBPgbNmyRXx9fc3nNBhw9sEHH8jgwYPlyJEjUrRoUWnbtq08fvzYvKfr6tevLyVLljQBx65du6R58+by5MkT874GFXo8c+fOlRMnTsiAAQPMOdi+fXs8nn0AsMgGAICH1K5d29a/f3/zc1BQkK1Dhw6O9y5fvqzRg23EiBGOdXv37jXr9D21aNEi83rfvn2ObU6dOmXW7d+/37x+4YUXbN27d3f53tdff93WpEkTx2vdPjg42GWbrVu3mvU3btyI8hiuXr1qtjt+/Lh5feHCBfN64cKFjm1OnDhh1mnbVNu2bW01atSIcH/379+3BQQE2Pbs2eOyvmvXruZzAOCtGLEAAHgNTXWyy5Ejh/m3TJkyz6y7cuWKY13KlCmlSpUqjtfFixc3aUqnTp0yr/VfHQVxpq/t79tVrlzZrTaePXvWjD4ULFhQAgMDTQqXunjxYqTHkitXLpd220csInLu3DkzCvPSSy+Z9C/7oiMYzilXAOBtKN4GAHgNnSHKTusUIlsXPu0oLmhthTs0ZUnrPxYsWCC5c+c2bSldurQ8fPjQZbuo2q11E5G5ffu2+ffbb7+VPHnyuLzn5+cXgyMCgITFiAUAIFHTugXngm6tidA6ixIlSpjX+q/WXjjT11rfEBUt9Fb2ugd17do1s/8PP/zQjDjovu0F1zGhoxlanxERbZcGEDoCUrhwYZclb968Mf4uAEgojFgAABI1HRno27evzJo1y6RF9enTR55//nmpWrWqeX/IkCHSpk0bqVChgjRo0EC++eYbM2PT5s2bo9yvjkroSMO6deukSZMmZpRBZ2/SmaDmz59v0pu08z9s2LAYt3n48OEmxat3797Sq1cvE8RoMfnrr78uWbNmNUXfWrCtIxw6A1VoaKgJhjT1qlOnTrE+VwAQnxixAAAkajpd69ChQ6Vdu3amdkLrEZYvX+54v2XLlmZGp2nTpkmpUqVk3rx5smjRIjPVa1Q0DUlnh9LAQWs7NGDRGaCWLVsmhw4dMulP2vmfOnVqjNuss0Rt3LhRjh49agKg6tWry9q1a01gpMaNGycjRowws0PpqEijRo1MapROPwsA3oonbwMAEi19joU+B0NTnwAAnsWIBQAAAADLCCwAAAAAWEYqFAAAAADLGLEAAAAAYBmBBQAAAADLCCwAAAAAWEZgAQAAAMAyAgsAAAAAlhFYAAAAALCMwAIAAACAZQQWAAAAACwjsAAAAAAgVv1/Q5+uUCnPwowAAAAASUVORK5CYII=",
"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
}