horizon-code 0.1.2 → 0.3.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.
@@ -7,6 +7,217 @@ import { COLORS } from "../theme/colors.ts";
7
7
  const SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
8
8
  const BAR = "\u2588";
9
9
 
10
+ // Exchange brand colors
11
+ const EXCHANGE_COLORS: Record<string, string> = {
12
+ polymarket: "#4A90D9", // blue
13
+ kalshi: "#22C55E", // green
14
+ stock: "#A855F7", // purple
15
+ yahoo: "#A855F7", // purple
16
+ cross: "#F59E0B", // amber
17
+ analysis: "#6366F1", // indigo
18
+ };
19
+
20
+ /** Format a price for chart Y-axis labels */
21
+ function fmtChartLabel(v: number): string {
22
+ return v >= 1000 ? `$${v.toFixed(0)}` : v >= 1 ? `$${v.toFixed(2)}` : `${(v * 100).toFixed(1)}c`;
23
+ }
24
+
25
+ interface ChartResult {
26
+ lines: string[];
27
+ colors: string[][]; // per-character color for each line
28
+ labels: { high: string; mid: string; low: string };
29
+ }
30
+
31
+ /**
32
+ * Multi-line ASCII candlestick chart.
33
+ * Each candle is 3 chars wide: ╻ upper wick
34
+ * ┃ body (bullish=filled, bearish=hollow)
35
+ * ╹ lower wick
36
+ * Bullish body: ┃ (solid) Bearish body: ┇ (dashed)
37
+ * Wicks: │ (thin line)
38
+ * 1 char gap between candles → each candle takes 4 cols total
39
+ */
40
+ function candlestickChart(
41
+ candles: { o: number; h: number; l: number; c: number }[],
42
+ maxCandles: number, height: number,
43
+ ): ChartResult {
44
+ if (candles.length < 2) return { lines: [], colors: [], labels: { high: "", mid: "", low: "" } };
45
+
46
+ // Sample down to maxCandles
47
+ const step = Math.max(1, Math.floor(candles.length / maxCandles));
48
+ const sampled: typeof candles = [];
49
+ for (let i = 0; i < candles.length; i += step) {
50
+ if (sampled.length >= maxCandles) break;
51
+ sampled.push(candles[i]!);
52
+ }
53
+
54
+ const allPrices = sampled.flatMap(c => [c.h, c.l]).filter(v => isFinite(v));
55
+ if (allPrices.length === 0) return { lines: [], colors: [], labels: { high: "", mid: "", low: "" } };
56
+ const min = Math.min(...allPrices);
57
+ const max = Math.max(...allPrices);
58
+ const range = max - min || 1;
59
+
60
+ const toRow = (price: number) => {
61
+ const r = height - 1 - Math.round(((price - min) / range) * (height - 1));
62
+ return Math.max(0, Math.min(height - 1, r));
63
+ };
64
+
65
+ // Each candle = 3 cols (space + body + space), builds a char grid
66
+ const totalWidth = sampled.length * 2; // 1 col body + 1 col gap
67
+ const grid: string[][] = [];
68
+ const colorGrid: string[][] = [];
69
+ for (let r = 0; r < height; r++) {
70
+ grid.push(new Array(totalWidth).fill(" "));
71
+ colorGrid.push(new Array(totalWidth).fill(""));
72
+ }
73
+
74
+ for (let i = 0; i < sampled.length; i++) {
75
+ const c = sampled[i]!;
76
+ const col = i * 2; // body column (gap column = col+1, stays space)
77
+ const bull = c.c >= c.o;
78
+ const color = bull ? COLORS.success : COLORS.error;
79
+
80
+ const bodyHi = toRow(Math.max(c.o, c.c));
81
+ const bodyLo = toRow(Math.min(c.o, c.c));
82
+ const wickHi = toRow(c.h);
83
+ const wickLo = toRow(c.l);
84
+
85
+ // Upper wick
86
+ for (let r = wickHi; r < bodyHi; r++) {
87
+ grid[r]![col] = "│";
88
+ colorGrid[r]![col] = color;
89
+ }
90
+
91
+ // Body
92
+ if (bodyHi === bodyLo) {
93
+ // Doji — open ≈ close
94
+ grid[bodyHi]![col] = "─";
95
+ colorGrid[bodyHi]![col] = color;
96
+ } else {
97
+ // Top of body
98
+ grid[bodyHi]![col] = bull ? "┎" : "┒";
99
+ colorGrid[bodyHi]![col] = color;
100
+ // Middle of body
101
+ for (let r = bodyHi + 1; r < bodyLo; r++) {
102
+ grid[r]![col] = bull ? "┃" : "┊";
103
+ colorGrid[r]![col] = color;
104
+ }
105
+ // Bottom of body
106
+ if (bodyLo > bodyHi) {
107
+ grid[bodyLo]![col] = bull ? "┖" : "┚";
108
+ colorGrid[bodyLo]![col] = color;
109
+ }
110
+ }
111
+
112
+ // Lower wick
113
+ for (let r = bodyLo + 1; r <= wickLo; r++) {
114
+ grid[r]![col] = "│";
115
+ colorGrid[r]![col] = color;
116
+ }
117
+ }
118
+
119
+ return {
120
+ lines: grid.map(row => row.join("")),
121
+ colors: colorGrid,
122
+ labels: { high: fmtChartLabel(max), mid: fmtChartLabel((max + min) / 2), low: fmtChartLabel(min) },
123
+ };
124
+ }
125
+
126
+ /** Multi-line ASCII area chart — renders price data as a filled line */
127
+ function areaChart(values: number[], width: number, height: number): ChartResult {
128
+ if (values.length < 2) return { lines: [], colors: [], labels: { high: "", mid: "", low: "" } };
129
+
130
+ const sampled: number[] = [];
131
+ for (let i = 0; i < width; i++) {
132
+ const idx = Math.floor((i / width) * values.length);
133
+ sampled.push(values[Math.min(idx, values.length - 1)] ?? 0);
134
+ }
135
+
136
+ const min = Math.min(...sampled);
137
+ const max = Math.max(...sampled);
138
+ const range = max - min || 1;
139
+ const trend = sampled[sampled.length - 1] >= sampled[0];
140
+ const color = trend ? COLORS.success : COLORS.error;
141
+
142
+ const grid: string[][] = [];
143
+ const colorGrid: string[][] = [];
144
+ for (let r = 0; r < height; r++) {
145
+ grid.push(new Array(width).fill(" "));
146
+ colorGrid.push(new Array(width).fill(""));
147
+ }
148
+
149
+ for (let x = 0; x < width; x++) {
150
+ const y = Math.round(((sampled[x] - min) / range) * (height - 1));
151
+ const row = height - 1 - y;
152
+ if (row >= 0 && row < height) {
153
+ grid[row]![x] = "█"; colorGrid[row]![x] = color;
154
+ }
155
+ for (let r = row + 1; r < height; r++) {
156
+ grid[r]![x] = "░"; colorGrid[r]![x] = color;
157
+ }
158
+ }
159
+
160
+ return {
161
+ lines: grid.map(row => row.join("")),
162
+ colors: colorGrid,
163
+ labels: { high: fmtChartLabel(max), mid: fmtChartLabel((max + min) / 2), low: fmtChartLabel(min) },
164
+ };
165
+ }
166
+
167
+ /** Render a ChartResult into the box with Y-axis labels and border */
168
+ function renderChart(box: BoxRenderable, renderer: CliRenderer, chart: ChartResult, defaultColor: string): void {
169
+ if (chart.lines.length === 0) return;
170
+
171
+ const topRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
172
+ topRow.add(new TextRenderable(renderer, { id: uid(), content: chart.labels.high.padEnd(10), fg: COLORS.textMuted }));
173
+ topRow.add(new TextRenderable(renderer, { id: uid(), content: "┐", fg: COLORS.borderDim }));
174
+ box.add(topRow);
175
+
176
+ for (let i = 0; i < chart.lines.length; i++) {
177
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
178
+ if (i === Math.floor(chart.lines.length / 2)) {
179
+ row.add(new TextRenderable(renderer, { id: uid(), content: chart.labels.mid.padEnd(10), fg: COLORS.textMuted }));
180
+ } else {
181
+ row.add(new TextRenderable(renderer, { id: uid(), content: " ", fg: COLORS.textMuted }));
182
+ }
183
+ row.add(new TextRenderable(renderer, { id: uid(), content: "│", fg: COLORS.borderDim }));
184
+
185
+ // If we have per-char colors (candlestick), render segments
186
+ if (chart.colors.length > 0 && chart.colors[i]) {
187
+ const lineChars = chart.lines[i]!;
188
+ const lineColors = chart.colors[i]!;
189
+ // Group consecutive same-color chars
190
+ let seg = "", segColor = lineColors[0] || defaultColor;
191
+ for (let j = 0; j < lineChars.length; j++) {
192
+ const c = lineColors[j] || defaultColor;
193
+ if (c !== segColor) {
194
+ if (seg) row.add(new TextRenderable(renderer, { id: uid(), content: seg, fg: segColor || defaultColor }));
195
+ seg = ""; segColor = c;
196
+ }
197
+ seg += lineChars[j];
198
+ }
199
+ if (seg) row.add(new TextRenderable(renderer, { id: uid(), content: seg, fg: segColor || defaultColor }));
200
+ } else {
201
+ row.add(new TextRenderable(renderer, { id: uid(), content: chart.lines[i]!, fg: defaultColor }));
202
+ }
203
+ box.add(row);
204
+ }
205
+
206
+ const botRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
207
+ botRow.add(new TextRenderable(renderer, { id: uid(), content: chart.labels.low.padEnd(10), fg: COLORS.textMuted }));
208
+ botRow.add(new TextRenderable(renderer, { id: uid(), content: "┘", fg: COLORS.borderDim }));
209
+ box.add(botRow);
210
+ }
211
+
212
+ function exchangeBadge(parent: BoxRenderable, renderer: CliRenderer, exchange: string, title: string): void {
213
+ const color = EXCHANGE_COLORS[exchange] ?? COLORS.textMuted;
214
+ const label = exchange.toUpperCase();
215
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
216
+ row.add(new TextRenderable(renderer, { id: uid(), content: `[${label}]`, fg: color, attributes: 1 }));
217
+ row.add(new TextRenderable(renderer, { id: uid(), content: ` ${title}`, fg: COLORS.text, attributes: 1 }));
218
+ parent.add(row);
219
+ }
220
+
10
221
  let widgetCounter = 0;
11
222
  function uid(): string { return `w-${Date.now()}-${widgetCounter++}`; }
12
223
 
@@ -94,6 +305,21 @@ export function renderToolWidget(toolName: string, data: any, renderer: CliRende
94
305
  case "webSearch": return renderWebSearch(data, renderer);
95
306
  case "calaKnowledge": return renderCala(data, renderer);
96
307
  case "probabilityCalculator": return renderProbability(data, renderer);
308
+ case "kalshiEventDetail": return renderKalshiDetail(data, renderer);
309
+ case "kalshiOrderBook": return renderKalshiOrderBook(data, renderer);
310
+ case "kalshiPriceHistory": return renderKalshiPriceChart(data, renderer);
311
+ case "marketMicrostructure": return renderMicrostructure(data, renderer);
312
+ case "liquidityScanner": return renderLiquidityScanner(data, renderer);
313
+ case "correlationAnalysis": return renderCorrelation(data, renderer);
314
+ case "walletProfiler": return renderWalletProfile(data, renderer);
315
+ case "botDetector": return renderBotDetector(data, renderer);
316
+ case "marketFlow": return renderMarketFlow(data, renderer);
317
+ case "riskMetrics": return renderRiskMetrics(data, renderer);
318
+ case "marketRegime": return renderMarketRegime(data, renderer);
319
+ case "stockQuote": return renderStockQuote(data, renderer);
320
+ case "stockChart": return renderStockChart(data, renderer);
321
+ case "stockSearch": return renderStockSearch(data, renderer);
322
+ case "stockScreener": return renderStockScreener(data, renderer);
97
323
  default: return null;
98
324
  }
99
325
  }
@@ -103,6 +329,7 @@ export function renderToolWidget(toolName: string, data: any, renderer: CliRende
103
329
 
104
330
  function renderMarketList(data: any, renderer: CliRenderer): BoxRenderable {
105
331
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
332
+ exchangeBadge(box, renderer, "polymarket", "Markets");
106
333
  const events = Array.isArray(data) ? data : (data?.events ?? data?.results ?? []);
107
334
 
108
335
  for (let idx = 0; idx < Math.min(events.length, 8); idx++) {
@@ -150,7 +377,7 @@ function renderMarketList(data: any, renderer: CliRenderer): BoxRenderable {
150
377
  function renderEventDetail(data: any, renderer: CliRenderer): BoxRenderable {
151
378
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
152
379
 
153
- box.add(new TextRenderable(renderer, { id: uid(), content: data.title ?? "", fg: COLORS.text, attributes: 1 }));
380
+ exchangeBadge(box, renderer, "polymarket", data.title ?? "");
154
381
  sep(box, renderer);
155
382
 
156
383
  // Markets as wide rows
@@ -190,17 +417,15 @@ function renderEventDetail(data: any, renderer: CliRenderer): BoxRenderable {
190
417
  function renderPriceChart(data: any, renderer: CliRenderer): BoxRenderable {
191
418
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
192
419
 
193
- const title = `${data.marketQuestion ?? data.title} (${data.interval})`;
194
- box.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.text, attributes: 1 }));
420
+ exchangeBadge(box, renderer, "polymarket", `${data.marketQuestion ?? data.title} (${data.interval})`);
195
421
 
196
422
  if (data.priceHistory?.length > 0) {
197
- const prices = data.priceHistory.map((p: any) => p.p);
423
+ const prices = data.priceHistory.map((p: any) => p.p).filter((p: any) => p != null);
198
424
  const trend = (data.change ?? 0) >= 0;
199
425
 
200
- // Wide sparkline
201
- box.add(new TextRenderable(renderer, { id: uid(), content: sparkline(prices, 72), fg: trend ? COLORS.success : COLORS.error }));
426
+ const chart = areaChart(prices, 64, 10);
427
+ renderChart(box, renderer, chart, trend ? COLORS.success : COLORS.error);
202
428
 
203
- // All stats on one row
204
429
  const stats = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
205
430
  stats.add(new TextRenderable(renderer, { id: uid(), content: `Current ${fmtDollar(data.current)}`.padEnd(18), fg: COLORS.text }));
206
431
  stats.add(new TextRenderable(renderer, { id: uid(), content: `High ${fmtDollar(data.high)}`.padEnd(16), fg: COLORS.success }));
@@ -208,6 +433,8 @@ function renderPriceChart(data: any, renderer: CliRenderer): BoxRenderable {
208
433
  stats.add(new TextRenderable(renderer, { id: uid(), content: `Change ${fmtPct(data.change)}`.padEnd(16), fg: pnlColor(data.change ?? 0) }));
209
434
  stats.add(new TextRenderable(renderer, { id: uid(), content: `${data.dataPoints ?? 0} pts`, fg: COLORS.textMuted }));
210
435
  box.add(stats);
436
+
437
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Timeframes: 1h 6h 1d 1w 1m max", fg: COLORS.borderDim }));
211
438
  }
212
439
 
213
440
  return box;
@@ -219,13 +446,12 @@ function renderPriceChart(data: any, renderer: CliRenderer): BoxRenderable {
219
446
  function renderOrderBook(data: any, renderer: CliRenderer): BoxRenderable {
220
447
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
221
448
 
222
- // Header row: title + spread info inline
449
+ exchangeBadge(box, renderer, "polymarket", `Order Book ${data.outcome ?? "Yes"}`);
223
450
  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 }));
451
+ header.add(new TextRenderable(renderer, { id: uid(), content: `Bid ${fmtDollar(data.bestBid)}`, fg: COLORS.success }));
226
452
  header.add(new TextRenderable(renderer, { id: uid(), content: ` Ask ${fmtDollar(data.bestAsk)}`, fg: COLORS.error }));
227
453
  if (data.spread !== null) {
228
- header.add(new TextRenderable(renderer, { id: uid(), content: ` Spread ${(data.spread * 100).toFixed(2)}%`, fg: COLORS.textMuted }));
454
+ header.add(new TextRenderable(renderer, { id: uid(), content: ` Spread ${((data.spread ?? 0) * 100).toFixed(2)}%`, fg: COLORS.textMuted }));
229
455
  }
230
456
  box.add(header);
231
457
  sep(box, renderer);
@@ -268,10 +494,9 @@ function renderOrderBook(data: any, renderer: CliRenderer): BoxRenderable {
268
494
  function renderWhaleTracker(data: any, renderer: CliRenderer): BoxRenderable {
269
495
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
270
496
 
271
- // Title + flow on same line
497
+ exchangeBadge(box, renderer, "polymarket", `Whales ${(data.title ?? "").slice(0, 35)}`);
272
498
  const flow = data.flow;
273
499
  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
500
  if (flow) {
276
501
  const color = flow.direction === "inflow" ? COLORS.success : COLORS.error;
277
502
  header.add(new TextRenderable(renderer, {
@@ -305,7 +530,7 @@ function renderSentiment(data: any, renderer: CliRenderer): BoxRenderable {
305
530
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
306
531
  const s = data.sentiment;
307
532
 
308
- box.add(new TextRenderable(renderer, { id: uid(), content: `Sentiment -- ${(data.title ?? "").slice(0, 50)}`, fg: COLORS.text, attributes: 1 }));
533
+ exchangeBadge(box, renderer, "polymarket", `Sentiment ${(data.title ?? "").slice(0, 42)}`);
309
534
 
310
535
  if (s) {
311
536
  const normalized = (s.score + 1) / 2;
@@ -339,10 +564,8 @@ function renderSentiment(data: any, renderer: CliRenderer): BoxRenderable {
339
564
  function renderEV(data: any, renderer: CliRenderer): BoxRenderable {
340
565
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
341
566
 
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);
567
+ exchangeBadge(box, renderer, "analysis", `EV Analysis — ${(data.title ?? "").slice(0, 40)}`);
568
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${data.side?.toUpperCase()} @ ${fmtDollar(data.marketPrice)}`, fg: COLORS.textMuted }));
346
569
  sep(box, renderer);
347
570
 
348
571
  // Two columns
@@ -373,19 +596,29 @@ function renderEV(data: any, renderer: CliRenderer): BoxRenderable {
373
596
 
374
597
  function renderKalshiList(data: any, renderer: CliRenderer): BoxRenderable {
375
598
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
599
+ exchangeBadge(box, renderer, "kalshi", "Markets");
376
600
  const events = Array.isArray(data) ? data : (data?.events ?? []);
377
601
 
378
- for (const e of events.slice(0, 8)) {
602
+ for (let idx = 0; idx < Math.min(events.length, 8); idx++) {
603
+ const e = events[idx];
379
604
  const market = e.markets?.[0];
380
- const price = market ? `$${(market.yesBid / 100).toFixed(2)}` : "--";
381
- const title = (e.title ?? "").padEnd(55).slice(0, 55);
605
+ const yesBid = market?.yesBid ?? market?.lastPrice ?? null;
606
+ const yesAsk = market?.yesAsk ?? null;
607
+ const price = yesBid != null ? `${yesBid}c` : "--";
382
608
 
383
609
  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 }));
610
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${(idx + 1).toString().padStart(2)} `, fg: COLORS.textMuted }));
611
+ row.add(new TextRenderable(renderer, { id: uid(), content: (e.title ?? "").slice(0, 40).padEnd(43), fg: COLORS.text, attributes: 1 }));
612
+ row.add(new TextRenderable(renderer, { id: uid(), content: ` Y ${price}`.padEnd(10), fg: COLORS.success }));
613
+ if (yesAsk != null) row.add(new TextRenderable(renderer, { id: uid(), content: `A ${yesAsk}c`.padEnd(8), fg: COLORS.error }));
614
+ if (market?.volume) row.add(new TextRenderable(renderer, { id: uid(), content: ` vol ${market.volume.toLocaleString()}`, fg: COLORS.textMuted }));
386
615
  row.add(new BoxRenderable(renderer, { id: uid(), flexGrow: 1 }));
387
616
  row.add(new TextRenderable(renderer, { id: uid(), content: e.category ?? "", fg: COLORS.textMuted }));
388
617
  box.add(row);
618
+
619
+ if (e.ticker) {
620
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${e.ticker}`, fg: COLORS.borderDim }));
621
+ }
389
622
  }
390
623
 
391
624
  return box;
@@ -397,7 +630,7 @@ function renderKalshiList(data: any, renderer: CliRenderer): BoxRenderable {
397
630
  function renderComparison(data: any, renderer: CliRenderer): BoxRenderable {
398
631
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
399
632
 
400
- box.add(new TextRenderable(renderer, { id: uid(), content: `Cross-Platform -- "${data.topic}"`, fg: COLORS.text, attributes: 1 }));
633
+ exchangeBadge(box, renderer, "cross", `Cross-Platform "${data.topic}"`);
401
634
  sep(box, renderer);
402
635
 
403
636
  // Header
@@ -435,18 +668,29 @@ function renderComparison(data: any, renderer: CliRenderer): BoxRenderable {
435
668
  function renderVolatility(data: any, renderer: CliRenderer): BoxRenderable {
436
669
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
437
670
 
438
- box.add(new TextRenderable(renderer, { id: uid(), content: `Volatility -- ${(data.title ?? "").slice(0, 50)}`, fg: COLORS.text, attributes: 1 }));
671
+ exchangeBadge(box, renderer, "analysis", `Volatility ${(data.title ?? "").slice(0, 42)}`);
439
672
 
440
673
  const regime = data.volatilityRegime;
441
674
  const regimeColor = regime === "high" ? COLORS.error : regime === "low" ? COLORS.success : COLORS.textMuted;
442
675
 
443
- // All stats on one row
676
+ // Regime + best estimate row
444
677
  const statsRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
445
678
  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 }));
679
+ statsRow.add(new TextRenderable(renderer, { id: uid(), content: `Best ${((data.realizedVolatility ?? 0) * 100).toFixed(1)}%`.padEnd(16), fg: COLORS.text }));
680
+ statsRow.add(new TextRenderable(renderer, { id: uid(), content: `Recent ${((data.recentVolatility ?? 0) * 100).toFixed(1)}%`, fg: regimeColor }));
448
681
  box.add(statsRow);
449
682
 
683
+ // Multi-estimator breakdown
684
+ const est = data.estimators;
685
+ if (est) {
686
+ const estRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
687
+ estRow.add(new TextRenderable(renderer, { id: uid(), content: `C2C ${(est.closeToClose * 100).toFixed(1)}%`.padEnd(14), fg: COLORS.textMuted }));
688
+ estRow.add(new TextRenderable(renderer, { id: uid(), content: `Parkinson ${(est.parkinson * 100).toFixed(1)}%`.padEnd(18), fg: COLORS.textMuted }));
689
+ estRow.add(new TextRenderable(renderer, { id: uid(), content: `G-K ${(est.garmanKlass * 100).toFixed(1)}%`.padEnd(14), fg: COLORS.textMuted }));
690
+ estRow.add(new TextRenderable(renderer, { id: uid(), content: `EWMA ${(est.ewma * 100).toFixed(1)}%`, fg: COLORS.textMuted }));
691
+ box.add(estRow);
692
+ }
693
+
450
694
  if (data.priceHistory?.length > 3) {
451
695
  const prices = data.priceHistory.map((p: any) => p.p);
452
696
  const trend = prices[prices.length - 1] >= prices[0];
@@ -499,7 +743,7 @@ function renderCala(data: any, renderer: CliRenderer): BoxRenderable {
499
743
  function renderProbability(data: any, renderer: CliRenderer): BoxRenderable {
500
744
  const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
501
745
 
502
- box.add(new TextRenderable(renderer, { id: uid(), content: "Probability Analysis", fg: COLORS.text, attributes: 1 }));
746
+ exchangeBadge(box, renderer, "analysis", "Probability Analysis");
503
747
  sep(box, renderer);
504
748
 
505
749
  if (data.marketA && data.marketB) {
@@ -521,3 +765,772 @@ function renderProbability(data: any, renderer: CliRenderer): BoxRenderable {
521
765
 
522
766
  return box;
523
767
  }
768
+
769
+ // ── Kalshi event detail ──
770
+
771
+ function renderKalshiDetail(data: any, renderer: CliRenderer): BoxRenderable {
772
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
773
+
774
+ exchangeBadge(box, renderer, "kalshi", data.title ?? data.ticker ?? "");
775
+ if (data.category) {
776
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${data.category} ${data.ticker ?? ""}`, fg: COLORS.textMuted }));
777
+ }
778
+ sep(box, renderer);
779
+
780
+ for (const m of (data.markets ?? []).slice(0, 8)) {
781
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
782
+ const title = (m.title ?? m.ticker ?? "").padEnd(40).slice(0, 40);
783
+ row.add(new TextRenderable(renderer, { id: uid(), content: title, fg: COLORS.textMuted }));
784
+ const bid = m.yesBid ?? m.lastPrice ?? null;
785
+ const ask = m.yesAsk ?? null;
786
+ row.add(new TextRenderable(renderer, { id: uid(), content: `Bid ${bid != null ? bid + "c" : "--"}`.padEnd(10), fg: COLORS.success }));
787
+ row.add(new TextRenderable(renderer, { id: uid(), content: `Ask ${ask != null ? ask + "c" : "--"}`.padEnd(10), fg: COLORS.error }));
788
+ if (m.spread != null) {
789
+ row.add(new TextRenderable(renderer, { id: uid(), content: `spread ${m.spread}c`.padEnd(14), fg: COLORS.textMuted }));
790
+ }
791
+ if (m.volume) {
792
+ row.add(new TextRenderable(renderer, { id: uid(), content: `vol ${m.volume.toLocaleString()}`, fg: COLORS.textMuted }));
793
+ }
794
+ box.add(row);
795
+ }
796
+
797
+ return box;
798
+ }
799
+
800
+ // ── Kalshi order book ──
801
+
802
+ function renderKalshiOrderBook(data: any, renderer: CliRenderer): BoxRenderable {
803
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
804
+
805
+ exchangeBadge(box, renderer, "kalshi", `Order Book — ${data.ticker ?? ""}`);
806
+ const header = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
807
+ header.add(new TextRenderable(renderer, { id: uid(), content: `Bid ${fmtDollar(data.bestBid)}`, fg: COLORS.success }));
808
+ header.add(new TextRenderable(renderer, { id: uid(), content: ` Ask ${fmtDollar(data.bestAsk)}`, fg: COLORS.error }));
809
+ if (data.spread != null) {
810
+ header.add(new TextRenderable(renderer, { id: uid(), content: ` Spread ${((data.spread ?? 0) * 100).toFixed(2)}%`, fg: COLORS.textMuted }));
811
+ }
812
+ box.add(header);
813
+ sep(box, renderer);
814
+
815
+ const allSizes = [...(data.bids ?? []), ...(data.asks ?? [])].map((l: any) => l.size);
816
+ const maxSize = Math.max(...allSizes, 1);
817
+ const barWidth = 36;
818
+
819
+ for (const a of (data.asks ?? []).slice(0, 6).reverse()) {
820
+ const barLen = Math.round((a.size / maxSize) * barWidth);
821
+ const bar = BAR.repeat(barLen) + " ".repeat(barWidth - barLen);
822
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
823
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${a.price.toFixed(2).padStart(6)}`, fg: COLORS.error }));
824
+ row.add(new TextRenderable(renderer, { id: uid(), content: ` ${bar} `, fg: COLORS.error }));
825
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${a.size.toLocaleString().padStart(8)}`, fg: COLORS.textMuted }));
826
+ box.add(row);
827
+ }
828
+
829
+ box.add(new TextRenderable(renderer, { id: uid(), content: `${"─".repeat(barWidth + 18)}`, fg: COLORS.borderDim }));
830
+
831
+ for (const b of (data.bids ?? []).slice(0, 6)) {
832
+ const barLen = Math.round((b.size / maxSize) * barWidth);
833
+ const bar = BAR.repeat(barLen) + " ".repeat(barWidth - barLen);
834
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
835
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${b.price.toFixed(2).padStart(6)}`, fg: COLORS.success }));
836
+ row.add(new TextRenderable(renderer, { id: uid(), content: ` ${bar} `, fg: COLORS.success }));
837
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${b.size.toLocaleString().padStart(8)}`, fg: COLORS.textMuted }));
838
+ box.add(row);
839
+ }
840
+
841
+ return box;
842
+ }
843
+
844
+ // ── Kalshi price chart ──
845
+
846
+ function renderKalshiPriceChart(data: any, renderer: CliRenderer): BoxRenderable {
847
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
848
+
849
+ exchangeBadge(box, renderer, "kalshi", `${data.ticker ?? ""} (${data.interval})`);
850
+
851
+ if (data.priceHistory?.length > 0) {
852
+ const history = data.priceHistory.filter((p: any) => p.p != null);
853
+ const trend = (data.change ?? 0) >= 0;
854
+
855
+ // Use candlestick if OHLC available
856
+ const hasOHLC = history.some((p: any) => p.open != null && p.close != null);
857
+ let chart: ChartResult;
858
+ if (hasOHLC) {
859
+ const candles = history.map((p: any) => ({
860
+ o: p.open ?? p.p, h: Math.max(p.open ?? p.p, p.close ?? p.p, p.p),
861
+ l: Math.min(p.open ?? p.p, p.close ?? p.p, p.p), c: p.p,
862
+ }));
863
+ chart = candlestickChart(candles, 32, 14);
864
+ } else {
865
+ chart = areaChart(history.map((p: any) => p.p), 64, 10);
866
+ }
867
+
868
+ renderChart(box, renderer, chart, trend ? COLORS.success : COLORS.error);
869
+
870
+ const stats = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
871
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `Current ${fmtDollar(data.current)}`.padEnd(18), fg: COLORS.text }));
872
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `High ${fmtDollar(data.high)}`.padEnd(16), fg: COLORS.success }));
873
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `Low ${fmtDollar(data.low)}`.padEnd(16), fg: COLORS.error }));
874
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `Change ${fmtPct(data.change)}`.padEnd(16), fg: pnlColor(data.change ?? 0) }));
875
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `${data.dataPoints ?? 0} pts`, fg: COLORS.textMuted }));
876
+ box.add(stats);
877
+
878
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Timeframes: 1h 6h 1d 1w 1m max", fg: COLORS.borderDim }));
879
+ } else {
880
+ box.add(new TextRenderable(renderer, { id: uid(), content: "No price data available", fg: COLORS.textMuted }));
881
+ }
882
+
883
+ return box;
884
+ }
885
+
886
+ // ── Market microstructure ──
887
+
888
+ function renderMicrostructure(data: any, renderer: CliRenderer): BoxRenderable {
889
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
890
+
891
+ exchangeBadge(box, renderer, "polymarket", `Microstructure — ${(data.title ?? "").slice(0, 38)}`);
892
+ sep(box, renderer);
893
+
894
+ // Liquidity score gauge
895
+ const score = data.liquidityScore ?? 0;
896
+ const gaugeWidth = 40;
897
+ const filled = Math.round((score / 100) * gaugeWidth);
898
+ const gauge = BAR.repeat(filled) + " ".repeat(gaugeWidth - filled);
899
+ const scoreColor = score >= 70 ? COLORS.success : score >= 40 ? COLORS.warning : COLORS.error;
900
+
901
+ const gaugeRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
902
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: "Liquidity ", fg: COLORS.textMuted }));
903
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: gauge, fg: scoreColor }));
904
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: ` ${score}/100 ${(data.liquidityLabel ?? "").toUpperCase()}`, fg: scoreColor }));
905
+ box.add(gaugeRow);
906
+
907
+ // Spread + imbalance row
908
+ kvPair(box, renderer,
909
+ "Spread", `${data.effectiveSpread ?? 0} (${data.spreadBps ?? 0} bps)`, undefined,
910
+ "Mid Price", fmtDollar(data.midPrice), undefined,
911
+ );
912
+ kvPair(box, renderer,
913
+ "Imbalance", `${((data.depthImbalance ?? 0) * 100).toFixed(1)}% ${data.depthImbalanceLabel ?? ""}`,
914
+ data.depthImbalance > 0.3 ? COLORS.success : data.depthImbalance < -0.3 ? COLORS.error : COLORS.textMuted,
915
+ "Kyle's λ", `${data.kyleLambda ?? 0} (${data.kyleLambdaLabel ?? ""})`,
916
+ data.kyleLambdaLabel === "high impact" ? COLORS.error : COLORS.textMuted,
917
+ );
918
+
919
+ // Depth volume
920
+ kvPair(box, renderer,
921
+ "Bid Volume", (data.bidVolume ?? 0).toLocaleString(), COLORS.success,
922
+ "Ask Volume", (data.askVolume ?? 0).toLocaleString(), COLORS.error,
923
+ );
924
+
925
+ // Depth levels
926
+ if (data.depthLevels?.length > 0) {
927
+ sep(box, renderer);
928
+ const hdr = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
929
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Lvl".padEnd(5), fg: COLORS.textMuted }));
930
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Bid Price".padEnd(12), fg: COLORS.textMuted }));
931
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Bid Size".padEnd(12), fg: COLORS.textMuted }));
932
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Ask Price".padEnd(12), fg: COLORS.textMuted }));
933
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Ask Size", fg: COLORS.textMuted }));
934
+ box.add(hdr);
935
+
936
+ for (const d of data.depthLevels) {
937
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
938
+ row.add(new TextRenderable(renderer, { id: uid(), content: `L${d.level}`.padEnd(5), fg: COLORS.textMuted }));
939
+ row.add(new TextRenderable(renderer, { id: uid(), content: (d.bidPrice !== null ? d.bidPrice.toFixed(2) : "--").padEnd(12), fg: COLORS.success }));
940
+ row.add(new TextRenderable(renderer, { id: uid(), content: d.bidSize.toLocaleString().padEnd(12), fg: COLORS.success }));
941
+ row.add(new TextRenderable(renderer, { id: uid(), content: (d.askPrice !== null ? d.askPrice.toFixed(2) : "--").padEnd(12), fg: COLORS.error }));
942
+ row.add(new TextRenderable(renderer, { id: uid(), content: d.askSize.toLocaleString(), fg: COLORS.error }));
943
+ box.add(row);
944
+ }
945
+ }
946
+
947
+ return box;
948
+ }
949
+
950
+ // ── Liquidity scanner ──
951
+
952
+ function renderLiquidityScanner(data: any, renderer: CliRenderer): BoxRenderable {
953
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
954
+
955
+ exchangeBadge(box, renderer, "polymarket", `Liquidity Scanner — ${data.query ?? ""}`);
956
+
957
+ // Summary row
958
+ if (data.summary) {
959
+ const sum = data.summary;
960
+ const sumRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
961
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: `Avg Spread ${sum.avgSpread}%`.padEnd(20), fg: COLORS.textMuted }));
962
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: `Total Liq ${fmtVol(sum.totalLiquidity)}`.padEnd(22), fg: COLORS.textMuted }));
963
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: `24h Vol ${fmtVol(sum.totalVolume24h)}`, fg: COLORS.textMuted }));
964
+ box.add(sumRow);
965
+ }
966
+ sep(box, renderer);
967
+
968
+ // Header
969
+ const hdr = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
970
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "#".padEnd(4), fg: COLORS.textMuted }));
971
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Market".padEnd(36), fg: COLORS.textMuted }));
972
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Price".padEnd(8), fg: COLORS.textMuted }));
973
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Spread".padEnd(10), fg: COLORS.textMuted }));
974
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Liquidity".padEnd(12), fg: COLORS.textMuted }));
975
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "24h Vol", fg: COLORS.textMuted }));
976
+ box.add(hdr);
977
+
978
+ for (let i = 0; i < (data.markets ?? []).length; i++) {
979
+ const m = data.markets[i];
980
+ const spreadColor = m.spreadPct < 2 ? COLORS.success : m.spreadPct < 5 ? COLORS.warning : COLORS.error;
981
+
982
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
983
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${(i + 1).toString().padStart(2)} `, fg: COLORS.textMuted }));
984
+ row.add(new TextRenderable(renderer, { id: uid(), content: (m.title ?? "").slice(0, 35).padEnd(36), fg: COLORS.text }));
985
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtPrice(m.yesPrice).padEnd(8), fg: COLORS.text }));
986
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${m.spreadPct}%`.padEnd(10), fg: spreadColor }));
987
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtVol(m.liquidity).padEnd(12), fg: COLORS.text }));
988
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtVol(m.volume24h), fg: COLORS.textMuted }));
989
+ box.add(row);
990
+ }
991
+
992
+ return box;
993
+ }
994
+
995
+ // ── Correlation analysis ──
996
+
997
+ function renderCorrelation(data: any, renderer: CliRenderer): BoxRenderable {
998
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
999
+
1000
+ if (data.error) {
1001
+ box.add(new TextRenderable(renderer, { id: uid(), content: `Correlation: ${data.error}`, fg: COLORS.error }));
1002
+ return box;
1003
+ }
1004
+
1005
+ exchangeBadge(box, renderer, "analysis", "Correlation Analysis");
1006
+ sep(box, renderer);
1007
+
1008
+ // Market A and B side by side
1009
+ if (data.marketA && data.marketB) {
1010
+ kvPair(box, renderer,
1011
+ "A", `${(data.marketA.title ?? "").slice(0, 30)} (${fmtDollar(data.marketA.current)})`, undefined,
1012
+ "B", `${(data.marketB.title ?? "").slice(0, 30)} (${fmtDollar(data.marketB.current)})`, undefined,
1013
+ );
1014
+ }
1015
+
1016
+ // Correlation gauge
1017
+ const corr = data.correlation ?? 0;
1018
+ const normalized = (corr + 1) / 2; // -1..1 → 0..1
1019
+ const gaugeWidth = 50;
1020
+ const filled = Math.round(normalized * gaugeWidth);
1021
+ const gauge = BAR.repeat(filled) + " ".repeat(gaugeWidth - filled);
1022
+ const corrColor = Math.abs(corr) > 0.7 ? COLORS.warning : Math.abs(corr) > 0.3 ? COLORS.text : COLORS.success;
1023
+
1024
+ const gaugeRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1025
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: "-1 ", fg: COLORS.error }));
1026
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: gauge, fg: corrColor }));
1027
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: ` +1 r = ${corr > 0 ? "+" : ""}${corr} ${data.label ?? ""}`, fg: corrColor }));
1028
+ box.add(gaugeRow);
1029
+
1030
+ // Stats
1031
+ kvPair(box, renderer,
1032
+ "Beta", `${data.beta ?? 0}`, undefined,
1033
+ "Data Points", `${data.dataPoints ?? 0} (${data.interval ?? ""})`, COLORS.textMuted,
1034
+ );
1035
+
1036
+ // Dual sparklines
1037
+ if (data.marketA?.priceHistory?.length > 3 && data.marketB?.priceHistory?.length > 3) {
1038
+ const sparkA = sparkline(data.marketA.priceHistory, 60);
1039
+ const sparkB = sparkline(data.marketB.priceHistory, 60);
1040
+ const trendA = (data.marketA.change ?? 0) >= 0;
1041
+ const trendB = (data.marketB.change ?? 0) >= 0;
1042
+
1043
+ const rowA = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1044
+ rowA.add(new TextRenderable(renderer, { id: uid(), content: "A ", fg: COLORS.textMuted }));
1045
+ rowA.add(new TextRenderable(renderer, { id: uid(), content: sparkA, fg: trendA ? COLORS.success : COLORS.error }));
1046
+ rowA.add(new TextRenderable(renderer, { id: uid(), content: ` ${fmtPct(data.marketA.change)}`, fg: pnlColor(data.marketA.change ?? 0) }));
1047
+ box.add(rowA);
1048
+
1049
+ const rowB = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1050
+ rowB.add(new TextRenderable(renderer, { id: uid(), content: "B ", fg: COLORS.textMuted }));
1051
+ rowB.add(new TextRenderable(renderer, { id: uid(), content: sparkB, fg: trendB ? COLORS.success : COLORS.error }));
1052
+ rowB.add(new TextRenderable(renderer, { id: uid(), content: ` ${fmtPct(data.marketB.change)}`, fg: pnlColor(data.marketB.change ?? 0) }));
1053
+ box.add(rowB);
1054
+ }
1055
+
1056
+ // Trading implication
1057
+ if (data.tradingImplication) {
1058
+ sep(box, renderer);
1059
+ box.add(new TextRenderable(renderer, { id: uid(), content: data.tradingImplication, fg: COLORS.textMuted }));
1060
+ }
1061
+
1062
+ return box;
1063
+ }
1064
+
1065
+ // ── Wallet profiler ──
1066
+
1067
+ function renderWalletProfile(data: any, renderer: CliRenderer): BoxRenderable {
1068
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1069
+
1070
+ const s = data.stats;
1071
+ if (!s) {
1072
+ box.add(new TextRenderable(renderer, { id: uid(), content: "No wallet data", fg: COLORS.textMuted }));
1073
+ return box;
1074
+ }
1075
+
1076
+ exchangeBadge(box, renderer, "polymarket", `Wallet — ${data.name ?? "Anonymous"}`);
1077
+
1078
+ const categoryColor = s.edgeCategory === "smart_money" ? COLORS.success
1079
+ : s.edgeCategory === "neutral" ? COLORS.textMuted
1080
+ : s.edgeCategory === "weak_hand" ? COLORS.warning : COLORS.error;
1081
+
1082
+ const header = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1083
+ header.add(new TextRenderable(renderer, { id: uid(), content: ` ${(s.edgeCategory ?? "").toUpperCase()}`, fg: categoryColor }));
1084
+ header.add(new TextRenderable(renderer, { id: uid(), content: ` score ${s.composite > 0 ? "+" : ""}${s.composite}`, fg: pnlColor(s.composite) }));
1085
+ box.add(header);
1086
+ sep(box, renderer);
1087
+
1088
+ // Composite score gauge
1089
+ const normalized = (s.composite + 1) / 2; // -1..1 → 0..1
1090
+ const gaugeWidth = 40;
1091
+ const filled = Math.round(normalized * gaugeWidth);
1092
+ const gauge = BAR.repeat(filled) + " ".repeat(gaugeWidth - filled);
1093
+ const gaugeRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1094
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: "weak ", fg: COLORS.error }));
1095
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: gauge, fg: categoryColor }));
1096
+ gaugeRow.add(new TextRenderable(renderer, { id: uid(), content: " smart", fg: COLORS.success }));
1097
+ box.add(gaugeRow);
1098
+
1099
+ // Stats
1100
+ kvPair(box, renderer,
1101
+ "Win Rate", `${(s.winRate * 100).toFixed(1)}%`, s.winRate > 0.55 ? COLORS.success : s.winRate < 0.45 ? COLORS.error : undefined,
1102
+ "Total PnL", `$${s.totalPnl.toFixed(2)}`, pnlColor(s.totalPnl),
1103
+ );
1104
+ kvPair(box, renderer,
1105
+ "Sharpe", `${s.sharpe}`, s.sharpe > 1 ? COLORS.success : undefined,
1106
+ "Sortino", `${s.sortino}`, s.sortino > 1 ? COLORS.success : undefined,
1107
+ );
1108
+ kvPair(box, renderer,
1109
+ "Positions", `${s.totalPositions}`, undefined,
1110
+ "Trades", `${s.totalTrades} (${(s.buyRatio * 100).toFixed(0)}% buy)`, undefined,
1111
+ );
1112
+
1113
+ // Top positions
1114
+ if (data.topPositions?.length > 0) {
1115
+ sep(box, renderer);
1116
+ for (const p of data.topPositions) {
1117
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1118
+ row.add(new TextRenderable(renderer, { id: uid(), content: (p.outcome ?? "YES").padEnd(4), fg: p.outcome === "YES" ? COLORS.success : COLORS.error }));
1119
+ row.add(new TextRenderable(renderer, { id: uid(), content: (p.market ?? "").slice(0, 30).padEnd(32), fg: COLORS.textMuted }));
1120
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${p.size} @ ${fmtDollar(p.avgPrice)}`.padEnd(16), fg: COLORS.text }));
1121
+ row.add(new TextRenderable(renderer, { id: uid(), content: `PnL ${fmtDollar(p.pnl)}`, fg: pnlColor(p.pnl) }));
1122
+ box.add(row);
1123
+ }
1124
+ }
1125
+
1126
+ return box;
1127
+ }
1128
+
1129
+ // ── Bot detector ──
1130
+
1131
+ function renderBotDetector(data: any, renderer: CliRenderer): BoxRenderable {
1132
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1133
+
1134
+ exchangeBadge(box, renderer, "polymarket", `Bot Detection — ${(data.title ?? "").slice(0, 38)}`);
1135
+
1136
+ // Summary bar
1137
+ const s = data.summary;
1138
+ if (s) {
1139
+ const botBar = BAR.repeat(Math.round((s.botPct / 100) * 40));
1140
+ const humanBar = " ".repeat(40 - botBar.length);
1141
+ const sumRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1142
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: "bots ", fg: COLORS.error }));
1143
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: botBar, fg: COLORS.error }));
1144
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: humanBar, fg: COLORS.borderDim }));
1145
+ sumRow.add(new TextRenderable(renderer, { id: uid(), content: ` humans ${s.bots} bots / ${s.humans} humans (${s.botPct}% bot)`, fg: COLORS.textMuted }));
1146
+ box.add(sumRow);
1147
+
1148
+ // Strategy breakdown
1149
+ const strats = s.strategies;
1150
+ if (strats) {
1151
+ const stratRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1152
+ if (strats.market_maker > 0) stratRow.add(new TextRenderable(renderer, { id: uid(), content: `MM:${strats.market_maker} `, fg: COLORS.textMuted }));
1153
+ if (strats.grid_bot > 0) stratRow.add(new TextRenderable(renderer, { id: uid(), content: `Grid:${strats.grid_bot} `, fg: COLORS.textMuted }));
1154
+ if (strats.momentum > 0) stratRow.add(new TextRenderable(renderer, { id: uid(), content: `Momentum:${strats.momentum} `, fg: COLORS.textMuted }));
1155
+ if (strats.sniper > 0) stratRow.add(new TextRenderable(renderer, { id: uid(), content: `Sniper:${strats.sniper} `, fg: COLORS.textMuted }));
1156
+ stratRow.add(new TextRenderable(renderer, { id: uid(), content: `Human:${strats.human}`, fg: COLORS.textMuted }));
1157
+ box.add(stratRow);
1158
+ }
1159
+ }
1160
+ sep(box, renderer);
1161
+
1162
+ // Trader rows
1163
+ const hdr = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1164
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Type".padEnd(6), fg: COLORS.textMuted }));
1165
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Name".padEnd(16), fg: COLORS.textMuted }));
1166
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Strategy".padEnd(14), fg: COLORS.textMuted }));
1167
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Trades".padEnd(8), fg: COLORS.textMuted }));
1168
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Freq/hr".padEnd(10), fg: COLORS.textMuted }));
1169
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Volume", fg: COLORS.textMuted }));
1170
+ box.add(hdr);
1171
+
1172
+ for (const t of (data.topTraders ?? []).slice(0, 8)) {
1173
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1174
+ row.add(new TextRenderable(renderer, { id: uid(), content: (t.isBot ? "BOT" : "HUMAN").padEnd(6), fg: t.isBot ? COLORS.error : COLORS.success }));
1175
+ row.add(new TextRenderable(renderer, { id: uid(), content: (t.name ?? "").slice(0, 15).padEnd(16), fg: COLORS.text }));
1176
+ row.add(new TextRenderable(renderer, { id: uid(), content: (t.strategyType ?? "").padEnd(14), fg: COLORS.textMuted }));
1177
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${t.trades}`.padEnd(8), fg: COLORS.text }));
1178
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${t.tradeFreq}`.padEnd(10), fg: t.tradeFreq > 10 ? COLORS.warning : COLORS.text }));
1179
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${t.totalVolume.toLocaleString()}`, fg: COLORS.textMuted }));
1180
+ box.add(row);
1181
+ }
1182
+
1183
+ return box;
1184
+ }
1185
+
1186
+ // ── Market flow ──
1187
+
1188
+ function renderMarketFlow(data: any, renderer: CliRenderer): BoxRenderable {
1189
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1190
+
1191
+ exchangeBadge(box, renderer, "polymarket", `Market Flow — ${(data.title ?? "").slice(0, 40)}`);
1192
+ sep(box, renderer);
1193
+
1194
+ const f = data.flow;
1195
+ if (f) {
1196
+ // Buy/sell bar
1197
+ const buyPct = f.buyPct ?? 50;
1198
+ const barWidth = 50;
1199
+ const buyWidth = Math.round((buyPct / 100) * barWidth);
1200
+ const sellWidth = barWidth - buyWidth;
1201
+ const flowRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1202
+ flowRow.add(new TextRenderable(renderer, { id: uid(), content: "BUY ", fg: COLORS.success }));
1203
+ flowRow.add(new TextRenderable(renderer, { id: uid(), content: BAR.repeat(buyWidth), fg: COLORS.success }));
1204
+ flowRow.add(new TextRenderable(renderer, { id: uid(), content: BAR.repeat(sellWidth), fg: COLORS.error }));
1205
+ flowRow.add(new TextRenderable(renderer, { id: uid(), content: ` SELL ${buyPct}% / ${(100 - buyPct).toFixed(1)}%`, fg: COLORS.textMuted }));
1206
+ box.add(flowRow);
1207
+
1208
+ kvPair(box, renderer,
1209
+ "Buy Vol", fmtVol(f.buyVolume), COLORS.success,
1210
+ "Sell Vol", fmtVol(f.sellVolume), COLORS.error,
1211
+ );
1212
+ kvPair(box, renderer,
1213
+ "Net Flow", `${fmtVol(Math.abs(f.netFlow))} ${f.direction}`, pnlColor(f.netFlow),
1214
+ "Trades", `${f.totalTrades} (${f.buyCount}B / ${f.sellCount}S)`, undefined,
1215
+ );
1216
+ }
1217
+
1218
+ // Concentration
1219
+ const c = data.concentration;
1220
+ if (c) {
1221
+ kvPair(box, renderer,
1222
+ "Wallets", `${c.uniqueWallets}`, undefined,
1223
+ "HHI", `${c.herfindahl} (${c.label})`, c.herfindahl > 0.25 ? COLORS.warning : COLORS.textMuted,
1224
+ );
1225
+ kv(box, renderer, "Top 3 Share", `${c.top3Share.toFixed(1)}%`, c.top3Share > 50 ? COLORS.warning : COLORS.textMuted);
1226
+ }
1227
+
1228
+ // Time distribution
1229
+ if (data.timeDistribution) {
1230
+ const td = data.timeDistribution;
1231
+ const max = Math.max(...td, 1);
1232
+ const miniSpark = td.map((v: number) => {
1233
+ const idx = Math.min(7, Math.floor((v / max) * 8));
1234
+ return SPARK[idx];
1235
+ }).join("");
1236
+ kv(box, renderer, "Activity", `${miniSpark} (old → new)`, undefined);
1237
+ }
1238
+
1239
+ // Top movers
1240
+ if (data.topMovers?.length > 0) {
1241
+ sep(box, renderer);
1242
+ for (const m of data.topMovers.slice(0, 6)) {
1243
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1244
+ row.add(new TextRenderable(renderer, { id: uid(), content: (m.direction === "buyer" ? "BUY " : "SELL").padEnd(5), fg: m.direction === "buyer" ? COLORS.success : COLORS.error }));
1245
+ row.add(new TextRenderable(renderer, { id: uid(), content: (m.name ?? "").slice(0, 16).padEnd(18), fg: COLORS.textMuted }));
1246
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtVol(m.total).padEnd(10), fg: COLORS.text }));
1247
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${m.share}%`, fg: m.share > 20 ? COLORS.warning : COLORS.textMuted }));
1248
+ box.add(row);
1249
+ }
1250
+ }
1251
+
1252
+ return box;
1253
+ }
1254
+
1255
+ // ── Risk metrics ──
1256
+
1257
+ function renderRiskMetrics(data: any, renderer: CliRenderer): BoxRenderable {
1258
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1259
+
1260
+ if (data.error) {
1261
+ box.add(new TextRenderable(renderer, { id: uid(), content: `Risk: ${data.error}`, fg: COLORS.error }));
1262
+ return box;
1263
+ }
1264
+
1265
+ exchangeBadge(box, renderer, "analysis", `Risk — ${(data.title ?? "").slice(0, 45)}`);
1266
+ sep(box, renderer);
1267
+
1268
+ const r = data.riskMetrics;
1269
+ const p = data.performanceMetrics;
1270
+ const d = data.distribution;
1271
+
1272
+ if (r) {
1273
+ kvPair(box, renderer,
1274
+ "VaR (95%)", `${r.varPct}%`, COLORS.error,
1275
+ "CVaR", `${r.cvarPct}%`, COLORS.error,
1276
+ );
1277
+ kvPair(box, renderer,
1278
+ "Max DD", `${r.maxDrawdownPct}%`, COLORS.error,
1279
+ "DD Period", `${r.drawdownPeriod?.length ?? 0} bars`, COLORS.textMuted,
1280
+ );
1281
+ }
1282
+
1283
+ if (p) {
1284
+ sep(box, renderer);
1285
+ kvPair(box, renderer,
1286
+ "Sharpe", `${p.sharpe}`, p.sharpe > 1 ? COLORS.success : COLORS.textMuted,
1287
+ "Sortino", `${p.sortino}`, p.sortino > 1 ? COLORS.success : COLORS.textMuted,
1288
+ );
1289
+ kvPair(box, renderer,
1290
+ "Calmar", `${p.calmar}`, p.calmar > 1 ? COLORS.success : COLORS.textMuted,
1291
+ "Ann. Return", `${p.annualizedReturn}%`, pnlColor(p.annualizedReturn),
1292
+ );
1293
+ kv(box, renderer, "Volatility", `${p.volatility}% annualized`, p.volatility > 100 ? COLORS.warning : COLORS.textMuted);
1294
+ }
1295
+
1296
+ if (d) {
1297
+ sep(box, renderer);
1298
+ kvPair(box, renderer,
1299
+ "Skewness", `${d.skewness} (${d.skewnessLabel})`, +d.skewness < -0.5 ? COLORS.error : COLORS.textMuted,
1300
+ "Kurtosis", `${d.kurtosis} (${d.kurtosisLabel})`, +d.kurtosis > 1 ? COLORS.warning : COLORS.textMuted,
1301
+ );
1302
+ }
1303
+
1304
+ // Sparkline
1305
+ if (data.priceHistory?.length > 3) {
1306
+ const trend = data.priceHistory[data.priceHistory.length - 1] >= data.priceHistory[0];
1307
+ box.add(new TextRenderable(renderer, { id: uid(), content: sparkline(data.priceHistory, 72), fg: trend ? COLORS.success : COLORS.error }));
1308
+ }
1309
+
1310
+ return box;
1311
+ }
1312
+
1313
+ // ── Market regime ──
1314
+
1315
+ function renderMarketRegime(data: any, renderer: CliRenderer): BoxRenderable {
1316
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1317
+
1318
+ if (data.error) {
1319
+ box.add(new TextRenderable(renderer, { id: uid(), content: `Regime: ${data.error}`, fg: COLORS.error }));
1320
+ return box;
1321
+ }
1322
+
1323
+ exchangeBadge(box, renderer, "analysis", `Regime — ${(data.title ?? "").slice(0, 43)}`);
1324
+
1325
+ // Regime label
1326
+ const regimeColor = data.regime === "trending" ? COLORS.success
1327
+ : data.regime === "mean-reverting" ? COLORS.warning
1328
+ : data.regime?.includes("efficient") ? COLORS.textMuted : COLORS.text;
1329
+ box.add(new TextRenderable(renderer, { id: uid(), content: ` ${(data.regime ?? "unknown").toUpperCase()}`, fg: regimeColor, attributes: 1 }));
1330
+ sep(box, renderer);
1331
+
1332
+ // Hurst gauge: 0 = mean-reverting, 0.5 = random, 1 = trending
1333
+ if (data.hurst) {
1334
+ const h = data.hurst;
1335
+ const gaugeWidth = 50;
1336
+ const pos = Math.round(h.value * gaugeWidth);
1337
+ const gauge = " ".repeat(Math.max(0, pos - 1)) + BAR + " ".repeat(gaugeWidth - pos);
1338
+ const hurstRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1339
+ hurstRow.add(new TextRenderable(renderer, { id: uid(), content: "MR ", fg: COLORS.warning }));
1340
+ hurstRow.add(new TextRenderable(renderer, { id: uid(), content: gauge, fg: regimeColor }));
1341
+ hurstRow.add(new TextRenderable(renderer, { id: uid(), content: ` TR H=${h.value} ${h.label}`, fg: COLORS.textMuted }));
1342
+ box.add(hurstRow);
1343
+ }
1344
+
1345
+ // Variance ratios
1346
+ if (data.varianceRatio) {
1347
+ const vr = data.varianceRatio;
1348
+ const vrRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1349
+ vrRow.add(new TextRenderable(renderer, { id: uid(), content: `VR(2)=${vr.vr2}`.padEnd(16), fg: vr.vr2 > 1.15 ? COLORS.success : vr.vr2 < 0.85 ? COLORS.warning : COLORS.textMuted }));
1350
+ vrRow.add(new TextRenderable(renderer, { id: uid(), content: `VR(5)=${vr.vr5}`.padEnd(16), fg: COLORS.textMuted }));
1351
+ vrRow.add(new TextRenderable(renderer, { id: uid(), content: `VR(10)=${vr.vr10}`.padEnd(16), fg: COLORS.textMuted }));
1352
+ vrRow.add(new TextRenderable(renderer, { id: uid(), content: vr.label, fg: COLORS.textMuted }));
1353
+ box.add(vrRow);
1354
+ }
1355
+
1356
+ // Entropy
1357
+ if (data.entropy) {
1358
+ kv(box, renderer, "Entropy", `${data.entropy.normalized} (${data.entropy.label})`,
1359
+ data.entropy.normalized > 0.85 ? COLORS.textMuted : COLORS.warning);
1360
+ }
1361
+
1362
+ // Autocorrelations
1363
+ if (data.autocorrelations) {
1364
+ const ac = data.autocorrelations;
1365
+ const acRow = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1366
+ acRow.add(new TextRenderable(renderer, { id: uid(), content: "AC: ", fg: COLORS.textMuted }));
1367
+ for (let i = 1; i <= 5; i++) {
1368
+ const val = ac[`lag${i}`] ?? 0;
1369
+ const color = Math.abs(val) > 0.2 ? COLORS.warning : COLORS.textMuted;
1370
+ acRow.add(new TextRenderable(renderer, { id: uid(), content: `L${i}=${val > 0 ? "+" : ""}${val} `, fg: color }));
1371
+ }
1372
+ box.add(acRow);
1373
+ }
1374
+
1375
+ // Strategy hint
1376
+ if (data.strategyHint) {
1377
+ sep(box, renderer);
1378
+ box.add(new TextRenderable(renderer, { id: uid(), content: data.strategyHint, fg: COLORS.textMuted }));
1379
+ }
1380
+
1381
+ // Sparkline
1382
+ if (data.priceHistory?.length > 3) {
1383
+ const trend = data.priceHistory[data.priceHistory.length - 1] >= data.priceHistory[0];
1384
+ box.add(new TextRenderable(renderer, { id: uid(), content: sparkline(data.priceHistory, 72), fg: trend ? COLORS.success : COLORS.error }));
1385
+ }
1386
+
1387
+ return box;
1388
+ }
1389
+
1390
+ // ── Stock quote ──
1391
+
1392
+ function fmtMarketCap(v: number | null): string {
1393
+ if (!v) return "--";
1394
+ if (v >= 1e12) return `$${(v / 1e12).toFixed(2)}T`;
1395
+ if (v >= 1e9) return `$${(v / 1e9).toFixed(1)}B`;
1396
+ if (v >= 1e6) return `$${(v / 1e6).toFixed(0)}M`;
1397
+ return `$${v.toLocaleString()}`;
1398
+ }
1399
+
1400
+ function fmtStockVol(v: number): string {
1401
+ if (v >= 1e6) return `${(v / 1e6).toFixed(1)}M`;
1402
+ if (v >= 1e3) return `${(v / 1e3).toFixed(0)}K`;
1403
+ return `${v}`;
1404
+ }
1405
+
1406
+ function renderStockQuote(data: any, renderer: CliRenderer): BoxRenderable {
1407
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1408
+ exchangeBadge(box, renderer, "stock", "Quotes");
1409
+ const quotes = data.quotes ?? [];
1410
+
1411
+ for (const q of quotes.slice(0, 8)) {
1412
+ const changeColor = (q.changePct ?? 0) >= 0 ? COLORS.success : COLORS.error;
1413
+ const changeStr = `${q.change >= 0 ? "+" : ""}${q.change?.toFixed(2)} (${q.changePct >= 0 ? "+" : ""}${q.changePct?.toFixed(2)}%)`;
1414
+
1415
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
1416
+ row.add(new TextRenderable(renderer, { id: uid(), content: (q.symbol ?? "").padEnd(8), fg: COLORS.text, attributes: 1 }));
1417
+ row.add(new TextRenderable(renderer, { id: uid(), content: (q.name ?? "").slice(0, 22).padEnd(24), fg: COLORS.textMuted }));
1418
+ row.add(new TextRenderable(renderer, { id: uid(), content: `$${q.price?.toFixed(2)}`.padEnd(12), fg: COLORS.text, attributes: 1 }));
1419
+ row.add(new TextRenderable(renderer, { id: uid(), content: changeStr.padEnd(20), fg: changeColor }));
1420
+ row.add(new TextRenderable(renderer, { id: uid(), content: `Vol ${fmtStockVol(q.volume ?? 0)}`.padEnd(14), fg: COLORS.textMuted }));
1421
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtMarketCap(q.marketCap), fg: COLORS.textMuted }));
1422
+ box.add(row);
1423
+
1424
+ if (q.peRatio || q.fiftyTwoWeekHigh) {
1425
+ const detail = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1426
+ detail.add(new TextRenderable(renderer, { id: uid(), content: " ", fg: COLORS.textMuted }));
1427
+ if (q.peRatio) detail.add(new TextRenderable(renderer, { id: uid(), content: `P/E ${q.peRatio.toFixed(1)}`.padEnd(12), fg: COLORS.textMuted }));
1428
+ if (q.fiftyTwoWeekLow && q.fiftyTwoWeekHigh) {
1429
+ detail.add(new TextRenderable(renderer, { id: uid(), content: `52w ${q.fiftyTwoWeekLow.toFixed(2)}-${q.fiftyTwoWeekHigh.toFixed(2)}`.padEnd(24), fg: COLORS.textMuted }));
1430
+ }
1431
+ if (q.fiftyDayAvg) detail.add(new TextRenderable(renderer, { id: uid(), content: `50d MA $${q.fiftyDayAvg.toFixed(2)}`.padEnd(18), fg: q.price > q.fiftyDayAvg ? COLORS.success : COLORS.error }));
1432
+ if (q.beta) detail.add(new TextRenderable(renderer, { id: uid(), content: `Beta ${q.beta.toFixed(2)}`, fg: COLORS.textMuted }));
1433
+ box.add(detail);
1434
+ }
1435
+ }
1436
+
1437
+ return box;
1438
+ }
1439
+
1440
+ // ── Stock chart ──
1441
+
1442
+ function renderStockChart(data: any, renderer: CliRenderer): BoxRenderable {
1443
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1444
+
1445
+ exchangeBadge(box, renderer, "stock", `${data.symbol ?? ""} — ${data.name ?? ""} (${data.range})`);
1446
+
1447
+ if (data.priceHistory?.length > 0) {
1448
+ const history = data.priceHistory.filter((p: any) => p.p != null);
1449
+ const trend = (data.change ?? 0) >= 0;
1450
+
1451
+ // Use candlestick when OHLC data is available, fallback to area chart
1452
+ const hasOHLC = history.some((p: any) => p.o != null && p.h != null && p.l != null);
1453
+ let chart: ChartResult;
1454
+ if (hasOHLC) {
1455
+ const candles = history.map((p: any) => ({
1456
+ o: p.o ?? p.p, h: p.h ?? Math.max(p.o ?? p.p, p.p),
1457
+ l: p.l ?? Math.min(p.o ?? p.p, p.p), c: p.p,
1458
+ }));
1459
+ chart = candlestickChart(candles, 32, 16);
1460
+ } else {
1461
+ chart = areaChart(history.map((p: any) => p.p), 64, 12);
1462
+ }
1463
+
1464
+ renderChart(box, renderer, chart, trend ? COLORS.success : COLORS.error);
1465
+
1466
+ // Stats row
1467
+ const stats = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1468
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `$${data.current?.toFixed(2)}`.padEnd(14), fg: COLORS.text, attributes: 1 }));
1469
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `High $${data.high?.toFixed(2)}`.padEnd(16), fg: COLORS.success }));
1470
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `Low $${data.low?.toFixed(2)}`.padEnd(16), fg: COLORS.error }));
1471
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `Change ${fmtPct(data.change)}`.padEnd(16), fg: pnlColor(data.change ?? 0) }));
1472
+ stats.add(new TextRenderable(renderer, { id: uid(), content: `${data.dataPoints ?? 0} pts`, fg: COLORS.textMuted }));
1473
+ box.add(stats);
1474
+
1475
+ // Timeframe hint
1476
+ box.add(new TextRenderable(renderer, { id: uid(), content: " Timeframes: 1d 5d 1mo 3mo 6mo 1y 5y max", fg: COLORS.borderDim }));
1477
+ } else {
1478
+ box.add(new TextRenderable(renderer, { id: uid(), content: "No chart data available", fg: COLORS.textMuted }));
1479
+ }
1480
+
1481
+ return box;
1482
+ }
1483
+
1484
+ // ── Stock search ──
1485
+
1486
+ function renderStockSearch(data: any, renderer: CliRenderer): BoxRenderable {
1487
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1488
+
1489
+ exchangeBadge(box, renderer, "stock", `Search: "${data.query}"`);
1490
+
1491
+ for (const r of (data.results ?? []).slice(0, 8)) {
1492
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row", width: "100%" });
1493
+ row.add(new TextRenderable(renderer, { id: uid(), content: (r.symbol ?? "").padEnd(10), fg: COLORS.text, attributes: 1 }));
1494
+ row.add(new TextRenderable(renderer, { id: uid(), content: (r.name ?? "").slice(0, 40).padEnd(42), fg: COLORS.textMuted }));
1495
+ row.add(new TextRenderable(renderer, { id: uid(), content: (r.type ?? "").padEnd(10), fg: COLORS.textMuted }));
1496
+ row.add(new TextRenderable(renderer, { id: uid(), content: r.exchange ?? "", fg: COLORS.borderDim }));
1497
+ box.add(row);
1498
+ }
1499
+
1500
+ return box;
1501
+ }
1502
+
1503
+ // ── Stock screener ──
1504
+
1505
+ function renderStockScreener(data: any, renderer: CliRenderer): BoxRenderable {
1506
+ const box = new BoxRenderable(renderer, { id: uid(), flexDirection: "column", marginBottom: 1 });
1507
+
1508
+ exchangeBadge(box, renderer, "stock", "Stock Screener");
1509
+ sep(box, renderer);
1510
+
1511
+ const hdr = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1512
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Symbol".padEnd(8), fg: COLORS.textMuted }));
1513
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Price".padEnd(10), fg: COLORS.textMuted }));
1514
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "Change".padEnd(10), fg: COLORS.textMuted }));
1515
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "MCap".padEnd(10), fg: COLORS.textMuted }));
1516
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "P/E".padEnd(8), fg: COLORS.textMuted }));
1517
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "From Hi".padEnd(10), fg: COLORS.textMuted }));
1518
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "50d".padEnd(6), fg: COLORS.textMuted }));
1519
+ hdr.add(new TextRenderable(renderer, { id: uid(), content: "200d", fg: COLORS.textMuted }));
1520
+ box.add(hdr);
1521
+
1522
+ for (const s of (data.stocks ?? []).slice(0, 10)) {
1523
+ const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
1524
+ row.add(new TextRenderable(renderer, { id: uid(), content: (s.symbol ?? "").padEnd(8), fg: COLORS.text, attributes: 1 }));
1525
+ row.add(new TextRenderable(renderer, { id: uid(), content: `$${s.price?.toFixed(2)}`.padEnd(10), fg: COLORS.text }));
1526
+ row.add(new TextRenderable(renderer, { id: uid(), content: `${s.changePct >= 0 ? "+" : ""}${s.changePct?.toFixed(1)}%`.padEnd(10), fg: pnlColor(s.changePct ?? 0) }));
1527
+ row.add(new TextRenderable(renderer, { id: uid(), content: fmtMarketCap(s.marketCap).padEnd(10), fg: COLORS.textMuted }));
1528
+ row.add(new TextRenderable(renderer, { id: uid(), content: (s.peRatio ? s.peRatio.toFixed(1) : "--").padEnd(8), fg: COLORS.textMuted }));
1529
+ row.add(new TextRenderable(renderer, { id: uid(), content: (s.distFromHigh ? `${s.distFromHigh}%` : "--").padEnd(10), fg: s.distFromHigh < -20 ? COLORS.error : COLORS.textMuted }));
1530
+ row.add(new TextRenderable(renderer, { id: uid(), content: (s.aboveFiftyDayMA === true ? ">" : s.aboveFiftyDayMA === false ? "<" : "-").padEnd(6), fg: s.aboveFiftyDayMA ? COLORS.success : COLORS.error }));
1531
+ row.add(new TextRenderable(renderer, { id: uid(), content: s.aboveTwoHundredDayMA === true ? ">" : s.aboveTwoHundredDayMA === false ? "<" : "-", fg: s.aboveTwoHundredDayMA ? COLORS.success : COLORS.error }));
1532
+ box.add(row);
1533
+ }
1534
+
1535
+ return box;
1536
+ }