// static/app.js const fmt2 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(2)); const fmt6 = (x) => (x == null || isNaN(x) ? "—" : Number(x).toFixed(6)); const fmtPct= (x) => (x == null || isNaN(x) ? "—" : (Number(x) * 100).toFixed(2) + "%"); async function getJSON(url) { const r = await fetch(url, { cache: "no-store" }); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.json(); } // prosty wykres linii na canvas (bez bibliotek) function drawLineChart(canvas, points) { const ctx = canvas.getContext("2d"); const pad = 32; const w = canvas.width, h = canvas.height; ctx.clearRect(0, 0, w, h); if (!points || points.length === 0) { ctx.fillStyle = "#9aa3b2"; ctx.fillText("Brak danych", 10, 20); return; } const n = points.length; const ys = points.map(p => p.y); const minY = Math.min(...ys), maxY = Math.max(...ys); const yLo = minY === maxY ? minY - 1 : minY; const yHi = minY === maxY ? maxY + 1 : maxY; const x0 = pad, y0 = h - pad, x1 = w - pad, y1 = pad; const xScale = (i) => x0 + (i / (n - 1)) * (x1 - x0); const yScale = (y) => y0 - ((y - yLo) / (yHi - yLo)) * (y0 - y1); // osie ctx.strokeStyle = "#242a36"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x0, y0); ctx.lineTo(x1, y0); ctx.moveTo(x0, y0); ctx.lineTo(x0, y1); ctx.stroke(); // siatka ctx.strokeStyle = "#1b2130"; [0.25, 0.5, 0.75].forEach(f => { const yy = y0 - (y0 - y1) * f; ctx.beginPath(); ctx.moveTo(x0, yy); ctx.lineTo(x1, yy); ctx.stroke(); }); // podpisy min/max ctx.fillStyle = "#9aa3b2"; ctx.font = "12px system-ui, -apple-system, Segoe UI, Roboto, Inter, Arial"; ctx.fillText(fmt2(yHi), 6, y1 + 10); ctx.fillText(fmt2(yLo), 6, y0 - 2); // linia ctx.strokeStyle = "#4da3ff"; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(xScale(0), yScale(ys[0])); for (let i = 1; i < n; i++) ctx.lineTo(xScale(i), yScale(ys[i])); ctx.stroke(); // kropka na końcu const lastX = xScale(n - 1), lastY = yScale(ys[n - 1]); ctx.fillStyle = "#e7e9ee"; ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill(); ctx.fillText(fmt2(ys[n - 1]), lastX + 6, lastY - 6); } async function loadAll() { try { const [snap, hist, pos, trades] = await Promise.all([ getJSON("/api/snapshot"), getJSON("/api/history"), getJSON("/api/positions"), getJSON("/api/trades"), ]); // czas document.getElementById("last-update").textContent = "Ostatnia aktualizacja: " + (snap?.last_history?.time || "—"); // ===== ostatni wiersz historii ===== const last = Array.isArray(hist) && hist.length ? hist[hist.length - 1] : null; const cashVal = Number(last?.cash ?? 0); const totalVal = Number( last?.total_value ?? (Number(last?.cash ?? 0) + Number(last?.positions_net ?? 0)) // fallback dla starszych logów ); // KARTY document.getElementById("cash").textContent = fmt2(cashVal); document.getElementById("total-value").textContent = fmt2(totalVal); // mapa ostatnich cen do liczenia zysku (unrealized) const lastPrice = new Map(); (snap?.signals || []).forEach(s => { const px = Number(s.price); if (!isNaN(px)) lastPrice.set(s.ticker, px); }); // Zysk = suma niezrealizowanych PnL na otwartych pozycjach let unrealPnL = 0; (pos || []).forEach(p => { const price = Number(lastPrice.get(p.ticker)); const entry = Number(p.entry); const qty = Number(p.qty); const side = Number(p.side); if (isNaN(price) || isNaN(entry) || isNaN(qty) || isNaN(side)) return; unrealPnL += side === 1 ? (price - entry) * qty : (entry - price) * qty; }); const unrlEl = document.getElementById("unrealized"); unrlEl.textContent = fmt2(unrealPnL); unrlEl.classList.remove("pnl-positive", "pnl-negative"); if (unrealPnL > 0) unrlEl.classList.add("pnl-positive"); else if (unrealPnL < 0) unrlEl.classList.add("pnl-negative"); // liczba otwartych pozycji document.getElementById("open-pos").textContent = Number(last?.open_positions ?? (pos?.length ?? 0)); // ===== WYKRES WARTOŚCI KONTA (TOTAL) ===== const totalCanvas = document.getElementById("totalChart"); const totalPoints = (hist || []) .map((row, i) => { const v = (row.total_value != null) ? Number(row.total_value) : Number(row.cash ?? 0) + Number(row.positions_net ?? 0); return { x: i, y: v }; }) .filter(p => !isNaN(p.y)) .slice(-500); drawLineChart(totalCanvas, totalPoints); // ===== WYKRES GOTÓWKI (CASH) ===== const cashCanvas = document.getElementById("cashChart"); const cashPoints = (hist || []) .map((row, i) => ({ x: i, y: Number(row.cash ?? NaN) })) .filter(p => !isNaN(p.y)) .slice(-500); drawLineChart(cashCanvas, cashPoints); // ===== POZYCJE ===== const posBody = document.querySelector("#positions-table tbody"); posBody.innerHTML = ""; (pos || []).forEach((p) => { const price = Number(lastPrice.get(p.ticker)); const entry = Number(p.entry); const qty = Number(p.qty); const side = Number(p.side); let upnl = NaN, upct = NaN; if (!isNaN(price) && !isNaN(entry) && !isNaN(qty) && !isNaN(side)) { upnl = side === 1 ? (price - entry) * qty : (entry - price) * qty; const denom = Math.max(qty * entry, 1e-12); upct = upnl / denom; } const pnlClass = upnl > 0 ? "pnl-positive" : (upnl < 0 ? "pnl-negative" : ""); const tr = document.createElement("tr"); tr.innerHTML = `