@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.
- package/dist/commands/dashboard.js +3 -14
- package/dist/commands/liquidity.d.ts +12 -0
- package/dist/commands/liquidity.js +293 -0
- package/dist/commands/performance.d.ts +8 -0
- package/dist/commands/performance.js +265 -0
- package/dist/index.js +24 -1
- package/dist/kalshi.d.ts +18 -0
- package/dist/kalshi.js +43 -0
- package/dist/topics.d.ts +14 -0
- package/dist/topics.js +44 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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,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
|
}
|
package/dist/topics.d.ts
ADDED
|
@@ -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
|
+
"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"
|