{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "b6f1606b",
   "metadata": {},
   "source": [
    "# 3) Backtest — Walk-forward Evaluation\n",
    "\n",
    "Loads saved model and evaluates on the hold-out split using the same minimal env."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "4222de5d",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Found 3 model(s):\n",
      " - ppo_EURUSD_M15.zip  (algo=PPO)\n",
      " - a2c_EURUSD_M15.zip  (algo=A2C)\n",
      " - dqn_EURUSD_M15.zip  (algo=DQN)\n",
      "\n",
      "Default selected for single-model cells: ppo_EURUSD_M15.zip | algo=PPO\n",
      "Test set: (9958, 17)\n"
     ]
    }
   ],
   "source": [
    "# ---- Path & imports ----\n",
    "import sys, os\n",
    "sys.path.insert(0, os.path.abspath('..'))  # Path fix\n",
    "\n",
    "import os, json\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# SB3 algos (multi-algo support)\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "import gymnasium as gym\n",
    "from gymnasium import spaces\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "import features\n",
    "\n",
    "# ---- helpers ----\n",
    "def infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "def collect_model_candidates(models_dir: Path, symbol: str, timeframe: str, algo: str, explicit: str | None = None):\n",
    "    # If user explicitly passed a filename, use only that\n",
    "    if explicit:\n",
    "        p = Path(explicit)\n",
    "        if not p.is_absolute():\n",
    "            p = models_dir / p\n",
    "        return [p] if p.exists() else []\n",
    "\n",
    "    cands = []\n",
    "    if algo in ALGO_MAP:  # specific algo requested\n",
    "        cands += list(models_dir.glob(f\"{algo.lower()}_{symbol}_{timeframe}.zip\"))\n",
    "        # fallback: any file for that algo\n",
    "        if not cands:\n",
    "            cands += list(models_dir.glob(f\"{algo.lower()}_*.zip\"))\n",
    "    else:\n",
    "        # auto/all: try perfect matches for all algos first\n",
    "        for a in ALGO_MAP:\n",
    "            cands += list(models_dir.glob(f\"{a}_{symbol}_{timeframe}.zip\"))\n",
    "        # then looser matches for this symbol/timeframe\n",
    "        if not cands:\n",
    "            cands += list(models_dir.glob(f\"*_{symbol}_{timeframe}.zip\"))\n",
    "        # finally, anything under models/\n",
    "        if not cands:\n",
    "            cands += list(models_dir.glob(\"*.zip\"))\n",
    "\n",
    "    # de-dup while preserving order\n",
    "    seen = set()\n",
    "    out = []\n",
    "    for p in cands:\n",
    "        if p not in seen:\n",
    "            out.append(p)\n",
    "            seen.add(p)\n",
    "    return out\n",
    "\n",
    "# ---- config/env ----\n",
    "load_dotenv()\n",
    "SYMBOL = os.getenv(\"TRAINING_SYMBOL\", \"EURUSD\")\n",
    "TIMEFRAME = os.getenv(\"TIMEFRAME\", \"M15\")\n",
    "SPLIT_RATIO = float(os.getenv(\"SPLIT_RATIO\", \"0.8\"))\n",
    "ALGO = os.getenv(\"ALGO\", \"auto\").lower()        # \"auto\" | \"all\" | \"ppo\" | \"a2c\" | \"dqn\"\n",
    "MODEL_FILE = os.getenv(\"MODEL_FILE\", \"\").strip()  # optional explicit filename\n",
    "\n",
    "# ---- paths ----\n",
    "DATA_CSV = Path(\"data\") / f\"ohlc_{SYMBOL}_{TIMEFRAME}.csv\"\n",
    "FEAT_JSON = Path(\"models\") / \"selected_features.json\"\n",
    "MODELS_DIR = Path(\"models\")\n",
    "\n",
    "# ---- resolve models ----\n",
    "MODEL_PATHS = collect_model_candidates(MODELS_DIR, SYMBOL, TIMEFRAME, ALGO, explicit=MODEL_FILE)\n",
    "assert DATA_CSV.exists(), \"Data CSV missing. Run 1_Data.ipynb\"\n",
    "assert FEAT_JSON.exists(), \"selected_features.json missing. Run 2_Train.ipynb\"\n",
    "assert MODEL_PATHS, (\n",
    "    f\"No model zips found in {MODELS_DIR}/ for {SYMBOL} {TIMEFRAME}. \"\n",
    "    f\"Train first, or set MODEL_FILE env to a specific zip.\"\n",
    ")\n",
    "\n",
    "# Build (path, algo) bundle and pick a default for legacy cells\n",
    "MODEL_BUNDLE = [(p, infer_algo_from_name(p)) for p in MODEL_PATHS]\n",
    "MODEL_PATH, ALGO_SELECTED = MODEL_BUNDLE[0]\n",
    "\n",
    "# ---- report ----\n",
    "print(f\"Found {len(MODEL_BUNDLE)} model(s):\")\n",
    "for p, a in MODEL_BUNDLE:\n",
    "    print(f\" - {p.name}  (algo={a.upper()})\")\n",
    "print(f\"\\nDefault selected for single-model cells: {MODEL_PATH.name} | algo={ALGO_SELECTED.upper()}\")\n",
    "\n",
    "# ---- load data & features ----\n",
    "df = pd.read_csv(DATA_CSV, parse_dates=[\"time\"], index_col=\"time\")\n",
    "df_feat = features.add_indicators(df.copy())\n",
    "with open(FEAT_JSON, \"r\", encoding=\"utf-8\") as f:\n",
    "    feature_cols = json.load(f)\n",
    "\n",
    "# test split\n",
    "n_split = int(len(df_feat) * SPLIT_RATIO)\n",
    "df_test = df_feat.iloc[n_split:].copy()\n",
    "print(\"Test set:\", df_test.shape)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "8cc0ed72",
   "metadata": {},
   "outputs": [],
   "source": [
    "# same env as training (mapping: 0=SELL, 1=HOLD, 2=BUY)\n",
    "class TradingEnv(gym.Env):\n",
    "    \"\"\"\n",
    "    Discrete(3) trading environment:\n",
    "      action 0 -> position -1 (SELL)\n",
    "      action 1 -> position  0 (HOLD/FLAT)\n",
    "      action 2 -> position +1 (BUY)\n",
    "    Reward = position * return - trade_cost_on_switch\n",
    "    \"\"\"\n",
    "    metadata = {\"render_modes\": []}\n",
    "\n",
    "    def __init__(self, df_feat, feature_cols, trade_cost=1e-4):\n",
    "        super().__init__()\n",
    "        import numpy as np\n",
    "        from gymnasium import spaces\n",
    "\n",
    "        self.df = df_feat\n",
    "        self.cols = feature_cols\n",
    "        self.trade_cost = float(trade_cost)\n",
    "\n",
    "        self.n = len(self.df)\n",
    "        self.idx = 0\n",
    "        self.position = 0\n",
    "\n",
    "        self.observation_space = spaces.Box(\n",
    "            low=-np.inf, high=np.inf, shape=(len(self.cols),), dtype=np.float32\n",
    "        )\n",
    "        self.action_space = spaces.Discrete(3)\n",
    "\n",
    "        # explicit mapping to avoid ambiguity\n",
    "        self.ACTION_TO_POS = {0: -1, 1: 0, 2: 1}\n",
    "\n",
    "    def _obs(self):\n",
    "        row = self.df.iloc[self.idx][self.cols].astype(float).values\n",
    "        return row.astype(np.float32)\n",
    "\n",
    "    def reset(self, seed=None, options=None):\n",
    "        super().reset(seed=seed)\n",
    "        # start at 1 so we can compute ret using idx-1\n",
    "        self.idx = 1\n",
    "        self.position = 0\n",
    "        return self._obs(), {}\n",
    "\n",
    "    def step(self, action):\n",
    "        # map action to target position\n",
    "        pos_new = self.ACTION_TO_POS.get(int(action), 0)\n",
    "\n",
    "        prev = float(self.df[\"close\"].iloc[self.idx - 1])\n",
    "        curr = float(self.df[\"close\"].iloc[self.idx])\n",
    "        ret = (curr - prev) / (prev + 1e-12)\n",
    "\n",
    "        # apply cost only when we change position\n",
    "        cost = self.trade_cost if pos_new != self.position else 0.0\n",
    "        reward = pos_new * ret - cost\n",
    "\n",
    "        # advance state\n",
    "        self.position = pos_new\n",
    "        self.idx += 1\n",
    "        truncated = self.idx >= (self.n - 1)\n",
    "\n",
    "        return self._obs(), float(reward), False, truncated, {\n",
    "            \"ret\": ret,\n",
    "            \"position\": self.position\n",
    "        }\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "4019140f",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Backtesting ppo_EURUSD_M15.zip (algo=PPO) ===\n",
      "Sharpe (rough): 0.897  |  MaxDD: -3.73%\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 51432 characters]",
      "text/plain": [
       "<Figure size 1000x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Backtesting a2c_EURUSD_M15.zip (algo=A2C) ===\n",
      "Sharpe (rough): -1.843  |  MaxDD: -7.80%\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 50304 characters]",
      "text/plain": [
       "<Figure size 1000x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Backtesting dqn_EURUSD_M15.zip (algo=DQN) ===\n",
      "Sharpe (rough): -11.667  |  MaxDD: -29.24%\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 38420 characters]",
      "text/plain": [
       "<Figure size 1000x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Leaderboard (by Sharpe):\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>model</th>\n",
       "      <th>algo</th>\n",
       "      <th>sharpe</th>\n",
       "      <th>max_drawdown</th>\n",
       "      <th>steps</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>ppo_EURUSD_M15.zip</td>\n",
       "      <td>ppo</td>\n",
       "      <td>0.896809</td>\n",
       "      <td>-0.037317</td>\n",
       "      <td>9956</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a2c_EURUSD_M15.zip</td>\n",
       "      <td>a2c</td>\n",
       "      <td>-1.843478</td>\n",
       "      <td>-0.077969</td>\n",
       "      <td>9956</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>dqn_EURUSD_M15.zip</td>\n",
       "      <td>dqn</td>\n",
       "      <td>-11.667301</td>\n",
       "      <td>-0.292419</td>\n",
       "      <td>9956</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                model algo     sharpe  max_drawdown  steps\n",
       "0  ppo_EURUSD_M15.zip  ppo   0.896809     -0.037317   9956\n",
       "1  a2c_EURUSD_M15.zip  a2c  -1.843478     -0.077969   9956\n",
       "2  dqn_EURUSD_M15.zip  dqn -11.667301     -0.292419   9956"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# ---- Multi-model backtest (loops over all discovered models) ----\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "# Fallbacks if previous cell didn't define helpers/vars\n",
    "def _infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "if 'MODEL_BUNDLE' not in globals():\n",
    "    # try single-model fallback\n",
    "    if 'MODEL_PATH' in globals():\n",
    "        MODEL_BUNDLE = [(Path(MODEL_PATH), _infer_algo_from_name(Path(MODEL_PATH)))]\n",
    "    else:\n",
    "        raise RuntimeError(\"No models found: define MODEL_BUNDLE or MODEL_PATH first.\")\n",
    "\n",
    "def _bars_per_year(tf: str) -> float:\n",
    "    tf = str(tf).upper()\n",
    "    if tf.startswith(\"M\"):\n",
    "        m = int(tf[1:])\n",
    "        bpd = 24*60/m\n",
    "    elif tf.startswith(\"H\"):\n",
    "        m = int(tf[1:]) * 60\n",
    "        bpd = 24*60/m\n",
    "    elif tf in (\"D1\", \"1D\"):\n",
    "        bpd = 1\n",
    "    else:\n",
    "        bpd = 24*4  # ~M15 default\n",
    "    return bpd * 252.0\n",
    "\n",
    "tf_str = TIMEFRAME if isinstance(TIMEFRAME, str) else \"M15\"\n",
    "ann_factor = _bars_per_year(tf_str)\n",
    "\n",
    "results = []  # collect per-model metrics\n",
    "\n",
    "for model_path, algo in MODEL_BUNDLE:\n",
    "    algo = algo.lower()\n",
    "    if algo not in ALGO_MAP:\n",
    "        print(f\"Skipping {model_path.name}: unsupported algo '{algo}'\")\n",
    "        continue\n",
    "\n",
    "    print(f\"\\n=== Backtesting {model_path.name} (algo={algo.upper()}) ===\")\n",
    "    model = ALGO_MAP[algo].load(model_path.as_posix())\n",
    "\n",
    "    env = TradingEnv(df_test, feature_cols)\n",
    "    obs, _ = env.reset()\n",
    "    equity = [1.0]\n",
    "    rets = []\n",
    "\n",
    "    while True:\n",
    "        action, _ = model.predict(obs, deterministic=True)\n",
    "        obs, reward, terminated, truncated, info = env.step(int(action))\n",
    "        equity.append(equity[-1] * (1.0 + reward))\n",
    "        rets.append(info.get(\"ret\", reward))\n",
    "        if terminated or truncated:\n",
    "            break\n",
    "\n",
    "    equity = pd.Series(equity, index=df_test.index[:len(equity)])\n",
    "    returns = equity.pct_change().dropna()\n",
    "    sharpe = float((returns.mean() / (returns.std() + 1e-12)) * np.sqrt(ann_factor))\n",
    "    dd = (equity / equity.cummax() - 1.0)\n",
    "    mdd = float(dd.min())\n",
    "\n",
    "    print(f\"Sharpe (rough): {sharpe:.3f}  |  MaxDD: {mdd:.2%}\")\n",
    "\n",
    "    # Save metrics row\n",
    "    results.append({\n",
    "        \"model\": model_path.name,\n",
    "        \"algo\": algo,\n",
    "        \"sharpe\": sharpe,\n",
    "        \"max_drawdown\": mdd,\n",
    "        \"steps\": len(returns)\n",
    "    })\n",
    "\n",
    "    # Plot equity per model (separate figure each to keep it clear)\n",
    "    plt.figure(figsize=(10,4))\n",
    "    equity.plot(title=f\"Equity Curve — {model_path.name} ({algo.upper()})\")\n",
    "    plt.grid(True)\n",
    "    plt.show()\n",
    "\n",
    "# Optional: show a small leaderboard\n",
    "if results:\n",
    "    leaderboard = pd.DataFrame(results).sort_values(\"sharpe\", ascending=False)\n",
    "    print(\"\\nLeaderboard (by Sharpe):\")\n",
    "    display(leaderboard)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Enriched Evaluation & Plots\n",
    "\n",
    "Adds action distribution, equity vs buy & hold, underwater (drawdown), rolling Sharpe, monthly returns, and optional QuantStats export."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "================ ppo_EURUSD_M15.zip | PPO ================\n",
      "bars            9956.000000\n",
      "Sharpe             0.896809\n",
      "Sortino            1.245242\n",
      "CAGR               0.065507\n",
      "Calmar             1.755408\n",
      "MaxDD             -0.037317\n",
      "WinRate            0.476195\n",
      "ProfitFactor       1.018795\n",
      "Exposure           0.995078\n",
      "Trades           238.000000\n",
      "AvgHoldBars      847.694255\n",
      "dtype: float64\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 164160 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 88028 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "================ a2c_EURUSD_M15.zip | A2C ================\n",
      "bars            9956.000000\n",
      "Sharpe            -1.843478\n",
      "Sortino           -2.168042\n",
      "CAGR              -0.128641\n",
      "Calmar            -1.649887\n",
      "MaxDD             -0.077969\n",
      "WinRate            0.498393\n",
      "ProfitFactor       0.962365\n",
      "Exposure           0.999196\n",
      "Trades            14.000000\n",
      "AvgHoldBars     2746.865810\n",
      "dtype: float64\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 142160 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 81236 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "================ dqn_EURUSD_M15.zip | DQN ================\n",
      "bars            9956.000000\n",
      "Sharpe           -11.667301\n",
      "Sortino          -12.971816\n",
      "CAGR              -0.578343\n",
      "Calmar            -1.977823\n",
      "MaxDD             -0.292414\n",
      "WinRate            0.465448\n",
      "ProfitFactor       0.783297\n",
      "Exposure           0.983126\n",
      "Trades          1050.000000\n",
      "AvgHoldBars      666.864403\n",
      "dtype: float64\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 116480 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 67760 characters]",
      "text/plain": [
       "<Figure size 1200x900 with 3 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Leaderboard (sorted by Sharpe, then Calmar):\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>model</th>\n",
       "      <th>algo</th>\n",
       "      <th>Sharpe</th>\n",
       "      <th>Sortino</th>\n",
       "      <th>CAGR</th>\n",
       "      <th>Calmar</th>\n",
       "      <th>MaxDD</th>\n",
       "      <th>WinRate</th>\n",
       "      <th>ProfitFactor</th>\n",
       "      <th>Exposure</th>\n",
       "      <th>Trades</th>\n",
       "      <th>AvgHoldBars</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>ppo_EURUSD_M15.zip</td>\n",
       "      <td>ppo</td>\n",
       "      <td>0.896809</td>\n",
       "      <td>1.245242</td>\n",
       "      <td>0.065507</td>\n",
       "      <td>1.755408</td>\n",
       "      <td>-0.037317</td>\n",
       "      <td>0.476195</td>\n",
       "      <td>1.018795</td>\n",
       "      <td>0.995078</td>\n",
       "      <td>238</td>\n",
       "      <td>847.694255</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>a2c_EURUSD_M15.zip</td>\n",
       "      <td>a2c</td>\n",
       "      <td>-1.843478</td>\n",
       "      <td>-2.168042</td>\n",
       "      <td>-0.128641</td>\n",
       "      <td>-1.649887</td>\n",
       "      <td>-0.077969</td>\n",
       "      <td>0.498393</td>\n",
       "      <td>0.962365</td>\n",
       "      <td>0.999196</td>\n",
       "      <td>14</td>\n",
       "      <td>2746.865810</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>dqn_EURUSD_M15.zip</td>\n",
       "      <td>dqn</td>\n",
       "      <td>-11.667301</td>\n",
       "      <td>-12.971816</td>\n",
       "      <td>-0.578343</td>\n",
       "      <td>-1.977823</td>\n",
       "      <td>-0.292414</td>\n",
       "      <td>0.465448</td>\n",
       "      <td>0.783297</td>\n",
       "      <td>0.983126</td>\n",
       "      <td>1050</td>\n",
       "      <td>666.864403</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                model algo     Sharpe    Sortino      CAGR    Calmar  \\\n",
       "0  ppo_EURUSD_M15.zip  ppo   0.896809   1.245242  0.065507  1.755408   \n",
       "1  a2c_EURUSD_M15.zip  a2c  -1.843478  -2.168042 -0.128641 -1.649887   \n",
       "2  dqn_EURUSD_M15.zip  dqn -11.667301 -12.971816 -0.578343 -1.977823   \n",
       "\n",
       "      MaxDD   WinRate  ProfitFactor  Exposure  Trades  AvgHoldBars  \n",
       "0 -0.037317  0.476195      1.018795  0.995078     238   847.694255  \n",
       "1 -0.077969  0.498393      0.962365  0.999196      14  2746.865810  \n",
       "2 -0.292414  0.465448      0.783297  0.983126    1050   666.864403  "
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved metrics to: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\models\\metrics_EURUSD_M15.csv\n"
     ]
    }
   ],
   "source": [
    "# === Enriched evaluation & plots for ALL models (PPO/A2C/DQN) ===\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "from collections import Counter\n",
    "\n",
    "# SB3 loaders (in case not imported above)\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "# Fallback to single model if bundle not provided\n",
    "def _infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "if 'MODEL_BUNDLE' not in globals():\n",
    "    if 'MODEL_PATH' in globals():\n",
    "        MODEL_BUNDLE = [(Path(MODEL_PATH), _infer_algo_from_name(Path(MODEL_PATH)))]\n",
    "    else:\n",
    "        raise RuntimeError(\"No models found: define MODEL_BUNDLE or MODEL_PATH first.\")\n",
    "\n",
    "def bars_per_day(tf: str) -> int:\n",
    "    tf = str(tf).upper()\n",
    "    if tf.startswith(\"M\"):\n",
    "        try:\n",
    "            m = int(tf[1:])\n",
    "            return int((24*60)/m)\n",
    "        except Exception:\n",
    "            return 96\n",
    "    if tf.startswith(\"H\"):\n",
    "        try:\n",
    "            h = int(tf[1:])\n",
    "            return int(24/h)\n",
    "        except Exception:\n",
    "            return 24\n",
    "    if tf in (\"D1\", \"1D\"):\n",
    "        return 1\n",
    "    return 96  # default ≈ M15\n",
    "\n",
    "def ann_factor(tf: str) -> float:\n",
    "    return np.sqrt(252 * bars_per_day(tf))\n",
    "\n",
    "def CAGR(eq: pd.Series) -> float:\n",
    "    if len(eq) < 2:\n",
    "        return np.nan\n",
    "    total = float(eq.iloc[-1]) / float(eq.iloc[0])\n",
    "    yrs = (eq.index[-1] - eq.index[0]).days / 365.25\n",
    "    return (total ** (1/max(yrs, 1e-9))) - 1.0\n",
    "\n",
    "def compute_metrics(equity: pd.Series, strat_rets: pd.Series, raw_rets: pd.Series, timeframe: str, positions: pd.Series, actions: pd.Series) -> dict:\n",
    "    ann = ann_factor(timeframe)\n",
    "    sharpe = float((strat_rets.mean() / (strat_rets.std() + 1e-12)) * ann)\n",
    "    downside = strat_rets[strat_rets < 0]\n",
    "    sortino = float((strat_rets.mean() / (downside.std() + 1e-12)) * ann)\n",
    "    cummax = equity.cummax()\n",
    "    dd = equity / cummax - 1.0\n",
    "    mdd = float(dd.min())\n",
    "    cagr = CAGR(equity)\n",
    "    calmar = float(cagr / (abs(mdd) + 1e-12))\n",
    "    win_rate = float((strat_rets > 0).mean())\n",
    "    pf = float(strat_rets[strat_rets > 0].sum() / (abs(strat_rets[strat_rets < 0].sum()) + 1e-12))\n",
    "    exposure = float((positions != 0).mean())\n",
    "    # trades = number of position changes (ignore first)\n",
    "    trades = int((positions.diff().fillna(0) != 0).sum())\n",
    "    avg_hold = float((positions != 0).astype(int).groupby((positions == 0).astype(int).cumsum()).transform('size').mean()) if trades > 0 else np.nan\n",
    "    return {\n",
    "        \"Sharpe\": sharpe,\n",
    "        \"Sortino\": sortino,\n",
    "        \"CAGR\": cagr,\n",
    "        \"Calmar\": calmar,\n",
    "        \"MaxDD\": mdd,\n",
    "        \"WinRate\": win_rate,\n",
    "        \"ProfitFactor\": pf,\n",
    "        \"Exposure\": exposure,\n",
    "        \"Trades\": trades,\n",
    "        \"AvgHoldBars\": avg_hold\n",
    "    }\n",
    "\n",
    "results = []\n",
    "tf_str = TIMEFRAME if isinstance(TIMEFRAME, str) else \"M15\"\n",
    "BPD = bars_per_day(tf_str)\n",
    "\n",
    "for model_path, algo in MODEL_BUNDLE:\n",
    "    algo = algo.lower()\n",
    "    if algo not in ALGO_MAP:\n",
    "        print(f\"Skipping {model_path.name}: unsupported algo '{algo}'\")\n",
    "        continue\n",
    "\n",
    "    print(f\"\\n================ {model_path.name} | {algo.upper()} ================\")\n",
    "    model = ALGO_MAP[algo].load(Path(model_path).as_posix())\n",
    "\n",
    "    env = TradingEnv(df_test, feature_cols)\n",
    "    obs, _ = env.reset()\n",
    "\n",
    "    equity_vals = [1.0]\n",
    "    rewards = []\n",
    "    raw_rets = []\n",
    "    positions = []\n",
    "    actions = []\n",
    "\n",
    "    while True:\n",
    "        action, _ = model.predict(obs, deterministic=True)\n",
    "        obs, reward, terminated, truncated, info = env.step(int(action))\n",
    "        equity_vals.append(equity_vals[-1] * (1.0 + reward))\n",
    "        rewards.append(float(reward))\n",
    "        raw_rets.append(float(info.get(\"ret\", 0.0)))\n",
    "        positions.append(int(info.get(\"position\", 0)))\n",
    "        actions.append(int(action))\n",
    "        if terminated or truncated:\n",
    "            break\n",
    "\n",
    "    # Align per-step series with the time index\n",
    "    idx = df_test.index[1:1+len(rewards)]\n",
    "    strat_rets = pd.Series(rewards, index=idx, name=\"strategy_ret\")\n",
    "    equity = pd.Series(equity_vals[1:], index=idx, name=\"equity\")\n",
    "    pos_series = pd.Series(positions, index=idx, name=\"position\")\n",
    "    action_series = pd.Series(actions, index=idx, name=\"action\")\n",
    "    bh = pd.Series((1.0 + pd.Series(raw_rets, index=idx)).cumprod(), name=\"buy_hold\")\n",
    "\n",
    "    # Metrics\n",
    "    m = compute_metrics(equity, strat_rets, pd.Series(raw_rets, index=idx), tf_str, pos_series, action_series)\n",
    "    summary = pd.Series({\n",
    "        \"bars\": len(strat_rets),\n",
    "        **{k: (round(v, 6) if isinstance(v, float) else v) for k, v in m.items()}\n",
    "    })\n",
    "    print(summary)\n",
    "\n",
    "    # ====== Plots (2 figures per model) ======\n",
    "    # Fig 1: Equity vs Buy&Hold + Drawdown + Position\n",
    "    fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True, gridspec_kw={\"height_ratios\": [3, 1.5, 1]})\n",
    "    (equity.rename(\"strategy\")).plot(ax=axes[0], lw=1.4)\n",
    "    bh.plot(ax=axes[0], lw=1.0, alpha=0.8)\n",
    "    axes[0].set_title(f\"Equity vs Buy&Hold — {model_path.name} ({algo.upper()})\")\n",
    "    axes[0].legend()\n",
    "    axes[0].grid(True)\n",
    "\n",
    "    dd = equity / equity.cummax() - 1.0\n",
    "    dd.plot(ax=axes[1], color=\"tab:red\")\n",
    "    axes[1].set_title(\"Drawdown\")\n",
    "    axes[1].grid(True)\n",
    "\n",
    "    pos_series.plot(ax=axes[2], drawstyle=\"steps-post\")\n",
    "    axes[2].set_title(\"Position (-1=Short, 0=Flat, +1=Long)\")\n",
    "    axes[2].grid(True)\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "    # Fig 2: Rolling Sharpe + Action distribution + Return histogram\n",
    "    fig, axes = plt.subplots(3, 1, figsize=(12, 9), gridspec_kw={\"height_ratios\": [2, 1.2, 1.2]})\n",
    "    roll_win = max(30, int(90 * BPD))  # ~90 days in bars (min 30)\n",
    "    rmean = strat_rets.rolling(roll_win).mean()\n",
    "    rstd = strat_rets.rolling(roll_win).std()\n",
    "    rsharpe = rmean / (rstd + 1e-12) * np.sqrt(252 * BPD)\n",
    "    rsharpe.plot(ax=axes[0])\n",
    "    axes[0].axhline(0, color=\"k\", lw=0.8)\n",
    "    axes[0].set_title(f\"Rolling Sharpe (window ≈ {roll_win} bars)\")\n",
    "    axes[0].grid(True)\n",
    "\n",
    "    cnt = Counter(action_series.values)  # 0=SELL, 1=HOLD, 2=BUY\n",
    "    axes[1].bar([\"SELL(0)\", \"HOLD(1)\", \"BUY(2)\"], [cnt.get(0,0), cnt.get(1,0), cnt.get(2,0)])\n",
    "    axes[1].set_title(\"Action distribution\")\n",
    "    axes[1].grid(True, axis=\"y\")\n",
    "\n",
    "    axes[2].hist(strat_rets.values, bins=50, alpha=0.9)\n",
    "    axes[2].set_title(\"Strategy return distribution (per bar)\")\n",
    "    axes[2].grid(True)\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "    # Collect to leaderboard\n",
    "    results.append({\n",
    "        \"model\": model_path.name,\n",
    "        \"algo\": algo,\n",
    "        **m\n",
    "    })\n",
    "\n",
    "# ===== Leaderboard across models =====\n",
    "if results:\n",
    "    leaderboard = pd.DataFrame(results).sort_values([\"Sharpe\", \"Calmar\"], ascending=False)\n",
    "    print(\"\\nLeaderboard (sorted by Sharpe, then Calmar):\")\n",
    "    display(leaderboard)\n",
    "\n",
    "    # Optional: save metrics table\n",
    "    out_csv = Path(\"models\") / f\"metrics_{SYMBOL}_{tf_str}.csv\"\n",
    "    leaderboard.to_csv(out_csv, index=False)\n",
    "    print(\"Saved metrics to:\", out_csv.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== ppo_EURUSD_M15.zip | PPO ===\n",
      "Action counts:\n",
      " action\n",
      "Sell    9766\n",
      "Buy      141\n",
      "Hold      49\n",
      "Name: count, dtype: int64\n",
      "\n",
      "=== a2c_EURUSD_M15.zip | A2C ===\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_31600\\2918636110.py:57: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.\n",
      "  monthly = strat_rets.resample(\"M\").apply(lambda x: (1 + x).prod() - 1.0)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Action counts:\n",
      " action\n",
      "Buy     9948\n",
      "Hold       8\n",
      "Name: count, dtype: int64\n",
      "\n",
      "=== dqn_EURUSD_M15.zip | DQN ===\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_31600\\2918636110.py:57: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.\n",
      "  monthly = strat_rets.resample(\"M\").apply(lambda x: (1 + x).prod() - 1.0)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Action counts:\n",
      " action\n",
      "Buy     6627\n",
      "Sell    3161\n",
      "Hold     168\n",
      "Name: count, dtype: int64\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_31600\\2918636110.py:57: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.\n",
      "  monthly = strat_rets.resample(\"M\").apply(lambda x: (1 + x).prod() - 1.0)\n",
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_31600\\2918636110.py:64: UserWarning: Converting to PeriodArray/Index representation will drop timezone information.\n",
      "  monthly_df.index = monthly_df.index.to_period(\"M\")\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<style type=\"text/css\">\n",
       "</style>\n",
       "<table id=\"T_d3c84\">\n",
       "  <caption>Monthly compounded returns (last 12)</caption>\n",
       "  <thead>\n",
       "    <tr>\n",
       "      <th class=\"blank level0\" >&nbsp;</th>\n",
       "      <th id=\"T_d3c84_level0_col0\" class=\"col_heading level0 col0\" >ppo_EURUSD_M15</th>\n",
       "      <th id=\"T_d3c84_level0_col1\" class=\"col_heading level0 col1\" >a2c_EURUSD_M15</th>\n",
       "      <th id=\"T_d3c84_level0_col2\" class=\"col_heading level0 col2\" >dqn_EURUSD_M15</th>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th class=\"index_name level0\" >time</th>\n",
       "      <th class=\"blank col0\" >&nbsp;</th>\n",
       "      <th class=\"blank col1\" >&nbsp;</th>\n",
       "      <th class=\"blank col2\" >&nbsp;</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th id=\"T_d3c84_level0_row0\" class=\"row_heading level0 row0\" >2024-08</th>\n",
       "      <td id=\"T_d3c84_row0_col0\" class=\"data row0 col0\" >-1.60%</td>\n",
       "      <td id=\"T_d3c84_row0_col1\" class=\"data row0 col1\" >1.09%</td>\n",
       "      <td id=\"T_d3c84_row0_col2\" class=\"data row0 col2\" >-2.82%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_d3c84_level0_row1\" class=\"row_heading level0 row1\" >2024-09</th>\n",
       "      <td id=\"T_d3c84_row1_col0\" class=\"data row1 col0\" >-1.20%</td>\n",
       "      <td id=\"T_d3c84_row1_col1\" class=\"data row1 col1\" >0.70%</td>\n",
       "      <td id=\"T_d3c84_row1_col2\" class=\"data row1 col2\" >-1.49%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_d3c84_level0_row2\" class=\"row_heading level0 row2\" >2024-10</th>\n",
       "      <td id=\"T_d3c84_row2_col0\" class=\"data row2 col0\" >1.37%</td>\n",
       "      <td id=\"T_d3c84_row2_col1\" class=\"data row2 col1\" >-2.30%</td>\n",
       "      <td id=\"T_d3c84_row2_col2\" class=\"data row2 col2\" >-6.79%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_d3c84_level0_row3\" class=\"row_heading level0 row3\" >2024-11</th>\n",
       "      <td id=\"T_d3c84_row3_col0\" class=\"data row3 col0\" >2.29%</td>\n",
       "      <td id=\"T_d3c84_row3_col1\" class=\"data row3 col1\" >-2.81%</td>\n",
       "      <td id=\"T_d3c84_row3_col2\" class=\"data row3 col2\" >-11.74%</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_d3c84_level0_row4\" class=\"row_heading level0 row4\" >2024-12</th>\n",
       "      <td id=\"T_d3c84_row4_col0\" class=\"data row4 col0\" >1.74%</td>\n",
       "      <td id=\"T_d3c84_row4_col1\" class=\"data row4 col1\" >-2.09%</td>\n",
       "      <td id=\"T_d3c84_row4_col2\" class=\"data row4 col2\" >-10.10%</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n"
      ],
      "text/plain": [
       "<pandas.io.formats.style.Styler at 0x2e8742275b0>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved monthly returns to: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\models\\monthly_EURUSD_M15.csv\n"
     ]
    }
   ],
   "source": [
    "# --- Action distribution & monthly returns (for ALL models) ---\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "# Fallback if only single model provided\n",
    "def _infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "if 'MODEL_BUNDLE' not in globals():\n",
    "    if 'MODEL_PATH' in globals():\n",
    "        MODEL_BUNDLE = [(Path(MODEL_PATH), _infer_algo_from_name(Path(MODEL_PATH)))]\n",
    "    else:\n",
    "        raise RuntimeError(\"No models found: define MODEL_BUNDLE or MODEL_PATH first.\")\n",
    "\n",
    "all_monthly = []\n",
    "\n",
    "for model_path, algo in MODEL_BUNDLE:\n",
    "    algo = algo.lower()\n",
    "    if algo not in ALGO_MAP:\n",
    "        print(f\"Skipping {model_path.name}: unsupported algo '{algo}'\")\n",
    "        continue\n",
    "\n",
    "    print(f\"\\n=== {model_path.name} | {algo.upper()} ===\")\n",
    "    model = ALGO_MAP[algo].load(Path(model_path).as_posix())\n",
    "\n",
    "    # Run once to get actions & returns\n",
    "    env = TradingEnv(df_test, feature_cols)\n",
    "    obs, _ = env.reset()\n",
    "    rewards, actions = [], []\n",
    "\n",
    "    while True:\n",
    "        action, _ = model.predict(obs, deterministic=True)\n",
    "        obs, reward, terminated, truncated, info = env.step(int(action))\n",
    "        rewards.append(float(reward))\n",
    "        actions.append(int(action))\n",
    "        if terminated or truncated:\n",
    "            break\n",
    "\n",
    "    idx = df_test.index[1:1+len(rewards)]\n",
    "    strat_rets = pd.Series(rewards, index=idx, name=\"strategy_ret\")\n",
    "    action_series = pd.Series(actions, index=idx, name=\"action\")\n",
    "\n",
    "    # Action distribution (0=Sell, 1=Hold, 2=Buy)\n",
    "    action_map = {0: \"Sell\", 1: \"Hold\", 2: \"Buy\"}\n",
    "    action_counts = action_series.map(action_map).value_counts()\n",
    "    print(\"Action counts:\\n\", action_counts)\n",
    "\n",
    "    # Monthly compounded returns\n",
    "    monthly = strat_rets.resample(\"M\").apply(lambda x: (1 + x).prod() - 1.0)\n",
    "    monthly.name = model_path.stem  # column label in merged table\n",
    "    all_monthly.append(monthly)\n",
    "\n",
    "# Merge monthly across models, show last 12 rows\n",
    "if all_monthly:\n",
    "    monthly_df = pd.concat(all_monthly, axis=1)\n",
    "    monthly_df.index = monthly_df.index.to_period(\"M\")\n",
    "    try:\n",
    "        display(monthly_df.tail(12).style.format(\"{:.2%}\").set_caption(\"Monthly compounded returns (last 12)\"))\n",
    "    except Exception:\n",
    "        print(\"\\nMonthly compounded returns (last 12):\")\n",
    "        print(monthly_df.tail(12).applymap(lambda v: f\"{v:.2%}\"))\n",
    "\n",
    "    # Optional: save to CSV\n",
    "    out_csv = Path(\"models\") / f\"monthly_{SYMBOL}_{TIMEFRAME}.csv\"\n",
    "    monthly_df.to_csv(out_csv, float_format=\"%.6f\")\n",
    "    print(\"Saved monthly returns to:\", out_csv.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "=== Building QuantStats report for ppo_EURUSD_M15.zip (PPO) ===\n",
      "Saved QuantStats report to: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\reports\\qs_ppo_EURUSD_M15.html\n",
      "\n",
      "=== Building QuantStats report for a2c_EURUSD_M15.zip (A2C) ===\n",
      "Saved QuantStats report to: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\reports\\qs_a2c_EURUSD_M15.html\n",
      "\n",
      "=== Building QuantStats report for dqn_EURUSD_M15.zip (DQN) ===\n",
      "Saved QuantStats report to: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\reports\\qs_dqn_EURUSD_M15.html\n",
      "Wrote report index: G:\\My Drive\\Bots DRL\\DRL\\DRL-MT5-Lab\\notebooks\\reports\\index.html\n"
     ]
    }
   ],
   "source": [
    "# === QuantStats HTML reports (daily) for ALL models — robust tz handling ===\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "def _infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "# Fallback to single model if bundle missing\n",
    "if 'MODEL_BUNDLE' not in globals():\n",
    "    if 'MODEL_PATH' in globals():\n",
    "        MODEL_BUNDLE = [(Path(MODEL_PATH), _infer_algo_from_name(Path(MODEL_PATH)))]\n",
    "    else:\n",
    "        raise RuntimeError(\"No models found: define MODEL_BUNDLE or MODEL_PATH first.\")\n",
    "\n",
    "def _to_naive_index(s: pd.Series) -> pd.Series:\n",
    "    \"\"\"Ensure DatetimeIndex is tz-naive (drop tz).\"\"\"\n",
    "    s = s.copy()\n",
    "    if isinstance(s.index, pd.DatetimeIndex) and s.index.tz is not None:\n",
    "        # if you want to preserve absolute UTC moments, convert to UTC first, then drop tz\n",
    "        s.index = s.index.tz_convert(\"UTC\").tz_localize(None)\n",
    "    return s\n",
    "\n",
    "def _daily_compounded(returns_bar: pd.Series) -> pd.Series:\n",
    "    \"\"\"Compound intrabar returns to daily returns and drop NaNs.\"\"\"\n",
    "    s = _to_naive_index(returns_bar.sort_index())\n",
    "    return s.resample(\"1D\").apply(lambda x: (1 + x).prod() - 1.0).dropna()\n",
    "\n",
    "try:\n",
    "    import quantstats as qs\n",
    "    qs.extend_pandas()\n",
    "except Exception as e:\n",
    "    print(\"QuantStats not available or failed to import:\", e)\n",
    "    print(\"Tip: pip install quantstats\")\n",
    "else:\n",
    "    INCLUDE_BENCHMARK = True  # set False if you want to skip benchmark to avoid alignment issues\n",
    "    out_dir = Path(\"reports\"); out_dir.mkdir(exist_ok=True)\n",
    "    links = []\n",
    "\n",
    "    for model_path, algo in MODEL_BUNDLE:\n",
    "        algo = algo.lower()\n",
    "        if algo not in ALGO_MAP:\n",
    "            print(f\"Skipping {model_path.name}: unsupported algo '{algo}'\")\n",
    "            continue\n",
    "\n",
    "        print(f\"\\n=== Building QuantStats report for {model_path.name} ({algo.upper()}) ===\")\n",
    "        model = ALGO_MAP[algo].load(model_path.as_posix())\n",
    "\n",
    "        # Run evaluation pass to collect per-bar strategy returns and raw returns\n",
    "        env = TradingEnv(df_test, feature_cols)\n",
    "        obs, _ = env.reset()\n",
    "        rewards, raw_rets = [], []\n",
    "\n",
    "        while True:\n",
    "            action, _ = model.predict(obs, deterministic=True)\n",
    "            obs, reward, terminated, truncated, info = env.step(int(action))\n",
    "            rewards.append(float(reward))\n",
    "            raw_rets.append(float(info.get(\"ret\", 0.0)))\n",
    "            if terminated or truncated:\n",
    "                break\n",
    "\n",
    "        # Build indexed series\n",
    "        idx = df_test.index[1:1+len(rewards)]\n",
    "        strat_rets = pd.Series(rewards, index=idx, name=\"strategy_ret\")\n",
    "        bh_rets    = pd.Series(raw_rets, index=idx, name=\"bh_ret\")\n",
    "\n",
    "        # Make both tz-naive BEFORE resampling\n",
    "        strat_rets = _to_naive_index(strat_rets)\n",
    "        bh_rets    = _to_naive_index(bh_rets)\n",
    "\n",
    "        # Daily compounded\n",
    "        daily_strat = _daily_compounded(strat_rets)\n",
    "        daily_bh    = _daily_compounded(bh_rets)\n",
    "\n",
    "        # Align indexes (important when passing a benchmark)\n",
    "        if INCLUDE_BENCHMARK and len(daily_bh):\n",
    "            common = daily_strat.index.intersection(daily_bh.index)\n",
    "            daily_strat = daily_strat.reindex(common).dropna()\n",
    "            daily_bh    = daily_bh.reindex(common).dropna()\n",
    "            benchmark = daily_bh if len(daily_bh) else None\n",
    "        else:\n",
    "            benchmark = None\n",
    "\n",
    "        # Debug (you can comment these out later)\n",
    "        # print(\"strat tz:\", getattr(daily_strat.index, \"tz\", None), \"| bh tz:\", getattr(daily_bh.index, \"tz\", None))\n",
    "        # print(\"strat idx dtype:\", daily_strat.index.dtype, \"| bh idx dtype:\", daily_bh.index.dtype)\n",
    "\n",
    "        out_html = out_dir / f\"qs_{model_path.stem}.html\"\n",
    "        title = f\"DRL-MT5 — {SYMBOL} {TIMEFRAME} — {model_path.stem}\"\n",
    "        try:\n",
    "            qs.reports.html(daily_strat, benchmark=benchmark, output=out_html.as_posix(), title=title)\n",
    "            print(\"Saved QuantStats report to:\", out_html.resolve())\n",
    "            links.append(out_html.name)\n",
    "        except Exception as e:\n",
    "            # As a fallback, try again without a benchmark if tz/type alignment still trips\n",
    "            print(f\"QuantStats with benchmark failed: {e}\")\n",
    "            try:\n",
    "                qs.reports.html(daily_strat, output=out_html.as_posix(), title=title)\n",
    "                print(\"Saved QuantStats report (no benchmark) to:\", out_html.resolve())\n",
    "                links.append(out_html.name)\n",
    "            except Exception as e2:\n",
    "                print(f\"QuantStats report failed for {model_path.name}:\", e2)\n",
    "\n",
    "    # Index page with links\n",
    "    if links:\n",
    "        index_path = out_dir / \"index.html\"\n",
    "        html = \"<h2>QuantStats Reports</h2><ul>\" + \"\".join([f'<li><a href=\"{name}\">{name}</a></li>' for name in links]) + \"</ul>\"\n",
    "        index_path.write_text(html, encoding=\"utf-8\")\n",
    "        print(\"Wrote report index:\", index_path.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "fd539a1a",
   "metadata": {},
   "outputs": [],
   "source": [
    "from plotly.subplots import make_subplots\n",
    "import plotly.graph_objects as go\n",
    "import numpy as np, pandas as pd\n",
    "from pathlib import Path\n",
    "\n",
    "def _to_naive_index_any(x):\n",
    "    \"\"\"TZ-normalize Series/DataFrame.\"\"\"\n",
    "    if isinstance(x, (pd.Series, pd.DataFrame)):\n",
    "        idx = x.index\n",
    "        if isinstance(idx, pd.DatetimeIndex) and idx.tz is not None:\n",
    "            x = x.copy()\n",
    "            x.index = idx.tz_convert(\"UTC\").tz_localize(None)\n",
    "    return x\n",
    "\n",
    "def _align_on_common_index(series_list):\n",
    "    \"\"\"Return copies aligned on the intersection of all non-empty indices.\"\"\"\n",
    "    s_ok = [s for s in series_list if s is not None and len(s) > 0]\n",
    "    if not s_ok:\n",
    "        return series_list, pd.DatetimeIndex([])\n",
    "    common = s_ok[0].index\n",
    "    for s in s_ok[1:]:\n",
    "        common = common.intersection(s.index)\n",
    "    aligned = []\n",
    "    for s in series_list:\n",
    "        if s is None or len(s) == 0:\n",
    "            aligned.append(s)\n",
    "        else:\n",
    "            aligned.append(s.reindex(common))\n",
    "    return aligned, common\n",
    "\n",
    "def build_plotly_dashboard(df_with_close, pos_series, strat_rets, raw_rets,\n",
    "                           model_name, algo, symbol, timeframe, out_dir=\"reports/plotly\"):\n",
    "    \"\"\"\n",
    "    Create interactive dashboard:\n",
    "      row1: Close + entries/exits\n",
    "      row2: Equity vs Buy&Hold\n",
    "      row3: Drawdown + Position (secondary axis)\n",
    "    \"\"\"\n",
    "    # 1) tz -> naive BEFORE any reindexing\n",
    "    pos_series = _to_naive_index_any(pos_series.sort_index())\n",
    "    strat_rets = _to_naive_index_any(strat_rets.sort_index())\n",
    "    raw_rets   = _to_naive_index_any(raw_rets.sort_index())\n",
    "    df_with_close = _to_naive_index_any(df_with_close)\n",
    "\n",
    "    # 2) align all on common index\n",
    "    (pos_series, strat_rets, raw_rets), idx = _align_on_common_index([pos_series, strat_rets, raw_rets])\n",
    "\n",
    "    # Guard: if nothing overlaps, bail early with a friendly message\n",
    "    if len(idx) == 0:\n",
    "        print(f\"[plotly] No overlapping index after alignment for {model_name}. Skipping.\")\n",
    "        return None\n",
    "\n",
    "    # Fill still-missing values conservatively\n",
    "    pos_series = pos_series.fillna(method=\"ffill\").fillna(0).astype(int)\n",
    "    strat_rets = strat_rets.fillna(0.0)\n",
    "    raw_rets   = raw_rets.fillna(0.0)\n",
    "\n",
    "    price = df_with_close[\"close\"].reindex(idx)\n",
    "\n",
    "    # 3) equity/drawdown\n",
    "    equity = (1.0 + strat_rets).cumprod()\n",
    "    bh     = (1.0 + raw_rets).cumprod()\n",
    "    dd     = equity / equity.cummax() - 1.0\n",
    "\n",
    "    # Entries/exits from position transitions\n",
    "    prev = pos_series.shift(1).fillna(0).astype(int)\n",
    "    cur  = pos_series.astype(int)\n",
    "    long_entries  = idx[(prev <= 0) & (cur ==  1)]\n",
    "    short_entries = idx[(prev >= 0) & (cur == -1)]\n",
    "    long_exits    = idx[(prev ==  1) & (cur <= 0)]\n",
    "    short_exits   = idx[(prev == -1) & (cur >= 0)]\n",
    "\n",
    "    total_reward = float(strat_rets.sum())\n",
    "    total_profit = float(equity.iloc[-1] - 1.0) if len(equity) else 0.0\n",
    "\n",
    "    fig = make_subplots(\n",
    "        rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.06,\n",
    "        specs=[[{\"secondary_y\": False}], [{\"secondary_y\": False}], [{\"secondary_y\": True}]]\n",
    "    )\n",
    "\n",
    "    # Row 1: price + markers\n",
    "    fig.add_trace(go.Scattergl(x=idx, y=price, mode=\"lines\", name=\"Close\", line=dict(width=1.2)), row=1, col=1)\n",
    "    if len(long_entries):  fig.add_trace(go.Scattergl(x=long_entries,  y=price.loc[long_entries],  mode=\"markers\",\n",
    "                                                      name=\"Long Entry\",  marker=dict(color=\"green\", size=6, symbol=\"circle\")), row=1, col=1)\n",
    "    if len(short_entries): fig.add_trace(go.Scattergl(x=short_entries, y=price.loc[short_entries], mode=\"markers\",\n",
    "                                                      name=\"Short Entry\", marker=dict(color=\"red\",   size=6, symbol=\"circle\")), row=1, col=1)\n",
    "    if len(long_exits):    fig.add_trace(go.Scattergl(x=long_exits,   y=price.loc[long_exits],   mode=\"markers\",\n",
    "                                                      name=\"Long Exit\",  marker=dict(color=\"green\", size=8, symbol=\"triangle-down\")), row=1, col=1)\n",
    "    if len(short_exits):   fig.add_trace(go.Scattergl(x=short_exits,  y=price.loc[short_exits],  mode=\"markers\",\n",
    "                                                      name=\"Short Exit\", marker=dict(color=\"red\",   size=8, symbol=\"triangle-up\")), row=1, col=1)\n",
    "\n",
    "    # Row 2: equity vs buy&hold\n",
    "    fig.add_trace(go.Scattergl(x=idx, y=equity, mode=\"lines\", name=\"Strategy equity\", line=dict(width=1.4)), row=2, col=1)\n",
    "    if len(bh):\n",
    "        fig.add_trace(go.Scattergl(x=idx, y=bh, mode=\"lines\", name=\"Buy&Hold\", line=dict(width=1, dash=\"dot\")), row=2, col=1)\n",
    "\n",
    "    # Row 3: drawdown + position\n",
    "    fig.add_trace(go.Scattergl(x=idx, y=dd, mode=\"lines\", name=\"Drawdown\", line=dict(color=\"red\")), row=3, col=1, secondary_y=False)\n",
    "    fig.add_trace(go.Scattergl(x=idx, y=pos_series, mode=\"lines\", name=\"Position (-1/0/+1)\",\n",
    "                               line=dict(width=1), line_shape=\"hv\"), row=3, col=1, secondary_y=True)\n",
    "\n",
    "    fig.update_yaxes(title_text=\"Price\", row=1, col=1)\n",
    "    fig.update_yaxes(title_text=\"Equity\", row=2, col=1)\n",
    "    fig.update_yaxes(title_text=\"Drawdown\", row=3, col=1, secondary_y=False, tickformat=\".1%\")\n",
    "    fig.update_yaxes(title_text=\"Pos\", row=3, col=1, secondary_y=True, range=[-1.2, 1.2], tickvals=[-1,0,1])\n",
    "\n",
    "    fig.update_layout(\n",
    "        title=f\"{symbol} {timeframe} — {model_name} ({algo.upper()})<br>\"\n",
    "              f\"Total Reward: {total_reward:.6f} • Total Profit: {total_profit:.6f}\",\n",
    "        hovermode=\"x unified\",\n",
    "        legend=dict(orientation=\"h\", yanchor=\"bottom\", y=1.02, xanchor=\"right\", x=1),\n",
    "        xaxis=dict(rangeslider=dict(visible=True))\n",
    "    )\n",
    "\n",
    "    out_dir = Path(out_dir); out_dir.mkdir(parents=True, exist_ok=True)\n",
    "    out_html = out_dir / f\"plotly_{Path(model_name).stem}.html\"\n",
    "    fig.write_html(out_html.as_posix(), include_plotlyjs=\"cdn\", auto_open=False)\n",
    "    return out_html\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "d55f4970",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_15772\\1367230339.py:54: FutureWarning: Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.\n",
      "  pos_series = pos_series.fillna(method=\"ffill\").fillna(0).astype(int)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\plotly\\plotly_ppo_EURUSD_M15.html\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_15772\\1367230339.py:54: FutureWarning:\n",
      "\n",
      "Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\plotly\\plotly_a2c_EURUSD_M15.html\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\moham\\AppData\\Local\\Temp\\ipykernel_15772\\1367230339.py:54: FutureWarning:\n",
      "\n",
      "Series.fillna with 'method' is deprecated and will raise in a future version. Use obj.ffill() or obj.bfill() instead.\n",
      "\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\plotly\\plotly_dqn_EURUSD_M15.html\n",
      "Index: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\plotly\\index.html\n"
     ]
    }
   ],
   "source": [
    "links = []\n",
    "for p, algo in MODEL_BUNDLE:\n",
    "    pos_series, strat_rets, raw_rets, model_name_str, algo_used = run_model_collect_series_with_raw(p, algo)\n",
    "    out_path = build_plotly_dashboard(df_test, pos_series, strat_rets, raw_rets,\n",
    "                                      model_name_str, algo_used, SYMBOL, TIMEFRAME)\n",
    "    if out_path:\n",
    "        print(\"Saved:\", out_path.resolve())\n",
    "        links.append(out_path.name)\n",
    "\n",
    "# optional index page\n",
    "if links:\n",
    "    idx_path = Path(\"reports/plotly/index.html\")\n",
    "    idx_path.parent.mkdir(parents=True, exist_ok=True)\n",
    "    idx_html = \"<h2>Interactive Model Dashboards</h2><ul>\" + \"\".join(\n",
    "        f'<li><a href=\"{name}\">{name}</a></li>' for name in links\n",
    "    ) + \"</ul>\"\n",
    "    idx_path.write_text(idx_html, encoding=\"utf-8\")\n",
    "    print(\"Index:\", idx_path.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "e95d96c5",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "import numpy as np, pandas as pd, matplotlib.pyplot as plt\n",
    "\n",
    "def plot_trades_on_price_from_series(df_with_close, pos_series, rewards, model_name, algo, save_png=False):\n",
    "    L = len(pos_series)\n",
    "    rewards = np.asarray(rewards, dtype=float)[:L]\n",
    "    idx = pos_series.index\n",
    "\n",
    "    price = df_with_close[\"close\"].reindex(idx)\n",
    "    prev = pos_series.shift(1).fillna(0).astype(int)\n",
    "    cur  = pos_series.astype(int)\n",
    "\n",
    "    long_entries  = idx[(prev <= 0) & (cur ==  1)]\n",
    "    short_entries = idx[(prev >= 0) & (cur == -1)]\n",
    "    long_exits    = idx[(prev ==  1) & (cur <= 0)]\n",
    "    short_exits   = idx[(prev == -1) & (cur >= 0)]\n",
    "\n",
    "    total_reward = float(rewards.sum())\n",
    "    rewards_s = pd.Series(rewards, index=idx[:len(rewards)])\n",
    "    total_profit = float((1.0 + rewards_s).prod() - 1.0)\n",
    "\n",
    "    plt.figure(figsize=(16, 6))\n",
    "    plt.plot(price.index, price.values, linewidth=1.2, label=\"Close\")\n",
    "    plt.scatter(long_entries,  price.loc[long_entries],  s=22, c=\"green\", marker=\"o\", label=\"Long Entry\")\n",
    "    plt.scatter(short_entries, price.loc[short_entries], s=22, c=\"red\",   marker=\"o\", label=\"Short Entry\")\n",
    "    plt.scatter(long_exits,   price.loc[long_exits],   s=36, c=\"green\", marker=\"v\", label=\"Long Exit\")\n",
    "    plt.scatter(short_exits,  price.loc[short_exits],  s=36, c=\"red\",   marker=\"^\", label=\"Short Exit\")\n",
    "    plt.title(f\"Total Reward: {total_reward:.6f} ~ Total Profit: {total_profit:.6f}  \"\n",
    "              f\"({model_name} | {algo.upper()})\")\n",
    "    plt.legend(loc=\"best\"); plt.grid(True); plt.tight_layout()\n",
    "\n",
    "    if save_png:\n",
    "        out = Path(\"reports\") / f\"trades_{Path(model_name).stem}.png\"\n",
    "        out.parent.mkdir(exist_ok=True)\n",
    "        plt.savefig(out, dpi=140)\n",
    "        print(\"Saved trades plot:\", out.resolve())\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "751e808b",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pathlib import Path\n",
    "from stable_baselines3 import PPO, A2C, DQN\n",
    "\n",
    "ALGO_MAP = {\"ppo\": PPO, \"a2c\": A2C, \"dqn\": DQN}\n",
    "\n",
    "def _infer_algo_from_name(p: Path) -> str:\n",
    "    name = p.stem.lower()\n",
    "    for k in ALGO_MAP:\n",
    "        if name.startswith(k + \"_\"):\n",
    "            return k\n",
    "    return \"ppo\"\n",
    "\n",
    "def run_model_collect_series(model_path, algo=\"auto\"):\n",
    "    p = Path(model_path)\n",
    "    if algo == \"auto\":\n",
    "        algo = _infer_algo_from_name(p)\n",
    "    model = ALGO_MAP[algo].load(p.as_posix())\n",
    "\n",
    "    env = TradingEnv(df_test, feature_cols)\n",
    "    obs, _ = env.reset()\n",
    "    rewards, positions = [], []\n",
    "\n",
    "    while True:\n",
    "        action, _ = model.predict(obs, deterministic=True)\n",
    "        obs, reward, terminated, truncated, info = env.step(int(action))\n",
    "        rewards.append(float(reward))\n",
    "        positions.append(int(info.get(\"position\", 0)))\n",
    "        if terminated or truncated:\n",
    "            break\n",
    "\n",
    "    idx = df_test.index[1:1+len(rewards)]\n",
    "    pos_series = pd.Series(positions, index=idx, name=\"position\")\n",
    "    return pos_series, rewards, p.name, algo\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "2d2c10bb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved trades plot: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\trades_ppo_EURUSD_M15.png\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 108796 characters]",
      "text/plain": [
       "<Figure size 1600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Uses MODEL_PATH / ALGO_SELECTED from your first block; falls back to auto if not set\n",
    "try:\n",
    "    model_name = MODEL_PATH\n",
    "    algo_hint = ALGO_SELECTED if 'ALGO_SELECTED' in globals() else \"auto\"\n",
    "except NameError:\n",
    "    # If you didn’t run the first block, hardcode a file here:\n",
    "    model_name = \"models/ppo_EURUSD_M15.zip\"\n",
    "    algo_hint = \"auto\"\n",
    "\n",
    "pos_series, rewards, model_name_str, algo_used = run_model_collect_series(model_name, algo_hint)\n",
    "plot_trades_on_price_from_series(df_test, pos_series, rewards, model_name_str, algo_used, save_png=True)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "bb64e225",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Saved trades plot: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\trades_ppo_EURUSD_M15.png\n",
      "Saved trades plot: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\trades_a2c_EURUSD_M15.png\n",
      "Saved trades plot: G:\\My Drive\\Github\\Deep-Reinforcement-Learning-MT5-Bot\\notebooks\\reports\\trades_dqn_EURUSD_M15.png\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 108796 characters]",
      "text/plain": [
       "<Figure size 1600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 95616 characters]",
      "text/plain": [
       "<Figure size 1600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "image/png": "[encoded data, 141840 characters]",
      "text/plain": [
       "<Figure size 1600x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Requires MODEL_BUNDLE from the earlier “collect models” block\n",
    "for p, algo in MODEL_BUNDLE:\n",
    "    pos_series, rewards, model_name_str, algo_used = run_model_collect_series(p, algo)\n",
    "    plot_trades_on_price_from_series(df_test, pos_series, rewards, model_name_str, algo_used, save_png=True)\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "drl",
   "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.10.18"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
