Step 3 – Strategy validation, regime detection and UI improvements

Main changes:
- Implemented multi-horizon trend regime detection in src/core/market_regime.py
  - EMA(20, 50, 100, 200) trend score model
  - 5 regime states: bull_strong, bull_moderate, sideways, bear_moderate, bear_strong
  - asymmetric persistence filter (bull 5 bars, sideways 3 bars, bear 2 bars)
  - window regime classification using average trend score

- Extended walk-forward outputs
  - window_regimes now include:
    - regime_detail
    - bull_strong_pct
    - bull_moderate_pct
    - sideways_detail_pct
    - bear_moderate_pct
    - bear_strong_pct
    - avg_score

- UI improvements in Step 3
  - regime analysis cards redesigned to show 5 regimes
  - visual "M layout": bull regimes on top, sideways + bear regimes below
  - table updated to display detailed regime percentages
  - equity chart background colored by regime (colorblind-friendly palette)
  - trade density chart improved with aligned Y-axis zero levels

- UX improvements
  - automatic scroll to charts when selecting a strategy
  - better regime badges and labeling
  - colorblind-friendly visualization

Result:
Step 3 now provides full strategy inspection including:
- OOS performance
- regime behaviour
- regime distribution per WF window
- visual regime overlays

Next step:
Implement strategy promotion / selection layer before Step 4 (parameter refinement).
This commit is contained in:
dam
2026-03-06 20:39:37 +01:00
parent 365304f396
commit a42255d58c
4 changed files with 702 additions and 8 deletions

View File

@@ -10,6 +10,7 @@ from src.data.storage import StorageManager
from src.utils.logger import log
from src.core.walk_forward import WalkForwardValidator
from src.core.market_regime import TrendScoreConfig, compute_regimes_for_windows
from src.risk.stops.fixed_stop import FixedStop
from src.risk.stops.trailing_stop import TrailingStop
@@ -184,10 +185,43 @@ def _compute_wf_diagnostics(
}
def _compute_regime_performance(
*,
window_returns_pct: List[float],
window_trades: List[int],
regime_windows: List[Dict[str, Any]],
) -> Dict[str, Any]:
def _stats(indices: List[int]) -> Dict[str, Any]:
rets = [float(window_returns_pct[i]) for i in indices if i < len(window_returns_pct)]
trds = [int(window_trades[i]) for i in indices if i < len(window_trades)]
return {
"n_windows": int(len(indices)),
"mean_return_pct": float(np.mean(rets)) if rets else 0.0,
"positive_window_rate": float(np.mean(np.asarray(rets) > 0.0)) if rets else 0.0,
"avg_trades": float(np.mean(trds)) if trds else 0.0,
}
out_group: Dict[str, Any] = {}
for regime in ("bull", "sideways", "bear"):
idx = [i for i, rw in enumerate(regime_windows) if rw.get("regime") == regime]
out_group[regime] = _stats(idx)
out_detail: Dict[str, Any] = {}
for regime in ("bull_strong", "bull_moderate", "sideways", "bear_moderate", "bear_strong"):
idx = [i for i, rw in enumerate(regime_windows) if rw.get("regime_detail") == regime]
out_detail[regime] = _stats(idx)
return {
"group": out_group,
"detail": out_detail,
}
# --------------------------------------------------
# Main
# --------------------------------------------------
def inspect_strategies_config(
*,
storage: StorageManager,
@@ -242,6 +276,27 @@ def inspect_strategies_config(
test_td = pd.Timedelta(days=int(payload.wf.test_days))
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
# Regime analysis is market-level (shared by all strategies for the same WF config)
regime_cfg = TrendScoreConfig()
wf_probe = WalkForwardValidator(
strategy_class=BuyAndHold,
param_grid=None,
fixed_params={},
data=df,
train_window=train_td,
test_window=test_td,
step_size=step_td,
initial_capital=float(payload.account_equity),
commission=float(payload.commission),
slippage=float(payload.slippage),
position_sizer=base_sizer,
stop_loss=stop_loss,
verbose=False,
)
wf_windows = wf_probe._generate_windows()
regime_bundle = compute_regimes_for_windows(df, wf_windows, config=regime_cfg)
regime_by_window = {int(r["window"]): r for r in regime_bundle["by_window"]}
overall_status = "ok"
results: List[Dict[str, Any]] = []
@@ -434,6 +489,7 @@ def inspect_strategies_config(
msg = "WF OK"
for _, r in win_df.iterrows():
regime_meta = regime_by_window.get(int(r["window"]), {})
windows_out.append({
"window": int(r["window"]),
"train_start": str(r["train_start"]),
@@ -445,6 +501,13 @@ def inspect_strategies_config(
"max_dd_pct": float(r["max_dd_pct"]),
"trades": int(r["trades"]),
"params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
"regime": regime_meta.get("regime"),
"regime_detail": regime_meta.get("regime_detail"),
"bull_strong_pct": float(regime_meta.get("bull_strong_pct", 0.0)),
"bull_moderate_pct": float(regime_meta.get("bull_moderate_pct", 0.0)),
"sideways_detail_pct": float(regime_meta.get("sideways_detail_pct", 0.0)),
"bear_moderate_pct": float(regime_meta.get("bear_moderate_pct", 0.0)),
"bear_strong_pct": float(regime_meta.get("bear_strong_pct", 0.0)),
})
oos_returns = win_df["return_pct"].astype(float).tolist()
@@ -462,6 +525,17 @@ def inspect_strategies_config(
hist_bins=10,
)
regime_windows = [regime_by_window.get(int(r["window"]), {"window": int(r["window"]), "regime": "sideways", "bull_pct": 0.0, "sideways_pct": 0.0, "bear_pct": 0.0}) for _, r in win_df.iterrows()]
diagnostics["regimes"] = {
"config": regime_bundle["config"],
"by_window": regime_windows,
"performance": _compute_regime_performance(
window_returns_pct=oos_returns,
window_trades=win_df["trades"].astype(int).tolist(),
regime_windows=regime_windows,
),
}
# keep worst-window DD also at top-level for backwards compatibility
diagnostics["drawdown"]["worst_window_dd_pct"] = float(oos_max_dd)
@@ -486,6 +560,7 @@ def inspect_strategies_config(
"window_returns_pct": oos_returns,
"window_equity": eq_curve,
"window_trades": win_df["trades"].tolist(),
"window_regimes": diagnostics.get("regimes", {}).get("by_window", []),
"diagnostics": diagnostics,
}
@@ -529,6 +604,10 @@ def inspect_strategies_config(
"commission": payload.commission,
"slippage": payload.slippage,
},
"regimes": {
"config": regime_bundle["config"],
"by_window": regime_bundle["by_window"],
},
}
if include_series: