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.
- package/bin/horizon.js +18 -1
- package/package.json +1 -1
- package/src/app.ts +85 -11
- package/src/components/code-panel.ts +141 -14
- package/src/platform/session-sync.ts +1 -1
- package/src/state/types.ts +1 -0
- package/src/strategy/code-stream.ts +3 -1
- package/src/strategy/dashboard.ts +189 -18
- package/src/strategy/prompts.ts +426 -6
- package/src/strategy/tools.ts +311 -54
- package/src/strategy/validator.ts +98 -0
- package/src/updater.ts +118 -0
|
@@ -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 .
|
|
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"
|
|
42
|
-
|
|
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
|
-
|
|
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",
|
|
60
|
-
["Win Rate", ((m.win_rate||0)*100).toFixed(1)+"%", (m.win_rate||0) >= 0.5],
|
|
61
|
-
["Trades", m.total_trades,
|
|
62
|
-
["Max DD", ((m.max_drawdown_pct||0)).toFixed(1)+"%",
|
|
63
|
-
].map(([l,v,
|
|
64
|
-
|
|
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 (
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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" },
|