Files
Trading-Bot/src/web/ui/v2/static/js/pages/calibration_risk.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

858 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. 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_risk.js
console.log(
"[calibration_risk] script loaded ✅",
new Date().toISOString()
);
// =================================================
// INSPECT RISK & STOPS
// =================================================
async function inspectCalibrationRisk() {
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
// --------------------------------------------------
// Load calibration context from Step 1
// --------------------------------------------------
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (!symbol || !timeframe) {
alert("Calibration context not found. Please complete Step 1 (Data) first.");
return;
}
// --------------------------------------------------
// Read form inputs
// --------------------------------------------------
const accountEquityEl = document.getElementById("account_equity");
const riskFractionEl = document.getElementById("risk_fraction");
const maxPositionEl = document.getElementById("max_position_fraction");
const stopTypeEl = document.getElementById("stop_type");
const stopFractionEl = document.getElementById("stop_fraction");
const atrPeriodEl = document.getElementById("atr_period");
const atrMultiplierEl = document.getElementById("atr_multiplier");
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
console.error("[calibration_risk] Missing required form elements");
alert("Internal error: risk form is incomplete.");
return;
}
// --------------------------------------------------
// Build stop config
// --------------------------------------------------
const stopType = stopTypeEl.value;
let stopConfig = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
if (!stopFractionEl) {
alert("Stop fraction input missing");
return;
}
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
}
if (stopType === "atr") {
if (!atrPeriodEl || !atrMultiplierEl) {
alert("ATR parameters missing");
return;
}
stopConfig.atr_period = parseInt(atrPeriodEl.value);
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
}
// --------------------------------------------------
// Build payload
// --------------------------------------------------
const payload = buildRiskPayload();
console.log("[calibration_risk] inspect payload:", payload);
// --------------------------------------------------
// Disable next step while inspecting
// --------------------------------------------------
disableNextStep();
// --------------------------------------------------
// Call API
// --------------------------------------------------
try {
const res = await fetch("/api/v2/calibration/risk/inspect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
console.log("[calibration_risk] inspect response:", data);
if (data.status === "ok" || data.status === "warning") {
persistRiskParametersForStep3();
}
renderRiskResult(payload, data);
// --------------------------------------------------
// Enable next step if OK or WARNING
// --------------------------------------------------
if (data.valid && data.status !== "fail") {
enableNextStep();
}
} catch (err) {
console.error("[calibration_risk] inspect FAILED", err);
alert("Error inspecting risk configuration");
disableNextStep();
}
}
// =================================================
// VALIDATE RISK & STOPS
// =================================================
async function validateCalibrationRisk() {
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
// --------------------------------------------------
// Load calibration context from Step 1
// --------------------------------------------------
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (!symbol || !timeframe) {
alert("Calibration context not found. Please complete Step 1 (Data) first.");
return;
}
// --------------------------------------------------
// Read form inputs
// --------------------------------------------------
const accountEquityEl = document.getElementById("account_equity");
const riskFractionEl = document.getElementById("risk_fraction");
const maxPositionEl = document.getElementById("max_position_fraction");
const stopTypeEl = document.getElementById("stop_type");
const stopFractionEl = document.getElementById("stop_fraction");
const atrPeriodEl = document.getElementById("atr_period");
const atrMultiplierEl = document.getElementById("atr_multiplier");
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
console.error("[calibration_risk] Missing required form elements");
alert("Internal error: risk form is incomplete.");
return;
}
// --------------------------------------------------
// Build stop config
// --------------------------------------------------
const stopType = stopTypeEl.value;
let stopConfig = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
if (!stopFractionEl) {
alert("Stop fraction input missing");
return;
}
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
}
if (stopType === "atr") {
if (!atrPeriodEl || !atrMultiplierEl) {
alert("ATR parameters missing");
return;
}
stopConfig.atr_period = parseInt(atrPeriodEl.value);
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
}
// --------------------------------------------------
// Build payload
// --------------------------------------------------
const payload = buildRiskPayload();
console.log("[calibration_risk] inspect payload:", payload);
// --------------------------------------------------
// Disable next step while inspecting
// --------------------------------------------------
disableNextStep();
// --------------------------------------------------
// Call API
// --------------------------------------------------
try {
const res = await fetch("/api/v2/calibration/risk/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
console.log("[calibration_risk] inspect response:", data);
if (data.status === "ok" || data.status === "warning") {
persistRiskParametersForStep3();
}
renderRiskResult(payload, data);
// --------------------------------------------------
// Enable next step if OK or WARNING
// --------------------------------------------------
if (data.valid && data.status !== "fail") {
enableNextStep();
}
} catch (err) {
console.error("[calibration_risk] inspect FAILED", err);
alert("Error inspecting risk configuration");
disableNextStep();
}
}
// =================================================
// PDF REPORT RISK & STOPS
// =================================================
async function generateRiskReport() {
console.log("[calibration_risk] generateRiskReport() START");
const payload = buildRiskPayload();
const res = await fetch("/api/v2/calibration/risk/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (data.url) {
// Mostrar visor
const viewer = document.getElementById("pdf_viewer_section");
const frame = document.getElementById("pdf_frame");
frame.src = data.url;
viewer.classList.remove("d-none");
// Scroll suave hacia el visor
viewer.scrollIntoView({ behavior: "smooth" });
} else {
alert("Failed to generate report");
}
}
// =================================================
// 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");
}
// =================================================
// RENDER RESULT
// =================================================
function renderRiskResult(payload, data) {
const container = document.getElementById("risk_result");
const badge = document.getElementById("risk_status_badge");
const message = document.getElementById("risk_message");
const debug = document.getElementById("risk_debug");
if (!container || !badge || !message || !debug) {
console.warn("[calibration_risk] Result elements missing");
return;
}
container.classList.remove("d-none");
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");
}
badge.textContent = data.status.toUpperCase();
message.textContent = data.message;
debug.textContent = JSON.stringify(data.checks, null, 2);
renderRiskChecks(data.checks);
if (data.checks?.stop_sanity?.metrics) {
renderStopQuantiles(data.checks.stop_sanity.metrics);
}
renderRiskFormulas(payload, data)
if (!data.series) {
console.warn("[calibration_risk] No series returned, skipping plots");
return;
}
if (data.series?.timestamps && data.series?.position_size_pct) {
renderPositionSizePlot({
timestamps: data.series.timestamps,
positionSizePct: data.series.position_size_pct,
maxPositionFraction: payload.risk.max_position_fraction
});
renderEffectiveRiskPlot({
timestamps: data.series.timestamps,
effectiveRiskPct: data.series.effective_risk_pct,
targetRiskFraction: payload.risk.risk_fraction
});
renderStopDistanceDistribution({
stopDistances: data.series.stop_distances,
quantiles: data.checks?.stop_sanity?.metrics
});
}
}
// =================================================
// RENDER RISK CHECKS
// =================================================
function renderRiskChecks(checks) {
const list = document.getElementById("risk_checks_list");
if (!list || !checks) return;
list.innerHTML = "";
Object.entries(checks).forEach(([key, check]) => {
const li = document.createElement("li");
li.classList.add("mb-1");
let icon = "❌";
let color = "text-danger";
if (check.status === "ok") {
icon = "✅";
color = "text-success";
} else if (check.status === "warning") {
icon = "⚠️";
color = "text-warning";
}
const title = key.replace(/_/g, " ");
li.innerHTML = `
<span class="${color}">
${icon} <strong>${title}</strong>
</span>
${check.message ? `<span class="text-muted"> — ${check.message}</span>` : ""}
`;
list.appendChild(li);
});
}
function renderStopQuantiles(metrics) {
const el = document.getElementById("stop_quantiles");
if (!el || !metrics) return;
el.innerHTML = `
<li>P50 (typical): ${(metrics.p50 * 100).toFixed(2)}%</li>
<li>P90 (wide): ${(metrics.p90 * 100).toFixed(2)}%</li>
<li>P99 (extreme): ${(metrics.p99 * 100).toFixed(2)}%</li>
`;
}
// =================================================
// RENDER RISK FORMULAS
// =================================================
function renderRiskFormulas(payload, result) {
const el = document.getElementById("risk_formulas");
if (!el) return;
const equity = payload.account_equity;
const riskFraction = payload.risk.risk_fraction;
const maxPositionFraction = payload.risk.max_position_fraction;
const stopMetrics = result.checks?.stop_sanity?.metrics;
if (!stopMetrics || stopMetrics.p50 == null) {
el.textContent = "Unable to compute risk formulas (missing stop metrics)";
return;
}
const stopP50 = stopMetrics.p50;
const idealPosition = (equity * riskFraction) / stopP50;
const maxPosition = equity * maxPositionFraction;
const effectivePosition = Math.min(idealPosition, maxPosition);
const effectiveRisk = (effectivePosition * stopP50) / equity;
el.innerHTML = `
<li>Position size (ideal)
= ${equity.toLocaleString()} × ${(riskFraction * 100).toFixed(2)}% ÷ ${(stopP50 * 100).toFixed(2)}%
${idealPosition.toFixed(0)}</li>
<li>Max position size
= ${equity.toLocaleString()} × ${(maxPositionFraction * 100).toFixed(2)}%
= ${maxPosition.toFixed(0)}</li>
<li>Effective position
= min(${idealPosition.toFixed(0)}, ${maxPosition.toFixed(0)})
= ${effectivePosition.toFixed(0)}</li>
<li>Effective risk
= ${effectivePosition.toFixed(0)} × ${(stopP50 * 100).toFixed(2)}% ÷ ${equity.toLocaleString()}
${(effectiveRisk * 100).toFixed(2)}%</li>
`;
}
// =================================================
// STOP TYPE UI LOGIC
// =================================================
function updateStopParamsUI() {
const stopType = document.getElementById("stop_type")?.value;
if (!stopType) return;
// Hide all stop param blocks
document.querySelectorAll(".stop-param").forEach(el => {
el.classList.add("d-none");
});
// Show relevant blocks
if (stopType === "fixed" || stopType === "trailing") {
document.querySelectorAll(".stop-fixed, .stop-trailing").forEach(el => {
el.classList.remove("d-none");
});
}
if (stopType === "atr") {
document.querySelectorAll(".stop-atr").forEach(el => {
el.classList.remove("d-none");
});
}
}
// =================================================
// RENDER PLOTS
// =================================================
function renderPositionSizePlot({
timestamps,
positionSizePct,
maxPositionFraction
}) {
const el = document.getElementById("position_size_plot");
if (!el) {
console.warn("[calibration_risk] position_size_plot not found");
return;
}
if (!timestamps || !positionSizePct || timestamps.length === 0) {
el.innerHTML = "<em>No position size data available</em>";
return;
}
const trace = {
x: timestamps,
y: positionSizePct.map(v => v * 100),
type: "scatter",
mode: "lines",
name: "Position size",
line: {
color: "#59a14f",
width: 2
}
};
const layout = {
yaxis: {
title: "Position size (% equity)",
rangemode: "tozero"
},
xaxis: {
title: "Time"
},
shapes: [
{
type: "line",
xref: "paper",
x0: 0,
x1: 1,
y0: maxPositionFraction * 100,
y1: maxPositionFraction * 100,
line: {
dash: "dot",
color: "#e15759",
width: 2
}
}
],
annotations: [
{
xref: "paper",
x: 1,
y: maxPositionFraction * 100,
xanchor: "left",
text: "Max position cap",
showarrow: false,
font: {
size: 11,
color: "#e15759"
}
}
],
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
function renderEffectiveRiskPlot({
timestamps,
effectiveRiskPct,
targetRiskFraction
}) {
const el = document.getElementById("effective_risk_plot");
if (!el) {
console.warn("[calibration_risk] effective_risk_plot not found");
return;
}
if (!timestamps || !effectiveRiskPct || timestamps.length === 0) {
el.innerHTML = "<em>No effective risk data available</em>";
return;
}
const trace = {
x: timestamps,
y: effectiveRiskPct.map(v => v * 100),
type: "scatter",
mode: "lines",
name: "Effective risk",
line: {
color: "#f28e2b",
width: 2
}
};
const layout = {
yaxis: {
title: "Effective risk (% equity)",
rangemode: "tozero"
},
xaxis: {
title: "Time"
},
shapes: [
{
type: "line",
xref: "paper",
x0: 0,
x1: 1,
y0: targetRiskFraction * 100,
y1: targetRiskFraction * 100,
line: {
dash: "dot",
color: "#4c78a8",
width: 2
}
}
],
annotations: [
{
xref: "paper",
x: 1,
y: targetRiskFraction * 100,
xanchor: "left",
text: "Target risk",
showarrow: false,
font: {
size: 11,
color: "#4c78a8"
}
}
],
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
function renderStopDistanceDistribution({
stopDistances,
quantiles
}) {
const el = document.getElementById("stop_distance_plot");
if (!el) {
console.warn("[calibration_risk] stop_distance_plot not found");
return;
}
if (!stopDistances || stopDistances.length === 0) {
el.innerHTML = "<em>No stop distance data available</em>";
return;
}
const valuesPct = stopDistances
.filter(v => v != null && v > 0)
.map(v => v * 100);
const trace = {
x: valuesPct,
type: "histogram",
nbinsx: 40,
name: "Stop distance",
marker: {
color: "#bab0ac"
}
};
const shapes = [];
const annotations = [];
if (quantiles) {
const qMap = [
{ key: "p50", label: "P50 (typical)", color: "#4c78a8" },
{ key: "p90", label: "P90 (wide)", color: "#f28e2b" },
{ key: "p99", label: "P99 (extreme)", color: "#e15759" }
];
qMap.forEach(q => {
const val = quantiles[q.key];
if (val != null) {
shapes.push({
type: "line",
xref: "x",
yref: "paper",
x0: val * 100,
x1: val * 100,
y0: 0,
y1: 1,
line: {
dash: "dot",
width: 2,
color: q.color
}
});
annotations.push({
x: val * 100,
y: 1,
yref: "paper",
xanchor: "left",
text: q.label,
showarrow: false,
font: {
size: 11,
color: q.color
}
});
}
});
}
const layout = {
xaxis: {
title: "Stop distance (% price)"
},
yaxis: {
title: "Frequency"
},
shapes: shapes,
annotations: annotations,
margin: { t: 20 }
};
Plotly.newPlot(el, [trace], layout, {
responsive: true,
displayModeBar: true
});
}
// =================================================
// UTILS
// =================================================
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 buildRiskPayload() {
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
const stopType = document.getElementById("stop_type")?.value;
const stop = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
stop.stop_fraction = num("stop_fraction");
}
if (stopType === "atr") {
stop.atr_period = num("atr_period");
stop.atr_multiplier = num("atr_multiplier");
}
const payload = {
symbol,
timeframe,
account_equity: num("account_equity"),
risk: {
risk_fraction: num("risk_fraction") / 100,
max_position_fraction: num("max_position_fraction") / 100,
},
stop,
global_rules: {
max_drawdown_pct: num("max_drawdown_pct") / 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"),
},
};
console.log("[calibration_risk] FINAL PAYLOAD:", payload);
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
// =================================================
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("inspect_risk_btn")
?.addEventListener("click", inspectCalibrationRisk);
const stopTypeSelect = document.getElementById("stop_type");
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (symbol && timeframe) {
const symbolEl = document.getElementById("ctx_symbol");
const timeframeEl = document.getElementById("ctx_timeframe");
if (symbolEl) symbolEl.textContent = symbol;
if (timeframeEl) timeframeEl.textContent = timeframe;
}
// Initial state
updateStopParamsUI();
});
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("validate_risk_btn")
?.addEventListener("click", validateCalibrationRisk);
const stopTypeSelect = document.getElementById("stop_type");
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (symbol && timeframe) {
const symbolEl = document.getElementById("ctx_symbol");
const timeframeEl = document.getElementById("ctx_timeframe");
if (symbolEl) symbolEl.textContent = symbol;
if (timeframeEl) timeframeEl.textContent = timeframe;
}
// Initial state
updateStopParamsUI();
});
document.addEventListener("DOMContentLoaded", () => {
console.log("[calibration_risk] DOMContentLoaded ✅");
document
.getElementById("generate_report_btn")
?.addEventListener("click", generateRiskReport);
document
.getElementById("close_pdf_btn")
?.addEventListener("click", () => {
document.getElementById("pdf_viewer_section").classList.add("d-none");
document.getElementById("pdf_frame").src = "";
});
});