810 lines
22 KiB
JavaScript
810 lines
22 KiB
JavaScript
// 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 = `
|
||
<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 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);
|
||
});
|
||
|
||
|