horizon-code 0.1.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/assets/python/highlights.scm +137 -0
- package/assets/python/tree-sitter-python.wasm +0 -0
- package/bin/horizon.js +2 -0
- package/package.json +40 -0
- package/src/ai/client.ts +369 -0
- package/src/ai/system-prompt.ts +86 -0
- package/src/app.ts +1454 -0
- package/src/chat/messages.ts +48 -0
- package/src/chat/renderer.ts +243 -0
- package/src/chat/types.ts +18 -0
- package/src/components/code-panel.ts +329 -0
- package/src/components/footer.ts +72 -0
- package/src/components/hooks-panel.ts +224 -0
- package/src/components/input-bar.ts +193 -0
- package/src/components/mode-bar.ts +245 -0
- package/src/components/session-panel.ts +294 -0
- package/src/components/settings-panel.ts +372 -0
- package/src/components/splash.ts +156 -0
- package/src/components/strategy-panel.ts +489 -0
- package/src/components/tab-bar.ts +112 -0
- package/src/components/tutorial-panel.ts +680 -0
- package/src/components/widgets/progress-bar.ts +38 -0
- package/src/components/widgets/sparkline.ts +57 -0
- package/src/hooks/executor.ts +109 -0
- package/src/index.ts +22 -0
- package/src/keys/handler.ts +198 -0
- package/src/platform/auth.ts +36 -0
- package/src/platform/client.ts +159 -0
- package/src/platform/config.ts +121 -0
- package/src/platform/session-sync.ts +158 -0
- package/src/platform/supabase.ts +376 -0
- package/src/platform/sync.ts +149 -0
- package/src/platform/tiers.ts +103 -0
- package/src/platform/tools.ts +163 -0
- package/src/platform/types.ts +86 -0
- package/src/platform/usage.ts +224 -0
- package/src/research/apis.ts +367 -0
- package/src/research/tools.ts +205 -0
- package/src/research/widgets.ts +523 -0
- package/src/state/store.ts +256 -0
- package/src/state/types.ts +109 -0
- package/src/strategy/ascii-chart.ts +74 -0
- package/src/strategy/code-stream.ts +146 -0
- package/src/strategy/dashboard.ts +140 -0
- package/src/strategy/persistence.ts +82 -0
- package/src/strategy/prompts.ts +626 -0
- package/src/strategy/sandbox.ts +137 -0
- package/src/strategy/tools.ts +764 -0
- package/src/strategy/validator.ts +216 -0
- package/src/strategy/widgets.ts +270 -0
- package/src/syntax/setup.ts +54 -0
- package/src/theme/colors.ts +107 -0
- package/src/theme/icons.ts +27 -0
- package/src/util/hyperlink.ts +21 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// CLI widgets for research tool results
|
|
2
|
+
// Designed for wide terminals (100+ cols) — uses horizontal space aggressively
|
|
3
|
+
|
|
4
|
+
import { BoxRenderable, TextRenderable, type CliRenderer } from "@opentui/core";
|
|
5
|
+
import { COLORS } from "../theme/colors.ts";
|
|
6
|
+
|
|
7
|
+
const SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
8
|
+
const BAR = "\u2588";
|
|
9
|
+
|
|
10
|
+
let widgetCounter = 0;
|
|
11
|
+
function uid(): string { return `w-${Date.now()}-${widgetCounter++}`; }
|
|
12
|
+
|
|
13
|
+
function sparkline(values: number[], width: number): string {
|
|
14
|
+
if (values.length === 0) return "";
|
|
15
|
+
const sampled: number[] = [];
|
|
16
|
+
for (let i = 0; i < width; i++) {
|
|
17
|
+
const idx = Math.floor((i / width) * values.length);
|
|
18
|
+
sampled.push(values[idx] ?? 0);
|
|
19
|
+
}
|
|
20
|
+
const min = Math.min(...sampled);
|
|
21
|
+
const max = Math.max(...sampled);
|
|
22
|
+
const range = max - min || 1;
|
|
23
|
+
return sampled.map((v) => {
|
|
24
|
+
const idx = Math.min(Math.floor(((v - min) / range) * SPARK.length), SPARK.length - 1);
|
|
25
|
+
return SPARK[idx];
|
|
26
|
+
}).join("");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function pnlColor(val: number): string { return val >= 0 ? COLORS.success : COLORS.error; }
|
|
30
|
+
|
|
31
|
+
function fmtPrice(p: number | string | null): string {
|
|
32
|
+
if (p === null || p === undefined) return "--";
|
|
33
|
+
const n = typeof p === "string" ? parseFloat(p) : p;
|
|
34
|
+
return isNaN(n) ? "--" : `${(n * 100).toFixed(0)}c`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fmtDollar(p: number | string | null): string {
|
|
38
|
+
if (p === null || p === undefined) return "--";
|
|
39
|
+
const n = typeof p === "string" ? parseFloat(p) : p;
|
|
40
|
+
return isNaN(n) ? "--" : `$${n.toFixed(2)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function fmtVol(v: number): string {
|
|
44
|
+
if (v >= 1000000) return `$${(v / 1000000).toFixed(1)}M`;
|
|
45
|
+
if (v >= 1000) return `$${(v / 1000).toFixed(0)}k`;
|
|
46
|
+
return `$${v}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function fmtPct(v: number | null): string {
|
|
50
|
+
if (v === null || v === undefined) return "";
|
|
51
|
+
const pct = (v * 100).toFixed(1);
|
|
52
|
+
return v >= 0 ? `+${pct}%` : `${pct}%`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Two-column row: label (dimmed, left) + value (colored, right) on the same line
|
|
56
|
+
function kv(parent: BoxRenderable, renderer: CliRenderer, label: string, value: string, color?: string, labelWidth = 12): void {
|
|
57
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
58
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: label.padEnd(labelWidth), fg: COLORS.textMuted }));
|
|
59
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: value, fg: color ?? COLORS.text }));
|
|
60
|
+
parent.add(row);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Horizontal pair of KV blocks side by side
|
|
64
|
+
function kvPair(parent: BoxRenderable, renderer: CliRenderer,
|
|
65
|
+
l1: string, v1: string, c1: string | undefined,
|
|
66
|
+
l2: string, v2: string, c2: string | undefined,
|
|
67
|
+
): void {
|
|
68
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
69
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: l1.padEnd(12), fg: COLORS.textMuted }));
|
|
70
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: v1.padEnd(16), fg: c1 ?? COLORS.text }));
|
|
71
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: l2.padEnd(12), fg: COLORS.textMuted }));
|
|
72
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: v2, fg: c2 ?? COLORS.text }));
|
|
73
|
+
parent.add(row);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sep(parent: BoxRenderable, renderer: CliRenderer): void {
|
|
77
|
+
parent.add(new TextRenderable(renderer, { id: uid(), content: "\u2500".repeat(72), fg: COLORS.borderDim }));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Public dispatcher ──
|
|
81
|
+
|
|
82
|
+
export function renderToolWidget(toolName: string, data: any, renderer: CliRenderer): BoxRenderable | null {
|
|
83
|
+
switch (toolName) {
|
|
84
|
+
case "polymarketEvents": return renderMarketList(data, renderer);
|
|
85
|
+
case "polymarketEventDetail": return renderEventDetail(data, renderer);
|
|
86
|
+
case "polymarketPriceHistory": return renderPriceChart(data, renderer);
|
|
87
|
+
case "polymarketOrderBook": return renderOrderBook(data, renderer);
|
|
88
|
+
case "whaleTracker": return renderWhaleTracker(data, renderer);
|
|
89
|
+
case "newsSentiment": return renderSentiment(data, renderer);
|
|
90
|
+
case "evCalculator": return renderEV(data, renderer);
|
|
91
|
+
case "kalshiEvents": return renderKalshiList(data, renderer);
|
|
92
|
+
case "compareMarkets": return renderComparison(data, renderer);
|
|
93
|
+
case "historicalVolatility": return renderVolatility(data, renderer);
|
|
94
|
+
case "webSearch": return renderWebSearch(data, renderer);
|
|
95
|
+
case "calaKnowledge": return renderCala(data, renderer);
|
|
96
|
+
case "probabilityCalculator": return renderProbability(data, renderer);
|
|
97
|
+
default: return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ── Market list ──
|
|
102
|
+
// Single wide row per market: # Title Yes 52c No 48c 24h $1.2M liq $850k +2.1%
|
|
103
|
+
|
|
104
|
+
function renderMarketList(data: any, renderer: CliRenderer): BoxRenderable {
|
|
105
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
106
|
+
const events = Array.isArray(data) ? data : (data?.events ?? data?.results ?? []);
|
|
107
|
+
|
|
108
|
+
for (let idx = 0; idx < Math.min(events.length, 8); idx++) {
|
|
109
|
+
const e = events[idx];
|
|
110
|
+
const market = e.markets?.[0];
|
|
111
|
+
const yesPrice = market?.yesPrice ? parseFloat(market.yesPrice) : null;
|
|
112
|
+
const noPrice = market?.noPrice ? parseFloat(market.noPrice) : null;
|
|
113
|
+
const change = e.oneDayPriceChange;
|
|
114
|
+
|
|
115
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
|
|
116
|
+
|
|
117
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${(idx + 1).toString().padStart(2)} `, fg: COLORS.textMuted }));
|
|
118
|
+
|
|
119
|
+
// Title — allow up to 40 chars
|
|
120
|
+
const title = (e.title ?? "").length > 40 ? e.title.slice(0, 40) + "..." : (e.title ?? "").padEnd(43);
|
|
121
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.text, attributes: 1 }));
|
|
122
|
+
|
|
123
|
+
// Yes/No prices inline
|
|
124
|
+
if (yesPrice !== null) row.add(new TextRenderable(renderer, { id: uid(), content: ` Y ${fmtPrice(yesPrice)}`.padEnd(9), fg: COLORS.success }));
|
|
125
|
+
if (noPrice !== null) row.add(new TextRenderable(renderer, { id: uid(), content: `N ${fmtPrice(noPrice)}`.padEnd(8), fg: COLORS.error }));
|
|
126
|
+
|
|
127
|
+
// Volume + liquidity inline
|
|
128
|
+
if (e.volume24hr) row.add(new TextRenderable(renderer, { id: uid(), content: ` ${fmtVol(e.volume24hr).padEnd(7)}`, fg: COLORS.textMuted }));
|
|
129
|
+
if (e.liquidity) row.add(new TextRenderable(renderer, { id: uid(), content: ` liq ${fmtVol(e.liquidity)}`, fg: COLORS.textMuted }));
|
|
130
|
+
|
|
131
|
+
// Change
|
|
132
|
+
if (change !== undefined && change !== null && change !== 0) {
|
|
133
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` ${fmtPct(change)}`, fg: pnlColor(change) }));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
box.add(row);
|
|
137
|
+
|
|
138
|
+
// Slug on a dim second line (compact)
|
|
139
|
+
if (e.slug) {
|
|
140
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: ` ${e.slug}`, fg: COLORS.borderDim }));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return box;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── Event detail ──
|
|
148
|
+
// Title, then markets as wide rows, then stats + sparkline side by side
|
|
149
|
+
|
|
150
|
+
function renderEventDetail(data: any, renderer: CliRenderer): BoxRenderable {
|
|
151
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
152
|
+
|
|
153
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: data.title ?? "", fg: COLORS.text, attributes: 1 }));
|
|
154
|
+
sep(box, renderer);
|
|
155
|
+
|
|
156
|
+
// Markets as wide rows
|
|
157
|
+
for (const m of (data.markets ?? []).slice(0, 5)) {
|
|
158
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
|
|
159
|
+
const q = (m.question ?? "").padEnd(40).slice(0, 40);
|
|
160
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: q, fg: COLORS.textMuted }));
|
|
161
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `Yes ${fmtDollar(m.yesPrice)}`.padEnd(12), fg: COLORS.success }));
|
|
162
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `No ${fmtDollar(m.noPrice)}`.padEnd(12), fg: COLORS.error }));
|
|
163
|
+
if (m.spread !== null && m.spread !== undefined) {
|
|
164
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `spread ${(m.spread * 100).toFixed(1)}%`, fg: COLORS.textMuted }));
|
|
165
|
+
}
|
|
166
|
+
box.add(row);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
sep(box, renderer);
|
|
170
|
+
|
|
171
|
+
// Stats as two columns side by side
|
|
172
|
+
kvPair(box, renderer, "Volume", fmtVol(data.volume ?? 0), undefined, "24h Volume", fmtVol(data.volume24hr ?? 0), undefined);
|
|
173
|
+
kvPair(box, renderer, "Liquidity", fmtVol(data.liquidity ?? 0), undefined, "Ends", data.endDate ? new Date(data.endDate).toLocaleDateString() : "--", undefined);
|
|
174
|
+
|
|
175
|
+
// Wide sparkline
|
|
176
|
+
if (data.priceHistory?.length > 3) {
|
|
177
|
+
const prices = data.priceHistory.map((p: any) => p.p);
|
|
178
|
+
const spark = sparkline(prices, 68);
|
|
179
|
+
const trend = prices[prices.length - 1] >= prices[0];
|
|
180
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: "", fg: COLORS.borderDim }));
|
|
181
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: spark, fg: trend ? COLORS.success : COLORS.error }));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return box;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Price chart ──
|
|
188
|
+
// Wide sparkline + stats row all on one level
|
|
189
|
+
|
|
190
|
+
function renderPriceChart(data: any, renderer: CliRenderer): BoxRenderable {
|
|
191
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
192
|
+
|
|
193
|
+
const title = `${data.marketQuestion ?? data.title} (${data.interval})`;
|
|
194
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.text, attributes: 1 }));
|
|
195
|
+
|
|
196
|
+
if (data.priceHistory?.length > 0) {
|
|
197
|
+
const prices = data.priceHistory.map((p: any) => p.p);
|
|
198
|
+
const trend = (data.change ?? 0) >= 0;
|
|
199
|
+
|
|
200
|
+
// Wide sparkline
|
|
201
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: sparkline(prices, 72), fg: trend ? COLORS.success : COLORS.error }));
|
|
202
|
+
|
|
203
|
+
// All stats on one row
|
|
204
|
+
const stats = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
205
|
+
stats.add(new TextRenderable(renderer, { id: uid(), content: `Current ${fmtDollar(data.current)}`.padEnd(18), fg: COLORS.text }));
|
|
206
|
+
stats.add(new TextRenderable(renderer, { id: uid(), content: `High ${fmtDollar(data.high)}`.padEnd(16), fg: COLORS.success }));
|
|
207
|
+
stats.add(new TextRenderable(renderer, { id: uid(), content: `Low ${fmtDollar(data.low)}`.padEnd(16), fg: COLORS.error }));
|
|
208
|
+
stats.add(new TextRenderable(renderer, { id: uid(), content: `Change ${fmtPct(data.change)}`.padEnd(16), fg: pnlColor(data.change ?? 0) }));
|
|
209
|
+
stats.add(new TextRenderable(renderer, { id: uid(), content: `${data.dataPoints ?? 0} pts`, fg: COLORS.textMuted }));
|
|
210
|
+
box.add(stats);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return box;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Order book ──
|
|
217
|
+
// Wider bars (40 chars), asks and bids side by side if terminal allows, else stacked
|
|
218
|
+
|
|
219
|
+
function renderOrderBook(data: any, renderer: CliRenderer): BoxRenderable {
|
|
220
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
221
|
+
|
|
222
|
+
// Header row: title + spread info inline
|
|
223
|
+
const header = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
224
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: `Order Book -- ${data.outcome ?? "Yes"}`, fg: COLORS.text, attributes: 1 }));
|
|
225
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: ` Bid ${fmtDollar(data.bestBid)}`, fg: COLORS.success }));
|
|
226
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: ` Ask ${fmtDollar(data.bestAsk)}`, fg: COLORS.error }));
|
|
227
|
+
if (data.spread !== null) {
|
|
228
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: ` Spread ${(data.spread * 100).toFixed(2)}%`, fg: COLORS.textMuted }));
|
|
229
|
+
}
|
|
230
|
+
box.add(header);
|
|
231
|
+
sep(box, renderer);
|
|
232
|
+
|
|
233
|
+
const allSizes = [...(data.bids ?? []), ...(data.asks ?? [])].map((l: any) => l.size);
|
|
234
|
+
const maxSize = Math.max(...allSizes, 1);
|
|
235
|
+
const barWidth = 36;
|
|
236
|
+
|
|
237
|
+
// Asks (reversed — highest first)
|
|
238
|
+
for (const a of (data.asks ?? []).slice(0, 6).reverse()) {
|
|
239
|
+
const barLen = Math.round((a.size / maxSize) * barWidth);
|
|
240
|
+
const bar = BAR.repeat(barLen) + " ".repeat(barWidth - barLen);
|
|
241
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
242
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${a.price.toFixed(2).padStart(6)}`, fg: COLORS.error }));
|
|
243
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` ${bar} `, fg: COLORS.error }));
|
|
244
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${a.size.toLocaleString().padStart(8)}`, fg: COLORS.textMuted }));
|
|
245
|
+
box.add(row);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Mid separator
|
|
249
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: `${"─".repeat(barWidth + 18)}`, fg: COLORS.borderDim }));
|
|
250
|
+
|
|
251
|
+
// Bids
|
|
252
|
+
for (const b of (data.bids ?? []).slice(0, 6)) {
|
|
253
|
+
const barLen = Math.round((b.size / maxSize) * barWidth);
|
|
254
|
+
const bar = BAR.repeat(barLen) + " ".repeat(barWidth - barLen);
|
|
255
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
256
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${b.price.toFixed(2).padStart(6)}`, fg: COLORS.success }));
|
|
257
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` ${bar} `, fg: COLORS.success }));
|
|
258
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${b.size.toLocaleString().padStart(8)}`, fg: COLORS.textMuted }));
|
|
259
|
+
box.add(row);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return box;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Whale tracker ──
|
|
266
|
+
// Flow summary inline, trades as wide table rows
|
|
267
|
+
|
|
268
|
+
function renderWhaleTracker(data: any, renderer: CliRenderer): BoxRenderable {
|
|
269
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
270
|
+
|
|
271
|
+
// Title + flow on same line
|
|
272
|
+
const flow = data.flow;
|
|
273
|
+
const header = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
274
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: `Whale Activity -- ${(data.title ?? "").slice(0, 30)}`, fg: COLORS.text, attributes: 1 }));
|
|
275
|
+
if (flow) {
|
|
276
|
+
const color = flow.direction === "inflow" ? COLORS.success : COLORS.error;
|
|
277
|
+
header.add(new TextRenderable(renderer, {
|
|
278
|
+
id: uid(),
|
|
279
|
+
content: ` Net: ${fmtVol(Math.abs(flow.netFlow))} ${flow.direction} (Buy ${fmtVol(flow.buyVolume)} / Sell ${fmtVol(flow.sellVolume)})`,
|
|
280
|
+
fg: color,
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
box.add(header);
|
|
284
|
+
sep(box, renderer);
|
|
285
|
+
|
|
286
|
+
// Trades as wide rows: side name size @ price
|
|
287
|
+
for (const t of (data.largeTrades ?? []).slice(0, 8)) {
|
|
288
|
+
const color = t.side === "BUY" ? COLORS.success : t.side === "SELL" ? COLORS.error : COLORS.textMuted;
|
|
289
|
+
const name = (t.name ?? "Anon").padEnd(18).slice(0, 18);
|
|
290
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
291
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: (t.side ?? "---").padEnd(5), fg: color }));
|
|
292
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: name, fg: COLORS.textMuted }));
|
|
293
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `${(t.size ?? 0).toLocaleString().padStart(10)}`, fg: COLORS.text }));
|
|
294
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` @ ${fmtDollar(t.price)}`, fg: COLORS.textMuted }));
|
|
295
|
+
box.add(row);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return box;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ── Sentiment ──
|
|
302
|
+
// Gauge bar wider, score + signals inline with gauge
|
|
303
|
+
|
|
304
|
+
function renderSentiment(data: any, renderer: CliRenderer): BoxRenderable {
|
|
305
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
306
|
+
const s = data.sentiment;
|
|
307
|
+
|
|
308
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: `Sentiment -- ${(data.title ?? "").slice(0, 50)}`, fg: COLORS.text, attributes: 1 }));
|
|
309
|
+
|
|
310
|
+
if (s) {
|
|
311
|
+
const normalized = (s.score + 1) / 2;
|
|
312
|
+
const width = 50;
|
|
313
|
+
const filled = Math.round(normalized * width);
|
|
314
|
+
const gauge = BAR.repeat(filled) + " ".repeat(width - filled);
|
|
315
|
+
const color = s.score >= 0.1 ? COLORS.success : s.score <= -0.1 ? COLORS.error : COLORS.textMuted;
|
|
316
|
+
|
|
317
|
+
// Gauge + score on same row
|
|
318
|
+
const gaugeRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
319
|
+
gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: "bear ", fg: COLORS.error }));
|
|
320
|
+
gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: gauge, fg: color }));
|
|
321
|
+
gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: ` bull ${s.label} (${s.score > 0 ? "+" : ""}${s.score}) ${s.bullishSignals} bull / ${s.bearishSignals} bear`, fg: color }));
|
|
322
|
+
box.add(gaugeRow);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Articles inline
|
|
326
|
+
if ((data.articles ?? []).length > 0) {
|
|
327
|
+
sep(box, renderer);
|
|
328
|
+
for (const a of data.articles.slice(0, 4)) {
|
|
329
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: ` - ${(a.title ?? "Untitled").slice(0, 80)}`, fg: COLORS.textMuted }));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return box;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── EV Calculator ──
|
|
337
|
+
// Two columns: left = probabilities, right = position sizing
|
|
338
|
+
|
|
339
|
+
function renderEV(data: any, renderer: CliRenderer): BoxRenderable {
|
|
340
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
341
|
+
|
|
342
|
+
const header = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
343
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: `EV Analysis -- ${(data.title ?? "").slice(0, 40)}`, fg: COLORS.text, attributes: 1 }));
|
|
344
|
+
header.add(new TextRenderable(renderer, { id: uid(), content: ` ${data.side?.toUpperCase()} @ ${fmtDollar(data.marketPrice)}`, fg: COLORS.textMuted }));
|
|
345
|
+
box.add(header);
|
|
346
|
+
sep(box, renderer);
|
|
347
|
+
|
|
348
|
+
// Two columns
|
|
349
|
+
kvPair(box, renderer,
|
|
350
|
+
"True Prob", `${(data.trueProb * 100).toFixed(1)}%`, undefined,
|
|
351
|
+
"Edge", `${(data.edge * 100).toFixed(1)}%`, data.edge > 0 ? COLORS.success : COLORS.error,
|
|
352
|
+
);
|
|
353
|
+
kvPair(box, renderer,
|
|
354
|
+
"EV/Dollar", `${(data.evPerDollar * 100).toFixed(1)}%`, pnlColor(data.evPerDollar),
|
|
355
|
+
"Risk/Reward", `${data.riskMetrics?.riskReward?.toFixed(1)}x`, undefined,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const ps = data.positionSizing;
|
|
359
|
+
if (ps) {
|
|
360
|
+
sep(box, renderer);
|
|
361
|
+
kvPair(box, renderer,
|
|
362
|
+
"Full Kelly", `$${ps.fullKelly?.amount?.toFixed(0)} (${(ps.fullKelly?.fraction * 100).toFixed(1)}%)`, undefined,
|
|
363
|
+
"Half Kelly", `$${ps.halfKelly?.amount?.toFixed(0)}`, COLORS.success,
|
|
364
|
+
);
|
|
365
|
+
kv(box, renderer, "Quarter", `$${ps.quarterKelly?.amount?.toFixed(0)}`, COLORS.textMuted);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return box;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ── Kalshi list ──
|
|
372
|
+
// Wide rows: price title (long) category
|
|
373
|
+
|
|
374
|
+
function renderKalshiList(data: any, renderer: CliRenderer): BoxRenderable {
|
|
375
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
376
|
+
const events = Array.isArray(data) ? data : (data?.events ?? []);
|
|
377
|
+
|
|
378
|
+
for (const e of events.slice(0, 8)) {
|
|
379
|
+
const market = e.markets?.[0];
|
|
380
|
+
const price = market ? `$${(market.yesBid / 100).toFixed(2)}` : "--";
|
|
381
|
+
const title = (e.title ?? "").padEnd(55).slice(0, 55);
|
|
382
|
+
|
|
383
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
|
|
384
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: price.padEnd(8), fg: COLORS.text, attributes: 1 }));
|
|
385
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.text }));
|
|
386
|
+
row.add(new BoxRenderable(renderer, { id: uid(), flexGrow: 1 }));
|
|
387
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: e.category ?? "", fg: COLORS.textMuted }));
|
|
388
|
+
box.add(row);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return box;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ── Cross-platform comparison ──
|
|
395
|
+
// One row per pair: Poly price | Kalshi price | Diff | Title
|
|
396
|
+
|
|
397
|
+
function renderComparison(data: any, renderer: CliRenderer): BoxRenderable {
|
|
398
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
399
|
+
|
|
400
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: `Cross-Platform -- "${data.topic}"`, fg: COLORS.text, attributes: 1 }));
|
|
401
|
+
sep(box, renderer);
|
|
402
|
+
|
|
403
|
+
// Header
|
|
404
|
+
const hdr = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
405
|
+
hdr.add(new TextRenderable(renderer, { id: uid(), content: "Polymarket".padEnd(14), fg: COLORS.textMuted }));
|
|
406
|
+
hdr.add(new TextRenderable(renderer, { id: uid(), content: "Kalshi".padEnd(14), fg: COLORS.textMuted }));
|
|
407
|
+
hdr.add(new TextRenderable(renderer, { id: uid(), content: "Diff".padEnd(10), fg: COLORS.textMuted }));
|
|
408
|
+
hdr.add(new TextRenderable(renderer, { id: uid(), content: "Market", fg: COLORS.textMuted }));
|
|
409
|
+
box.add(hdr);
|
|
410
|
+
|
|
411
|
+
for (const c of (data.comparisons ?? []).slice(0, 5)) {
|
|
412
|
+
const diff = c.priceDiff;
|
|
413
|
+
const diffStr = diff !== null ? fmtPct(diff) : "--";
|
|
414
|
+
const diffColor = diff !== null ? (Math.abs(diff) > 0.03 ? COLORS.warning : COLORS.textMuted) : COLORS.textMuted;
|
|
415
|
+
const title = (c.polymarket?.title ?? c.kalshi?.title ?? "").slice(0, 35);
|
|
416
|
+
|
|
417
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
418
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: fmtDollar(c.polymarket?.yesPrice).padEnd(14), fg: COLORS.text }));
|
|
419
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: fmtDollar(c.kalshi?.yesPrice).padEnd(14), fg: COLORS.text }));
|
|
420
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: diffStr.padEnd(10), fg: diffColor }));
|
|
421
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.textMuted }));
|
|
422
|
+
box.add(row);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if ((data.comparisons ?? []).length === 0) {
|
|
426
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: "No matching markets found across platforms", fg: COLORS.textMuted }));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return box;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Volatility ──
|
|
433
|
+
// Stats horizontal, sparkline wide
|
|
434
|
+
|
|
435
|
+
function renderVolatility(data: any, renderer: CliRenderer): BoxRenderable {
|
|
436
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
437
|
+
|
|
438
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: `Volatility -- ${(data.title ?? "").slice(0, 50)}`, fg: COLORS.text, attributes: 1 }));
|
|
439
|
+
|
|
440
|
+
const regime = data.volatilityRegime;
|
|
441
|
+
const regimeColor = regime === "high" ? COLORS.error : regime === "low" ? COLORS.success : COLORS.textMuted;
|
|
442
|
+
|
|
443
|
+
// All stats on one row
|
|
444
|
+
const statsRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
445
|
+
statsRow.add(new TextRenderable(renderer, { id: uid(), content: `Regime ${(regime ?? "?").toUpperCase()}`.padEnd(18), fg: regimeColor }));
|
|
446
|
+
statsRow.add(new TextRenderable(renderer, { id: uid(), content: `Ann. Vol ${((data.realizedVolatility ?? 0) * 100).toFixed(1)}%`.padEnd(20), fg: COLORS.text }));
|
|
447
|
+
statsRow.add(new TextRenderable(renderer, { id: uid(), content: `Daily StdDev ${((data.dailyStdDev ?? 0) * 100).toFixed(3)}%`, fg: COLORS.text }));
|
|
448
|
+
box.add(statsRow);
|
|
449
|
+
|
|
450
|
+
if (data.priceHistory?.length > 3) {
|
|
451
|
+
const prices = data.priceHistory.map((p: any) => p.p);
|
|
452
|
+
const trend = prices[prices.length - 1] >= prices[0];
|
|
453
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: sparkline(prices, 72), fg: trend ? COLORS.success : COLORS.error }));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return box;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ── Web search ──
|
|
460
|
+
|
|
461
|
+
function renderWebSearch(data: any, renderer: CliRenderer): BoxRenderable {
|
|
462
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
463
|
+
const results = Array.isArray(data) ? data : (data?.results ?? []);
|
|
464
|
+
|
|
465
|
+
for (const r of results.slice(0, 6)) {
|
|
466
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
|
|
467
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` - ${(r.title ?? "Untitled").slice(0, 70)}`, fg: COLORS.text }));
|
|
468
|
+
if (r.snippet) {
|
|
469
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: ` ${(r.snippet ?? "").slice(0, 50)}`, fg: COLORS.textMuted }));
|
|
470
|
+
}
|
|
471
|
+
box.add(row);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return box;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Cala knowledge ──
|
|
478
|
+
|
|
479
|
+
function renderCala(data: any, renderer: CliRenderer): BoxRenderable {
|
|
480
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
481
|
+
|
|
482
|
+
if (data.content) {
|
|
483
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: (data.content as string).slice(0, 800), fg: COLORS.text }));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (data.sources?.length > 0) {
|
|
487
|
+
sep(box, renderer);
|
|
488
|
+
for (const s of data.sources.slice(0, 3)) {
|
|
489
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: ` - ${(s.fact ?? s.source ?? "").slice(0, 90)}`, fg: COLORS.textMuted }));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return box;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Probability ──
|
|
497
|
+
// All on two rows
|
|
498
|
+
|
|
499
|
+
function renderProbability(data: any, renderer: CliRenderer): BoxRenderable {
|
|
500
|
+
const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
|
|
501
|
+
|
|
502
|
+
box.add(new TextRenderable(renderer, { id: uid(), content: "Probability Analysis", fg: COLORS.text, attributes: 1 }));
|
|
503
|
+
sep(box, renderer);
|
|
504
|
+
|
|
505
|
+
if (data.marketA && data.marketB) {
|
|
506
|
+
kvPair(box, renderer,
|
|
507
|
+
"A", `${data.marketA.title} (${(data.marketA.probability * 100).toFixed(0)}%)`, undefined,
|
|
508
|
+
"B", `${data.marketB.title} (${(data.marketB.probability * 100).toFixed(0)}%)`, undefined,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const joint = `${((data.jointProbability ?? 0) * 100).toFixed(1)}%`;
|
|
513
|
+
const pAB = data.pAGivenB !== null ? `${((data.pAGivenB ?? 0) * 100).toFixed(1)}%` : "--";
|
|
514
|
+
const pBA = data.pBGivenA !== null ? `${((data.pBGivenA ?? 0) * 100).toFixed(1)}%` : "--";
|
|
515
|
+
|
|
516
|
+
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
517
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `Joint ${joint}`.padEnd(20), fg: COLORS.text }));
|
|
518
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `P(A|B) ${pAB}`.padEnd(20), fg: COLORS.text }));
|
|
519
|
+
row.add(new TextRenderable(renderer, { id: uid(), content: `P(B|A) ${pBA}`, fg: COLORS.text }));
|
|
520
|
+
box.add(row);
|
|
521
|
+
|
|
522
|
+
return box;
|
|
523
|
+
}
|