diff --git a/app.py b/app.py index cb2a61a..df1cf04 100644 --- a/app.py +++ b/app.py @@ -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 diff --git a/optimizer/ai_guided_loop.py b/optimizer/ai_guided_loop.py index bcffd90..74bc359 100644 --- a/optimizer/ai_guided_loop.py +++ b/optimizer/ai_guided_loop.py @@ -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), }) diff --git a/optimizer/pipeline.py b/optimizer/pipeline.py index c790794..ae2a847 100644 --- a/optimizer/pipeline.py +++ b/optimizer/pipeline.py @@ -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), } diff --git a/optimizer/result_ranker.py b/optimizer/result_ranker.py index 4c70f0c..cd9800b 100644 --- a/optimizer/result_ranker.py +++ b/optimizer/result_ranker.py @@ -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