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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user