Files
Trading-Bot/src/web/ui/v2/static/js/pages/calibration_optimization.js
DaM 547a909965 Step 3 y 4 medio preparados. Ha habido una decision es separar en dos step distintos la eleccion de estrategias y luego su optimizacion. A partir de aqui vamos a hacer una refactorizacion quirurgica de los Steps 3 y 4.
Prompt para Char GPT:
Estamos trabajando en un Trading Bot con arquitectura backend/frontend separada.

Stack:
- Backend: FastAPI (Python 3.12)
- Frontend: HTML + Vanilla JS + Tabler UI
- DB: PostgreSQL
- Cache opcional: Redis
- Proyecto estructurado bajo /src
- Carpeta /reports fuera de src

Wizard actual:

Step 1 · Data
Step 2 · Risk & Stops
Step 3 · Strategies (actualmente mezcla validación y optimización)
Step 4 · Optimization (renombrado pero no 100% ajustado aún)

Decisión arquitectónica ya tomada:
- Step 3 será Strategy Validation (parámetros fijos, sin grid)
- Step 4 será Parameter Optimization (grid min/max/step)

Importante:
- Ya he duplicado los archivos para separar Step 3 y Step 4.
- No queremos rehacer desde cero.
- Queremos hacer una refactorización quirúrgica.
- Queremos eliminar lógica de grid del Step 3.
- Queremos mantener infraestructura WF, async jobs, ranking y reporting.

Objetivo de esta sesión:
Refactorizar Step 3 (Validation) de forma limpia y profesional partiendo del código actual.

Reglas:
- No romper Step 4.
- No reescribir todo desde cero.
- Simplificar quirúrgicamente.
- Mantener coherencia de arquitectura.
- Mantener compatibilidad con Step 2 (risk snapshot heredado).
- Mantener generación de PDF.
- Mantener botón Promote to Optimization.

Te adjunto el zip completo de la carpeta src.

Analiza la estructura primero.
No escribas código todavía.
Primero dame:
1. Un diagnóstico estructural.
2. Qué archivos tocar.
3. Qué eliminar.
4. Qué simplificar.
5. Qué mantener.
6. Orden de refactorización seguro.

Después empezaremos la refactorización paso a paso.
Despues empezaremos la refactorizacion paso a paso.
2026-02-15 17:01:00 +01:00

1041 lines
29 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 = `
<div class="mb-3">
<label class="form-label">Strategy ${index + 1}</label>
<select class="form-select" id="strategy_select_${index}">
<option value="">None</option>
${STRATEGY_CATALOG.map(s =>
`<option value="${s.strategy_id}">${s.name}</option>`
).join("")}
</select>
</div>
<div id="strategy_params_${index}" class="row g-3"></div>
<div class="mt-2 text-end">
<small class="text-muted">
Combinations:
<span id="strategy_combo_${index}">0</span>
</small>
</div>
`;
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 = `
<label class="form-label fw-semibold">${paramName}</label>
<div class="row g-2">
<div class="col">
<small class="text-muted">Min</small>
<input type="number"
class="form-control param-input"
id="${paramName}_min_${index}">
</div>
<div class="col">
<small class="text-muted">Max</small>
<input type="number"
class="form-control param-input"
id="${paramName}_max_${index}">
</div>
<div class="col">
<small class="text-muted">Step</small>
<input type="number"
class="form-control param-input"
id="${paramName}_step_${index}">
</div>
</div>
`;
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 = `
<div class="card">
<div class="card-body">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" data-strategy-id="${s.strategy_id}">
<span class="form-check-label">
<b>${s.name}</b>
<span class="text-secondary ms-2">${(s.tags || []).join(" · ")}</span>
</span>
</label>
<div class="mt-3">
<label class="form-label">param_grid (JSON)</label>
<textarea class="form-control" rows="7" spellcheck="false">${defaultGridText}</textarea>
<div class="form-hint">Tip: usa listas. Ej: {"fast":[10,20],"slow":[50,100]}</div>
</div>
</div>
</div>
`;
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(`
<tr>
<td><b>${r.strategy_id}</b></td>
<td>${r.status}</td>
<td>${r.n_windows}</td>
<td>${Number(r.oos_total_return_pct).toFixed(2)}%</td>
<td>${Number(r.oos_max_dd_worst_pct).toFixed(2)}%</td>
<td>${Number(r.oos_final_equity).toFixed(2)}</td>
<td class="text-secondary">${r.message || ""}</td>
</tr>
`);
});
wrap.innerHTML = `
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr>
<th>Strategy</th>
<th>Status</th>
<th>Windows</th>
<th>OOS return</th>
<th>Worst DD</th>
<th>Final equity</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${rows.join("")}
</tbody>
</table>
</div>
`;
}
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 = `<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Strategy</th>
<th>Status</th>
<th>OOS Return %</th>
<th>OOS Max DD %</th>
<th>Windows</th>
</tr>
</thead>
<tbody>`;
for (const r of data.results) {
html += `
<tr>
<td>${r.strategy_id}</td>
<td>${r.status}</td>
<td>${r.oos_total_return_pct?.toFixed(2)}</td>
<td>${r.oos_max_dd_worst_pct?.toFixed(2)}</td>
<td>${r.n_windows}</td>
</tr>
`;
}
html += "</tbody></table>";
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();