307 lines
8.5 KiB
JavaScript
307 lines
8.5 KiB
JavaScript
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);
|