// src/web/ui/v2/static/js/pages/calibration_strategies.js console.log("[calibration_strategies] script loaded ✅", new Date().toISOString()); let STRATEGY_CATALOG = []; let strategySlots = []; let selectedStrategyId = null; let lastValidationResult = null; const MAX_STRATEGIES = 10; // ================================================= // WIZARD NAVIGATION // ================================================= function enableNextStep() { const btn = document.getElementById("next-step-btn"); if (!btn) return; btn.classList.remove("btn-outline-secondary"); btn.classList.add("btn-outline-primary"); btn.setAttribute("aria-disabled", "false"); } function disableNextStep() { const btn = document.getElementById("next-step-btn"); if (!btn) return; btn.classList.remove("btn-outline-primary"); btn.classList.add("btn-outline-secondary"); btn.setAttribute("aria-disabled", "true"); } // ================================================= // UTILS // ================================================= function loadContextFromLocalStorage() { const symbol = localStorage.getItem("calibration.symbol"); const timeframe = localStorage.getItem("calibration.timeframe"); if (symbol) setVal("symbol", symbol); if (timeframe) setVal("timeframe", timeframe); // Step 2 snapshot (if stored) const stop_type = localStorage.getItem("calibration.stop.type"); const stop_fraction = localStorage.getItem("calibration.stop.stop_fraction"); const atr_period = localStorage.getItem("calibration.stop.atr_period"); const atr_multiplier = localStorage.getItem("calibration.stop.atr_multiplier"); const risk_fraction = localStorage.getItem("calibration.risk.risk_fraction"); const max_position_fraction = localStorage.getItem("calibration.risk.max_position_fraction"); const max_drawdown_pct = localStorage.getItem("calibration.rules.max_drawdown_pct"); const daily_loss_limit_pct = localStorage.getItem("calibration.rules.daily_loss_limit_pct"); const max_consecutive_losses = localStorage.getItem("calibration.rules.max_consecutive_losses"); const cooldown_bars = localStorage.getItem("calibration.rules.cooldown_bars"); const account_equity = localStorage.getItem("calibration.account_equity"); if (stop_type) setVal("stop_type", stop_type); if (stop_fraction) setVal("stop_fraction", stop_fraction); if (atr_period) setVal("atr_period", atr_period); if (atr_multiplier) setVal("atr_multiplier", atr_multiplier); if (risk_fraction) setVal("risk_fraction", risk_fraction); if (max_position_fraction) setVal("max_position_fraction", max_position_fraction); if (max_drawdown_pct) setVal("max_drawdown_pct", max_drawdown_pct); if (daily_loss_limit_pct) setVal("daily_loss_limit_pct", daily_loss_limit_pct); if (max_consecutive_losses) setVal("max_consecutive_losses", max_consecutive_losses); if (cooldown_bars) setVal("cooldown_bars", cooldown_bars); if (account_equity) setVal("account_equity", account_equity); // WF defaults (if stored) const wf_train_days = localStorage.getItem("calibration.wf.train_days"); const wf_test_days = localStorage.getItem("calibration.wf.test_days"); const wf_step_days = localStorage.getItem("calibration.wf.step_days"); const wf_min_trades_test = localStorage.getItem("calibration.wf.min_trades_test"); if (wf_train_days) setVal("wf_train_days", wf_train_days); if (wf_test_days) setVal("wf_test_days", wf_test_days); if (wf_step_days) setVal("wf_step_days", wf_step_days); if (wf_min_trades_test) setVal("wf_min_trades_test", wf_min_trades_test); // Optional fees const commission = localStorage.getItem("calibration.commission"); const slippage = localStorage.getItem("calibration.slippage"); if (commission) setVal("commission", commission); if (slippage) setVal("slippage", slippage); } function setVal(id, v) { const el = document.getElementById(id); if (el) el.value = v; } function str(id) { const el = document.getElementById(id); return el ? String(el.value || "").trim() : ""; } function num(id) { const el = document.getElementById(id); if (!el) return null; const v = parseFloat(el.value); return Number.isFinite(v) ? v : null; } function sleep(ms) { return new Promise(res => setTimeout(res, ms)); } // ================================================= // FETCH CATALOG // ================================================= async function fetchCatalog() { const symbol = str("symbol"); const timeframe = str("timeframe"); const res = await fetch(`/api/v2/calibration/strategies/catalog?symbol=${encodeURIComponent(symbol)}&timeframe=${encodeURIComponent(timeframe)}`); if (!res.ok) throw new Error(`catalog failed: ${res.status}`); const data = await res.json(); STRATEGY_CATALOG = data.strategies || []; console.log("[catalog] strategies:", STRATEGY_CATALOG.map(s => s.strategy_id)); } // ================================================= // UI RENDERING // ================================================= function initSlots() { strategySlots = [ { strategy_id: null, parameters: {} } ]; } function rerenderStrategySlots() { const container = document.getElementById("strategies_container"); container.innerHTML = ""; const currentStrategies = strategySlots .filter(s => s.strategy_id !== null); strategySlots = []; currentStrategies.forEach((slotData, index) => { strategySlots.push({ strategy_id: slotData.strategy_id, parameters: slotData.parameters || {} }); renderStrategySlot(index); }); // always one empty slot at end (if room) if (strategySlots.length < MAX_STRATEGIES) { strategySlots.push({ strategy_id: null, parameters: {} }); renderStrategySlot(strategySlots.length - 1); } updateCombinationCounter(); } function renderStrategySlot(index) { const container = document.getElementById("strategies_container"); const slot = document.createElement("div"); slot.className = "card p-3"; slot.id = `strategy_slot_${index}`; slot.innerHTML = `
Combinations: 0
`; container.appendChild(slot); const select = document.getElementById(`strategy_select_${index}`); const removeBtn = document.getElementById(`remove_strategy_${index}`); // set initial value select.value = strategySlots[index].strategy_id || ""; select.addEventListener("change", (e) => { onStrategySelected(index, e.target.value); }); removeBtn.addEventListener("click", () => { removeStrategySlot(index); }); // if already selected, render params if (strategySlots[index].strategy_id) { renderParametersOnly(index, strategySlots[index].strategy_id); } } function onStrategySelected(index, strategyId) { if (!strategyId) { removeStrategySlot(index); return; } strategySlots[index].strategy_id = strategyId; renderParametersOnly(index, strategyId); // Si es el último slot activo, añadir nuevo vacío if (index === strategySlots.length - 1 && strategySlots.length < MAX_STRATEGIES) { strategySlots.push({ strategy_id: null, parameters: {} }); renderStrategySlot(strategySlots.length - 1); } updateCombinationCounter(); } function renderParametersOnly(index, strategyId) { const paramsContainer = document.getElementById(`strategy_params_${index}`); paramsContainer.innerHTML = ""; if (!strategyId) return; const strategyMeta = STRATEGY_CATALOG.find( s => s.strategy_id === strategyId ); if (!strategyMeta || !strategyMeta.parameters_meta) return; strategyMeta.parameters_meta.forEach(meta => { const col = document.createElement("div"); col.className = "col-md-4"; let inputHtml = ""; const paramName = meta.name; // INT / FLOAT if (meta.type === "int" || meta.type === "float") { inputHtml = ` `; } // ENUM else if (meta.type === "enum") { const options = (meta.choices || []).map(choice => ` `).join(""); inputHtml = ` `; } // BOOL else if (meta.type === "bool") { inputHtml = ` `; } col.innerHTML = ` ${inputHtml} `; paramsContainer.appendChild(col); }); validateParameterInputs(); } function validateParameterInputs() { let valid = true; // Limpiar estados previos document.querySelectorAll(".param-input").forEach(input => { input.classList.remove("is-invalid"); }); strategySlots.forEach((slot, index) => { if (!slot.strategy_id) return; const strategyMeta = STRATEGY_CATALOG.find( s => s.strategy_id === slot.strategy_id ); if (!strategyMeta || !strategyMeta.parameters_meta) return; strategyMeta.parameters_meta.forEach(meta => { const el = document.getElementById(`${meta.name}_value_${index}`); if (!el) return; let raw = el.value; let value = raw; // 🔹 INT if (meta.type === "int") { value = parseInt(raw); if (isNaN(value)) valid = false; } // 🔹 FLOAT else if (meta.type === "float") { value = parseFloat(raw); if (isNaN(value)) valid = false; } // 🔹 BOOL else if (meta.type === "bool") { if (raw !== "true" && raw !== "false") valid = false; } // 🔹 ENUM else if (meta.type === "enum") { if (!meta.choices || !meta.choices.includes(raw)) { valid = false; } } // 🔹 Rango (solo números) if ((meta.type === "int" || meta.type === "float") && !isNaN(value)) { if (meta.min !== null && meta.min !== undefined && value < meta.min) { valid = false; } if (meta.max !== null && meta.max !== undefined && value > meta.max) { valid = false; } } if (!valid) { el.classList.add("is-invalid"); } }); }); updateCombinationCounter(); return valid; } function updateCombinationCounter() { let hasAnyStrategy = false; strategySlots.forEach((slot, index) => { if (!slot.strategy_id) return; hasAnyStrategy = true; const perStrategyEl = document.getElementById(`strategy_combo_${index}`); if (perStrategyEl) { perStrategyEl.textContent = "1"; } }); const globalTotal = hasAnyStrategy ? 1 : 0; const globalEl = document.getElementById("combination_counter"); if (globalEl) globalEl.textContent = globalTotal; applyCombinationWarnings(globalTotal); updateTimeEstimate(globalTotal); return globalTotal; } function applyCombinationWarnings(total) { const maxComb = parseInt( document.getElementById("opt_max_combinations")?.value || 0 ); const counter = document.getElementById("combination_counter"); if (!counter) return; counter.classList.remove("text-warning", "text-danger"); if (total > 10000) { counter.classList.add("text-danger"); } else if (maxComb && total > maxComb) { counter.classList.add("text-warning"); } } function updateTimeEstimate(totalComb) { const trainDays = parseInt( document.getElementById("wf_train_days")?.value || 0 ); const testDays = parseInt( document.getElementById("wf_test_days")?.value || 0 ); const approxWindows = Math.max( Math.floor(365 / testDays), 1 ); const operations = totalComb * approxWindows; // 0.003s por combinación (estimación conservadora) const seconds = operations * 0.003; let label; if (seconds < 60) { label = `~ ${seconds.toFixed(1)} sec`; } else if (seconds < 3600) { label = `~ ${(seconds / 60).toFixed(1)} min`; } else { label = `~ ${(seconds / 3600).toFixed(1)} h`; } const el = document.getElementById("wf_time_estimate"); if (el) el.textContent = label; } function removeStrategySlot(index) { strategySlots.splice(index, 1); rerenderStrategySlots(); } // ================================================= // PAYLOAD // ================================================= function buildPayload() { const symbol = str("symbol"); const timeframe = str("timeframe"); const stopType = str("stop_type"); if (!symbol || !timeframe) { throw new Error("symbol/timeframe missing"); } const stop = { type: stopType }; if (stopType === "fixed" || stopType === "trailing") { stop.stop_fraction = (num("stop_fraction") ?? 1.0) / 100; } if (stopType === "atr") { stop.atr_period = num("atr_period") ?? 14; stop.atr_multiplier = num("atr_multiplier") ?? 3.0; } const risk_fraction = (num("risk_fraction") ?? 1.0) / 100; const max_position_fraction = (num("max_position_fraction") ?? 95) / 100; const global_rules = { max_drawdown_pct: (num("max_drawdown_pct") ?? 20) / 100, daily_loss_limit_pct: num("daily_loss_limit_pct") ? num("daily_loss_limit_pct") / 100 : null, max_consecutive_losses: num("max_consecutive_losses"), cooldown_bars: num("cooldown_bars"), }; const wf_train_days = num("wf_train_days") ?? 120; const wf_test_days = num("wf_test_days") ?? 30; const wf_step_days = num("wf_step_days"); const wf_min_trades_test = num("wf_min_trades_test") const strategies = collectSelectedStrategies(); return { symbol, timeframe, account_equity: num("account_equity") ?? 10000, stop, risk: { risk_fraction, max_position_fraction, }, global_rules, strategies, wf: { train_days: wf_train_days, test_days: wf_test_days, step_days: wf_step_days, min_trades_test: wf_min_trades_test, }, commission: num("commission") ?? 0.001, slippage: num("slippage") ?? 0.0005, }; } function collectSelectedStrategies() { const strategies = []; console.log("strategySlots:", strategySlots); strategySlots.forEach((slot, index) => { if (!slot.strategy_id) return; const strategyMeta = STRATEGY_CATALOG.find( s => s.strategy_id === slot.strategy_id ); if (!strategyMeta) return; const parameters = {}; strategyMeta.parameters_meta.forEach(meta => { const el = document.getElementById(`${meta.name}_value_${index}`); if (!el) return; let value = el.value; if (meta.type === "int") { value = parseInt(value); } else if (meta.type === "float") { value = parseFloat(value); } else if (meta.type === "bool") { value = value === "true"; } parameters[meta.name] = value; }); strategies.push({ strategy_id: slot.strategy_id, parameters: parameters }); }); console.log("Collected strategies:", strategies); return strategies; } async function fetchAvailableStrategies() { const res = await fetch("/api/v2/calibration/strategies/catalog"); const data = await res.json(); return data.strategies || []; } function num(id) { const el = document.getElementById(id); if (!el) return null; const val = el.value; if (val === "" || val === null || val === undefined) return null; const n = Number(val); return Number.isFinite(n) ? n : null; } function str(id) { const el = document.getElementById(id); if (!el) return null; const v = el.value; return v === null || v === undefined ? null : String(v); } function setVal(id, value) { const el = document.getElementById(id); if (!el) return; el.value = value ?? ""; } function loadFromStep2() { document.getElementById("risk_fraction").value = localStorage.getItem("calibration.risk_fraction") ?? ""; document.getElementById("max_position_fraction").value = localStorage.getItem("calibration.max_position_fraction") ?? ""; document.getElementById("stop_type").value = localStorage.getItem("calibration.stop_type") ?? "fixed"; document.getElementById("stop_fraction").value = localStorage.getItem("calibration.stop_fraction") ?? ""; document.getElementById("atr_period").value = localStorage.getItem("calibration.atr_period") ?? ""; document.getElementById("atr_multiplier").value = localStorage.getItem("calibration.atr_multiplier") ?? ""; document.getElementById("max_drawdown_pct").value = localStorage.getItem("calibration.max_drawdown_pct") ?? ""; document.getElementById("daily_loss_limit_pct").value = localStorage.getItem("calibration.daily_loss_limit_pct") ?? ""; document.getElementById("max_consecutive_losses").value = localStorage.getItem("calibration.max_consecutive_losses") ?? ""; document.getElementById("cooldown_bars").value = localStorage.getItem("calibration.cooldown_bars") ?? ""; // Forzar actualización de UI según stop heredado setTimeout(() => { updateStopUI(); }, 0); console.log("[calibration_strategies] Parameters loaded from Step 2 ✅"); } function updateStopUI() { const type = document.getElementById("stop_type").value; const stopFraction = document.getElementById("stop_fraction_group"); const atrPeriod = document.getElementById("atr_group"); const atrMultiplier = document.getElementById("atr_multiplier_group"); if (type === "fixed" || type === "trailing") { stopFraction.classList.remove("d-none"); atrPeriod.classList.add("d-none"); atrMultiplier.classList.add("d-none"); } if (type === "atr") { stopFraction.classList.add("d-none"); atrPeriod.classList.remove("d-none"); atrMultiplier.classList.remove("d-none"); } } async function loadStrategyCatalog() { const res = await fetch("/api/v2/calibration/strategies/catalog"); const data = await res.json(); STRATEGY_CATALOG = data.strategies; } function addStrategySlot() { // Si ya hay un slot vacío al final, no crear otro if (strategySlots.length > 0 && strategySlots[strategySlots.length - 1].strategy_id === null) { return; } if (strategySlots.length >= MAX_STRATEGIES) return; const index = strategySlots.length; strategySlots.push({ strategy_id: null, parameters: {} }); renderStrategySlot(index); } // ================================================= // PROGRESS BAR // ================================================= function startWF() { document .getElementById("wf_progress_card") .classList.remove("d-none"); document.getElementById("wfProgressBar").style.width = "0%"; document.getElementById("wfProgressBar").innerText = "0%"; } async function pollStatus(jobId) { const interval = setInterval(async () => { const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`); const data = await res.json(); const bar = document.getElementById("wfProgressBar"); bar.style.width = data.progress + "%"; bar.innerText = data.progress + "%"; if (data.status === "done") { clearInterval(interval); bar.classList.remove("progress-bar-animated"); console.log("WF finished"); } }, 1000); } // ================================================= // DIFFERENT RENDER PLOTS // ================================================= function renderEquityAndReturns(strategyId, s, data) { const equity = s.window_equity || []; const ret = s.window_returns_pct || []; const trd = s.window_trades || []; const regimeWindows = s.window_regimes || s.diagnostics?.regimes?.by_window || []; function regimeBgColor(regime) { switch (regime) { case "bull_strong": return "rgba(0,114,178,0.20)"; case "bull_moderate": return "rgba(0,114,178,0.10)"; case "bear_strong": return "rgba(230,159,0,0.20)"; case "bear_moderate": return "rgba(230,159,0,0.10)"; default: return "rgba(150,150,150,0.12)"; } } function buildRegimeBackgroundShapes(regimeWindows, topYRef = "paper") { if (!Array.isArray(regimeWindows) || regimeWindows.length === 0) return []; const shapes = []; regimeWindows.forEach((w, idx) => { const x0 = idx - 0.5; const x1 = idx + 0.5; shapes.push({ type: "rect", xref: "x", yref: topYRef, x0, x1, y0: 0.60, // mismo inicio que domain del gráfico superior y1: 1.00, // mismo final que domain del gráfico superior fillcolor: regimeBgColor(w.regime_detail || w.regime), line: { width: 0 }, layer: "below" }); }); return shapes; } // X común (windows) const n = Math.max(equity.length, ret.length, trd.length); const x = Array.from({ length: n }, (_, i) => i); // ---- 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" }; const regimeShapesTop = buildRegimeBackgroundShapes(regimeWindows); // ---- 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: [ ...regimeShapesTop, { 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)); } 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); 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) { const edges = s.diagnostics?.distribution?.hist_bin_edges || []; const counts = s.diagnostics?.distribution?.hist_counts || []; const centers = edges.map((_, i) => (edges[i] + edges[i + 1]) / 2); Plotly.newPlot("plot_strategy", [{ x: centers, y: counts, type: "bar", name: "OOS Return% (bins)" }], { margin: { t: 40 }, title: { text: `OOS Returns Distribution — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico xaxis: { title: "Return % (window)" }, yaxis: { title: "Count" } }); } function renderDrawdownEvolution(strategyId, s, data) { const dd = s.diagnostics?.drawdown?.drawdown_pct || []; const x = dd.map((_, i) => i); Plotly.newPlot("plot_strategy", [{ x, y: dd, type: "scatter", mode: "lines", name: "Drawdown %" }], { margin: { t: 40 }, title: { text: `Drawdown Evolution — ${strategyId}`, x: 0.5 }, // ✅ centrado + dinámico xaxis: { title: "Point (initial + windows)" }, yaxis: { title: "Drawdown %", zeroline: true } }); } function renderTradeDensity(strategyId, s, data) { const tpw = s.diagnostics?.trades?.trades_per_window || []; const tpd = s.diagnostics?.trades?.trades_per_day || []; const x = tpw.map((_, i) => i + 1); const tpwMax = Math.max(0, ...tpw); const tpdMax = Math.max(0, ...tpd); Plotly.newPlot("plot_strategy", [ { x, y: tpw, type: "bar", name: "Trades / window", yaxis: "y" }, { x, y: tpd, type: "scatter", mode: "lines+markers", name: "Trades / day", yaxis: "y2" } ], { margin: { t: 40 }, title: { text: `Trade Density — ${strategyId}`, x: 0.5 }, barmode: "group", xaxis: { title: "Window" }, yaxis: { title: "Trades / window", range: [0, tpwMax], zeroline: true, zerolinewidth: 2 }, yaxis2: { title: "Trades / day", overlaying: "y", side: "right", range: [0, tpdMax], zeroline: true, zerolinewidth: 2 } }); } // ================================================= // RENDER RESULTS // ================================================= function renderStrategiesList(strategies) { const list = document.getElementById("strategies_list"); if (!list) return; list.innerHTML = ""; strategies.forEach((s) => { const col = document.createElement("div"); col.className = "col-12 col-lg-6"; col.setAttribute("data-strategy-item", "1"); const defaultGrid = s.default_grid || {}; const defaultGridText = JSON.stringify(defaultGrid, null, 2); col.innerHTML = `
Tip: usa listas. Ej: {"fast":[10,20],"slow":[50,100]}
`; list.appendChild(col); }); } function setBadge(status) { const badge = document.getElementById("strategies_status_badge"); if (!badge) return; badge.classList.remove("bg-secondary", "bg-success", "bg-warning", "bg-danger"); badge.classList.add( status === "ok" ? "bg-success" : status === "warning" ? "bg-warning" : status === "fail" ? "bg-danger" : "bg-secondary" ); badge.textContent = status ? status.toUpperCase() : "—"; } function renderResultsTable(data) { const wrap = document.getElementById("strategies_table_wrap"); if (!wrap) return; const rows = []; (data.results || []).forEach((r) => { rows.push(` ${r.strategy_id} ${r.status} ${r.n_windows} ${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 || ""} ${Array.isArray(r.warnings) && r.warnings.length ? `
${r.warnings.map(escapeHtml).join(" · ")}
` : ""} `); }); wrap.innerHTML = `
${rows.join("")}
Strategy Status Windows OOS return Worst DD Final equity Message
`; } function populatePlotSelector(data) { const sel = document.getElementById("plot_strategy_select"); if (!sel) return; sel.innerHTML = ""; // ✅ 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; opt.textContent = sid; sel.appendChild(opt); }); sel.onchange = () => { const sid = sel.value; selectStrategy(sid, data); }; if (ids.length > 0) { sel.value = selectedStrategyId || ids[0]; } } function selectStrategy(strategyId, data) { if (!strategyId || !data) return; // Actualiza selectedStrategyId selectedStrategyId = strategyId; const row = (data.results || []).find(r => r.strategy_id === strategyId); // 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(); } // 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) Verificar si los datos de la estrategia están disponibles const strategyData = data?.series?.strategies?.[selectedStrategyId]; if (!strategyData) { showPlotAlert( "warning", `No data available — ${strategyId}`, "Strategy data not available for rendering.", [] ); clearPlots(); highlightSelectedRow(strategyId); return; } // 4) Mantén el gráfico previamente seleccionado en el dropdown const chartType = document.getElementById("plot_strategy_select").value; renderChart(chartType, selectedStrategyId, strategyData, data); // Renderiza el gráfico correctamente renderRegimeSummary(selectedStrategyId, strategyData, data); highlightSelectedRow(strategyId); // scroll automático al gráfico document.getElementById("plot_strategy")?.scrollIntoView({ behavior: "smooth", block: "start" }); } function renderValidateResponse(data) { lastValidationResult = data; // ------------------------------- // 1️⃣ Badge + message // ------------------------------- const badge = document.getElementById("strategies_status_badge"); const msg = document.getElementById("strategies_message"); badge.textContent = data.status ?? "—"; badge.className = "badge"; if (data.status === "ok") badge.classList.add("bg-success"); else if (data.status === "warning") badge.classList.add("bg-warning"); else badge.classList.add("bg-danger"); msg.textContent = data.message ?? ""; // ------------------------------- // 2️⃣ Debug JSON // ------------------------------- document.getElementById("strategies_debug").textContent = JSON.stringify(data, null, 2); // ------------------------------- // 3️⃣ Plots (primera estrategia por ahora) // ------------------------------- if (data.results && data.results.length > 0) { if (!selectedStrategyId) { selectedStrategyId = data.results[0].strategy_id; } selectStrategy(selectedStrategyId, data); } // ------------------------------- // 4️⃣ Table // ------------------------------- const wrap = document.getElementById("strategies_table_wrap"); wrap.innerHTML = ""; if (data.results) { let html = ``; for (const r of data.results) { let bestRegime = "-"; const strat = data?.series?.strategies?.[r.strategy_id]; const perf = strat?.diagnostics?.regimes?.performance; if (perf) { const entries = ["bull", "sideways", "bear"] .map(k => ({ regime: k, val: perf?.[k]?.mean_return_pct ?? -Infinity })); entries.sort((a,b)=>b.val-a.val); if (entries[0].val > -Infinity) { bestRegime = entries[0].regime; } } html += ` `; // 🔸 Mostrar warnings debajo de la fila si existen if (r.warnings && r.warnings.length > 0) { html += ` `; } } html += "
Strategy Status OOS Return % OOS Max DD % Windows Best Regime
${r.strategy_id} ${r.status} ${r.oos_total_return_pct?.toFixed(2)} ${r.oos_max_dd_worst_pct?.toFixed(2)} ${r.n_windows} ${bestRegime}
    ${r.warnings.map(w => `
  • ${w}
  • `).join("")}
"; wrap.innerHTML = html; document.querySelectorAll(".strategy-row").forEach(el => { el.addEventListener("click", function () { console.log("Clicked:", selectedStrategyId); selectedStrategyId = this.dataset.strategy; selectStrategy(selectedStrategyId, lastValidationResult); }); }); } } function renderChart(chartType, strategyId, s, data) { switch (chartType) { case "equity": renderEquityAndReturns(strategyId, s, data); break; case "rolling_sharpe": renderRollingSharpe(strategyId, s, data); break; case "hist_oos_returns": renderOOSReturnsDistribution(strategyId, s, data); break; case "drawdown": renderDrawdownEvolution(strategyId, s, data); break; case "trade_density": renderTradeDensity(strategyId, s, data); break; default: renderEquityAndReturns(strategyId, s, data); break; } } function highlightSelectedRow(strategyId) { document.querySelectorAll(".strategy-row").forEach(el => { el.style.backgroundColor = ""; }); const active = document.querySelector( `.strategy-row[data-strategy="${strategyId}"]` ); if (active) { active.style.backgroundColor = "#e6f0ff"; } } async function validateStrategies() { console.log("[calibration_strategies] validateStrategies() START"); const bar = document.getElementById("wfProgressBar"); const txt = document.getElementById("wf_progress_text"); const setProgress = (pct, text) => { const p = Math.max(0, Math.min(100, Number(pct || 0))); bar.style.width = `${p}%`; bar.textContent = `${p}%`; if (text) txt.textContent = text; }; if (!validateParameterInputs()) { alert("Please fix parameter errors before running WF."); return; } try { // 0) Reset UI setProgress(0, "Starting..."); // 1) Construye payload igual que antes (usa tu función existente) const payload = buildPayload(); // <-- NO CAMBIES tu builder, reutilízalo // 2) Arranca job async const runResp = await fetch("/api/v2/calibration/strategies/run", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!runResp.ok) { const errText = await runResp.text(); throw new Error(`Run failed: ${runResp.status} ${errText}`); } const { job_id } = await runResp.json(); if (!job_id) throw new Error("No job_id returned from /run"); // 3) Poll status const pollEveryMs = 500; const maxMinutes = 30; const maxPolls = Math.ceil((maxMinutes * 60 * 1000) / pollEveryMs); for (let i = 0; i < maxPolls; i++) { await new Promise((r) => setTimeout(r, pollEveryMs)); const stResp = await fetch(`/api/v2/calibration/strategies/status/${job_id}`); if (!stResp.ok) continue; const st = await stResp.json(); const pct = st.progress ?? 0; const cw = st.current_window ?? 0; const tw = st.total_windows ?? 0; const label = tw > 0 ? `WF running... window ${cw}/${tw}` : "WF running..."; setProgress(pct, label); if (st.status === "done") { setProgress(100, "WF completed ✅"); // 4) Renderiza resultados usando el MISMO renderer que usabas con /validate // (ojo: el resultado viene dentro de st.result) if (!st.result) throw new Error("Job done but no result in status payload"); renderValidateResponse(st.result); // <-- usa tu función existente de render (plots, tablas, etc.) console.log("[calibration_strategies] validateStrategies() DONE ok"); return; } if (st.status === "unknown") { setProgress(0, "Unknown job (server lost state?)"); break; } } throw new Error("Timeout waiting for WF job to finish"); } catch (err) { console.error(err); // deja un estado visible const txt = document.getElementById("wf_progress_text"); if (txt) txt.textContent = `Error: ${err.message}`; console.log("[calibration_strategies] validateStrategies() DONE fail"); } } async function generateReport() { console.log("[calibration_strategies] generateReport() START"); let payload; try { payload = buildPayload(); } catch (e) { alert(e.message); return; } const res = await fetch("/api/v2/calibration/strategies/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const data = await res.json(); if (data.url) { const viewer = document.getElementById("pdf_viewer_section"); const frame = document.getElementById("pdf_frame"); frame.src = data.url; viewer.classList.remove("d-none"); viewer.scrollIntoView({ behavior: "smooth" }); } else { alert("Failed to generate report"); } } function wireButtons() { document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies); document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport); document.getElementById("refresh_strategies_btn")?.addEventListener("click", async () => { const strategies = await fetchAvailableStrategies(); renderStrategiesList(strategies); }); document.getElementById("load_step2_btn")?.addEventListener("click", () => { loadContextFromLocalStorage(); }); document.getElementById("close_pdf_btn")?.addEventListener("click", () => { const viewer = document.getElementById("pdf_viewer_section"); const frame = document.getElementById("pdf_frame"); frame.src = ""; viewer.classList.add("d-none"); }); } function applyInheritedLock() { const locked = document.getElementById("lock_inherited").checked; const fields = document.querySelectorAll(".inherited-field"); fields.forEach(f => { f.disabled = locked; if (locked) { f.classList.add("bg-light"); } else { f.classList.remove("bg-light"); } }); } async function init() { await loadStrategyCatalog(); addStrategySlot(); loadContextFromLocalStorage(); loadFromStep2(); applyInheritedLock(); document.getElementById("stop_type") .addEventListener("change", updateStopUI); wireButtons(); document.getElementById("plot_strategy_select").addEventListener("change", function() { const chartType = this.value; const strategyData = lastValidationResult.series.strategies[selectedStrategyId]; // Verifica que selectedStrategyId tenga el valor correcto console.log("selectedStrategyId:", selectedStrategyId); console.log("Strategy Data:", strategyData); renderChart(chartType, selectedStrategyId, strategyData, lastValidationResult); }); const strategies = await fetchAvailableStrategies(); renderStrategiesList(strategies); // Pre-select 1 strategy by default (moving_average) if exists setTimeout(() => { const first = document.querySelector('input[type=checkbox][data-strategy-id="moving_average"]'); if (first) first.checked = true; }, 0); } // ================================================= // MARKET REGIME // ================================================= function fmtPct(v, digits = 2) { const n = Number(v ?? 0); return `${n.toFixed(digits)}%`; } function fmtNum(v, digits = 2) { const n = Number(v ?? 0); return n.toFixed(digits); } function regimeBadgeClass(regime) { switch (regime) { case "bull": case "bull_moderate": case "bull_strong": return "bg-blue-lt text-blue"; case "bear": case "bear_moderate": case "bear_strong": return "bg-orange-lt text-orange"; default: return "bg-secondary-lt text-secondary"; } } function regimeLabel(regime) { switch (regime) { case "bull_strong": return "Bull strong"; case "bull_moderate": return "Bull moderate"; case "bear_strong": return "Bear strong"; case "bear_moderate": return "Bear moderate"; case "bull": return "Bull"; case "bear": return "Bear"; default: return "Sideways"; } } function ensureRegimeContainer() { let el = document.getElementById("regime_analysis_wrap"); if (el) return el; const anchor = document.getElementById("plot_strategy"); if (!anchor || !anchor.parentElement) return null; el = document.createElement("div"); el.id = "regime_analysis_wrap"; el.className = "mt-4"; anchor.parentElement.appendChild(el); return el; } function clearRegimeSummary() { const el = document.getElementById("regime_analysis_wrap"); if (el) el.innerHTML = ""; } function getBestRegime(perf) { const candidates = ["bull", "sideways", "bear"] .map((k) => ({ regime: k, n_windows: Number(perf?.[k]?.n_windows ?? 0), mean_return_pct: Number(perf?.[k]?.mean_return_pct ?? -Infinity), })) .filter((x) => x.n_windows > 0); if (!candidates.length) return null; candidates.sort((a, b) => b.mean_return_pct - a.mean_return_pct); return candidates[0].regime; } function renderRegimeSummary(strategyId, s, data) { const wrap = ensureRegimeContainer(); if (!wrap) return; const regimesDiag = s?.diagnostics?.regimes || {}; const perf = regimesDiag.performance?.group || {}; const perfDetail = regimesDiag.performance?.detail || {}; const byWindow = s?.window_regimes || regimesDiag.by_window || []; const cfg = regimesDiag.config || data?.regimes?.config || {}; const bestRegime = getBestRegime(perf); const detailOrderTop = ["bull_moderate", "bear_moderate"]; const detailOrderBottom = ["bull_strong", "sideways", "bear_strong"]; function renderDetailCard(regime, extraClass = "") { const p = perfDetail?.[regime] || {}; return `

${regimeLabel(regime)}

${regimeLabel(regime).toUpperCase()}
Windows
${Number(p.n_windows ?? 0)}
Mean return
${fmtPct(p.mean_return_pct)}
Positive rate
${fmtPct((Number(p.positive_window_rate ?? 0) * 100.0))}
Avg trades
${fmtNum(p.avg_trades)}
`; } const topCards = detailOrderTop .map((regime) => renderDetailCard(regime, "col-md-6")) .join(""); const bottomCards = detailOrderBottom .map((regime) => renderDetailCard(regime, "col-md-4")) .join(""); const rows = byWindow.map((w) => ` ${Number(w.window ?? 0)} ${regimeLabel(w.regime_detail || w.regime)} ${fmtPct(Number(w.bull_strong_pct ?? 0) * 100.0)} ${fmtPct(Number(w.bull_moderate_pct ?? 0) * 100.0)} ${fmtPct(Number(w.sideways_detail_pct ?? 0) * 100.0)} ${fmtPct(Number(w.bear_moderate_pct ?? 0) * 100.0)} ${fmtPct(Number(w.bear_strong_pct ?? 0) * 100.0)} `).join(""); wrap.innerHTML = `

Regime Analysis — ${strategyId}

EMA: ${(cfg.ema_periods || []).join(" / ")} · Bull persistence: ${cfg.bull_persistence_bars ?? "-"} · Sideways persistence: ${cfg.sideways_persistence_bars ?? "-"} · Bear persistence: ${cfg.bear_persistence_bars ?? "-"} · Bull ≥ ${cfg.bull_threshold ?? "-"} · Bear ≤ ${cfg.bear_threshold ?? "-"}
${ bestRegime ? ` Best regime: ${regimeLabel(bestRegime)} ` : `Best regime: —` }
${topCards}
${bottomCards}
${rows || ``}
Window Majority regime Bull strong % Bull mod % Sideways % Bear mod % Bear strong %
No regime data available.
`; } // ================================================= // 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_strategy"); 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_strategy"); if (eq) eq.innerHTML = ""; clearRegimeSummary(); } document.getElementById("lock_inherited") .addEventListener("change", applyInheritedLock); init();