@spfunctions/cli 1.4.2 → 1.4.3
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/performance.d.ts +3 -0
- package/dist/commands/performance.js +116 -179
- package/package.json +1 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sf performance — Portfolio P&L over time with thesis event annotations
|
|
3
|
+
*
|
|
4
|
+
* Layout: each row is a position, with inline sparkline and current P&L.
|
|
5
|
+
* Scales from 7 days to months — sparkline adapts to available data.
|
|
3
6
|
*/
|
|
4
7
|
export declare function performanceCommand(opts: {
|
|
5
8
|
ticker?: string;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* sf performance — Portfolio P&L over time with thesis event annotations
|
|
4
|
+
*
|
|
5
|
+
* Layout: each row is a position, with inline sparkline and current P&L.
|
|
6
|
+
* Scales from 7 days to months — sparkline adapts to available data.
|
|
4
7
|
*/
|
|
5
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
9
|
exports.performanceCommand = performanceCommand;
|
|
@@ -8,53 +11,34 @@ const client_js_1 = require("../client.js");
|
|
|
8
11
|
const kalshi_js_1 = require("../kalshi.js");
|
|
9
12
|
const config_js_1 = require("../config.js");
|
|
10
13
|
const utils_js_1 = require("../utils.js");
|
|
11
|
-
/** Abbreviate ticker to something readable:
|
|
12
|
-
* KXWTIMAX-26DEC31-T135 → Oil 135
|
|
13
|
-
* KXRECSSNBER-26 → Recsn
|
|
14
|
-
* KXAAAGASM-26MAR31-4.40 → Gas 4.40
|
|
15
|
-
* KXINXY-26DEC31H1600-T4000 → S&P 4000
|
|
16
|
-
*/
|
|
17
|
-
function abbrevTicker(ticker) {
|
|
18
|
-
// Short topic names for performance table
|
|
19
|
-
const SHORT_TOPICS = {
|
|
20
|
-
KXWTIMAX: 'Oil', KXWTI: 'Oil', KXRECSSNBER: 'Recsn',
|
|
21
|
-
KXAAAGASM: 'Gas', KXCPI: 'CPI', KXINXY: 'S&P',
|
|
22
|
-
KXFEDDECISION: 'Fed', KXUNEMPLOYMENT: 'Unemp', KXCLOSEHORMUZ: 'Hormuz',
|
|
23
|
-
};
|
|
24
|
-
const sorted = Object.keys(SHORT_TOPICS).sort((a, b) => b.length - a.length);
|
|
25
|
-
let topic = '';
|
|
26
|
-
for (const prefix of sorted) {
|
|
27
|
-
if (ticker.startsWith(prefix)) {
|
|
28
|
-
topic = SHORT_TOPICS[prefix];
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
// Extract the meaningful suffix (strike/level)
|
|
33
|
-
const parts = ticker.split('-');
|
|
34
|
-
const last = parts[parts.length - 1] || '';
|
|
35
|
-
const suffix = last.startsWith('T') ? last.slice(1) : '';
|
|
36
|
-
if (topic && suffix)
|
|
37
|
-
return `${topic} ${suffix}`;
|
|
38
|
-
if (topic)
|
|
39
|
-
return topic;
|
|
40
|
-
return last || ticker.slice(0, 8);
|
|
41
|
-
}
|
|
42
|
-
/** Format date as "Mar 01" */
|
|
43
14
|
function fmtDate(d) {
|
|
44
15
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
45
16
|
return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2, '0')}`;
|
|
46
17
|
}
|
|
47
|
-
/** Format dollar amount: positive -> +$12.30, negative -> -$12.30 */
|
|
48
18
|
function fmtDollar(cents) {
|
|
49
19
|
const abs = Math.abs(cents / 100);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return `-$${abs.toFixed(abs >= 100 ? 1 : 2)}`;
|
|
20
|
+
const str = abs >= 1000 ? `${(abs / 1000).toFixed(1)}k` : abs >= 100 ? abs.toFixed(0) : abs.toFixed(2);
|
|
21
|
+
return cents >= 0 ? `+$${str}` : `-$${str}`;
|
|
53
22
|
}
|
|
54
|
-
/** Date string key YYYY-MM-DD from a Date */
|
|
55
23
|
function dateKey(d) {
|
|
56
24
|
return d.toISOString().slice(0, 10);
|
|
57
25
|
}
|
|
26
|
+
/** Build a sparkline string from an array of values */
|
|
27
|
+
function sparkline(values, colorFn) {
|
|
28
|
+
if (values.length === 0)
|
|
29
|
+
return '';
|
|
30
|
+
const min = Math.min(...values);
|
|
31
|
+
const max = Math.max(...values);
|
|
32
|
+
const range = max - min || 1;
|
|
33
|
+
const blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
34
|
+
return values.map(v => {
|
|
35
|
+
const idx = Math.round(((v - min) / range) * (blocks.length - 1));
|
|
36
|
+
const ch = blocks[idx];
|
|
37
|
+
if (colorFn)
|
|
38
|
+
return colorFn(v) + ch + utils_js_1.c.reset;
|
|
39
|
+
return ch;
|
|
40
|
+
}).join('');
|
|
41
|
+
}
|
|
58
42
|
async function performanceCommand(opts) {
|
|
59
43
|
if (!(0, kalshi_js_1.isKalshiConfigured)()) {
|
|
60
44
|
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.`);
|
|
@@ -66,34 +50,23 @@ async function performanceCommand(opts) {
|
|
|
66
50
|
console.log(`${utils_js_1.c.dim}No fills found.${utils_js_1.c.reset}`);
|
|
67
51
|
return;
|
|
68
52
|
}
|
|
69
|
-
const fills = fillsResult.fills;
|
|
70
53
|
const tickerMap = new Map();
|
|
71
|
-
for (const fill of fills) {
|
|
54
|
+
for (const fill of fillsResult.fills) {
|
|
72
55
|
const ticker = fill.ticker || fill.market_ticker || '';
|
|
73
56
|
if (!ticker)
|
|
74
57
|
continue;
|
|
75
|
-
const side = fill.side || 'yes';
|
|
76
58
|
const action = fill.action || 'buy';
|
|
77
59
|
const count = Math.round(parseFloat(fill.count_fp || fill.count || '0'));
|
|
78
|
-
const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
|
|
79
|
-
// Determine direction: buy yes = +count, sell yes = -count
|
|
60
|
+
const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
|
|
80
61
|
let delta = count;
|
|
81
62
|
if (action === 'sell')
|
|
82
63
|
delta = -count;
|
|
83
|
-
const info = tickerMap.get(ticker) || {
|
|
84
|
-
ticker,
|
|
85
|
-
netQty: 0,
|
|
86
|
-
totalCostCents: 0,
|
|
87
|
-
totalContracts: 0,
|
|
88
|
-
earliestFillTs: Infinity,
|
|
89
|
-
};
|
|
64
|
+
const info = tickerMap.get(ticker) || { ticker, netQty: 0, totalCostCents: 0, totalContracts: 0, earliestFillTs: Infinity };
|
|
90
65
|
info.netQty += delta;
|
|
91
66
|
if (delta > 0) {
|
|
92
|
-
// Buying: accumulate cost
|
|
93
67
|
info.totalCostCents += yesPrice * count;
|
|
94
68
|
info.totalContracts += count;
|
|
95
69
|
}
|
|
96
|
-
// Track earliest fill
|
|
97
70
|
const fillTime = fill.created_time || fill.ts || fill.created_at;
|
|
98
71
|
if (fillTime) {
|
|
99
72
|
const ts = Math.floor(new Date(fillTime).getTime() / 1000);
|
|
@@ -102,9 +75,7 @@ async function performanceCommand(opts) {
|
|
|
102
75
|
}
|
|
103
76
|
tickerMap.set(ticker, info);
|
|
104
77
|
}
|
|
105
|
-
// 3. Filter out fully closed positions (net qty = 0)
|
|
106
78
|
let tickers = [...tickerMap.values()].filter(t => t.netQty !== 0);
|
|
107
|
-
// 4. Apply --ticker fuzzy filter
|
|
108
79
|
if (opts.ticker) {
|
|
109
80
|
const needle = opts.ticker.toLowerCase();
|
|
110
81
|
tickers = tickers.filter(t => t.ticker.toLowerCase().includes(needle));
|
|
@@ -113,194 +84,160 @@ async function performanceCommand(opts) {
|
|
|
113
84
|
console.log(`${utils_js_1.c.dim}No open positions found${opts.ticker ? ` matching "${opts.ticker}"` : ''}.${utils_js_1.c.reset}`);
|
|
114
85
|
return;
|
|
115
86
|
}
|
|
116
|
-
//
|
|
87
|
+
// 3. Fetch candlesticks
|
|
117
88
|
const sinceTs = opts.since
|
|
118
89
|
? Math.floor(new Date(opts.since).getTime() / 1000)
|
|
119
90
|
: Math.min(...tickers.map(t => t.earliestFillTs === Infinity ? Math.floor(Date.now() / 1000) - 30 * 86400 : t.earliestFillTs));
|
|
120
91
|
const nowTs = Math.floor(Date.now() / 1000);
|
|
121
|
-
// 5. Fetch candlesticks
|
|
122
92
|
const candleData = await (0, kalshi_js_1.getBatchCandlesticks)({
|
|
123
93
|
tickers: tickers.map(t => t.ticker),
|
|
124
94
|
startTs: sinceTs,
|
|
125
95
|
endTs: nowTs,
|
|
126
96
|
periodInterval: 1440,
|
|
127
97
|
});
|
|
128
|
-
// Build candlestick lookup: ticker ->
|
|
98
|
+
// Build candlestick lookup: ticker -> sorted [{ date, closeCents }]
|
|
129
99
|
const candleMap = new Map();
|
|
130
100
|
for (const mc of candleData) {
|
|
131
|
-
const
|
|
101
|
+
const entries = [];
|
|
132
102
|
for (const candle of (mc.candlesticks || [])) {
|
|
133
|
-
// close_dollars is a string like "0.4800"
|
|
134
|
-
// price object may be empty; use midpoint of yes_bid.close and yes_ask.close
|
|
135
103
|
const bidClose = parseFloat(candle.yes_bid?.close_dollars || '0');
|
|
136
104
|
const askClose = parseFloat(candle.yes_ask?.close_dollars || '0');
|
|
137
105
|
const mid = bidClose > 0 && askClose > 0 ? (bidClose + askClose) / 2 : bidClose || askClose;
|
|
138
106
|
const closeDollars = parseFloat(candle.price?.close_dollars || '0') || mid;
|
|
139
107
|
const closeCents = Math.round(closeDollars * 100);
|
|
140
108
|
const ts = candle.end_period_ts || candle.period_end_ts || candle.ts;
|
|
141
|
-
if (ts)
|
|
142
|
-
|
|
143
|
-
priceByDate.set(dateKey(d), closeCents);
|
|
144
|
-
}
|
|
109
|
+
if (ts)
|
|
110
|
+
entries.push({ date: dateKey(new Date(ts * 1000)), close: closeCents });
|
|
145
111
|
}
|
|
146
|
-
|
|
112
|
+
entries.sort((a, b) => a.date.localeCompare(b.date));
|
|
113
|
+
candleMap.set(mc.market_ticker, entries);
|
|
147
114
|
}
|
|
148
|
-
//
|
|
149
|
-
// Collect all unique dates across all tickers
|
|
115
|
+
// Collect all dates for total P&L
|
|
150
116
|
const allDates = new Set();
|
|
151
|
-
for (const [,
|
|
152
|
-
for (const
|
|
153
|
-
allDates.add(
|
|
154
|
-
}
|
|
155
|
-
}
|
|
117
|
+
for (const [, entries] of candleMap)
|
|
118
|
+
for (const e of entries)
|
|
119
|
+
allDates.add(e.date);
|
|
156
120
|
const sortedDates = [...allDates].sort();
|
|
157
|
-
//
|
|
121
|
+
// Entry prices
|
|
158
122
|
const entryPrices = new Map();
|
|
159
123
|
for (const t of tickers) {
|
|
160
|
-
|
|
161
|
-
entryPrices.set(t.ticker, avgEntry);
|
|
162
|
-
}
|
|
163
|
-
const dailyRows = [];
|
|
164
|
-
for (const dk of sortedDates) {
|
|
165
|
-
const pnlByTicker = new Map();
|
|
166
|
-
let total = 0;
|
|
167
|
-
for (const t of tickers) {
|
|
168
|
-
const prices = candleMap.get(t.ticker);
|
|
169
|
-
const closePrice = prices?.get(dk);
|
|
170
|
-
if (closePrice !== undefined) {
|
|
171
|
-
const entry = entryPrices.get(t.ticker) || 0;
|
|
172
|
-
const pnl = (closePrice - entry) * t.netQty;
|
|
173
|
-
pnlByTicker.set(t.ticker, pnl);
|
|
174
|
-
total += pnl;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
dailyRows.push({ date: dk, pnlByTicker, total });
|
|
124
|
+
entryPrices.set(t.ticker, t.totalContracts > 0 ? Math.round(t.totalCostCents / t.totalContracts) : 0);
|
|
178
125
|
}
|
|
179
126
|
const events = [];
|
|
180
127
|
try {
|
|
181
128
|
const config = (0, config_js_1.loadConfig)();
|
|
182
129
|
const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
|
|
183
130
|
const feedData = await client.getFeed(720);
|
|
184
|
-
const feedItems = feedData?.feed || feedData?.items || feedData
|
|
131
|
+
const feedItems = feedData?.feed || feedData?.items || feedData || [];
|
|
185
132
|
if (Array.isArray(feedItems)) {
|
|
186
133
|
for (const item of feedItems) {
|
|
187
|
-
const confDelta = item.delta ?? item.confidenceDelta ??
|
|
134
|
+
const confDelta = item.delta ?? item.confidenceDelta ?? 0;
|
|
188
135
|
if (Math.abs(confDelta) >= 0.02) {
|
|
189
|
-
const itemDate = item.evaluatedAt || item.createdAt || item.
|
|
136
|
+
const itemDate = item.evaluatedAt || item.createdAt || item.timestamp || '';
|
|
190
137
|
if (itemDate) {
|
|
191
138
|
events.push({
|
|
192
139
|
date: dateKey(new Date(itemDate)),
|
|
193
140
|
direction: confDelta > 0 ? 'up' : 'down',
|
|
194
141
|
deltaPct: Math.round(confDelta * 100),
|
|
195
|
-
summary: item.summary ||
|
|
142
|
+
summary: item.summary || '',
|
|
196
143
|
});
|
|
197
144
|
}
|
|
198
145
|
}
|
|
199
146
|
}
|
|
200
147
|
}
|
|
201
148
|
}
|
|
202
|
-
catch {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
149
|
+
catch { /* feed unavailable */ }
|
|
150
|
+
const perfs = [];
|
|
151
|
+
for (const t of tickers) {
|
|
152
|
+
const entry = entryPrices.get(t.ticker) || 0;
|
|
153
|
+
const entries = candleMap.get(t.ticker) || [];
|
|
154
|
+
const current = entries.length > 0 ? entries[entries.length - 1].close : entry;
|
|
155
|
+
const pnlCents = (current - entry) * t.netQty;
|
|
156
|
+
const costBasis = entry * t.netQty;
|
|
157
|
+
const pnlPct = costBasis !== 0 ? (pnlCents / Math.abs(costBasis)) * 100 : 0;
|
|
158
|
+
const dailyPnl = entries.map(e => (e.close - entry) * t.netQty);
|
|
159
|
+
perfs.push({ ticker: t.ticker, qty: t.netQty, entry, current, pnlCents, pnlPct, dailyPnl });
|
|
160
|
+
}
|
|
161
|
+
// Sort by absolute P&L descending
|
|
162
|
+
perfs.sort((a, b) => Math.abs(b.pnlCents) - Math.abs(a.pnlCents));
|
|
163
|
+
// Total daily P&L
|
|
164
|
+
const totalDailyPnl = sortedDates.map(dk => {
|
|
165
|
+
let total = 0;
|
|
166
|
+
for (const t of tickers) {
|
|
167
|
+
const entries = candleMap.get(t.ticker) || [];
|
|
168
|
+
const entry = entryPrices.get(t.ticker) || 0;
|
|
169
|
+
const dayEntry = entries.find(e => e.date === dk);
|
|
170
|
+
if (dayEntry)
|
|
171
|
+
total += (dayEntry.close - entry) * t.netQty;
|
|
172
|
+
}
|
|
173
|
+
return total;
|
|
174
|
+
});
|
|
175
|
+
// Summary
|
|
206
176
|
const totalCostCents = tickers.reduce((sum, t) => sum + t.totalCostCents, 0);
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
const pnlPct = totalCostCents > 0 ? (currentPnlCents / totalCostCents) * 100 : 0;
|
|
211
|
-
// 8. Output
|
|
177
|
+
const totalPnlCents = perfs.reduce((sum, p) => sum + p.pnlCents, 0);
|
|
178
|
+
const totalPnlPct = totalCostCents > 0 ? (totalPnlCents / totalCostCents) * 100 : 0;
|
|
179
|
+
// 6. JSON output
|
|
212
180
|
if (opts.json) {
|
|
213
181
|
console.log(JSON.stringify({
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
return { date: row.date, tickers: tickerPnl, total: row.total };
|
|
220
|
-
}),
|
|
182
|
+
positions: perfs.map(p => ({
|
|
183
|
+
ticker: p.ticker, qty: p.qty, entry: p.entry, current: p.current,
|
|
184
|
+
pnl: p.pnlCents, pnlPct: Math.round(p.pnlPct * 10) / 10,
|
|
185
|
+
})),
|
|
186
|
+
totalDailyPnl: sortedDates.map((d, i) => ({ date: d, pnl: totalDailyPnl[i] })),
|
|
221
187
|
events,
|
|
222
|
-
summary: {
|
|
223
|
-
cost: totalCostCents,
|
|
224
|
-
current: currentValueCents,
|
|
225
|
-
pnl: currentPnlCents,
|
|
226
|
-
pnlPct: Math.round(pnlPct * 10) / 10,
|
|
227
|
-
},
|
|
188
|
+
summary: { cost: totalCostCents, pnl: totalPnlCents, pnlPct: Math.round(totalPnlPct * 10) / 10 },
|
|
228
189
|
}, null, 2));
|
|
229
190
|
return;
|
|
230
191
|
}
|
|
231
|
-
// Formatted output
|
|
192
|
+
// 7. Formatted output — rows are positions
|
|
232
193
|
const startDate = sortedDates.length > 0 ? fmtDate(new Date(sortedDates[0])) : '?';
|
|
233
194
|
const endDate = fmtDate(new Date());
|
|
234
195
|
console.log();
|
|
235
|
-
console.log(` ${utils_js_1.c.bold}Portfolio Performance${utils_js_1.c.reset} ${utils_js_1.c.dim}(${startDate}
|
|
236
|
-
console.log(` ${utils_js_1.c.dim}${'─'.repeat(
|
|
196
|
+
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}`);
|
|
197
|
+
console.log(` ${utils_js_1.c.dim}${'─'.repeat(76)}${utils_js_1.c.reset}`);
|
|
237
198
|
console.log();
|
|
238
|
-
//
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const totalStr = fmtDollar(row.total);
|
|
265
|
-
const totalColor = row.total > 0 ? utils_js_1.c.green : row.total < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
|
|
266
|
-
console.log(` ${dateStr}${cols}${totalColor}${(0, utils_js_1.rpad)(totalStr, colWidth)}${utils_js_1.c.reset}`);
|
|
267
|
-
}
|
|
268
|
-
// Sparkline of total P&L
|
|
269
|
-
if (dailyRows.length >= 2) {
|
|
270
|
-
const totals = dailyRows.map(r => r.total);
|
|
271
|
-
const min = Math.min(...totals);
|
|
272
|
-
const max = Math.max(...totals);
|
|
273
|
-
const range = max - min || 1;
|
|
274
|
-
const blocks = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
275
|
-
const spark = totals.map(v => {
|
|
276
|
-
const idx = Math.round(((v - min) / range) * (blocks.length - 1));
|
|
277
|
-
const ch = blocks[idx];
|
|
278
|
-
return v >= 0 ? `${utils_js_1.c.green}${ch}${utils_js_1.c.reset}` : `${utils_js_1.c.red}${ch}${utils_js_1.c.reset}`;
|
|
279
|
-
}).join('');
|
|
280
|
-
console.log();
|
|
281
|
-
console.log(` ${utils_js_1.c.dim}P&L trend:${utils_js_1.c.reset} ${spark}`);
|
|
282
|
-
}
|
|
199
|
+
// Header
|
|
200
|
+
const maxTickerLen = Math.max(...perfs.map(p => p.ticker.length), 5) + 2;
|
|
201
|
+
const w = maxTickerLen + 50;
|
|
202
|
+
const pad2 = (s, n) => s.padEnd(n);
|
|
203
|
+
console.log(` ${utils_js_1.c.dim}${pad2('Ticker', maxTickerLen)} Qty Entry Now P&L Trend${utils_js_1.c.reset}`);
|
|
204
|
+
for (const p of perfs) {
|
|
205
|
+
const pnlStr = fmtDollar(p.pnlCents);
|
|
206
|
+
const pnlColor = p.pnlCents > 0 ? utils_js_1.c.green : p.pnlCents < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
|
|
207
|
+
const spark = sparkline(p.dailyPnl, v => v >= 0 ? utils_js_1.c.green : utils_js_1.c.red);
|
|
208
|
+
console.log(` ${pad2(p.ticker, maxTickerLen)} ` +
|
|
209
|
+
`${(0, utils_js_1.rpad)(String(p.qty), 8)}` +
|
|
210
|
+
`${(0, utils_js_1.rpad)(p.entry + '¢', 7)}` +
|
|
211
|
+
`${(0, utils_js_1.rpad)(p.current + '¢', 7)}` +
|
|
212
|
+
`${pnlColor}${(0, utils_js_1.rpad)(pnlStr, 13)}${utils_js_1.c.reset}` +
|
|
213
|
+
spark);
|
|
214
|
+
}
|
|
215
|
+
// Total row
|
|
216
|
+
console.log(` ${utils_js_1.c.dim}${'─'.repeat(w)}${utils_js_1.c.reset}`);
|
|
217
|
+
const totalPnlStr = fmtDollar(totalPnlCents);
|
|
218
|
+
const totalPctStr = `${totalPnlPct >= 0 ? '+' : ''}${totalPnlPct.toFixed(1)}%`;
|
|
219
|
+
const totalColor = totalPnlCents >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
|
|
220
|
+
const totalSpark = sparkline(totalDailyPnl, v => v >= 0 ? utils_js_1.c.green : utils_js_1.c.red);
|
|
221
|
+
console.log(` ${utils_js_1.c.bold}${pad2('TOTAL', maxTickerLen)}${utils_js_1.c.reset} ` +
|
|
222
|
+
`${(0, utils_js_1.rpad)('', 22)}` +
|
|
223
|
+
`${totalColor}${utils_js_1.c.bold}${(0, utils_js_1.rpad)(`${totalPnlStr} (${totalPctStr})`, 13)}${utils_js_1.c.reset}` +
|
|
224
|
+
totalSpark);
|
|
283
225
|
// Events
|
|
284
226
|
if (events.length > 0) {
|
|
285
|
-
console.log();
|
|
286
|
-
// Match events to date range
|
|
287
227
|
const dateSet = new Set(sortedDates);
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
228
|
+
const relevant = events.filter(e => dateSet.has(e.date));
|
|
229
|
+
if (relevant.length > 0) {
|
|
230
|
+
console.log();
|
|
231
|
+
for (const ev of relevant.slice(0, 8)) {
|
|
232
|
+
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}`;
|
|
233
|
+
const summary = ev.summary.length > 55 ? ev.summary.slice(0, 54) + '…' : ev.summary;
|
|
234
|
+
console.log(` ${arrow} ${utils_js_1.c.dim}${fmtDate(new Date(ev.date))}${utils_js_1.c.reset} ${ev.deltaPct > 0 ? '+' : ''}${ev.deltaPct}% → ${summary}`);
|
|
235
|
+
}
|
|
295
236
|
}
|
|
296
237
|
}
|
|
297
|
-
// Summary
|
|
238
|
+
// Summary
|
|
298
239
|
console.log();
|
|
299
240
|
const costStr = `$${(totalCostCents / 100).toFixed(0)}`;
|
|
300
|
-
|
|
301
|
-
const pnlStr = fmtDollar(currentPnlCents);
|
|
302
|
-
const pnlPctStr = `${pnlPct >= 0 ? '+' : ''}${pnlPct.toFixed(1)}%`;
|
|
303
|
-
const pnlColor = currentPnlCents >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
|
|
304
|
-
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}`);
|
|
241
|
+
console.log(` ${utils_js_1.c.dim}Cost basis:${utils_js_1.c.reset} ${costStr} ${utils_js_1.c.dim}|${utils_js_1.c.reset} ${totalColor}${utils_js_1.c.bold}${totalPnlStr} (${totalPctStr})${utils_js_1.c.reset}`);
|
|
305
242
|
console.log();
|
|
306
243
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfunctions/cli",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
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"
|