Merge Trade Chart and Price History into unified Price Chart panel
Browse files- Remove standalone Trade Chart panel from the grid
- Rename Price History panel to Price Chart (full-width, below grid)
- Add "Live" as default period option: shows last 100 trades as a line chart
that auto-refreshes on every SSE trade event
- Historical periods (1H/8H/1D/1W/1M) now also use a consistent line chart
style (close price line + gradient fill + volume bars) instead of candlesticks
- Unified drawChart() function renders both live trades and OHLCV candles with
the same visual: green line, gradient fill, blue volume bars, time axis
- Remove renderPriceChart() and dead #chart-symbol dropdown references
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dashboard/templates/index.html +113 -203
dashboard/templates/index.html
CHANGED
|
@@ -290,15 +290,6 @@
|
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
-
<div class="panel">
|
| 294 |
-
<h2>Trade Chart
|
| 295 |
-
<select id="chart-symbol" onchange="renderPriceChart()" style="margin-left:10px; padding:2px 5px;">
|
| 296 |
-
<option value="">All Symbols</option>
|
| 297 |
-
</select>
|
| 298 |
-
</h2>
|
| 299 |
-
<canvas id="price-chart" style="width:100%; flex-grow:1;"></canvas>
|
| 300 |
-
</div>
|
| 301 |
-
|
| 302 |
<div class="panel">
|
| 303 |
<h2>Order Book
|
| 304 |
<select id="bbo-symbol-select" onchange="renderOrderBookPanel()" style="margin-left:10px; padding:2px 5px;">
|
|
@@ -337,16 +328,17 @@
|
|
| 337 |
|
| 338 |
</div>
|
| 339 |
|
| 340 |
-
<!-- Price
|
| 341 |
<div class="panel-full">
|
| 342 |
-
<h2>Price
|
| 343 |
<select id="history-symbol" onchange="loadHistory()" style="margin-left:10px; padding:2px 5px;">
|
| 344 |
<option value="">Select symbol...</option>
|
| 345 |
</select>
|
| 346 |
<select id="history-period" onchange="loadHistory()" style="margin-left:5px; padding:2px 5px;">
|
|
|
|
| 347 |
<option value="1h">1 Hour</option>
|
| 348 |
<option value="8h">8 Hours</option>
|
| 349 |
-
<option value="1d"
|
| 350 |
<option value="1w">1 Week</option>
|
| 351 |
<option value="1m">1 Month</option>
|
| 352 |
</select>
|
|
@@ -674,20 +666,6 @@
|
|
| 674 |
renderOrderBookPanel();
|
| 675 |
}
|
| 676 |
|
| 677 |
-
// Update chart symbol select
|
| 678 |
-
const chartSel = document.getElementById("chart-symbol");
|
| 679 |
-
const chartCurrent = chartSel.value;
|
| 680 |
-
const existingOpts = Array.from(chartSel.options).map(o => o.value);
|
| 681 |
-
symbols.forEach(sym => {
|
| 682 |
-
if (!existingOpts.includes(sym)) {
|
| 683 |
-
const opt = document.createElement("option");
|
| 684 |
-
opt.value = sym;
|
| 685 |
-
opt.textContent = sym;
|
| 686 |
-
chartSel.appendChild(opt);
|
| 687 |
-
}
|
| 688 |
-
});
|
| 689 |
-
if (chartCurrent) chartSel.value = chartCurrent;
|
| 690 |
-
|
| 691 |
// Update history symbol select
|
| 692 |
const histSel = document.getElementById("history-symbol");
|
| 693 |
const histCurrent = histSel.value;
|
|
@@ -768,103 +746,113 @@
|
|
| 768 |
// Alias for backward compatibility
|
| 769 |
function renderFullBBO() { renderOrderBookPanel(); }
|
| 770 |
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
const
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
ctx.fillStyle = "#
|
| 783 |
-
ctx.
|
| 784 |
-
ctx.textAlign = "center";
|
| 785 |
-
ctx.fillText("No trade data available", canvas.width / 2, canvas.height / 2);
|
| 786 |
return;
|
| 787 |
}
|
| 788 |
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
const
|
| 794 |
-
const
|
| 795 |
-
const
|
|
|
|
| 796 |
|
| 797 |
-
|
| 798 |
-
const
|
| 799 |
-
const
|
| 800 |
-
const
|
| 801 |
-
const
|
| 802 |
-
const maxVolume = Math.max(...volumes);
|
| 803 |
|
| 804 |
-
|
| 805 |
|
| 806 |
-
//
|
| 807 |
-
ctx.strokeStyle = "#eee";
|
| 808 |
-
ctx.lineWidth = 1;
|
| 809 |
for (let i = 0; i <= 5; i++) {
|
| 810 |
-
const y =
|
| 811 |
-
ctx.beginPath();
|
| 812 |
-
ctx.
|
| 813 |
-
|
| 814 |
-
ctx.
|
| 815 |
}
|
| 816 |
|
| 817 |
-
//
|
| 818 |
-
|
| 819 |
-
ctx.
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
});
|
| 826 |
|
| 827 |
-
//
|
| 828 |
-
ctx.
|
| 829 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
ctx.beginPath();
|
| 831 |
-
|
| 832 |
-
const x =
|
| 833 |
-
const y =
|
| 834 |
-
|
| 835 |
-
else ctx.lineTo(x, y);
|
| 836 |
});
|
| 837 |
ctx.stroke();
|
| 838 |
|
| 839 |
-
//
|
| 840 |
-
|
| 841 |
-
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
});
|
| 848 |
|
| 849 |
-
//
|
| 850 |
-
ctx.fillStyle = "#
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
const
|
| 855 |
-
|
| 856 |
-
|
|
|
|
| 857 |
}
|
| 858 |
|
| 859 |
// Legend
|
| 860 |
-
ctx.font = "
|
| 861 |
-
ctx.
|
| 862 |
-
ctx.fillStyle = "
|
| 863 |
-
ctx.
|
| 864 |
-
ctx.fillStyle = "rgba(33, 150, 243, 0.6)";
|
| 865 |
-
ctx.fillText("Volume", padding.left + 60, 12);
|
| 866 |
-
ctx.fillStyle = "#666";
|
| 867 |
-
ctx.fillText(`(${trades.length} trades${selectedSymbol ? " for " + selectedSymbol : ""})`, padding.left + 130, 12);
|
| 868 |
}
|
| 869 |
|
| 870 |
function renderOrderBook(book) {
|
|
@@ -1000,7 +988,7 @@
|
|
| 1000 |
renderTrades();
|
| 1001 |
renderBBOs();
|
| 1002 |
renderStats();
|
| 1003 |
-
|
| 1004 |
});
|
| 1005 |
|
| 1006 |
eventSource.addEventListener("order", (e) => {
|
|
@@ -1016,7 +1004,7 @@
|
|
| 1016 |
if (state.trades.length > 200) state.trades.pop();
|
| 1017 |
renderTrades();
|
| 1018 |
renderStats();
|
| 1019 |
-
|
| 1020 |
});
|
| 1021 |
|
| 1022 |
eventSource.addEventListener("snapshot", (e) => {
|
|
@@ -1052,7 +1040,7 @@
|
|
| 1052 |
renderTrades();
|
| 1053 |
renderBBOs();
|
| 1054 |
renderStats();
|
| 1055 |
-
|
| 1056 |
} catch (e) {
|
| 1057 |
console.error("Fetch data failed:", e);
|
| 1058 |
}
|
|
@@ -1137,111 +1125,33 @@
|
|
| 1137 |
const period = document.getElementById("history-period").value;
|
| 1138 |
const statusEl = document.getElementById("history-status");
|
| 1139 |
if (!sym) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
statusEl.textContent = "Loading...";
|
| 1141 |
try {
|
| 1142 |
const r = await fetch(`/history/${sym}?period=${period}`);
|
| 1143 |
const data = await r.json();
|
| 1144 |
-
|
| 1145 |
-
|
| 1146 |
-
|
| 1147 |
-
|
| 1148 |
} catch (e) {
|
| 1149 |
statusEl.textContent = "Error: " + e.message;
|
| 1150 |
}
|
| 1151 |
}
|
| 1152 |
|
| 1153 |
-
function drawHistoryChart(candles) {
|
| 1154 |
-
const canvas = document.getElementById("history-chart");
|
| 1155 |
-
const ctx = canvas.getContext("2d");
|
| 1156 |
-
canvas.width = canvas.offsetWidth || 900;
|
| 1157 |
-
canvas.height = 350;
|
| 1158 |
-
|
| 1159 |
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 1160 |
-
|
| 1161 |
-
if (!candles || candles.length === 0) {
|
| 1162 |
-
ctx.fillStyle = "#999";
|
| 1163 |
-
ctx.font = "14px Arial";
|
| 1164 |
-
ctx.textAlign = "center";
|
| 1165 |
-
ctx.fillText("No history data for this period", canvas.width / 2, canvas.height / 2);
|
| 1166 |
-
return;
|
| 1167 |
-
}
|
| 1168 |
-
|
| 1169 |
-
const pad = { top: 25, right: 20, bottom: 40, left: 65 };
|
| 1170 |
-
const W = canvas.width - pad.left - pad.right;
|
| 1171 |
-
const H = canvas.height - pad.top - pad.bottom;
|
| 1172 |
-
const priceH = H * 0.70;
|
| 1173 |
-
const gapH = H * 0.05;
|
| 1174 |
-
const volH = H * 0.25;
|
| 1175 |
-
const priceY = pad.top;
|
| 1176 |
-
const volY = pad.top + priceH + gapH;
|
| 1177 |
-
|
| 1178 |
-
const maxP = Math.max(...candles.map(c => c.h)) * 1.002;
|
| 1179 |
-
const minP = Math.min(...candles.map(c => c.l)) * 0.998;
|
| 1180 |
-
const maxV = Math.max(...candles.map(c => c.v), 1);
|
| 1181 |
-
|
| 1182 |
-
// Price grid lines
|
| 1183 |
-
ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
|
| 1184 |
-
for (let i = 0; i <= 5; i++) {
|
| 1185 |
-
const y = priceY + (priceH * i / 5);
|
| 1186 |
-
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(canvas.width - pad.right, y); ctx.stroke();
|
| 1187 |
-
const p = maxP - (maxP - minP) * i / 5;
|
| 1188 |
-
ctx.fillStyle = "#666"; ctx.font = "10px Arial"; ctx.textAlign = "right";
|
| 1189 |
-
ctx.fillText(p.toFixed(2), pad.left - 5, y + 4);
|
| 1190 |
-
}
|
| 1191 |
-
|
| 1192 |
-
// Divider between price and volume areas
|
| 1193 |
-
ctx.strokeStyle = "#ddd"; ctx.lineWidth = 1;
|
| 1194 |
-
ctx.beginPath(); ctx.moveTo(pad.left, volY); ctx.lineTo(canvas.width - pad.right, volY); ctx.stroke();
|
| 1195 |
-
|
| 1196 |
-
const n = candles.length;
|
| 1197 |
-
const slotW = W / n;
|
| 1198 |
-
const barW = Math.max(1, Math.floor(slotW * 0.7));
|
| 1199 |
-
|
| 1200 |
-
candles.forEach((c, i) => {
|
| 1201 |
-
const x = pad.left + i * slotW + (slotW - barW) / 2;
|
| 1202 |
-
const isUp = c.c >= c.o;
|
| 1203 |
-
const color = isUp ? "#26a69a" : "#ef5350";
|
| 1204 |
-
const toY = p => priceY + priceH - ((p - minP) / (maxP - minP)) * priceH;
|
| 1205 |
-
|
| 1206 |
-
// Wick
|
| 1207 |
-
const midX = x + barW / 2;
|
| 1208 |
-
ctx.strokeStyle = color; ctx.lineWidth = 1;
|
| 1209 |
-
ctx.beginPath(); ctx.moveTo(midX, toY(c.h)); ctx.lineTo(midX, toY(c.l)); ctx.stroke();
|
| 1210 |
-
|
| 1211 |
-
// Candle body
|
| 1212 |
-
const bodyTop = Math.min(toY(c.o), toY(c.c));
|
| 1213 |
-
const bodyH = Math.max(1, Math.abs(toY(c.c) - toY(c.o)));
|
| 1214 |
-
ctx.fillStyle = color;
|
| 1215 |
-
ctx.fillRect(x, bodyTop, barW, bodyH);
|
| 1216 |
-
|
| 1217 |
-
// Volume bar
|
| 1218 |
-
const vH = (c.v / maxV) * volH;
|
| 1219 |
-
ctx.fillStyle = isUp ? "rgba(38,166,154,0.5)" : "rgba(239,83,80,0.5)";
|
| 1220 |
-
ctx.fillRect(x, volY + volH - vH, barW, vH);
|
| 1221 |
-
});
|
| 1222 |
-
|
| 1223 |
-
// Time axis labels (up to 6 evenly spaced)
|
| 1224 |
-
ctx.fillStyle = "#666"; ctx.font = "10px Arial"; ctx.textAlign = "center";
|
| 1225 |
-
const step = Math.max(1, Math.floor(n / 6));
|
| 1226 |
-
for (let i = 0; i < n; i += step) {
|
| 1227 |
-
const dt = new Date(candles[i].t * 1000);
|
| 1228 |
-
const lbl = (dt.getMonth()+1) + "/" + dt.getDate() + " "
|
| 1229 |
-
+ dt.getHours().toString().padStart(2,"0") + ":"
|
| 1230 |
-
+ dt.getMinutes().toString().padStart(2,"0");
|
| 1231 |
-
const x = pad.left + i * slotW + slotW / 2;
|
| 1232 |
-
ctx.fillText(lbl, x, canvas.height - pad.bottom + 15);
|
| 1233 |
-
}
|
| 1234 |
-
|
| 1235 |
-
// VOL label rotated
|
| 1236 |
-
ctx.save();
|
| 1237 |
-
ctx.translate(10, volY + volH / 2);
|
| 1238 |
-
ctx.rotate(-Math.PI / 2);
|
| 1239 |
-
ctx.textAlign = "center";
|
| 1240 |
-
ctx.fillStyle = "#aaa"; ctx.font = "9px Arial";
|
| 1241 |
-
ctx.fillText("VOL", 0, 0);
|
| 1242 |
-
ctx.restore();
|
| 1243 |
-
}
|
| 1244 |
-
|
| 1245 |
// Initial load: fetch data via REST, then connect SSE
|
| 1246 |
async function init() {
|
| 1247 |
await fetchData();
|
|
|
|
| 290 |
</div>
|
| 291 |
</div>
|
| 292 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
<div class="panel">
|
| 294 |
<h2>Order Book
|
| 295 |
<select id="bbo-symbol-select" onchange="renderOrderBookPanel()" style="margin-left:10px; padding:2px 5px;">
|
|
|
|
| 328 |
|
| 329 |
</div>
|
| 330 |
|
| 331 |
+
<!-- Unified Price Chart panel (full width) -->
|
| 332 |
<div class="panel-full">
|
| 333 |
+
<h2>Price Chart
|
| 334 |
<select id="history-symbol" onchange="loadHistory()" style="margin-left:10px; padding:2px 5px;">
|
| 335 |
<option value="">Select symbol...</option>
|
| 336 |
</select>
|
| 337 |
<select id="history-period" onchange="loadHistory()" style="margin-left:5px; padding:2px 5px;">
|
| 338 |
+
<option value="live" selected>Live</option>
|
| 339 |
<option value="1h">1 Hour</option>
|
| 340 |
<option value="8h">8 Hours</option>
|
| 341 |
+
<option value="1d">1 Day</option>
|
| 342 |
<option value="1w">1 Week</option>
|
| 343 |
<option value="1m">1 Month</option>
|
| 344 |
</select>
|
|
|
|
| 666 |
renderOrderBookPanel();
|
| 667 |
}
|
| 668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 669 |
// Update history symbol select
|
| 670 |
const histSel = document.getElementById("history-symbol");
|
| 671 |
const histCurrent = histSel.value;
|
|
|
|
| 746 |
// Alias for backward compatibility
|
| 747 |
function renderFullBBO() { renderOrderBookPanel(); }
|
| 748 |
|
| 749 |
+
// ── Unified price chart (live trades or OHLCV history) ─────────────────────
|
| 750 |
+
// points: [{price, volume, ts}] – normalised by caller
|
| 751 |
+
function drawChart(points, labelSuffix) {
|
| 752 |
+
const canvas = document.getElementById("history-chart");
|
| 753 |
+
const ctx = canvas.getContext("2d");
|
| 754 |
+
canvas.width = canvas.offsetWidth || 900;
|
| 755 |
+
canvas.height = parseInt(canvas.style.height) || 350;
|
| 756 |
+
|
| 757 |
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
| 758 |
+
|
| 759 |
+
if (!points || points.length === 0) {
|
| 760 |
+
ctx.fillStyle = "#999"; ctx.font = "14px Arial"; ctx.textAlign = "center";
|
| 761 |
+
ctx.fillText("No data for this period", canvas.width / 2, canvas.height / 2);
|
|
|
|
|
|
|
| 762 |
return;
|
| 763 |
}
|
| 764 |
|
| 765 |
+
const pad = { top: 25, right: 20, bottom: 40, left: 65 };
|
| 766 |
+
const W = canvas.width - pad.left - pad.right;
|
| 767 |
+
const H = canvas.height - pad.top - pad.bottom;
|
| 768 |
+
const priceH = H * 0.72;
|
| 769 |
+
const gapH = H * 0.03;
|
| 770 |
+
const volH = H * 0.25;
|
| 771 |
+
const priceY = pad.top;
|
| 772 |
+
const volY = pad.top + priceH + gapH;
|
| 773 |
|
| 774 |
+
const prices = points.map(p => p.price);
|
| 775 |
+
const volumes = points.map(p => p.volume);
|
| 776 |
+
const minP = Math.min(...prices) * 0.997;
|
| 777 |
+
const maxP = Math.max(...prices) * 1.003;
|
| 778 |
+
const maxV = Math.max(...volumes, 1);
|
|
|
|
| 779 |
|
| 780 |
+
const toY = p => priceY + priceH - ((p - minP) / (maxP - minP)) * priceH;
|
| 781 |
|
| 782 |
+
// Grid lines
|
| 783 |
+
ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
|
|
|
|
| 784 |
for (let i = 0; i <= 5; i++) {
|
| 785 |
+
const y = priceY + priceH * i / 5;
|
| 786 |
+
ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(canvas.width - pad.right, y); ctx.stroke();
|
| 787 |
+
ctx.fillStyle = "#666"; ctx.font = "10px Arial"; ctx.textAlign = "right";
|
| 788 |
+
const p = maxP - (maxP - minP) * i / 5;
|
| 789 |
+
ctx.fillText(p.toFixed(2), pad.left - 5, y + 4);
|
| 790 |
}
|
| 791 |
|
| 792 |
+
// Volume / price area divider
|
| 793 |
+
ctx.strokeStyle = "#ddd";
|
| 794 |
+
ctx.beginPath(); ctx.moveTo(pad.left, volY); ctx.lineTo(canvas.width - pad.right, volY); ctx.stroke();
|
| 795 |
+
|
| 796 |
+
const n = points.length;
|
| 797 |
+
const slotW = W / n;
|
| 798 |
+
const barW = Math.max(2, Math.floor(slotW * 0.7));
|
| 799 |
+
|
| 800 |
+
// Volume bars
|
| 801 |
+
points.forEach((p, i) => {
|
| 802 |
+
const x = pad.left + i * slotW + (slotW - barW) / 2;
|
| 803 |
+
const bh = (p.volume / maxV) * volH;
|
| 804 |
+
ctx.fillStyle = "rgba(33,150,243,0.35)";
|
| 805 |
+
ctx.fillRect(x, volY + volH - bh, barW, bh);
|
| 806 |
});
|
| 807 |
|
| 808 |
+
// Gradient fill under line
|
| 809 |
+
const grad = ctx.createLinearGradient(0, priceY, 0, priceY + priceH);
|
| 810 |
+
grad.addColorStop(0, "rgba(76,175,80,0.18)");
|
| 811 |
+
grad.addColorStop(1, "rgba(76,175,80,0)");
|
| 812 |
+
ctx.beginPath();
|
| 813 |
+
ctx.moveTo(pad.left + slotW / 2, toY(points[0].price));
|
| 814 |
+
points.forEach((p, i) => ctx.lineTo(pad.left + i * slotW + slotW / 2, toY(p.price)));
|
| 815 |
+
ctx.lineTo(pad.left + (n - 1) * slotW + slotW / 2, priceY + priceH);
|
| 816 |
+
ctx.lineTo(pad.left + slotW / 2, priceY + priceH);
|
| 817 |
+
ctx.closePath();
|
| 818 |
+
ctx.fillStyle = grad;
|
| 819 |
+
ctx.fill();
|
| 820 |
+
|
| 821 |
+
// Price line
|
| 822 |
+
ctx.strokeStyle = "#4CAF50"; ctx.lineWidth = 2;
|
| 823 |
ctx.beginPath();
|
| 824 |
+
points.forEach((p, i) => {
|
| 825 |
+
const x = pad.left + i * slotW + slotW / 2;
|
| 826 |
+
const y = toY(p.price);
|
| 827 |
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
|
|
| 828 |
});
|
| 829 |
ctx.stroke();
|
| 830 |
|
| 831 |
+
// Dots (only when few points)
|
| 832 |
+
if (n <= 60) {
|
| 833 |
+
ctx.fillStyle = "#4CAF50";
|
| 834 |
+
points.forEach((p, i) => {
|
| 835 |
+
const x = pad.left + i * slotW + slotW / 2;
|
| 836 |
+
ctx.beginPath(); ctx.arc(x, toY(p.price), 3, 0, Math.PI * 2); ctx.fill();
|
| 837 |
+
});
|
| 838 |
+
}
|
|
|
|
| 839 |
|
| 840 |
+
// Time axis labels (up to 6 evenly spaced)
|
| 841 |
+
ctx.fillStyle = "#666"; ctx.font = "10px Arial"; ctx.textAlign = "center";
|
| 842 |
+
const step = Math.max(1, Math.floor(n / 6));
|
| 843 |
+
for (let i = 0; i < n; i += step) {
|
| 844 |
+
const dt = new Date(points[i].ts * 1000);
|
| 845 |
+
const lbl = (dt.getMonth()+1) + "/" + dt.getDate() + " "
|
| 846 |
+
+ dt.getHours().toString().padStart(2,"0") + ":"
|
| 847 |
+
+ dt.getMinutes().toString().padStart(2,"0");
|
| 848 |
+
ctx.fillText(lbl, pad.left + i * slotW + slotW / 2, canvas.height - pad.bottom + 15);
|
| 849 |
}
|
| 850 |
|
| 851 |
// Legend
|
| 852 |
+
ctx.font = "11px Arial"; ctx.textAlign = "left";
|
| 853 |
+
ctx.fillStyle = "#4CAF50"; ctx.fillText("● Price", pad.left, 16);
|
| 854 |
+
ctx.fillStyle = "rgba(33,150,243,0.7)"; ctx.fillText("■ Volume", pad.left + 65, 16);
|
| 855 |
+
if (labelSuffix) { ctx.fillStyle = "#999"; ctx.fillText(labelSuffix, pad.left + 145, 16); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 856 |
}
|
| 857 |
|
| 858 |
function renderOrderBook(book) {
|
|
|
|
| 988 |
renderTrades();
|
| 989 |
renderBBOs();
|
| 990 |
renderStats();
|
| 991 |
+
loadHistory();
|
| 992 |
});
|
| 993 |
|
| 994 |
eventSource.addEventListener("order", (e) => {
|
|
|
|
| 1004 |
if (state.trades.length > 200) state.trades.pop();
|
| 1005 |
renderTrades();
|
| 1006 |
renderStats();
|
| 1007 |
+
if (document.getElementById("history-period").value === "live") loadHistory();
|
| 1008 |
});
|
| 1009 |
|
| 1010 |
eventSource.addEventListener("snapshot", (e) => {
|
|
|
|
| 1040 |
renderTrades();
|
| 1041 |
renderBBOs();
|
| 1042 |
renderStats();
|
| 1043 |
+
loadHistory();
|
| 1044 |
} catch (e) {
|
| 1045 |
console.error("Fetch data failed:", e);
|
| 1046 |
}
|
|
|
|
| 1125 |
const period = document.getElementById("history-period").value;
|
| 1126 |
const statusEl = document.getElementById("history-status");
|
| 1127 |
if (!sym) return;
|
| 1128 |
+
|
| 1129 |
+
if (period === "live") {
|
| 1130 |
+
let trades = state.trades.filter(t => t.symbol === sym);
|
| 1131 |
+
trades = trades.slice(0, 100).reverse();
|
| 1132 |
+
const points = trades.map(t => ({
|
| 1133 |
+
price: t.price || 0,
|
| 1134 |
+
volume: t.quantity || t.qty || 0,
|
| 1135 |
+
ts: t.timestamp || Date.now() / 1000,
|
| 1136 |
+
}));
|
| 1137 |
+
drawChart(points, `${trades.length} trades`);
|
| 1138 |
+
statusEl.textContent = trades.length ? `${trades.length} trades` : "No trades yet";
|
| 1139 |
+
return;
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
statusEl.textContent = "Loading...";
|
| 1143 |
try {
|
| 1144 |
const r = await fetch(`/history/${sym}?period=${period}`);
|
| 1145 |
const data = await r.json();
|
| 1146 |
+
const candles = data.candles || [];
|
| 1147 |
+
const points = candles.map(c => ({ price: c.c, volume: c.v, ts: c.t }));
|
| 1148 |
+
drawChart(points, `${candles.length} candles`);
|
| 1149 |
+
statusEl.textContent = candles.length ? `${candles.length} candles` : "No data yet";
|
| 1150 |
} catch (e) {
|
| 1151 |
statusEl.textContent = "Error: " + e.message;
|
| 1152 |
}
|
| 1153 |
}
|
| 1154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
// Initial load: fetch data via REST, then connect SSE
|
| 1156 |
async function init() {
|
| 1157 |
await fetchData();
|