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.
@@ -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. Calculates prices using strategy.calculatePrice()
8
- * 4. Places orders via OrderManager (routes to Lambda for live mode)
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. Process each signal
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,