// 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); 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); 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(); // reutiliza la misma función que validate const res = await fetch("/api/v2/calibration/risk/report", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (!res.ok) { console.error("Failed to generate report"); return; } const blob = await res.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "risk_report.pdf"; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } // ================================================= // 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 = ` ${icon} ${title} ${check.message ? ` — ${check.message}` : ""} `; list.appendChild(li); }); } function renderStopQuantiles(metrics) { const el = document.getElementById("stop_quantiles"); if (!el || !metrics) return; el.innerHTML = `
  • P50 (typical): ${(metrics.p50 * 100).toFixed(2)}%
  • P90 (wide): ${(metrics.p90 * 100).toFixed(2)}%
  • P99 (extreme): ${(metrics.p99 * 100).toFixed(2)}%
  • `; } // ================================================= // 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 = `
  • Position size (ideal) = ${equity.toLocaleString()} × ${(riskFraction * 100).toFixed(2)}% ÷ ${(stopP50 * 100).toFixed(2)}% ≈ ${idealPosition.toFixed(0)}
  • Max position size = ${equity.toLocaleString()} × ${(maxPositionFraction * 100).toFixed(2)}% = ${maxPosition.toFixed(0)}
  • Effective position = min(${idealPosition.toFixed(0)}, ${maxPosition.toFixed(0)}) = ${effectivePosition.toFixed(0)}
  • Effective risk = ${effectivePosition.toFixed(0)} × ${(stopP50 * 100).toFixed(2)}% ÷ ${equity.toLocaleString()} ≈ ${(effectiveRisk * 100).toFixed(2)}%
  • `; } // ================================================= // 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 = "No position size data available"; 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 = "No effective risk data available"; 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 = "No stop distance data available"; 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 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; } // ================================================= // 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); });