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.
1041 lines
29 KiB
JavaScript
1041 lines
29 KiB
JavaScript
// 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();
|