create-polymarket-strategy 0.2.0 → 0.2.2
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/dist/index.js +317 -28
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/worker/package.json +1 -1
- package/templates/worker/src/durable-objects/positions.ts +189 -2
- package/templates/worker/src/durable-objects/scheduler.ts +108 -0
- package/templates/worker/src/lib/quote-manager.ts +358 -0
- package/templates/worker/src/scanner.ts +55 -3
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quote Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages two-sided quotes (bid + ask pairs) for market making.
|
|
5
|
+
* Handles placing, updating, and cancelling quote pairs atomically.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Env } from "../types.js";
|
|
9
|
+
import {
|
|
10
|
+
type OrderManager,
|
|
11
|
+
type QuotePair,
|
|
12
|
+
type Order,
|
|
13
|
+
createOrderManagerFromEnv,
|
|
14
|
+
} from "polymarket-trading-sdk";
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Result of placing a quote.
|
|
22
|
+
*/
|
|
23
|
+
export interface QuotePlacementResult {
|
|
24
|
+
success: boolean;
|
|
25
|
+
tokenId: string;
|
|
26
|
+
bidOrderId?: string;
|
|
27
|
+
askOrderId?: string;
|
|
28
|
+
bidError?: string;
|
|
29
|
+
askError?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* An active quote tracked by the manager.
|
|
34
|
+
*/
|
|
35
|
+
export interface ActiveQuote {
|
|
36
|
+
tokenId: string;
|
|
37
|
+
bidOrderId: string;
|
|
38
|
+
askOrderId: string;
|
|
39
|
+
bidPrice: number;
|
|
40
|
+
askPrice: number;
|
|
41
|
+
bidSize: number;
|
|
42
|
+
askSize: number;
|
|
43
|
+
placedAt: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for quote placement.
|
|
48
|
+
*/
|
|
49
|
+
export interface QuotePlacementOptions {
|
|
50
|
+
/** Slug for order tracking */
|
|
51
|
+
slug?: string;
|
|
52
|
+
/** Label for order tracking */
|
|
53
|
+
label?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// Quote Manager
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Manages two-sided quotes for a trading strategy.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const manager = new QuoteManager(env);
|
|
66
|
+
*
|
|
67
|
+
* // Place a quote
|
|
68
|
+
* const result = await manager.placeQuote({
|
|
69
|
+
* tokenId: "abc123",
|
|
70
|
+
* bidPrice: 0.48,
|
|
71
|
+
* bidSize: 10,
|
|
72
|
+
* askPrice: 0.52,
|
|
73
|
+
* askSize: 10,
|
|
74
|
+
* });
|
|
75
|
+
*
|
|
76
|
+
* // Update quote
|
|
77
|
+
* await manager.updateQuote("abc123", {
|
|
78
|
+
* tokenId: "abc123",
|
|
79
|
+
* bidPrice: 0.47,
|
|
80
|
+
* bidSize: 10,
|
|
81
|
+
* askPrice: 0.53,
|
|
82
|
+
* askSize: 10,
|
|
83
|
+
* });
|
|
84
|
+
*
|
|
85
|
+
* // Cancel quote
|
|
86
|
+
* await manager.cancelQuote("abc123");
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export class QuoteManager {
|
|
90
|
+
private orderManager: OrderManager;
|
|
91
|
+
private ordersStub: DurableObjectStub;
|
|
92
|
+
private activeQuotes: Map<string, ActiveQuote> = new Map();
|
|
93
|
+
|
|
94
|
+
constructor(private env: Env) {
|
|
95
|
+
this.orderManager = createOrderManagerFromEnv(env);
|
|
96
|
+
|
|
97
|
+
const ordersId = env.ORDERS.idFromName("main");
|
|
98
|
+
this.ordersStub = env.ORDERS.get(ordersId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Place a two-sided quote (bid and ask).
|
|
103
|
+
*
|
|
104
|
+
* Both orders are placed in parallel. If one fails, the other may still succeed.
|
|
105
|
+
* Check the result for individual order status.
|
|
106
|
+
*/
|
|
107
|
+
async placeQuote(
|
|
108
|
+
quote: QuotePair,
|
|
109
|
+
options: QuotePlacementOptions = {}
|
|
110
|
+
): Promise<QuotePlacementResult> {
|
|
111
|
+
const { slug = "quote", label = quote.tokenId.slice(-8) } = options;
|
|
112
|
+
|
|
113
|
+
const result: QuotePlacementResult = {
|
|
114
|
+
success: false,
|
|
115
|
+
tokenId: quote.tokenId,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Place bid and ask in parallel
|
|
119
|
+
const [bidResult, askResult] = await Promise.all([
|
|
120
|
+
this.orderManager.placeOrder({
|
|
121
|
+
slug: `${slug}-bid`,
|
|
122
|
+
label,
|
|
123
|
+
tokenId: quote.tokenId,
|
|
124
|
+
side: "buy",
|
|
125
|
+
price: quote.bidPrice,
|
|
126
|
+
size: quote.bidSize,
|
|
127
|
+
}),
|
|
128
|
+
this.orderManager.placeOrder({
|
|
129
|
+
slug: `${slug}-ask`,
|
|
130
|
+
label,
|
|
131
|
+
tokenId: quote.tokenId,
|
|
132
|
+
side: "sell",
|
|
133
|
+
price: quote.askPrice,
|
|
134
|
+
size: quote.askSize,
|
|
135
|
+
}),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
// Record results
|
|
139
|
+
if (bidResult.success) {
|
|
140
|
+
result.bidOrderId = bidResult.clobOrderId;
|
|
141
|
+
|
|
142
|
+
// Record in DO
|
|
143
|
+
await this.recordOrder({
|
|
144
|
+
slug: `${slug}-bid`,
|
|
145
|
+
label,
|
|
146
|
+
tokenId: quote.tokenId,
|
|
147
|
+
side: "buy",
|
|
148
|
+
price: quote.bidPrice,
|
|
149
|
+
size: quote.bidSize,
|
|
150
|
+
clobOrderId: bidResult.clobOrderId,
|
|
151
|
+
});
|
|
152
|
+
} else {
|
|
153
|
+
result.bidError = bidResult.error;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (askResult.success) {
|
|
157
|
+
result.askOrderId = askResult.clobOrderId;
|
|
158
|
+
|
|
159
|
+
// Record in DO
|
|
160
|
+
await this.recordOrder({
|
|
161
|
+
slug: `${slug}-ask`,
|
|
162
|
+
label,
|
|
163
|
+
tokenId: quote.tokenId,
|
|
164
|
+
side: "sell",
|
|
165
|
+
price: quote.askPrice,
|
|
166
|
+
size: quote.askSize,
|
|
167
|
+
clobOrderId: askResult.clobOrderId,
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
result.askError = askResult.error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Track active quote if at least one side succeeded
|
|
174
|
+
if (result.bidOrderId || result.askOrderId) {
|
|
175
|
+
this.activeQuotes.set(quote.tokenId, {
|
|
176
|
+
tokenId: quote.tokenId,
|
|
177
|
+
bidOrderId: result.bidOrderId || "",
|
|
178
|
+
askOrderId: result.askOrderId || "",
|
|
179
|
+
bidPrice: quote.bidPrice,
|
|
180
|
+
askPrice: quote.askPrice,
|
|
181
|
+
bidSize: quote.bidSize,
|
|
182
|
+
askSize: quote.askSize,
|
|
183
|
+
placedAt: Date.now(),
|
|
184
|
+
});
|
|
185
|
+
result.success = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Cancel all orders for a token.
|
|
193
|
+
*/
|
|
194
|
+
async cancelQuote(tokenId: string): Promise<{ success: boolean; error?: string }> {
|
|
195
|
+
try {
|
|
196
|
+
// Cancel on CLOB
|
|
197
|
+
const cancelResult = await this.orderManager.cancelAllOrders(tokenId);
|
|
198
|
+
|
|
199
|
+
if (!cancelResult.success) {
|
|
200
|
+
return { success: false, error: cancelResult.error };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Remove from tracking
|
|
204
|
+
this.activeQuotes.delete(tokenId);
|
|
205
|
+
|
|
206
|
+
// Note: Orders will be marked as cancelled by reconciliation
|
|
207
|
+
// or we could explicitly mark them here
|
|
208
|
+
|
|
209
|
+
return { success: true };
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return {
|
|
212
|
+
success: false,
|
|
213
|
+
error: e instanceof Error ? e.message : String(e),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Update a quote by cancelling the old one and placing a new one.
|
|
220
|
+
*
|
|
221
|
+
* This is a cancel-and-replace operation. The new quote is placed
|
|
222
|
+
* after the old one is cancelled.
|
|
223
|
+
*/
|
|
224
|
+
async updateQuote(
|
|
225
|
+
tokenId: string,
|
|
226
|
+
newQuote: QuotePair,
|
|
227
|
+
options: QuotePlacementOptions = {}
|
|
228
|
+
): Promise<QuotePlacementResult> {
|
|
229
|
+
// First cancel existing quote
|
|
230
|
+
await this.cancelQuote(tokenId);
|
|
231
|
+
|
|
232
|
+
// Small delay to ensure cancellation propagates
|
|
233
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
234
|
+
|
|
235
|
+
// Place new quote
|
|
236
|
+
return this.placeQuote(newQuote, options);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get all active quotes.
|
|
241
|
+
*/
|
|
242
|
+
getActiveQuotes(): Map<string, ActiveQuote> {
|
|
243
|
+
return new Map(this.activeQuotes);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Get active quote for a specific token.
|
|
248
|
+
*/
|
|
249
|
+
getQuote(tokenId: string): ActiveQuote | undefined {
|
|
250
|
+
return this.activeQuotes.get(tokenId);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if there's an active quote for a token.
|
|
255
|
+
*/
|
|
256
|
+
hasQuote(tokenId: string): boolean {
|
|
257
|
+
return this.activeQuotes.has(tokenId);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Cancel all active quotes.
|
|
262
|
+
*/
|
|
263
|
+
async cancelAllQuotes(): Promise<{ cancelled: number; errors: string[] }> {
|
|
264
|
+
const tokenIds = [...this.activeQuotes.keys()];
|
|
265
|
+
let cancelled = 0;
|
|
266
|
+
const errors: string[] = [];
|
|
267
|
+
|
|
268
|
+
for (const tokenId of tokenIds) {
|
|
269
|
+
const result = await this.cancelQuote(tokenId);
|
|
270
|
+
if (result.success) {
|
|
271
|
+
cancelled++;
|
|
272
|
+
} else {
|
|
273
|
+
errors.push(`${tokenId}: ${result.error}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { cancelled, errors };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Refresh active quotes from the orders DO.
|
|
282
|
+
* Call this when starting up to restore state.
|
|
283
|
+
*/
|
|
284
|
+
async refreshFromDO(): Promise<void> {
|
|
285
|
+
try {
|
|
286
|
+
const response = await this.ordersStub.fetch(
|
|
287
|
+
new Request("http://do/list-pending")
|
|
288
|
+
);
|
|
289
|
+
const pendingOrders = (await response.json()) as Order[];
|
|
290
|
+
|
|
291
|
+
// Group by token
|
|
292
|
+
const byToken = new Map<string, { bids: Order[]; asks: Order[] }>();
|
|
293
|
+
|
|
294
|
+
for (const order of pendingOrders) {
|
|
295
|
+
if (!byToken.has(order.tokenId)) {
|
|
296
|
+
byToken.set(order.tokenId, { bids: [], asks: [] });
|
|
297
|
+
}
|
|
298
|
+
const entry = byToken.get(order.tokenId)!;
|
|
299
|
+
if (order.side === "buy") {
|
|
300
|
+
entry.bids.push(order);
|
|
301
|
+
} else {
|
|
302
|
+
entry.asks.push(order);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Reconstruct active quotes
|
|
307
|
+
this.activeQuotes.clear();
|
|
308
|
+
for (const [tokenId, { bids, asks }] of byToken) {
|
|
309
|
+
// Take the most recent bid and ask
|
|
310
|
+
const bid = bids[0];
|
|
311
|
+
const ask = asks[0];
|
|
312
|
+
|
|
313
|
+
if (bid && ask) {
|
|
314
|
+
this.activeQuotes.set(tokenId, {
|
|
315
|
+
tokenId,
|
|
316
|
+
bidOrderId: bid.clobOrderId || bid.id,
|
|
317
|
+
askOrderId: ask.clobOrderId || ask.id,
|
|
318
|
+
bidPrice: bid.price,
|
|
319
|
+
askPrice: ask.price,
|
|
320
|
+
bidSize: bid.size,
|
|
321
|
+
askSize: ask.size,
|
|
322
|
+
placedAt: new Date(bid.createdAt).getTime(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
} catch (e) {
|
|
327
|
+
console.error("Failed to refresh quotes from DO:", e);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Record an order in the orders DO.
|
|
333
|
+
*/
|
|
334
|
+
private async recordOrder(order: {
|
|
335
|
+
slug: string;
|
|
336
|
+
label: string;
|
|
337
|
+
tokenId: string;
|
|
338
|
+
side: "buy" | "sell";
|
|
339
|
+
price: number;
|
|
340
|
+
size: number;
|
|
341
|
+
clobOrderId?: string;
|
|
342
|
+
}): Promise<void> {
|
|
343
|
+
await this.ordersStub.fetch(
|
|
344
|
+
new Request("http://do/create", {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: { "Content-Type": "application/json" },
|
|
347
|
+
body: JSON.stringify(order),
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Create a QuoteManager from environment.
|
|
355
|
+
*/
|
|
356
|
+
export function createQuoteManager(env: Env): QuoteManager {
|
|
357
|
+
return new QuoteManager(env);
|
|
358
|
+
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* Orchestrates the scanning process:
|
|
5
5
|
* 1. Calls strategy.scan() to find signals
|
|
6
6
|
* 2. Checks for duplicate pending orders
|
|
7
|
-
* 3.
|
|
8
|
-
* 4.
|
|
7
|
+
* 3. Checks risk limits before placing
|
|
8
|
+
* 4. Calculates prices using strategy.calculatePrice()
|
|
9
|
+
* 5. Places orders via OrderManager (routes to Lambda for live mode)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import type { Env } from "./types.js";
|
|
@@ -14,13 +15,17 @@ import {
|
|
|
14
15
|
type StrategyContext,
|
|
15
16
|
type Order,
|
|
16
17
|
type NewOrder,
|
|
18
|
+
type TokenInventory,
|
|
19
|
+
type RiskLimits,
|
|
17
20
|
MarketDataClient,
|
|
18
21
|
createOrderManagerFromEnv,
|
|
22
|
+
checkOrderRisk,
|
|
19
23
|
} from "polymarket-trading-sdk";
|
|
20
24
|
|
|
21
25
|
export interface ScanResult {
|
|
22
26
|
signalsFound: number;
|
|
23
27
|
ordersPlaced: number;
|
|
28
|
+
ordersSkippedRisk: number;
|
|
24
29
|
errors: string[];
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -34,6 +39,7 @@ export async function runScanner(
|
|
|
34
39
|
const result: ScanResult = {
|
|
35
40
|
signalsFound: 0,
|
|
36
41
|
ordersPlaced: 0,
|
|
42
|
+
ordersSkippedRisk: 0,
|
|
37
43
|
errors: [],
|
|
38
44
|
};
|
|
39
45
|
|
|
@@ -60,6 +66,9 @@ export async function runScanner(
|
|
|
60
66
|
const ordersId = env.ORDERS.idFromName("main");
|
|
61
67
|
const ordersStub = env.ORDERS.get(ordersId);
|
|
62
68
|
|
|
69
|
+
const positionsId = env.POSITIONS.idFromName("main");
|
|
70
|
+
const positionsStub = env.POSITIONS.get(positionsId);
|
|
71
|
+
|
|
63
72
|
try {
|
|
64
73
|
// 1. Scan for signals
|
|
65
74
|
const signals = await strategy.scan(ctx);
|
|
@@ -75,7 +84,24 @@ export async function runScanner(
|
|
|
75
84
|
);
|
|
76
85
|
const pendingOrders = (await pendingOrdersResponse.json()) as Order[];
|
|
77
86
|
|
|
78
|
-
// 3.
|
|
87
|
+
// 3. Get current inventories for risk checking
|
|
88
|
+
const inventoryResponse = await positionsStub.fetch(
|
|
89
|
+
new Request("http://do/inventory/all")
|
|
90
|
+
);
|
|
91
|
+
const inventories = (await inventoryResponse.json()) as TokenInventory[];
|
|
92
|
+
|
|
93
|
+
// 4. Get risk limits from config (with defaults)
|
|
94
|
+
const riskLimits: RiskLimits = {
|
|
95
|
+
maxPositionPerToken: (config.maxPositionPerToken as number) ?? 100,
|
|
96
|
+
maxTotalExposure: (config.maxTotalExposure as number) ?? 500,
|
|
97
|
+
maxLossPerToken: config.maxLossPerToken as number | undefined,
|
|
98
|
+
maxDailyLoss: config.maxDailyLoss as number | undefined,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Track current prices for risk calculations
|
|
102
|
+
const currentPrices = new Map<string, number>();
|
|
103
|
+
|
|
104
|
+
// 5. Process each signal
|
|
79
105
|
for (const signal of signals) {
|
|
80
106
|
try {
|
|
81
107
|
// Check dedup
|
|
@@ -95,6 +121,11 @@ export async function runScanner(
|
|
|
95
121
|
// Get market data for pricing
|
|
96
122
|
const data = await marketData.getMarketData(signal.tokenId);
|
|
97
123
|
|
|
124
|
+
// Track price for risk calc
|
|
125
|
+
if (data.midpoint !== null) {
|
|
126
|
+
currentPrices.set(signal.tokenId, data.midpoint);
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
// Calculate price
|
|
99
130
|
const price = strategy.calculatePrice(signal, data, config);
|
|
100
131
|
|
|
@@ -105,6 +136,27 @@ export async function runScanner(
|
|
|
105
136
|
// Determine order size
|
|
106
137
|
const size = (config.orderSize as number) ?? 5;
|
|
107
138
|
|
|
139
|
+
// Check risk limits before placing
|
|
140
|
+
const riskCheck = checkOrderRisk(
|
|
141
|
+
inventories,
|
|
142
|
+
{
|
|
143
|
+
tokenId: signal.tokenId,
|
|
144
|
+
side: signal.side,
|
|
145
|
+
size,
|
|
146
|
+
price,
|
|
147
|
+
},
|
|
148
|
+
riskLimits,
|
|
149
|
+
currentPrices
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (!riskCheck.allowed) {
|
|
153
|
+
result.ordersSkippedRisk++;
|
|
154
|
+
result.errors.push(
|
|
155
|
+
`Risk limit blocked ${signal.label}: ${riskCheck.reason}`
|
|
156
|
+
);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
108
160
|
// Place order via OrderManager (handles live vs paper routing)
|
|
109
161
|
const orderResult = await orderManager.placeOrder({
|
|
110
162
|
slug: signal.slug,
|