@spfunctions/cli 1.4.5 → 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.
- package/dist/commands/agent.js +349 -39
- package/dist/commands/book.d.ts +17 -0
- package/dist/commands/book.js +220 -0
- 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/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 +92 -8
- 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/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
|
+
}
|
|
@@ -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
|
}
|
|
@@ -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));
|