horizon-code 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,13 +15,16 @@ const DASHBOARD_HTML = (strategyId: string) => `<!DOCTYPE html>
15
15
  body { font-family: 'SF Mono', 'Fira Code', monospace; background: #1a1a1a; color: #e0e0e0; }
16
16
  .header { padding: 16px 24px; border-bottom: 1px solid #333; display: flex; align-items: center; gap: 12px; }
17
17
  .header h1 { font-size: 14px; color: #fab283; letter-spacing: 2px; }
18
- .header .status { font-size: 12px; color: #7fd88f; }
18
+ .header .strategy-name { font-size: 13px; color: #ccc; font-weight: normal; }
19
+ .header .status { font-size: 12px; color: #7fd88f; margin-left: auto; }
20
+ .header .uptime { font-size: 11px; color: #888; }
19
21
  .grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; padding: 16px 24px; }
20
22
  .metric { background: #212121; border: 1px solid #333; border-radius: 8px; padding: 12px 16px; }
21
23
  .metric .label { font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 1px; }
22
24
  .metric .value { font-size: 20px; font-weight: bold; margin-top: 4px; }
23
25
  .metric .value.green { color: #7fd88f; }
24
26
  .metric .value.red { color: #e06c75; }
27
+ .metric .value.neutral { color: #e0e0e0; }
25
28
  .charts { display: grid; grid-template-columns: 2fr 1fr; gap: 12px; padding: 0 24px 16px; }
26
29
  .chart-box { background: #212121; border: 1px solid #333; border-radius: 8px; padding: 16px; }
27
30
  .chart-box h3 { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; }
@@ -29,22 +32,93 @@ const DASHBOARD_HTML = (strategyId: string) => `<!DOCTYPE html>
29
32
  .logs-box { background: #212121; border: 1px solid #333; border-radius: 8px; padding: 16px; max-height: 200px; overflow-y: auto; }
30
33
  .logs-box h3 { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 1px; margin-bottom: 8px; }
31
34
  .log-line { font-size: 11px; color: #aaa; padding: 2px 0; font-family: monospace; }
35
+ .waiting { display: flex; align-items: center; justify-content: center; height: 120px; color: #666; font-size: 12px; }
36
+ .waiting .pulse { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #fab283; margin-right: 8px; animation: pulse 1.5s ease-in-out infinite; }
37
+ @keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
32
38
  </style>
33
39
  </head>
34
40
  <body>
35
41
  <div class="header">
36
42
  <h1>H O R I Z O N</h1>
43
+ <span class="strategy-name" id="strategyName"></span>
37
44
  <span class="status" id="status">● connecting...</span>
45
+ <span class="uptime" id="uptime"></span>
38
46
  </div>
39
47
  <div class="grid" id="metrics"></div>
40
48
  <div class="charts">
41
- <div class="chart-box"><h3>Equity Curve</h3><canvas id="equity"></canvas></div>
42
- <div class="chart-box"><h3>Positions</h3><div id="positions"></div></div>
49
+ <div class="chart-box">
50
+ <h3>Equity Curve</h3>
51
+ <div id="equityWaiting" class="waiting"><span class="pulse"></span>Waiting for metrics...</div>
52
+ <canvas id="equity" style="display:none"></canvas>
53
+ </div>
54
+ <div class="chart-box"><h3>Positions</h3><div id="positions"><div class="waiting"><span class="pulse"></span>Waiting for data...</div></div></div>
43
55
  </div>
44
- <div class="logs"><div class="logs-box"><h3>Logs</h3><div id="logs"></div></div></div>
56
+ <div class="logs"><div class="logs-box"><h3>Logs</h3><div id="logs"><div class="waiting"><span class="pulse"></span>Waiting for logs...</div></div></div></div>
45
57
  <script>
46
58
  const SID = "${strategyId.replace(/[^a-zA-Z0-9-]/g, "")}";
47
- let equityChart;
59
+ const POLL_INTERVAL_S = 30; // metrics are polled every 30s
60
+ let equityChart = null;
61
+ let prevHistLen = 0;
62
+ let strategyStartedAt = null;
63
+
64
+ // --- Fetch strategy metadata once on load ---
65
+ (async function loadStrategy() {
66
+ try {
67
+ const res = await fetch("/api/strategy");
68
+ if (!res.ok) return;
69
+ const data = await res.json();
70
+ const name = data.name || data.strategy_name || data.title || SID;
71
+ document.getElementById("strategyName").textContent = "/ " + name;
72
+ // Try to find a start timestamp for uptime calculation
73
+ strategyStartedAt = data.started_at || data.created_at || data.deployed_at || null;
74
+ if (strategyStartedAt) updateUptime();
75
+ } catch { /* non-critical */ }
76
+ })();
77
+
78
+ function updateUptime() {
79
+ if (!strategyStartedAt) return;
80
+ const start = new Date(strategyStartedAt).getTime();
81
+ const now = Date.now();
82
+ const diffS = Math.floor((now - start) / 1000);
83
+ if (diffS < 0) return;
84
+ const h = Math.floor(diffS / 3600);
85
+ const m = Math.floor((diffS % 3600) / 60);
86
+ const s = diffS % 60;
87
+ let str = "";
88
+ if (h > 0) str += h + "h ";
89
+ if (m > 0 || h > 0) str += m + "m ";
90
+ str += s + "s";
91
+ document.getElementById("uptime").textContent = "up " + str;
92
+ }
93
+ setInterval(updateUptime, 1000);
94
+
95
+ function pnlColor(val) {
96
+ const n = parseFloat(val);
97
+ if (isNaN(n) || n === 0) return "neutral";
98
+ return n > 0 ? "green" : "red";
99
+ }
100
+
101
+ function formatPnl(val) {
102
+ const n = parseFloat(val);
103
+ if (isNaN(n)) return "$0.00";
104
+ const sign = n >= 0 ? "+$" : "-$";
105
+ return sign + Math.abs(n).toFixed(2);
106
+ }
107
+
108
+ function makeTimeLabels(count) {
109
+ // Each data point is ~POLL_INTERVAL_S apart, most recent is "now"
110
+ const labels = [];
111
+ for (let i = 0; i < count; i++) {
112
+ const secsAgo = (count - 1 - i) * POLL_INTERVAL_S;
113
+ if (secsAgo === 0) { labels.push("now"); continue; }
114
+ const m = Math.floor(secsAgo / 60);
115
+ if (m < 1) { labels.push(secsAgo + "s"); }
116
+ else if (m < 60) { labels.push(m + "m"); }
117
+ else { labels.push(Math.floor(m / 60) + "h" + (m % 60) + "m"); }
118
+ }
119
+ return labels;
120
+ }
121
+
48
122
  async function refresh() {
49
123
  try {
50
124
  const [mRes, lRes] = await Promise.all([
@@ -54,24 +128,112 @@ async function refresh() {
54
128
  document.getElementById("status").textContent = "● live";
55
129
  document.getElementById("status").style.color = "#7fd88f";
56
130
  const m = mRes.latest || {};
131
+
132
+ // --- Metrics grid with proper PnL coloring ---
133
+ const pnlVal = m.total_pnl ?? 0;
57
134
  const grid = document.getElementById("metrics");
58
135
  grid.innerHTML = [
59
- ["Total P&L", m.total_pnl, m.total_pnl >= 0],
60
- ["Win Rate", ((m.win_rate||0)*100).toFixed(1)+"%", (m.win_rate||0) >= 0.5],
61
- ["Trades", m.total_trades, true],
62
- ["Max DD", ((m.max_drawdown_pct||0)).toFixed(1)+"%", false],
63
- ].map(([l,v,g]) => '<div class="metric"><div class="label">'+l+'</div><div class="value '+(g?"green":"red")+'">'+v+'</div></div>').join("");
64
- // Equity chart
136
+ ["Total P&L", formatPnl(pnlVal), pnlColor(pnlVal)],
137
+ ["Win Rate", ((m.win_rate||0)*100).toFixed(1)+"%", (m.win_rate||0) >= 0.5 ? "green" : (m.win_rate||0) > 0 ? "neutral" : "red"],
138
+ ["Trades", m.total_trades ?? 0, "neutral"],
139
+ ["Max DD", ((m.max_drawdown_pct||0)).toFixed(1)+"%", "red"],
140
+ ].map(([l,v,c]) =>
141
+ '<div class="metric"><div class="label">'+l+'</div><div class="value '+c+'">'+v+'</div></div>'
142
+ ).join("");
143
+
144
+ // --- Equity chart with timestamp X-axis, no animation, incremental updates ---
65
145
  const hist = (mRes.pnl_history||[]).map(h => h.total_pnl);
66
- if (equityChart) { equityChart.data.labels = hist.map((_,i)=>i); equityChart.data.datasets[0].data = hist; equityChart.update(); }
67
- else { equityChart = new Chart(document.getElementById("equity"), { type: "line", data: { labels: hist.map((_,i)=>i), datasets: [{ data: hist, borderColor: "#fab283", borderWidth: 1.5, fill: false, pointRadius: 0 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { ticks: { color: "#888" }, grid: { color: "#333" } } } } }); }
68
- // Positions
146
+ if (hist.length === 0) {
147
+ // Show waiting state
148
+ document.getElementById("equityWaiting").style.display = "flex";
149
+ document.getElementById("equity").style.display = "none";
150
+ } else {
151
+ document.getElementById("equityWaiting").style.display = "none";
152
+ document.getElementById("equity").style.display = "block";
153
+ const labels = makeTimeLabels(hist.length);
154
+
155
+ if (!equityChart) {
156
+ equityChart = new Chart(document.getElementById("equity"), {
157
+ type: "line",
158
+ data: {
159
+ labels: labels,
160
+ datasets: [{
161
+ data: hist,
162
+ borderColor: "#fab283",
163
+ borderWidth: 1.5,
164
+ fill: false,
165
+ pointRadius: 0,
166
+ tension: 0.1,
167
+ }],
168
+ },
169
+ options: {
170
+ animation: false,
171
+ responsive: true,
172
+ maintainAspectRatio: true,
173
+ plugins: { legend: { display: false } },
174
+ scales: {
175
+ x: {
176
+ ticks: { color: "#666", font: { size: 10 }, maxTicksLimit: 8 },
177
+ grid: { color: "#2a2a2a" },
178
+ },
179
+ y: {
180
+ ticks: { color: "#888", callback: function(v) { return "$" + v.toFixed(2); } },
181
+ grid: { color: "#333" },
182
+ },
183
+ },
184
+ },
185
+ });
186
+ prevHistLen = hist.length;
187
+ } else if (hist.length !== prevHistLen) {
188
+ // Incremental update — push new points, shift old if over 60
189
+ const newPoints = hist.length - prevHistLen;
190
+ if (newPoints > 0 && newPoints < hist.length) {
191
+ for (let i = hist.length - newPoints; i < hist.length; i++) {
192
+ equityChart.data.labels.push(labels[i]);
193
+ equityChart.data.datasets[0].data.push(hist[i]);
194
+ }
195
+ // Cap at 60 data points
196
+ while (equityChart.data.labels.length > 60) {
197
+ equityChart.data.labels.shift();
198
+ equityChart.data.datasets[0].data.shift();
199
+ }
200
+ // Recompute time labels after shift
201
+ const currentLen = equityChart.data.labels.length;
202
+ equityChart.data.labels = makeTimeLabels(currentLen);
203
+ } else {
204
+ // Full reset (data shrank or jumped)
205
+ equityChart.data.labels = labels;
206
+ equityChart.data.datasets[0].data = hist;
207
+ }
208
+ prevHistLen = hist.length;
209
+ equityChart.update("none"); // "none" mode skips animation
210
+ }
211
+ }
212
+
213
+ // --- Positions ---
69
214
  const pos = m.positions || [];
70
- document.getElementById("positions").innerHTML = pos.length === 0 ? '<div style="color:#888;font-size:12px">No positions</div>' : pos.map(p => '<div style="font-size:12px;padding:4px 0;border-bottom:1px solid #333"><span style="color:'+(p.side==="BUY"?"#7fd88f":"#e06c75")+'">'+p.side+'</span> '+p.slug+' <span style="color:#888">'+p.size+'@'+p.avg_entry_price.toFixed(2)+'</span></div>').join("");
71
- // Logs
215
+ document.getElementById("positions").innerHTML = pos.length === 0
216
+ ? '<div style="color:#888;font-size:12px">No open positions</div>'
217
+ : pos.map(p =>
218
+ '<div style="font-size:12px;padding:4px 0;border-bottom:1px solid #333">'
219
+ + '<span style="color:'+(p.side==="BUY"?"#7fd88f":"#e06c75")+'">'+p.side+'</span> '
220
+ + p.slug+' <span style="color:#888">'+p.size+'@'+Number(p.avg_entry_price).toFixed(2)+'</span>'
221
+ + '</div>'
222
+ ).join("");
223
+
224
+ // --- Logs ---
72
225
  const logs = lRes.logs || lRes || [];
73
- document.getElementById("logs").innerHTML = (Array.isArray(logs) ? logs : []).slice(-20).map(l => '<div class="log-line">'+(l.message||l.text||JSON.stringify(l))+'</div>').join("");
74
- } catch { document.getElementById("status").textContent = "● disconnected"; document.getElementById("status").style.color = "#e06c75"; }
226
+ const logArr = Array.isArray(logs) ? logs : [];
227
+ document.getElementById("logs").innerHTML = logArr.length === 0
228
+ ? '<div class="waiting"><span class="pulse"></span>Waiting for logs...</div>'
229
+ : logArr.slice(-20).map(l =>
230
+ '<div class="log-line">'+(l.message||l.text||JSON.stringify(l))+'</div>'
231
+ ).join("");
232
+
233
+ } catch {
234
+ document.getElementById("status").textContent = "● disconnected";
235
+ document.getElementById("status").style.color = "#e06c75";
236
+ }
75
237
  }
76
238
  refresh();
77
239
  setInterval(refresh, 3000);
@@ -117,6 +279,15 @@ export class DashboardServer {
117
279
  }
118
280
  }
119
281
 
282
+ if (url.pathname === "/api/strategy") {
283
+ try {
284
+ const data = await platform.getStrategy(strategyId);
285
+ return Response.json(data);
286
+ } catch (err) {
287
+ return Response.json({ error: String(err) }, { status: 500 });
288
+ }
289
+ }
290
+
120
291
  // Serve dashboard HTML
121
292
  return new Response(DASHBOARD_HTML(strategyId), {
122
293
  headers: { "Content-Type": "text/html" },