mirror of
https://github.com/lucasmos/GoldRegime_X.git
synced 2026-11-02 06:26:15 +00:00
Add interactive research notebook for GoldRegime X and update study_headway_cent.db
This commit is contained in:
Binary file not shown.
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user