fix: 4 critical optimizer bugs
- NaN composite score: dropna().mean() on empty series returns NaN, nan is truthy so (nan or 0)=nan. Fixed: use None when no MFE data so scorer uses 0.5 fallback instead of propagating NaN. - Analysis stopping: gate WFV was importing from main.execute_run (old CLI code that uses broken runner). Fixed: gate.run_walk_forward now accepts an executor callable from optimizer_loop. - Back to Dashboard opened blank tab: Reports button used window.open(_blank). Fixed: same-tab navigation + history.back(). - NaN in reports card: downstream of composite_score NaN above.
This commit is contained in:
Binary file not shown.
Binary file not shown.
+20
-4
@@ -268,11 +268,24 @@ class OptimizerLoop:
|
||||
no_improve_count += 1
|
||||
continue
|
||||
|
||||
# Walk-forward
|
||||
# Walk-forward — pass an executor so gate never imports 'main'
|
||||
self._emit("log", {"level": "info", "msg": "Running walk-forward validation..."})
|
||||
_store, _builder, _runner, _parser, _log_rdr, _analyzers, _scorer = (
|
||||
store, builder, runner, parser, log_rdr, analyzers, scorer
|
||||
)
|
||||
def _wfv_executor(params, start, end, fold_id):
|
||||
m, _ = self._execute_run(
|
||||
run_id=fold_id, params=params,
|
||||
period_start=start, period_end=end,
|
||||
phase="wfv", hypothesis_id=None,
|
||||
store=_store, builder=_builder, runner=_runner,
|
||||
parser=_parser, log_rdr=_log_rdr,
|
||||
analyzers=_analyzers, scorer=_scorer,
|
||||
)
|
||||
return m
|
||||
wfv = gate.run_walk_forward(
|
||||
iteration_best_params, cfg, store, builder, runner,
|
||||
parser, log_rdr, analyzers, scorer,
|
||||
params=iteration_best_params,
|
||||
executor=_wfv_executor,
|
||||
)
|
||||
self._emit("log", {
|
||||
"level": "success" if wfv.passed else "warn",
|
||||
@@ -406,7 +419,10 @@ class OptimizerLoop:
|
||||
reversals = trades_df[trades_df["result_class"] == "reversal"]
|
||||
metrics.reversal_rate = len(reversals) / max(1, len(losers))
|
||||
if "mfe_capture_ratio" in trades_df.columns:
|
||||
metrics.avg_mfe_capture = float(trades_df["mfe_capture_ratio"].dropna().mean() or 0)
|
||||
# dropna() first — if no MFE data, series is all-NaN, mean()=NaN
|
||||
# Use None (not NaN) so scorer uses its safe default of 0.5
|
||||
cap_series = trades_df["mfe_capture_ratio"].dropna()
|
||||
metrics.avg_mfe_capture = float(cap_series.mean()) if not cap_series.empty else None
|
||||
|
||||
metrics.composite_score = scorer.score(metrics)
|
||||
store.save_metrics(metrics)
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<button class="btn btn-secondary" id="btn-reports" onclick="window.open('/reports','_blank')">
|
||||
<button class="btn btn-secondary" id="btn-reports" onclick="window.location.href='/reports'">
|
||||
📁 Reports
|
||||
</button>
|
||||
<button class="btn btn-pause hidden" id="btn-pause" onclick="pauseOptimizer()">
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<h1>📁 Optimization Reports</h1>
|
||||
<div class="sub">{{ runs|length }} run(s) recorded · Click any card to open the full report</div>
|
||||
</div>
|
||||
<a class="back-link" href="/">← Back to Dashboard</a>
|
||||
<a class="back-link" href="/" onclick="if(document.referrer){{history.back();return false;}}">← Back to Dashboard</a>
|
||||
</header>
|
||||
|
||||
{% if not runs %}
|
||||
|
||||
+8
-23
@@ -60,16 +60,9 @@ class ValidationGate:
|
||||
|
||||
def run_walk_forward(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
cfg: dict,
|
||||
store, # DataStore
|
||||
builder, # IniBuilder
|
||||
runner, # MT5Runner
|
||||
parser, # ReportParser
|
||||
log_rdr, # TradeLogReader
|
||||
analyzers, # list[BaseAnalyzer]
|
||||
scorer, # CompositeScorer
|
||||
n_folds: int = 2,
|
||||
params: dict[str, Any],
|
||||
executor, # callable(params, start_str, end_str, fold_id) -> Optional[RunMetrics]
|
||||
n_folds: int = 2,
|
||||
) -> WFVResult:
|
||||
"""
|
||||
Split training period into n_folds sub-periods.
|
||||
@@ -94,21 +87,13 @@ class ValidationGate:
|
||||
if i == n_folds - 1:
|
||||
fold_end = train_end # last fold gets remainder
|
||||
|
||||
fold_id = f"wfv_fold{i+1}_{uuid.uuid4().hex[:6]}"
|
||||
logger.info(f"WFV fold {i+1}/{n_folds}: {fold_start.date()} → {fold_end.date()}")
|
||||
|
||||
# Import here to avoid circular
|
||||
from main import execute_run
|
||||
fm, _ = execute_run(
|
||||
run_id=fold_id,
|
||||
params=params,
|
||||
period_start=fold_start.strftime("%Y.%m.%d"),
|
||||
period_end=fold_end.strftime("%Y.%m.%d"),
|
||||
phase="wfv",
|
||||
hypothesis_id=None,
|
||||
cfg=cfg, store=store, builder=builder, runner=runner,
|
||||
parser=parser, log_rdr=log_rdr, analyzers=analyzers, scorer=scorer,
|
||||
)
|
||||
fold_id = f"wfv_fold{i+1}_{uuid.uuid4().hex[:6]}"
|
||||
start_s = fold_start.strftime("%Y.%m.%d")
|
||||
end_s = fold_end.strftime("%Y.%m.%d")
|
||||
|
||||
fm = executor(params, start_s, end_s, fold_id)
|
||||
if fm:
|
||||
fold_metrics.append(fm)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user