@spfunctions/cli 1.4.5 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+ /**
3
+ * sf book — Orderbook depth, price history, and liquidity for individual markets
4
+ *
5
+ * Usage:
6
+ * sf book KXWTIMAX-26DEC31-T135 Single Kalshi market
7
+ * sf book KXWTI-T135 KXCPI-26MAY Multiple markets
8
+ * sf book --poly "oil price" Polymarket search
9
+ * sf book KXWTIMAX-26DEC31-T135 --history With 7d price history
10
+ * sf book KXWTIMAX-26DEC31-T135 --json JSON output
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.bookCommand = bookCommand;
14
+ const client_js_1 = require("../client.js");
15
+ const kalshi_js_1 = require("../kalshi.js");
16
+ const polymarket_js_1 = require("../polymarket.js");
17
+ const utils_js_1 = require("../utils.js");
18
+ async function bookCommand(tickers, opts) {
19
+ const results = [];
20
+ // ── Polymarket search mode ──
21
+ if (opts.poly) {
22
+ console.log(`${utils_js_1.c.dim}Searching Polymarket for "${opts.poly}"...${utils_js_1.c.reset}`);
23
+ const events = await (0, polymarket_js_1.polymarketSearch)(opts.poly, 10);
24
+ for (const event of events) {
25
+ for (const m of (event.markets || []).slice(0, 5)) {
26
+ if (!m.active || m.closed || !m.clobTokenIds)
27
+ continue;
28
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
29
+ if (!ids)
30
+ continue;
31
+ const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
32
+ if (!depth)
33
+ continue;
34
+ const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
35
+ const book = {
36
+ venue: 'polymarket',
37
+ ticker: m.conditionId?.slice(0, 16) || m.id,
38
+ title: m.groupItemTitle
39
+ ? `${event.title}: ${m.groupItemTitle}`
40
+ : m.question || event.title,
41
+ bestBid: depth.bestBid,
42
+ bestAsk: depth.bestAsk,
43
+ spread: depth.spread,
44
+ bidDepth: depth.totalBidDepth,
45
+ askDepth: depth.totalAskDepth,
46
+ liquidityScore: depth.liquidityScore,
47
+ volume24h: m.volume24hr || 0,
48
+ openInterest: m.liquidityNum || 0,
49
+ lastPrice: prices[0] ? (0, polymarket_js_1.toCents)(prices[0]) : 0,
50
+ expiry: m.endDateIso || null,
51
+ bidLevels: depth.levels.bids.slice(0, 5).map(l => ({ price: Math.round(parseFloat(l.price) * 100), size: Math.round(parseFloat(l.size)) })),
52
+ askLevels: depth.levels.asks.slice(0, 5).map(l => ({ price: Math.round(parseFloat(l.price) * 100), size: Math.round(parseFloat(l.size)) })),
53
+ };
54
+ // Price history sparkline
55
+ if (opts.history) {
56
+ try {
57
+ const hist = await (0, polymarket_js_1.polymarketGetPriceHistory)({ tokenId: ids[0], interval: '1w', fidelity: 360 });
58
+ if (hist.length > 0) {
59
+ book.sparkline = makeSparkline(hist.map(h => h.p * 100));
60
+ }
61
+ }
62
+ catch { /* skip */ }
63
+ }
64
+ results.push(book);
65
+ }
66
+ }
67
+ }
68
+ // ── Kalshi tickers ──
69
+ for (const ticker of tickers) {
70
+ console.log(`${utils_js_1.c.dim}Fetching ${ticker}...${utils_js_1.c.reset}`);
71
+ try {
72
+ const market = await (0, client_js_1.kalshiFetchMarket)(ticker);
73
+ const ob = await (0, kalshi_js_1.getPublicOrderbook)(ticker);
74
+ const yesBids = (ob?.yes_dollars || [])
75
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
76
+ .filter((l) => l.price > 0)
77
+ .sort((a, b) => b.price - a.price);
78
+ const noAsks = (ob?.no_dollars || [])
79
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
80
+ .filter((l) => l.price > 0)
81
+ .sort((a, b) => b.price - a.price);
82
+ const bestBid = yesBids[0]?.price || 0;
83
+ const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
84
+ const spread = bestAsk - bestBid;
85
+ const bidDepth = yesBids.reduce((s, l) => s + l.size, 0);
86
+ const askDepth = noAsks.reduce((s, l) => s + l.size, 0);
87
+ const totalDepth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) +
88
+ noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
89
+ const liq = spread <= 2 && totalDepth >= 500 ? 'high' : spread <= 5 && totalDepth >= 100 ? 'medium' : 'low';
90
+ const lastPrice = parseFloat(market.last_price_dollars || '0') * 100;
91
+ const book = {
92
+ venue: 'kalshi',
93
+ ticker: market.ticker || ticker,
94
+ title: market.title || market.subtitle || ticker,
95
+ bestBid,
96
+ bestAsk,
97
+ spread,
98
+ bidDepth,
99
+ askDepth,
100
+ liquidityScore: liq,
101
+ volume24h: parseFloat(market.volume_24h_fp || '0'),
102
+ openInterest: parseFloat(market.open_interest_fp || '0'),
103
+ lastPrice: Math.round(lastPrice),
104
+ expiry: market.close_time || market.expiration_time || null,
105
+ bidLevels: yesBids.slice(0, 5),
106
+ askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
107
+ };
108
+ // Kalshi price history
109
+ if (opts.history) {
110
+ // Try authenticated candlestick API first (requires Kalshi keys)
111
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
112
+ try {
113
+ const now = Math.floor(Date.now() / 1000);
114
+ const weekAgo = now - 7 * 86400;
115
+ const candleResults = await (0, kalshi_js_1.getBatchCandlesticks)({
116
+ tickers: [ticker],
117
+ startTs: weekAgo,
118
+ endTs: now,
119
+ periodInterval: 1440,
120
+ });
121
+ const mktEntry = candleResults.find((e) => e.market_ticker === ticker) || candleResults[0];
122
+ const mktCandles = mktEntry?.candlesticks || [];
123
+ if (Array.isArray(mktCandles) && mktCandles.length > 0) {
124
+ const prices = mktCandles.map((cd) => {
125
+ // Kalshi nests prices: cd.price.close_dollars or cd.yes_ask.close_dollars
126
+ const p = cd.price?.close_dollars ?? cd.yes_ask?.close_dollars ?? cd.yes_bid?.close_dollars ?? cd.close ?? cd.price;
127
+ const v = typeof p === 'string' ? parseFloat(p) * 100 : (typeof p === 'number' ? p : 0);
128
+ return Math.round(v);
129
+ }).filter((p) => p > 0);
130
+ if (prices.length >= 2) {
131
+ book.sparkline = makeSparkline(prices);
132
+ }
133
+ }
134
+ }
135
+ catch { /* skip */ }
136
+ }
137
+ // Fallback: show previous vs current from market data
138
+ if (!book.sparkline) {
139
+ const prev = parseFloat(market.previous_price_dollars || '0') * 100;
140
+ if (prev > 0 && Math.abs(prev - book.lastPrice) > 0) {
141
+ const delta = book.lastPrice - Math.round(prev);
142
+ const deltaColor = delta >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
143
+ const deltaStr = delta >= 0 ? `+${delta}` : `${delta}`;
144
+ book.sparkline = `prev ${Math.round(prev)}¢ → now ${book.lastPrice}¢ ${deltaColor}${deltaStr}${utils_js_1.c.reset}`;
145
+ }
146
+ }
147
+ }
148
+ results.push(book);
149
+ }
150
+ catch (err) {
151
+ console.error(`${utils_js_1.c.red}Failed to fetch ${ticker}: ${err.message}${utils_js_1.c.reset}`);
152
+ }
153
+ }
154
+ if (results.length === 0) {
155
+ console.log(`${utils_js_1.c.dim}No markets found.${utils_js_1.c.reset}`);
156
+ return;
157
+ }
158
+ // ── JSON output ──
159
+ if (opts.json) {
160
+ console.log(JSON.stringify(results, null, 2));
161
+ return;
162
+ }
163
+ // ── Formatted output ──
164
+ for (const book of results) {
165
+ const venueTag = book.venue === 'polymarket' ? `${utils_js_1.c.blue}POLY${utils_js_1.c.reset}` : `${utils_js_1.c.cyan}KLSH${utils_js_1.c.reset}`;
166
+ console.log();
167
+ console.log(`${utils_js_1.c.bold}${venueTag} ${book.title}${utils_js_1.c.reset}`);
168
+ console.log(`${utils_js_1.c.dim}${book.ticker}${utils_js_1.c.reset}`);
169
+ console.log();
170
+ // Summary line
171
+ const liqColor = book.liquidityScore === 'high' ? utils_js_1.c.green : book.liquidityScore === 'medium' ? utils_js_1.c.yellow : utils_js_1.c.red;
172
+ console.log(` Last ${utils_js_1.c.bold}${book.lastPrice}¢${utils_js_1.c.reset} ` +
173
+ `Bid ${utils_js_1.c.green}${book.bestBid}¢${utils_js_1.c.reset} ` +
174
+ `Ask ${utils_js_1.c.red}${book.bestAsk}¢${utils_js_1.c.reset} ` +
175
+ `Spread ${liqColor}${book.spread}¢${utils_js_1.c.reset} ` +
176
+ `Liq ${liqColor}${book.liquidityScore}${utils_js_1.c.reset}`);
177
+ console.log(` Vol24h ${(0, utils_js_1.vol)(book.volume24h)} ` +
178
+ `OI ${(0, utils_js_1.vol)(book.openInterest)}` +
179
+ (book.expiry ? ` Expires ${book.expiry.slice(0, 10)}` : ''));
180
+ // Sparkline
181
+ if (book.sparkline) {
182
+ console.log(` 7d ${book.sparkline}`);
183
+ }
184
+ // Orderbook depth
185
+ if (book.bidLevels && book.askLevels) {
186
+ console.log();
187
+ console.log(` ${utils_js_1.c.dim}${(0, utils_js_1.pad)('BID', 18)} ${(0, utils_js_1.pad)('ASK', 18)}${utils_js_1.c.reset}`);
188
+ console.log(` ${utils_js_1.c.dim}${'─'.repeat(38)}${utils_js_1.c.reset}`);
189
+ const maxLevels = Math.max(book.bidLevels.length, book.askLevels.length);
190
+ for (let i = 0; i < Math.min(maxLevels, 5); i++) {
191
+ const bid = book.bidLevels[i];
192
+ const ask = book.askLevels[i];
193
+ const bidStr = bid ? `${utils_js_1.c.green}${(0, utils_js_1.rpad)(`${bid.price}¢`, 5)}${utils_js_1.c.reset} ${(0, utils_js_1.rpad)(String(bid.size), 8)}` : (0, utils_js_1.pad)('', 18);
194
+ const askStr = ask ? `${utils_js_1.c.red}${(0, utils_js_1.rpad)(`${ask.price}¢`, 5)}${utils_js_1.c.reset} ${(0, utils_js_1.rpad)(String(ask.size), 8)}` : '';
195
+ console.log(` ${bidStr} ${askStr}`);
196
+ }
197
+ console.log(` ${utils_js_1.c.dim}depth: ${book.bidDepth} bid / ${book.askDepth} ask${utils_js_1.c.reset}`);
198
+ }
199
+ console.log();
200
+ }
201
+ }
202
+ // ── Sparkline helper ──
203
+ function makeSparkline(values) {
204
+ if (values.length < 2)
205
+ return '';
206
+ const min = Math.min(...values);
207
+ const max = Math.max(...values);
208
+ const range = max - min || 1;
209
+ const blocks = '▁▂▃▄▅▆▇█';
210
+ const line = values.map(v => {
211
+ const idx = Math.round(((v - min) / range) * 7);
212
+ return blocks[idx];
213
+ }).join('');
214
+ const startPrice = Math.round(values[0]);
215
+ const endPrice = Math.round(values[values.length - 1]);
216
+ const delta = endPrice - startPrice;
217
+ const deltaColor = delta >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
218
+ const deltaStr = delta >= 0 ? `+${delta}` : `${delta}`;
219
+ return `${line} ${startPrice}¢→${endPrice}¢ ${deltaColor}${deltaStr}${utils_js_1.c.reset}`;
220
+ }
@@ -1,5 +1,7 @@
1
1
  export declare function createCommand(thesis: string, opts: {
2
2
  async?: boolean;
3
+ json?: boolean;
4
+ timeout?: number;
3
5
  apiKey?: string;
4
6
  apiUrl?: string;
5
7
  }): Promise<void>;
@@ -6,15 +6,27 @@ const utils_js_1 = require("../utils.js");
6
6
  async function createCommand(thesis, opts) {
7
7
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
8
8
  const sync = !opts.async;
9
- if (sync) {
10
- console.log(`${utils_js_1.c.dim}Creating thesis (sync mode — waiting for formation)...${utils_js_1.c.reset}`);
11
- }
12
- else {
13
- console.log(`${utils_js_1.c.dim}Creating thesis (async mode)...${utils_js_1.c.reset}`);
9
+ if (!opts.json) {
10
+ if (sync) {
11
+ console.log(`${utils_js_1.c.dim}Creating thesis (sync mode — waiting for formation)...${utils_js_1.c.reset}`);
12
+ }
13
+ else {
14
+ console.log(`${utils_js_1.c.dim}Creating thesis (async mode)...${utils_js_1.c.reset}`);
15
+ }
14
16
  }
15
17
  const result = await client.createThesis(thesis, sync);
18
+ const id = result.thesis?.id || result.thesisId || result.id || null;
19
+ if (opts.json) {
20
+ console.log(JSON.stringify({ id, status: result.thesis?.status || result.status || 'forming', result }, null, 2));
21
+ return;
22
+ }
23
+ if (!id) {
24
+ console.error(`${utils_js_1.c.red}✗${utils_js_1.c.reset} Thesis creation returned no ID.`);
25
+ console.error(`${utils_js_1.c.dim}Response: ${JSON.stringify(result).slice(0, 200)}${utils_js_1.c.reset}`);
26
+ process.exit(1);
27
+ }
16
28
  console.log(`\n${utils_js_1.c.green}✓${utils_js_1.c.reset} Thesis created`);
17
- console.log(` ${utils_js_1.c.bold}ID:${utils_js_1.c.reset} ${result.thesis?.id || result.id}`);
29
+ console.log(` ${utils_js_1.c.bold}ID:${utils_js_1.c.reset} ${id}`);
18
30
  console.log(` ${utils_js_1.c.bold}Status:${utils_js_1.c.reset} ${result.thesis?.status || result.status}`);
19
31
  if (result.thesis?.confidence) {
20
32
  console.log(` ${utils_js_1.c.bold}Confidence:${utils_js_1.c.reset} ${Math.round(parseFloat(result.thesis.confidence) * 100)}%`);
@@ -25,7 +37,6 @@ async function createCommand(thesis, opts) {
25
37
  if (result.thesis?.edgeAnalysis?.edges) {
26
38
  console.log(` ${utils_js_1.c.bold}Edges:${utils_js_1.c.reset} ${result.thesis.edgeAnalysis.edges.length}`);
27
39
  }
28
- const id = result.thesis?.id || result.id;
29
40
  console.log(`\n${utils_js_1.c.dim}View: sf get ${(0, utils_js_1.shortId)(id)}${utils_js_1.c.reset}`);
30
41
  console.log(`${utils_js_1.c.dim}Context: sf context ${(0, utils_js_1.shortId)(id)}${utils_js_1.c.reset}`);
31
42
  }
@@ -11,6 +11,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.dashboardCommand = dashboardCommand;
12
12
  const client_js_1 = require("../client.js");
13
13
  const kalshi_js_1 = require("../kalshi.js");
14
+ const polymarket_js_1 = require("../polymarket.js");
15
+ const config_js_1 = require("../config.js");
14
16
  const topics_js_1 = require("../topics.js");
15
17
  const dashboard_js_1 = require("../tui/dashboard.js");
16
18
  function categorize(ticker) {
@@ -90,9 +92,36 @@ async function dashboardCommand(opts) {
90
92
  .filter(e => !positionedTickers.has(e.marketId))
91
93
  .sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
92
94
  .slice(0, 10);
95
+ // Fetch additional data for JSON mode
96
+ const [orders, balance, polyPositions] = await Promise.all([
97
+ opts?.json ? (0, kalshi_js_1.getOrders)({ status: 'resting' }).catch(() => []) : Promise.resolve([]),
98
+ opts?.json ? (0, kalshi_js_1.getBalance)().catch(() => null) : Promise.resolve(null),
99
+ opts?.json && (0, config_js_1.loadConfig)().polymarketWalletAddress
100
+ ? (0, polymarket_js_1.polymarketGetPositions)((0, config_js_1.loadConfig)().polymarketWalletAddress).catch(() => [])
101
+ : Promise.resolve([]),
102
+ ]);
103
+ // Fetch feed for recent evaluations
104
+ let feed = [];
105
+ if (opts?.json) {
106
+ try {
107
+ feed = (await client.getFeed(24, 20)).evaluations || [];
108
+ }
109
+ catch { /* skip */ }
110
+ }
93
111
  // ── JSON output ──
94
112
  if (opts?.json) {
95
- console.log(JSON.stringify({ theses, positions, unpositionedEdges }, null, 2));
113
+ console.log(JSON.stringify({
114
+ theses,
115
+ positions: positions || [],
116
+ polymarketPositions: polyPositions,
117
+ orders,
118
+ balance,
119
+ unpositionedEdges,
120
+ feed,
121
+ kalshiConfigured: (0, kalshi_js_1.isKalshiConfigured)(),
122
+ polymarketConfigured: !!(0, config_js_1.loadConfig)().polymarketWalletAddress,
123
+ timestamp: new Date().toISOString(),
124
+ }, null, 2));
96
125
  return;
97
126
  }
98
127
  // ── One-time formatted output ──
@@ -9,4 +9,6 @@ export declare function liquidityCommand(opts: {
9
9
  horizon?: string;
10
10
  minDepth?: number;
11
11
  json?: boolean;
12
+ all?: boolean;
13
+ venue?: string;
12
14
  }): Promise<void>;
@@ -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
  }
@@ -1,4 +1,5 @@
1
1
  export declare function listCommand(opts: {
2
+ json?: boolean;
2
3
  apiKey?: string;
3
4
  apiUrl?: string;
4
5
  }): Promise<void>;
@@ -6,6 +6,10 @@ const utils_js_1 = require("../utils.js");
6
6
  async function listCommand(opts) {
7
7
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
8
8
  const { theses } = await client.listTheses();
9
+ if (opts.json) {
10
+ console.log(JSON.stringify(theses, null, 2));
11
+ return;
12
+ }
9
13
  if (theses.length === 0) {
10
14
  console.log(`${utils_js_1.c.dim}No theses found.${utils_js_1.c.reset}`);
11
15
  return;