@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
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.liquidityCommand = liquidityCommand;
10
10
  const client_js_1 = require("../client.js");
11
11
  const kalshi_js_1 = require("../kalshi.js");
12
+ const polymarket_js_1 = require("../polymarket.js");
12
13
  const topics_js_1 = require("../topics.js");
13
14
  const utils_js_1 = require("../utils.js");
14
15
  // ── Horizon classification ───────────────────────────────────────────────────
@@ -78,12 +79,28 @@ async function batchProcess(items, fn, batchSize, delayMs) {
78
79
  }
79
80
  // ── Main command ─────────────────────────────────────────────────────────────
80
81
  async function liquidityCommand(opts) {
81
- // Determine which topics to scan
82
82
  const allTopics = Object.keys(topics_js_1.TOPIC_SERIES);
83
+ // If no topic and no --all, show available topics
84
+ if (!opts.topic && !opts.all) {
85
+ console.log();
86
+ console.log(`${utils_js_1.c.bold}Available Topics${utils_js_1.c.reset} ${utils_js_1.c.dim}(${allTopics.length} topics)${utils_js_1.c.reset}`);
87
+ console.log(utils_js_1.c.dim + '─'.repeat(50) + utils_js_1.c.reset);
88
+ console.log();
89
+ for (const topic of allTopics) {
90
+ const series = topics_js_1.TOPIC_SERIES[topic];
91
+ console.log(` ${utils_js_1.c.cyan}${(0, utils_js_1.pad)(topic, 14)}${utils_js_1.c.reset} ${utils_js_1.c.dim}${series.slice(0, 3).join(', ')}${series.length > 3 ? ` +${series.length - 3} more` : ''}${utils_js_1.c.reset}`);
92
+ }
93
+ console.log();
94
+ console.log(`${utils_js_1.c.dim}Usage: sf liquidity <topic> (e.g. sf liquidity oil)${utils_js_1.c.reset}`);
95
+ console.log(`${utils_js_1.c.dim} sf liquidity --all (scan all topics)${utils_js_1.c.reset}`);
96
+ console.log();
97
+ return;
98
+ }
99
+ // Determine which topics to scan
83
100
  const topics = opts.topic
84
101
  ? allTopics.filter(t => t.toLowerCase() === opts.topic.toLowerCase())
85
102
  : allTopics;
86
- if (topics.length === 0) {
103
+ if (opts.topic && topics.length === 0) {
87
104
  const valid = allTopics.join(', ');
88
105
  console.error(`Unknown topic: ${opts.topic}. Valid topics: ${valid}`);
89
106
  process.exit(1);
@@ -119,6 +136,37 @@ async function liquidityCommand(opts) {
119
136
  topicMarkets[topic] = markets;
120
137
  }
121
138
  }
139
+ // ── Polymarket: search for matching markets ──
140
+ const scanPoly = opts.venue !== 'kalshi';
141
+ if (scanPoly) {
142
+ for (const topic of topics) {
143
+ try {
144
+ const events = await (0, polymarket_js_1.polymarketSearch)(topic, 5);
145
+ for (const event of events) {
146
+ for (const m of (event.markets || [])) {
147
+ if (!m.active || m.closed || !m.enableOrderBook)
148
+ continue;
149
+ if (!topicMarkets[topic])
150
+ topicMarkets[topic] = [];
151
+ topicMarkets[topic].push({
152
+ ticker: m.conditionId?.slice(0, 16) || m.id,
153
+ close_time: m.endDateIso || '',
154
+ venue: 'polymarket',
155
+ question: m.question || event.title,
156
+ clobTokenIds: m.clobTokenIds,
157
+ bestBid: m.bestBid,
158
+ bestAsk: m.bestAsk,
159
+ spread: m.spread,
160
+ liquidityNum: m.liquidityNum,
161
+ });
162
+ }
163
+ }
164
+ }
165
+ catch {
166
+ // skip Polymarket errors
167
+ }
168
+ }
169
+ }
122
170
  // Filter by horizon if specified, classify all markets
123
171
  const horizonFilter = opts.horizon;
124
172
  const marketInfos = [];
@@ -130,33 +178,54 @@ async function liquidityCommand(opts) {
130
178
  const horizon = classifyHorizon(closeTime);
131
179
  if (horizonFilter && horizon !== horizonFilter)
132
180
  continue;
133
- marketInfos.push({ ticker: m.ticker, closeTime, topic, horizon });
181
+ marketInfos.push({
182
+ ticker: m.ticker,
183
+ closeTime,
184
+ topic,
185
+ horizon,
186
+ venue: m.venue || 'kalshi',
187
+ question: m.question,
188
+ clobTokenIds: m.clobTokenIds,
189
+ });
134
190
  }
135
191
  }
136
192
  if (marketInfos.length === 0) {
137
193
  console.log('No markets found matching filters.');
138
194
  return;
139
195
  }
140
- // Fetch orderbooks in batches of 5, 100ms between batches
141
- const orderbooks = await batchProcess(marketInfos, async (info) => {
196
+ // Split by venue
197
+ const kalshiInfos = marketInfos.filter(m => m.venue === 'kalshi');
198
+ const polyInfos = marketInfos.filter(m => m.venue === 'polymarket');
199
+ // Fetch Kalshi orderbooks in batches
200
+ const kalshiResults = await batchProcess(kalshiInfos, async (info) => {
142
201
  const ob = await (0, kalshi_js_1.getPublicOrderbook)(info.ticker);
143
202
  return { info, ob };
144
203
  }, 5, 100);
204
+ // Fetch Polymarket orderbooks in batches
205
+ const polyResults = await batchProcess(polyInfos, async (info) => {
206
+ if (!info.clobTokenIds)
207
+ return { info, depth: null };
208
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(info.clobTokenIds);
209
+ if (!ids)
210
+ return { info, depth: null };
211
+ const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
212
+ return { info, depth };
213
+ }, 5, 100);
145
214
  // Build liquidity rows
146
215
  const rows = [];
147
- for (const result of orderbooks) {
216
+ // Process Kalshi results
217
+ for (const result of kalshiResults) {
148
218
  if (!result || !result.ob)
149
219
  continue;
150
220
  const { info, ob } = result;
151
221
  const yesDollars = ob.yes_dollars.map(([p, q]) => ({
152
222
  price: Math.round(parseFloat(p) * 100),
153
223
  qty: parseFloat(q),
154
- })).filter(l => l.price > 0);
224
+ })).filter((l) => l.price > 0);
155
225
  const noDollars = ob.no_dollars.map(([p, q]) => ({
156
226
  price: Math.round(parseFloat(p) * 100),
157
227
  qty: parseFloat(q),
158
- })).filter(l => l.price > 0);
159
- // Sort descending
228
+ })).filter((l) => l.price > 0);
160
229
  yesDollars.sort((a, b) => b.price - a.price);
161
230
  noDollars.sort((a, b) => b.price - a.price);
162
231
  const bestBid = yesDollars.length > 0 ? yesDollars[0].price : 0;
@@ -165,21 +234,40 @@ async function liquidityCommand(opts) {
165
234
  const bidDepth = yesDollars.reduce((sum, l) => sum + l.qty, 0);
166
235
  const askDepth = noDollars.reduce((sum, l) => sum + l.qty, 0);
167
236
  const slippage100 = calcSlippage100(ob.no_dollars, 100);
168
- // Filter by minDepth
169
237
  if (opts.minDepth && (bidDepth + askDepth) < opts.minDepth)
170
238
  continue;
171
239
  rows.push({
172
240
  ticker: info.ticker,
173
- shortTicker: info.ticker, // will abbreviate per-group below
241
+ shortTicker: info.ticker,
242
+ topic: info.topic.toUpperCase(),
174
243
  horizon: info.horizon,
175
244
  closeTime: info.closeTime,
176
- bestBid,
177
- bestAsk,
178
- spread,
179
- bidDepth,
180
- askDepth,
181
- slippage100,
245
+ bestBid, bestAsk, spread, bidDepth, askDepth, slippage100,
182
246
  held: heldTickers.has(info.ticker),
247
+ venue: 'kalshi',
248
+ });
249
+ }
250
+ // Process Polymarket results
251
+ for (const result of polyResults) {
252
+ if (!result || !result.depth)
253
+ continue;
254
+ const { info, depth } = result;
255
+ if (opts.minDepth && (depth.bidDepthTop3 + depth.askDepthTop3) < opts.minDepth)
256
+ continue;
257
+ rows.push({
258
+ ticker: (info.question || info.ticker).slice(0, 30),
259
+ shortTicker: (info.question || info.ticker).slice(0, 30),
260
+ topic: info.topic.toUpperCase(),
261
+ horizon: info.horizon,
262
+ closeTime: info.closeTime,
263
+ bestBid: depth.bestBid,
264
+ bestAsk: depth.bestAsk,
265
+ spread: depth.spread,
266
+ bidDepth: depth.totalBidDepth,
267
+ askDepth: depth.totalAskDepth,
268
+ slippage100: '-',
269
+ held: false,
270
+ venue: 'polymarket',
183
271
  });
184
272
  }
185
273
  // ── JSON output ────────────────────────────────────────────────────────────
@@ -192,21 +280,10 @@ async function liquidityCommand(opts) {
192
280
  console.log();
193
281
  console.log(`${utils_js_1.c.bold}Liquidity Scanner${utils_js_1.c.reset} ${utils_js_1.c.dim}(${now} UTC)${utils_js_1.c.reset}`);
194
282
  console.log(utils_js_1.c.dim + '─'.repeat(68) + utils_js_1.c.reset);
195
- // Group rows by topic → horizon
283
+ // Group rows by topic → horizon (topic is pre-set on each row)
196
284
  const grouped = {};
197
285
  for (const row of rows) {
198
- // find which topic this ticker belongs to
199
- let topic = 'OTHER';
200
- for (const [t, series] of Object.entries(topics_js_1.TOPIC_SERIES)) {
201
- for (const s of series) {
202
- if (row.ticker.toUpperCase().startsWith(s)) {
203
- topic = t.toUpperCase();
204
- break;
205
- }
206
- }
207
- if (topic !== 'OTHER')
208
- break;
209
- }
286
+ const topic = row.topic || 'OTHER';
210
287
  if (!grouped[topic])
211
288
  grouped[topic] = {};
212
289
  if (!grouped[topic][row.horizon])
@@ -222,21 +299,28 @@ async function liquidityCommand(opts) {
222
299
  const marketRows = horizons[h];
223
300
  if (!marketRows || marketRows.length === 0)
224
301
  continue;
225
- // Find common prefix for abbreviation within this group
226
- const commonPrefix = findCommonPrefix(marketRows.map(r => r.ticker));
227
- // Abbreviate tickers
228
- for (const row of marketRows) {
229
- row.shortTicker = commonPrefix.length > 0
230
- ? row.ticker.slice(commonPrefix.length).replace(/^-/, '')
231
- : row.ticker;
232
- if (row.shortTicker.length === 0)
233
- row.shortTicker = row.ticker;
302
+ // Abbreviate Kalshi tickers (strip common prefix), keep Polymarket titles as-is
303
+ const kalshiRows = marketRows.filter(r => r.venue === 'kalshi');
304
+ if (kalshiRows.length > 1) {
305
+ const commonPrefix = findCommonPrefix(kalshiRows.map(r => r.ticker));
306
+ for (const row of kalshiRows) {
307
+ const short = commonPrefix.length > 0
308
+ ? row.ticker.slice(commonPrefix.length).replace(/^-/, '')
309
+ : row.ticker;
310
+ row.shortTicker = short.length > 0 ? short : row.ticker;
311
+ }
234
312
  }
235
- // Sort by ticker
236
- marketRows.sort((a, b) => a.ticker.localeCompare(b.ticker));
313
+ // Sort: Kalshi by ticker, Polymarket by spread
314
+ marketRows.sort((a, b) => {
315
+ if (a.venue !== b.venue)
316
+ return a.venue === 'kalshi' ? -1 : 1;
317
+ return a.venue === 'kalshi'
318
+ ? a.ticker.localeCompare(b.ticker)
319
+ : a.spread - b.spread;
320
+ });
237
321
  console.log();
238
322
  console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}${topic}${utils_js_1.c.reset} ${utils_js_1.c.dim}— ${horizonLabel(h)}${utils_js_1.c.reset}`);
239
- console.log(`${utils_js_1.c.dim}${(0, utils_js_1.pad)('Ticker', 20)} ${(0, utils_js_1.rpad)('Bid¢', 5)} ${(0, utils_js_1.rpad)('Ask¢', 5)} ${(0, utils_js_1.rpad)('Spread', 6)} ${(0, utils_js_1.rpad)('BidDep', 6)} ${(0, utils_js_1.rpad)('AskDep', 6)} ${(0, utils_js_1.rpad)('Slip100', 7)}${utils_js_1.c.reset}`);
323
+ console.log(`${utils_js_1.c.dim} ${(0, utils_js_1.pad)('Market', 22)} ${(0, utils_js_1.rpad)('Bid¢', 5)} ${(0, utils_js_1.rpad)('Ask¢', 5)} ${(0, utils_js_1.rpad)('Spread', 6)} ${(0, utils_js_1.rpad)('BidDep', 6)} ${(0, utils_js_1.rpad)('AskDep', 6)} ${(0, utils_js_1.rpad)('Slip100', 7)}${utils_js_1.c.reset}`);
240
324
  for (const row of marketRows) {
241
325
  totalMarkets++;
242
326
  if (row.held)
@@ -264,7 +348,8 @@ async function liquidityCommand(opts) {
264
348
  : row.spread <= 5
265
349
  ? `${utils_js_1.c.yellow}${spreadPadded}${utils_js_1.c.reset}`
266
350
  : `${utils_js_1.c.red}${spreadPadded}${utils_js_1.c.reset}`;
267
- console.log(`${(0, utils_js_1.pad)(row.shortTicker, 20)} ${(0, utils_js_1.rpad)(String(row.bestBid), 5)} ${(0, utils_js_1.rpad)(String(row.bestAsk), 5)} ${spreadColored} ${(0, utils_js_1.rpad)(String(Math.round(row.bidDepth)), 6)} ${(0, utils_js_1.rpad)(String(Math.round(row.askDepth)), 6)} ${(0, utils_js_1.rpad)(row.slippage100, 7)}${thinMark}${heldMark}`);
351
+ const venueTag = row.venue === 'polymarket' ? `${utils_js_1.c.blue}POLY${utils_js_1.c.reset} ` : `${utils_js_1.c.cyan}KLSH${utils_js_1.c.reset} `;
352
+ console.log(`${venueTag}${(0, utils_js_1.pad)(row.shortTicker, 22)} ${(0, utils_js_1.rpad)(String(row.bestBid), 5)} ${(0, utils_js_1.rpad)(String(row.bestAsk), 5)} ${spreadColored} ${(0, utils_js_1.rpad)(String(Math.round(row.bidDepth)), 6)} ${(0, utils_js_1.rpad)(String(Math.round(row.askDepth)), 6)} ${(0, utils_js_1.rpad)(row.slippage100, 7)}${thinMark}${heldMark}`);
268
353
  }
269
354
  }
270
355
  }
@@ -56,15 +56,22 @@ async function performanceCommand(opts) {
56
56
  if (!ticker)
57
57
  continue;
58
58
  const action = fill.action || 'buy';
59
+ const side = fill.side || 'yes';
59
60
  const count = Math.round(parseFloat(fill.count_fp || fill.count || '0'));
60
61
  const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
62
+ // buy yes = +count, sell yes = -count
63
+ // buy no = -count (short yes), sell no = +count (close short)
61
64
  let delta = count;
62
65
  if (action === 'sell')
63
- delta = -count;
66
+ delta = -delta;
67
+ if (side === 'no')
68
+ delta = -delta;
64
69
  const info = tickerMap.get(ticker) || { ticker, netQty: 0, totalCostCents: 0, totalContracts: 0, earliestFillTs: Infinity };
65
70
  info.netQty += delta;
66
71
  if (delta > 0) {
67
- info.totalCostCents += yesPrice * count;
72
+ // Entering a position — accumulate cost
73
+ const costPerContract = side === 'no' ? (100 - yesPrice) : yesPrice;
74
+ info.totalCostCents += costPerContract * count;
68
75
  info.totalContracts += count;
69
76
  }
70
77
  const fillTime = fill.created_time || fill.ts || fill.created_at;
@@ -14,6 +14,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
14
14
  exports.positionsCommand = positionsCommand;
15
15
  const client_js_1 = require("../client.js");
16
16
  const kalshi_js_1 = require("../kalshi.js");
17
+ const polymarket_js_1 = require("../polymarket.js");
18
+ const config_js_1 = require("../config.js");
17
19
  const utils_js_1 = require("../utils.js");
18
20
  async function positionsCommand(opts) {
19
21
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
@@ -23,6 +25,18 @@ async function positionsCommand(opts) {
23
25
  console.log(`${utils_js_1.c.dim}Fetching Kalshi positions...${utils_js_1.c.reset}`);
24
26
  positions = await (0, kalshi_js_1.getPositions)();
25
27
  }
28
+ // ── Step 1b: Fetch Polymarket positions (if wallet configured) ──
29
+ const config = (0, config_js_1.loadConfig)();
30
+ let polyPositions = [];
31
+ if (config.polymarketWalletAddress) {
32
+ console.log(`${utils_js_1.c.dim}Fetching Polymarket positions...${utils_js_1.c.reset}`);
33
+ try {
34
+ polyPositions = await (0, polymarket_js_1.polymarketGetPositions)(config.polymarketWalletAddress);
35
+ }
36
+ catch {
37
+ // skip
38
+ }
39
+ }
26
40
  // ── Step 2: Fetch all theses and their edges (via SF API) ──
27
41
  console.log(`${utils_js_1.c.dim}Fetching thesis edges...${utils_js_1.c.reset}`);
28
42
  let theses = [];
@@ -100,7 +114,9 @@ async function positionsCommand(opts) {
100
114
  if (opts.json) {
101
115
  console.log(JSON.stringify({
102
116
  kalshiConfigured: (0, kalshi_js_1.isKalshiConfigured)(),
117
+ polymarketConfigured: !!config.polymarketWalletAddress,
103
118
  positions: positions || [],
119
+ polymarketPositions: polyPositions,
104
120
  edges: allEdges.map(e => ({ ...e.edge, thesisId: e.thesisId })),
105
121
  }, null, 2));
106
122
  return;
@@ -169,6 +185,40 @@ async function positionsCommand(opts) {
169
185
  else {
170
186
  console.log(`\n${utils_js_1.c.yellow}Kalshi not configured.${utils_js_1.c.reset} Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH to see positions.\n`);
171
187
  }
188
+ // C) Polymarket positions
189
+ if (polyPositions.length > 0) {
190
+ (0, utils_js_1.header)('Polymarket Positions');
191
+ console.log(' ' + utils_js_1.c.bold +
192
+ (0, utils_js_1.pad)('Market', 35) +
193
+ (0, utils_js_1.rpad)('Side', 5) +
194
+ (0, utils_js_1.rpad)('Size', 8) +
195
+ (0, utils_js_1.rpad)('Avg', 6) +
196
+ (0, utils_js_1.rpad)('Now', 6) +
197
+ (0, utils_js_1.rpad)('P&L', 9) +
198
+ utils_js_1.c.reset);
199
+ console.log(' ' + utils_js_1.c.dim + '─'.repeat(75) + utils_js_1.c.reset);
200
+ for (const pos of polyPositions) {
201
+ const title = (pos.title || pos.slug || pos.asset || '').slice(0, 34);
202
+ const side = pos.outcome || 'YES';
203
+ const size = pos.size || 0;
204
+ const avgPrice = Math.round((pos.avgPrice || 0) * 100);
205
+ const curPrice = Math.round((pos.curPrice || pos.currentPrice || 0) * 100);
206
+ const pnl = pos.cashPnl || ((curPrice - avgPrice) * size / 100);
207
+ const pnlColor = pnl >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
208
+ const pnlStr = pnl >= 0 ? `+$${pnl.toFixed(2)}` : `-$${Math.abs(pnl).toFixed(2)}`;
209
+ console.log(' ' +
210
+ (0, utils_js_1.pad)(title, 35) +
211
+ (0, utils_js_1.rpad)(side.toUpperCase(), 5) +
212
+ (0, utils_js_1.rpad)(String(Math.round(size)), 8) +
213
+ (0, utils_js_1.rpad)(`${avgPrice}¢`, 6) +
214
+ (0, utils_js_1.rpad)(`${curPrice}¢`, 6) +
215
+ `${pnlColor}${(0, utils_js_1.rpad)(pnlStr, 9)}${utils_js_1.c.reset}`);
216
+ }
217
+ console.log('');
218
+ }
219
+ else if (config.polymarketWalletAddress) {
220
+ console.log(`${utils_js_1.c.dim}No open positions on Polymarket.${utils_js_1.c.reset}\n`);
221
+ }
172
222
  // B) Unpositioned edges (edges without matching positions)
173
223
  const positionedTickers = new Set((positions || []).map(p => p.ticker));
174
224
  const unpositionedEdges = allEdges.filter(e => !positionedTickers.has(e.edge.marketId));
@@ -1,6 +1,7 @@
1
1
  interface ScanOpts {
2
2
  series?: string;
3
3
  market?: string;
4
+ venue?: string;
4
5
  json?: boolean;
5
6
  apiKey?: string;
6
7
  apiUrl?: string;
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.scanCommand = scanCommand;
4
4
  const client_js_1 = require("../client.js");
5
+ const polymarket_js_1 = require("../polymarket.js");
5
6
  const utils_js_1 = require("../utils.js");
6
7
  async function scanCommand(query, opts) {
7
8
  // Mode 1: --market TICKER — single market detail
@@ -14,8 +15,8 @@ async function scanCommand(query, opts) {
14
15
  await showSeries(opts.series.toUpperCase(), opts.json);
15
16
  return;
16
17
  }
17
- // Mode 3: keyword scan across all series
18
- await keywordScan(query, opts.json);
18
+ // Mode 3: keyword scan across all series + Polymarket
19
+ await keywordScan(query, opts.json, opts.venue || 'all');
19
20
  }
20
21
  async function showMarket(ticker, json) {
21
22
  console.log(`${utils_js_1.c.dim}Fetching market ${ticker}...${utils_js_1.c.reset}`);
@@ -77,10 +78,19 @@ async function showSeries(seriesTicker, json) {
77
78
  printMarketsTable(markets);
78
79
  }
79
80
  }
80
- async function keywordScan(query, json) {
81
+ async function keywordScan(query, json, venue = 'all') {
81
82
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
82
- console.log(`${utils_js_1.c.dim}Scanning Kalshi for: "${query}"...${utils_js_1.c.reset}`);
83
- const allSeries = await (0, client_js_1.kalshiFetchAllSeries)();
83
+ // ── Polymarket search (runs in parallel with Kalshi) ───────────────────────
84
+ const polyPromise = (venue === 'kalshi')
85
+ ? Promise.resolve([])
86
+ : (0, polymarket_js_1.polymarketSearch)(query, 10).catch(() => []);
87
+ if (venue !== 'polymarket') {
88
+ console.log(`${utils_js_1.c.dim}Scanning Kalshi for: "${query}"...${utils_js_1.c.reset}`);
89
+ }
90
+ if (venue !== 'kalshi') {
91
+ console.log(`${utils_js_1.c.dim}Scanning Polymarket for: "${query}"...${utils_js_1.c.reset}`);
92
+ }
93
+ const allSeries = venue === 'polymarket' ? [] : await (0, client_js_1.kalshiFetchAllSeries)();
84
94
  // Score each series
85
95
  const thesisKeywords = [
86
96
  'oil', 'wti', 'gas', 'recession', 'gdp', 'fed', 'inflation',
@@ -126,13 +136,14 @@ async function keywordScan(query, json) {
126
136
  for (const m of markets) {
127
137
  if (m.status === 'open' || m.status === 'active') {
128
138
  allMarkets.push({
139
+ venue: 'kalshi',
129
140
  seriesTicker: s.ticker,
130
141
  ticker: m.ticker,
131
142
  title: m.title || m.subtitle || '',
132
143
  yesAsk: parseFloat(m.yes_ask_dollars || '0'),
133
144
  lastPrice: parseFloat(m.last_price_dollars || '0'),
134
145
  volume24h: parseFloat(m.volume_24h_fp || '0'),
135
- liquidity: parseFloat(m.liquidity_dollars || '0'),
146
+ liquidity: parseFloat(m.open_interest_fp || m.liquidity_dollars || '0'),
136
147
  });
137
148
  }
138
149
  }
@@ -144,27 +155,67 @@ async function keywordScan(query, json) {
144
155
  }
145
156
  }
146
157
  allMarkets.sort((a, b) => b.liquidity - a.liquidity);
158
+ // ── Polymarket results ──────────────────────────────────────────────────────
159
+ const polyEvents = await polyPromise;
160
+ const polyMarkets = [];
161
+ for (const event of polyEvents) {
162
+ for (const m of (event.markets || [])) {
163
+ if (!m.active || m.closed)
164
+ continue;
165
+ const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
166
+ const yesPrice = prices[0] || 0;
167
+ polyMarkets.push({
168
+ venue: 'polymarket',
169
+ ticker: m.conditionId?.slice(0, 16) || m.id,
170
+ title: m.groupItemTitle
171
+ ? `${event.title}: ${m.groupItemTitle}`
172
+ : m.question || event.title,
173
+ yesAsk: yesPrice,
174
+ lastPrice: m.lastTradePrice || yesPrice,
175
+ volume24h: m.volume24hr || 0,
176
+ liquidity: m.liquidityNum || 0,
177
+ });
178
+ }
179
+ }
180
+ // ── Merge & output ─────────────────────────────────────────────────────────
181
+ // Tag Kalshi markets with venue
182
+ for (const m of allMarkets) {
183
+ if (!m.venue)
184
+ m.venue = 'kalshi';
185
+ }
186
+ const combined = [...allMarkets, ...polyMarkets];
187
+ combined.sort((a, b) => b.liquidity - a.liquidity);
147
188
  if (json) {
148
- console.log(JSON.stringify(allMarkets, null, 2));
189
+ console.log(JSON.stringify(combined, null, 2));
149
190
  return;
150
191
  }
151
- (0, utils_js_1.header)(`${allMarkets.length} Live Markets`);
192
+ (0, utils_js_1.header)(`${combined.length} Live Markets (${allMarkets.length} Kalshi + ${polyMarkets.length} Polymarket)`);
152
193
  console.log(utils_js_1.c.bold +
153
- (0, utils_js_1.pad)('Ticker', 35) +
194
+ (0, utils_js_1.pad)('', 5) +
195
+ (0, utils_js_1.pad)('Ticker', 30) +
154
196
  (0, utils_js_1.rpad)('Yes', 6) +
155
197
  (0, utils_js_1.rpad)('Last', 6) +
156
198
  (0, utils_js_1.rpad)('Vol24h', 10) +
157
199
  (0, utils_js_1.rpad)('Liq', 10) +
158
200
  ' Title' +
159
201
  utils_js_1.c.reset);
160
- (0, utils_js_1.hr)(110);
161
- for (const m of allMarkets.slice(0, 50)) {
162
- console.log((0, utils_js_1.pad)(m.ticker, 35) +
202
+ (0, utils_js_1.hr)(120);
203
+ for (const m of combined.slice(0, 60)) {
204
+ // Venue tag: fixed 5-char column, color applied AFTER padding
205
+ const venuePad = m.venue === 'polymarket' ? 'POLY ' : 'KLSH ';
206
+ const venueColored = m.venue === 'polymarket'
207
+ ? utils_js_1.c.blue + venuePad + utils_js_1.c.reset
208
+ : utils_js_1.c.cyan + venuePad + utils_js_1.c.reset;
209
+ const tickerStr = m.venue === 'polymarket'
210
+ ? (m.ticker || '').slice(0, 28)
211
+ : (m.ticker || '').slice(0, 28);
212
+ console.log(venueColored +
213
+ (0, utils_js_1.pad)(tickerStr, 30) +
163
214
  (0, utils_js_1.rpad)(`${Math.round(m.yesAsk * 100)}¢`, 6) +
164
215
  (0, utils_js_1.rpad)(`${Math.round(m.lastPrice * 100)}¢`, 6) +
165
- (0, utils_js_1.rpad)((0, utils_js_1.vol)(m.volume24h), 10) +
166
- (0, utils_js_1.rpad)((0, utils_js_1.vol)(m.liquidity), 10) +
167
- ` ${m.title.slice(0, 55)}`);
216
+ (0, utils_js_1.rpad)((0, utils_js_1.vol)(Math.round(m.volume24h)), 10) +
217
+ (0, utils_js_1.rpad)((0, utils_js_1.vol)(Math.round(m.liquidity)), 10) +
218
+ ` ${(m.title || '').slice(0, 55)}`);
168
219
  }
169
220
  console.log('');
170
221
  }
@@ -18,6 +18,7 @@ interface SetupOpts {
18
18
  enableTrading?: boolean;
19
19
  disableTrading?: boolean;
20
20
  kalshi?: boolean;
21
+ polymarket?: boolean;
21
22
  }
22
23
  export declare function setupCommand(opts: SetupOpts): Promise<void>;
23
24
  export {};
@@ -165,6 +165,17 @@ async function setupCommand(opts) {
165
165
  blank();
166
166
  return;
167
167
  }
168
+ // ── sf setup --polymarket (reconfigure Polymarket credentials) ──────────
169
+ if (opts.polymarket) {
170
+ const existing = (0, config_js_1.loadFileConfig)();
171
+ blank();
172
+ console.log(` ${bold('Reconfigure Polymarket Credentials')}`);
173
+ blank();
174
+ await promptForPolymarket(existing);
175
+ (0, config_js_1.saveConfig)(existing);
176
+ blank();
177
+ return;
178
+ }
168
179
  // ── sf setup --enable-trading / --disable-trading ────────────────────────
169
180
  if (opts.enableTrading) {
170
181
  const existing = (0, config_js_1.loadFileConfig)();
@@ -211,6 +222,13 @@ async function showCheck() {
211
222
  else {
212
223
  info(`${dim('○')} KALSHI ${dim('skipped')}`);
213
224
  }
225
+ // Polymarket
226
+ if (config.polymarketWalletAddress) {
227
+ ok(`POLYMARKET ${dim(mask(config.polymarketWalletAddress))}`);
228
+ }
229
+ else {
230
+ info(`${dim('○')} POLYMARKET ${dim('skipped')}`);
231
+ }
214
232
  // Tavily
215
233
  if (config.tavilyKey) {
216
234
  ok(`TAVILY ${dim(mask(config.tavilyKey))}`);
@@ -318,9 +336,27 @@ async function runWizard() {
318
336
  }
319
337
  (0, config_js_1.saveConfig)(config);
320
338
  // ════════════════════════════════════════════════════════════════════════════
321
- // Step 4: Tavily
339
+ // Step 4: Polymarket
340
+ // ════════════════════════════════════════════════════════════════════════════
341
+ console.log(` ${bold('Step 4: Polymarket (optional)')}`);
342
+ blank();
343
+ const existingPolyWallet = process.env.POLYMARKET_WALLET_ADDRESS || config.polymarketWalletAddress;
344
+ if (existingPolyWallet) {
345
+ ok(`Detected wallet — ${dim(mask(existingPolyWallet))}`);
346
+ info(dim('Skipping.'));
347
+ config.polymarketWalletAddress = existingPolyWallet;
348
+ blank();
349
+ }
350
+ else {
351
+ await promptForPolymarket(config);
352
+ }
353
+ (0, config_js_1.saveConfig)(config);
354
+ if (config.polymarketWalletAddress)
355
+ process.env.POLYMARKET_WALLET_ADDRESS = config.polymarketWalletAddress;
356
+ // ════════════════════════════════════════════════════════════════════════════
357
+ // Step 5: Tavily
322
358
  // ════════════════════════════════════════════════════════════════════════════
323
- console.log(` ${bold('Step 4: News Search (optional)')}`);
359
+ console.log(` ${bold('Step 5: News Search (optional)')}`);
324
360
  blank();
325
361
  const existingTavily = process.env.TAVILY_API_KEY || config.tavilyKey;
326
362
  if (existingTavily) {
@@ -346,7 +382,7 @@ async function runWizard() {
346
382
  // Step 5: Trading
347
383
  // ════════════════════════════════════════════════════════════════════════════
348
384
  if (config.kalshiKeyId) {
349
- console.log(` ${bold('Step 5: Trading (optional)')}`);
385
+ console.log(` ${bold('Step 6: Trading (optional)')}`);
350
386
  blank();
351
387
  info('Warning: enabling this unlocks sf buy / sf sell / sf cancel.');
352
388
  info('Your Kalshi API key must have read+write permissions.');
@@ -493,7 +529,36 @@ async function promptForTavily() {
493
529
  blank();
494
530
  return answer;
495
531
  }
496
- // ─── Step 6: Thesis ──────────────────────────────────────────────────────────
532
+ async function promptForPolymarket(config) {
533
+ info('Connect Polymarket to view positions and scan orderbooks.');
534
+ info('Your Polygon wallet address is needed (starts with 0x...).');
535
+ info(`Find it at ${cyan('https://polymarket.com')} → Settings → Profile.`);
536
+ info('Press Enter to skip:');
537
+ blank();
538
+ const walletAddress = await prompt(' Wallet address > ');
539
+ if (!walletAddress) {
540
+ info(dim('Skipped.'));
541
+ blank();
542
+ return;
543
+ }
544
+ if (!walletAddress.startsWith('0x') || walletAddress.length < 40) {
545
+ fail('Invalid wallet address (must start with 0x and be 42 characters)');
546
+ info(dim('Saved anyway. You can fix it later with sf setup.'));
547
+ }
548
+ else {
549
+ ok(`Wallet: ${mask(walletAddress)}`);
550
+ }
551
+ config.polymarketWalletAddress = walletAddress;
552
+ // Optionally configure private key for future trading
553
+ info(dim('Private key (for future trading) — press Enter to skip:'));
554
+ const keyPath = await prompt(' Key path > ');
555
+ if (keyPath) {
556
+ config.polymarketPrivateKeyPath = keyPath;
557
+ ok(`Private key path: ${dim(keyPath)}`);
558
+ }
559
+ blank();
560
+ }
561
+ // ─── Step 7: Thesis ──────────────────────────────────────────────────────────
497
562
  async function handleThesisStep(config) {
498
563
  try {
499
564
  const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
@@ -501,7 +566,7 @@ async function handleThesisStep(config) {
501
566
  const theses = data.theses || [];
502
567
  const activeTheses = theses.filter((t) => t.status === 'active');
503
568
  if (activeTheses.length > 0) {
504
- console.log(` ${bold('Step 6: Theses')}`);
569
+ console.log(` ${bold('Step 7: Theses')}`);
505
570
  blank();
506
571
  ok(`Found ${activeTheses.length} active thesis(es):`);
507
572
  for (const t of activeTheses.slice(0, 5)) {
@@ -542,7 +607,7 @@ async function handleThesisStep(config) {
542
607
  return;
543
608
  }
544
609
  // No theses — offer to create one
545
- console.log(` ${bold('Step 6: Create Your First Thesis')}`);
610
+ console.log(` ${bold('Step 7: Create Your First Thesis')}`);
546
611
  blank();
547
612
  info('A thesis is your core market conviction. The system builds a causal model');
548
613
  info('from it, then continuously scans prediction markets for mispriced contracts.');
@@ -0,0 +1,15 @@
1
+ /**
2
+ * sf telegram — Start Telegram bot
3
+ *
4
+ * Token is saved to ~/.sf/config.json on first use.
5
+ * --daemon: fork to background, write PID to ~/.sf/telegram.pid
6
+ * --stop: kill running daemon
7
+ * --status: check if daemon is running
8
+ */
9
+ export declare function telegramCommand(opts: {
10
+ token?: string;
11
+ chatId?: string;
12
+ daemon?: boolean;
13
+ stop?: boolean;
14
+ status?: boolean;
15
+ }): Promise<void>;