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.
Files changed (54) hide show
  1. package/assets/python/highlights.scm +137 -0
  2. package/assets/python/tree-sitter-python.wasm +0 -0
  3. package/bin/horizon.js +2 -0
  4. package/package.json +40 -0
  5. package/src/ai/client.ts +369 -0
  6. package/src/ai/system-prompt.ts +86 -0
  7. package/src/app.ts +1454 -0
  8. package/src/chat/messages.ts +48 -0
  9. package/src/chat/renderer.ts +243 -0
  10. package/src/chat/types.ts +18 -0
  11. package/src/components/code-panel.ts +329 -0
  12. package/src/components/footer.ts +72 -0
  13. package/src/components/hooks-panel.ts +224 -0
  14. package/src/components/input-bar.ts +193 -0
  15. package/src/components/mode-bar.ts +245 -0
  16. package/src/components/session-panel.ts +294 -0
  17. package/src/components/settings-panel.ts +372 -0
  18. package/src/components/splash.ts +156 -0
  19. package/src/components/strategy-panel.ts +489 -0
  20. package/src/components/tab-bar.ts +112 -0
  21. package/src/components/tutorial-panel.ts +680 -0
  22. package/src/components/widgets/progress-bar.ts +38 -0
  23. package/src/components/widgets/sparkline.ts +57 -0
  24. package/src/hooks/executor.ts +109 -0
  25. package/src/index.ts +22 -0
  26. package/src/keys/handler.ts +198 -0
  27. package/src/platform/auth.ts +36 -0
  28. package/src/platform/client.ts +159 -0
  29. package/src/platform/config.ts +121 -0
  30. package/src/platform/session-sync.ts +158 -0
  31. package/src/platform/supabase.ts +376 -0
  32. package/src/platform/sync.ts +149 -0
  33. package/src/platform/tiers.ts +103 -0
  34. package/src/platform/tools.ts +163 -0
  35. package/src/platform/types.ts +86 -0
  36. package/src/platform/usage.ts +224 -0
  37. package/src/research/apis.ts +367 -0
  38. package/src/research/tools.ts +205 -0
  39. package/src/research/widgets.ts +523 -0
  40. package/src/state/store.ts +256 -0
  41. package/src/state/types.ts +109 -0
  42. package/src/strategy/ascii-chart.ts +74 -0
  43. package/src/strategy/code-stream.ts +146 -0
  44. package/src/strategy/dashboard.ts +140 -0
  45. package/src/strategy/persistence.ts +82 -0
  46. package/src/strategy/prompts.ts +626 -0
  47. package/src/strategy/sandbox.ts +137 -0
  48. package/src/strategy/tools.ts +764 -0
  49. package/src/strategy/validator.ts +216 -0
  50. package/src/strategy/widgets.ts +270 -0
  51. package/src/syntax/setup.ts +54 -0
  52. package/src/theme/colors.ts +107 -0
  53. package/src/theme/icons.ts +27 -0
  54. 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
+ }