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);