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.
- package/package.json +1 -1
- package/src/ai/client.ts +33 -8
- package/src/ai/system-prompt.ts +48 -6
- package/src/app.ts +92 -4
- package/src/components/code-panel.ts +83 -4
- package/src/components/footer.ts +3 -0
- package/src/components/hooks-panel.ts +1 -1
- package/src/components/mode-bar.ts +1 -1
- package/src/components/settings-panel.ts +15 -1
- package/src/components/tab-bar.ts +10 -7
- package/src/components/tutorial-panel.ts +1 -1
- package/src/platform/exchanges.ts +154 -0
- package/src/platform/supabase.ts +17 -2
- package/src/research/apis.ts +291 -13
- package/src/research/stock-apis.ts +117 -0
- package/src/research/tools.ts +929 -17
- package/src/research/widgets.ts +1044 -29
- package/src/theme/colors.ts +6 -6
package/src/research/widgets.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
201
|
-
box
|
|
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
|
-
|
|
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: `
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
343
|
-
|
|
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 (
|
|
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
|
|
381
|
-
const
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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: `
|
|
447
|
-
statsRow.add(new TextRenderable(renderer, { id: uid(), content: `
|
|
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
|
|
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
|
+
}
|