@spfunctions/cli 1.2.1 → 1.4.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 +365 -0
- 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 +261 -0
- package/dist/commands/setup.js +123 -123
- package/dist/index.js +26 -3
- 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 +2 -2
|
@@ -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,261 @@
|
|
|
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 = fill.count || 0;
|
|
52
|
+
const yesPrice = fill.yes_price || 0; // 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
|
+
const closeDollars = parseFloat(candle.close_dollars || candle.close || '0');
|
|
109
|
+
const closeCents = Math.round(closeDollars * 100);
|
|
110
|
+
const ts = candle.end_period_ts || candle.period_end_ts || candle.ts;
|
|
111
|
+
if (ts) {
|
|
112
|
+
const d = new Date(ts * 1000);
|
|
113
|
+
priceByDate.set(dateKey(d), closeCents);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
candleMap.set(mc.market_ticker, priceByDate);
|
|
117
|
+
}
|
|
118
|
+
// 6. Build daily P&L matrix
|
|
119
|
+
// Collect all unique dates across all tickers
|
|
120
|
+
const allDates = new Set();
|
|
121
|
+
for (const [, priceByDate] of candleMap) {
|
|
122
|
+
for (const dk of priceByDate.keys()) {
|
|
123
|
+
allDates.add(dk);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const sortedDates = [...allDates].sort();
|
|
127
|
+
// For each ticker compute entry price
|
|
128
|
+
const entryPrices = new Map();
|
|
129
|
+
for (const t of tickers) {
|
|
130
|
+
const avgEntry = t.totalContracts > 0 ? Math.round(t.totalCostCents / t.totalContracts) : 0;
|
|
131
|
+
entryPrices.set(t.ticker, avgEntry);
|
|
132
|
+
}
|
|
133
|
+
const dailyRows = [];
|
|
134
|
+
for (const dk of sortedDates) {
|
|
135
|
+
const pnlByTicker = new Map();
|
|
136
|
+
let total = 0;
|
|
137
|
+
for (const t of tickers) {
|
|
138
|
+
const prices = candleMap.get(t.ticker);
|
|
139
|
+
const closePrice = prices?.get(dk);
|
|
140
|
+
if (closePrice !== undefined) {
|
|
141
|
+
const entry = entryPrices.get(t.ticker) || 0;
|
|
142
|
+
const pnl = (closePrice - entry) * t.netQty;
|
|
143
|
+
pnlByTicker.set(t.ticker, pnl);
|
|
144
|
+
total += pnl;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
dailyRows.push({ date: dk, pnlByTicker, total });
|
|
148
|
+
}
|
|
149
|
+
const events = [];
|
|
150
|
+
try {
|
|
151
|
+
const config = (0, config_js_1.loadConfig)();
|
|
152
|
+
const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
|
|
153
|
+
const feedData = await client.getFeed(720);
|
|
154
|
+
const feedItems = feedData?.items || feedData?.events || feedData || [];
|
|
155
|
+
if (Array.isArray(feedItems)) {
|
|
156
|
+
for (const item of feedItems) {
|
|
157
|
+
const confDelta = item.confidenceDelta ?? item.confidence_delta ?? 0;
|
|
158
|
+
if (Math.abs(confDelta) > 0.03) {
|
|
159
|
+
const itemDate = item.createdAt || item.created_at || item.timestamp || '';
|
|
160
|
+
if (itemDate) {
|
|
161
|
+
events.push({
|
|
162
|
+
date: dateKey(new Date(itemDate)),
|
|
163
|
+
direction: confDelta > 0 ? 'up' : 'down',
|
|
164
|
+
deltaPct: Math.round(confDelta * 100),
|
|
165
|
+
summary: item.summary || item.title || item.description || '',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
// Feed unavailable — continue without events
|
|
174
|
+
}
|
|
175
|
+
// Compute summary
|
|
176
|
+
const totalCostCents = tickers.reduce((sum, t) => sum + t.totalCostCents, 0);
|
|
177
|
+
const lastRow = dailyRows.length > 0 ? dailyRows[dailyRows.length - 1] : null;
|
|
178
|
+
const currentPnlCents = lastRow?.total ?? 0;
|
|
179
|
+
const currentValueCents = totalCostCents + currentPnlCents;
|
|
180
|
+
const pnlPct = totalCostCents > 0 ? (currentPnlCents / totalCostCents) * 100 : 0;
|
|
181
|
+
// 8. Output
|
|
182
|
+
if (opts.json) {
|
|
183
|
+
console.log(JSON.stringify({
|
|
184
|
+
daily: dailyRows.map(row => {
|
|
185
|
+
const tickerPnl = {};
|
|
186
|
+
for (const [tk, pnl] of row.pnlByTicker) {
|
|
187
|
+
tickerPnl[tk] = pnl;
|
|
188
|
+
}
|
|
189
|
+
return { date: row.date, tickers: tickerPnl, total: row.total };
|
|
190
|
+
}),
|
|
191
|
+
events,
|
|
192
|
+
summary: {
|
|
193
|
+
cost: totalCostCents,
|
|
194
|
+
current: currentValueCents,
|
|
195
|
+
pnl: currentPnlCents,
|
|
196
|
+
pnlPct: Math.round(pnlPct * 10) / 10,
|
|
197
|
+
},
|
|
198
|
+
}, null, 2));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
// Formatted output
|
|
202
|
+
const startDate = sortedDates.length > 0 ? fmtDate(new Date(sortedDates[0])) : '?';
|
|
203
|
+
const endDate = fmtDate(new Date());
|
|
204
|
+
console.log();
|
|
205
|
+
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}`);
|
|
206
|
+
console.log(` ${utils_js_1.c.dim}${'─'.repeat(50)}${utils_js_1.c.reset}`);
|
|
207
|
+
console.log();
|
|
208
|
+
// Column headers
|
|
209
|
+
const abbrevs = tickers.map(t => abbrevTicker(t.ticker));
|
|
210
|
+
const colWidth = 9;
|
|
211
|
+
const dateCol = 'Date'.padEnd(12);
|
|
212
|
+
const headerCols = abbrevs.map(a => (0, utils_js_1.rpad)(a, colWidth)).join('');
|
|
213
|
+
const totalCol = (0, utils_js_1.rpad)('Total', colWidth);
|
|
214
|
+
console.log(` ${utils_js_1.c.dim}${dateCol}${headerCols}${totalCol}${utils_js_1.c.reset}`);
|
|
215
|
+
// Rows — show at most ~30 rows, sample if more
|
|
216
|
+
const maxRows = 30;
|
|
217
|
+
let rowsToShow = dailyRows;
|
|
218
|
+
if (dailyRows.length > maxRows) {
|
|
219
|
+
// Sample evenly + always include first and last
|
|
220
|
+
const step = Math.ceil(dailyRows.length / maxRows);
|
|
221
|
+
rowsToShow = dailyRows.filter((_, i) => i === 0 || i === dailyRows.length - 1 || i % step === 0);
|
|
222
|
+
}
|
|
223
|
+
for (const row of rowsToShow) {
|
|
224
|
+
const d = new Date(row.date);
|
|
225
|
+
const dateStr = fmtDate(d).padEnd(12);
|
|
226
|
+
const cols = tickers.map(t => {
|
|
227
|
+
const pnl = row.pnlByTicker.get(t.ticker);
|
|
228
|
+
if (pnl === undefined)
|
|
229
|
+
return (0, utils_js_1.rpad)('--', colWidth);
|
|
230
|
+
const str = fmtDollar(pnl);
|
|
231
|
+
const color = pnl > 0 ? utils_js_1.c.green : pnl < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
|
|
232
|
+
return color + (0, utils_js_1.rpad)(str, colWidth) + utils_js_1.c.reset;
|
|
233
|
+
}).join('');
|
|
234
|
+
const totalStr = fmtDollar(row.total);
|
|
235
|
+
const totalColor = row.total > 0 ? utils_js_1.c.green : row.total < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
|
|
236
|
+
console.log(` ${dateStr}${cols}${totalColor}${(0, utils_js_1.rpad)(totalStr, colWidth)}${utils_js_1.c.reset}`);
|
|
237
|
+
}
|
|
238
|
+
// Events
|
|
239
|
+
if (events.length > 0) {
|
|
240
|
+
console.log();
|
|
241
|
+
// Match events to date range
|
|
242
|
+
const dateSet = new Set(sortedDates);
|
|
243
|
+
const relevantEvents = events.filter(e => dateSet.has(e.date));
|
|
244
|
+
for (const ev of relevantEvents.slice(0, 10)) {
|
|
245
|
+
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}`;
|
|
246
|
+
const dateFmt = fmtDate(new Date(ev.date));
|
|
247
|
+
const sign = ev.deltaPct > 0 ? '+' : '';
|
|
248
|
+
const summary = ev.summary.length > 60 ? ev.summary.slice(0, 59) + '...' : ev.summary;
|
|
249
|
+
console.log(` ${arrow} ${utils_js_1.c.dim}${dateFmt}${utils_js_1.c.reset} ${sign}${ev.deltaPct}% -> ${summary}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Summary line
|
|
253
|
+
console.log();
|
|
254
|
+
const costStr = `$${(totalCostCents / 100).toFixed(0)}`;
|
|
255
|
+
const currentStr = `$${(currentValueCents / 100).toFixed(0)}`;
|
|
256
|
+
const pnlStr = fmtDollar(currentPnlCents);
|
|
257
|
+
const pnlPctStr = `${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%`;
|
|
258
|
+
const pnlColor = currentPnlCents >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
|
|
259
|
+
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}`);
|
|
260
|
+
console.log();
|
|
261
|
+
}
|