feat(calibration): complete step 1 data inspection with data quality v1

This commit is contained in:
DaM
2026-02-08 22:29:09 +01:00
parent f85c522f22
commit 4d769af8bf
89 changed files with 5014 additions and 203 deletions

View File

@@ -0,0 +1,306 @@
let equityChart = null;
let showDrawdown = true;
let currentRange = "all";
// --------------------------------------------------
// STATUS
// --------------------------------------------------
async function updateStatus() {
const data = await apiGet("/bot/status");
const el = document.getElementById("bot-status");
el.textContent = data.state;
el.className =
"ms-auto badge " +
(data.state === "RUNNING"
? "bg-green"
: data.state === "ERROR"
? "bg-red"
: "bg-secondary");
}
// --------------------------------------------------
// KPI
// --------------------------------------------------
async function updateEquity() {
const data = await apiGet("/equity/state");
document.getElementById("kpi-equity").textContent =
data.equity?.toFixed(2) ?? "—";
document.getElementById("kpi-pnl").textContent =
data.realized_pnl?.toFixed(2) ?? "—";
}
async function fetchTrades() {
const data = await apiGet("/trades?limit=500");
return data.items || [];
}
// --------------------------------------------------
// MAIN CHART
// --------------------------------------------------
async function updateCurve() {
const data = await apiGet(`/equity/curve?range=${currentRange}`);
const labels = data.timestamps;
const equity = data.equity;
const cash = data.cash;
// -----------------------------
// Max Equity (for drawdown)
// -----------------------------
const maxEquityCurve = [];
let runningMax = -Infinity;
for (let i = 0; i < equity.length; i++) {
runningMax = Math.max(runningMax, equity[i]);
maxEquityCurve.push(runningMax);
}
// -----------------------------
// Max Drawdown KPI
// -----------------------------
let maxDD = 0;
for (let i = 0; i < equity.length; i++) {
const dd = (equity[i] / maxEquityCurve[i] - 1) * 100;
maxDD = Math.min(maxDD, dd);
}
const elDD = document.getElementById("kpi-max-dd");
if (elDD) elDD.textContent = `${maxDD.toFixed(2)} %`;
// -----------------------------
// Trades → markers
// -----------------------------
const trades = await fetchTrades();
const buyPoints = [];
const sellPoints = [];
const minEquity = Math.min(...equity);
const maxEquity = Math.max(...equity);
const offset = Math.max((maxEquity - minEquity) * 0.05, 350);
trades.forEach(t => {
if (!t.timestamp || !t.side) return;
const tradeTs = new Date(t.timestamp).getTime();
let idx = labels.length - 1;
for (let i = labels.length - 1; i >= 0; i--) {
if (new Date(labels[i]).getTime() <= tradeTs) {
idx = i;
break;
}
}
const y = equity[idx];
if (y == null) return;
if (t.side === "BUY") {
buyPoints.push({ x: labels[idx], y: y - offset, trade: t });
}
if (t.side === "SELL" || t.side === "CLOSE") {
sellPoints.push({ x: labels[idx], y: y + offset, trade: t });
}
});
// --------------------------------------------------
// INIT CHART
// --------------------------------------------------
if (!equityChart) {
const ctx = document.getElementById("equityChart").getContext("2d");
equityChart = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "Max Equity",
data: maxEquityCurve,
borderColor: "rgba(0,0,0,0)",
pointRadius: 0
},
{
label: "Equity",
data: equity,
borderColor: "#206bc4",
backgroundColor: "rgba(214,57,57,0.15)",
pointRadius: 0,
fill: {
target: 0,
above: "rgba(0,0,0,0)",
below: "rgba(214,57,57,0.15)"
}
},
{
label: "Cash",
data: cash,
borderColor: "#2fb344",
borderDash: [5, 5],
pointRadius: 0
},
{
type: "scatter",
label: "BUY",
data: buyPoints,
pointStyle: "triangle",
pointRotation: 0,
pointRadius: 6,
backgroundColor: "#2fb344"
},
{
type: "scatter",
label: "SELL",
data: sellPoints,
pointStyle: "triangle",
pointRotation: 180,
pointRadius: 6,
backgroundColor: "#d63939"
}
]
},
options: {
responsive: true,
animation: false,
// -----------------------------
// SOLUCIÓN 1: TIME SCALE
// -----------------------------
scales: {
x: {
type: "time",
time: {
tooltipFormat: "yyyy-MM-dd HH:mm",
displayFormats: {
minute: "HH:mm",
hour: "HH:mm",
day: "MMM dd"
}
}
}
},
// -----------------------------
// SOLUCIÓN 2: ZOOM + PAN
// -----------------------------
plugins: {
zoom: {
limits: {
x: { min: "original", max: "original" }
},
pan: {
enabled: true,
mode: "x",
modifierKey: "ctrl"
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x"
}
},
// -----------------------------
// SOLUCIÓN 3: DECIMATION
// -----------------------------
decimation: {
enabled: true,
algorithm: "lttb",
samples: 500
},
// -----------------------------
// LEYENDA
// -----------------------------
legend: {
display: true,
labels: {
usePointStyle: true,
generateLabels(chart) {
return chart.data.datasets
// ❌ fuera Max Equity
.map((ds, i) => ({ ds, i }))
.filter(({ ds }) => ds.label !== "Max Equity")
.map(({ ds, i }) => {
const isScatter = ds.type === "scatter";
return {
text: ds.label,
datasetIndex: i,
hidden: !chart.isDatasetVisible(i),
// 🎨 colores reales del dataset
fillStyle: isScatter ? ds.backgroundColor : ds.borderColor,
strokeStyle: ds.borderColor,
// 📏 línea vs punto
lineWidth: isScatter ? 0 : 3,
borderDash: ds.borderDash || [],
// 🔺 BUY / SELL = triángulos reales
pointStyle: isScatter ? ds.pointStyle : "line",
rotation: isScatter ? ds.pointRotation || 0 : 0,
};
});
}
}
}
}
}
});
} else {
equityChart.data.labels = labels;
equityChart.data.datasets[0].data = maxEquityCurve;
equityChart.data.datasets[1].data = equity;
equityChart.data.datasets[2].data = cash;
equityChart.data.datasets[3].data = buyPoints;
equityChart.data.datasets[4].data = sellPoints;
equityChart.update("none");
}
}
// --------------------------------------------------
// SOLUCIÓN 4: TIME WINDOWS
// --------------------------------------------------
function setTimeWindow(hours) {
if (!equityChart) return;
if (hours === "ALL") {
equityChart.options.scales.x.min = undefined;
equityChart.options.scales.x.max = undefined;
} else {
const now = Date.now();
equityChart.options.scales.x.min = now - hours * 3600_000;
equityChart.options.scales.x.max = now;
}
equityChart.update();
}
async function updateEvents() {
const data = await apiGet("/events?limit=20");
document.getElementById("events-log").textContent =
data.items.join("\n");
}
// --------------------------------------------------
// UI
// --------------------------------------------------
document.getElementById("reset-zoom")?.addEventListener("click", () => {
equityChart?.resetZoom();
});
document.getElementById("toggle-dd")?.addEventListener("change", e => {
showDrawdown = e.target.checked;
equityChart.data.datasets[1].fill = showDrawdown
? { target: 0, above: "rgba(0,0,0,0)", below: "rgba(214,57,57,0.15)" }
: false;
equityChart.update("none");
});
// --------------------------------------------------
poll(updateStatus, 2000);
poll(updateEquity, 5000);
poll(updateCurve, 10000);
poll(updateEvents, 10000);