@spfunctions/cli 1.3.0 → 1.4.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.
@@ -9,24 +9,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.dashboardCommand = dashboardCommand;
10
10
  const client_js_1 = require("../client.js");
11
11
  const kalshi_js_1 = require("../kalshi.js");
12
- // ── Risk category mapping by Kalshi ticker prefix ────────────────────────────
13
- const RISK_CATEGORIES = {
14
- KXWTIMAX: 'Oil',
15
- KXWTI: 'Oil',
16
- KXRECSSNBER: 'Recession',
17
- KXAAAGASM: 'Gas',
18
- KXCPI: 'Inflation',
19
- KXINXY: 'S&P 500',
20
- KXFEDDECISION: 'Fed Rate',
21
- KXUNEMPLOYMENT: 'Unemployment',
22
- KXCLOSEHORMUZ: 'Hormuz',
23
- };
12
+ const topics_js_1 = require("../topics.js");
24
13
  function categorize(ticker) {
25
14
  // Match longest prefix first
26
- const sorted = Object.keys(RISK_CATEGORIES).sort((a, b) => b.length - a.length);
15
+ const sorted = Object.keys(topics_js_1.RISK_CATEGORIES).sort((a, b) => b.length - a.length);
27
16
  for (const prefix of sorted) {
28
17
  if (ticker.startsWith(prefix))
29
- return RISK_CATEGORIES[prefix];
18
+ return topics_js_1.RISK_CATEGORIES[prefix];
30
19
  }
31
20
  return 'Other';
32
21
  }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * sf liquidity — Market liquidity scanner by topic and horizon
3
+ *
4
+ * Scans known series, fetches public orderbooks, and displays
5
+ * spread/depth/slippage data grouped by topic and horizon.
6
+ */
7
+ export declare function liquidityCommand(opts: {
8
+ topic?: string;
9
+ horizon?: string;
10
+ minDepth?: number;
11
+ json?: boolean;
12
+ }): Promise<void>;
@@ -0,0 +1,293 @@
1
+ "use strict";
2
+ /**
3
+ * sf liquidity — Market liquidity scanner by topic and horizon
4
+ *
5
+ * Scans known series, fetches public orderbooks, and displays
6
+ * spread/depth/slippage data grouped by topic and horizon.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.liquidityCommand = liquidityCommand;
10
+ const client_js_1 = require("../client.js");
11
+ const kalshi_js_1 = require("../kalshi.js");
12
+ const topics_js_1 = require("../topics.js");
13
+ const utils_js_1 = require("../utils.js");
14
+ // ── Horizon classification ───────────────────────────────────────────────────
15
+ function classifyHorizon(closeTime) {
16
+ const now = Date.now();
17
+ const close = new Date(closeTime).getTime();
18
+ const daysAway = (close - now) / (1000 * 60 * 60 * 24);
19
+ if (daysAway < 7)
20
+ return 'weekly';
21
+ if (daysAway <= 35)
22
+ return 'monthly';
23
+ return 'long-term';
24
+ }
25
+ function horizonLabel(h) {
26
+ switch (h) {
27
+ case 'weekly': return 'weekly (<7d)';
28
+ case 'monthly': return 'monthly (7-35d)';
29
+ case 'long-term': return 'long-term (>35d)';
30
+ }
31
+ }
32
+ // ── Slippage calculation ─────────────────────────────────────────────────────
33
+ /**
34
+ * Calculate weighted average price to buy `qty` YES contracts
35
+ * by eating NO bids (selling NO = buying YES).
36
+ *
37
+ * no_dollars are sorted low→high by price. The best NO bid
38
+ * (highest price) is the cheapest YES ask.
39
+ */
40
+ function calcSlippage100(noDollars, qty) {
41
+ // Sort descending by price (highest no bid = cheapest yes ask)
42
+ const levels = noDollars
43
+ .map(([price, amount]) => ({
44
+ noPrice: parseFloat(price),
45
+ yesAsk: 1.0 - parseFloat(price),
46
+ qty: parseFloat(amount),
47
+ }))
48
+ .filter(l => l.noPrice > 0 && l.qty > 0)
49
+ .sort((a, b) => b.noPrice - a.noPrice); // highest no price first = lowest yes ask
50
+ let remaining = qty;
51
+ let totalCost = 0;
52
+ for (const level of levels) {
53
+ if (remaining <= 0)
54
+ break;
55
+ const fill = Math.min(remaining, level.qty);
56
+ totalCost += fill * level.yesAsk;
57
+ remaining -= fill;
58
+ }
59
+ if (remaining > 0)
60
+ return '∞';
61
+ const avgPrice = totalCost / qty;
62
+ return (avgPrice * 100).toFixed(1) + '¢';
63
+ }
64
+ // ── Batch concurrency helper ─────────────────────────────────────────────────
65
+ async function batchProcess(items, fn, batchSize, delayMs) {
66
+ const results = [];
67
+ for (let i = 0; i < items.length; i += batchSize) {
68
+ const batch = items.slice(i, i + batchSize);
69
+ const settled = await Promise.allSettled(batch.map(fn));
70
+ for (const s of settled) {
71
+ results.push(s.status === 'fulfilled' ? s.value : null);
72
+ }
73
+ if (i + batchSize < items.length) {
74
+ await new Promise(resolve => setTimeout(resolve, delayMs));
75
+ }
76
+ }
77
+ return results;
78
+ }
79
+ // ── Main command ─────────────────────────────────────────────────────────────
80
+ async function liquidityCommand(opts) {
81
+ // Determine which topics to scan
82
+ const allTopics = Object.keys(topics_js_1.TOPIC_SERIES);
83
+ const topics = opts.topic
84
+ ? allTopics.filter(t => t.toLowerCase() === opts.topic.toLowerCase())
85
+ : allTopics;
86
+ if (topics.length === 0) {
87
+ const valid = allTopics.join(', ');
88
+ console.error(`Unknown topic: ${opts.topic}. Valid topics: ${valid}`);
89
+ process.exit(1);
90
+ }
91
+ // Fetch held positions if Kalshi is configured
92
+ let heldTickers = new Set();
93
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
94
+ try {
95
+ const positions = await (0, kalshi_js_1.getPositions)();
96
+ if (positions) {
97
+ heldTickers = new Set(positions.map(p => p.ticker));
98
+ }
99
+ }
100
+ catch {
101
+ // ignore — positions are optional decoration
102
+ }
103
+ }
104
+ // Collect all markets per topic
105
+ const topicMarkets = {};
106
+ for (const topic of topics) {
107
+ const series = topics_js_1.TOPIC_SERIES[topic];
108
+ const markets = [];
109
+ for (const seriesTicker of series) {
110
+ try {
111
+ const m = await (0, client_js_1.kalshiFetchMarketsBySeries)(seriesTicker);
112
+ markets.push(...m);
113
+ }
114
+ catch {
115
+ // skip failed series
116
+ }
117
+ }
118
+ if (markets.length > 0) {
119
+ topicMarkets[topic] = markets;
120
+ }
121
+ }
122
+ // Filter by horizon if specified, classify all markets
123
+ const horizonFilter = opts.horizon;
124
+ const marketInfos = [];
125
+ for (const [topic, markets] of Object.entries(topicMarkets)) {
126
+ for (const m of markets) {
127
+ const closeTime = m.close_time || m.expiration_time || '';
128
+ if (!closeTime)
129
+ continue;
130
+ const horizon = classifyHorizon(closeTime);
131
+ if (horizonFilter && horizon !== horizonFilter)
132
+ continue;
133
+ marketInfos.push({ ticker: m.ticker, closeTime, topic, horizon });
134
+ }
135
+ }
136
+ if (marketInfos.length === 0) {
137
+ console.log('No markets found matching filters.');
138
+ return;
139
+ }
140
+ // Fetch orderbooks in batches of 5, 100ms between batches
141
+ const orderbooks = await batchProcess(marketInfos, async (info) => {
142
+ const ob = await (0, kalshi_js_1.getPublicOrderbook)(info.ticker);
143
+ return { info, ob };
144
+ }, 5, 100);
145
+ // Build liquidity rows
146
+ const rows = [];
147
+ for (const result of orderbooks) {
148
+ if (!result || !result.ob)
149
+ continue;
150
+ const { info, ob } = result;
151
+ const yesDollars = ob.yes_dollars.map(([p, q]) => ({
152
+ price: Math.round(parseFloat(p) * 100),
153
+ qty: parseFloat(q),
154
+ })).filter(l => l.price > 0);
155
+ const noDollars = ob.no_dollars.map(([p, q]) => ({
156
+ price: Math.round(parseFloat(p) * 100),
157
+ qty: parseFloat(q),
158
+ })).filter(l => l.price > 0);
159
+ // Sort descending
160
+ yesDollars.sort((a, b) => b.price - a.price);
161
+ noDollars.sort((a, b) => b.price - a.price);
162
+ const bestBid = yesDollars.length > 0 ? yesDollars[0].price : 0;
163
+ const bestAsk = noDollars.length > 0 ? (100 - noDollars[0].price) : 100;
164
+ const spread = bestAsk - bestBid;
165
+ const bidDepth = yesDollars.reduce((sum, l) => sum + l.qty, 0);
166
+ const askDepth = noDollars.reduce((sum, l) => sum + l.qty, 0);
167
+ const slippage100 = calcSlippage100(ob.no_dollars, 100);
168
+ // Filter by minDepth
169
+ if (opts.minDepth && (bidDepth + askDepth) < opts.minDepth)
170
+ continue;
171
+ rows.push({
172
+ ticker: info.ticker,
173
+ shortTicker: info.ticker, // will abbreviate per-group below
174
+ horizon: info.horizon,
175
+ closeTime: info.closeTime,
176
+ bestBid,
177
+ bestAsk,
178
+ spread,
179
+ bidDepth,
180
+ askDepth,
181
+ slippage100,
182
+ held: heldTickers.has(info.ticker),
183
+ });
184
+ }
185
+ // ── JSON output ────────────────────────────────────────────────────────────
186
+ if (opts.json) {
187
+ console.log(JSON.stringify(rows, null, 2));
188
+ return;
189
+ }
190
+ // ── Formatted output ───────────────────────────────────────────────────────
191
+ const now = new Date().toISOString().slice(0, 10);
192
+ console.log();
193
+ 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
+ console.log(utils_js_1.c.dim + '─'.repeat(68) + utils_js_1.c.reset);
195
+ // Group rows by topic → horizon
196
+ const grouped = {};
197
+ 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
+ }
210
+ if (!grouped[topic])
211
+ grouped[topic] = {};
212
+ if (!grouped[topic][row.horizon])
213
+ grouped[topic][row.horizon] = [];
214
+ grouped[topic][row.horizon].push(row);
215
+ }
216
+ let totalMarkets = 0;
217
+ let thinMarkets = 0;
218
+ let heldCount = 0;
219
+ const horizonOrder = ['weekly', 'monthly', 'long-term'];
220
+ for (const [topic, horizons] of Object.entries(grouped)) {
221
+ for (const h of horizonOrder) {
222
+ const marketRows = horizons[h];
223
+ if (!marketRows || marketRows.length === 0)
224
+ 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;
234
+ }
235
+ // Sort by ticker
236
+ marketRows.sort((a, b) => a.ticker.localeCompare(b.ticker));
237
+ console.log();
238
+ 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}`);
240
+ for (const row of marketRows) {
241
+ totalMarkets++;
242
+ if (row.held)
243
+ heldCount++;
244
+ const thin = row.spread > 5;
245
+ if (thin)
246
+ thinMarkets++;
247
+ // Color spread
248
+ let spreadStr;
249
+ if (row.spread <= 2) {
250
+ spreadStr = `${utils_js_1.c.green}${row.spread}¢${utils_js_1.c.reset}`;
251
+ }
252
+ else if (row.spread <= 5) {
253
+ spreadStr = `${utils_js_1.c.yellow}${row.spread}¢${utils_js_1.c.reset}`;
254
+ }
255
+ else {
256
+ spreadStr = `${utils_js_1.c.red}${row.spread}¢${utils_js_1.c.reset}`;
257
+ }
258
+ const thinMark = thin ? ' \u26A0\uFE0F' : '';
259
+ const heldMark = row.held ? ` ${utils_js_1.c.magenta}\u2190 held${utils_js_1.c.reset}` : '';
260
+ // Pad spread field accounting for ANSI codes
261
+ const spreadPadded = (0, utils_js_1.rpad)(`${row.spread}¢`, 6);
262
+ const spreadColored = row.spread <= 2
263
+ ? `${utils_js_1.c.green}${spreadPadded}${utils_js_1.c.reset}`
264
+ : row.spread <= 5
265
+ ? `${utils_js_1.c.yellow}${spreadPadded}${utils_js_1.c.reset}`
266
+ : `${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}`);
268
+ }
269
+ }
270
+ }
271
+ // Summary
272
+ console.log();
273
+ console.log(`${utils_js_1.c.dim}Summary: ${totalMarkets} markets | ${thinMarkets} thin (spread>5¢) | ${heldCount} held${utils_js_1.c.reset}`);
274
+ console.log();
275
+ }
276
+ // ── Helpers ──────────────────────────────────────────────────────────────────
277
+ function findCommonPrefix(strings) {
278
+ if (strings.length === 0)
279
+ return '';
280
+ if (strings.length === 1)
281
+ return '';
282
+ let prefix = strings[0];
283
+ for (let i = 1; i < strings.length; i++) {
284
+ while (!strings[i].startsWith(prefix)) {
285
+ prefix = prefix.slice(0, -1);
286
+ if (prefix.length === 0)
287
+ return '';
288
+ }
289
+ }
290
+ // Don't strip if it would leave nothing for some tickers
291
+ // Also strip trailing hyphen from prefix for cleaner display
292
+ return prefix;
293
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * sf performance — Portfolio P&L over time with thesis event annotations
3
+ */
4
+ export declare function performanceCommand(opts: {
5
+ ticker?: string;
6
+ since?: string;
7
+ json?: boolean;
8
+ }): Promise<void>;
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ /**
3
+ * sf performance — Portfolio P&L over time with thesis event annotations
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.performanceCommand = performanceCommand;
7
+ const client_js_1 = require("../client.js");
8
+ const kalshi_js_1 = require("../kalshi.js");
9
+ const config_js_1 = require("../config.js");
10
+ const utils_js_1 = require("../utils.js");
11
+ /** Abbreviate ticker: KXWTIMAX-26DEC31-T135 -> T135 */
12
+ function abbrevTicker(ticker) {
13
+ const parts = ticker.split('-');
14
+ return parts[parts.length - 1] || ticker;
15
+ }
16
+ /** Format date as "Mar 01" */
17
+ function fmtDate(d) {
18
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
19
+ return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2, '0')}`;
20
+ }
21
+ /** Format dollar amount: positive -> +$12.30, negative -> -$12.30 */
22
+ function fmtDollar(cents) {
23
+ const abs = Math.abs(cents / 100);
24
+ if (cents >= 0)
25
+ return `+$${abs.toFixed(cents === 0 ? 2 : abs >= 100 ? 1 : 2)}`;
26
+ return `-$${abs.toFixed(abs >= 100 ? 1 : 2)}`;
27
+ }
28
+ /** Date string key YYYY-MM-DD from a Date */
29
+ function dateKey(d) {
30
+ return d.toISOString().slice(0, 10);
31
+ }
32
+ async function performanceCommand(opts) {
33
+ if (!(0, kalshi_js_1.isKalshiConfigured)()) {
34
+ console.log(`${utils_js_1.c.yellow}Kalshi not configured.${utils_js_1.c.reset} Run ${utils_js_1.c.cyan}sf setup --kalshi${utils_js_1.c.reset} first.`);
35
+ return;
36
+ }
37
+ // 1. Fetch fills
38
+ const fillsResult = await (0, kalshi_js_1.getFills)({ limit: 500 });
39
+ if (!fillsResult || fillsResult.fills.length === 0) {
40
+ console.log(`${utils_js_1.c.dim}No fills found.${utils_js_1.c.reset}`);
41
+ return;
42
+ }
43
+ const fills = fillsResult.fills;
44
+ const tickerMap = new Map();
45
+ for (const fill of fills) {
46
+ const ticker = fill.ticker || fill.market_ticker || '';
47
+ if (!ticker)
48
+ continue;
49
+ const side = fill.side || 'yes';
50
+ const action = fill.action || 'buy';
51
+ const count = Math.round(parseFloat(fill.count_fp || fill.count || '0'));
52
+ const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100); // dollars string → cents int
53
+ // Determine direction: buy yes = +count, sell yes = -count
54
+ let delta = count;
55
+ if (action === 'sell')
56
+ delta = -count;
57
+ const info = tickerMap.get(ticker) || {
58
+ ticker,
59
+ netQty: 0,
60
+ totalCostCents: 0,
61
+ totalContracts: 0,
62
+ earliestFillTs: Infinity,
63
+ };
64
+ info.netQty += delta;
65
+ if (delta > 0) {
66
+ // Buying: accumulate cost
67
+ info.totalCostCents += yesPrice * count;
68
+ info.totalContracts += count;
69
+ }
70
+ // Track earliest fill
71
+ const fillTime = fill.created_time || fill.ts || fill.created_at;
72
+ if (fillTime) {
73
+ const ts = Math.floor(new Date(fillTime).getTime() / 1000);
74
+ if (ts < info.earliestFillTs)
75
+ info.earliestFillTs = ts;
76
+ }
77
+ tickerMap.set(ticker, info);
78
+ }
79
+ // 3. Filter out fully closed positions (net qty = 0)
80
+ let tickers = [...tickerMap.values()].filter(t => t.netQty !== 0);
81
+ // 4. Apply --ticker fuzzy filter
82
+ if (opts.ticker) {
83
+ const needle = opts.ticker.toLowerCase();
84
+ tickers = tickers.filter(t => t.ticker.toLowerCase().includes(needle));
85
+ }
86
+ if (tickers.length === 0) {
87
+ console.log(`${utils_js_1.c.dim}No open positions found${opts.ticker ? ` matching "${opts.ticker}"` : ''}.${utils_js_1.c.reset}`);
88
+ return;
89
+ }
90
+ // Determine date range
91
+ const sinceTs = opts.since
92
+ ? Math.floor(new Date(opts.since).getTime() / 1000)
93
+ : Math.min(...tickers.map(t => t.earliestFillTs === Infinity ? Math.floor(Date.now() / 1000) - 30 * 86400 : t.earliestFillTs));
94
+ const nowTs = Math.floor(Date.now() / 1000);
95
+ // 5. Fetch candlesticks
96
+ const candleData = await (0, kalshi_js_1.getBatchCandlesticks)({
97
+ tickers: tickers.map(t => t.ticker),
98
+ startTs: sinceTs,
99
+ endTs: nowTs,
100
+ periodInterval: 1440,
101
+ });
102
+ // Build candlestick lookup: ticker -> dateKey -> close price (cents)
103
+ const candleMap = new Map();
104
+ for (const mc of candleData) {
105
+ const priceByDate = new Map();
106
+ for (const candle of (mc.candlesticks || [])) {
107
+ // close_dollars is a string like "0.4800"
108
+ // price object may be empty; use midpoint of yes_bid.close and yes_ask.close
109
+ const bidClose = parseFloat(candle.yes_bid?.close_dollars || '0');
110
+ const askClose = parseFloat(candle.yes_ask?.close_dollars || '0');
111
+ const mid = bidClose > 0 && askClose > 0 ? (bidClose + askClose) / 2 : bidClose || askClose;
112
+ const closeDollars = parseFloat(candle.price?.close_dollars || '0') || mid;
113
+ const closeCents = Math.round(closeDollars * 100);
114
+ const ts = candle.end_period_ts || candle.period_end_ts || candle.ts;
115
+ if (ts) {
116
+ const d = new Date(ts * 1000);
117
+ priceByDate.set(dateKey(d), closeCents);
118
+ }
119
+ }
120
+ candleMap.set(mc.market_ticker, priceByDate);
121
+ }
122
+ // 6. Build daily P&L matrix
123
+ // Collect all unique dates across all tickers
124
+ const allDates = new Set();
125
+ for (const [, priceByDate] of candleMap) {
126
+ for (const dk of priceByDate.keys()) {
127
+ allDates.add(dk);
128
+ }
129
+ }
130
+ const sortedDates = [...allDates].sort();
131
+ // For each ticker compute entry price
132
+ const entryPrices = new Map();
133
+ for (const t of tickers) {
134
+ const avgEntry = t.totalContracts > 0 ? Math.round(t.totalCostCents / t.totalContracts) : 0;
135
+ entryPrices.set(t.ticker, avgEntry);
136
+ }
137
+ const dailyRows = [];
138
+ for (const dk of sortedDates) {
139
+ const pnlByTicker = new Map();
140
+ let total = 0;
141
+ for (const t of tickers) {
142
+ const prices = candleMap.get(t.ticker);
143
+ const closePrice = prices?.get(dk);
144
+ if (closePrice !== undefined) {
145
+ const entry = entryPrices.get(t.ticker) || 0;
146
+ const pnl = (closePrice - entry) * t.netQty;
147
+ pnlByTicker.set(t.ticker, pnl);
148
+ total += pnl;
149
+ }
150
+ }
151
+ dailyRows.push({ date: dk, pnlByTicker, total });
152
+ }
153
+ const events = [];
154
+ try {
155
+ const config = (0, config_js_1.loadConfig)();
156
+ const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
157
+ const feedData = await client.getFeed(720);
158
+ const feedItems = feedData?.items || feedData?.events || feedData || [];
159
+ if (Array.isArray(feedItems)) {
160
+ for (const item of feedItems) {
161
+ const confDelta = item.confidenceDelta ?? item.confidence_delta ?? 0;
162
+ if (Math.abs(confDelta) > 0.03) {
163
+ const itemDate = item.createdAt || item.created_at || item.timestamp || '';
164
+ if (itemDate) {
165
+ events.push({
166
+ date: dateKey(new Date(itemDate)),
167
+ direction: confDelta > 0 ? 'up' : 'down',
168
+ deltaPct: Math.round(confDelta * 100),
169
+ summary: item.summary || item.title || item.description || '',
170
+ });
171
+ }
172
+ }
173
+ }
174
+ }
175
+ }
176
+ catch {
177
+ // Feed unavailable — continue without events
178
+ }
179
+ // Compute summary
180
+ const totalCostCents = tickers.reduce((sum, t) => sum + t.totalCostCents, 0);
181
+ const lastRow = dailyRows.length > 0 ? dailyRows[dailyRows.length - 1] : null;
182
+ const currentPnlCents = lastRow?.total ?? 0;
183
+ const currentValueCents = totalCostCents + currentPnlCents;
184
+ const pnlPct = totalCostCents > 0 ? (currentPnlCents / totalCostCents) * 100 : 0;
185
+ // 8. Output
186
+ if (opts.json) {
187
+ console.log(JSON.stringify({
188
+ daily: dailyRows.map(row => {
189
+ const tickerPnl = {};
190
+ for (const [tk, pnl] of row.pnlByTicker) {
191
+ tickerPnl[tk] = pnl;
192
+ }
193
+ return { date: row.date, tickers: tickerPnl, total: row.total };
194
+ }),
195
+ events,
196
+ summary: {
197
+ cost: totalCostCents,
198
+ current: currentValueCents,
199
+ pnl: currentPnlCents,
200
+ pnlPct: Math.round(pnlPct * 10) / 10,
201
+ },
202
+ }, null, 2));
203
+ return;
204
+ }
205
+ // Formatted output
206
+ const startDate = sortedDates.length > 0 ? fmtDate(new Date(sortedDates[0])) : '?';
207
+ const endDate = fmtDate(new Date());
208
+ console.log();
209
+ console.log(` ${utils_js_1.c.bold}Portfolio Performance${utils_js_1.c.reset} ${utils_js_1.c.dim}(${startDate} -> ${endDate})${utils_js_1.c.reset}`);
210
+ console.log(` ${utils_js_1.c.dim}${'─'.repeat(50)}${utils_js_1.c.reset}`);
211
+ console.log();
212
+ // Column headers
213
+ const abbrevs = tickers.map(t => abbrevTicker(t.ticker));
214
+ const colWidth = 9;
215
+ const dateCol = 'Date'.padEnd(12);
216
+ const headerCols = abbrevs.map(a => (0, utils_js_1.rpad)(a, colWidth)).join('');
217
+ const totalCol = (0, utils_js_1.rpad)('Total', colWidth);
218
+ console.log(` ${utils_js_1.c.dim}${dateCol}${headerCols}${totalCol}${utils_js_1.c.reset}`);
219
+ // Rows — show at most ~30 rows, sample if more
220
+ const maxRows = 30;
221
+ let rowsToShow = dailyRows;
222
+ if (dailyRows.length > maxRows) {
223
+ // Sample evenly + always include first and last
224
+ const step = Math.ceil(dailyRows.length / maxRows);
225
+ rowsToShow = dailyRows.filter((_, i) => i === 0 || i === dailyRows.length - 1 || i % step === 0);
226
+ }
227
+ for (const row of rowsToShow) {
228
+ const d = new Date(row.date);
229
+ const dateStr = fmtDate(d).padEnd(12);
230
+ const cols = tickers.map(t => {
231
+ const pnl = row.pnlByTicker.get(t.ticker);
232
+ if (pnl === undefined)
233
+ return (0, utils_js_1.rpad)('--', colWidth);
234
+ const str = fmtDollar(pnl);
235
+ const color = pnl > 0 ? utils_js_1.c.green : pnl < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
236
+ return color + (0, utils_js_1.rpad)(str, colWidth) + utils_js_1.c.reset;
237
+ }).join('');
238
+ const totalStr = fmtDollar(row.total);
239
+ const totalColor = row.total > 0 ? utils_js_1.c.green : row.total < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
240
+ console.log(` ${dateStr}${cols}${totalColor}${(0, utils_js_1.rpad)(totalStr, colWidth)}${utils_js_1.c.reset}`);
241
+ }
242
+ // Events
243
+ if (events.length > 0) {
244
+ console.log();
245
+ // Match events to date range
246
+ const dateSet = new Set(sortedDates);
247
+ const relevantEvents = events.filter(e => dateSet.has(e.date));
248
+ for (const ev of relevantEvents.slice(0, 10)) {
249
+ const arrow = ev.direction === 'up' ? `${utils_js_1.c.green}▲${utils_js_1.c.reset}` : `${utils_js_1.c.red}▼${utils_js_1.c.reset}`;
250
+ const dateFmt = fmtDate(new Date(ev.date));
251
+ const sign = ev.deltaPct > 0 ? '+' : '';
252
+ const summary = ev.summary.length > 60 ? ev.summary.slice(0, 59) + '...' : ev.summary;
253
+ console.log(` ${arrow} ${utils_js_1.c.dim}${dateFmt}${utils_js_1.c.reset} ${sign}${ev.deltaPct}% -> ${summary}`);
254
+ }
255
+ }
256
+ // Summary line
257
+ console.log();
258
+ const costStr = `$${(totalCostCents / 100).toFixed(0)}`;
259
+ const currentStr = `$${(currentValueCents / 100).toFixed(0)}`;
260
+ const pnlStr = fmtDollar(currentPnlCents);
261
+ const pnlPctStr = `${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%`;
262
+ const pnlColor = currentPnlCents >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
263
+ console.log(` ${utils_js_1.c.dim}Cost:${utils_js_1.c.reset} ${costStr} ${utils_js_1.c.dim}| Current:${utils_js_1.c.reset} ${currentStr} ${utils_js_1.c.dim}| P&L:${utils_js_1.c.reset} ${pnlColor}${pnlStr} (${pnlPctStr})${utils_js_1.c.reset}`);
264
+ console.log();
265
+ }
package/dist/index.js CHANGED
@@ -50,6 +50,8 @@ const schedule_js_1 = require("./commands/schedule.js");
50
50
  const rfq_js_1 = require("./commands/rfq.js");
51
51
  const announcements_js_1 = require("./commands/announcements.js");
52
52
  const history_js_1 = require("./commands/history.js");
53
+ const performance_js_1 = require("./commands/performance.js");
54
+ const liquidity_js_1 = require("./commands/liquidity.js");
53
55
  const utils_js_1 = require("./utils.js");
54
56
  // ── Apply ~/.sf/config.json to process.env BEFORE any command ────────────────
55
57
  // This means client.ts, kalshi.ts, agent.ts keep reading process.env and just work.
@@ -62,7 +64,7 @@ program
62
64
  .option('--api-key <key>', 'API key (or set SF_API_KEY env var)')
63
65
  .option('--api-url <url>', 'API base URL (or set SF_API_URL env var)');
64
66
  // ── Pre-action guard: check configuration ────────────────────────────────────
65
- const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history']);
67
+ const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history', 'liquidity']);
66
68
  program.hook('preAction', (thisCommand, actionCommand) => {
67
69
  const cmdName = actionCommand.name();
68
70
  if (NO_CONFIG_COMMANDS.has(cmdName))
@@ -386,6 +388,27 @@ program
386
388
  .action(async (ticker, opts) => {
387
389
  await run(() => (0, history_js_1.historyCommand)(ticker, opts));
388
390
  });
391
+ // ── sf performance ───────────────────────────────────────────────────────────
392
+ program
393
+ .command('performance')
394
+ .description('Portfolio P&L over time with thesis event annotations')
395
+ .option('--ticker <ticker>', 'Filter by ticker (fuzzy match)')
396
+ .option('--since <date>', 'Start date (ISO, e.g. 2026-03-01)')
397
+ .option('--json', 'JSON output')
398
+ .action(async (opts) => {
399
+ await run(() => (0, performance_js_1.performanceCommand)(opts));
400
+ });
401
+ // ── sf liquidity ─────────────────────────────────────────────────────────────
402
+ program
403
+ .command('liquidity')
404
+ .description('Market liquidity scanner by topic and horizon')
405
+ .option('--topic <topic>', 'Filter topic (oil, recession, fed, cpi, gas, sp500)')
406
+ .option('--horizon <horizon>', 'Filter horizon (weekly, monthly, long-term)')
407
+ .option('--min-depth <depth>', 'Minimum bid+ask depth', parseInt)
408
+ .option('--json', 'JSON output')
409
+ .action(async (opts) => {
410
+ await run(() => (0, liquidity_js_1.liquidityCommand)(opts));
411
+ });
389
412
  // ── sf strategies ─────────────────────────────────────────────────────────────
390
413
  (0, strategies_js_1.registerStrategies)(program);
391
414
  // ── Error wrapper ─────────────────────────────────────────────────────────────
package/dist/kalshi.d.ts CHANGED
@@ -58,6 +58,15 @@ export interface LocalOrderbook {
58
58
  liquidityScore: 'high' | 'medium' | 'low';
59
59
  }
60
60
  export declare function getOrderbook(ticker: string): Promise<LocalOrderbook | null>;
61
+ export interface PublicOrderbook {
62
+ yes_dollars: Array<[string, string]>;
63
+ no_dollars: Array<[string, string]>;
64
+ }
65
+ /**
66
+ * Fetch orderbook for a ticker using the public (unauthenticated) endpoint.
67
+ * Returns raw yes_dollars/no_dollars arrays or null on failure.
68
+ */
69
+ export declare function getPublicOrderbook(ticker: string, depth?: number): Promise<PublicOrderbook | null>;
61
70
  export declare function getSettlements(params?: {
62
71
  limit?: number;
63
72
  cursor?: string;
@@ -116,6 +125,15 @@ export declare function amendOrder(orderId: string, params: {
116
125
  }): Promise<{
117
126
  order: any;
118
127
  }>;
128
+ export declare function getBatchCandlesticks(params: {
129
+ tickers: string[];
130
+ startTs: number;
131
+ endTs: number;
132
+ periodInterval?: number;
133
+ }): Promise<{
134
+ market_ticker: string;
135
+ candlesticks: any[];
136
+ }[]>;
119
137
  export declare function createRFQ(params: {
120
138
  market_ticker: string;
121
139
  contracts: number;
package/dist/kalshi.js CHANGED
@@ -19,6 +19,7 @@ exports.getPositions = getPositions;
19
19
  exports.kalshiPriceCents = kalshiPriceCents;
20
20
  exports.getMarketPrice = getMarketPrice;
21
21
  exports.getOrderbook = getOrderbook;
22
+ exports.getPublicOrderbook = getPublicOrderbook;
22
23
  exports.getSettlements = getSettlements;
23
24
  exports.getBalance = getBalance;
24
25
  exports.getOrders = getOrders;
@@ -30,6 +31,7 @@ exports.createOrder = createOrder;
30
31
  exports.cancelOrder = cancelOrder;
31
32
  exports.batchCancelOrders = batchCancelOrders;
32
33
  exports.amendOrder = amendOrder;
34
+ exports.getBatchCandlesticks = getBatchCandlesticks;
33
35
  exports.createRFQ = createRFQ;
34
36
  const fs_1 = __importDefault(require("fs"));
35
37
  const path_1 = __importDefault(require("path"));
@@ -278,6 +280,27 @@ async function getOrderbook(ticker) {
278
280
  return null;
279
281
  }
280
282
  }
283
+ /**
284
+ * Fetch orderbook for a ticker using the public (unauthenticated) endpoint.
285
+ * Returns raw yes_dollars/no_dollars arrays or null on failure.
286
+ */
287
+ async function getPublicOrderbook(ticker, depth = 20) {
288
+ try {
289
+ const url = `${KALSHI_API_BASE}/markets/${ticker}/orderbook?depth=${depth}`;
290
+ const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
291
+ if (!res.ok)
292
+ return null;
293
+ const data = await res.json();
294
+ const ob = data.orderbook_fp || data.orderbook || data;
295
+ return {
296
+ yes_dollars: ob.yes_dollars || ob.yes || [],
297
+ no_dollars: ob.no_dollars || ob.no || [],
298
+ };
299
+ }
300
+ catch {
301
+ return null;
302
+ }
303
+ }
281
304
  // ============================================================================
282
305
  // SETTLEMENTS (Authenticated)
283
306
  // ============================================================================
@@ -450,6 +473,26 @@ async function amendOrder(orderId, params) {
450
473
  }
451
474
  return res.json();
452
475
  }
476
+ // ============================================================================
477
+ // CANDLESTICKS (Authenticated)
478
+ // ============================================================================
479
+ async function getBatchCandlesticks(params) {
480
+ if (!isKalshiConfigured())
481
+ return [];
482
+ try {
483
+ const searchParams = new URLSearchParams();
484
+ searchParams.set('market_tickers', params.tickers.join(','));
485
+ searchParams.set('start_ts', params.startTs.toString());
486
+ searchParams.set('end_ts', params.endTs.toString());
487
+ searchParams.set('period_interval', (params.periodInterval ?? 1440).toString());
488
+ const data = await kalshiAuthGet(`/markets/candlesticks?${searchParams.toString()}`);
489
+ return data.markets || [];
490
+ }
491
+ catch (err) {
492
+ console.warn('[Kalshi] Failed to fetch candlesticks:', err);
493
+ return [];
494
+ }
495
+ }
453
496
  async function createRFQ(params) {
454
497
  return kalshiAuthPost('/communications/rfqs', params);
455
498
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Topic → Kalshi series mapping
3
+ *
4
+ * Shared between dashboard, liquidity scanner, and other commands
5
+ * that need to categorize markets by topic.
6
+ */
7
+ export declare const TOPIC_SERIES: Record<string, string[]>;
8
+ /** Map a series prefix to a human-readable category name (for dashboard display) */
9
+ export declare const RISK_CATEGORIES: Record<string, string>;
10
+ /**
11
+ * Given a ticker string, return the topic name (uppercased).
12
+ * Matches longest prefix first to avoid ambiguity (e.g. KXWTIMAX before KXWTI).
13
+ */
14
+ export declare function tickerToTopic(ticker: string): string;
package/dist/topics.js ADDED
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ /**
3
+ * Topic → Kalshi series mapping
4
+ *
5
+ * Shared between dashboard, liquidity scanner, and other commands
6
+ * that need to categorize markets by topic.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.RISK_CATEGORIES = exports.TOPIC_SERIES = void 0;
10
+ exports.tickerToTopic = tickerToTopic;
11
+ exports.TOPIC_SERIES = {
12
+ oil: ['KXWTIMAX', 'KXWTIW', 'KXWTID'],
13
+ recession: ['KXRECSSNBER'],
14
+ fed: ['KXFEDDECISION'],
15
+ cpi: ['KXCPI'],
16
+ gas: ['KXAAAGASM'],
17
+ sp500: ['KXINXY'],
18
+ };
19
+ /** Map a series prefix to a human-readable category name (for dashboard display) */
20
+ exports.RISK_CATEGORIES = {
21
+ KXWTIMAX: 'Oil',
22
+ KXWTI: 'Oil',
23
+ KXRECSSNBER: 'Recession',
24
+ KXAAAGASM: 'Gas',
25
+ KXCPI: 'Inflation',
26
+ KXINXY: 'S&P 500',
27
+ KXFEDDECISION: 'Fed Rate',
28
+ KXUNEMPLOYMENT: 'Unemployment',
29
+ KXCLOSEHORMUZ: 'Hormuz',
30
+ };
31
+ /**
32
+ * Given a ticker string, return the topic name (uppercased).
33
+ * Matches longest prefix first to avoid ambiguity (e.g. KXWTIMAX before KXWTI).
34
+ */
35
+ function tickerToTopic(ticker) {
36
+ const sorted = Object.entries(exports.TOPIC_SERIES)
37
+ .flatMap(([topic, series]) => series.map(s => ({ prefix: s, topic })))
38
+ .sort((a, b) => b.prefix.length - a.prefix.length);
39
+ for (const { prefix, topic } of sorted) {
40
+ if (ticker.toUpperCase().startsWith(prefix))
41
+ return topic.toUpperCase();
42
+ }
43
+ return 'OTHER';
44
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "description": "Prediction market intelligence CLI. Causal thesis model, 24/7 Kalshi/Polymarket scan, live orderbook, edge detection. Interactive agent mode with tool calling.",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"