@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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const polymarket_js_1 = require("./polymarket.js");
|
|
5
|
+
const mockFetch = vitest_1.vi.fn();
|
|
6
|
+
vitest_1.vi.stubGlobal('fetch', mockFetch);
|
|
7
|
+
(0, vitest_1.beforeEach)(() => {
|
|
8
|
+
mockFetch.mockReset();
|
|
9
|
+
});
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// PURE FUNCTIONS (no network)
|
|
12
|
+
// ============================================================================
|
|
13
|
+
(0, vitest_1.describe)('computeOrderbookDepth', () => {
|
|
14
|
+
(0, vitest_1.it)('computes best bid/ask and spread in cents', () => {
|
|
15
|
+
const raw = {
|
|
16
|
+
market: 'cond1', asset_id: 'tok1', timestamp: '123', hash: 'h',
|
|
17
|
+
bids: [
|
|
18
|
+
{ price: '0.45', size: '200' },
|
|
19
|
+
{ price: '0.44', size: '150' },
|
|
20
|
+
{ price: '0.43', size: '100' },
|
|
21
|
+
],
|
|
22
|
+
asks: [
|
|
23
|
+
{ price: '0.48', size: '300' },
|
|
24
|
+
{ price: '0.49', size: '250' },
|
|
25
|
+
{ price: '0.50', size: '100' },
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
const depth = (0, polymarket_js_1.computeOrderbookDepth)(raw);
|
|
29
|
+
(0, vitest_1.expect)(depth.bestBid).toBe(45);
|
|
30
|
+
(0, vitest_1.expect)(depth.bestAsk).toBe(48);
|
|
31
|
+
(0, vitest_1.expect)(depth.spread).toBe(3);
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)('computes top-3 depth', () => {
|
|
34
|
+
const raw = {
|
|
35
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
36
|
+
bids: [
|
|
37
|
+
{ price: '0.50', size: '100' },
|
|
38
|
+
{ price: '0.49', size: '200' },
|
|
39
|
+
{ price: '0.48', size: '300' },
|
|
40
|
+
{ price: '0.47', size: '400' }, // 4th level, not counted
|
|
41
|
+
],
|
|
42
|
+
asks: [
|
|
43
|
+
{ price: '0.52', size: '50' },
|
|
44
|
+
{ price: '0.53', size: '60' },
|
|
45
|
+
{ price: '0.54', size: '70' },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
const depth = (0, polymarket_js_1.computeOrderbookDepth)(raw);
|
|
49
|
+
(0, vitest_1.expect)(depth.bidDepthTop3).toBe(600); // 100+200+300
|
|
50
|
+
(0, vitest_1.expect)(depth.askDepthTop3).toBe(180); // 50+60+70
|
|
51
|
+
(0, vitest_1.expect)(depth.totalBidDepth).toBe(1000); // includes 4th level
|
|
52
|
+
(0, vitest_1.expect)(depth.totalAskDepth).toBe(180);
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.it)('sorts bids descending and asks ascending', () => {
|
|
55
|
+
const raw = {
|
|
56
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
57
|
+
bids: [
|
|
58
|
+
{ price: '0.40', size: '100' },
|
|
59
|
+
{ price: '0.50', size: '200' }, // best bid (highest)
|
|
60
|
+
],
|
|
61
|
+
asks: [
|
|
62
|
+
{ price: '0.60', size: '100' },
|
|
63
|
+
{ price: '0.55', size: '200' }, // best ask (lowest)
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const depth = (0, polymarket_js_1.computeOrderbookDepth)(raw);
|
|
67
|
+
(0, vitest_1.expect)(depth.bestBid).toBe(50);
|
|
68
|
+
(0, vitest_1.expect)(depth.bestAsk).toBe(55);
|
|
69
|
+
(0, vitest_1.expect)(depth.spread).toBe(5);
|
|
70
|
+
});
|
|
71
|
+
(0, vitest_1.it)('handles empty orderbook', () => {
|
|
72
|
+
const raw = {
|
|
73
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
74
|
+
bids: [], asks: [],
|
|
75
|
+
};
|
|
76
|
+
const depth = (0, polymarket_js_1.computeOrderbookDepth)(raw);
|
|
77
|
+
(0, vitest_1.expect)(depth.bestBid).toBe(0);
|
|
78
|
+
(0, vitest_1.expect)(depth.bestAsk).toBe(100);
|
|
79
|
+
(0, vitest_1.expect)(depth.spread).toBe(100);
|
|
80
|
+
(0, vitest_1.expect)(depth.bidDepthTop3).toBe(0);
|
|
81
|
+
(0, vitest_1.expect)(depth.askDepthTop3).toBe(0);
|
|
82
|
+
(0, vitest_1.expect)(depth.liquidityScore).toBe('low');
|
|
83
|
+
});
|
|
84
|
+
(0, vitest_1.it)('assigns correct liquidity scores', () => {
|
|
85
|
+
// high: spread ≤ 2¢, depth ≥ 500
|
|
86
|
+
const high = {
|
|
87
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
88
|
+
bids: [{ price: '0.49', size: '300' }],
|
|
89
|
+
asks: [{ price: '0.51', size: '300' }],
|
|
90
|
+
};
|
|
91
|
+
(0, vitest_1.expect)((0, polymarket_js_1.computeOrderbookDepth)(high).liquidityScore).toBe('high');
|
|
92
|
+
// medium: spread ≤ 5¢, depth ≥ 100
|
|
93
|
+
const medium = {
|
|
94
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
95
|
+
bids: [{ price: '0.47', size: '60' }],
|
|
96
|
+
asks: [{ price: '0.52', size: '60' }],
|
|
97
|
+
};
|
|
98
|
+
(0, vitest_1.expect)((0, polymarket_js_1.computeOrderbookDepth)(medium).liquidityScore).toBe('medium');
|
|
99
|
+
// low: wide spread
|
|
100
|
+
const low = {
|
|
101
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
102
|
+
bids: [{ price: '0.30', size: '10' }],
|
|
103
|
+
asks: [{ price: '0.70', size: '10' }],
|
|
104
|
+
};
|
|
105
|
+
(0, vitest_1.expect)((0, polymarket_js_1.computeOrderbookDepth)(low).liquidityScore).toBe('low');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.describe)('scoreLiquidity', () => {
|
|
109
|
+
(0, vitest_1.it)('returns high for tight spread + deep book', () => {
|
|
110
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(1, 600)).toBe('high');
|
|
111
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(2, 500)).toBe('high');
|
|
112
|
+
});
|
|
113
|
+
(0, vitest_1.it)('returns medium for moderate spread + depth', () => {
|
|
114
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(3, 200)).toBe('medium');
|
|
115
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(5, 100)).toBe('medium');
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)('returns low for wide spread or thin book', () => {
|
|
118
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(6, 1000)).toBe('low');
|
|
119
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(1, 50)).toBe('low');
|
|
120
|
+
(0, vitest_1.expect)((0, polymarket_js_1.scoreLiquidity)(10, 10)).toBe('low');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
(0, vitest_1.describe)('parseClobTokenIds', () => {
|
|
124
|
+
(0, vitest_1.it)('parses valid JSON array', () => {
|
|
125
|
+
const result = (0, polymarket_js_1.parseClobTokenIds)('["tok_yes_123","tok_no_456"]');
|
|
126
|
+
(0, vitest_1.expect)(result).toEqual(['tok_yes_123', 'tok_no_456']);
|
|
127
|
+
});
|
|
128
|
+
(0, vitest_1.it)('returns null for invalid JSON', () => {
|
|
129
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseClobTokenIds)('not json')).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
(0, vitest_1.it)('returns null for array with < 2 elements', () => {
|
|
132
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseClobTokenIds)('["only_one"]')).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
(0, vitest_1.it)('returns null for empty string', () => {
|
|
135
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseClobTokenIds)('')).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
(0, vitest_1.describe)('parseOutcomes', () => {
|
|
139
|
+
(0, vitest_1.it)('parses yes/no outcomes', () => {
|
|
140
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseOutcomes)('["Yes","No"]')).toEqual(['Yes', 'No']);
|
|
141
|
+
});
|
|
142
|
+
(0, vitest_1.it)('parses multi-outcome', () => {
|
|
143
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseOutcomes)('["Trump","Biden","Other"]')).toEqual(['Trump', 'Biden', 'Other']);
|
|
144
|
+
});
|
|
145
|
+
(0, vitest_1.it)('returns empty for invalid', () => {
|
|
146
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseOutcomes)('bad')).toEqual([]);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
(0, vitest_1.describe)('parseOutcomePrices', () => {
|
|
150
|
+
(0, vitest_1.it)('parses price array', () => {
|
|
151
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseOutcomePrices)('[0.65, 0.35]')).toEqual([0.65, 0.35]);
|
|
152
|
+
});
|
|
153
|
+
(0, vitest_1.it)('returns empty for invalid', () => {
|
|
154
|
+
(0, vitest_1.expect)((0, polymarket_js_1.parseOutcomePrices)('')).toEqual([]);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
(0, vitest_1.describe)('toCents', () => {
|
|
158
|
+
(0, vitest_1.it)('converts dollars to cents', () => {
|
|
159
|
+
(0, vitest_1.expect)((0, polymarket_js_1.toCents)(0.55)).toBe(55);
|
|
160
|
+
(0, vitest_1.expect)((0, polymarket_js_1.toCents)(1.0)).toBe(100);
|
|
161
|
+
(0, vitest_1.expect)((0, polymarket_js_1.toCents)(0)).toBe(0);
|
|
162
|
+
(0, vitest_1.expect)((0, polymarket_js_1.toCents)(0.123)).toBe(12); // rounds
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
(0, vitest_1.describe)('polymarketUrl', () => {
|
|
166
|
+
(0, vitest_1.it)('builds correct URL', () => {
|
|
167
|
+
(0, vitest_1.expect)((0, polymarket_js_1.polymarketUrl)('fed-decision')).toBe('https://polymarket.com/event/fed-decision');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// API FUNCTIONS (mocked fetch)
|
|
172
|
+
// ============================================================================
|
|
173
|
+
(0, vitest_1.describe)('polymarketSearch', () => {
|
|
174
|
+
(0, vitest_1.it)('calls Gamma API with query params', async () => {
|
|
175
|
+
mockFetch.mockResolvedValue({
|
|
176
|
+
ok: true,
|
|
177
|
+
json: () => Promise.resolve({ events: [{ id: '1', title: 'Test' }] }),
|
|
178
|
+
});
|
|
179
|
+
const events = await (0, polymarket_js_1.polymarketSearch)('recession', 5);
|
|
180
|
+
(0, vitest_1.expect)(events).toHaveLength(1);
|
|
181
|
+
(0, vitest_1.expect)(events[0].title).toBe('Test');
|
|
182
|
+
const url = mockFetch.mock.calls[0][0];
|
|
183
|
+
(0, vitest_1.expect)(url).toContain('gamma-api.polymarket.com/public-search');
|
|
184
|
+
(0, vitest_1.expect)(url).toContain('q=recession');
|
|
185
|
+
(0, vitest_1.expect)(url).toContain('limit_per_type=5');
|
|
186
|
+
});
|
|
187
|
+
(0, vitest_1.it)('throws on error response', async () => {
|
|
188
|
+
mockFetch.mockResolvedValue({ ok: false, status: 500 });
|
|
189
|
+
await (0, vitest_1.expect)((0, polymarket_js_1.polymarketSearch)('test')).rejects.toThrow('500');
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
(0, vitest_1.describe)('polymarketListEvents', () => {
|
|
193
|
+
(0, vitest_1.it)('fetches active events with pagination', async () => {
|
|
194
|
+
mockFetch.mockResolvedValue({
|
|
195
|
+
ok: true,
|
|
196
|
+
json: () => Promise.resolve([{ id: '1' }, { id: '2' }]),
|
|
197
|
+
});
|
|
198
|
+
const events = await (0, polymarket_js_1.polymarketListEvents)({ limit: 10, offset: 20, order: 'volume_24hr' });
|
|
199
|
+
const url = mockFetch.mock.calls[0][0];
|
|
200
|
+
(0, vitest_1.expect)(url).toContain('active=true');
|
|
201
|
+
(0, vitest_1.expect)(url).toContain('limit=10');
|
|
202
|
+
(0, vitest_1.expect)(url).toContain('offset=20');
|
|
203
|
+
(0, vitest_1.expect)(url).toContain('order=volume_24hr');
|
|
204
|
+
(0, vitest_1.expect)(events).toHaveLength(2);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
(0, vitest_1.describe)('polymarketGetEvent', () => {
|
|
208
|
+
(0, vitest_1.it)('fetches single event by ID', async () => {
|
|
209
|
+
mockFetch.mockResolvedValue({
|
|
210
|
+
ok: true,
|
|
211
|
+
json: () => Promise.resolve({ id: '42', title: 'Fed Decision' }),
|
|
212
|
+
});
|
|
213
|
+
const event = await (0, polymarket_js_1.polymarketGetEvent)('42');
|
|
214
|
+
(0, vitest_1.expect)(event.id).toBe('42');
|
|
215
|
+
(0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toContain('/events/42');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
(0, vitest_1.describe)('polymarketGetMarket', () => {
|
|
219
|
+
(0, vitest_1.it)('fetches single market', async () => {
|
|
220
|
+
mockFetch.mockResolvedValue({
|
|
221
|
+
ok: true,
|
|
222
|
+
json: () => Promise.resolve({ id: 'm1', question: 'Will X happen?' }),
|
|
223
|
+
});
|
|
224
|
+
const market = await (0, polymarket_js_1.polymarketGetMarket)('m1');
|
|
225
|
+
(0, vitest_1.expect)(market.question).toBe('Will X happen?');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
(0, vitest_1.describe)('polymarketListTags', () => {
|
|
229
|
+
(0, vitest_1.it)('returns tags array', async () => {
|
|
230
|
+
mockFetch.mockResolvedValue({
|
|
231
|
+
ok: true,
|
|
232
|
+
json: () => Promise.resolve([
|
|
233
|
+
{ id: '1', label: 'Politics', slug: 'politics' },
|
|
234
|
+
{ id: '2', label: 'Crypto', slug: 'crypto' },
|
|
235
|
+
]),
|
|
236
|
+
});
|
|
237
|
+
const tags = await (0, polymarket_js_1.polymarketListTags)();
|
|
238
|
+
(0, vitest_1.expect)(tags).toHaveLength(2);
|
|
239
|
+
(0, vitest_1.expect)(tags[0].label).toBe('Politics');
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
(0, vitest_1.describe)('polymarketGetOrderbook', () => {
|
|
243
|
+
(0, vitest_1.it)('fetches raw orderbook', async () => {
|
|
244
|
+
mockFetch.mockResolvedValue({
|
|
245
|
+
ok: true,
|
|
246
|
+
json: () => Promise.resolve({
|
|
247
|
+
market: 'cond1', asset_id: 'tok1', timestamp: '123', hash: 'h',
|
|
248
|
+
bids: [{ price: '0.45', size: '100' }],
|
|
249
|
+
asks: [{ price: '0.55', size: '100' }],
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
const ob = await (0, polymarket_js_1.polymarketGetOrderbook)('tok1');
|
|
253
|
+
(0, vitest_1.expect)(ob).not.toBeNull();
|
|
254
|
+
(0, vitest_1.expect)(ob.bids).toHaveLength(1);
|
|
255
|
+
(0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toContain('clob.polymarket.com/book?token_id=tok1');
|
|
256
|
+
});
|
|
257
|
+
(0, vitest_1.it)('returns null on error', async () => {
|
|
258
|
+
mockFetch.mockResolvedValue({ ok: false, status: 404 });
|
|
259
|
+
const ob = await (0, polymarket_js_1.polymarketGetOrderbook)('bad');
|
|
260
|
+
(0, vitest_1.expect)(ob).toBeNull();
|
|
261
|
+
});
|
|
262
|
+
(0, vitest_1.it)('returns null on API error response', async () => {
|
|
263
|
+
mockFetch.mockResolvedValue({
|
|
264
|
+
ok: true,
|
|
265
|
+
json: () => Promise.resolve({ error: 'No orderbook exists' }),
|
|
266
|
+
});
|
|
267
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetOrderbook)('bad')).toBeNull();
|
|
268
|
+
});
|
|
269
|
+
(0, vitest_1.it)('returns null on network error', async () => {
|
|
270
|
+
mockFetch.mockRejectedValue(new Error('network'));
|
|
271
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetOrderbook)('bad')).toBeNull();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
(0, vitest_1.describe)('polymarketGetOrderbookWithDepth', () => {
|
|
275
|
+
(0, vitest_1.it)('fetches and computes depth in one call', async () => {
|
|
276
|
+
mockFetch.mockResolvedValue({
|
|
277
|
+
ok: true,
|
|
278
|
+
json: () => Promise.resolve({
|
|
279
|
+
market: 'c', asset_id: 't', timestamp: '0', hash: '',
|
|
280
|
+
bids: [{ price: '0.49', size: '300' }],
|
|
281
|
+
asks: [{ price: '0.51', size: '400' }],
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)('tok1');
|
|
285
|
+
(0, vitest_1.expect)(depth).not.toBeNull();
|
|
286
|
+
(0, vitest_1.expect)(depth.bestBid).toBe(49);
|
|
287
|
+
(0, vitest_1.expect)(depth.bestAsk).toBe(51);
|
|
288
|
+
(0, vitest_1.expect)(depth.spread).toBe(2);
|
|
289
|
+
(0, vitest_1.expect)(depth.liquidityScore).toBe('high');
|
|
290
|
+
});
|
|
291
|
+
(0, vitest_1.it)('returns null when orderbook unavailable', async () => {
|
|
292
|
+
mockFetch.mockResolvedValue({ ok: false });
|
|
293
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)('bad')).toBeNull();
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
(0, vitest_1.describe)('polymarketGetMidpoint', () => {
|
|
297
|
+
(0, vitest_1.it)('returns midpoint as number', async () => {
|
|
298
|
+
mockFetch.mockResolvedValue({
|
|
299
|
+
ok: true,
|
|
300
|
+
json: () => Promise.resolve({ mid: '0.52' }),
|
|
301
|
+
});
|
|
302
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetMidpoint)('tok1')).toBe(0.52);
|
|
303
|
+
});
|
|
304
|
+
(0, vitest_1.it)('returns null on error', async () => {
|
|
305
|
+
mockFetch.mockResolvedValue({ ok: false });
|
|
306
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetMidpoint)('bad')).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
(0, vitest_1.describe)('polymarketGetSpread', () => {
|
|
310
|
+
(0, vitest_1.it)('returns spread as number', async () => {
|
|
311
|
+
mockFetch.mockResolvedValue({
|
|
312
|
+
ok: true,
|
|
313
|
+
json: () => Promise.resolve({ spread: '0.03' }),
|
|
314
|
+
});
|
|
315
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetSpread)('tok1')).toBe(0.03);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
(0, vitest_1.describe)('polymarketGetMidpoints', () => {
|
|
319
|
+
(0, vitest_1.it)('posts batch of token IDs', async () => {
|
|
320
|
+
mockFetch.mockResolvedValue({
|
|
321
|
+
ok: true,
|
|
322
|
+
json: () => Promise.resolve({ tok1: 0.5, tok2: 0.7 }),
|
|
323
|
+
});
|
|
324
|
+
const result = await (0, polymarket_js_1.polymarketGetMidpoints)(['tok1', 'tok2']);
|
|
325
|
+
(0, vitest_1.expect)(result.tok1).toBe(0.5);
|
|
326
|
+
(0, vitest_1.expect)(result.tok2).toBe(0.7);
|
|
327
|
+
(0, vitest_1.expect)(mockFetch.mock.calls[0][1].method).toBe('POST');
|
|
328
|
+
});
|
|
329
|
+
(0, vitest_1.it)('returns empty for empty input', async () => {
|
|
330
|
+
const result = await (0, polymarket_js_1.polymarketGetMidpoints)([]);
|
|
331
|
+
(0, vitest_1.expect)(result).toEqual({});
|
|
332
|
+
(0, vitest_1.expect)(mockFetch).not.toHaveBeenCalled();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
(0, vitest_1.describe)('polymarketGetOHLC', () => {
|
|
336
|
+
(0, vitest_1.it)('fetches candlestick data', async () => {
|
|
337
|
+
const candles = [
|
|
338
|
+
{ t: 1000, o: 0.5, h: 0.55, l: 0.48, c: 0.52, v: 100 },
|
|
339
|
+
];
|
|
340
|
+
mockFetch.mockResolvedValue({
|
|
341
|
+
ok: true,
|
|
342
|
+
json: () => Promise.resolve(candles),
|
|
343
|
+
});
|
|
344
|
+
const result = await (0, polymarket_js_1.polymarketGetOHLC)({
|
|
345
|
+
tokenId: 'tok1',
|
|
346
|
+
startTs: 1000,
|
|
347
|
+
fidelity: '1h',
|
|
348
|
+
});
|
|
349
|
+
(0, vitest_1.expect)(result).toHaveLength(1);
|
|
350
|
+
(0, vitest_1.expect)(result[0].c).toBe(0.52);
|
|
351
|
+
const url = mockFetch.mock.calls[0][0];
|
|
352
|
+
(0, vitest_1.expect)(url).toContain('clob.polymarket.com/ohlc');
|
|
353
|
+
(0, vitest_1.expect)(url).toContain('asset_id=tok1');
|
|
354
|
+
(0, vitest_1.expect)(url).toContain('fidelity=1h');
|
|
355
|
+
});
|
|
356
|
+
(0, vitest_1.it)('returns empty array on error', async () => {
|
|
357
|
+
mockFetch.mockResolvedValue({ ok: false, status: 400 });
|
|
358
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetOHLC)({ tokenId: 'bad', startTs: 0 })).toEqual([]);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
(0, vitest_1.describe)('polymarketGetPriceHistory', () => {
|
|
362
|
+
(0, vitest_1.it)('fetches price history with interval', async () => {
|
|
363
|
+
mockFetch.mockResolvedValue({
|
|
364
|
+
ok: true,
|
|
365
|
+
json: () => Promise.resolve({
|
|
366
|
+
history: [{ t: 1000, p: 0.5 }, { t: 2000, p: 0.55 }],
|
|
367
|
+
}),
|
|
368
|
+
});
|
|
369
|
+
const result = await (0, polymarket_js_1.polymarketGetPriceHistory)({
|
|
370
|
+
tokenId: 'tok1',
|
|
371
|
+
interval: '1d',
|
|
372
|
+
});
|
|
373
|
+
(0, vitest_1.expect)(result).toHaveLength(2);
|
|
374
|
+
const url = mockFetch.mock.calls[0][0];
|
|
375
|
+
(0, vitest_1.expect)(url).toContain('prices-history');
|
|
376
|
+
(0, vitest_1.expect)(url).toContain('interval=1d');
|
|
377
|
+
});
|
|
378
|
+
(0, vitest_1.it)('fetches with absolute timestamps', async () => {
|
|
379
|
+
mockFetch.mockResolvedValue({
|
|
380
|
+
ok: true,
|
|
381
|
+
json: () => Promise.resolve({ history: [] }),
|
|
382
|
+
});
|
|
383
|
+
await (0, polymarket_js_1.polymarketGetPriceHistory)({
|
|
384
|
+
tokenId: 'tok1',
|
|
385
|
+
startTs: 1000,
|
|
386
|
+
endTs: 2000,
|
|
387
|
+
fidelity: 60,
|
|
388
|
+
});
|
|
389
|
+
const url = mockFetch.mock.calls[0][0];
|
|
390
|
+
(0, vitest_1.expect)(url).toContain('startTs=1000');
|
|
391
|
+
(0, vitest_1.expect)(url).toContain('endTs=2000');
|
|
392
|
+
(0, vitest_1.expect)(url).toContain('fidelity=60');
|
|
393
|
+
(0, vitest_1.expect)(url).not.toContain('interval=');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
(0, vitest_1.describe)('polymarketGetPositions', () => {
|
|
397
|
+
(0, vitest_1.it)('fetches positions for wallet address', async () => {
|
|
398
|
+
mockFetch.mockResolvedValue({
|
|
399
|
+
ok: true,
|
|
400
|
+
json: () => Promise.resolve([
|
|
401
|
+
{ asset: 'tok1', size: 100, outcome: 'Yes' },
|
|
402
|
+
]),
|
|
403
|
+
});
|
|
404
|
+
const positions = await (0, polymarket_js_1.polymarketGetPositions)('0xabc123');
|
|
405
|
+
(0, vitest_1.expect)(positions).toHaveLength(1);
|
|
406
|
+
(0, vitest_1.expect)(positions[0].outcome).toBe('Yes');
|
|
407
|
+
(0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toContain('data-api.polymarket.com/positions?user=0xabc123');
|
|
408
|
+
});
|
|
409
|
+
(0, vitest_1.it)('returns empty on error', async () => {
|
|
410
|
+
mockFetch.mockResolvedValue({ ok: false });
|
|
411
|
+
(0, vitest_1.expect)(await (0, polymarket_js_1.polymarketGetPositions)('bad')).toEqual([]);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
(0, vitest_1.describe)('polymarketGetClosedPositions', () => {
|
|
415
|
+
(0, vitest_1.it)('fetches closed positions', async () => {
|
|
416
|
+
mockFetch.mockResolvedValue({
|
|
417
|
+
ok: true,
|
|
418
|
+
json: () => Promise.resolve([{ asset: 'tok1', outcome: 'No' }]),
|
|
419
|
+
});
|
|
420
|
+
const positions = await (0, polymarket_js_1.polymarketGetClosedPositions)('0xabc');
|
|
421
|
+
(0, vitest_1.expect)(positions).toHaveLength(1);
|
|
422
|
+
(0, vitest_1.expect)(mockFetch.mock.calls[0][0]).toContain('closed-positions');
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -274,6 +274,53 @@ async function buildTools(sfClient, thesisId, latestContext) {
|
|
|
274
274
|
},
|
|
275
275
|
},
|
|
276
276
|
];
|
|
277
|
+
// Trading tools (only if enabled)
|
|
278
|
+
if (config.tradingEnabled) {
|
|
279
|
+
tools.push({
|
|
280
|
+
name: 'place_order',
|
|
281
|
+
label: 'Place Order',
|
|
282
|
+
description: 'Place a buy or sell order on Kalshi. Requires trading to be enabled. Always confirm with user before placing.',
|
|
283
|
+
parameters: Type.Object({
|
|
284
|
+
ticker: Type.String({ description: 'Market ticker (e.g. KXWTIMAX-26DEC31-T135)' }),
|
|
285
|
+
side: Type.String({ description: 'yes or no' }),
|
|
286
|
+
action: Type.String({ description: 'buy or sell' }),
|
|
287
|
+
count: Type.Number({ description: 'Number of contracts' }),
|
|
288
|
+
yes_price: Type.Number({ description: 'Price in cents (1-99)' }),
|
|
289
|
+
}),
|
|
290
|
+
execute: async (_id, p) => {
|
|
291
|
+
try {
|
|
292
|
+
const result = await kalshi.createOrder({
|
|
293
|
+
ticker: p.ticker,
|
|
294
|
+
side: p.side,
|
|
295
|
+
action: p.action,
|
|
296
|
+
type: 'limit',
|
|
297
|
+
count: p.count,
|
|
298
|
+
yes_price: p.yes_price,
|
|
299
|
+
});
|
|
300
|
+
return { content: [{ type: 'text', text: `✓ Order placed: ${(result.order || result).order_id || 'OK'}\n${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${p.yes_price}¢` }], details: {} };
|
|
301
|
+
}
|
|
302
|
+
catch (err) {
|
|
303
|
+
return { content: [{ type: 'text', text: `✗ Order failed: ${err.message}` }], details: {} };
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
}, {
|
|
307
|
+
name: 'cancel_order',
|
|
308
|
+
label: 'Cancel Order',
|
|
309
|
+
description: 'Cancel a resting order on Kalshi.',
|
|
310
|
+
parameters: Type.Object({
|
|
311
|
+
orderId: Type.String({ description: 'Order ID to cancel' }),
|
|
312
|
+
}),
|
|
313
|
+
execute: async (_id, p) => {
|
|
314
|
+
try {
|
|
315
|
+
await kalshi.cancelOrder(p.orderId);
|
|
316
|
+
return { content: [{ type: 'text', text: `✓ Order ${p.orderId} cancelled.` }], details: {} };
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
return { content: [{ type: 'text', text: `✗ Cancel failed: ${err.message}` }], details: {} };
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
}
|
|
277
324
|
return tools;
|
|
278
325
|
}
|
|
279
326
|
async function getOrCreateAgent(sfClient, session) {
|
|
@@ -303,17 +350,43 @@ async function getOrCreateAgent(sfClient, session) {
|
|
|
303
350
|
supportsImages: true, supportsTools: true,
|
|
304
351
|
};
|
|
305
352
|
}
|
|
306
|
-
const
|
|
353
|
+
const edgesSummary = (ctx.edges || [])
|
|
354
|
+
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
355
|
+
.slice(0, 5)
|
|
356
|
+
.map((e) => ` ${(e.market || '').slice(0, 35)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}¢ → thesis ${e.thesisPrice}¢ | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
357
|
+
.join('\n') || ' (no edges)';
|
|
358
|
+
const nodesSummary = (ctx.causalTree?.nodes || [])
|
|
359
|
+
.filter((n) => n.depth === 0 || !n.depth)
|
|
360
|
+
.slice(0, 5)
|
|
361
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 35)} — ${Math.round((n.probability || 0.5) * 100)}%`)
|
|
362
|
+
.join('\n') || ' (no causal tree)';
|
|
363
|
+
const systemPrompt = `You are a prediction market trading assistant on Telegram. Your job is to help the user see reality clearly and make correct trading decisions.
|
|
307
364
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
365
|
+
## Framework
|
|
366
|
+
Edge = thesis price - market price. Positive = market underprices. Negative = overpriced.
|
|
367
|
+
Contracts with large edge + good liquidity = most tradeable.
|
|
311
368
|
|
|
312
|
-
Rules
|
|
313
|
-
- Keep
|
|
314
|
-
- Prices
|
|
369
|
+
## Rules
|
|
370
|
+
- Keep Telegram messages SHORT — bullet points, no walls of text.
|
|
371
|
+
- Prices in cents (¢). P&L in dollars ($). Don't re-convert tool output units.
|
|
372
|
+
- Call tools for fresh data. Never guess prices or P&L from this prompt.
|
|
373
|
+
- If user mentions news, inject_signal immediately. Don't ask "should I?"
|
|
374
|
+
- If user says "evaluate" or "run it", trigger immediately.
|
|
375
|
+
- Don't end with "anything else?" — user will ask.
|
|
315
376
|
- Use Chinese if user writes Chinese, English if English.
|
|
316
|
-
-
|
|
377
|
+
${config.tradingEnabled ? '- Trading ENABLED. You have place_order and cancel_order. ALWAYS confirm before placing.' : '- Trading DISABLED. Tell user: sf setup --enable-trading'}
|
|
378
|
+
|
|
379
|
+
## Current State
|
|
380
|
+
Thesis: ${(ctx.thesis || ctx.rawThesis || 'N/A').slice(0, 200)}
|
|
381
|
+
ID: ${session.thesisId.slice(0, 8)} | Confidence: ${conf}%
|
|
382
|
+
|
|
383
|
+
Causal nodes:
|
|
384
|
+
${nodesSummary}
|
|
385
|
+
|
|
386
|
+
Top edges:
|
|
387
|
+
${edgesSummary}
|
|
388
|
+
|
|
389
|
+
${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 200)}` : ''}`;
|
|
317
390
|
const agent = new Agent({
|
|
318
391
|
initialState: {
|
|
319
392
|
systemPrompt,
|
package/dist/topics.d.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Shared between dashboard, liquidity scanner, and other commands
|
|
5
5
|
* that need to categorize markets by topic.
|
|
6
|
+
*
|
|
7
|
+
* Sourced from Kalshi's top series by volume (non-sports).
|
|
8
|
+
* Run `sf scan` to discover new series.
|
|
6
9
|
*/
|
|
7
10
|
export declare const TOPIC_SERIES: Record<string, string[]>;
|
|
8
11
|
/** Map a series prefix to a human-readable category name (for dashboard display) */
|
package/dist/topics.js
CHANGED
|
@@ -4,29 +4,87 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Shared between dashboard, liquidity scanner, and other commands
|
|
6
6
|
* that need to categorize markets by topic.
|
|
7
|
+
*
|
|
8
|
+
* Sourced from Kalshi's top series by volume (non-sports).
|
|
9
|
+
* Run `sf scan` to discover new series.
|
|
7
10
|
*/
|
|
8
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
12
|
exports.RISK_CATEGORIES = exports.TOPIC_SERIES = void 0;
|
|
10
13
|
exports.tickerToTopic = tickerToTopic;
|
|
11
14
|
exports.TOPIC_SERIES = {
|
|
12
|
-
oil: ['KXWTIMAX', 'KXWTIW', 'KXWTID'],
|
|
15
|
+
oil: ['KXWTIMAX', 'KXWTIW', 'KXWTID', 'KXWTI'],
|
|
16
|
+
gas: ['KXAAAGASM', 'KXAAAGASW', 'KXCPIGAS'],
|
|
17
|
+
fed: ['KXFEDDECISION', 'KXFED', 'KXRATECUT', 'KXRATECUTCOUNT'],
|
|
18
|
+
cpi: ['KXCPI', 'KXCPIYOY'],
|
|
13
19
|
recession: ['KXRECSSNBER'],
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
sp500: ['KXINXY', 'KXINXU', 'KXINX'],
|
|
21
|
+
nasdaq: ['KXNASDAQ100', 'KXNASDAQ100U', 'KXNASDAQ100Y'],
|
|
22
|
+
crypto: ['KXBTCD', 'KXBTC', 'KXBTC15M', 'KXBTCMAXY', 'KXBTCMINY', 'KXBTCY',
|
|
23
|
+
'KXETHD', 'KXETH', 'KXETH15M', 'KXETHMAXY', 'KXETHMINY',
|
|
24
|
+
'KXSOL15M', 'KXXRP15M'],
|
|
25
|
+
unemployment: ['KXU3', 'KXPAYROLLS'],
|
|
26
|
+
gdp: ['KXGDP'],
|
|
27
|
+
treasury: ['KXTNOTEW', 'KXTNOTED'],
|
|
28
|
+
geopolitics: ['KXCLOSEHORMUZ', 'KXHORMUZTRAFFICW', 'KXHORMUZTRAFFIC', 'KXHORMUZNORM',
|
|
29
|
+
'KXLEADERSOUT', 'KXLEADEROUT', 'KXMADUROOUT', 'KXKHAMENEIOUT'],
|
|
30
|
+
elections: ['PRES', 'KXFEDCHAIRNOM', 'KXPRESNOMD', 'KXPRESNOMR', 'KXPRESPERSON',
|
|
31
|
+
'KXNEXTPOPE', 'KXTRUMPOUT', 'KXCANADAPM'],
|
|
32
|
+
politics: ['KXGOVSHUT', 'KXGOVTSHUTDOWN', 'KXGOVSHUTLENGTH', 'KXGOVTCUTS',
|
|
33
|
+
'KXTRUMPMENTION', 'KXEOWEEK', 'KXGREENLAND', 'KXCANCOALITION'],
|
|
34
|
+
centralbanks: ['KXCBDECISIONJAPAN', 'KXCBDECISIONENGLAND', 'KXCBDECISIONEU',
|
|
35
|
+
'KXCBDECISIONAUSTRALIA', 'KXCBDECISIONCANADA', 'KXCBDECISIONCHINA',
|
|
36
|
+
'KXCBDECISIONMEXICO', 'KXCBDECISIONKOREA'],
|
|
37
|
+
forex: ['KXUSDJPY'],
|
|
38
|
+
tariffs: ['KXTARIFFRATEPRC', 'KXTARIFFRATECAN', 'KXTARIFFRATECA',
|
|
39
|
+
'KXTARIFFRATEINDIA', 'KXTARIFFRATEBR', 'KXTARIFFRATEEU',
|
|
40
|
+
'KXTARIFFRATEJP', 'KXTARIFFRATEKR'],
|
|
41
|
+
tech: ['KXLLM1', 'KXTOPMODEL', 'KXALIENS'],
|
|
18
42
|
};
|
|
19
43
|
/** Map a series prefix to a human-readable category name (for dashboard display) */
|
|
20
44
|
exports.RISK_CATEGORIES = {
|
|
21
45
|
KXWTIMAX: 'Oil',
|
|
46
|
+
KXWTIW: 'Oil',
|
|
47
|
+
KXWTID: 'Oil',
|
|
22
48
|
KXWTI: 'Oil',
|
|
23
|
-
KXRECSSNBER: 'Recession',
|
|
24
49
|
KXAAAGASM: 'Gas',
|
|
50
|
+
KXAAAGASW: 'Gas',
|
|
51
|
+
KXCPIGAS: 'Gas',
|
|
52
|
+
KXRECSSNBER: 'Recession',
|
|
25
53
|
KXCPI: 'Inflation',
|
|
54
|
+
KXCPIYOY: 'Inflation',
|
|
26
55
|
KXINXY: 'S&P 500',
|
|
56
|
+
KXINXU: 'S&P 500',
|
|
57
|
+
KXINX: 'S&P 500',
|
|
58
|
+
KXNASDAQ100: 'Nasdaq',
|
|
59
|
+
KXNASDAQ100U: 'Nasdaq',
|
|
60
|
+
KXNASDAQ100Y: 'Nasdaq',
|
|
27
61
|
KXFEDDECISION: 'Fed Rate',
|
|
28
|
-
|
|
62
|
+
KXFED: 'Fed Rate',
|
|
63
|
+
KXRATECUT: 'Fed Rate',
|
|
64
|
+
KXRATECUTCOUNT: 'Fed Rate',
|
|
65
|
+
KXBTCD: 'Bitcoin',
|
|
66
|
+
KXBTC: 'Bitcoin',
|
|
67
|
+
KXBTC15M: 'Bitcoin',
|
|
68
|
+
KXETHD: 'Ethereum',
|
|
69
|
+
KXETH: 'Ethereum',
|
|
70
|
+
KXETH15M: 'Ethereum',
|
|
71
|
+
KXU3: 'Unemployment',
|
|
72
|
+
KXPAYROLLS: 'Jobs',
|
|
73
|
+
KXGDP: 'GDP',
|
|
74
|
+
KXTNOTEW: 'Treasury',
|
|
75
|
+
KXTNOTED: 'Treasury',
|
|
29
76
|
KXCLOSEHORMUZ: 'Hormuz',
|
|
77
|
+
KXHORMUZTRAFFICW: 'Hormuz',
|
|
78
|
+
KXHORMUZTRAFFIC: 'Hormuz',
|
|
79
|
+
KXUSDJPY: 'USD/JPY',
|
|
80
|
+
KXGOVSHUT: 'Govt Shutdown',
|
|
81
|
+
KXGOVTSHUTDOWN: 'Govt Shutdown',
|
|
82
|
+
PRES: 'Elections',
|
|
83
|
+
KXFEDCHAIRNOM: 'Elections',
|
|
84
|
+
KXTARIFFRATEPRC: 'Tariffs',
|
|
85
|
+
KXCBDECISIONJAPAN: 'Central Banks',
|
|
86
|
+
KXCBDECISIONENGLAND: 'Central Banks',
|
|
87
|
+
KXCBDECISIONEU: 'Central Banks',
|
|
30
88
|
};
|
|
31
89
|
/**
|
|
32
90
|
* Given a ticker string, return the topic name (uppercased).
|