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:
1040
src/web/ui/v2/static/js/pages/calibration_optimization.js
Normal file
1040
src/web/ui/v2/static/js/pages/calibration_optimization.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,10 @@ async function inspectCalibrationRisk() {
|
||||
const data = await res.json();
|
||||
console.log("[calibration_risk] inspect response:", data);
|
||||
|
||||
if (data.status === "ok" || data.status === "warning") {
|
||||
persistRiskParametersForStep3();
|
||||
}
|
||||
|
||||
renderRiskResult(payload, data);
|
||||
|
||||
// --------------------------------------------------
|
||||
@@ -189,6 +193,10 @@ async function validateCalibrationRisk() {
|
||||
const data = await res.json();
|
||||
console.log("[calibration_risk] inspect response:", data);
|
||||
|
||||
if (data.status === "ok" || data.status === "warning") {
|
||||
persistRiskParametersForStep3();
|
||||
}
|
||||
|
||||
renderRiskResult(payload, data);
|
||||
|
||||
// --------------------------------------------------
|
||||
@@ -698,6 +706,13 @@ function num(id) {
|
||||
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 buildRiskPayload() {
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
@@ -742,6 +757,34 @@ function buildRiskPayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
function persistRiskParametersForStep3() {
|
||||
|
||||
try {
|
||||
|
||||
const dataToPersist = {
|
||||
risk_fraction: num("risk_fraction"),
|
||||
max_position_fraction: num("max_position_fraction"),
|
||||
stop_type: str("stop_type"),
|
||||
stop_fraction: num("stop_fraction"),
|
||||
atr_period: num("atr_period"),
|
||||
atr_multiplier: num("atr_multiplier"),
|
||||
max_drawdown_pct: num("max_drawdown_pct"),
|
||||
daily_loss_limit_pct: num("daily_loss_limit_pct"),
|
||||
max_consecutive_losses: num("max_consecutive_losses"),
|
||||
cooldown_bars: num("cooldown_bars"),
|
||||
};
|
||||
|
||||
Object.entries(dataToPersist).forEach(([key, value]) => {
|
||||
localStorage.setItem(`calibration.${key}`, value ?? "");
|
||||
});
|
||||
|
||||
console.log("[calibration_risk] Parameters saved for Step 3 ✅");
|
||||
|
||||
} catch (err) {
|
||||
console.error("[calibration_risk] Persist failed ❌", err);
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// INIT
|
||||
// =================================================
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user