RayMelius Claude Sonnet 4.6 commited on
Commit
ce62533
Β·
1 Parent(s): 1c61b9e

Move Price Chart into grid beside Trading Statistics; traditional chart style

Browse files

- Price Chart moves from full-width panel to grid panel (row 3, col 2)
- Traditional chart: candlestick bodies + wicks (teal/red), orange close-price
line overlay, colour-matched volume bars at bottom (27% height)
- Live mode: individual trades rendered as single-tick candles (o=h=l=c=price)
+ orange close line connecting ticks + volume bars
- Historical modes: full OHLCV candles (open/high/low/close) passed to drawChart
- Canvas resizes dynamically to fill panel height (flex-grow:1)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. dashboard/templates/index.html +89 -91
dashboard/templates/index.html CHANGED
@@ -326,25 +326,24 @@
326
  </div>
327
  </div>
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>
345
- <span id="history-status" style="font-size:12px; color:#999; margin-left:12px;"></span>
346
- </h2>
347
- <canvas id="history-chart" style="width:100%; height:350px; display:block;"></canvas>
348
  </div>
349
 
350
  <script>
@@ -746,113 +745,108 @@
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) {
@@ -1129,13 +1123,15 @@
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
 
@@ -1144,7 +1140,9 @@
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) {
 
326
  </div>
327
  </div>
328
 
329
+ <div class="panel">
330
+ <h2 style="flex-shrink:0;">Price Chart
331
+ <select id="history-symbol" onchange="loadHistory()" style="margin-left:8px; padding:2px 4px; font-size:11px;">
332
+ <option value="">Symbol...</option>
333
+ </select>
334
+ <select id="history-period" onchange="loadHistory()" style="margin-left:4px; padding:2px 4px; font-size:11px;">
335
+ <option value="live" selected>Live</option>
336
+ <option value="1h">1H</option>
337
+ <option value="8h">8H</option>
338
+ <option value="1d">1D</option>
339
+ <option value="1w">1W</option>
340
+ <option value="1m">1M</option>
341
+ </select>
342
+ <span id="history-status" style="font-size:10px; color:#999; margin-left:6px;"></span>
343
+ </h2>
344
+ <canvas id="history-chart" style="width:100%; flex-grow:1; min-height:0;"></canvas>
345
+ </div>
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  </div>
348
 
349
  <script>
 
745
  // Alias for backward compatibility
746
  function renderFullBBO() { renderOrderBookPanel(); }
747
 
748
+ // ── Unified price chart: candlesticks + close line + volume ────────────────
749
+ // points: [{open, high, low, close, volume, ts}]
750
  function drawChart(points, labelSuffix) {
751
  const canvas = document.getElementById("history-chart");
752
  const ctx = canvas.getContext("2d");
753
+ canvas.width = canvas.offsetWidth || 500;
754
+ canvas.height = canvas.offsetHeight || 300;
755
 
756
  ctx.clearRect(0, 0, canvas.width, canvas.height);
757
 
758
  if (!points || points.length === 0) {
759
+ ctx.fillStyle = "#999"; ctx.font = "13px Arial"; ctx.textAlign = "center";
760
  ctx.fillText("No data for this period", canvas.width / 2, canvas.height / 2);
761
  return;
762
  }
763
 
764
+ const pad = { top: 18, right: 12, bottom: 32, left: 55 };
765
+ const W = canvas.width - pad.left - pad.right;
766
+ const H = canvas.height - pad.top - pad.bottom;
767
+ const priceH = H * 0.70;
768
  const gapH = H * 0.03;
769
+ const volH = H * 0.27;
770
  const priceY = pad.top;
771
  const volY = pad.top + priceH + gapH;
772
 
773
+ const maxP = Math.max(...points.map(p => p.high)) * 1.002;
774
+ const minP = Math.min(...points.map(p => p.low)) * 0.998;
775
+ const maxV = Math.max(...points.map(p => p.volume), 1);
776
+ const toY = p => priceY + priceH - ((p - minP) / (maxP - minP)) * priceH;
 
777
 
778
+ const n = points.length;
779
+ const slotW = W / n;
780
+ const bodyW = Math.max(1, Math.floor(slotW * 0.65));
781
 
782
+ // Price grid
783
  ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
784
+ for (let i = 0; i <= 4; i++) {
785
+ const y = priceY + priceH * i / 4;
786
  ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(canvas.width - pad.right, y); ctx.stroke();
787
+ ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "right";
788
+ ctx.fillText((maxP - (maxP - minP) * i / 4).toFixed(2), pad.left - 3, y + 3);
 
789
  }
790
 
791
+ // Volume / price divider
792
+ ctx.strokeStyle = "#e0e0e0";
793
  ctx.beginPath(); ctx.moveTo(pad.left, volY); ctx.lineTo(canvas.width - pad.right, volY); ctx.stroke();
794
 
 
 
 
 
795
  // Volume bars
796
  points.forEach((p, i) => {
797
+ const x = pad.left + i * slotW + (slotW - bodyW) / 2;
798
  const bh = (p.volume / maxV) * volH;
799
+ ctx.fillStyle = p.close >= p.open ? "rgba(38,166,154,0.4)" : "rgba(239,83,80,0.4)";
800
+ ctx.fillRect(x, volY + volH - bh, bodyW, bh);
801
  });
802
 
803
+ // Candlestick wicks + bodies
804
+ points.forEach((p, i) => {
805
+ const midX = pad.left + i * slotW + slotW / 2;
806
+ const x = pad.left + i * slotW + (slotW - bodyW) / 2;
807
+ const isUp = p.close >= p.open;
808
+ const color = isUp ? "#26a69a" : "#ef5350";
809
+
810
+ // Wick
811
+ ctx.strokeStyle = color; ctx.lineWidth = 1;
812
+ ctx.beginPath(); ctx.moveTo(midX, toY(p.high)); ctx.lineTo(midX, toY(p.low)); ctx.stroke();
813
+
814
+ // Body (min 1px so single-price ticks still show)
815
+ const bodyTop = Math.min(toY(p.open), toY(p.close));
816
+ const bodyH = Math.max(1, Math.abs(toY(p.close) - toY(p.open)));
817
+ ctx.fillStyle = color;
818
+ ctx.fillRect(x, bodyTop, bodyW, bodyH);
819
+ });
820
+
821
+ // Close-price line overlay (orange)
822
+ ctx.strokeStyle = "rgba(255,152,0,0.85)"; ctx.lineWidth = 1.5;
823
  ctx.beginPath();
824
  points.forEach((p, i) => {
825
  const x = pad.left + i * slotW + slotW / 2;
826
+ i === 0 ? ctx.moveTo(x, toY(p.close)) : ctx.lineTo(x, toY(p.close));
 
827
  });
828
  ctx.stroke();
829
 
830
+ // Time labels (up to 5 evenly spaced)
831
+ ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
832
+ const step = Math.max(1, Math.floor(n / 5));
 
 
 
 
 
 
 
 
 
833
  for (let i = 0; i < n; i += step) {
834
  const dt = new Date(points[i].ts * 1000);
835
  const lbl = (dt.getMonth()+1) + "/" + dt.getDate() + " "
836
  + dt.getHours().toString().padStart(2,"0") + ":"
837
  + dt.getMinutes().toString().padStart(2,"0");
838
+ ctx.fillText(lbl, pad.left + i * slotW + slotW / 2, canvas.height - pad.bottom + 12);
839
  }
840
 
841
  // Legend
842
+ ctx.font = "9px Arial"; ctx.textAlign = "left";
843
+ ctx.fillStyle = "#26a69a"; ctx.fillText("β–²", pad.left, 12);
844
+ ctx.fillStyle = "#ef5350"; ctx.fillText("β–Ό", pad.left + 14, 12);
845
+ ctx.fillStyle = "#888"; ctx.fillText("candle", pad.left + 24, 12);
846
+ ctx.strokeStyle = "rgba(255,152,0,0.85)"; ctx.lineWidth = 1.5;
847
+ ctx.beginPath(); ctx.moveTo(pad.left + 66, 9); ctx.lineTo(pad.left + 78, 9); ctx.stroke();
848
+ ctx.fillStyle = "#ff9800"; ctx.fillText("close", pad.left + 80, 12);
849
+ if (labelSuffix) { ctx.fillStyle = "#bbb"; ctx.fillText(labelSuffix, pad.left + 114, 12); }
850
  }
851
 
852
  function renderOrderBook(book) {
 
1123
  if (period === "live") {
1124
  let trades = state.trades.filter(t => t.symbol === sym);
1125
  trades = trades.slice(0, 100).reverse();
1126
+ // Individual trades have no spread: treat each tick as o=h=l=c
1127
+ const points = trades.map(t => {
1128
+ const p = t.price || 0;
1129
+ return { open: p, high: p, low: p, close: p,
1130
+ volume: t.quantity || t.qty || 0,
1131
+ ts: t.timestamp || Date.now() / 1000 };
1132
+ });
1133
+ drawChart(points, `${trades.length} ticks`);
1134
+ statusEl.textContent = trades.length ? `${trades.length} ticks` : "No trades yet";
1135
  return;
1136
  }
1137
 
 
1140
  const r = await fetch(`/history/${sym}?period=${period}`);
1141
  const data = await r.json();
1142
  const candles = data.candles || [];
1143
+ const points = candles.map(c => ({
1144
+ open: c.o, high: c.h, low: c.l, close: c.c, volume: c.v, ts: c.t
1145
+ }));
1146
  drawChart(points, `${candles.length} candles`);
1147
  statusEl.textContent = candles.length ? `${candles.length} candles` : "No data yet";
1148
  } catch (e) {