Ahora el Step 3 esta parcialmente acabado, y vamos a pasar a realizar el Step 3.5 donde añadimos el regimen de mercado
This commit is contained in:
0
src/core/market_regime.py
Normal file
0
src/core/market_regime.py
Normal file
@@ -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);
|
||||
|
||||
// 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
|
||||
});
|
||||
const minTrades = data.config?.wf?.min_trades_test ?? 10;
|
||||
|
||||
// Datos de Equity
|
||||
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 = {
|
||||
y: s.window_equity, // Datos de la equity
|
||||
type: "scatter", // Tipo de gráfico: línea
|
||||
mode: "lines", // Modo: línea
|
||||
name: "Equity"
|
||||
x,
|
||||
y: equity,
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
name: "Equity",
|
||||
xaxis: "x",
|
||||
yaxis: "y"
|
||||
};
|
||||
|
||||
// Datos de Return %
|
||||
const returnsTrace = {
|
||||
y: s.window_returns_pct || [], // Datos de returns
|
||||
type: "bar", // Tipo de gráfico: barra
|
||||
x,
|
||||
y: ret,
|
||||
type: "bar",
|
||||
name: "Return %",
|
||||
marker: { color: "#3b82f6" }, // Color de la barra
|
||||
yaxis: "y1", // Asociar al primer eje Y
|
||||
marker: { color: "#3b82f6" },
|
||||
xaxis: "x2",
|
||||
yaxis: "y2",
|
||||
offsetgroup: "returns",
|
||||
alignmentgroup: "bottom"
|
||||
};
|
||||
|
||||
// Datos de Trades
|
||||
const tradesTrace = {
|
||||
y: s.window_trades || [], // Datos de trades
|
||||
type: "bar", // Tipo de gráfico: barra
|
||||
x,
|
||||
y: trd,
|
||||
type: "bar",
|
||||
name: "Trades",
|
||||
marker: { color: "#f59e0b" }, // Color de la barra
|
||||
yaxis: "y2", // Asociar al segundo eje Y
|
||||
marker: { color: "#f59e0b" },
|
||||
xaxis: "x2",
|
||||
yaxis: "y3",
|
||||
offsetgroup: "trades",
|
||||
alignmentgroup: "bottom"
|
||||
};
|
||||
|
||||
// Agregar la traza de "Equity" al subplot (fila 1, columna 1)
|
||||
fig.addTrace(equityTrace, 1, 1);
|
||||
// ---- Layout ----
|
||||
const layout = {
|
||||
grid: { rows: 2, columns: 1, pattern: "independent" },
|
||||
|
||||
// Agregar las trazas de "Returns" y "Trades" al subplot (fila 2, columna 1)
|
||||
fig.addTrace(returnsTrace, 2, 1);
|
||||
fig.addTrace(tradesTrace, 2, 1);
|
||||
// IMPORTANTE: share X de verdad (pan/zoom sincronizado)
|
||||
xaxis: { matches: "x2", showgrid: true },
|
||||
xaxis2: { matches: "x", showgrid: true },
|
||||
|
||||
// Configurar los ejes y los márgenes
|
||||
fig.update_layout({
|
||||
title: `Strategy Overview — ${strategyId}`,
|
||||
// Dominios (más altura para ver mejor)
|
||||
// Ajusta estos números a gusto:
|
||||
yaxis: {
|
||||
domain: [0.60, 1.00],
|
||||
title: "Equity",
|
||||
showgrid: true
|
||||
},
|
||||
yaxis2: {
|
||||
title: "Return % / Trades",
|
||||
overlaying: "y",
|
||||
side: "right",
|
||||
showgrid: true
|
||||
showgrid: true,
|
||||
rangemode: "tozero"
|
||||
},
|
||||
xaxis: {
|
||||
title: "Windows",
|
||||
domain: [0.00, 1.00],
|
||||
anchor: "y"
|
||||
},
|
||||
|
||||
yaxis2: {
|
||||
domain: [0.00, 0.25],
|
||||
title: "Return %",
|
||||
range: y1Range,
|
||||
zeroline: true,
|
||||
zerolinewidth: 2,
|
||||
showgrid: true
|
||||
},
|
||||
showlegend: 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
|
||||
});
|
||||
|
||||
// Renderizar en el contenedor del gráfico
|
||||
Plotly.newPlot("plot_strategy", fig);
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderRollingSharpe(strategyId, s, data) {
|
||||
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
|
||||
const x = roll.map((_, i) => i + 1);
|
||||
|
||||
Plotly.newPlot("plot_strategy", [{
|
||||
const k = s.diagnostics?.rolling?.rolling_window ?? "?";
|
||||
|
||||
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" }
|
||||
});
|
||||
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" },
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0-beta19/dist/js/tabler.min.js"></script>
|
||||
|
||||
<!-- UI v2 core JS -->
|
||||
<script src="/static/js/app.js"></script>
|
||||
<!-- <script src="/static/js/app.js"></script>
|
||||
<script src="/static/js/api.js"></script>
|
||||
<script src="/static/js/router.js"></script>
|
||||
<script src="/static/js/router.js"></script> -->
|
||||
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div id="plot_strategy" style="height: 320px;"></div>
|
||||
<div id="plot_strategy" style="height: 700px; width: 100%;"></div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
@@ -348,6 +348,6 @@
|
||||
|
||||
</div>
|
||||
|
||||
<script src="/static/js/plotly.js"></script>
|
||||
<script src="https://cdn.plot.ly/plotly-3.3.1.min.js"></script>
|
||||
<script src="/static/js/pages/calibration_strategies.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user