@spfunctions/cli 1.7.7 → 1.7.10

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.
@@ -2,8 +2,7 @@
2
2
  * sf query "iran oil"
3
3
  *
4
4
  * LLM-enhanced prediction market knowledge search. No auth required.
5
- * Searches Kalshi, Polymarket, and SimpleFunctions content.
6
- * Returns structured answer with markets, thesis, content, key factors.
5
+ * Searches Kalshi, Polymarket, X, traditional markets, and SimpleFunctions content.
7
6
  *
8
7
  * Usage:
9
8
  * sf query "iran oil" — Formatted output
@@ -3,8 +3,7 @@
3
3
  * sf query "iran oil"
4
4
  *
5
5
  * LLM-enhanced prediction market knowledge search. No auth required.
6
- * Searches Kalshi, Polymarket, and SimpleFunctions content.
7
- * Returns structured answer with markets, thesis, content, key factors.
6
+ * Searches Kalshi, Polymarket, X, traditional markets, and SimpleFunctions content.
8
7
  *
9
8
  * Usage:
10
9
  * sf query "iran oil" — Formatted output
@@ -49,59 +48,80 @@ async function queryCommand(q, opts) {
49
48
  }
50
49
  console.log();
51
50
  }
52
- // Markets — show each venue separately
53
- const formatMarket = (m) => {
54
- const venue = m.venue === 'kalshi' ? '\x1b[36mK\x1b[39m' : '\x1b[35mP\x1b[39m';
55
- const vol = m.volume > 1_000_000 ? `${(m.volume / 1_000_000).toFixed(1)}M` :
56
- m.volume > 1_000 ? `${(m.volume / 1_000).toFixed(0)}K` :
57
- String(Math.round(m.volume));
58
- const ticker = m.ticker ? ` ${m.ticker}` : '';
59
- console.log(` ${venue} ${String(m.price).padStart(3)}¢ vol ${vol.padStart(6)} ${m.title.slice(0, 55)}${ticker}`);
60
- };
51
+ // Markets — each venue separately
61
52
  const kalshiList = data.kalshi || [];
62
53
  const polyList = data.polymarket || [];
63
- const hasMarkets = kalshiList.length > 0 || polyList.length > 0;
64
- if (hasMarkets) {
54
+ if (kalshiList.length > 0 || polyList.length > 0) {
65
55
  console.log(' \x1b[1mMarkets\x1b[22m');
66
- if (kalshiList.length > 0) {
67
- for (const m of kalshiList.slice(0, 8))
68
- formatMarket(m);
56
+ for (const m of kalshiList.slice(0, 8)) {
57
+ const vol = fmtVol(m.volume);
58
+ const id = m.ticker ? ` \x1b[2m${m.ticker}\x1b[22m` : '';
59
+ console.log(` \x1b[36mK\x1b[39m ${String(m.price).padStart(3)}¢ vol ${vol.padStart(6)} ${m.title.slice(0, 55)}${id}`);
69
60
  }
70
- if (polyList.length > 0) {
71
- if (kalshiList.length > 0)
72
- console.log(); // separator between venues
73
- for (const m of polyList.slice(0, 8))
74
- formatMarket(m);
61
+ if (kalshiList.length > 0 && polyList.length > 0)
62
+ console.log();
63
+ for (const m of polyList.slice(0, 8)) {
64
+ const vol = fmtVol(m.volume);
65
+ const id = m.slug ? ` \x1b[2m${m.slug}\x1b[22m` : '';
66
+ console.log(` \x1b[35mP\x1b[39m ${String(m.price).padStart(3)}¢ vol ${vol.padStart(6)} ${m.title.slice(0, 55)}${id}`);
67
+ }
68
+ console.log();
69
+ }
70
+ // X posts
71
+ const xList = data.x || [];
72
+ if (xList.length > 0) {
73
+ console.log(' \x1b[1mX signals\x1b[22m');
74
+ for (const p of xList.slice(0, 5)) {
75
+ const engagement = `${fmtNum(p.likes)}♥ ${fmtNum(p.retweets)}⟲`;
76
+ console.log(` @${p.author} ${engagement} ${p.text.slice(0, 80)}`);
77
+ }
78
+ console.log();
79
+ }
80
+ // Traditional markets
81
+ const tradList = data.traditional || [];
82
+ if (tradList.length > 0) {
83
+ console.log(' \x1b[1mTraditional\x1b[22m');
84
+ for (const m of tradList) {
85
+ const sign = m.change1d >= 0 ? '+' : '';
86
+ console.log(` ${m.symbol.padEnd(5)} $${m.price} ${sign}${m.change1d} (${sign}${m.changePct}%) ${m.name}`);
75
87
  }
76
88
  console.log();
77
89
  }
78
90
  // Theses
79
91
  if (data.theses?.length > 0) {
80
- console.log(` \x1b[1mTheses\x1b[22m`);
92
+ console.log(' \x1b[1mTheses\x1b[22m');
81
93
  for (const t of data.theses.slice(0, 3)) {
82
94
  console.log(` ${t.confidence || '?'}% ${String(t.edges || 0).padStart(2)} edges ${t.title.slice(0, 50)}`);
83
95
  }
84
96
  console.log();
85
97
  }
86
- else if (data.thesis) {
87
- // Backward compat
88
- const t = data.thesis;
89
- console.log(` \x1b[1mThesis\x1b[22m`);
90
- console.log(` ${t.confidence}% confidence ${t.edges} edges /thesis/${t.slug}`);
91
- console.log();
92
- }
93
98
  // Content
94
99
  if (data.content?.length > 0) {
95
100
  console.log(' \x1b[1mContent\x1b[22m');
96
101
  for (const c of data.content.slice(0, 5)) {
97
102
  const type = c.type.padEnd(9);
98
- console.log(` \x1b[2m${type}\x1b[22m ${c.title.slice(0, 60)}`);
103
+ console.log(` \x1b[2m${type}\x1b[22m ${c.title.slice(0, 50)} \x1b[2m${c.url}\x1b[22m`);
99
104
  }
100
105
  console.log();
101
106
  }
102
- // Sources
103
- if (data.sources?.length > 0) {
104
- console.log(` \x1b[2mSources: ${data.sources.join(', ')}\x1b[22m`);
105
- console.log();
106
- }
107
+ // Meta
108
+ const meta = data.meta || {};
109
+ const sources = meta.sources || data.sources || [];
110
+ const latency = meta.latencyMs ? `${(meta.latencyMs / 1000).toFixed(1)}s` : '';
111
+ console.log(` \x1b[2mSources: ${sources.join(', ')}${latency ? ` (${latency})` : ''}\x1b[22m`);
112
+ console.log();
113
+ }
114
+ function fmtVol(v) {
115
+ if (v > 1_000_000)
116
+ return `${(v / 1_000_000).toFixed(1)}M`;
117
+ if (v > 1_000)
118
+ return `${(v / 1_000).toFixed(0)}K`;
119
+ return String(Math.round(v));
120
+ }
121
+ function fmtNum(n) {
122
+ if (n > 1_000_000)
123
+ return `${(n / 1_000_000).toFixed(1)}M`;
124
+ if (n > 1_000)
125
+ return `${(n / 1_000).toFixed(1)}K`;
126
+ return String(n);
107
127
  }
@@ -4,6 +4,8 @@ exports.scanCommand = scanCommand;
4
4
  const client_js_1 = require("../client.js");
5
5
  const polymarket_js_1 = require("../polymarket.js");
6
6
  const utils_js_1 = require("../utils.js");
7
+ // SF API base — scan goes through the server which holds the proxy key
8
+ const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
7
9
  async function scanCommand(query, opts) {
8
10
  // Mode 1: --market TICKER — single market detail
9
11
  if (opts.market) {
@@ -79,7 +81,6 @@ async function showSeries(seriesTicker, json) {
79
81
  }
80
82
  }
81
83
  async function keywordScan(query, json, venue = 'all') {
82
- const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
83
84
  // ── Polymarket search (runs in parallel with Kalshi) ───────────────────────
84
85
  const polyPromise = (venue === 'kalshi')
85
86
  ? Promise.resolve([])
@@ -90,68 +91,36 @@ async function keywordScan(query, json, venue = 'all') {
90
91
  if (venue !== 'kalshi') {
91
92
  console.log(`${utils_js_1.c.dim}Scanning Polymarket for: "${query}"...${utils_js_1.c.reset}`);
92
93
  }
93
- const allSeries = venue === 'polymarket' ? [] : await (0, client_js_1.kalshiFetchAllSeries)();
94
- // Score each series
95
- const thesisKeywords = [
96
- 'oil', 'wti', 'gas', 'recession', 'gdp', 'fed', 'inflation',
97
- 'unemployment', 'cpi', 'interest rate', 'congress', 'election',
98
- 'iran', 'hormuz', 'war', 'tariff', 'trade', 'rate',
99
- ];
100
- const matches = [];
101
- for (const s of allSeries) {
102
- const text = `${s.title} ${s.ticker} ${(s.tags || []).join(' ')} ${s.category}`.toLowerCase();
103
- let score = 0;
104
- for (const term of terms) {
105
- if (text.includes(term))
106
- score += 10;
107
- }
108
- for (const kw of thesisKeywords) {
109
- if (text.includes(kw))
110
- score += 5;
111
- }
112
- const v = parseFloat(s.volume_fp || '0');
113
- if (v > 1_000_000)
114
- score += 3;
115
- else if (v > 100_000)
116
- score += 1;
117
- if (score > 0 && v > 1000)
118
- matches.push({ series: s, score, volume: v });
119
- }
120
- // Sort by score first, then by volume as tiebreaker
121
- matches.sort((a, b) => b.score - a.score || b.volume - a.volume);
122
- const topSeries = matches.slice(0, 15);
123
- console.log(`\n${utils_js_1.c.bold}Found ${matches.length} relevant series. Top ${topSeries.length}:${utils_js_1.c.reset}\n`);
124
- for (const { series: s, volume } of topSeries) {
125
- const volStr = volume >= 1_000_000 ? `$${(volume / 1_000_000).toFixed(1)}M` : volume >= 1000 ? `$${(volume / 1000).toFixed(0)}k` : `$${volume.toFixed(0)}`;
126
- console.log(` ${(0, utils_js_1.rpad)(volStr, 10)} ${(0, utils_js_1.pad)(s.ticker, 25)} ${s.title}`);
127
- }
128
- // Fetch live markets for top 10
129
- console.log(`\n${utils_js_1.c.dim}Fetching live markets...${utils_js_1.c.reset}\n`);
94
+ // ── Kalshi via SF API /scan (proxies to CF worker, key stays server-side)
130
95
  const allMarkets = [];
131
- for (const { series: s } of topSeries.slice(0, 10)) {
96
+ if (venue !== 'polymarket') {
132
97
  try {
133
- const events = await (0, client_js_1.kalshiFetchEvents)(s.ticker);
134
- for (const event of events) {
135
- const markets = event.markets || [];
136
- for (const m of markets) {
137
- if (m.status === 'open' || m.status === 'active') {
138
- allMarkets.push({
139
- venue: 'kalshi',
140
- seriesTicker: s.ticker,
141
- ticker: m.ticker,
142
- title: m.title || m.subtitle || '',
143
- yesAsk: parseFloat(m.yes_ask_dollars || '0'),
144
- lastPrice: parseFloat(m.last_price_dollars || '0'),
145
- volume24h: parseFloat(m.volume_24h_fp || '0'),
146
- liquidity: parseFloat(m.open_interest_fp || m.liquidity_dollars || '0'),
147
- });
148
- }
149
- }
98
+ const scanUrl = `${SF_API_URL}/api/public/scan?q=${encodeURIComponent(query)}&limit=30`;
99
+ const res = await fetch(scanUrl);
100
+ if (!res.ok)
101
+ throw new Error(`Proxy ${res.status}`);
102
+ const data = await res.json();
103
+ const matchedSeries = data.matchedSeries || [];
104
+ const markets = data.markets || [];
105
+ console.log(`\n${utils_js_1.c.bold}Matched ${matchedSeries.length} series via proxy:${utils_js_1.c.reset}\n`);
106
+ for (const s of matchedSeries.slice(0, 15)) {
107
+ console.log(` ${(0, utils_js_1.pad)(s.ticker, 25)} score=${s.score} ${s.title || ''}`);
108
+ }
109
+ for (const m of markets) {
110
+ allMarkets.push({
111
+ venue: 'kalshi',
112
+ seriesTicker: m.series_ticker,
113
+ ticker: m.ticker,
114
+ title: m.title || m.exchange_title || '',
115
+ yesAsk: parseFloat(m.yes_ask_dollars || '0'),
116
+ lastPrice: parseFloat(m.last_price_dollars || '0'),
117
+ volume24h: parseFloat(m.volume_24h_fp || '0'),
118
+ liquidity: parseFloat(m.open_interest_fp || m.liquidity_dollars || '0'),
119
+ });
150
120
  }
151
- await new Promise(r => setTimeout(r, 150));
152
121
  }
153
- catch {
154
- // skip failed series
122
+ catch (err) {
123
+ console.warn(`${utils_js_1.c.dim}Kalshi scan failed: ${err}${utils_js_1.c.reset}`);
155
124
  }
156
125
  }
157
126
  allMarkets.sort((a, b) => b.liquidity - a.liquidity);
@@ -21,11 +21,22 @@ async function executeOrder(ticker, qty, action, opts) {
21
21
  if (isNaN(quantity) || quantity <= 0)
22
22
  throw new Error('Quantity must be a positive integer');
23
23
  const side = (opts.side || 'yes');
24
+ if (!opts.market && !opts.price) {
25
+ throw new Error('Limit order requires --price <cents>. Example: sf buy TICKER 10 --price 45\nFor market orders use: sf buy TICKER 10 --market --price 99');
26
+ }
27
+ // Kalshi API always requires yes_price — even for "market" orders.
28
+ // For market orders without --price: auto-fetch current market price.
24
29
  const orderType = opts.market ? 'market' : 'limit';
25
- if (orderType === 'limit' && !opts.price) {
26
- throw new Error('Limit order requires --price <cents>. Use --market for market orders.');
30
+ let priceCents = opts.price ? parseInt(opts.price) : undefined;
31
+ if (opts.market && !priceCents) {
32
+ console.log(`${utils_js_1.c.dim}Fetching current market price for ${ticker}...${utils_js_1.c.reset}`);
33
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(ticker.toUpperCase());
34
+ if (!livePrice) {
35
+ throw new Error(`Could not fetch market price for ${ticker}. Use --price <cents> to set manually.`);
36
+ }
37
+ priceCents = livePrice;
38
+ console.log(`${utils_js_1.c.dim}Using market price: ${livePrice}¢${utils_js_1.c.reset}`);
27
39
  }
28
- const priceCents = opts.price ? parseInt(opts.price) : undefined;
29
40
  if (priceCents !== undefined && (priceCents < 1 || priceCents > 99)) {
30
41
  throw new Error('Price must be 1-99 cents.');
31
42
  }
package/dist/index.js CHANGED
@@ -434,8 +434,8 @@ program
434
434
  // ── sf milestones ────────────────────────────────────────────────────────────
435
435
  program
436
436
  .command('milestones')
437
- .description('Upcoming events from Kalshi calendar')
438
- .option('--category <cat>', 'Filter by category')
437
+ .description('Upcoming events from Kalshi calendar. Use --category to filter (e.g., Economics, Politics, Financials, Climate)')
438
+ .option('--category <cat>', 'Filter by category (e.g., Economics, Politics, Financials, Climate, Sports)')
439
439
  .option('--thesis <id>', 'Show milestones matching thesis edges')
440
440
  .option('--hours <n>', 'Hours ahead (default 168)', '168')
441
441
  .option('--json', 'JSON output')
@@ -446,7 +446,7 @@ program
446
446
  // ── sf forecast <eventTicker> ────────────────────────────────────────────────
447
447
  program
448
448
  .command('forecast <eventTicker>')
449
- .description('Market distribution forecast (P50/P75/P90 history)')
449
+ .description('Market distribution forecast (P50/P75/P90). Use EVENT ticker (e.g., KXWTIMAX-26DEC31), not series ticker.')
450
450
  .option('--days <n>', 'Days of history (default 7)', '7')
451
451
  .option('--json', 'JSON output')
452
452
  .action(async (eventTicker, opts) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.7.7",
3
+ "version": "1.7.10",
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"