@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.
- package/README.md +2 -0
- package/dist/client.js +11 -2
- package/dist/client.test.js +1 -1
- package/dist/commands/agent.js +370 -48
- package/dist/commands/book.d.ts +17 -0
- package/dist/commands/book.js +220 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +18 -7
- package/dist/commands/dashboard.js +30 -1
- package/dist/commands/liquidity.d.ts +2 -0
- package/dist/commands/liquidity.js +128 -43
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +4 -0
- package/dist/commands/positions.js +50 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +66 -15
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +71 -6
- package/dist/config.d.ts +2 -0
- package/dist/config.js +8 -0
- package/dist/index.js +97 -11
- package/dist/polymarket.d.ts +237 -0
- package/dist/polymarket.js +353 -0
- package/dist/polymarket.test.d.ts +1 -0
- package/dist/polymarket.test.js +424 -0
- package/dist/telegram/agent-bridge.js +81 -8
- package/dist/topics.d.ts +3 -0
- package/dist/topics.js +65 -7
- package/dist/topics.test.js +83 -6
- package/dist/tui/dashboard.js +65 -30
- package/dist/tui/widgets/edges.js +5 -4
- package/dist/tui/widgets/portfolio.js +3 -2
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/dist/commands/create.js
CHANGED
|
@@ -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 (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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} ${
|
|
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({
|
|
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,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({
|
|
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
|
-
//
|
|
141
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
row
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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) =>
|
|
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)('
|
|
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
|
-
|
|
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
|
}
|
package/dist/commands/list.d.ts
CHANGED
package/dist/commands/list.js
CHANGED
|
@@ -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;
|