fix: hands-on bugs found during pre-submission audit

API + data integrity
- /api/settings GET no longer leaks the active Anthropic API key — returns a
  masked preview (sk-ant-XX…YYYY) plus a boolean `anthropic_api_key_set` flag
- /api/settings POST won't overwrite a real key with the masked placeholder
  the client receives back on GET (length<30 / "…" / "..." / "***" markers
  trigger a preserve-existing path)
- /api/best_result, /api/status, _make_run_dict, _result_to_dict, all AI-loop
  emits, validation_run_complete, optimization_complete, ai_iteration_complete,
  ai_targets_met, run_complete: max_drawdown and win_rate are now consistently
  emitted as PERCENTAGES (0–100), matching the dashboard's existing display
  formatters. They were previously emitted as fractions (0.13 = 13%) so the UI
  rendered "0.13%" instead of "13%"
- ResultRanker.make_result() now sets `passing` and `raw_score` on every result
  it produces. Previously these were only set during a full ranker.rank() pass,
  so individual Phase 2 / Phase 3 runs hit _make_run_dict with passing=False
  even when they cleared all gates (history showed "0 passing" when 22/22
  actually passed)
This commit is contained in:
LEGSTECH Optimizer
2026-04-25 11:55:22 +00:00
parent 6caafdb794
commit 746ab8fb11
4 changed files with 72 additions and 28 deletions
+8 -8
View File
@@ -142,7 +142,7 @@ class AIGuidedLoop:
self._emit("ai_targets_met", {
"iteration": iteration - 1,
"profit_factor": round(self.best_result.profit_factor, 3),
"max_drawdown": round(self.best_result.max_drawdown, 2),
"max_drawdown": round(self.best_result.max_drawdown * 100, 2),
"calmar": round(self.best_result.calmar, 3),
})
self.pipeline._emit_early_termination(
@@ -151,7 +151,7 @@ class AIGuidedLoop:
details={
"iteration": iteration - 1,
"profit_factor": round(self.best_result.profit_factor, 3),
"max_drawdown": round(self.best_result.max_drawdown, 2),
"max_drawdown": round(self.best_result.max_drawdown * 100, 2),
"calmar": round(self.best_result.calmar, 3),
},
)
@@ -298,7 +298,7 @@ class AIGuidedLoop:
kind="warning", iteration=iteration,
)
# Emit iteration complete
# Emit iteration complete (max_drawdown emitted as %)
self._emit("ai_iteration_complete", {
"iteration": iteration,
"max_iterations": max_iterations,
@@ -306,10 +306,10 @@ class AIGuidedLoop:
"score": round(result.score, 4),
"profit_factor": round(result.profit_factor, 3),
"calmar": round(result.calmar, 3),
"max_drawdown": round(result.max_drawdown, 2),
"max_drawdown": round(result.max_drawdown * 100, 2),
"net_profit": round(result.net_profit, 2),
"total_trades": result.total_trades,
"passing": result.passing,
"passing": bool(result.passing),
"best_score": round(self.best_result.score, 4) if self.best_result else 0,
"best_pf": round(self.best_result.profit_factor, 3) if self.best_result else 0,
"best_calmar": round(self.best_result.calmar, 3) if self.best_result else 0,
@@ -327,10 +327,10 @@ class AIGuidedLoop:
"net_profit": round(result.net_profit, 2),
"calmar": round(result.calmar, 3),
"profit_factor": round(result.profit_factor, 3),
"win_rate": round(result.win_rate, 1),
"max_drawdown": round(result.max_drawdown, 2),
"win_rate": round(result.win_rate * 100, 1),
"max_drawdown": round(result.max_drawdown * 100, 2),
"total_trades": result.total_trades,
"passing": result.passing,
"passing": bool(result.passing),
"score": round(result.score, 4),
"progress_pct": round(self.pipeline._run_count / max(self.pipeline._total_runs, 1) * 100),
})
+20 -14
View File
@@ -131,8 +131,8 @@ class OptimizationPipeline:
"best_net_profit": round(best.net_profit, 2) if best else None,
"best_calmar": round(best.calmar, 3) if best else None,
"best_pf": round(best.profit_factor, 3) if best else None,
"best_win_rate": round(best.win_rate, 1) if best else None,
"best_max_drawdown": round(best.max_drawdown, 2) if best else None,
"best_win_rate": round(best.win_rate * 100, 1) if best else None,
"best_max_drawdown": round(best.max_drawdown * 100, 2) if best else None,
"best_total_trades": best.total_trades if best else None,
}
@@ -495,7 +495,7 @@ class OptimizationPipeline:
"net_profit": round(oos_result.net_profit, 2),
"profit_factor": round(oos_result.profit_factor, 3),
"calmar": round(oos_result.calmar, 3),
"max_drawdown": round(oos_result.max_drawdown, 2),
"max_drawdown": round(oos_result.max_drawdown * 100, 2),
"total_trades": oos_result.total_trades,
"passing": oos_result.passing,
})
@@ -566,9 +566,9 @@ class OptimizationPipeline:
"net_profit": round(sr.net_profit, 2),
"profit_factor": round(sr.profit_factor, 3),
"calmar": round(sr.calmar, 3),
"max_drawdown": round(sr.max_drawdown, 2),
"max_drawdown": round(sr.max_drawdown * 100, 2),
"total_trades": sr.total_trades,
"passing": sr.passing,
"passing": bool(sr.passing),
})
# Determine verdict
@@ -603,8 +603,8 @@ class OptimizationPipeline:
"net_profit": round(self.final_result.net_profit, 2),
"calmar": round(self.final_result.calmar, 3),
"profit_factor": round(self.final_result.profit_factor, 3),
"win_rate": round(self.final_result.win_rate, 1),
"max_drawdown": round(self.final_result.max_drawdown, 2),
"win_rate": round(self.final_result.win_rate * 100, 1),
"max_drawdown": round(self.final_result.max_drawdown * 100, 2),
"total_trades": self.final_result.total_trades,
"oos_profit": round(oos_result.net_profit, 2) if oos_result else None,
"oos_calmar": round(oos_result.calmar, 3) if oos_result else None,
@@ -863,7 +863,13 @@ class OptimizationPipeline:
# ── Helpers ───────────────────────────────────────────────────────────────
def _make_run_dict(self, run_id: str, result: RankedResult, phase: str) -> dict:
"""Canonical run dict used for both _completed_runs and run_complete emits."""
"""Canonical run dict used for both _completed_runs and run_complete emits.
IMPORTANT — unit convention: win_rate and max_drawdown are emitted as
PERCENTAGES (0100), not fractions, so the dashboard can render them
directly with `${val}%`. RankedResult stores these as fractions so we
scale here at the API boundary.
"""
d = {
"run_id": run_id,
"phase": phase,
@@ -871,10 +877,10 @@ class OptimizationPipeline:
"net_profit": round(result.net_profit, 2),
"calmar": round(result.calmar, 3),
"profit_factor": round(result.profit_factor, 3),
"win_rate": round(result.win_rate, 1),
"max_drawdown": round(result.max_drawdown, 2),
"win_rate": round(result.win_rate * 100, 1),
"max_drawdown": round(result.max_drawdown * 100, 2),
"total_trades": result.total_trades,
"passing": result.passing,
"passing": bool(result.passing),
"score": round(result.score, 4),
"params": result.params,
}
@@ -891,10 +897,10 @@ class OptimizationPipeline:
"net_profit": round(r.net_profit, 2),
"calmar": round(r.calmar, 3),
"profit_factor": round(r.profit_factor, 3),
"win_rate": round(r.win_rate, 1),
"max_drawdown": round(r.max_drawdown, 2),
"win_rate": round(r.win_rate * 100, 1), # fraction → %
"max_drawdown": round(r.max_drawdown * 100, 2), # fraction → %
"total_trades": r.total_trades,
"passing": r.passing,
"passing": bool(r.passing),
"params": r.params, # full param dict
"params_summary": self._params_summary(r.params),
}
+6 -1
View File
@@ -165,7 +165,7 @@ class ResultRanker:
run_id=run_id, params=params, phase=phase,
error=error or "run_failed",
)
return RankedResult(
result = RankedResult(
run_id = run_id,
params = params,
phase = phase,
@@ -176,3 +176,8 @@ class ResultRanker:
max_drawdown = getattr(metrics, "max_drawdown_pct", 0.0) or 0.0,
total_trades = getattr(metrics, "total_trades", 0) or 0,
)
# Set passing + raw_score now so single-run dispatchers (Phase 2 AI loop,
# validation runs) get correct values without waiting for a full rank() pass.
result.passing = self._is_passing(result)
result.raw_score = self._raw_score(result) if result.passing else 0.0
return result