@spfunctions/cli 1.4.5 → 1.5.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.
@@ -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',
@@ -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 {};