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:
LEGSTECH Optimizer
2026-04-13 03:05:40 +00:00
parent 2546b4a9c0
commit 26e5f9d0c4
6 changed files with 30 additions and 29 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+20 -4
View File
@@ -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)
+1 -1
View File
@@ -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()">
+1 -1
View File
@@ -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
View File
@@ -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)