// src/web/ui/v2/static/js/pages/calibration_strategies.js console.log("[calibration_strategies] script loaded ✅", new Date().toISOString()); let STRATEGY_CATALOG = []; let strategySlots = []; 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 (account_equity) setVal("account_equity", 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); } 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 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, }, optimization: { optimizer_metric: str("opt_metric") ?? "sharpe_ratio", max_combinations: num("opt_max_combinations") ?? 300, min_trades_train: num("opt_min_trades_train") ?? 30, min_trades_test: num("opt_min_trades_test") ?? 10, }, commission: num("commission") ?? 0.001, slippage: num("slippage") ?? 0.0005, }; } function collectSelectedStrategies() { const strategies = []; strategySlots.forEach((slot, index) => { if (!slot.strategy_id) return; const strategyMeta = STRATEGY_CATALOG.find( s => s.strategy_id === slot.strategy_id ); const parameters = {}; strategyMeta.params.forEach(paramName => { const min = parseFloat( document.getElementById(`${paramName}_min_${index}`)?.value ); const max = parseFloat( document.getElementById(`${paramName}_max_${index}`)?.value ); const step = parseFloat( document.getElementById(`${paramName}_step_${index}`)?.value ); parameters[paramName] = { min: min, max: max, step: step }; }); strategies.push({ strategy_id: slot.strategy_id, parameters: parameters }); }); 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); } 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); document .getElementById(`strategy_select_${index}`) .addEventListener("change", (e) => { onStrategySelected(index, e.target.value); }); } 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 validateParameterInputs() { let valid = true; 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 ); strategyMeta.params.forEach(paramName => { const minEl = document.getElementById(`${paramName}_min_${index}`); const maxEl = document.getElementById(`${paramName}_max_${index}`); const stepEl = document.getElementById(`${paramName}_step_${index}`); const min = parseFloat(minEl?.value); const max = parseFloat(maxEl?.value); const step = parseFloat(stepEl?.value); if (max < min) { maxEl.classList.add("is-invalid"); valid = false; } if (step <= 0) { stepEl.classList.add("is-invalid"); valid = false; } }); }); updateCombinationCounter(); return valid; } function updateCombinationCounter() { let globalTotal = 1; let hasAnyStrategy = false; strategySlots.forEach((slot, index) => { if (!slot.strategy_id) return; hasAnyStrategy = true; const strategyMeta = STRATEGY_CATALOG.find( s => s.strategy_id === slot.strategy_id ); let strategyTotal = 1; strategyMeta.params.forEach(paramName => { const min = parseFloat( document.getElementById(`${paramName}_min_${index}`)?.value ); const max = parseFloat( document.getElementById(`${paramName}_max_${index}`)?.value ); const step = parseFloat( document.getElementById(`${paramName}_step_${index}`)?.value ); if (isNaN(min) || isNaN(max) || isNaN(step)) return; if (min === max || step == 0) { strategyTotal *= 1; } else { const count = Math.floor((max - min) / step) + 1; strategyTotal *= Math.max(count, 1); } }); const perStrategyEl = document.getElementById(`strategy_combo_${index}`); if (perStrategyEl) { perStrategyEl.textContent = strategyTotal; } globalTotal *= strategyTotal; }); if (!hasAnyStrategy) globalTotal = 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(); } 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: {} }); renderStrategySlot(index); const select = document.getElementById(`strategy_select_${index}`); select.value = slotData.strategy_id; renderParametersOnly(index, slotData.strategy_id); }); // Siempre añadir un slot vacío al final if (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) return; strategyMeta.params.forEach(paramName => { const col = document.createElement("div"); col.className = "col-md-4"; col.innerHTML = `
Min
Max
Step
`; paramsContainer.appendChild(col); }); } // ================================================= // 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); } // ================================================= // 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 || ""} `); }); 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 = ""; const ids = Object.keys((data.series && data.series.strategies) ? data.series.strategies : {}); ids.forEach((sid) => { const opt = document.createElement("option"); opt.value = sid; opt.textContent = sid; sel.appendChild(opt); }); sel.onchange = () => renderPlotsForSelected(data); if (ids.length > 0) { sel.value = ids[0]; } } function renderPlotsForSelected(data) { const sel = document.getElementById("plot_strategy_select"); const sid = sel ? sel.value : null; if (!sid) return; const s = data.series?.strategies?.[sid]; if (!s) return; 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); 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 }); 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 }); } function renderValidateResponse(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.series && data.series.strategies) { const keys = Object.keys(data.series.strategies); if (keys.length > 0) { const s = data.series.strategies[keys[0]]; Plotly.newPlot("plot_equity", [{ y: s.window_equity, type: "scatter", mode: "lines", name: "Equity" }], { margin: { t: 20 } }); Plotly.newPlot("plot_returns", [{ y: s.window_returns_pct, type: "bar", name: "Window returns %" }], { margin: { t: 20 } }); } } // ------------------------------- // 4️⃣ Table // ------------------------------- const wrap = document.getElementById("strategies_table_wrap"); wrap.innerHTML = ""; if (data.results) { let html = ``; for (const r of data.results) { html += ` `; } html += "
Strategy Status OOS Return % OOS Max DD % Windows
${r.strategy_id} ${r.status} ${r.oos_total_return_pct?.toFixed(2)} ${r.oos_max_dd_worst_pct?.toFixed(2)} ${r.n_windows}
"; wrap.innerHTML = html; } } 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(); 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); } document.getElementById("lock_inherited") .addEventListener("change", applyInheritedLock); init();