@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 +2 -0
- package/dist/client.js +11 -2
- package/dist/client.test.js +1 -1
- package/dist/commands/agent.js +370 -48
- package/dist/commands/book.d.ts +17 -0
- package/dist/commands/book.js +220 -0
- package/dist/commands/create.d.ts +2 -0
- package/dist/commands/create.js +18 -7
- package/dist/commands/dashboard.js +30 -1
- package/dist/commands/liquidity.d.ts +2 -0
- package/dist/commands/liquidity.js +128 -43
- package/dist/commands/list.d.ts +1 -0
- package/dist/commands/list.js +4 -0
- package/dist/commands/positions.js +50 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +66 -15
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +71 -6
- package/dist/config.d.ts +2 -0
- package/dist/config.js +8 -0
- package/dist/index.js +97 -11
- package/dist/polymarket.d.ts +237 -0
- package/dist/polymarket.js +353 -0
- package/dist/polymarket.test.d.ts +1 -0
- package/dist/polymarket.test.js +424 -0
- package/dist/telegram/agent-bridge.js +81 -8
- package/dist/topics.d.ts +3 -0
- package/dist/topics.js +65 -7
- package/dist/topics.test.js +83 -6
- package/dist/tui/dashboard.js +65 -30
- package/dist/tui/widgets/edges.js +5 -4
- package/dist/tui/widgets/portfolio.js +3 -2
- package/package.json +1 -1
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
|
+

|
|
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
|
-
|
|
41
|
-
|
|
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
|
}
|
package/dist/client.test.js
CHANGED
|
@@ -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
|
|
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({
|
package/dist/commands/agent.js
CHANGED
|
@@ -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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
810
|
-
.filter((s) => keywords.
|
|
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,
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
|
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 (
|
|
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: '
|
|
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(
|
|
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.
|
|
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
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
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
|
-
|
|
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
|
|
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 {};
|