@spfunctions/cli 1.4.1 → 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.
@@ -671,6 +671,8 @@ async function agentCommand(thesisId, opts) {
671
671
  const matched = series
672
672
  .filter((s) => keywords.every((kw) => (s.title || '').toLowerCase().includes(kw) ||
673
673
  (s.ticker || '').toLowerCase().includes(kw)))
674
+ .filter((s) => parseFloat(s.volume_fp || '0') > 1000)
675
+ .sort((a, b) => parseFloat(b.volume_fp || '0') - parseFloat(a.volume_fp || '0'))
674
676
  .slice(0, 15);
675
677
  result = matched;
676
678
  }
@@ -2187,7 +2189,7 @@ async function runPlainTextAgent(params) {
2187
2189
  else if (p.query) {
2188
2190
  const series = await (0, client_js_1.kalshiFetchAllSeries)();
2189
2191
  const kws = p.query.toLowerCase().split(/\s+/);
2190
- result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).slice(0, 15);
2192
+ result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).filter((s) => parseFloat(s.volume_fp || '0') > 1000).sort((a, b) => parseFloat(b.volume_fp || '0') - parseFloat(a.volume_fp || '0')).slice(0, 15);
2191
2193
  }
2192
2194
  else {
2193
2195
  result = { error: 'Provide query, series, or market' };
@@ -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,27 +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: 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
14
  function fmtDate(d) {
18
15
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
19
16
  return `${months[d.getMonth()]} ${String(d.getDate()).padStart(2, '0')}`;
20
17
  }
21
- /** Format dollar amount: positive -> +$12.30, negative -> -$12.30 */
22
18
  function fmtDollar(cents) {
23
19
  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)}`;
20
+ const str = abs >= 1000 ? `${(abs / 1000).toFixed(1)}k` : abs >= 100 ? abs.toFixed(0) : abs.toFixed(2);
21
+ return cents >= 0 ? `+$${str}` : `-$${str}`;
27
22
  }
28
- /** Date string key YYYY-MM-DD from a Date */
29
23
  function dateKey(d) {
30
24
  return d.toISOString().slice(0, 10);
31
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
+ }
32
42
  async function performanceCommand(opts) {
33
43
  if (!(0, kalshi_js_1.isKalshiConfigured)()) {
34
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.`);
@@ -40,34 +50,23 @@ async function performanceCommand(opts) {
40
50
  console.log(`${utils_js_1.c.dim}No fills found.${utils_js_1.c.reset}`);
41
51
  return;
42
52
  }
43
- const fills = fillsResult.fills;
44
53
  const tickerMap = new Map();
45
- for (const fill of fills) {
54
+ for (const fill of fillsResult.fills) {
46
55
  const ticker = fill.ticker || fill.market_ticker || '';
47
56
  if (!ticker)
48
57
  continue;
49
- const side = fill.side || 'yes';
50
58
  const action = fill.action || 'buy';
51
59
  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
60
+ const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
54
61
  let delta = count;
55
62
  if (action === 'sell')
56
63
  delta = -count;
57
- const info = tickerMap.get(ticker) || {
58
- ticker,
59
- netQty: 0,
60
- totalCostCents: 0,
61
- totalContracts: 0,
62
- earliestFillTs: Infinity,
63
- };
64
+ const info = tickerMap.get(ticker) || { ticker, netQty: 0, totalCostCents: 0, totalContracts: 0, earliestFillTs: Infinity };
64
65
  info.netQty += delta;
65
66
  if (delta > 0) {
66
- // Buying: accumulate cost
67
67
  info.totalCostCents += yesPrice * count;
68
68
  info.totalContracts += count;
69
69
  }
70
- // Track earliest fill
71
70
  const fillTime = fill.created_time || fill.ts || fill.created_at;
72
71
  if (fillTime) {
73
72
  const ts = Math.floor(new Date(fillTime).getTime() / 1000);
@@ -76,9 +75,7 @@ async function performanceCommand(opts) {
76
75
  }
77
76
  tickerMap.set(ticker, info);
78
77
  }
79
- // 3. Filter out fully closed positions (net qty = 0)
80
78
  let tickers = [...tickerMap.values()].filter(t => t.netQty !== 0);
81
- // 4. Apply --ticker fuzzy filter
82
79
  if (opts.ticker) {
83
80
  const needle = opts.ticker.toLowerCase();
84
81
  tickers = tickers.filter(t => t.ticker.toLowerCase().includes(needle));
@@ -87,179 +84,160 @@ async function performanceCommand(opts) {
87
84
  console.log(`${utils_js_1.c.dim}No open positions found${opts.ticker ? ` matching "${opts.ticker}"` : ''}.${utils_js_1.c.reset}`);
88
85
  return;
89
86
  }
90
- // Determine date range
87
+ // 3. Fetch candlesticks
91
88
  const sinceTs = opts.since
92
89
  ? Math.floor(new Date(opts.since).getTime() / 1000)
93
90
  : Math.min(...tickers.map(t => t.earliestFillTs === Infinity ? Math.floor(Date.now() / 1000) - 30 * 86400 : t.earliestFillTs));
94
91
  const nowTs = Math.floor(Date.now() / 1000);
95
- // 5. Fetch candlesticks
96
92
  const candleData = await (0, kalshi_js_1.getBatchCandlesticks)({
97
93
  tickers: tickers.map(t => t.ticker),
98
94
  startTs: sinceTs,
99
95
  endTs: nowTs,
100
96
  periodInterval: 1440,
101
97
  });
102
- // Build candlestick lookup: ticker -> dateKey -> close price (cents)
98
+ // Build candlestick lookup: ticker -> sorted [{ date, closeCents }]
103
99
  const candleMap = new Map();
104
100
  for (const mc of candleData) {
105
- const priceByDate = new Map();
101
+ const entries = [];
106
102
  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
103
  const bidClose = parseFloat(candle.yes_bid?.close_dollars || '0');
110
104
  const askClose = parseFloat(candle.yes_ask?.close_dollars || '0');
111
105
  const mid = bidClose > 0 && askClose > 0 ? (bidClose + askClose) / 2 : bidClose || askClose;
112
106
  const closeDollars = parseFloat(candle.price?.close_dollars || '0') || mid;
113
107
  const closeCents = Math.round(closeDollars * 100);
114
108
  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
- }
109
+ if (ts)
110
+ entries.push({ date: dateKey(new Date(ts * 1000)), close: closeCents });
119
111
  }
120
- candleMap.set(mc.market_ticker, priceByDate);
112
+ entries.sort((a, b) => a.date.localeCompare(b.date));
113
+ candleMap.set(mc.market_ticker, entries);
121
114
  }
122
- // 6. Build daily P&L matrix
123
- // Collect all unique dates across all tickers
115
+ // Collect all dates for total P&L
124
116
  const allDates = new Set();
125
- for (const [, priceByDate] of candleMap) {
126
- for (const dk of priceByDate.keys()) {
127
- allDates.add(dk);
128
- }
129
- }
117
+ for (const [, entries] of candleMap)
118
+ for (const e of entries)
119
+ allDates.add(e.date);
130
120
  const sortedDates = [...allDates].sort();
131
- // For each ticker compute entry price
121
+ // Entry prices
132
122
  const entryPrices = new Map();
133
123
  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 });
124
+ entryPrices.set(t.ticker, t.totalContracts > 0 ? Math.round(t.totalCostCents / t.totalContracts) : 0);
152
125
  }
153
126
  const events = [];
154
127
  try {
155
128
  const config = (0, config_js_1.loadConfig)();
156
129
  const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
157
130
  const feedData = await client.getFeed(720);
158
- const feedItems = feedData?.items || feedData?.events || feedData || [];
131
+ const feedItems = feedData?.feed || feedData?.items || feedData || [];
159
132
  if (Array.isArray(feedItems)) {
160
133
  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 || '';
134
+ const confDelta = item.delta ?? item.confidenceDelta ?? 0;
135
+ if (Math.abs(confDelta) >= 0.02) {
136
+ const itemDate = item.evaluatedAt || item.createdAt || item.timestamp || '';
164
137
  if (itemDate) {
165
138
  events.push({
166
139
  date: dateKey(new Date(itemDate)),
167
140
  direction: confDelta > 0 ? 'up' : 'down',
168
141
  deltaPct: Math.round(confDelta * 100),
169
- summary: item.summary || item.title || item.description || '',
142
+ summary: item.summary || '',
170
143
  });
171
144
  }
172
145
  }
173
146
  }
174
147
  }
175
148
  }
176
- catch {
177
- // Feed unavailable — continue without events
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 });
178
160
  }
179
- // Compute summary
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
180
176
  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
177
+ const totalPnlCents = perfs.reduce((sum, p) => sum + p.pnlCents, 0);
178
+ const totalPnlPct = totalCostCents > 0 ? (totalPnlCents / totalCostCents) * 100 : 0;
179
+ // 6. JSON output
186
180
  if (opts.json) {
187
181
  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
- }),
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] })),
195
187
  events,
196
- summary: {
197
- cost: totalCostCents,
198
- current: currentValueCents,
199
- pnl: currentPnlCents,
200
- pnlPct: Math.round(pnlPct * 10) / 10,
201
- },
188
+ summary: { cost: totalCostCents, pnl: totalPnlCents, pnlPct: Math.round(totalPnlPct * 10) / 10 },
202
189
  }, null, 2));
203
190
  return;
204
191
  }
205
- // Formatted output
192
+ // 7. Formatted output — rows are positions
206
193
  const startDate = sortedDates.length > 0 ? fmtDate(new Date(sortedDates[0])) : '?';
207
194
  const endDate = fmtDate(new Date());
208
195
  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}`);
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}`);
211
198
  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}`);
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);
241
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);
242
225
  // Events
243
226
  if (events.length > 0) {
244
- console.log();
245
- // Match events to date range
246
227
  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}`);
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
+ }
254
236
  }
255
237
  }
256
- // Summary line
238
+ // Summary
257
239
  console.log();
258
240
  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}`);
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}`);
264
242
  console.log();
265
243
  }
@@ -104,14 +104,16 @@ async function keywordScan(query, json) {
104
104
  score += 3;
105
105
  else if (v > 100_000)
106
106
  score += 1;
107
- if (score > 0)
108
- matches.push({ series: s, score });
107
+ if (score > 0 && v > 1000)
108
+ matches.push({ series: s, score, volume: v });
109
109
  }
110
- matches.sort((a, b) => b.score - a.score);
110
+ // Sort by score first, then by volume as tiebreaker
111
+ matches.sort((a, b) => b.score - a.score || b.volume - a.volume);
111
112
  const topSeries = matches.slice(0, 15);
112
113
  console.log(`\n${utils_js_1.c.bold}Found ${matches.length} relevant series. Top ${topSeries.length}:${utils_js_1.c.reset}\n`);
113
- for (const { series: s, score } of topSeries) {
114
- console.log(` ${utils_js_1.c.dim}[${score}]${utils_js_1.c.reset} ${(0, utils_js_1.pad)(s.ticker, 25)} ${s.title}`);
114
+ for (const { series: s, volume } of topSeries) {
115
+ const volStr = volume >= 1_000_000 ? `$${(volume / 1_000_000).toFixed(1)}M` : volume >= 1000 ? `$${(volume / 1000).toFixed(0)}k` : `$${volume.toFixed(0)}`;
116
+ console.log(` ${(0, utils_js_1.rpad)(volStr, 10)} ${(0, utils_js_1.pad)(s.ticker, 25)} ${s.title}`);
115
117
  }
116
118
  // Fetch live markets for top 10
117
119
  console.log(`\n${utils_js_1.c.dim}Fetching live markets...${utils_js_1.c.reset}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.4.1",
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"