@spfunctions/cli 1.4.5 → 1.5.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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Prediction market intelligence CLI. Build causal thesis models, scan Kalshi/Polymarket for mispricings, detect edges, and trade — all from the terminal.
4
4
 
5
+ ![demo](https://raw.githubusercontent.com/spfunctions/simplefunctions-cli/main/demo.gif)
6
+
5
7
  ## Quick Start
6
8
 
7
9
  ```bash
package/dist/client.js CHANGED
@@ -37,8 +37,17 @@ class SFClient {
37
37
  body: body ? JSON.stringify(body) : undefined,
38
38
  });
39
39
  if (!res.ok) {
40
- const text = await res.text();
41
- throw new Error(`API error ${res.status}: ${text}`);
40
+ let errorBody = null;
41
+ try {
42
+ const text = await res.text();
43
+ errorBody = text ? JSON.parse(text) : null;
44
+ }
45
+ catch { /* not JSON */ }
46
+ const err = new Error(errorBody?.error || errorBody?.message || `API error ${res.status}`);
47
+ err.status = res.status;
48
+ err.code = errorBody?.code || `HTTP_${res.status}`;
49
+ err.details = errorBody;
50
+ throw err;
42
51
  }
43
52
  return res.json();
44
53
  }
@@ -66,7 +66,7 @@ vitest_1.vi.stubGlobal('fetch', mockFetch);
66
66
  status: 404,
67
67
  text: () => Promise.resolve('Not found'),
68
68
  });
69
- await (0, vitest_1.expect)(client.getThesis('bad-id')).rejects.toThrow('API error 404: Not found');
69
+ await (0, vitest_1.expect)(client.getThesis('bad-id')).rejects.toThrow('API error 404');
70
70
  });
71
71
  (0, vitest_1.it)('getContext calls correct path', async () => {
72
72
  mockFetch.mockResolvedValue({
@@ -23,6 +23,8 @@ const path_1 = __importDefault(require("path"));
23
23
  const os_1 = __importDefault(require("os"));
24
24
  const client_js_1 = require("../client.js");
25
25
  const kalshi_js_1 = require("../kalshi.js");
26
+ const polymarket_js_1 = require("../polymarket.js");
27
+ const topics_js_1 = require("../topics.js");
26
28
  const config_js_1 = require("../config.js");
27
29
  // ─── Session persistence ─────────────────────────────────────────────────────
28
30
  function getSessionDir() {
@@ -793,34 +795,55 @@ async function agentCommand(thesisId, opts) {
793
795
  {
794
796
  name: 'scan_markets',
795
797
  label: 'Scan Markets',
796
- description: 'Search Kalshi prediction markets. Provide exactly one of: query (keyword search), series (series ticker), or market (specific ticker). If multiple are provided, priority is: market > series > query.',
798
+ description: 'Search Kalshi + Polymarket prediction markets. Provide exactly one of: query (keyword search), series (Kalshi series ticker), or market (specific Kalshi ticker). Keyword search returns results from BOTH venues.',
797
799
  parameters: scanParams,
798
800
  execute: async (_toolCallId, params) => {
799
- let result;
800
801
  if (params.market) {
801
- result = await (0, client_js_1.kalshiFetchMarket)(params.market);
802
+ const result = await (0, client_js_1.kalshiFetchMarket)(params.market);
803
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
802
804
  }
803
- else if (params.series) {
804
- result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
805
+ if (params.series) {
806
+ const result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
807
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
805
808
  }
806
- else if (params.query) {
809
+ if (params.query) {
810
+ // Kalshi: keyword grep on series
807
811
  const series = await (0, client_js_1.kalshiFetchAllSeries)();
808
812
  const keywords = params.query.toLowerCase().split(/\s+/);
809
- const matched = series
810
- .filter((s) => keywords.every((kw) => (s.title || '').toLowerCase().includes(kw) ||
813
+ const kalshiMatched = series
814
+ .filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
811
815
  (s.ticker || '').toLowerCase().includes(kw)))
812
816
  .filter((s) => parseFloat(s.volume_24h_fp || s.volume_fp || '0') > 0)
813
817
  .sort((a, b) => parseFloat(b.volume_24h_fp || b.volume_fp || '0') - parseFloat(a.volume_24h_fp || a.volume_fp || '0'))
814
- .slice(0, 15);
815
- result = matched;
816
- }
817
- else {
818
- result = { error: 'Provide query, series, or market parameter' };
818
+ .slice(0, 10)
819
+ .map((s) => ({ venue: 'kalshi', ticker: s.ticker, title: s.title, volume: s.volume_fp }));
820
+ // Polymarket: Gamma API search
821
+ let polyMatched = [];
822
+ try {
823
+ const events = await (0, polymarket_js_1.polymarketSearch)(params.query, 10);
824
+ for (const event of events) {
825
+ for (const m of (event.markets || []).slice(0, 3)) {
826
+ if (!m.active || m.closed)
827
+ continue;
828
+ const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
829
+ polyMatched.push({
830
+ venue: 'polymarket',
831
+ id: m.conditionId || m.id,
832
+ title: m.groupItemTitle ? `${event.title}: ${m.groupItemTitle}` : m.question || event.title,
833
+ price: prices[0] ? Math.round(prices[0] * 100) : null,
834
+ volume24h: m.volume24hr,
835
+ liquidity: m.liquidityNum,
836
+ });
837
+ }
838
+ }
839
+ }
840
+ catch { /* Polymarket search optional */ }
841
+ return {
842
+ content: [{ type: 'text', text: JSON.stringify({ kalshi: kalshiMatched, polymarket: polyMatched }, null, 2) }],
843
+ details: {},
844
+ };
819
845
  }
820
- return {
821
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
822
- details: {},
823
- };
846
+ return { content: [{ type: 'text', text: '{"error":"Provide query, series, or market parameter"}' }], details: {} };
824
847
  },
825
848
  },
826
849
  {
@@ -839,36 +862,57 @@ async function agentCommand(thesisId, opts) {
839
862
  {
840
863
  name: 'get_positions',
841
864
  label: 'Get Positions',
842
- description: 'Get Kalshi exchange positions with live prices and PnL',
865
+ description: 'Get positions across Kalshi + Polymarket with live prices and PnL',
843
866
  parameters: emptyParams,
844
867
  execute: async () => {
868
+ const result = { kalshi: [], polymarket: [] };
869
+ // Kalshi positions
845
870
  const positions = await (0, kalshi_js_1.getPositions)();
846
- if (!positions) {
871
+ if (positions) {
872
+ for (const pos of positions) {
873
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
874
+ if (livePrice !== null) {
875
+ pos.current_value = livePrice;
876
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
877
+ }
878
+ }
879
+ cachedPositions = positions;
880
+ result.kalshi = positions.map((p) => ({
881
+ venue: 'kalshi',
882
+ ticker: p.ticker,
883
+ side: p.side,
884
+ quantity: p.quantity,
885
+ avg_price: `${p.average_price_paid}¢`,
886
+ current_price: `${p.current_value}¢`,
887
+ unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
888
+ total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
889
+ }));
890
+ }
891
+ // Polymarket positions
892
+ const config = (0, config_js_1.loadConfig)();
893
+ if (config.polymarketWalletAddress) {
894
+ try {
895
+ const polyPos = await (0, polymarket_js_1.polymarketGetPositions)(config.polymarketWalletAddress);
896
+ result.polymarket = polyPos.map((p) => ({
897
+ venue: 'polymarket',
898
+ market: p.title || p.slug || p.asset,
899
+ side: p.outcome || 'Yes',
900
+ size: p.size,
901
+ avg_price: `${Math.round((p.avgPrice || 0) * 100)}¢`,
902
+ current_price: `${Math.round((p.curPrice || p.currentPrice || 0) * 100)}¢`,
903
+ pnl: `$${(p.cashPnl || 0).toFixed(2)}`,
904
+ }));
905
+ }
906
+ catch { /* skip */ }
907
+ }
908
+ if (result.kalshi.length === 0 && result.polymarket.length === 0) {
847
909
  return {
848
- content: [{ type: 'text', text: 'Kalshi not configured. Set KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH.' }],
910
+ content: [{ type: 'text', text: 'No positions found. Configure Kalshi (KALSHI_API_KEY_ID) or Polymarket (sf setup --polymarket) to see positions.' }],
849
911
  details: {},
850
912
  };
851
913
  }
852
- for (const pos of positions) {
853
- const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
854
- if (livePrice !== null) {
855
- pos.current_value = livePrice;
856
- pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
857
- }
858
- }
859
- cachedPositions = positions;
860
- const formatted = positions.map((p) => ({
861
- ticker: p.ticker,
862
- side: p.side,
863
- quantity: p.quantity,
864
- avg_price: `${p.average_price_paid}¢`,
865
- current_price: `${p.current_value}¢`,
866
- unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
867
- total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
868
- realized_pnl: `$${(p.realized_pnl / 100).toFixed(2)}`,
869
- }));
870
914
  return {
871
- content: [{ type: 'text', text: JSON.stringify(formatted, null, 2) }],
915
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
872
916
  details: {},
873
917
  };
874
918
  },
@@ -1129,6 +1173,139 @@ async function agentCommand(thesisId, opts) {
1129
1173
  return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
1130
1174
  },
1131
1175
  },
1176
+ {
1177
+ name: 'get_liquidity',
1178
+ label: 'Liquidity Scanner',
1179
+ description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
1180
+ parameters: Type.Object({
1181
+ topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
1182
+ }),
1183
+ execute: async (_toolCallId, params) => {
1184
+ const topicKey = params.topic.toLowerCase();
1185
+ const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
1186
+ if (!seriesList) {
1187
+ return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
1188
+ }
1189
+ const results = [];
1190
+ for (const series of seriesList) {
1191
+ try {
1192
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
1193
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
1194
+ if (!res.ok)
1195
+ continue;
1196
+ const markets = (await res.json()).markets || [];
1197
+ const obResults = await Promise.allSettled(markets.slice(0, 20).map((m) => (0, kalshi_js_1.getPublicOrderbook)(m.ticker).then(ob => ({ ticker: m.ticker, title: m.title, ob }))));
1198
+ for (const r of obResults) {
1199
+ if (r.status !== 'fulfilled' || !r.value.ob)
1200
+ continue;
1201
+ const { ticker, title, ob } = r.value;
1202
+ const yes = (ob.yes_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1203
+ const no = (ob.no_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1204
+ const bestBid = yes[0]?.price || 0;
1205
+ const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
1206
+ const spread = bestAsk - bestBid;
1207
+ const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
1208
+ const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
1209
+ results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
1210
+ }
1211
+ }
1212
+ catch { /* skip */ }
1213
+ }
1214
+ try {
1215
+ const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
1216
+ for (const event of events) {
1217
+ for (const m of (event.markets || []).slice(0, 5)) {
1218
+ if (!m.active || m.closed || !m.clobTokenIds)
1219
+ continue;
1220
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
1221
+ if (!ids)
1222
+ continue;
1223
+ const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
1224
+ if (!d)
1225
+ continue;
1226
+ results.push({ venue: 'polymarket', ticker: (m.question || event.title).slice(0, 50), bestBid: d.bestBid, bestAsk: d.bestAsk, spread: d.spread, depth: d.bidDepthTop3 + d.askDepthTop3, liquidityScore: d.liquidityScore });
1227
+ }
1228
+ }
1229
+ }
1230
+ catch { /* skip */ }
1231
+ results.sort((a, b) => a.spread - b.spread);
1232
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
1233
+ },
1234
+ },
1235
+ {
1236
+ name: 'inspect_book',
1237
+ label: 'Orderbook',
1238
+ description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
1239
+ parameters: Type.Object({
1240
+ ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
1241
+ polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
1242
+ }),
1243
+ execute: async (_toolCallId, params) => {
1244
+ const results = [];
1245
+ if (params.ticker) {
1246
+ try {
1247
+ const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
1248
+ const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
1249
+ const yesBids = (ob?.yes_dollars || [])
1250
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
1251
+ .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1252
+ const noAsks = (ob?.no_dollars || [])
1253
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
1254
+ .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
1255
+ const bestBid = yesBids[0]?.price || 0;
1256
+ const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
1257
+ const spread = bestAsk - bestBid;
1258
+ const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
1259
+ const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
1260
+ results.push({
1261
+ venue: 'kalshi', ticker: params.ticker, title: market.title,
1262
+ bestBid, bestAsk, spread, liquidityScore: liq,
1263
+ bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
1264
+ totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
1265
+ totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
1266
+ lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
1267
+ volume24h: parseFloat(market.volume_24h_fp || '0'),
1268
+ openInterest: parseFloat(market.open_interest_fp || '0'),
1269
+ expiry: market.close_time || null,
1270
+ });
1271
+ }
1272
+ catch (err) {
1273
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
1274
+ }
1275
+ }
1276
+ if (params.polyQuery) {
1277
+ try {
1278
+ const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
1279
+ for (const event of events) {
1280
+ for (const m of (event.markets || []).slice(0, 3)) {
1281
+ if (!m.active || m.closed || !m.clobTokenIds)
1282
+ continue;
1283
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
1284
+ if (!ids)
1285
+ continue;
1286
+ const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
1287
+ if (!depth)
1288
+ continue;
1289
+ const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
1290
+ results.push({
1291
+ venue: 'polymarket', title: m.question || event.title,
1292
+ bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
1293
+ liquidityScore: depth.liquidityScore,
1294
+ totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
1295
+ lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
1296
+ volume24h: m.volume24hr || 0,
1297
+ });
1298
+ }
1299
+ }
1300
+ }
1301
+ catch { /* skip */ }
1302
+ }
1303
+ if (results.length === 0) {
1304
+ return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
1305
+ }
1306
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
1307
+ },
1308
+ },
1132
1309
  {
1133
1310
  name: 'get_schedule',
1134
1311
  label: 'Schedule',
@@ -2529,6 +2706,139 @@ async function runPlainTextAgent(params) {
2529
2706
  return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
2530
2707
  },
2531
2708
  },
2709
+ {
2710
+ name: 'get_liquidity',
2711
+ label: 'Liquidity Scanner',
2712
+ description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
2713
+ parameters: Type.Object({
2714
+ topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
2715
+ }),
2716
+ execute: async (_toolCallId, params) => {
2717
+ const topicKey = params.topic.toLowerCase();
2718
+ const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
2719
+ if (!seriesList) {
2720
+ return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
2721
+ }
2722
+ const results = [];
2723
+ for (const series of seriesList) {
2724
+ try {
2725
+ const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
2726
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
2727
+ if (!res.ok)
2728
+ continue;
2729
+ const markets = (await res.json()).markets || [];
2730
+ const obResults = await Promise.allSettled(markets.slice(0, 20).map((m) => (0, kalshi_js_1.getPublicOrderbook)(m.ticker).then(ob => ({ ticker: m.ticker, title: m.title, ob }))));
2731
+ for (const r of obResults) {
2732
+ if (r.status !== 'fulfilled' || !r.value.ob)
2733
+ continue;
2734
+ const { ticker, title, ob } = r.value;
2735
+ const yes = (ob.yes_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
2736
+ const no = (ob.no_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
2737
+ const bestBid = yes[0]?.price || 0;
2738
+ const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
2739
+ const spread = bestAsk - bestBid;
2740
+ const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
2741
+ const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
2742
+ results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
2743
+ }
2744
+ }
2745
+ catch { /* skip */ }
2746
+ }
2747
+ try {
2748
+ const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
2749
+ for (const event of events) {
2750
+ for (const m of (event.markets || []).slice(0, 5)) {
2751
+ if (!m.active || m.closed || !m.clobTokenIds)
2752
+ continue;
2753
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
2754
+ if (!ids)
2755
+ continue;
2756
+ const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
2757
+ if (!d)
2758
+ continue;
2759
+ results.push({ venue: 'polymarket', ticker: (m.question || event.title).slice(0, 50), bestBid: d.bestBid, bestAsk: d.bestAsk, spread: d.spread, depth: d.bidDepthTop3 + d.askDepthTop3, liquidityScore: d.liquidityScore });
2760
+ }
2761
+ }
2762
+ }
2763
+ catch { /* skip */ }
2764
+ results.sort((a, b) => a.spread - b.spread);
2765
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
2766
+ },
2767
+ },
2768
+ {
2769
+ name: 'inspect_book',
2770
+ label: 'Orderbook',
2771
+ description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
2772
+ parameters: Type.Object({
2773
+ ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
2774
+ polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
2775
+ }),
2776
+ execute: async (_toolCallId, params) => {
2777
+ const results = [];
2778
+ if (params.ticker) {
2779
+ try {
2780
+ const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
2781
+ const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
2782
+ const yesBids = (ob?.yes_dollars || [])
2783
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
2784
+ .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
2785
+ const noAsks = (ob?.no_dollars || [])
2786
+ .map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
2787
+ .filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
2788
+ const bestBid = yesBids[0]?.price || 0;
2789
+ const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
2790
+ const spread = bestAsk - bestBid;
2791
+ const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
2792
+ const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
2793
+ results.push({
2794
+ venue: 'kalshi', ticker: params.ticker, title: market.title,
2795
+ bestBid, bestAsk, spread, liquidityScore: liq,
2796
+ bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
2797
+ totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
2798
+ totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
2799
+ lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
2800
+ volume24h: parseFloat(market.volume_24h_fp || '0'),
2801
+ openInterest: parseFloat(market.open_interest_fp || '0'),
2802
+ expiry: market.close_time || null,
2803
+ });
2804
+ }
2805
+ catch (err) {
2806
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
2807
+ }
2808
+ }
2809
+ if (params.polyQuery) {
2810
+ try {
2811
+ const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
2812
+ for (const event of events) {
2813
+ for (const m of (event.markets || []).slice(0, 3)) {
2814
+ if (!m.active || m.closed || !m.clobTokenIds)
2815
+ continue;
2816
+ const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
2817
+ if (!ids)
2818
+ continue;
2819
+ const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
2820
+ if (!depth)
2821
+ continue;
2822
+ const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
2823
+ results.push({
2824
+ venue: 'polymarket', title: m.question || event.title,
2825
+ bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
2826
+ liquidityScore: depth.liquidityScore,
2827
+ totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
2828
+ lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
2829
+ volume24h: m.volume24hr || 0,
2830
+ });
2831
+ }
2832
+ }
2833
+ }
2834
+ catch { /* skip */ }
2835
+ }
2836
+ if (results.length === 0) {
2837
+ return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
2838
+ }
2839
+ return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
2840
+ },
2841
+ },
2532
2842
  {
2533
2843
  name: 'get_schedule',
2534
2844
  label: 'Schedule',
@@ -2870,22 +3180,34 @@ async function runPlainTextAgent(params) {
2870
3180
  .map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
2871
3181
  .join('\n') || ' (no causal tree)';
2872
3182
  const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
2873
- const systemPrompt = `You are a prediction market trading assistant. Help the user make correct trading decisions.
3183
+ const systemPrompt = `You are a prediction market trading assistant. Your job is not to please the user — it is to help them see reality clearly and make correct trading decisions.
2874
3184
 
2875
- Current thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
2876
- ID: ${resolvedThesisId}
2877
- Confidence: ${conf}%
2878
- Status: ${ctx.status}
3185
+ ## Framework
3186
+ Each thesis has a causal tree. Every node is a hypothesis with a probability. Edge = thesis-implied price - market price. Positive edge = market underprices. Contracts with large edge + good liquidity = most tradeable.
3187
+ executableEdge = edge after subtracting bid-ask spread. A big theoretical edge with wide spread may not be worth entering.
3188
+
3189
+ ## Rules
3190
+ - Be concise. Use tools for fresh data. Don't guess prices.
3191
+ - If user mentions news, inject_signal immediately.
3192
+ - If user says "evaluate", trigger immediately. Don't confirm.
3193
+ - Don't end with "anything else?"
3194
+ - If an edge is narrowing or disappearing, say so proactively.
3195
+ - Use Chinese if user writes Chinese, English if English.
3196
+ - Prices in cents (¢). P&L in dollars ($). Don't re-convert tool output.
3197
+ - When a trade idea emerges, create_strategy to record it.
3198
+ ${config.tradingEnabled ? '- Trading ENABLED. You have place_order and cancel_order tools.' : '- Trading DISABLED. Tell user: sf setup --enable-trading'}
2879
3199
 
2880
- Causal tree nodes:
3200
+ ## Current State
3201
+ Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
3202
+ ID: ${resolvedThesisId} | Confidence: ${conf}% | Status: ${ctx.status}
3203
+
3204
+ Causal nodes:
2881
3205
  ${nodesSummary}
2882
3206
 
2883
3207
  Top edges:
2884
3208
  ${edgesSummary}
2885
3209
 
2886
- ${ctx.lastEvaluation?.summary ? `Latest evaluation: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}
2887
-
2888
- Rules: Be concise. Use tools when needed. Don't ask "anything else?". Prices are in cents (e.g. 35¢). P&L, cost, and balance are in dollars (e.g. $90.66). Tool outputs are pre-formatted with units — do not re-convert.`;
3210
+ ${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
2889
3211
  // ── Create agent ──────────────────────────────────────────────────────────
2890
3212
  const agent = new Agent({
2891
3213
  initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
@@ -0,0 +1,17 @@
1
+ /**
2
+ * sf book — Orderbook depth, price history, and liquidity for individual markets
3
+ *
4
+ * Usage:
5
+ * sf book KXWTIMAX-26DEC31-T135 Single Kalshi market
6
+ * sf book KXWTI-T135 KXCPI-26MAY Multiple markets
7
+ * sf book --poly "oil price" Polymarket search
8
+ * sf book KXWTIMAX-26DEC31-T135 --history With 7d price history
9
+ * sf book KXWTIMAX-26DEC31-T135 --json JSON output
10
+ */
11
+ interface BookOpts {
12
+ poly?: string;
13
+ history?: boolean;
14
+ json?: boolean;
15
+ }
16
+ export declare function bookCommand(tickers: string[], opts: BookOpts): Promise<void>;
17
+ export {};