horizon-code 0.2.0 → 0.3.1

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