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:
@@ -320,10 +320,11 @@ def best_result():
|
||||
"net_profit": round(best_run.net_profit, 2),
|
||||
"profit_factor": round(best_run.profit_factor, 3),
|
||||
"calmar": round(best_run.calmar, 3),
|
||||
"max_drawdown": round(best_run.max_drawdown, 2),
|
||||
"win_rate": round(best_run.win_rate, 1),
|
||||
# max_drawdown + win_rate are stored as fractions on RankedResult; emit as %
|
||||
"max_drawdown": round(best_run.max_drawdown * 100, 2),
|
||||
"win_rate": round(best_run.win_rate * 100, 1),
|
||||
"total_trades": best_run.total_trades,
|
||||
"passing": best_run.passing,
|
||||
"passing": bool(best_run.passing),
|
||||
"phase": getattr(best_run, "phase", "phase2_ai"),
|
||||
"params": best_run.params,
|
||||
"evolution": evolution,
|
||||
@@ -357,9 +358,21 @@ def ai_insights_all():
|
||||
return jsonify([])
|
||||
|
||||
|
||||
def _mask_key(k: str) -> str:
|
||||
"""Mask an API key so the GET response never exposes the secret."""
|
||||
if not k:
|
||||
return ""
|
||||
if k.startswith("${") or k in ("YOUR_API_KEY", "sk-ant-..."):
|
||||
return "" # placeholder — return empty so the field shows blank
|
||||
if len(k) <= 12:
|
||||
return "***"
|
||||
return k[:8] + "…" + k[-4:]
|
||||
|
||||
|
||||
@app.route("/api/settings", methods=["GET"])
|
||||
def get_settings():
|
||||
import yaml
|
||||
import os
|
||||
config_path = BASE_DIR / "config.yaml"
|
||||
try:
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
@@ -368,10 +381,15 @@ def get_settings():
|
||||
mt5_cfg = cfg.get("mt5", {})
|
||||
broker_cfg = cfg.get("broker", {})
|
||||
thresh_cfg = cfg.get("thresholds", {})
|
||||
# Resolve the active API key — placeholder in config falls back to env var.
|
||||
raw_key = ai_cfg.get("anthropic_api_key", "")
|
||||
if not raw_key or raw_key.startswith("${"):
|
||||
raw_key = os.environ.get("ANTHROPIC_API_KEY", "")
|
||||
return jsonify({
|
||||
"ai": {
|
||||
"enabled": ai_cfg.get("enabled", True),
|
||||
"anthropic_api_key": ai_cfg.get("anthropic_api_key", ""),
|
||||
"anthropic_api_key": _mask_key(raw_key),
|
||||
"anthropic_api_key_set": bool(raw_key),
|
||||
"model": ai_cfg.get("model", "claude-opus-4-7"),
|
||||
"timeout_seconds": ai_cfg.get("timeout_seconds", 30),
|
||||
},
|
||||
@@ -411,10 +429,25 @@ def save_settings():
|
||||
if section in data and isinstance(data[section], dict):
|
||||
if section not in cfg:
|
||||
cfg[section] = {}
|
||||
# Don't overwrite the real API key with the masked one we sent
|
||||
# the client. We accept a key only if it's empty (clearing) or
|
||||
# looks like a full key (sk-ant-… with no mask markers and at
|
||||
# least 30 chars). Anything ambiguous → preserve the existing.
|
||||
if section == "ai":
|
||||
incoming_key = data["ai"].get("anthropic_api_key", "")
|
||||
if incoming_key:
|
||||
looks_masked = (
|
||||
"…" in incoming_key
|
||||
or "..." in incoming_key
|
||||
or "***" in incoming_key
|
||||
or len(incoming_key) < 30
|
||||
)
|
||||
if looks_masked:
|
||||
data["ai"].pop("anthropic_api_key", None)
|
||||
cfg[section].update(data[section])
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(cfg, f, default_flow_style=False, allow_unicode=True)
|
||||
return jsonify({"ok": True})
|
||||
return jsonify({"ok": True, "note": "Settings saved. Will apply on the next optimization run."})
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "error": str(e)}), 500
|
||||
|
||||
|
||||
@@ -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
@@ -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 (0–100), 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),
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user