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
+ ? `${warnings.map(w => `- ${escapeHtml(w)}
`).join("")}
`
+ : "";
+
+ el.innerHTML = `
+
+
+
${escapeHtml(title)}
+
${escapeHtml(message || "")}
+ ${warnHtml}
+
+
+ `;
+}
+
+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);