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