From 365304f39654793ee841938bb92cbda0bbd6ab4c Mon Sep 17 00:00:00 2001 From: dam Date: Fri, 6 Mar 2026 11:19:38 +0100 Subject: [PATCH] =?UTF-8?q?Ahora=20el=20Step=203=20esta=20parcialmente=20a?= =?UTF-8?q?cabado,=20y=20vamos=20a=20pasar=20a=20realizar=20el=20Step=203.?= =?UTF-8?q?5=20donde=20a=C3=B1adimos=20el=20regimen=20de=20mercado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/market_regime.py | 0 .../static/js/pages/calibration_strategies.js | 412 ++++++++++++++---- src/web/ui/v2/templates/base.html | 4 +- .../calibration/calibration_strategies.html | 4 +- 4 files changed, 332 insertions(+), 88 deletions(-) create mode 100644 src/core/market_regime.py diff --git a/src/core/market_regime.py b/src/core/market_regime.py new file mode 100644 index 0000000..e69de29 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 0dddb27..d3c87cf 100644 --- a/src/web/ui/v2/static/js/pages/calibration_strategies.js +++ b/src/web/ui/v2/static/js/pages/calibration_strategies.js @@ -734,98 +734,342 @@ async function pollStatus(jobId) { // ================================================= function renderEquityAndReturns(strategyId, s, data) { + const equity = s.window_equity || []; + const ret = s.window_returns_pct || []; + const trd = s.window_trades || []; - console.log("Plotly object:", Plotly); // Esto debería mostrarte el objeto Plotly completo + // X común (windows) + const n = Math.max(equity.length, ret.length, trd.length); + const x = Array.from({ length: n }, (_, i) => i); - if (Plotly && Plotly.subplots) { - console.log("make_subplots is available"); - } else { - console.error("make_subplots is NOT available"); + // ---- Escalado para alinear visualmente el 0 (returns) con el 0 (trades) ---- + const retMax = Math.max(0, ...ret); + const retMin = Math.min(0, ...ret); + + const minTrades = data.config?.wf?.min_trades_test ?? 10; + + const trdMaxRaw = Math.max(0, ...trd); + const trdMax = Math.max(trdMaxRaw, minTrades); + + const retPosSpan = Math.max(1e-9, retMax); + const retNegSpan = Math.abs(retMin); + + const trdNegSpan = (retNegSpan / retPosSpan) * trdMax; + + const y1Range = [retMin, retMax]; + const y2Range = [-trdNegSpan, trdMax]; + + // ---- Trazas ---- + const equityTrace = { + x, + y: equity, + type: "scatter", + mode: "lines", + name: "Equity", + xaxis: "x", + yaxis: "y" + }; + + const returnsTrace = { + x, + y: ret, + type: "bar", + name: "Return %", + marker: { color: "#3b82f6" }, + xaxis: "x2", + yaxis: "y2", + offsetgroup: "returns", + alignmentgroup: "bottom" + }; + + const tradesTrace = { + x, + y: trd, + type: "bar", + name: "Trades", + marker: { color: "#f59e0b" }, + xaxis: "x2", + yaxis: "y3", + offsetgroup: "trades", + alignmentgroup: "bottom" + }; + + // ---- Layout ---- + const layout = { + grid: { rows: 2, columns: 1, pattern: "independent" }, + + // IMPORTANTE: share X de verdad (pan/zoom sincronizado) + xaxis: { matches: "x2", showgrid: true }, + xaxis2: { matches: "x", showgrid: true }, + + // Dominios (más altura para ver mejor) + // Ajusta estos números a gusto: + yaxis: { + domain: [0.60, 1.00], + title: "Equity", + showgrid: true, + rangemode: "tozero" + }, + xaxis: { + domain: [0.00, 1.00], + anchor: "y" + }, + + yaxis2: { + domain: [0.00, 0.25], + title: "Return %", + range: y1Range, + zeroline: true, + zerolinewidth: 2, + showgrid: true + }, + xaxis2: { + domain: [0.00, 1.00], + anchor: "y2", + title: "Windows" + }, + + yaxis3: { + title: "Trades", + overlaying: "y2", + side: "right", + range: y2Range, + zeroline: true, + zerolinewidth: 2, + showgrid: false + }, + + // Barras lado a lado + barmode: "group", + bargap: 0.2, + + shapes: [ + { + type: "line", + x0: -0.5, + x1: n - 0.5, + y0: minTrades, + y1: minTrades, + xref: "x2", + yref: "y3", + line: { color: "red", width: 2, dash: "dash" } + } + ], + + legend: { orientation: "h" }, + margin: { t: 50, r: 70, l: 70, b: 50 } + }; + + const topDomain = layout.yaxis.domain; // [start,end] + const botDomain = layout.yaxis2.domain; // [start,end] + + layout.annotations = [ + { + text: `Equity — ${strategyId}`, + x: 0.5, xref: "paper", + y: topDomain[1] + 0.05, yref: "paper", + showarrow: false, + font: { size: 16 } + }, + { + text: `Returns & Trades — ${strategyId}`, + x: 0.5, xref: "paper", + y: botDomain[1] + 0.05, yref: "paper", + showarrow: false, + font: { size: 16 } + } + ]; + + Plotly.newPlot("plot_strategy", [equityTrace, returnsTrace, tradesTrace], layout, { + displayModeBar: true, + responsive: true + }); + + const gd = document.getElementById("plot_strategy"); + + // Limpia listeners anteriores (si re-renderizas) + gd.removeAllListeners?.("plotly_relayout"); + + // Flag para evitar bucles cuando hacemos relayout desde el listener + gd.__syncing = false; + + function clamp01(v) { + return Math.max(0, Math.min(1, v)); } - // Crea el subplot con dos filas y una columna (1x2) - var fig = Plotly.subplots.make_subplots({ - rows: 2, // 2 filas - cols: 1, // 1 columna - shared_xaxes: true, // Compartir el eje X entre ambos gráficos - vertical_spacing: 0.1, // Espacio entre los gráficos - subplot_titles: [`Equity — ${strategyId}`, `Returns & Trades — ${strategyId}`], // Títulos para cada subgráfico - column_widths: [0.7] // Ajustar el ancho de las columnas si es necesario + function zeroFrac(min, max) { + const span = max - min; + if (!isFinite(span) || span <= 1e-12) return 0; + return clamp01((0 - min) / span); + } + + // Dado un max fijo y una fracción f (posición del 0), calcula el min + function minFromMaxAndFrac(max, f) { + const denom = Math.max(1e-9, (1 - f)); + return -(f / denom) * max; + } + + gd.on("plotly_relayout", (ev) => { + if (gd.__syncing) return; + + const update = {}; + + // ===== 0) DETECTAR AUTOSCALE / RESET (prioridad máxima) ===== + // Plotly puede indicar autoscale de varias maneras. + const autoscaleTriggered = + ev["yaxis2.autorange"] === true || + ev["yaxis3.autorange"] === true || + ev["yaxis.autorange"] === true || + ev["xaxis.autorange"] === true || + ev["xaxis2.autorange"] === true || + ev["autosize"] === true || + ev["resetScale2d"] === true || + ev["xaxis.range[0]"] === undefined && ev["xaxis.range[1]"] === undefined && ev["yaxis.range[0]"] === undefined && ev["yaxis.range[1]"] === undefined + ? false + : false; + + // Si el usuario pulsa Autoscale/Reset, queremos SIEMPRE: + // - Reforzar yaxis2/yaxis3 con tus rangos calculados (0 alineado) + // - Volver a dibujar la línea minTrades + // - (Opcional) dejar X en autorange en ambos + if ( + ev["yaxis2.autorange"] === true || + ev["yaxis3.autorange"] === true || + ev["yaxis.autorange"] === true || + ev["xaxis.autorange"] === true || + ev["xaxis2.autorange"] === true || + ev["resetScale2d"] === true + ) { + // update["yaxis2.autorange"] = false; + // update["yaxis3.autorange"] = false; + update["yaxis2.range"] = y1Range; + update["yaxis3.range"] = y2Range; + + // Asegura que la línea de Min Trades se vea SIEMPRE + update["shapes[0].type"] = "line"; + update["shapes[0].xref"] = "x2"; + update["shapes[0].yref"] = "y3"; + update["shapes[0].x0"] = -0.5; + update["shapes[0].x1"] = n - 0.5; + update["shapes[0].y0"] = minTrades; + update["shapes[0].y1"] = minTrades; + update["shapes[0].line.color"] = "red"; + update["shapes[0].line.width"] = 2; + update["shapes[0].line.dash"] = "dash"; + + // Mantén X sincronizado también tras autoscale + // if (ev["xaxis.autorange"] === true || ev["xaxis2.autorange"] === true || ev["resetScale2d"] === true) { + // update["xaxis.autorange"] = true; + // update["xaxis2.autorange"] = true; + // } + + gd.__syncing = true; + Plotly.relayout(gd, update).finally(() => { + gd.__syncing = false; + }); + return; // IMPORTANTÍSIMO: no seguimos con el resto de sincronizaciones + } + + // ===== 1) Sync X arriba/abajo (pan/zoom normal) ===== + if (ev["xaxis2.range[0]"] !== undefined && ev["xaxis2.range[1]"] !== undefined) { + update["xaxis.range"] = [ev["xaxis2.range[0]"], ev["xaxis2.range[1]"]]; + update["xaxis.autorange"] = false; + } + + if (ev["xaxis.range[0]"] !== undefined && ev["xaxis.range[1]"] !== undefined) { + update["xaxis2.range"] = [ev["xaxis.range[0]"], ev["xaxis.range[1]"]]; + update["xaxis2.autorange"] = false; + } + + // ===== 2) Mantener 0 alineado cuando pan/zoom en Y2 o Y3 ===== + const y2Changed = ev["yaxis2.range[0]"] !== undefined && ev["yaxis2.range[1]"] !== undefined; + const y3Changed = ev["yaxis3.range[0]"] !== undefined && ev["yaxis3.range[1]"] !== undefined; + + const full = gd._fullLayout || {}; + const curY2 = full.yaxis2?.range || y1Range; + const curY3 = full.yaxis3?.range || y2Range; + + if (y2Changed && !y3Changed) { + const newY2 = [ev["yaxis2.range[0]"], ev["yaxis2.range[1]"]]; + const f = zeroFrac(newY2[0], newY2[1]); + + const y3MaxKeep = (curY3 && curY3.length === 2) ? curY3[1] : y2Range[1]; + const y3Min = minFromMaxAndFrac(y3MaxKeep, f); + + update["yaxis2.autorange"] = false; + update["yaxis3.autorange"] = false; + update["yaxis2.range"] = newY2; + update["yaxis3.range"] = [y3Min, y3MaxKeep]; + } + + if (y3Changed && !y2Changed) { + const newY3 = [ev["yaxis3.range[0]"], ev["yaxis3.range[1]"]]; + const f = zeroFrac(newY3[0], newY3[1]); + + const y2MaxKeep = (curY2 && curY2.length === 2) ? curY2[1] : y1Range[1]; + const y2Min = minFromMaxAndFrac(y2MaxKeep, f); + + update["yaxis2.autorange"] = false; + update["yaxis3.autorange"] = false; + update["yaxis3.range"] = newY3; + update["yaxis2.range"] = [y2Min, y2MaxKeep]; + } + + // Si ambos cambian (zoom box), prioriza el zeroFrac de y2 + if (y2Changed && y3Changed) { + const newY2 = [ev["yaxis2.range[0]"], ev["yaxis2.range[1]"]]; + const f = zeroFrac(newY2[0], newY2[1]); + + const newY3 = [ev["yaxis3.range[0]"], ev["yaxis3.range[1]"]]; + const y3MaxKeep = newY3[1]; + const y3Min = minFromMaxAndFrac(y3MaxKeep, f); + + update["yaxis2.autorange"] = false; + update["yaxis3.autorange"] = false; + update["yaxis2.range"] = newY2; + update["yaxis3.range"] = [y3Min, y3MaxKeep]; + } + + // ===== 3) Re-asegurar la línea minTrades (por si Plotly la toca en relayouts) ===== + // (No molesta y evita que desaparezca en algunos reset) + update["shapes[0].y0"] = minTrades; + update["shapes[0].y1"] = minTrades; + + if (Object.keys(update).length === 0) return; + + gd.__syncing = true; + Plotly.relayout(gd, update).finally(() => { + gd.__syncing = false; + }); }); - - // Datos de Equity - const equityTrace = { - y: s.window_equity, // Datos de la equity - type: "scatter", // Tipo de gráfico: línea - mode: "lines", // Modo: línea - name: "Equity" - }; - - // Datos de Return % - const returnsTrace = { - y: s.window_returns_pct || [], // Datos de returns - type: "bar", // Tipo de gráfico: barra - name: "Return %", - marker: { color: "#3b82f6" }, // Color de la barra - yaxis: "y1", // Asociar al primer eje Y - }; - - // Datos de Trades - const tradesTrace = { - y: s.window_trades || [], // Datos de trades - type: "bar", // Tipo de gráfico: barra - name: "Trades", - marker: { color: "#f59e0b" }, // Color de la barra - yaxis: "y2", // Asociar al segundo eje Y - }; - - // Agregar la traza de "Equity" al subplot (fila 1, columna 1) - fig.addTrace(equityTrace, 1, 1); - - // Agregar las trazas de "Returns" y "Trades" al subplot (fila 2, columna 1) - fig.addTrace(returnsTrace, 2, 1); - fig.addTrace(tradesTrace, 2, 1); - - // Configurar los ejes y los márgenes - fig.update_layout({ - title: `Strategy Overview — ${strategyId}`, - yaxis: { - title: "Equity", - showgrid: true - }, - yaxis2: { - title: "Return % / Trades", - overlaying: "y", - side: "right", - showgrid: true - }, - xaxis: { - title: "Windows", - showgrid: true - }, - showlegend: true - }); - - // Renderizar en el contenedor del gráfico - Plotly.newPlot("plot_strategy", fig); } function renderRollingSharpe(strategyId, s, data) { const roll = s.diagnostics?.rolling?.rolling_sharpe_like || []; const x = roll.map((_, i) => i + 1); - Plotly.newPlot("plot_strategy", [{ - x, - y: roll, - type: "scatter", - mode: "lines+markers", - name: `Rolling Sharpe-like (k=${s.diagnostics?.rolling?.rolling_window ?? "?"})` - }], { - margin: { t: 40 }, - title: `Rolling Sharpe-like — ${strategyId}`, - xaxis: { title: "Window" }, - yaxis: { title: "Sharpe-like" } - }); + const k = s.diagnostics?.rolling?.rolling_window ?? "?"; + + Plotly.newPlot( + "plot_strategy", + [ + { + x, + y: roll, + type: "scatter", + mode: "lines+markers", + name: `Rolling Sharpe-like (k=${k})` + } + ], + { + margin: { t: 60, r: 40, l: 60, b: 50 }, + title: { text: `Rolling Sharpe-like — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico + xaxis: { title: "Window", showgrid: true, zeroline: false }, + yaxis: { title: "Sharpe-like", showgrid: true, zeroline: true, zerolinewidth: 2 }, + legend: { orientation: "h" } + }, + { responsive: true, displayModeBar: true } + ); } function renderOOSReturnsDistribution(strategyId, s, data) { @@ -841,7 +1085,7 @@ function renderOOSReturnsDistribution(strategyId, s, data) { name: "OOS Return% (bins)" }], { margin: { t: 40 }, - title: `OOS Returns Distribution — ${strategyId}`, + title: { text: `OOS Returns Distribution — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico xaxis: { title: "Return % (window)" }, yaxis: { title: "Count" } }); @@ -859,7 +1103,7 @@ function renderDrawdownEvolution(strategyId, s, data) { name: "Drawdown %" }], { margin: { t: 40 }, - title: `Drawdown Evolution — ${strategyId}`, + title: { text: `Drawdown Evolution — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico xaxis: { title: "Point (initial + windows)" }, yaxis: { title: "Drawdown %", zeroline: true } }); @@ -888,7 +1132,7 @@ function renderTradeDensity(strategyId, s, data) { } ], { margin: { t: 40 }, - title: `Trade Density — ${strategyId}`, + title: { text: `Trade Density — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico, barmode: "group", xaxis: { title: "Window" }, yaxis: { title: "Trades / window" }, diff --git a/src/web/ui/v2/templates/base.html b/src/web/ui/v2/templates/base.html index 48ebccb..3d0997a 100644 --- a/src/web/ui/v2/templates/base.html +++ b/src/web/ui/v2/templates/base.html @@ -27,9 +27,9 @@ - + {% block extra_js %}{% endblock %} diff --git a/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html b/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html index 5cc19d8..a6bff9c 100644 --- a/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html +++ b/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html @@ -317,7 +317,7 @@
-
+

@@ -348,6 +348,6 @@ - + {% endblock %}