@spfunctions/cli 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +205 -48
  2. package/dist/cache.d.ts +6 -0
  3. package/dist/cache.js +31 -0
  4. package/dist/cache.test.d.ts +1 -0
  5. package/dist/cache.test.js +73 -0
  6. package/dist/client.test.d.ts +1 -0
  7. package/dist/client.test.js +89 -0
  8. package/dist/commands/agent.js +594 -106
  9. package/dist/commands/book.d.ts +17 -0
  10. package/dist/commands/book.js +220 -0
  11. package/dist/commands/dashboard.d.ts +6 -3
  12. package/dist/commands/dashboard.js +53 -22
  13. package/dist/commands/liquidity.d.ts +2 -0
  14. package/dist/commands/liquidity.js +128 -43
  15. package/dist/commands/performance.js +9 -2
  16. package/dist/commands/positions.js +50 -0
  17. package/dist/commands/scan.d.ts +1 -0
  18. package/dist/commands/scan.js +66 -15
  19. package/dist/commands/setup.d.ts +1 -0
  20. package/dist/commands/setup.js +71 -6
  21. package/dist/commands/telegram.d.ts +15 -0
  22. package/dist/commands/telegram.js +125 -0
  23. package/dist/config.d.ts +3 -0
  24. package/dist/config.js +9 -0
  25. package/dist/config.test.d.ts +1 -0
  26. package/dist/config.test.js +138 -0
  27. package/dist/index.js +107 -9
  28. package/dist/polymarket.d.ts +237 -0
  29. package/dist/polymarket.js +353 -0
  30. package/dist/polymarket.test.d.ts +1 -0
  31. package/dist/polymarket.test.js +424 -0
  32. package/dist/telegram/agent-bridge.d.ts +15 -0
  33. package/dist/telegram/agent-bridge.js +368 -0
  34. package/dist/telegram/bot.d.ts +10 -0
  35. package/dist/telegram/bot.js +297 -0
  36. package/dist/telegram/commands.d.ts +11 -0
  37. package/dist/telegram/commands.js +120 -0
  38. package/dist/telegram/format.d.ts +11 -0
  39. package/dist/telegram/format.js +51 -0
  40. package/dist/telegram/format.test.d.ts +1 -0
  41. package/dist/telegram/format.test.js +73 -0
  42. package/dist/telegram/poller.d.ts +6 -0
  43. package/dist/telegram/poller.js +32 -0
  44. package/dist/topics.d.ts +3 -0
  45. package/dist/topics.js +65 -7
  46. package/dist/topics.test.d.ts +1 -0
  47. package/dist/topics.test.js +131 -0
  48. package/dist/tui/border.d.ts +33 -0
  49. package/dist/tui/border.js +87 -0
  50. package/dist/tui/chart.d.ts +19 -0
  51. package/dist/tui/chart.js +117 -0
  52. package/dist/tui/dashboard.d.ts +9 -0
  53. package/dist/tui/dashboard.js +814 -0
  54. package/dist/tui/layout.d.ts +16 -0
  55. package/dist/tui/layout.js +41 -0
  56. package/dist/tui/screen.d.ts +33 -0
  57. package/dist/tui/screen.js +102 -0
  58. package/dist/tui/state.d.ts +40 -0
  59. package/dist/tui/state.js +36 -0
  60. package/dist/tui/widgets/commandbar.d.ts +8 -0
  61. package/dist/tui/widgets/commandbar.js +82 -0
  62. package/dist/tui/widgets/detail.d.ts +9 -0
  63. package/dist/tui/widgets/detail.js +151 -0
  64. package/dist/tui/widgets/edges.d.ts +4 -0
  65. package/dist/tui/widgets/edges.js +34 -0
  66. package/dist/tui/widgets/liquidity.d.ts +9 -0
  67. package/dist/tui/widgets/liquidity.js +142 -0
  68. package/dist/tui/widgets/orders.d.ts +4 -0
  69. package/dist/tui/widgets/orders.js +37 -0
  70. package/dist/tui/widgets/portfolio.d.ts +4 -0
  71. package/dist/tui/widgets/portfolio.js +59 -0
  72. package/dist/tui/widgets/signals.d.ts +4 -0
  73. package/dist/tui/widgets/signals.js +31 -0
  74. package/dist/tui/widgets/statusbar.d.ts +8 -0
  75. package/dist/tui/widgets/statusbar.js +72 -0
  76. package/dist/tui/widgets/thesis.d.ts +4 -0
  77. package/dist/tui/widgets/thesis.js +66 -0
  78. package/dist/tui/widgets/trade.d.ts +9 -0
  79. package/dist/tui/widgets/trade.js +117 -0
  80. package/dist/tui/widgets/upcoming.d.ts +4 -0
  81. package/dist/tui/widgets/upcoming.js +41 -0
  82. package/dist/tui/widgets/whatif.d.ts +7 -0
  83. package/dist/tui/widgets/whatif.js +113 -0
  84. package/dist/utils.test.d.ts +1 -0
  85. package/dist/utils.test.js +111 -0
  86. package/package.json +6 -2
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Telegram slash command handlers — direct API calls, zero LLM cost
3
+ */
4
+ import { SFClient } from '../client.js';
5
+ export declare function handleContext(client: SFClient, thesisId: string): Promise<string>;
6
+ export declare function handlePositions(): Promise<string>;
7
+ export declare function handleEdges(client: SFClient): Promise<string>;
8
+ export declare function handleBalance(): Promise<string>;
9
+ export declare function handleOrders(): Promise<string>;
10
+ export declare function handleEval(client: SFClient, thesisId: string): Promise<string>;
11
+ export declare function handleList(client: SFClient): Promise<string>;
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Telegram slash command handlers — direct API calls, zero LLM cost
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.handleContext = handleContext;
7
+ exports.handlePositions = handlePositions;
8
+ exports.handleEdges = handleEdges;
9
+ exports.handleBalance = handleBalance;
10
+ exports.handleOrders = handleOrders;
11
+ exports.handleEval = handleEval;
12
+ exports.handleList = handleList;
13
+ const kalshi_js_1 = require("../kalshi.js");
14
+ const format_js_1 = require("./format.js");
15
+ async function handleContext(client, thesisId) {
16
+ const ctx = await client.getContext(thesisId);
17
+ const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : '?';
18
+ const thesis = ctx.thesis || ctx.rawThesis || 'N/A';
19
+ let text = `📋 <b>${(0, format_js_1.escapeHtml)(thesis.slice(0, 80))}</b>\n`;
20
+ text += `Confidence: <b>${conf}%</b> | Status: ${ctx.status}\n\n`;
21
+ const edges = (ctx.edges || []).slice(0, 8);
22
+ if (edges.length > 0) {
23
+ text += `<b>Top Edges:</b>\n<pre>`;
24
+ for (const e of edges) {
25
+ const name = (e.market || e.marketTitle || '???').slice(0, 35);
26
+ text += `${name.padEnd(36)} ${String(e.edge || 0).padStart(3)}¢ ${e.direction || 'yes'}\n`;
27
+ }
28
+ text += `</pre>`;
29
+ }
30
+ return text;
31
+ }
32
+ async function handlePositions() {
33
+ if (!(0, kalshi_js_1.isKalshiConfigured)())
34
+ return '⚠️ Kalshi not configured. Run <code>sf setup --kalshi</code>';
35
+ const positions = await (0, kalshi_js_1.getPositions)();
36
+ if (!positions || positions.length === 0)
37
+ return 'No open positions.';
38
+ // Enrich with live prices
39
+ let totalPnlCents = 0;
40
+ const lines = [];
41
+ for (const pos of positions) {
42
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
43
+ const current = livePrice ?? pos.average_price_paid;
44
+ const pnlCents = (current - pos.average_price_paid) * pos.quantity;
45
+ totalPnlCents += pnlCents;
46
+ const pnl = (0, format_js_1.fmtDollar)(pnlCents);
47
+ lines.push(`${pos.ticker.slice(0, 28).padEnd(29)} ${String(pos.quantity).padStart(5)} ${String(pos.average_price_paid).padStart(3)}¢→${String(current).padStart(3)}¢ ${pnl}`);
48
+ }
49
+ let text = `📊 <b>Positions</b>\n<pre>`;
50
+ text += lines.join('\n');
51
+ text += `\n${'─'.repeat(50)}\nTotal P&L: ${(0, format_js_1.fmtDollar)(totalPnlCents)}`;
52
+ text += `</pre>`;
53
+ return text;
54
+ }
55
+ async function handleEdges(client) {
56
+ const { theses } = await client.listTheses();
57
+ const active = (theses || []).filter((t) => t.status === 'active');
58
+ const results = await Promise.allSettled(active.map(async (t) => {
59
+ const ctx = await client.getContext(t.id);
60
+ return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
61
+ }));
62
+ const allEdges = [];
63
+ for (const r of results) {
64
+ if (r.status === 'fulfilled')
65
+ allEdges.push(...r.value);
66
+ }
67
+ allEdges.sort((a, b) => Math.abs(b.edge || 0) - Math.abs(a.edge || 0));
68
+ const top = allEdges.slice(0, 10);
69
+ if (top.length === 0)
70
+ return 'No edges found.';
71
+ let text = `📈 <b>Top Edges</b>\n<pre>`;
72
+ for (const e of top) {
73
+ const name = (e.market || '???').slice(0, 35);
74
+ const liq = e.orderbook?.liquidityScore || '?';
75
+ text += `${name.padEnd(36)} +${String(e.edge || 0).padStart(2)}¢ ${liq}\n`;
76
+ }
77
+ text += `</pre>`;
78
+ return text;
79
+ }
80
+ async function handleBalance() {
81
+ if (!(0, kalshi_js_1.isKalshiConfigured)())
82
+ return '⚠️ Kalshi not configured.';
83
+ const bal = await (0, kalshi_js_1.getBalance)();
84
+ if (!bal)
85
+ return '⚠️ Failed to fetch balance.';
86
+ return `💰 Balance: <b>$${bal.balance.toFixed(2)}</b> | Portfolio: <b>$${bal.portfolioValue.toFixed(2)}</b>`;
87
+ }
88
+ async function handleOrders() {
89
+ if (!(0, kalshi_js_1.isKalshiConfigured)())
90
+ return '⚠️ Kalshi not configured.';
91
+ const result = await (0, kalshi_js_1.getOrders)({ status: 'resting', limit: 20 });
92
+ if (!result || !result.orders || result.orders.length === 0)
93
+ return 'No resting orders.';
94
+ let text = `📋 <b>Resting Orders</b>\n<pre>`;
95
+ for (const o of result.orders) {
96
+ const qty = Math.round(parseFloat(o.remaining_count_fp || '0'));
97
+ const price = Math.round(parseFloat(o.yes_price_dollars || '0') * 100);
98
+ const ticker = (o.ticker || '???').slice(0, 25);
99
+ text += `${o.action} ${qty}x ${ticker} @ ${price}¢\n`;
100
+ }
101
+ text += `</pre>`;
102
+ return text;
103
+ }
104
+ async function handleEval(client, thesisId) {
105
+ await client.evaluate(thesisId);
106
+ return '⚡ Evaluation triggered. Results in ~2 minutes.';
107
+ }
108
+ async function handleList(client) {
109
+ const { theses } = await client.listTheses();
110
+ if (!theses || theses.length === 0)
111
+ return 'No theses.';
112
+ let text = `📝 <b>Theses</b>\n<pre>`;
113
+ for (const t of theses) {
114
+ const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
115
+ const title = (t.rawThesis || t.thesis || '').slice(0, 50);
116
+ text += `${t.id.slice(0, 8)} ${String(conf).padStart(3)}% ${t.status.padEnd(8)} ${title}\n`;
117
+ }
118
+ text += `</pre>`;
119
+ return text;
120
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Telegram message formatting utilities
3
+ */
4
+ /** Split long text into chunks under Telegram's 4096 char limit */
5
+ export declare function splitMessage(text: string, maxLen?: number): string[];
6
+ /** Format a number as dollars */
7
+ export declare function fmtDollar(cents: number): string;
8
+ /** Simple sparkline */
9
+ export declare function sparkline(values: number[]): string;
10
+ /** Escape HTML special characters for Telegram HTML parse mode */
11
+ export declare function escapeHtml(s: string): string;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ /**
3
+ * Telegram message formatting utilities
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.splitMessage = splitMessage;
7
+ exports.fmtDollar = fmtDollar;
8
+ exports.sparkline = sparkline;
9
+ exports.escapeHtml = escapeHtml;
10
+ /** Split long text into chunks under Telegram's 4096 char limit */
11
+ function splitMessage(text, maxLen = 4000) {
12
+ if (text.length <= maxLen)
13
+ return [text];
14
+ const chunks = [];
15
+ let remaining = text;
16
+ while (remaining.length > 0) {
17
+ if (remaining.length <= maxLen) {
18
+ chunks.push(remaining);
19
+ break;
20
+ }
21
+ // Find a good split point (newline near maxLen)
22
+ let splitAt = remaining.lastIndexOf('\n', maxLen);
23
+ if (splitAt < maxLen * 0.5)
24
+ splitAt = maxLen;
25
+ chunks.push(remaining.slice(0, splitAt));
26
+ remaining = remaining.slice(splitAt);
27
+ }
28
+ return chunks;
29
+ }
30
+ /** Format a number as dollars */
31
+ function fmtDollar(cents) {
32
+ const d = cents / 100;
33
+ return d >= 0 ? `+$${d.toFixed(2)}` : `-$${Math.abs(d).toFixed(2)}`;
34
+ }
35
+ /** Simple sparkline */
36
+ function sparkline(values) {
37
+ if (values.length === 0)
38
+ return '';
39
+ const min = Math.min(...values);
40
+ const max = Math.max(...values);
41
+ const range = max - min || 1;
42
+ const blocks = '▁▂▃▄▅▆▇█';
43
+ return values.map(v => {
44
+ const idx = Math.round(((v - min) / range) * 7);
45
+ return blocks[idx];
46
+ }).join('');
47
+ }
48
+ /** Escape HTML special characters for Telegram HTML parse mode */
49
+ function escapeHtml(s) {
50
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
51
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const format_js_1 = require("./format.js");
5
+ (0, vitest_1.describe)('splitMessage', () => {
6
+ (0, vitest_1.it)('returns single chunk for short text', () => {
7
+ (0, vitest_1.expect)((0, format_js_1.splitMessage)('short text')).toEqual(['short text']);
8
+ });
9
+ (0, vitest_1.it)('splits long text at newlines', () => {
10
+ const lines = Array.from({ length: 50 }, (_, i) => `Line ${i}`).join('\n');
11
+ const chunks = (0, format_js_1.splitMessage)(lines, 100);
12
+ for (const chunk of chunks) {
13
+ (0, vitest_1.expect)(chunk.length).toBeLessThanOrEqual(100);
14
+ }
15
+ (0, vitest_1.expect)(chunks.join('')).toBe(lines);
16
+ });
17
+ (0, vitest_1.it)('splits at maxLen when no good newline found', () => {
18
+ const long = 'x'.repeat(200);
19
+ const chunks = (0, format_js_1.splitMessage)(long, 100);
20
+ (0, vitest_1.expect)(chunks.length).toBe(2);
21
+ (0, vitest_1.expect)(chunks[0].length).toBe(100);
22
+ });
23
+ (0, vitest_1.it)('handles empty string', () => {
24
+ (0, vitest_1.expect)((0, format_js_1.splitMessage)('')).toEqual(['']);
25
+ });
26
+ });
27
+ (0, vitest_1.describe)('fmtDollar', () => {
28
+ (0, vitest_1.it)('formats positive cents', () => {
29
+ (0, vitest_1.expect)((0, format_js_1.fmtDollar)(550)).toBe('+$5.50');
30
+ });
31
+ (0, vitest_1.it)('formats negative cents', () => {
32
+ (0, vitest_1.expect)((0, format_js_1.fmtDollar)(-200)).toBe('-$2.00');
33
+ });
34
+ (0, vitest_1.it)('formats zero', () => {
35
+ (0, vitest_1.expect)((0, format_js_1.fmtDollar)(0)).toBe('+$0.00');
36
+ });
37
+ });
38
+ (0, vitest_1.describe)('sparkline', () => {
39
+ (0, vitest_1.it)('generates sparkline of correct length', () => {
40
+ const result = (0, format_js_1.sparkline)([1, 5, 3, 8, 2]);
41
+ (0, vitest_1.expect)(result.length).toBe(5);
42
+ });
43
+ (0, vitest_1.it)('returns empty for empty array', () => {
44
+ (0, vitest_1.expect)((0, format_js_1.sparkline)([])).toBe('');
45
+ });
46
+ (0, vitest_1.it)('handles single value', () => {
47
+ const result = (0, format_js_1.sparkline)([5]);
48
+ (0, vitest_1.expect)(result.length).toBe(1);
49
+ });
50
+ (0, vitest_1.it)('handles equal values', () => {
51
+ const result = (0, format_js_1.sparkline)([3, 3, 3]);
52
+ (0, vitest_1.expect)(result.length).toBe(3);
53
+ });
54
+ (0, vitest_1.it)('uses correct block chars for min/max', () => {
55
+ const result = (0, format_js_1.sparkline)([0, 100]);
56
+ (0, vitest_1.expect)(result[0]).toBe('▁');
57
+ (0, vitest_1.expect)(result[1]).toBe('█');
58
+ });
59
+ });
60
+ (0, vitest_1.describe)('escapeHtml', () => {
61
+ (0, vitest_1.it)('escapes ampersands', () => {
62
+ (0, vitest_1.expect)((0, format_js_1.escapeHtml)('a&b')).toBe('a&amp;b');
63
+ });
64
+ (0, vitest_1.it)('escapes angle brackets', () => {
65
+ (0, vitest_1.expect)((0, format_js_1.escapeHtml)('<div>')).toBe('&lt;div&gt;');
66
+ });
67
+ (0, vitest_1.it)('escapes multiple characters', () => {
68
+ (0, vitest_1.expect)((0, format_js_1.escapeHtml)('<script>alert("xss")&</script>')).toBe('&lt;script&gt;alert("xss")&amp;&lt;/script&gt;');
69
+ });
70
+ (0, vitest_1.it)('leaves safe strings unchanged', () => {
71
+ (0, vitest_1.expect)((0, format_js_1.escapeHtml)('hello world')).toBe('hello world');
72
+ });
73
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Delta API poller — push confidence changes to Telegram
3
+ */
4
+ import type { Bot } from 'grammy';
5
+ import { SFClient } from '../client.js';
6
+ export declare function startPoller(bot: Bot, chatId: number, client: SFClient, thesisId: string): NodeJS.Timeout;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ /**
3
+ * Delta API poller — push confidence changes to Telegram
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.startPoller = startPoller;
7
+ const format_js_1 = require("./format.js");
8
+ function startPoller(bot, chatId, client, thesisId) {
9
+ let lastCheck = new Date().toISOString();
10
+ return setInterval(async () => {
11
+ try {
12
+ const changes = await client.getChanges(thesisId, lastCheck);
13
+ lastCheck = new Date().toISOString();
14
+ if (!changes || !changes.changed)
15
+ return;
16
+ const delta = changes.confidenceDelta ?? changes.delta ?? 0;
17
+ if (Math.abs(delta) < 0.02)
18
+ return;
19
+ const conf = typeof changes.confidence === 'number' ? Math.round(changes.confidence * 100) : '?';
20
+ const arrow = delta > 0 ? '📈' : '📉';
21
+ const sign = delta > 0 ? '+' : '';
22
+ const summary = changes.summary || changes.latestSummary || '';
23
+ let text = `${arrow} <b>Confidence ${sign}${Math.round(delta * 100)}% → ${conf}%</b>\n`;
24
+ if (summary)
25
+ text += `${(0, format_js_1.escapeHtml)(summary.slice(0, 200))}`;
26
+ await bot.api.sendMessage(chatId, text, { parse_mode: 'HTML' });
27
+ }
28
+ catch {
29
+ // Silent — polling errors should not crash the bot
30
+ }
31
+ }, 60_000); // every 60 seconds
32
+ }
package/dist/topics.d.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  *
4
4
  * Shared between dashboard, liquidity scanner, and other commands
5
5
  * that need to categorize markets by topic.
6
+ *
7
+ * Sourced from Kalshi's top series by volume (non-sports).
8
+ * Run `sf scan` to discover new series.
6
9
  */
7
10
  export declare const TOPIC_SERIES: Record<string, string[]>;
8
11
  /** Map a series prefix to a human-readable category name (for dashboard display) */
package/dist/topics.js CHANGED
@@ -4,29 +4,87 @@
4
4
  *
5
5
  * Shared between dashboard, liquidity scanner, and other commands
6
6
  * that need to categorize markets by topic.
7
+ *
8
+ * Sourced from Kalshi's top series by volume (non-sports).
9
+ * Run `sf scan` to discover new series.
7
10
  */
8
11
  Object.defineProperty(exports, "__esModule", { value: true });
9
12
  exports.RISK_CATEGORIES = exports.TOPIC_SERIES = void 0;
10
13
  exports.tickerToTopic = tickerToTopic;
11
14
  exports.TOPIC_SERIES = {
12
- oil: ['KXWTIMAX', 'KXWTIW', 'KXWTID'],
15
+ oil: ['KXWTIMAX', 'KXWTIW', 'KXWTID', 'KXWTI'],
16
+ gas: ['KXAAAGASM', 'KXAAAGASW', 'KXCPIGAS'],
17
+ fed: ['KXFEDDECISION', 'KXFED', 'KXRATECUT', 'KXRATECUTCOUNT'],
18
+ cpi: ['KXCPI', 'KXCPIYOY'],
13
19
  recession: ['KXRECSSNBER'],
14
- fed: ['KXFEDDECISION'],
15
- cpi: ['KXCPI'],
16
- gas: ['KXAAAGASM'],
17
- sp500: ['KXINXY'],
20
+ sp500: ['KXINXY', 'KXINXU', 'KXINX'],
21
+ nasdaq: ['KXNASDAQ100', 'KXNASDAQ100U', 'KXNASDAQ100Y'],
22
+ crypto: ['KXBTCD', 'KXBTC', 'KXBTC15M', 'KXBTCMAXY', 'KXBTCMINY', 'KXBTCY',
23
+ 'KXETHD', 'KXETH', 'KXETH15M', 'KXETHMAXY', 'KXETHMINY',
24
+ 'KXSOL15M', 'KXXRP15M'],
25
+ unemployment: ['KXU3', 'KXPAYROLLS'],
26
+ gdp: ['KXGDP'],
27
+ treasury: ['KXTNOTEW', 'KXTNOTED'],
28
+ geopolitics: ['KXCLOSEHORMUZ', 'KXHORMUZTRAFFICW', 'KXHORMUZTRAFFIC', 'KXHORMUZNORM',
29
+ 'KXLEADERSOUT', 'KXLEADEROUT', 'KXMADUROOUT', 'KXKHAMENEIOUT'],
30
+ elections: ['PRES', 'KXFEDCHAIRNOM', 'KXPRESNOMD', 'KXPRESNOMR', 'KXPRESPERSON',
31
+ 'KXNEXTPOPE', 'KXTRUMPOUT', 'KXCANADAPM'],
32
+ politics: ['KXGOVSHUT', 'KXGOVTSHUTDOWN', 'KXGOVSHUTLENGTH', 'KXGOVTCUTS',
33
+ 'KXTRUMPMENTION', 'KXEOWEEK', 'KXGREENLAND', 'KXCANCOALITION'],
34
+ centralbanks: ['KXCBDECISIONJAPAN', 'KXCBDECISIONENGLAND', 'KXCBDECISIONEU',
35
+ 'KXCBDECISIONAUSTRALIA', 'KXCBDECISIONCANADA', 'KXCBDECISIONCHINA',
36
+ 'KXCBDECISIONMEXICO', 'KXCBDECISIONKOREA'],
37
+ forex: ['KXUSDJPY'],
38
+ tariffs: ['KXTARIFFRATEPRC', 'KXTARIFFRATECAN', 'KXTARIFFRATECA',
39
+ 'KXTARIFFRATEINDIA', 'KXTARIFFRATEBR', 'KXTARIFFRATEEU',
40
+ 'KXTARIFFRATEJP', 'KXTARIFFRATEKR'],
41
+ tech: ['KXLLM1', 'KXTOPMODEL', 'KXALIENS'],
18
42
  };
19
43
  /** Map a series prefix to a human-readable category name (for dashboard display) */
20
44
  exports.RISK_CATEGORIES = {
21
45
  KXWTIMAX: 'Oil',
46
+ KXWTIW: 'Oil',
47
+ KXWTID: 'Oil',
22
48
  KXWTI: 'Oil',
23
- KXRECSSNBER: 'Recession',
24
49
  KXAAAGASM: 'Gas',
50
+ KXAAAGASW: 'Gas',
51
+ KXCPIGAS: 'Gas',
52
+ KXRECSSNBER: 'Recession',
25
53
  KXCPI: 'Inflation',
54
+ KXCPIYOY: 'Inflation',
26
55
  KXINXY: 'S&P 500',
56
+ KXINXU: 'S&P 500',
57
+ KXINX: 'S&P 500',
58
+ KXNASDAQ100: 'Nasdaq',
59
+ KXNASDAQ100U: 'Nasdaq',
60
+ KXNASDAQ100Y: 'Nasdaq',
27
61
  KXFEDDECISION: 'Fed Rate',
28
- KXUNEMPLOYMENT: 'Unemployment',
62
+ KXFED: 'Fed Rate',
63
+ KXRATECUT: 'Fed Rate',
64
+ KXRATECUTCOUNT: 'Fed Rate',
65
+ KXBTCD: 'Bitcoin',
66
+ KXBTC: 'Bitcoin',
67
+ KXBTC15M: 'Bitcoin',
68
+ KXETHD: 'Ethereum',
69
+ KXETH: 'Ethereum',
70
+ KXETH15M: 'Ethereum',
71
+ KXU3: 'Unemployment',
72
+ KXPAYROLLS: 'Jobs',
73
+ KXGDP: 'GDP',
74
+ KXTNOTEW: 'Treasury',
75
+ KXTNOTED: 'Treasury',
29
76
  KXCLOSEHORMUZ: 'Hormuz',
77
+ KXHORMUZTRAFFICW: 'Hormuz',
78
+ KXHORMUZTRAFFIC: 'Hormuz',
79
+ KXUSDJPY: 'USD/JPY',
80
+ KXGOVSHUT: 'Govt Shutdown',
81
+ KXGOVTSHUTDOWN: 'Govt Shutdown',
82
+ PRES: 'Elections',
83
+ KXFEDCHAIRNOM: 'Elections',
84
+ KXTARIFFRATEPRC: 'Tariffs',
85
+ KXCBDECISIONJAPAN: 'Central Banks',
86
+ KXCBDECISIONENGLAND: 'Central Banks',
87
+ KXCBDECISIONEU: 'Central Banks',
30
88
  };
31
89
  /**
32
90
  * Given a ticker string, return the topic name (uppercased).
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const topics_js_1 = require("./topics.js");
5
+ (0, vitest_1.describe)('tickerToTopic', () => {
6
+ (0, vitest_1.it)('matches oil tickers (longest prefix first)', () => {
7
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXWTIMAX-26DEC31-T135')).toBe('OIL');
8
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXWTID-something')).toBe('OIL');
9
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXWTIW-2026')).toBe('OIL');
10
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXWTI-26MAR24-T100')).toBe('OIL');
11
+ });
12
+ (0, vitest_1.it)('matches recession', () => {
13
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXRECSSNBER-26')).toBe('RECESSION');
14
+ });
15
+ (0, vitest_1.it)('matches fed', () => {
16
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXFEDDECISION-2026')).toBe('FED');
17
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXFED-something')).toBe('FED');
18
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXRATECUT-2026')).toBe('FED');
19
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXRATECUTCOUNT-2026')).toBe('FED');
20
+ });
21
+ (0, vitest_1.it)('matches cpi', () => {
22
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXCPI-26MAY-T0.4')).toBe('CPI');
23
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXCPIYOY-2026')).toBe('CPI');
24
+ });
25
+ (0, vitest_1.it)('matches gas', () => {
26
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXAAAGASM-26MAR31-4.40')).toBe('GAS');
27
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXAAAGASW-2026')).toBe('GAS');
28
+ });
29
+ (0, vitest_1.it)('matches sp500', () => {
30
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXINXY-26DEC31H1600-T4000')).toBe('SP500');
31
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXINXU-2026')).toBe('SP500');
32
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXINX-2026')).toBe('SP500');
33
+ });
34
+ (0, vitest_1.it)('matches nasdaq', () => {
35
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXNASDAQ100-2026')).toBe('NASDAQ');
36
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXNASDAQ100U-2026')).toBe('NASDAQ');
37
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXNASDAQ100Y-2026')).toBe('NASDAQ');
38
+ });
39
+ (0, vitest_1.it)('matches crypto', () => {
40
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXBTCD-2026')).toBe('CRYPTO');
41
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXBTC15M-2026')).toBe('CRYPTO');
42
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXBTCMAXY-2026')).toBe('CRYPTO');
43
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXETHD-2026')).toBe('CRYPTO');
44
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXETH15M-2026')).toBe('CRYPTO');
45
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXSOL15M-2026')).toBe('CRYPTO');
46
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXXRP15M-2026')).toBe('CRYPTO');
47
+ });
48
+ (0, vitest_1.it)('matches unemployment & jobs', () => {
49
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXU3-2026')).toBe('UNEMPLOYMENT');
50
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXPAYROLLS-2026')).toBe('UNEMPLOYMENT');
51
+ });
52
+ (0, vitest_1.it)('matches gdp', () => {
53
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXGDP-Q1-2026')).toBe('GDP');
54
+ });
55
+ (0, vitest_1.it)('matches geopolitics', () => {
56
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXCLOSEHORMUZ-2026')).toBe('GEOPOLITICS');
57
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXHORMUZTRAFFICW-26MAR')).toBe('GEOPOLITICS');
58
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXKHAMENEIOUT-2026')).toBe('GEOPOLITICS');
59
+ });
60
+ (0, vitest_1.it)('matches elections', () => {
61
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('PRES-2028')).toBe('ELECTIONS');
62
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXFEDCHAIRNOM-2026')).toBe('ELECTIONS');
63
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTRUMPOUT-2026')).toBe('ELECTIONS');
64
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXNEXTPOPE-2026')).toBe('ELECTIONS');
65
+ });
66
+ (0, vitest_1.it)('matches politics', () => {
67
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXGOVSHUT-2026')).toBe('POLITICS');
68
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXGOVTSHUTDOWN-2026')).toBe('POLITICS');
69
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXGREENLAND-2026')).toBe('POLITICS');
70
+ });
71
+ (0, vitest_1.it)('matches central banks', () => {
72
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXCBDECISIONJAPAN-2026')).toBe('CENTRALBANKS');
73
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXCBDECISIONEU-2026')).toBe('CENTRALBANKS');
74
+ });
75
+ (0, vitest_1.it)('matches tariffs', () => {
76
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTARIFFRATEPRC-2026')).toBe('TARIFFS');
77
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTARIFFRATECA-2026')).toBe('TARIFFS');
78
+ });
79
+ (0, vitest_1.it)('matches treasury', () => {
80
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTNOTEW-2026')).toBe('TREASURY');
81
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTNOTED-2026')).toBe('TREASURY');
82
+ });
83
+ (0, vitest_1.it)('matches forex', () => {
84
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXUSDJPY-2026')).toBe('FOREX');
85
+ });
86
+ (0, vitest_1.it)('matches tech', () => {
87
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXLLM1-2026')).toBe('TECH');
88
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('KXTOPMODEL-2026')).toBe('TECH');
89
+ });
90
+ (0, vitest_1.it)('returns OTHER for unknown tickers', () => {
91
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('UNKNOWN-TICKER')).toBe('OTHER');
92
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('RANDOMSERIES-123')).toBe('OTHER');
93
+ });
94
+ (0, vitest_1.it)('is case-insensitive', () => {
95
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('kxwtimax-lower')).toBe('OIL');
96
+ (0, vitest_1.expect)((0, topics_js_1.tickerToTopic)('kxbtcd-lower')).toBe('CRYPTO');
97
+ });
98
+ });
99
+ (0, vitest_1.describe)('TOPIC_SERIES', () => {
100
+ (0, vitest_1.it)('has all expected topics', () => {
101
+ const topics = Object.keys(topics_js_1.TOPIC_SERIES);
102
+ (0, vitest_1.expect)(topics).toEqual(vitest_1.expect.arrayContaining([
103
+ 'oil', 'gas', 'fed', 'cpi', 'recession', 'sp500', 'nasdaq',
104
+ 'crypto', 'unemployment', 'gdp', 'treasury', 'geopolitics',
105
+ 'elections', 'politics', 'centralbanks', 'forex', 'tariffs', 'tech',
106
+ ]));
107
+ });
108
+ (0, vitest_1.it)('has at least 18 topics', () => {
109
+ (0, vitest_1.expect)(Object.keys(topics_js_1.TOPIC_SERIES).length).toBeGreaterThanOrEqual(18);
110
+ });
111
+ (0, vitest_1.it)('each topic has at least one series', () => {
112
+ for (const [, series] of Object.entries(topics_js_1.TOPIC_SERIES)) {
113
+ (0, vitest_1.expect)(series.length).toBeGreaterThan(0);
114
+ }
115
+ });
116
+ });
117
+ (0, vitest_1.describe)('RISK_CATEGORIES', () => {
118
+ (0, vitest_1.it)('maps oil series to Oil', () => {
119
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXWTIMAX']).toBe('Oil');
120
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXWTI']).toBe('Oil');
121
+ });
122
+ (0, vitest_1.it)('maps crypto series', () => {
123
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXBTCD']).toBe('Bitcoin');
124
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXETHD']).toBe('Ethereum');
125
+ });
126
+ (0, vitest_1.it)('maps financial series', () => {
127
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXINXY']).toBe('S&P 500');
128
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXNASDAQ100']).toBe('Nasdaq');
129
+ (0, vitest_1.expect)(topics_js_1.RISK_CATEGORIES['KXFEDDECISION']).toBe('Fed Rate');
130
+ });
131
+ });
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Border drawing — thin box-drawing characters
3
+ */
4
+ import type { ScreenBuffer } from './screen.js';
5
+ import type { Region } from './layout.js';
6
+ export declare const rgb: (r: number, g: number, b: number) => string;
7
+ export declare const bgRgb: (r: number, g: number, b: number) => string;
8
+ export declare const CLR: {
9
+ border: string;
10
+ borderDim: string;
11
+ title: string;
12
+ text: string;
13
+ dim: string;
14
+ veryDim: string;
15
+ emerald: string;
16
+ green: string;
17
+ red: string;
18
+ yellow: string;
19
+ white: string;
20
+ bg: string;
21
+ bgPanel: string;
22
+ };
23
+ /** Draw a bordered box with title */
24
+ export declare function drawBorder(screen: ScreenBuffer, region: Region, title?: string): void;
25
+ /** Draw a horizontal divider across a region at a relative row */
26
+ export declare function drawHDivider(screen: ScreenBuffer, region: Region, relRow: number): void;
27
+ /** Pad/truncate string to exact width */
28
+ export declare function fit(s: string, width: number, align?: 'left' | 'right'): string;
29
+ /** Build sparkline from values */
30
+ export declare function sparkline(values: number[], colorFn?: (v: number) => string): Array<{
31
+ text: string;
32
+ fg?: string;
33
+ }>;