From 8259e85b687c1459ada01cc1096e85b5867338c3 Mon Sep 17 00:00:00 2001 From: DaM Date: Mon, 2 Mar 2026 20:04:42 +0100 Subject: [PATCH] Update status for using KubuntuPC --- src/calibration/strategies_inspector.py | 61 +++++- src/strategies/rsi_strategy.py | 18 +- .../api/v2/schemas/calibration_strategies.py | 5 + .../static/js/pages/calibration_strategies.js | 190 ++++++++++++------ 4 files changed, 203 insertions(+), 71 deletions(-) diff --git a/src/calibration/strategies_inspector.py b/src/calibration/strategies_inspector.py index 72f152a..43ac929 100644 --- a/src/calibration/strategies_inspector.py +++ b/src/calibration/strategies_inspector.py @@ -135,8 +135,6 @@ def inspect_strategies_config( overall_status = "ok" - log.info(f"🔥 Strategies received: {len(payload.strategies)}") - results: List[Dict[str, Any]] = [] series: Dict[str, Any] = {"strategies": {}} if include_series else {} @@ -154,6 +152,9 @@ def inspect_strategies_config( "strategy_id": sid, "status": "fail", "message": f"Unknown strategy_id: {sid}", + "warnings": [], + "series_available": False, + "series_error": "Unknown strategy_id (not in registry)", "n_windows": 0, "oos_final_equity": payload.account_equity, "oos_total_return_pct": 0.0, @@ -180,6 +181,9 @@ def inspect_strategies_config( "strategy_id": sid, "status": "fail", "message": msg, + "warnings": [], + "series_available": False, + "series_error": "Unknown strategy_id (not in registry)", "n_windows": 0, "oos_final_equity": payload.account_equity, "oos_total_return_pct": 0.0, @@ -224,6 +228,9 @@ def inspect_strategies_config( log.info(f"🧠 Step3 | WF run | strategy={sid}") try: + series_available = False + series_error = None + wf = WalkForwardValidator( strategy_class=strategy_class, param_grid=None, @@ -252,11 +259,54 @@ def inspect_strategies_config( status = "warning" msg = "No closed trades in OOS" warnings_list.append("Walk-forward produced no closed trades.") + + # ✅ Registrar resultado SIEMPRE (no continue silencioso) + results.append({ + "strategy_id": sid, + "status": status, + "message": msg, + "warnings": warnings_list, + + # ✅ OPCIÓN B: serie disponible si include_series (aunque sea baseline/empty) + "series_available": bool(include_series), + "series_error": None if include_series else "WF produced no closed trades / empty windows", + + "n_windows": 0, + "oos_final_equity": float(payload.account_equity), + "oos_total_return_pct": 0.0, + "oos_max_dd_worst_pct": 0.0, + "degradation_sharpe": None, + "windows": [], + }) + + # ✅ Serie mínima para poder renderizar algo (equity baseline) + if include_series: + series["strategies"][sid] = { + "window_returns_pct": [], + "window_equity": [float(payload.account_equity)], + "window_trades": [], + } + + if overall_status == "ok": + overall_status = "warning" + + continue else: + # 🔒 Validación explícita de columnas WF (no fallbacks silenciosos) + required_cols = {"return_pct", "max_dd_pct", "trades", "window", "train_start", "train_end", "test_start", "test_end", "sharpe", "params"} + missing = required_cols - set(win_df.columns) + if missing: + raise ValueError(f"WF windows missing required columns: {sorted(missing)}") + oos_returns = win_df["return_pct"].tolist() oos_dd = win_df["max_dd_pct"].tolist() n_windows = len(win_df) + required_cols = {"return_pct", "max_dd_pct", "trades", "window", "train_start", "train_end", "test_start", "test_end", "sharpe", "params"} + missing = required_cols - set(win_df.columns) + if missing: + raise ValueError(f"WF windows missing required columns: {sorted(missing)}") + trades = win_df["trades"].astype(int).tolist() too_few = sum(t < int(payload.wf.min_trades_test) for t in trades) @@ -301,7 +351,9 @@ def inspect_strategies_config( "strategy_id": sid, "status": status, "message": msg, - "warnings": warnings_list if status == "warning" else [], + "warnings": warnings_list, + "series_available": bool(include_series), + "series_error": None, "n_windows": int(len(windows_out)), "oos_final_equity": oos_final, "oos_total_return_pct": float(oos_total_return), @@ -323,6 +375,9 @@ def inspect_strategies_config( "strategy_id": sid, "status": "fail", "message": f"Exception: {e}", + "warnings": [], + "series_available": False, + "series_error": f"{type(e).__name__}: {e}", "n_windows": 0, "oos_final_equity": float(payload.account_equity), "oos_total_return_pct": 0.0, diff --git a/src/strategies/rsi_strategy.py b/src/strategies/rsi_strategy.py index 79b312f..f6d5b5e 100644 --- a/src/strategies/rsi_strategy.py +++ b/src/strategies/rsi_strategy.py @@ -10,31 +10,31 @@ class RSIStrategy(Strategy): Estrategia basada en RSI (Relative Strength Index) Señales: - - BUY: Cuando RSI < oversold_threshold (mercado sobrevendido) - - SELL: Cuando RSI > overbought_threshold (mercado sobrecomprado) + - BUY: Cuando RSI < oversold (mercado sobrevendido) + - SELL: Cuando RSI > overbought (mercado sobrecomprado) - HOLD: RSI en zona neutral Parámetros: rsi_period: Periodo del RSI (default: 14) - oversold_threshold: Umbral de sobreventa (default: 30) - overbought_threshold: Umbral de sobrecompra (default: 70) + oversold: Umbral de sobreventa (default: 30) + overbought: Umbral de sobrecompra (default: 70) """ strategy_id = "rsi" - def __init__(self, rsi_period: int = 14, oversold_threshold: float = 30, overbought_threshold: float = 70): + def __init__(self, rsi_period: int = 14, oversold: float = 30, overbought: float = 70): params = { 'rsi_period': rsi_period, - 'oversold': oversold_threshold, - 'overbought': overbought_threshold + 'oversold': oversold, + 'overbought': overbought } super().__init__(name="RSI Strategy", params=params) self.rsi_period = rsi_period - self.oversold = oversold_threshold - self.overbought = overbought_threshold + self.oversold = oversold + self.overbought = overbought @classmethod def parameters_schema(cls) -> dict: diff --git a/src/web/api/v2/schemas/calibration_strategies.py b/src/web/api/v2/schemas/calibration_strategies.py index c1edb96..d272f3b 100644 --- a/src/web/api/v2/schemas/calibration_strategies.py +++ b/src/web/api/v2/schemas/calibration_strategies.py @@ -57,6 +57,11 @@ class StrategyRunResultSchema(BaseModel): status: Literal["ok", "warning", "fail"] message: str + # ✅ explicitar warnings y disponibilidad de serie + warnings: List[str] = Field(default_factory=list) + series_available: bool = False + series_error: Optional[str] = None + n_windows: int oos_final_equity: float oos_total_return_pct: float diff --git a/src/web/ui/v2/static/js/pages/calibration_strategies.js b/src/web/ui/v2/static/js/pages/calibration_strategies.js index 2373258..6f03708 100644 --- a/src/web/ui/v2/static/js/pages/calibration_strategies.js +++ b/src/web/ui/v2/static/js/pages/calibration_strategies.js @@ -795,7 +795,10 @@ function renderResultsTable(data) { ${Number(r.oos_total_return_pct).toFixed(2)}% ${Number(r.oos_max_dd_worst_pct).toFixed(2)}% ${Number(r.oos_final_equity).toFixed(2)} - ${r.message || ""} + + ${r.message || ""} + ${Array.isArray(r.warnings) && r.warnings.length ? `
${r.warnings.map(escapeHtml).join(" · ")}
` : ""} + `); }); @@ -827,7 +830,10 @@ function populatePlotSelector(data) { if (!sel) return; sel.innerHTML = ""; - const ids = Object.keys((data.series && data.series.strategies) ? data.series.strategies : {}); + + // ✅ usar results, no series (para que aparezcan también warning/fail) + const ids = (data.results || []).map(r => r.strategy_id); + ids.forEach((sid) => { const opt = document.createElement("option"); opt.value = sid; @@ -835,43 +841,74 @@ function populatePlotSelector(data) { sel.appendChild(opt); }); - sel.onchange = () => renderPlotsForSelected(data); + sel.onchange = () => { + const sid = sel.value; + selectStrategy(sid, data); + }; if (ids.length > 0) { - sel.value = ids[0]; + sel.value = selectedStrategyId || ids[0]; } } -function renderPlotsForSelected(data) { - const sel = document.getElementById("plot_strategy_select"); - const sid = sel ? sel.value : null; - if (!sid) return; +function selectStrategy(strategyId, data) { + if (!strategyId || !data) return; - const s = data.series?.strategies?.[sid]; - if (!s) return; + selectedStrategyId = strategyId; - const equity = s.window_equity || []; - const returns = s.window_returns_pct || []; - const xEq = [...Array(equity.length).keys()]; - const xRet = [...Array(returns.length).keys()].map((i) => i + 1); + const row = (data.results || []).find(r => r.strategy_id === strategyId); - Plotly.newPlot("plot_equity", [ - { x: xEq, y: equity, type: "scatter", mode: "lines", name: "Equity (OOS)" }, - ], { - title: `WF OOS equity · ${sid}`, - margin: { t: 40, l: 50, r: 20, b: 40 }, - xaxis: { title: "Window index" }, - yaxis: { title: "Equity" }, - }, { displayModeBar: false }); + // 1) Alerts por status (siempre explícito) + if (row?.status === "warning") { + showPlotAlert("warning", `WARNING — ${strategyId}`, row.message || "Strategy warning.", row.warnings); + } else if (row?.status === "fail") { + showPlotAlert("danger", `FAIL — ${strategyId}`, row.message || "Strategy failed.", row.warnings); + } else { + clearPlotAlert(); + } - Plotly.newPlot("plot_returns", [ - { x: xRet, y: returns, type: "bar", name: "Return % (per window)" }, - ], { - title: `WF returns per window · ${sid}`, - margin: { t: 40, l: 50, r: 20, b: 40 }, - xaxis: { title: "Window" }, - yaxis: { title: "Return (%)" }, - }, { displayModeBar: false }); + // 2) Si el backend indica que NO hay serie, no intentamos renderizar + if (row && row.series_available === false) { + showPlotAlert( + row.status === "fail" ? "danger" : "warning", + `${(row.status || "warning").toUpperCase()} — ${strategyId}`, + row.series_error || row.message || "No chart series available for this strategy.", + row.warnings + ); + clearPlots(); + highlightSelectedRow(strategyId); + return; + } + + // 3) Renderizar serie si existe + const s = data?.series?.strategies?.[strategyId]; + if (!s) { + // fallback explícito (por si backend antiguo no manda series_available) + showPlotAlert( + row?.status === "fail" ? "danger" : "warning", + `${(row?.status || "warning").toUpperCase()} — ${strategyId}`, + row?.message || "No chart series available for this strategy.", + row?.warnings + ); + clearPlots(); + highlightSelectedRow(strategyId); + return; + } + + renderStrategyCharts(strategyId, s, data); + highlightSelectedRow(strategyId); + + // 4) Caso “serie vacía” (opción B) -> warning explícito (aunque ya lo tengas) + const trd = s.window_trades || []; + const hasTrades = Array.isArray(trd) && trd.some(v => (v ?? 0) > 0); + if (!hasTrades && row?.status !== "fail") { + showPlotAlert( + "warning", + `NO TRADES — ${strategyId}`, + "Walk-forward produced no closed trades in OOS. Charts may be flat/empty.", + row?.warnings + ); + } } function renderValidateResponse(data) { @@ -902,26 +939,13 @@ function renderValidateResponse(data) { // ------------------------------- // 3️⃣ Plots (primera estrategia por ahora) // ------------------------------- - if (data.series && data.series.strategies) { - - const strategies = data.series.strategies; - const keys = Object.keys(strategies); - - if (!selectedStrategyId && keys.length > 0) { - selectedStrategyId = keys[0]; - } - - if (selectedStrategyId && strategies[selectedStrategyId]) { - renderStrategyCharts( - selectedStrategyId, - strategies[selectedStrategyId], - data - ); - highlightSelectedRow(selectedStrategyId); + if (data.results && data.results.length > 0) { + if (!selectedStrategyId) { + selectedStrategyId = data.results[0].strategy_id; } + selectStrategy(selectedStrategyId, data); } - // ------------------------------- // 4️⃣ Table // ------------------------------- @@ -981,18 +1005,7 @@ function renderValidateResponse(data) { console.log("Clicked:", selectedStrategyId); selectedStrategyId = this.dataset.strategy; - - if (!lastValidationResult?.series?.strategies[selectedStrategyId]) { - return; - } - - renderStrategyCharts( - selectedStrategyId, - lastValidationResult.series.strategies[selectedStrategyId], - lastValidationResult - ); - - highlightSelectedRow(selectedStrategyId); + selectStrategy(selectedStrategyId, lastValidationResult); }); }); } @@ -1307,6 +1320,65 @@ async function init() { }, 0); } +// ================================================= +// PLOT ALERTS (Tabler) + SAFE PLOT CLEAR +// ================================================= + +function ensurePlotAlertContainer() { + // Lo colocamos antes del primer plot si existe + let el = document.getElementById("plot_alert"); + if (el) return el; + + const anchor = document.getElementById("plot_equity"); + if (!anchor || !anchor.parentElement) return null; + + el = document.createElement("div"); + el.id = "plot_alert"; + el.className = "mb-3"; + anchor.parentElement.insertBefore(el, anchor); + return el; +} + +function escapeHtml(str) { + return String(str ?? "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function showPlotAlert(type, title, message, warnings) { + const el = ensurePlotAlertContainer(); + if (!el) return; + + const warnHtml = Array.isArray(warnings) && warnings.length + ? `` + : ""; + + el.innerHTML = ` + + `; +} + +function clearPlotAlert() { + const el = document.getElementById("plot_alert"); + if (el) el.innerHTML = ""; +} + +function clearPlots() { + const eq = document.getElementById("plot_equity"); + const ret = document.getElementById("plot_returns"); + if (eq) eq.innerHTML = ""; + if (ret) ret.innerHTML = ""; +} + document.getElementById("lock_inherited") .addEventListener("change", applyInheritedLock);