Add interactive research notebook for GoldRegime X and update study_headway_cent.db

This commit is contained in:
Luke Mosoti
2026-05-11 14:14:21 +03:00
parent 600edff829
commit b2724a4115
2 changed files with 592 additions and 0 deletions
Binary file not shown.
+592
View File
@@ -0,0 +1,592 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "cell00",
"metadata": {},
"source": [
"# GoldRegime X — Interactive Research Notebook\n",
"\n",
"> Interactive exploration sandbox for XAUUSD hybrid HMM + XGBoost trading system.\n",
"> Run **Section 1** first to load data and models, then explore any section below.\n",
"\n",
"| Section | Feature | Speed |\n",
"|---------|---------|-------|\n",
"| 1 | Data & Model Loader | ~5 s |\n",
"| 2 | Equity Curve Explorer | instant |\n",
"| 3 | WFO Window Analysis | ~2–3 min |\n",
"| 4 | Feature & Regime Explorer | instant |\n",
"| 5 | Parameter Sensitivity | ~30–60 s |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell01",
"metadata": {},
"outputs": [],
"source": [
"import sys, os, warnings\n",
"_here = os.path.abspath('.')\n",
"if os.path.basename(_here).lower() == 'notebooks':\n",
" os.chdir('..')\n",
"sys.path.insert(0, '.')\n",
"warnings.filterwarnings('ignore')\n",
"\n",
"%matplotlib inline\n",
"import numpy as np\n",
"import pandas as pd\n",
"import matplotlib\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib.gridspec as gridspec\n",
"import matplotlib.patches as mpatches\n",
"import ipywidgets as widgets\n",
"from IPython.display import display, clear_output\n",
"\n",
"matplotlib.rcParams.update({\n",
" 'figure.dpi': 110,\n",
" 'axes.grid': True,\n",
" 'grid.alpha': 0.3,\n",
" 'axes.spines.top': False,\n",
" 'axes.spines.right': False,\n",
"})\n",
"\n",
"from src.processor import load_data_with_hmm_labels\n",
"from src.optimizer import get_best_params, _run_wfo, WFO_PARAMS\n",
"from src.engine_hmm import (load_model, predict_states, fit_hmm,\n",
" get_model_path, STATE_NAMES_2, STATE_NAMES_3, STATE_NAMES_4)\n",
"from src.engine_xgb import (load_xgb, train_xgb, prepare_features,\n",
" get_predictions, get_ensemble_path)\n",
"from src.backtester import vectorized_backtest\n",
"\n",
"_CACHE = {}\n",
"STATE_NAMES_MAP = {2: STATE_NAMES_2, 3: STATE_NAMES_3, 4: STATE_NAMES_4}\n",
"STATE_COLORS = {0: '#27AE60', 1: '#E74C3C', 2: '#7F8C8D', 3: '#F39C12'}\n",
"print('Setup complete — working directory:', os.getcwd())"
]
},
{
"cell_type": "markdown",
"id": "cell02",
"metadata": {},
"source": [
"---\n",
"## Section 1 — Data & Model Loader\n",
"Select timeframe, broker, and balance then click **Load Data & Models**.\n",
"Processed parquet + saved HMM/XGB models are loaded; if missing they are fitted from scratch."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell03",
"metadata": {},
"outputs": [],
"source": [
"_style = {'description_width': 'initial'}\n",
"_layout = widgets.Layout(width='220px')\n",
"\n",
"tf_w = widgets.Dropdown(options=['H1', 'M15', 'M5'], value='H1',\n",
" description='Timeframe:', style=_style, layout=_layout)\n",
"broker_w = widgets.Dropdown(options=['headway_cent', 'standard'], value='headway_cent',\n",
" description='Broker:', style=_style, layout=_layout)\n",
"balance_w = widgets.IntSlider(min=10, max=500, step=5, value=15,\n",
" description='Balance $:', style=_style,\n",
" layout=widgets.Layout(width='380px'), continuous_update=False)\n",
"load_btn = widgets.Button(description='Load Data & Models', button_style='primary',\n",
" layout=widgets.Layout(width='200px'))\n",
"out1 = widgets.Output()\n",
"\n",
"\n",
"def _do_load(_):\n",
" with out1:\n",
" clear_output(wait=True)\n",
" tf, broker, balance = tf_w.value, broker_w.value, balance_w.value\n",
" print(f'Loading {tf} / {broker} / ${balance} ...')\n",
"\n",
" try:\n",
" df = load_data_with_hmm_labels(tf, broker)\n",
" except FileNotFoundError as exc:\n",
" print(f'ERROR: {exc}'); return\n",
"\n",
" try:\n",
" params = get_best_params(balance, broker, tf)\n",
" print(f'Params: n_states={params.get(\"n_states\",3)} '\n",
" f'obs_cov={params.get(\"obs_cov\",1.0):.4f} '\n",
" f'lr={params.get(\"learning_rate\",0.1):.4f}')\n",
" except Exception:\n",
" params = {}\n",
" print('No Optuna study found — using built-in defaults')\n",
"\n",
" try:\n",
" hmm_model = load_model(get_model_path(tf, broker))\n",
" except Exception:\n",
" n_st = int(params.get('n_states', 3))\n",
" hmm_model, _, _ = fit_hmm(df, n_states=n_st, tf=tf)\n",
" print('No saved HMM — fitted from scratch')\n",
"\n",
" try:\n",
" xgb_model, xgb_metrics = load_xgb(get_ensemble_path(tf, broker))\n",
" except Exception:\n",
" states_tmp = predict_states(hmm_model, df)\n",
" X_tmp, y_tmp, _, _ = prepare_features(df, states_tmp, tf=tf)\n",
" kws = {k: params[k] for k in ['max_depth','learning_rate','n_estimators',\n",
" 'subsample','min_child_weight','gamma','reg_alpha',\n",
" 'colsample_bytree','scale_pos_weight'] if k in params}\n",
" xgb_model, xgb_metrics = train_xgb(X_tmp, y_tmp, **kws)\n",
" print('No saved XGB — trained from scratch')\n",
"\n",
" states = (df['hmm_state'].values if 'hmm_state' in df.columns\n",
" else predict_states(hmm_model, df))\n",
" X, y, df_al, scaler = prepare_features(df, states, tf=tf)\n",
" _, probs = get_predictions(xgb_model, X)\n",
" states_al = pd.Series(states, index=df.index).loc[df_al.index].values\n",
" split_idx = xgb_metrics.get('split_idx', int(len(X) * 0.8))\n",
"\n",
" _CACHE.update(dict(\n",
" df=df, df_aligned=df_al, states=states, states_aligned=states_al,\n",
" probs=probs, split_idx=split_idx, params=params, tf=tf,\n",
" broker=broker, balance=balance, hmm_model=hmm_model,\n",
" xgb_model=xgb_model, xgb_metrics=xgb_metrics, scaler=scaler, X=X, y=y,\n",
" ))\n",
"\n",
" n_st = int(states.max()) + 1\n",
" sname = STATE_NAMES_MAP.get(n_st, STATE_NAMES_3)\n",
" cnts = pd.Series(states).value_counts().sort_index()\n",
" pct = (cnts / len(states) * 100).round(1)\n",
" print(f'\\n{\"=\"*56}')\n",
" print(f'Rows: {len(df):>10,} ({df.index[0].date()} → {df.index[-1].date()})')\n",
" print(f'Features: {list(X.columns)}')\n",
" print(f'IS split: {split_idx:>10,} ({split_idx/len(X)*100:.0f}%)')\n",
" print(f'Regimes: ' + ' | '.join(f'{sname[s]}={pct[s]:.1f}%' for s in cnts.index))\n",
" print(f'{\"=\"*56}')\n",
" print('Done — run any section below.')\n",
"\n",
"\n",
"load_btn.on_click(_do_load)\n",
"display(widgets.VBox([\n",
" widgets.HBox([tf_w, broker_w]),\n",
" balance_w,\n",
" load_btn, out1,\n",
"]))"
]
},
{
"cell_type": "markdown",
"id": "cell04",
"metadata": {},
"source": [
"---\n",
"## Section 2 — Equity Curve Explorer\n",
"Adjust **Balance** or **Broker** — the equity curve re-runs automatically (backtest only, no retraining).\n",
"Orange dashed line = IS / OOS split."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell05",
"metadata": {},
"outputs": [],
"source": [
"bal2_w = widgets.IntSlider(min=10, max=500, step=5, value=15, description='Balance $:',\n",
" style={'description_width': 'initial'},\n",
" layout=widgets.Layout(width='380px'), continuous_update=False)\n",
"broker2_w = widgets.Dropdown(options=['headway_cent', 'standard'], value='headway_cent',\n",
" description='Broker:', style={'description_width': 'initial'},\n",
" layout=widgets.Layout(width='220px'))\n",
"\n",
"\n",
"def _plot_equity(balance, broker):\n",
" if 'df_aligned' not in _CACHE:\n",
" print('Run Section 1 first.'); return\n",
"\n",
" df_al = _CACHE['df_aligned']\n",
" probs = _CACHE['probs']\n",
" states_al = _CACHE['states_aligned']\n",
" split_idx = _CACHE['split_idx']\n",
" tf = _CACHE['tf']\n",
"\n",
" r = vectorized_backtest(df_al, probs, states_al, split_idx=split_idx,\n",
" account_size=balance, broker=broker, tf=tf)\n",
" ts = pd.DatetimeIndex(r['equity_timestamps'])\n",
" eq = r['equity_values']\n",
" bh = balance * np.exp(np.cumsum(df_al['log_return'].fillna(0).values))\n",
"\n",
" fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 7), sharex=True,\n",
" gridspec_kw={'height_ratios': [3, 1]})\n",
" if split_idx < len(ts):\n",
" for ax in (ax1, ax2):\n",
" ax.axvline(ts[split_idx], color='orange', ls='--', lw=1.5)\n",
" ax1.plot(ts, eq, lw=1.5, label='Strategy', color='#3498DB')\n",
" ax1.plot(ts, bh, lw=1.0, ls=':', label='Buy & Hold', color='#E67E22', alpha=0.7)\n",
" ax1.axhline(balance, color='grey', lw=0.6, alpha=0.5)\n",
" ax1.set_ylabel('Equity ($)')\n",
" ax1.legend(loc='upper left', fontsize=9)\n",
"\n",
" peak = np.maximum.accumulate(eq)\n",
" dd = (eq - peak) / peak * 100\n",
" ax2.fill_between(ts, dd, 0, color='#E74C3C', alpha=0.4)\n",
" ax2.set_ylabel('Drawdown (%)')\n",
" ax2.set_xlabel('Date')\n",
"\n",
" is_sh = r.get('is_sharpe_ratio', r['sharpe_ratio'])\n",
" oos_sh = r.get('oos_sharpe_ratio', r['sharpe_ratio'])\n",
" is_n = r.get('is_n_trades', r['n_trades'])\n",
" oos_n = r.get('oos_n_trades', r['n_trades'])\n",
" fig.suptitle(\n",
" f'{tf} | {broker} | ${balance} '\n",
" f'IS Sharpe: {is_sh:.2f} ({is_n} trades) | '\n",
" f'OOS Sharpe: {oos_sh:.2f} ({oos_n} trades) | '\n",
" f'Max DD: {r[\"max_drawdown\"]*100:.1f}% | WR: {r[\"win_rate\"]*100:.1f}%',\n",
" fontsize=10\n",
" )\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" is_dd = r.get('is_max_drawdown', r['max_drawdown'])\n",
" oos_dd = r.get('oos_max_drawdown', r['max_drawdown'])\n",
" is_wr = r.get('is_win_rate', r['win_rate'])\n",
" oos_wr = r.get('oos_win_rate', r['win_rate'])\n",
" rows = [\n",
" ('Sharpe', f'{is_sh:.3f}', f'{oos_sh:.3f}', f'{r[\"sharpe_ratio\"]:.3f}'),\n",
" ('Max Drawdown', f'{is_dd*100:.1f}%', f'{oos_dd*100:.1f}%', f'{r[\"max_drawdown\"]*100:.1f}%'),\n",
" ('Win Rate', f'{is_wr*100:.1f}%', f'{oos_wr*100:.1f}%', f'{r[\"win_rate\"]*100:.1f}%'),\n",
" ('Trades', str(is_n), str(oos_n), str(r['n_trades'])),\n",
" ('Profit Factor', '–', '–', f'{r.get(\"profit_factor\", 0):.2f}'),\n",
" ('Recovery Factor', '–', '–', f'{r.get(\"recovery_factor\", 0):.2f}'),\n",
" ('Avg Efficiency', '–', '–', f'{r.get(\"avg_efficiency\", 0):.2f}x'),\n",
" ]\n",
" print(f' {\"Metric\":<20} {\"IS\":>10} {\"OOS\":>10} {\"Total\":>10}')\n",
" print(' ' + '─' * 54)\n",
" for m, i, o, t in rows:\n",
" print(f' {m:<20} {i:>10} {o:>10} {t:>10}')\n",
"\n",
"\n",
"out2 = widgets.interactive_output(_plot_equity, {'balance': bal2_w, 'broker': broker2_w})\n",
"display(widgets.VBox([widgets.HBox([bal2_w, broker2_w]), out2]))"
]
},
{
"cell_type": "markdown",
"id": "cell06",
"metadata": {},
"source": [
"---\n",
"## Section 3 — WFO Window Analysis\n",
"Click **Run WFO Analysis** to evaluate the best Optuna params across all rolling windows (~2–3 min).\n",
"Then use the **Window #** slider to drill into any OOS window's equity curve."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell07",
"metadata": {},
"outputs": [],
"source": [
"wfo_btn = widgets.Button(description='Run WFO Analysis', button_style='warning',\n",
" layout=widgets.Layout(width='180px'))\n",
"out3_wfo = widgets.Output()\n",
"win_slider = widgets.IntSlider(min=0, max=25, step=1, value=0, description='Window #:',\n",
" style={'description_width': 'initial'},\n",
" layout=widgets.Layout(width='380px'), continuous_update=False)\n",
"out3_drill = widgets.Output()\n",
"\n",
"\n",
"def _run_wfo_analysis(_):\n",
" with out3_wfo:\n",
" clear_output(wait=True)\n",
" if 'df_aligned' not in _CACHE:\n",
" print('Run Section 1 first.'); return\n",
"\n",
" df_al = _CACHE['df_aligned']\n",
" params = _CACHE['params']\n",
" tf = _CACHE['tf']\n",
" broker = _CACHE['broker']\n",
" balance = _CACHE['balance']\n",
"\n",
" if not params:\n",
" print('No Optuna params — run optimize first.'); return\n",
"\n",
" cfg = WFO_PARAMS.get(tf.upper(), WFO_PARAMS['H1'])\n",
" n_states = int(params.get('n_states', 3))\n",
" xgb_kws = {k: params[k] for k in ['max_depth','learning_rate','n_estimators',\n",
" 'subsample','min_child_weight','gamma','reg_alpha',\n",
" 'colsample_bytree','scale_pos_weight'] if k in params}\n",
"\n",
" print(f'WFO tf={tf} IS={cfg[\"is_bars\"]}bars OOS={cfg[\"oos_bars\"]}bars '\n",
" f'n_states={n_states}')\n",
" print('Running ... (each window fits its own HMM — takes a few minutes)')\n",
"\n",
" wfo = _run_wfo(df_al, n_states=n_states, tf=tf, balance=balance, broker=broker,\n",
" xgb_kwargs=xgb_kws, **cfg)\n",
" _CACHE['wfo_result'] = wfo\n",
" _CACHE['wfo_cfg'] = cfg\n",
"\n",
" # Update window slider range\n",
" win_slider.max = max(0, len(wfo['window_scores']) - 1)\n",
"\n",
" scores = wfo['window_scores']\n",
" colors = ['#27AE60' if s > 0 else '#E74C3C' for s in scores]\n",
"\n",
" # Build OOS start-year labels\n",
" is_b, oos_b, emb_b, step_b = (cfg[k] for k in\n",
" ['is_bars','oos_bars','embargo_bars','step_bars'])\n",
" labels = []\n",
" for i in range(len(scores)):\n",
" oos_st = i * step_b + is_b + emb_b\n",
" if oos_st < len(df_al):\n",
" labels.append(str(df_al.index[oos_st].date())[:7])\n",
" else:\n",
" labels.append('')\n",
"\n",
" fig, ax = plt.subplots(figsize=(14, 4))\n",
" ax.bar(range(len(scores)), scores, color=colors, edgecolor='white', lw=0.5)\n",
" ax.axhline(0, color='black', lw=0.8)\n",
" ax.axhline(wfo['wfo_score'], color='orange', ls='--', lw=1.5,\n",
" label=f'WFO score = {wfo[\"wfo_score\"]:.3f}')\n",
" ax.set_xticks(range(len(scores)))\n",
" ax.set_xticklabels(labels, rotation=45, ha='right', fontsize=7)\n",
" ax.set_xlabel('OOS window start')\n",
" ax.set_ylabel('OOS Complex Criterion')\n",
" ax.set_title(\n",
" f'WFO Window Scores — {tf} | '\n",
" f'valid: {wfo[\"n_valid_windows\"]}/{wfo[\"n_windows\"]} | '\n",
" f'std_sharpe={wfo[\"std_sharpe\"]:.2f} | '\n",
" f'WFE={wfo[\"wfe_ratio\"]:.2f}'\n",
" )\n",
" ax.legend()\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" print(f'WFO score: {wfo[\"wfo_score\"]:.4f} (median – 0.15×std)')\n",
" print(f'WFE ratio: {wfo[\"wfe_ratio\"]:.4f} (>0.5 = robust)')\n",
" print(f'Std Sharpe: {wfo[\"std_sharpe\"]:.4f} (lower = consistent)')\n",
" print(f'Median trades: {wfo[\"median_trades\"]}')\n",
" print(f'Valid windows: {wfo[\"n_valid_windows\"]}/{wfo[\"n_windows\"]}')\n",
"\n",
"\n",
"def _drill_window(window_idx):\n",
" with out3_drill:\n",
" clear_output(wait=True)\n",
" if 'wfo_cfg' not in _CACHE:\n",
" print('Run WFO Analysis first.'); return\n",
"\n",
" df_al = _CACHE['df_aligned']\n",
" probs = _CACHE['probs']\n",
" st_al = _CACHE['states_aligned']\n",
" balance = _CACHE['balance']\n",
" broker = _CACHE['broker']\n",
" tf = _CACHE['tf']\n",
" cfg = _CACHE['wfo_cfg']\n",
" scores = _CACHE['wfo_result']['window_scores']\n",
"\n",
" is_b, oos_b, emb_b, step_b = (cfg[k] for k in\n",
" ['is_bars','oos_bars','embargo_bars','step_bars'])\n",
" oos_st = window_idx * step_b + is_b + emb_b\n",
" oos_end = min(oos_st + oos_b, len(df_al))\n",
"\n",
" if oos_st >= len(df_al):\n",
" print(f'Window {window_idx} is out of data range.'); return\n",
"\n",
" df_w = df_al.iloc[oos_st:oos_end]\n",
" pr_w = probs[oos_st:oos_end]\n",
" st_w = st_al[oos_st:oos_end]\n",
"\n",
" r = vectorized_backtest(df_w, pr_w, st_w, account_size=balance,\n",
" broker=broker, tf=tf)\n",
" ts = pd.DatetimeIndex(r['equity_timestamps'])\n",
" eq = r['equity_values']\n",
" bh = balance * np.exp(np.cumsum(df_w['log_return'].fillna(0).values))\n",
"\n",
" wfo_s = scores[window_idx] if window_idx < len(scores) else float('nan')\n",
" fig, ax = plt.subplots(figsize=(12, 4))\n",
" ax.plot(ts, eq, lw=1.5, label='Strategy', color='#3498DB')\n",
" ax.plot(ts, bh, lw=1.0, ls=':', label='Buy & Hold', color='#E67E22', alpha=0.7)\n",
" ax.axhline(balance, color='grey', lw=0.6, alpha=0.5)\n",
" ax.set_title(\n",
" f'Window {window_idx} OOS: {ts[0].date()} → {ts[-1].date()} | '\n",
" f'Sharpe={r[\"sharpe_ratio\"]:.2f} DD={r[\"max_drawdown\"]*100:.1f}% '\n",
" f'Trades={r[\"n_trades\"]} WFO score={wfo_s:.3f}'\n",
" )\n",
" ax.legend(fontsize=9)\n",
" ax.set_ylabel('Equity ($)')\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"out3_drill_ui = widgets.interactive_output(_drill_window, {'window_idx': win_slider})\n",
"\n",
"wfo_btn.on_click(_run_wfo_analysis)\n",
"display(widgets.VBox([\n",
" wfo_btn,\n",
" out3_wfo,\n",
" widgets.Label('Window drill-down (uses globally loaded model, not per-window refitted):'),\n",
" win_slider,\n",
" out3_drill_ui,\n",
"]))"
]
},
{
"cell_type": "markdown",
"id": "cell08",
"metadata": {},
"source": [
"---\n",
"## Section 4 — Feature & Regime Explorer\n",
"Shows regime overlay on price, GMM vol cluster distribution, feature distribution by regime,\n",
"and XGB feature importance. Re-execute this cell after loading new data in Section 1."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell09",
"metadata": {},
"outputs": [],
"source": "def _shade_regimes(ax, ts, states):\n boundaries = np.concatenate([[0], np.where(np.diff(states))[0] + 1, [len(states)]])\n for k in range(len(boundaries) - 1):\n i, j = boundaries[k], boundaries[k+1] - 1\n s = int(states[i])\n ax.axvspan(ts[i], ts[j], alpha=0.15, color=STATE_COLORS.get(s, '#888888'), lw=0)\n\n\ndef _plot_regime_explorer():\n if 'df_aligned' not in _CACHE:\n print('Run Section 1 first.'); return\n\n df_al = _CACHE['df_aligned']\n states = _CACHE['states_aligned']\n X = _CACHE['X']\n xgb_m = _CACHE['xgb_metrics']\n tf = _CACHE['tf']\n n_st = int(states.max()) + 1\n snames = STATE_NAMES_MAP.get(n_st, STATE_NAMES_3)\n ts = df_al.index\n\n fig = plt.figure(figsize=(16, 13))\n gs = gridspec.GridSpec(3, 2, figure=fig, hspace=0.48, wspace=0.35)\n\n # ── Price + Regime overlay (full width) ──────────────────────────────────\n ax1 = fig.add_subplot(gs[0, :])\n if 'close' in df_al.columns:\n price = df_al['close'].values\n elif 'Close' in df_al.columns:\n price = df_al['Close'].values\n else:\n price = 100 * np.exp(np.cumsum(df_al['log_return'].fillna(0).values))\n _shade_regimes(ax1, ts, states)\n ax1.plot(ts, price, lw=0.6, color='black', zorder=3)\n patches = [mpatches.Patch(color=STATE_COLORS[s], alpha=0.5, label=n)\n for s, n in snames.items()]\n ax1.legend(handles=patches, loc='upper left', fontsize=9)\n ax1.set_title(f'{tf} Price + HMM Regime Overlay', fontsize=11)\n ax1.set_ylabel('Price')\n\n # ── Regime distribution pie ───────────────────────────────────────────────\n ax2 = fig.add_subplot(gs[1, 0])\n cnts = [int((states == s).sum()) for s in snames]\n pie_cols = [STATE_COLORS[s] for s in snames]\n ax2.pie(cnts, labels=list(snames.values()), colors=pie_cols,\n autopct='%1.1f%%', startangle=90)\n ax2.set_title('Regime Distribution')\n\n # ── GMM vol cluster distribution ─────────────────────────────────────────\n ax3 = fig.add_subplot(gs[1, 1])\n if 'gmm_vol_cluster' in df_al.columns:\n clusters = df_al['gmm_vol_cluster'].dropna().astype(int)\n unique_c = sorted(clusters.unique())\n counts_c = [int((clusters == c).sum()) for c in unique_c]\n bar_cols = ['#3498DB', '#E67E22', '#9B59B6', '#1ABC9C'][:len(unique_c)]\n ax3.bar(unique_c, counts_c, color=bar_cols, edgecolor='white')\n for c, cnt in zip(unique_c, counts_c):\n ax3.text(c, cnt + counts_c[0] * 0.01,\n f'{cnt/len(clusters)*100:.1f}%', ha='center', fontsize=8)\n ax3.set_title('GMM Volatility Cluster Distribution')\n ax3.set_xlabel('Cluster ID')\n ax3.set_ylabel('Bar count')\n else:\n ax3.text(0.5, 0.5, 'gmm_vol_cluster not in data',\n transform=ax3.transAxes, ha='center', va='center')\n\n # ── Feature distributions per regime ─────────────────────────────────────\n ax4 = fig.add_subplot(gs[2, 0])\n feat = 'rsi_slope' if 'rsi_slope' in X.columns else X.columns[1]\n for s, sname in snames.items():\n mask = states == s # numpy bool — use positional indexing, not .loc\n vals = X[mask][feat].dropna() if mask.sum() > 0 else pd.Series(dtype=float)\n if len(vals) > 0:\n ax4.hist(vals, bins=60, alpha=0.5, label=sname,\n color=STATE_COLORS[s], density=True)\n ax4.set_title(f'{feat} Distribution per Regime')\n ax4.set_xlabel(feat)\n ax4.set_ylabel('Density')\n ax4.legend(fontsize=9)\n\n # ── XGB Feature Importance ────────────────────────────────────────────────\n ax5 = fig.add_subplot(gs[2, 1])\n imp = xgb_m.get('feature_importance', {})\n if imp:\n sorted_imp = sorted(imp.items(), key=lambda x: x[1])\n names_i, vals_i = zip(*sorted_imp)\n colors_i = ['#3498DB' if v > 0.05 else '#BDC3C7' for v in vals_i]\n ax5.barh(names_i, vals_i, color=colors_i)\n ax5.set_title('XGB Feature Importance')\n ax5.set_xlabel('Importance')\n else:\n ax5.text(0.5, 0.5, 'No importance data', transform=ax5.transAxes,\n ha='center', va='center')\n\n fig.suptitle(f'Feature & Regime Explorer — {tf}', fontsize=13, y=1.01)\n plt.show()\n\n\n_plot_regime_explorer()\n"
},
{
"cell_type": "markdown",
"id": "cell10",
"metadata": {},
"source": [
"---\n",
"## Section 5 — Parameter Sensitivity\n",
"Adjust sliders then click **Run Comparison** to re-fit HMM + XGB with new params\n",
"and compare metrics against the Section 1 baseline (~30–60 s)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cell11",
"metadata": {},
"outputs": [],
"source": [
"_s = {'description_width': 'initial'}\n",
"_w = widgets.Layout(width='380px')\n",
"\n",
"n_states5_w = widgets.IntSlider(min=2, max=4, step=1, value=3,\n",
" description='n_states:', style=_s, layout=_w)\n",
"depth5_w = widgets.IntSlider(min=2, max=6, step=1, value=4,\n",
" description='max_depth:', style=_s, layout=_w)\n",
"lr5_w = widgets.FloatLogSlider(min=-3.0, max=-0.5, step=0.1, value=0.1, base=10,\n",
" description='learning_rate:', style=_s, layout=_w,\n",
" readout_format='.4f')\n",
"alpha5_w = widgets.FloatLogSlider(min=-6.0, max=-0.5, step=0.2, value=0.1, base=10,\n",
" description='reg_alpha:', style=_s, layout=_w,\n",
" readout_format='.5f')\n",
"run_btn5 = widgets.Button(description='Run Comparison', button_style='info',\n",
" layout=widgets.Layout(width='160px'))\n",
"out5 = widgets.Output()\n",
"\n",
"\n",
"def _run_sensitivity(_):\n",
" with out5:\n",
" clear_output(wait=True)\n",
" if 'df_aligned' not in _CACHE:\n",
" print('Run Section 1 first.'); return\n",
"\n",
" df_al = _CACHE['df_aligned']\n",
" df_raw = _CACHE['df']\n",
" params = _CACHE['params']\n",
" tf = _CACHE['tf']\n",
" broker = _CACHE['broker']\n",
" bal = _CACHE['balance']\n",
"\n",
" # Baseline\n",
" print('Baseline backtest ...')\n",
" bl_r = vectorized_backtest(_CACHE['df_aligned'], _CACHE['probs'],\n",
" _CACHE['states_aligned'], split_idx=_CACHE['split_idx'],\n",
" account_size=bal, broker=broker, tf=tf)\n",
"\n",
" # Modified: re-fit HMM with new n_states\n",
" n_st_new = n_states5_w.value\n",
" print(f'Fitting HMM n_states={n_st_new} ...')\n",
" new_hmm, new_st_full, _ = fit_hmm(df_raw, n_states=n_st_new, tf=tf)\n",
" new_states = pd.Series(new_st_full, index=df_raw.index).loc[df_al.index].values\n",
"\n",
" # Modified: re-train XGB with new params\n",
" kws = {k: params.get(k, v) for k, v in [\n",
" ('n_estimators', 200), ('subsample', 0.8),\n",
" ('min_child_weight', 5), ('colsample_bytree', 0.8), ('scale_pos_weight', 1.0),\n",
" ]}\n",
" kws['max_depth'] = depth5_w.value\n",
" kws['learning_rate'] = lr5_w.value\n",
" kws['gamma'] = params.get('gamma', 0.1)\n",
" kws['reg_alpha'] = alpha5_w.value\n",
"\n",
" print(f'Training XGB depth={depth5_w.value} lr={lr5_w.value:.4f} '\n",
" f'alpha={alpha5_w.value:.5f} ...')\n",
" X_new, y_new, df_al2, _ = prepare_features(df_al, new_states, tf=tf)\n",
" new_xgb, new_met = train_xgb(X_new, y_new, **kws)\n",
" _, new_probs = get_predictions(new_xgb, X_new)\n",
" new_st2 = pd.Series(new_states, index=df_al.index).loc[df_al2.index].values\n",
" new_spl = new_met.get('split_idx', int(len(X_new) * 0.8))\n",
"\n",
" print('Backtesting modified config ...')\n",
" new_r = vectorized_backtest(df_al2, new_probs, new_st2,\n",
" split_idx=new_spl, account_size=bal,\n",
" broker=broker, tf=tf)\n",
"\n",
" # Comparison bar chart\n",
" bl_oos_sh = bl_r.get('oos_sharpe_ratio', bl_r['sharpe_ratio'])\n",
" new_oos_sh = new_r.get('oos_sharpe_ratio', new_r['sharpe_ratio'])\n",
" metrics_cmp = {\n",
" 'OOS Sharpe': [bl_oos_sh, new_oos_sh],\n",
" 'Max DD %': [bl_r['max_drawdown'] * 100, new_r['max_drawdown'] * 100],\n",
" 'Win Rate %': [bl_r['win_rate'] * 100, new_r['win_rate'] * 100],\n",
" 'Profit Factor': [bl_r.get('profit_factor', 0), new_r.get('profit_factor', 0)],\n",
" 'Recovery F.': [bl_r.get('recovery_factor', 0), new_r.get('recovery_factor', 0)],\n",
" }\n",
"\n",
" orig_n_st = int(_CACHE['states'].max()) + 1\n",
" bl_lab = (f'Baseline\\nn_st={orig_n_st} depth={params.get(\"max_depth\",\"?\")}')\n",
" new_lab = (f'Modified\\nn_st={n_st_new} depth={depth5_w.value} '\n",
" f'lr={lr5_w.value:.3f}')\n",
"\n",
" fig, axes = plt.subplots(1, len(metrics_cmp), figsize=(16, 4))\n",
" for ax, (name, (bv, nv)) in zip(axes, metrics_cmp.items()):\n",
" ax.bar([bl_lab, new_lab], [bv, nv], color=['#3498DB', '#E67E22'],\n",
" width=0.5, edgecolor='white')\n",
" for xi, v in enumerate([bv, nv]):\n",
" ax.text(xi, v * 1.02 + 0.001, f'{v:.2f}',\n",
" ha='center', fontsize=9, fontweight='bold')\n",
" ax.set_title(name, fontsize=10)\n",
" ymax = max(bv, nv) * 1.35 + 0.01\n",
" ax.set_ylim(0, ymax if ymax > 0 else 1)\n",
" plt.suptitle('Baseline vs Modified Configuration', fontsize=12)\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
" print(f'\\n {\"Metric\":<22} {\"Baseline\":>12} {\"Modified\":>12} {\"Delta\":>10}')\n",
" print(' ' + '─' * 58)\n",
" for name, (bv, nv) in metrics_cmp.items():\n",
" d = nv - bv\n",
" print(f' {name:<22} {bv:>12.3f} {nv:>12.3f} {\"+\" if d>=0 else \"\"}{d:>9.3f}')\n",
"\n",
"\n",
"run_btn5.on_click(_run_sensitivity)\n",
"display(widgets.VBox([\n",
" n_states5_w, depth5_w, lr5_w, alpha5_w,\n",
" run_btn5, out5,\n",
"]))"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python",
"version": "3.11.0"
}
},
"nbformat": 4,
"nbformat_minor": 5
}