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

Price Chart: show all securities as normalised % chart when no symbol selected

Browse files

- Default selection is "All Symbols" (empty value) instead of auto-picking first symbol
- drawMultiChart(): plots each security as a % change line from its first data point,
so differently-priced securities (e.g. EXAE ~42 vs AAAK ~4) are comparable on one chart
- Right-side vertical legend with colour-coded symbol names
- Zero-line dashed when chart spans positive and negative % territory
- loadAllHistory(): live mode groups state.trades by symbol; historical mode fetches
all symbols in parallel via Promise.all and draws the multi-line chart
- loadHistory() routes to loadAllHistory() when no symbol is selected

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

Files changed (1) hide show
  1. dashboard/templates/index.html +146 -6
dashboard/templates/index.html CHANGED
@@ -329,7 +329,7 @@
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>
@@ -677,10 +677,9 @@
677
  histSel.appendChild(opt);
678
  }
679
  });
680
- if (!histCurrent && histSel.options.length > 1) {
681
- histSel.value = histSel.options[1].value;
682
- loadHistory();
683
- } else if (histCurrent) {
684
  histSel.value = histCurrent;
685
  }
686
  }
@@ -1114,11 +1113,152 @@
1114
 
1115
  // ── Price History chart ────────────────────────────────────────────────────
1116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1117
  async function loadHistory() {
1118
  const sym = document.getElementById("history-symbol").value;
1119
  const period = document.getElementById("history-period").value;
1120
  const statusEl = document.getElementById("history-status");
1121
- if (!sym) return;
1122
 
1123
  if (period === "live") {
1124
  let trades = state.trades.filter(t => t.symbol === sym);
 
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="">All Symbols</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>
 
677
  histSel.appendChild(opt);
678
  }
679
  });
680
+ if (!histCurrent) {
681
+ loadHistory(); // draws all-symbols multi-chart
682
+ } else {
 
683
  histSel.value = histCurrent;
684
  }
685
  }
 
1113
 
1114
  // ── Price History chart ────────────────────────────────────────────────────
1115
 
1116
+ const CHART_COLORS = [
1117
+ "#2196F3","#4CAF50","#FF9800","#E91E63","#9C27B0",
1118
+ "#00BCD4","#FF5722","#8BC34A","#607D8B","#F44336"
1119
+ ];
1120
+
1121
+ // ── Multi-symbol normalised % chart ────────────────────────────────────────
1122
+ function drawMultiChart(series, labelSuffix) {
1123
+ const canvas = document.getElementById("history-chart");
1124
+ const ctx = canvas.getContext("2d");
1125
+ canvas.width = canvas.offsetWidth || 500;
1126
+ canvas.height = canvas.offsetHeight || 300;
1127
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1128
+
1129
+ const active = series.filter(s => s.points.length > 0);
1130
+ if (!active.length) {
1131
+ ctx.fillStyle = "#999"; ctx.font = "13px Arial"; ctx.textAlign = "center";
1132
+ ctx.fillText("No data for this period", canvas.width / 2, canvas.height / 2);
1133
+ return;
1134
+ }
1135
+
1136
+ const pad = { top: 18, right: 48, bottom: 32, left: 46 };
1137
+ const W = canvas.width - pad.left - pad.right;
1138
+ const H = canvas.height - pad.top - pad.bottom;
1139
+
1140
+ // Normalise each series to % change from its first point
1141
+ const norm = active.map(s => {
1142
+ const base = s.points[0].price || 1;
1143
+ return { ...s, pcts: s.points.map(p => (p.price - base) / base * 100) };
1144
+ });
1145
+
1146
+ const allPcts = norm.flatMap(s => s.pcts);
1147
+ let minPct = Math.min(...allPcts);
1148
+ let maxPct = Math.max(...allPcts);
1149
+ const rng = maxPct - minPct || 1;
1150
+ minPct -= rng * 0.06; maxPct += rng * 0.06;
1151
+
1152
+ // Global ts range
1153
+ const allTs = active.flatMap(s => s.points.map(p => p.ts));
1154
+ const minTs = Math.min(...allTs);
1155
+ const maxTs = Math.max(...allTs);
1156
+ const tsRng = maxTs - minTs || 1;
1157
+
1158
+ const toX = ts => pad.left + ((ts - minTs) / tsRng) * W;
1159
+ const toY = pct => pad.top + H - ((pct - minPct) / (maxPct - minPct)) * H;
1160
+
1161
+ // Grid + Y labels
1162
+ ctx.strokeStyle = "#eee"; ctx.lineWidth = 1;
1163
+ for (let i = 0; i <= 4; i++) {
1164
+ const pct = minPct + (maxPct - minPct) * i / 4;
1165
+ const y = toY(pct);
1166
+ ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + W, y); ctx.stroke();
1167
+ ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "right";
1168
+ ctx.fillText(pct.toFixed(1) + "%", pad.left - 3, y + 3);
1169
+ }
1170
+
1171
+ // Zero line
1172
+ if (minPct < 0 && maxPct > 0) {
1173
+ const y0 = toY(0);
1174
+ ctx.strokeStyle = "#ccc"; ctx.lineWidth = 1; ctx.setLineDash([3, 3]);
1175
+ ctx.beginPath(); ctx.moveTo(pad.left, y0); ctx.lineTo(pad.left + W, y0); ctx.stroke();
1176
+ ctx.setLineDash([]);
1177
+ }
1178
+
1179
+ // Series lines
1180
+ norm.forEach((s, idx) => {
1181
+ const color = CHART_COLORS[idx % CHART_COLORS.length];
1182
+ ctx.strokeStyle = color; ctx.lineWidth = 1.5;
1183
+ ctx.beginPath();
1184
+ s.points.forEach((p, i) => {
1185
+ const x = toX(p.ts);
1186
+ const y = toY(s.pcts[i]);
1187
+ i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
1188
+ });
1189
+ ctx.stroke();
1190
+ });
1191
+
1192
+ // Time labels (up to 4)
1193
+ ctx.fillStyle = "#888"; ctx.font = "9px Arial"; ctx.textAlign = "center";
1194
+ for (let i = 0; i <= 3; i++) {
1195
+ const ts = minTs + tsRng * i / 3;
1196
+ const dt = new Date(ts * 1000);
1197
+ const lbl = (dt.getMonth()+1)+"/"+dt.getDate()+" "
1198
+ + dt.getHours().toString().padStart(2,"0")+":"
1199
+ + dt.getMinutes().toString().padStart(2,"0");
1200
+ ctx.fillText(lbl, toX(ts), canvas.height - pad.bottom + 12);
1201
+ }
1202
+
1203
+ // Right-side vertical legend
1204
+ ctx.font = "9px Arial"; ctx.textAlign = "left";
1205
+ norm.forEach((s, idx) => {
1206
+ const color = CHART_COLORS[idx % CHART_COLORS.length];
1207
+ const ly = pad.top + idx * 13;
1208
+ ctx.fillStyle = color;
1209
+ ctx.fillRect(canvas.width - pad.right + 2, ly - 1, 10, 3);
1210
+ ctx.fillText(s.symbol, canvas.width - pad.right + 14, ly + 3);
1211
+ });
1212
+
1213
+ if (labelSuffix) {
1214
+ ctx.fillStyle = "#bbb"; ctx.textAlign = "right";
1215
+ ctx.fillText(labelSuffix, canvas.width - pad.right - 2, 12);
1216
+ }
1217
+ }
1218
+
1219
+ async function loadAllHistory() {
1220
+ const period = document.getElementById("history-period").value;
1221
+ const statusEl = document.getElementById("history-status");
1222
+ const symbols = Object.keys(state.bbos).sort();
1223
+
1224
+ if (!symbols.length) {
1225
+ drawMultiChart([], "");
1226
+ statusEl.textContent = "No symbols yet";
1227
+ return;
1228
+ }
1229
+
1230
+ if (period === "live") {
1231
+ const series = symbols.map(sym => {
1232
+ let trades = state.trades.filter(t => t.symbol === sym).slice(0, 100).reverse();
1233
+ return { symbol: sym, points: trades.map(t => ({ price: t.price || 0, ts: t.timestamp || Date.now()/1000 })) };
1234
+ });
1235
+ drawMultiChart(series, `${symbols.length} symbols`);
1236
+ statusEl.textContent = `${symbols.length} symbols`;
1237
+ return;
1238
+ }
1239
+
1240
+ statusEl.textContent = "Loading...";
1241
+ try {
1242
+ const results = await Promise.all(
1243
+ symbols.map(sym => fetch(`/history/${sym}?period=${period}`).then(r => r.json()))
1244
+ );
1245
+ const series = symbols.map((sym, i) => ({
1246
+ symbol: sym,
1247
+ points: (results[i].candles || []).map(c => ({ price: c.c, ts: c.t }))
1248
+ }));
1249
+ const filled = series.filter(s => s.points.length > 0);
1250
+ drawMultiChart(series, `${filled.length} symbols`);
1251
+ statusEl.textContent = `${filled.length} symbols`;
1252
+ } catch (e) {
1253
+ statusEl.textContent = "Error: " + e.message;
1254
+ }
1255
+ }
1256
+
1257
  async function loadHistory() {
1258
  const sym = document.getElementById("history-symbol").value;
1259
  const period = document.getElementById("history-period").value;
1260
  const statusEl = document.getElementById("history-status");
1261
+ if (!sym) { await loadAllHistory(); return; }
1262
 
1263
  if (period === "live") {
1264
  let trades = state.trades.filter(t => t.symbol === sym);