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.
This commit is contained in:
DaM
2026-02-15 17:01:00 +01:00
parent 4365366e7d
commit 547a909965
13 changed files with 2852 additions and 93 deletions

View File

@@ -2,6 +2,10 @@
console.log("[calibration_strategies] script loaded ✅", new Date().toISOString());
let STRATEGY_CATALOG = [];
let strategySlots = [];
const MAX_STRATEGIES = 10;
// =================================================
// WIZARD NAVIGATION
// =================================================
@@ -125,35 +129,51 @@ function buildPayload() {
}
function collectSelectedStrategies() {
const items = document.querySelectorAll("[data-strategy-item]");
const out = [];
items.forEach((node) => {
const checkbox = node.querySelector("input[type=checkbox]");
if (!checkbox || !checkbox.checked) return;
const strategies = [];
const sid = checkbox.getAttribute("data-strategy-id");
const textarea = node.querySelector("textarea");
let grid = {};
if (textarea && textarea.value.trim()) {
try {
grid = JSON.parse(textarea.value);
} catch (e) {
throw new Error(`Invalid JSON param_grid for ${sid}: ${e.message}`);
}
}
strategySlots.forEach((slot, index) => {
out.push({ strategy_id: sid, param_grid: grid });
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
});
});
if (out.length === 0) {
throw new Error("Select at least 1 strategy");
}
return out;
return strategies;
}
async function fetchAvailableStrategies() {
const res = await fetch("/api/v2/calibration/strategies/available");
const res = await fetch("/api/v2/calibration/strategies/catalog");
const data = await res.json();
return data.strategies || [];
}
@@ -180,6 +200,398 @@ function setVal(id, value) {
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
// =================================================
@@ -451,6 +863,12 @@ async function validateStrategies() {
if (text) txt.textContent = text;
};
if (!validateParameterInputs()) {
alert("Please fix parameter errors before running WF.");
return;
}
try {
// 0) Reset UI
setProgress(0, "Starting...");
@@ -578,10 +996,37 @@ function wireButtons() {
});
}
async function init() {
loadContextFromLocalStorage();
wireButtons();
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");
}
});
}
document.getElementById("lock_inherited")
.addEventListener("change", applyInheritedLock);
async function init() {
await loadStrategyCatalog();
addStrategySlot();
loadContextFromLocalStorage();
loadFromStep2();
applyInheritedLock();
document.getElementById("stop_type")
.addEventListener("change", updateStopUI);
wireButtons();
const strategies = await fetchAvailableStrategies();
renderStrategiesList(strategies);