@spfunctions/cli 1.4.4 → 1.4.5

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 (71) 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 +245 -67
  9. package/dist/commands/dashboard.d.ts +6 -3
  10. package/dist/commands/dashboard.js +28 -26
  11. package/dist/commands/performance.js +9 -2
  12. package/dist/commands/telegram.d.ts +15 -0
  13. package/dist/commands/telegram.js +125 -0
  14. package/dist/config.d.ts +1 -0
  15. package/dist/config.js +1 -0
  16. package/dist/config.test.d.ts +1 -0
  17. package/dist/config.test.js +138 -0
  18. package/dist/index.js +16 -2
  19. package/dist/telegram/agent-bridge.d.ts +15 -0
  20. package/dist/telegram/agent-bridge.js +368 -0
  21. package/dist/telegram/bot.d.ts +10 -0
  22. package/dist/telegram/bot.js +297 -0
  23. package/dist/telegram/commands.d.ts +11 -0
  24. package/dist/telegram/commands.js +120 -0
  25. package/dist/telegram/format.d.ts +11 -0
  26. package/dist/telegram/format.js +51 -0
  27. package/dist/telegram/format.test.d.ts +1 -0
  28. package/dist/telegram/format.test.js +73 -0
  29. package/dist/telegram/poller.d.ts +6 -0
  30. package/dist/telegram/poller.js +32 -0
  31. package/dist/topics.test.d.ts +1 -0
  32. package/dist/topics.test.js +54 -0
  33. package/dist/tui/border.d.ts +33 -0
  34. package/dist/tui/border.js +87 -0
  35. package/dist/tui/chart.d.ts +19 -0
  36. package/dist/tui/chart.js +117 -0
  37. package/dist/tui/dashboard.d.ts +9 -0
  38. package/dist/tui/dashboard.js +779 -0
  39. package/dist/tui/layout.d.ts +16 -0
  40. package/dist/tui/layout.js +41 -0
  41. package/dist/tui/screen.d.ts +33 -0
  42. package/dist/tui/screen.js +102 -0
  43. package/dist/tui/state.d.ts +40 -0
  44. package/dist/tui/state.js +36 -0
  45. package/dist/tui/widgets/commandbar.d.ts +8 -0
  46. package/dist/tui/widgets/commandbar.js +82 -0
  47. package/dist/tui/widgets/detail.d.ts +9 -0
  48. package/dist/tui/widgets/detail.js +151 -0
  49. package/dist/tui/widgets/edges.d.ts +4 -0
  50. package/dist/tui/widgets/edges.js +33 -0
  51. package/dist/tui/widgets/liquidity.d.ts +9 -0
  52. package/dist/tui/widgets/liquidity.js +142 -0
  53. package/dist/tui/widgets/orders.d.ts +4 -0
  54. package/dist/tui/widgets/orders.js +37 -0
  55. package/dist/tui/widgets/portfolio.d.ts +4 -0
  56. package/dist/tui/widgets/portfolio.js +58 -0
  57. package/dist/tui/widgets/signals.d.ts +4 -0
  58. package/dist/tui/widgets/signals.js +31 -0
  59. package/dist/tui/widgets/statusbar.d.ts +8 -0
  60. package/dist/tui/widgets/statusbar.js +72 -0
  61. package/dist/tui/widgets/thesis.d.ts +4 -0
  62. package/dist/tui/widgets/thesis.js +66 -0
  63. package/dist/tui/widgets/trade.d.ts +9 -0
  64. package/dist/tui/widgets/trade.js +117 -0
  65. package/dist/tui/widgets/upcoming.d.ts +4 -0
  66. package/dist/tui/widgets/upcoming.js +41 -0
  67. package/dist/tui/widgets/whatif.d.ts +7 -0
  68. package/dist/tui/widgets/whatif.js +113 -0
  69. package/dist/utils.test.d.ts +1 -0
  70. package/dist/utils.test.js +111 -0
  71. package/package.json +6 -2
@@ -0,0 +1,779 @@
1
+ "use strict";
2
+ /**
3
+ * TUI Dashboard Engine — main loop
4
+ *
5
+ * Manages the render loop, data loading, keypress handling, and mode transitions.
6
+ * This is NOT the commander entry point — see commands/dashboard.ts for that.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.loadAllData = loadAllData;
10
+ exports.startDashboard = startDashboard;
11
+ const screen_js_1 = require("./screen.js");
12
+ const layout_js_1 = require("./layout.js");
13
+ const state_js_1 = require("./state.js");
14
+ const border_js_1 = require("./border.js");
15
+ const cache_js_1 = require("../cache.js");
16
+ const config_js_1 = require("../config.js");
17
+ const client_js_1 = require("../client.js");
18
+ const kalshi_js_1 = require("../kalshi.js");
19
+ const topics_js_1 = require("../topics.js");
20
+ // Widget renderers
21
+ const portfolio_js_1 = require("./widgets/portfolio.js");
22
+ const thesis_js_1 = require("./widgets/thesis.js");
23
+ const edges_js_1 = require("./widgets/edges.js");
24
+ const upcoming_js_1 = require("./widgets/upcoming.js");
25
+ const orders_js_1 = require("./widgets/orders.js");
26
+ const signals_js_1 = require("./widgets/signals.js");
27
+ const statusbar_js_1 = require("./widgets/statusbar.js");
28
+ const commandbar_js_1 = require("./widgets/commandbar.js");
29
+ const detail_js_1 = require("./widgets/detail.js");
30
+ const liquidity_js_1 = require("./widgets/liquidity.js");
31
+ const whatif_js_1 = require("./widgets/whatif.js");
32
+ const trade_js_1 = require("./widgets/trade.js");
33
+ // Refresh intervals (ms)
34
+ const REFRESH_POSITIONS = 15_000;
35
+ const REFRESH_THESES = 30_000;
36
+ const REFRESH_ORDERS = 10_000;
37
+ const REFRESH_BALANCE = 30_000;
38
+ const REFRESH_FEED = 60_000;
39
+ const REFRESH_CANDLES = 60_000;
40
+ let sfClient = null;
41
+ function getClient() {
42
+ if (sfClient)
43
+ return sfClient;
44
+ try {
45
+ const config = (0, config_js_1.loadConfig)();
46
+ if (!config.apiKey)
47
+ return null;
48
+ sfClient = new client_js_1.SFClient(config.apiKey, config.apiUrl);
49
+ return sfClient;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ // ============================================================================
56
+ // DATA LOADING
57
+ // ============================================================================
58
+ async function loadAllData(state) {
59
+ const client = getClient();
60
+ // Parallel fetch of primary data
61
+ const [positionsResult, thesesResult, ordersResult, balanceResult] = await Promise.allSettled([
62
+ (0, cache_js_1.cached)('positions', REFRESH_POSITIONS, () => (0, kalshi_js_1.getPositions)()),
63
+ client ? (0, cache_js_1.cached)('theses', REFRESH_THESES, () => client.listTheses()) : Promise.resolve(null),
64
+ (0, cache_js_1.cached)('orders', REFRESH_ORDERS, () => (0, kalshi_js_1.getOrders)({ status: 'resting' })),
65
+ (0, cache_js_1.cached)('balance', REFRESH_BALANCE, () => (0, kalshi_js_1.getBalance)()),
66
+ ]);
67
+ // Positions
68
+ if (positionsResult.status === 'fulfilled' && positionsResult.value) {
69
+ const positions = positionsResult.value;
70
+ // Enrich with live prices
71
+ const priceResults = await Promise.allSettled(positions.map(p => (0, cache_js_1.cached)(`price:${p.ticker}`, 10_000, () => (0, kalshi_js_1.getMarketPrice)(p.ticker))));
72
+ for (let i = 0; i < positions.length; i++) {
73
+ const pr = priceResults[i];
74
+ if (pr.status === 'fulfilled' && pr.value != null) {
75
+ positions[i].current_value = pr.value;
76
+ positions[i].unrealized_pnl = Math.round((pr.value - positions[i].average_price_paid) * positions[i].quantity);
77
+ }
78
+ }
79
+ state.positions = positions;
80
+ }
81
+ // Theses
82
+ if (thesesResult.status === 'fulfilled' && thesesResult.value) {
83
+ const raw = thesesResult.value;
84
+ state.theses = raw.theses || raw || [];
85
+ }
86
+ // Orders
87
+ if (ordersResult.status === 'fulfilled' && ordersResult.value) {
88
+ state.orders = ordersResult.value.orders || [];
89
+ }
90
+ // Balance
91
+ if (balanceResult.status === 'fulfilled' && balanceResult.value) {
92
+ state.balance = balanceResult.value.balance ?? 0;
93
+ }
94
+ // Fetch contexts for each thesis (parallel)
95
+ if (client && state.theses.length > 0) {
96
+ const ctxResults = await Promise.allSettled(state.theses.map(t => (0, cache_js_1.cached)(`ctx:${t.id}`, REFRESH_THESES, () => client.getContext(t.id))));
97
+ for (let i = 0; i < state.theses.length; i++) {
98
+ const cr = ctxResults[i];
99
+ if (cr.status === 'fulfilled' && cr.value) {
100
+ state.contexts.set(state.theses[i].id, cr.value);
101
+ }
102
+ }
103
+ }
104
+ // Compute edges from all contexts
105
+ computeEdges(state);
106
+ // Fetch candlesticks for position tickers
107
+ await loadCandles(state);
108
+ // Fetch feed for signals
109
+ if (client) {
110
+ try {
111
+ const feed = await (0, cache_js_1.cached)('feed', REFRESH_FEED, () => client.getFeed(48));
112
+ const entries = feed.entries || feed.feed || feed;
113
+ if (Array.isArray(entries)) {
114
+ state.signals = entries;
115
+ }
116
+ }
117
+ catch { /* ignore */ }
118
+ }
119
+ // Collect upcoming events from contexts
120
+ collectEvents(state);
121
+ // Check exchange status
122
+ checkExchangeStatus(state);
123
+ state.lastRefresh.all = Date.now();
124
+ }
125
+ function computeEdges(state) {
126
+ const allEdges = [];
127
+ for (const [, ctx] of state.contexts) {
128
+ if (!ctx?.edges)
129
+ continue;
130
+ for (const e of ctx.edges) {
131
+ allEdges.push(e);
132
+ }
133
+ }
134
+ // Dedup by marketId, keep highest |edge|
135
+ const edgeMap = new Map();
136
+ for (const e of allEdges) {
137
+ const key = e.marketId || e.ticker || e.market;
138
+ const existing = edgeMap.get(key);
139
+ if (!existing || Math.abs(e.edge ?? 0) > Math.abs(existing.edge ?? 0)) {
140
+ edgeMap.set(key, e);
141
+ }
142
+ }
143
+ // Filter out held positions, sort by |edge|
144
+ const heldTickers = new Set(state.positions.map(p => p.ticker_symbol || p.ticker));
145
+ state.edges = [...edgeMap.values()]
146
+ .filter(e => !heldTickers.has(e.marketId || e.ticker))
147
+ .sort((a, b) => Math.abs(b.edge ?? 0) - Math.abs(a.edge ?? 0));
148
+ }
149
+ async function loadCandles(state) {
150
+ const tickers = state.positions.map(p => p.ticker).filter(Boolean);
151
+ if (tickers.length === 0)
152
+ return;
153
+ try {
154
+ const now = Math.floor(Date.now() / 1000);
155
+ const sevenDaysAgo = now - 7 * 86400;
156
+ const result = await (0, cache_js_1.cached)('candles', REFRESH_CANDLES, () => (0, kalshi_js_1.getBatchCandlesticks)({ tickers, startTs: sevenDaysAgo, endTs: now }));
157
+ for (const item of result) {
158
+ if (item.market_ticker && item.candlesticks) {
159
+ // Parse candles to extract close price in cents
160
+ const parsed = item.candlesticks.map((c) => {
161
+ const bidClose = parseFloat(c.yes_bid?.close_dollars || '0');
162
+ const askClose = parseFloat(c.yes_ask?.close_dollars || '0');
163
+ const mid = bidClose > 0 && askClose > 0 ? (bidClose + askClose) / 2 : bidClose || askClose;
164
+ const closeDollars = parseFloat(c.price?.close_dollars || '0') || mid;
165
+ return {
166
+ close: Math.round(closeDollars * 100), // cents
167
+ date: c.end_period_ts ? new Date(c.end_period_ts * 1000).toISOString().slice(0, 10) : '',
168
+ end_period_ts: c.end_period_ts,
169
+ };
170
+ }).filter((c) => c.close > 0);
171
+ state.candleCache.set(item.market_ticker, parsed);
172
+ }
173
+ }
174
+ }
175
+ catch { /* ignore */ }
176
+ }
177
+ function collectEvents(state) {
178
+ const events = [];
179
+ for (const [, ctx] of state.contexts) {
180
+ if (ctx?.milestones) {
181
+ for (const m of ctx.milestones) {
182
+ events.push(m);
183
+ }
184
+ }
185
+ if (ctx?.upcomingEvents) {
186
+ for (const e of ctx.upcomingEvents) {
187
+ events.push(e);
188
+ }
189
+ }
190
+ }
191
+ // Sort by timestamp ascending (soonest first)
192
+ events.sort((a, b) => {
193
+ const ta = new Date(a.timestamp || a.date || a.time || 0).getTime();
194
+ const tb = new Date(b.timestamp || b.date || b.time || 0).getTime();
195
+ return ta - tb;
196
+ });
197
+ state.events = events.filter(e => {
198
+ const t = new Date(e.timestamp || e.date || e.time || 0).getTime();
199
+ return t > Date.now();
200
+ });
201
+ }
202
+ function checkExchangeStatus(state) {
203
+ // Kalshi exchange hours: Mon-Fri, no federal holidays
204
+ // Simplified heuristic
205
+ const now = new Date();
206
+ const day = now.getUTCDay();
207
+ state.exchangeOpen = day >= 1 && day <= 5;
208
+ }
209
+ // ============================================================================
210
+ // RENDERING
211
+ // ============================================================================
212
+ function renderFrame(screen, state) {
213
+ // Clear back buffer (it's already cleared by ScreenBuffer)
214
+ // Status bar (row 0)
215
+ (0, statusbar_js_1.renderStatusBar)(screen, 0, state);
216
+ // Command bar (last row)
217
+ (0, commandbar_js_1.renderCommandBar)(screen, screen.rows - 1, state);
218
+ // Mode-specific content
219
+ switch (state.mode) {
220
+ case 'overview':
221
+ renderOverview(screen, state);
222
+ break;
223
+ case 'detail':
224
+ (0, detail_js_1.renderDetail)(screen, (0, layout_js_1.fullLayout)(screen.cols, screen.rows), state);
225
+ break;
226
+ case 'liquidity':
227
+ (0, liquidity_js_1.renderLiquidity)(screen, (0, layout_js_1.fullLayout)(screen.cols, screen.rows), state);
228
+ break;
229
+ case 'whatif':
230
+ (0, whatif_js_1.renderWhatif)(screen, (0, layout_js_1.fullLayout)(screen.cols, screen.rows), state);
231
+ break;
232
+ case 'trade':
233
+ // Render overview or detail behind the trade overlay
234
+ if (state.prevMode === 'detail') {
235
+ (0, detail_js_1.renderDetail)(screen, (0, layout_js_1.fullLayout)(screen.cols, screen.rows), state);
236
+ }
237
+ else {
238
+ renderOverview(screen, state);
239
+ }
240
+ (0, trade_js_1.renderTrade)(screen, (0, layout_js_1.fullLayout)(screen.cols, screen.rows), state);
241
+ break;
242
+ }
243
+ screen.flush();
244
+ }
245
+ function renderOverview(screen, state) {
246
+ const isNarrow = screen.cols < 80;
247
+ const regions = isNarrow
248
+ ? (0, layout_js_1.narrowLayout)(screen.cols, screen.rows)
249
+ : (0, layout_js_1.overviewLayout)(screen.cols, screen.rows);
250
+ // Render vertical divider for wide layout
251
+ if (!isNarrow) {
252
+ const leftWidth = Math.floor(screen.cols * 0.55);
253
+ for (let r = 1; r < screen.rows - 1; r++) {
254
+ screen.write(r, leftWidth, '│', border_js_1.CLR.borderDim);
255
+ }
256
+ }
257
+ for (const region of regions) {
258
+ switch (region.name) {
259
+ case 'positions':
260
+ (0, portfolio_js_1.renderPortfolio)(screen, region, state);
261
+ break;
262
+ case 'thesis':
263
+ (0, thesis_js_1.renderThesis)(screen, region, state);
264
+ break;
265
+ case 'edges':
266
+ (0, edges_js_1.renderEdges)(screen, region, state);
267
+ break;
268
+ case 'upcoming':
269
+ (0, upcoming_js_1.renderUpcoming)(screen, region, state);
270
+ break;
271
+ case 'orders':
272
+ (0, orders_js_1.renderOrders)(screen, region, state);
273
+ break;
274
+ case 'signals':
275
+ (0, signals_js_1.renderSignals)(screen, region, state);
276
+ break;
277
+ }
278
+ }
279
+ }
280
+ // ============================================================================
281
+ // KEYPRESS HANDLING
282
+ // ============================================================================
283
+ function handleKeypress(state, key, screen, scheduleRender, cleanup) {
284
+ const ch = key.toString('utf-8');
285
+ const isEsc = key[0] === 0x1b && key.length === 1;
286
+ const isUp = key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x41;
287
+ const isDown = key[0] === 0x1b && key[1] === 0x5b && key[2] === 0x42;
288
+ const isEnter = ch === '\r' || ch === '\n';
289
+ const isCtrlC = key[0] === 0x03;
290
+ if (isCtrlC || (ch === 'q' && state.mode !== 'trade')) {
291
+ cleanup();
292
+ return;
293
+ }
294
+ state.error = null;
295
+ switch (state.mode) {
296
+ case 'overview':
297
+ handleOverviewKey(ch, isUp, isDown, isEnter, state, screen, scheduleRender, cleanup);
298
+ break;
299
+ case 'detail':
300
+ handleDetailKey(ch, isEsc, state, screen, scheduleRender);
301
+ break;
302
+ case 'liquidity':
303
+ handleLiquidityKey(ch, isUp, isDown, isEsc, state, screen, scheduleRender);
304
+ break;
305
+ case 'whatif':
306
+ handleWhatifKey(ch, isUp, isDown, isEnter, isEsc, state, screen, scheduleRender);
307
+ break;
308
+ case 'trade':
309
+ handleTradeKey(ch, isUp, isDown, isEnter, isEsc, state, screen, scheduleRender);
310
+ break;
311
+ }
312
+ }
313
+ function handleOverviewKey(ch, isUp, isDown, isEnter, state, screen, scheduleRender, cleanup) {
314
+ const list = state.focusArea === 'positions' ? state.positions : state.edges;
315
+ const maxIdx = Math.max(0, list.length - 1);
316
+ if (ch === 'j' || isDown) {
317
+ state.selectedIndex = Math.min(state.selectedIndex + 1, maxIdx);
318
+ scheduleRender();
319
+ }
320
+ else if (ch === 'k' || isUp) {
321
+ state.selectedIndex = Math.max(state.selectedIndex - 1, 0);
322
+ scheduleRender();
323
+ }
324
+ else if (ch === '\t') {
325
+ state.focusArea = state.focusArea === 'positions' ? 'edges' : 'positions';
326
+ state.selectedIndex = 0;
327
+ scheduleRender();
328
+ }
329
+ else if (isEnter) {
330
+ if (state.focusArea === 'positions' && state.positions.length > 0) {
331
+ const pos = state.positions[state.selectedIndex];
332
+ state.detailTicker = pos?.ticker || null;
333
+ state.prevMode = 'overview';
334
+ state.mode = 'detail';
335
+ scheduleRender();
336
+ }
337
+ }
338
+ else if (ch === 'l') {
339
+ state.prevMode = 'overview';
340
+ state.mode = 'liquidity';
341
+ state.liquiditySelectedIndex = 0;
342
+ loadLiquidityData(state).then(scheduleRender);
343
+ }
344
+ else if (ch === 'w') {
345
+ if (state.theses.length > 0) {
346
+ state.prevMode = 'overview';
347
+ state.mode = 'whatif';
348
+ state.whatifThesisId = state.theses[0].id;
349
+ loadWhatifData(state).then(scheduleRender);
350
+ }
351
+ }
352
+ else if (ch === 'b') {
353
+ openTrade(state, 'buy');
354
+ scheduleRender();
355
+ }
356
+ else if (ch === 's') {
357
+ openTrade(state, 'sell');
358
+ scheduleRender();
359
+ }
360
+ else if (ch === 'e') {
361
+ triggerEvaluate(state).then(scheduleRender);
362
+ }
363
+ else if (ch === 'r') {
364
+ (0, cache_js_1.invalidateAll)();
365
+ loadAllData(state).then(scheduleRender);
366
+ }
367
+ }
368
+ function handleDetailKey(ch, isEsc, state, screen, scheduleRender) {
369
+ if (isEsc) {
370
+ state.mode = 'overview';
371
+ scheduleRender();
372
+ }
373
+ else if (ch === 'b') {
374
+ openTrade(state, 'buy');
375
+ scheduleRender();
376
+ }
377
+ else if (ch === 's') {
378
+ openTrade(state, 'sell');
379
+ scheduleRender();
380
+ }
381
+ else if (ch === 'w') {
382
+ if (state.theses.length > 0) {
383
+ state.prevMode = 'detail';
384
+ state.mode = 'whatif';
385
+ state.whatifThesisId = state.theses[0].id;
386
+ loadWhatifData(state).then(scheduleRender);
387
+ }
388
+ }
389
+ }
390
+ function handleLiquidityKey(ch, isUp, isDown, isEsc, state, screen, scheduleRender) {
391
+ if (isEsc) {
392
+ state.mode = state.prevMode;
393
+ scheduleRender();
394
+ }
395
+ else if (ch === 'j' || isDown) {
396
+ state.liquiditySelectedIndex++;
397
+ scheduleRender();
398
+ }
399
+ else if (ch === 'k' || isUp) {
400
+ state.liquiditySelectedIndex = Math.max(0, state.liquiditySelectedIndex - 1);
401
+ scheduleRender();
402
+ }
403
+ else if (ch === '\t') {
404
+ // Cycle to next topic
405
+ const topics = Object.keys(topics_js_1.TOPIC_SERIES);
406
+ const idx = topics.indexOf(state.liquidityTopic);
407
+ state.liquidityTopic = topics[(idx + 1) % topics.length];
408
+ state.liquiditySelectedIndex = 0;
409
+ loadLiquidityData(state).then(scheduleRender);
410
+ }
411
+ else if (ch === 'b') {
412
+ openTradeFromLiquidity(state);
413
+ scheduleRender();
414
+ }
415
+ else if (ch === 'r') {
416
+ loadLiquidityData(state).then(scheduleRender);
417
+ }
418
+ }
419
+ function handleWhatifKey(ch, isUp, isDown, isEnter, isEsc, state, screen, scheduleRender) {
420
+ if (isEsc) {
421
+ state.mode = state.prevMode;
422
+ scheduleRender();
423
+ }
424
+ else if (ch === 'j' || isDown) {
425
+ const max = (state.whatifResult?.scenarios?.length || 1) - 1;
426
+ state.whatifScenarioIndex = Math.min(state.whatifScenarioIndex + 1, max);
427
+ scheduleRender();
428
+ }
429
+ else if (ch === 'k' || isUp) {
430
+ state.whatifScenarioIndex = Math.max(0, state.whatifScenarioIndex - 1);
431
+ scheduleRender();
432
+ }
433
+ else if (isEnter) {
434
+ // Apply scenario — could trigger re-evaluation
435
+ scheduleRender();
436
+ }
437
+ }
438
+ function handleTradeKey(ch, isUp, isDown, isEnter, isEsc, state, screen, scheduleRender) {
439
+ if (isEsc) {
440
+ state.mode = state.prevMode;
441
+ state.tradeParams = null;
442
+ state.tradeCountdown = -1;
443
+ scheduleRender();
444
+ return;
445
+ }
446
+ if (!state.tradeParams)
447
+ return;
448
+ if (ch === '\t') {
449
+ state.tradeField = state.tradeField === 'qty' ? 'price' : 'qty';
450
+ scheduleRender();
451
+ }
452
+ else if (isUp) {
453
+ if (state.tradeField === 'qty') {
454
+ state.tradeParams.qty = Math.min(state.tradeParams.qty + 1, 9999);
455
+ }
456
+ else {
457
+ state.tradeParams.price = Math.min(state.tradeParams.price + 1, 99);
458
+ }
459
+ scheduleRender();
460
+ }
461
+ else if (isDown) {
462
+ if (state.tradeField === 'qty') {
463
+ state.tradeParams.qty = Math.max(state.tradeParams.qty - 1, 1);
464
+ }
465
+ else {
466
+ state.tradeParams.price = Math.max(state.tradeParams.price - 1, 1);
467
+ }
468
+ scheduleRender();
469
+ }
470
+ else if (isEnter) {
471
+ if (state.tradeCountdown < 0) {
472
+ // Start countdown
473
+ state.tradeCountdown = 3;
474
+ scheduleRender();
475
+ startCountdown(state, screen, scheduleRender);
476
+ }
477
+ }
478
+ }
479
+ function startCountdown(state, screen, scheduleRender) {
480
+ const tick = () => {
481
+ if (state.tradeCountdown <= 0 || state.mode !== 'trade')
482
+ return;
483
+ state.tradeCountdown--;
484
+ scheduleRender();
485
+ if (state.tradeCountdown === 0) {
486
+ // Execute trade
487
+ executeTrade(state, scheduleRender);
488
+ }
489
+ else {
490
+ setTimeout(tick, 1000);
491
+ }
492
+ };
493
+ setTimeout(tick, 1000);
494
+ }
495
+ // ============================================================================
496
+ // TRADE HELPERS
497
+ // ============================================================================
498
+ function openTrade(state, action) {
499
+ let ticker = '';
500
+ if (state.focusArea === 'positions' && state.positions.length > 0) {
501
+ const pos = state.positions[state.selectedIndex];
502
+ ticker = pos?.ticker_symbol || pos?.ticker || '';
503
+ }
504
+ else if (state.focusArea === 'edges' && state.edges.length > 0) {
505
+ const edge = state.edges[state.selectedIndex];
506
+ ticker = edge?.ticker || edge?.marketId || '';
507
+ }
508
+ else if (state.detailTicker) {
509
+ ticker = state.detailTicker;
510
+ }
511
+ if (!ticker) {
512
+ state.error = 'No market selected';
513
+ return;
514
+ }
515
+ state.prevMode = state.mode;
516
+ state.mode = 'trade';
517
+ state.tradeParams = {
518
+ ticker,
519
+ side: 'yes',
520
+ action,
521
+ qty: 10,
522
+ price: 50,
523
+ };
524
+ state.tradeCountdown = -1;
525
+ state.tradeField = 'qty';
526
+ }
527
+ function openTradeFromLiquidity(state) {
528
+ const markets = state.liquidityData.get(state.liquidityTopic) || [];
529
+ if (markets.length === 0)
530
+ return;
531
+ const sel = markets[Math.min(state.liquiditySelectedIndex, markets.length - 1)];
532
+ if (!sel)
533
+ return;
534
+ state.prevMode = state.mode;
535
+ state.mode = 'trade';
536
+ state.tradeParams = {
537
+ ticker: sel.ticker || '',
538
+ side: 'yes',
539
+ action: 'buy',
540
+ qty: 10,
541
+ price: sel.bestAsk ?? sel.yes_ask ?? 50,
542
+ };
543
+ state.tradeCountdown = -1;
544
+ state.tradeField = 'qty';
545
+ }
546
+ async function executeTrade(state, scheduleRender) {
547
+ if (!state.tradeParams)
548
+ return;
549
+ try {
550
+ const config = (0, config_js_1.loadConfig)();
551
+ if (!config.tradingEnabled) {
552
+ state.error = 'Trading disabled. Run: sf setup --enable-trading';
553
+ state.tradeCountdown = -1;
554
+ scheduleRender();
555
+ return;
556
+ }
557
+ await (0, kalshi_js_1.createOrder)({
558
+ ticker: state.tradeParams.ticker,
559
+ side: state.tradeParams.side,
560
+ action: state.tradeParams.action,
561
+ type: 'limit',
562
+ count: state.tradeParams.qty,
563
+ yes_price: state.tradeParams.price,
564
+ });
565
+ state.error = null;
566
+ state.mode = state.prevMode;
567
+ state.tradeParams = null;
568
+ state.tradeCountdown = -1;
569
+ // Refresh orders
570
+ (0, cache_js_1.invalidateAll)();
571
+ await loadAllData(state);
572
+ }
573
+ catch (err) {
574
+ state.error = err instanceof Error ? err.message.slice(0, 40) : 'Trade failed';
575
+ state.tradeCountdown = -1;
576
+ }
577
+ scheduleRender();
578
+ }
579
+ // ============================================================================
580
+ // SECONDARY DATA LOADING
581
+ // ============================================================================
582
+ async function loadLiquidityData(state) {
583
+ const topic = state.liquidityTopic;
584
+ const seriesList = topics_js_1.TOPIC_SERIES[topic];
585
+ if (!seriesList)
586
+ return;
587
+ try {
588
+ // Fetch markets for each series in the topic
589
+ const allMarkets = [];
590
+ for (const series of seriesList) {
591
+ const markets = await (0, cache_js_1.cached)(`liq:${series}`, 30_000, async () => {
592
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
593
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
594
+ if (!res.ok)
595
+ return [];
596
+ const data = await res.json();
597
+ return data.markets || [];
598
+ });
599
+ // Enrich with orderbook data
600
+ for (const mkt of markets) {
601
+ const ob = await (0, cache_js_1.cached)(`ob:${mkt.ticker}`, 30_000, () => (0, kalshi_js_1.getPublicOrderbook)(mkt.ticker));
602
+ if (ob) {
603
+ const yes = (ob.yes_dollars || []).map((l) => ({
604
+ price: Math.round(parseFloat(l[0]) * 100),
605
+ qty: parseFloat(l[1]),
606
+ })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
607
+ const no = (ob.no_dollars || []).map((l) => ({
608
+ price: Math.round(parseFloat(l[0]) * 100),
609
+ qty: parseFloat(l[1]),
610
+ })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
611
+ mkt.bestBid = yes.length > 0 ? yes[0].price : 0;
612
+ mkt.bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
613
+ mkt.spread = mkt.bestAsk - mkt.bestBid;
614
+ mkt.totalDepth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0)
615
+ + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
616
+ }
617
+ allMarkets.push(mkt);
618
+ }
619
+ }
620
+ state.liquidityData.set(topic, allMarkets);
621
+ }
622
+ catch (err) {
623
+ state.error = 'Failed to load liquidity data';
624
+ }
625
+ }
626
+ async function loadWhatifData(state) {
627
+ const client = getClient();
628
+ if (!client || !state.whatifThesisId)
629
+ return;
630
+ try {
631
+ const ctx = state.contexts.get(state.whatifThesisId);
632
+ if (!ctx)
633
+ return;
634
+ // Build scenario list from causal tree nodes
635
+ const nodes = ctx.causalTree?.nodes || ctx.nodes || [];
636
+ const scenarios = nodes.slice(0, 10).map((n) => ({
637
+ name: n.name || n.title || n.id,
638
+ nodeId: n.id,
639
+ description: `Set ${n.name || n.id} to low probability`,
640
+ }));
641
+ state.whatifResult = {
642
+ scenarios,
643
+ before: {
644
+ confidence: state.theses[0]?.confidence,
645
+ edges: state.edges.slice(0, 5),
646
+ },
647
+ after: null,
648
+ };
649
+ }
650
+ catch {
651
+ state.error = 'Failed to load what-if data';
652
+ }
653
+ }
654
+ async function triggerEvaluate(state) {
655
+ const client = getClient();
656
+ if (!client || state.theses.length === 0)
657
+ return;
658
+ try {
659
+ state.error = null;
660
+ const thesis = state.theses[0];
661
+ await client.evaluate(thesis.id);
662
+ // Refresh after evaluation
663
+ (0, cache_js_1.invalidateAll)();
664
+ await loadAllData(state);
665
+ }
666
+ catch (err) {
667
+ state.error = err instanceof Error ? err.message.slice(0, 40) : 'Evaluation failed';
668
+ }
669
+ }
670
+ // ============================================================================
671
+ // MAIN LOOP
672
+ // ============================================================================
673
+ async function startDashboard() {
674
+ const screen = new screen_js_1.ScreenBuffer();
675
+ const state = (0, state_js_1.initialState)();
676
+ // Enter alternate screen, hide cursor
677
+ process.stdout.write('\x1b[?1049h');
678
+ process.stdout.write('\x1b[?25l');
679
+ // Enable raw mode
680
+ if (process.stdin.isTTY) {
681
+ process.stdin.setRawMode(true);
682
+ }
683
+ process.stdin.resume();
684
+ let renderQueued = false;
685
+ const scheduleRender = () => {
686
+ if (renderQueued)
687
+ return;
688
+ renderQueued = true;
689
+ setImmediate(() => {
690
+ renderQueued = false;
691
+ renderFrame(screen, state);
692
+ });
693
+ };
694
+ // Cleanup function
695
+ const intervals = [];
696
+ let cleaned = false;
697
+ const cleanup = () => {
698
+ if (cleaned)
699
+ return;
700
+ cleaned = true;
701
+ for (const iv of intervals)
702
+ clearInterval(iv);
703
+ // Restore terminal
704
+ process.stdout.write('\x1b[?1049l'); // Exit alternate screen
705
+ process.stdout.write('\x1b[?25h'); // Show cursor
706
+ if (process.stdin.isTTY) {
707
+ process.stdin.setRawMode(false);
708
+ }
709
+ process.stdin.pause();
710
+ process.exit(0);
711
+ };
712
+ // Handle signals
713
+ process.on('SIGINT', cleanup);
714
+ process.on('SIGTERM', cleanup);
715
+ // Load initial data
716
+ try {
717
+ await loadAllData(state);
718
+ }
719
+ catch (err) {
720
+ state.error = err instanceof Error ? err.message.slice(0, 50) : 'Failed to load data';
721
+ }
722
+ // First render
723
+ renderFrame(screen, state);
724
+ // Set up refresh intervals
725
+ intervals.push(setInterval(async () => {
726
+ try {
727
+ const positions = await (0, cache_js_1.cached)('positions', REFRESH_POSITIONS, () => (0, kalshi_js_1.getPositions)());
728
+ if (positions) {
729
+ const priceResults = await Promise.allSettled(positions.map(p => (0, cache_js_1.cached)(`price:${p.ticker}`, 10_000, () => (0, kalshi_js_1.getMarketPrice)(p.ticker))));
730
+ for (let i = 0; i < positions.length; i++) {
731
+ const pr = priceResults[i];
732
+ if (pr.status === 'fulfilled' && pr.value != null) {
733
+ positions[i].current_value = pr.value;
734
+ }
735
+ }
736
+ state.positions = positions;
737
+ computeEdges(state);
738
+ }
739
+ scheduleRender();
740
+ }
741
+ catch { /* ignore */ }
742
+ }, REFRESH_POSITIONS));
743
+ intervals.push(setInterval(async () => {
744
+ try {
745
+ const ordersResult = await (0, cache_js_1.cached)('orders', REFRESH_ORDERS, () => (0, kalshi_js_1.getOrders)({ status: 'resting' }));
746
+ if (ordersResult) {
747
+ state.orders = ordersResult.orders || [];
748
+ }
749
+ scheduleRender();
750
+ }
751
+ catch { /* ignore */ }
752
+ }, REFRESH_ORDERS));
753
+ intervals.push(setInterval(async () => {
754
+ try {
755
+ const balanceResult = await (0, cache_js_1.cached)('balance', REFRESH_BALANCE, () => (0, kalshi_js_1.getBalance)());
756
+ if (balanceResult) {
757
+ state.balance = balanceResult.balance ?? 0;
758
+ }
759
+ scheduleRender();
760
+ }
761
+ catch { /* ignore */ }
762
+ }, REFRESH_BALANCE));
763
+ intervals.push(setInterval(async () => {
764
+ try {
765
+ await loadCandles(state);
766
+ scheduleRender();
767
+ }
768
+ catch { /* ignore */ }
769
+ }, REFRESH_CANDLES));
770
+ // Keypress handler
771
+ process.stdin.on('data', (key) => {
772
+ handleKeypress(state, key, screen, scheduleRender, cleanup);
773
+ });
774
+ // Resize handler
775
+ process.stdout.on('resize', () => {
776
+ screen.resize();
777
+ scheduleRender();
778
+ });
779
+ }