create-polymarket-strategy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Scheduler Durable Object
3
+ *
4
+ * Uses DO alarms for granular, self-contained scheduling.
5
+ * Manages periodic scanning with configurable intervals.
6
+ */
7
+
8
+ import type { Env } from "../types.js";
9
+
10
+ interface SchedulerConfig {
11
+ interval: number; // milliseconds
12
+ enabled: boolean;
13
+ lastRun: string | null;
14
+ runCount: number;
15
+ }
16
+
17
+ export class SchedulerDO implements DurableObject {
18
+ private sql: SqlStorage;
19
+
20
+ constructor(
21
+ private state: DurableObjectState,
22
+ private env: Env
23
+ ) {
24
+ this.sql = state.storage.sql;
25
+ this.initSchema();
26
+ }
27
+
28
+ private initSchema(): void {
29
+ this.sql.exec(`
30
+ CREATE TABLE IF NOT EXISTS config (
31
+ key TEXT PRIMARY KEY,
32
+ value TEXT
33
+ );
34
+
35
+ CREATE TABLE IF NOT EXISTS runs (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ started_at TEXT NOT NULL,
38
+ completed_at TEXT,
39
+ status TEXT NOT NULL DEFAULT 'running',
40
+ signals_found INTEGER DEFAULT 0,
41
+ orders_placed INTEGER DEFAULT 0,
42
+ error TEXT
43
+ );
44
+ `);
45
+
46
+ const result = this.sql.exec(
47
+ `SELECT value FROM config WHERE key = 'interval'`
48
+ );
49
+ if (result.toArray().length === 0) {
50
+ this.sql.exec(
51
+ `INSERT INTO config (key, value) VALUES ('interval', '3600000')`
52
+ );
53
+ this.sql.exec(`INSERT INTO config (key, value) VALUES ('enabled', '0')`);
54
+ this.sql.exec(
55
+ `INSERT INTO config (key, value) VALUES ('run_count', '0')`
56
+ );
57
+ }
58
+ }
59
+
60
+ async fetch(request: Request): Promise<Response> {
61
+ const url = new URL(request.url);
62
+
63
+ try {
64
+ switch (url.pathname) {
65
+ case "/start":
66
+ return this.handleStart(url);
67
+ case "/stop":
68
+ return this.handleStop();
69
+ case "/trigger":
70
+ return this.handleTrigger();
71
+ case "/status":
72
+ return this.handleStatus();
73
+ case "/history":
74
+ return this.handleHistory();
75
+ case "/config":
76
+ return this.handleConfig(request, url);
77
+ default:
78
+ return new Response("Not found", { status: 404 });
79
+ }
80
+ } catch (error) {
81
+ const message = error instanceof Error ? error.message : "Unknown error";
82
+ return Response.json({ error: message }, { status: 500 });
83
+ }
84
+ }
85
+
86
+ async alarm(): Promise<void> {
87
+ const config = this.getConfig();
88
+
89
+ if (!config.enabled) {
90
+ return;
91
+ }
92
+
93
+ await this.runScan();
94
+ await this.scheduleNext(config.interval);
95
+ }
96
+
97
+ private async handleStart(url: URL): Promise<Response> {
98
+ const intervalParam = url.searchParams.get("interval");
99
+ const interval = intervalParam ? parseInt(intervalParam) : 3600000;
100
+
101
+ if (interval < 10000) {
102
+ return Response.json(
103
+ { error: "Interval must be at least 10000ms (10 seconds)" },
104
+ { status: 400 }
105
+ );
106
+ }
107
+
108
+ this.sql.exec(
109
+ `UPDATE config SET value = ? WHERE key = 'interval'`,
110
+ interval.toString()
111
+ );
112
+ this.sql.exec(`UPDATE config SET value = '1' WHERE key = 'enabled'`);
113
+
114
+ await this.scheduleNext(interval);
115
+
116
+ return Response.json({
117
+ status: "started",
118
+ interval,
119
+ nextRun: await this.state.storage.getAlarm(),
120
+ });
121
+ }
122
+
123
+ private async handleStop(): Promise<Response> {
124
+ this.sql.exec(`UPDATE config SET value = '0' WHERE key = 'enabled'`);
125
+ await this.state.storage.deleteAlarm();
126
+
127
+ return Response.json({ status: "stopped" });
128
+ }
129
+
130
+ private async handleTrigger(): Promise<Response> {
131
+ const result = await this.runScan();
132
+
133
+ return Response.json({
134
+ status: "triggered",
135
+ ...result,
136
+ });
137
+ }
138
+
139
+ private async handleStatus(): Promise<Response> {
140
+ const config = this.getConfig();
141
+ const nextAlarm = await this.state.storage.getAlarm();
142
+
143
+ const lastRunResult = this.sql.exec(
144
+ `SELECT * FROM runs ORDER BY id DESC LIMIT 1`
145
+ );
146
+ const lastRuns = lastRunResult.toArray();
147
+ const lastRun = lastRuns.length > 0 ? lastRuns[0] : null;
148
+
149
+ return Response.json({
150
+ enabled: config.enabled,
151
+ interval: config.interval,
152
+ nextRun: nextAlarm,
153
+ runCount: config.runCount,
154
+ lastRun: lastRun
155
+ ? {
156
+ startedAt: lastRun.started_at,
157
+ completedAt: lastRun.completed_at,
158
+ status: lastRun.status,
159
+ signalsFound: lastRun.signals_found,
160
+ ordersPlaced: lastRun.orders_placed,
161
+ error: lastRun.error,
162
+ }
163
+ : null,
164
+ });
165
+ }
166
+
167
+ private handleHistory(): Response {
168
+ const result = this.sql.exec(
169
+ `SELECT * FROM runs ORDER BY id DESC LIMIT 20`
170
+ );
171
+
172
+ const runs = result.toArray().map((row) => ({
173
+ id: row.id,
174
+ startedAt: row.started_at,
175
+ completedAt: row.completed_at,
176
+ status: row.status,
177
+ signalsFound: row.signals_found,
178
+ ordersPlaced: row.orders_placed,
179
+ error: row.error,
180
+ }));
181
+
182
+ return Response.json(runs);
183
+ }
184
+
185
+ private async handleConfig(request: Request, url: URL): Promise<Response> {
186
+ if (request.method === "GET") {
187
+ return Response.json(this.getConfig());
188
+ }
189
+
190
+ if (request.method === "PUT") {
191
+ const body = (await request.json()) as { interval?: number };
192
+
193
+ if (body.interval) {
194
+ if (body.interval < 10000) {
195
+ return Response.json(
196
+ { error: "Interval must be at least 10000ms" },
197
+ { status: 400 }
198
+ );
199
+ }
200
+ this.sql.exec(
201
+ `UPDATE config SET value = ? WHERE key = 'interval'`,
202
+ body.interval.toString()
203
+ );
204
+ }
205
+
206
+ return Response.json(this.getConfig());
207
+ }
208
+
209
+ return new Response("Method not allowed", { status: 405 });
210
+ }
211
+
212
+ private getConfig(): SchedulerConfig {
213
+ const result = this.sql.exec(`SELECT key, value FROM config`);
214
+ const rows = result.toArray();
215
+
216
+ const config: Record<string, string> = {};
217
+ for (const row of rows) {
218
+ config[row.key as string] = row.value as string;
219
+ }
220
+
221
+ return {
222
+ interval: parseInt(config.interval || "3600000"),
223
+ enabled: config.enabled === "1",
224
+ lastRun: config.last_run || null,
225
+ runCount: parseInt(config.run_count || "0"),
226
+ };
227
+ }
228
+
229
+ private async scheduleNext(intervalMs: number): Promise<void> {
230
+ const nextTime = Date.now() + intervalMs;
231
+ await this.state.storage.setAlarm(nextTime);
232
+ }
233
+
234
+ private async runScan(): Promise<{
235
+ signalsFound: number;
236
+ ordersPlaced: number;
237
+ error?: string;
238
+ }> {
239
+ const startedAt = new Date().toISOString();
240
+
241
+ this.sql.exec(
242
+ `INSERT INTO runs (started_at, status) VALUES (?, 'running')`,
243
+ startedAt
244
+ );
245
+
246
+ const runIdResult = this.sql.exec(`SELECT last_insert_rowid() as id`);
247
+ const runId = (runIdResult.toArray()[0]?.id as number) || 0;
248
+
249
+ try {
250
+ // TODO: Implement actual scanning logic via worker callback
251
+ const completedAt = new Date().toISOString();
252
+ const signalsFound = 0;
253
+ const ordersPlaced = 0;
254
+
255
+ this.sql.exec(
256
+ `UPDATE runs SET
257
+ completed_at = ?,
258
+ status = 'completed',
259
+ signals_found = ?,
260
+ orders_placed = ?
261
+ WHERE id = ?`,
262
+ completedAt,
263
+ signalsFound,
264
+ ordersPlaced,
265
+ runId
266
+ );
267
+
268
+ this.sql.exec(
269
+ `UPDATE config SET value = CAST(CAST(value AS INTEGER) + 1 AS TEXT)
270
+ WHERE key = 'run_count'`
271
+ );
272
+
273
+ return { signalsFound, ordersPlaced };
274
+ } catch (error) {
275
+ const errorMsg = error instanceof Error ? error.message : "Unknown error";
276
+
277
+ this.sql.exec(
278
+ `UPDATE runs SET
279
+ completed_at = ?,
280
+ status = 'error',
281
+ error = ?
282
+ WHERE id = ?`,
283
+ new Date().toISOString(),
284
+ errorMsg,
285
+ runId
286
+ );
287
+
288
+ return { signalsFound: 0, ordersPlaced: 0, error: errorMsg };
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,221 @@
1
+ /**
2
+ * {{name}} - Polymarket Trading Bot
3
+ *
4
+ * A Cloudflare Worker that runs the {{strategy-name}} trading strategy.
5
+ */
6
+
7
+ import type { Env } from "./types.js";
8
+ import {
9
+ MarketDataClient,
10
+ createOrderManagerFromEnv,
11
+ checkAuth,
12
+ unauthorizedResponse,
13
+ getStrategy,
14
+ listStrategies,
15
+ } from "polymarket-trading-sdk";
16
+ import { runScanner } from "./scanner.js";
17
+ import { OrdersDO } from "./durable-objects/orders.js";
18
+ import { PositionsDO } from "./durable-objects/positions.js";
19
+ import { SchedulerDO } from "./durable-objects/scheduler.js";
20
+
21
+ // Import strategies (self-register)
22
+ import "./strategies/{{strategy-name}}.js";
23
+
24
+ // Export Durable Objects
25
+ export { OrdersDO, PositionsDO, SchedulerDO };
26
+
27
+ // CORS headers
28
+ const corsHeaders = {
29
+ "Access-Control-Allow-Origin": "*",
30
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
31
+ "Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
32
+ };
33
+
34
+ export default {
35
+ async fetch(
36
+ request: Request,
37
+ env: Env,
38
+ ctx: ExecutionContext
39
+ ): Promise<Response> {
40
+ // Handle CORS preflight
41
+ if (request.method === "OPTIONS") {
42
+ return new Response(null, { headers: corsHeaders });
43
+ }
44
+
45
+ // Auth check
46
+ const auth = checkAuth(request, env);
47
+ if (!auth.authorized) {
48
+ return unauthorizedResponse(auth.error || "Unauthorized");
49
+ }
50
+
51
+ const url = new URL(request.url);
52
+
53
+ try {
54
+ // Route handling
55
+ switch (url.pathname) {
56
+ case "/":
57
+ return Response.json(
58
+ {
59
+ name: "{{name}}",
60
+ strategy: env.STRATEGY_NAME,
61
+ mode: env.MODE,
62
+ endpoints: [
63
+ "GET /",
64
+ "GET /health",
65
+ "POST /scan",
66
+ "GET /orders",
67
+ "GET /positions",
68
+ "GET /balance",
69
+ "GET /stats",
70
+ "GET /strategies",
71
+ "GET /scheduler/status",
72
+ "POST /scheduler/start?interval=60000",
73
+ "POST /scheduler/stop",
74
+ "POST /scheduler/trigger",
75
+ "GET /scheduler/history",
76
+ ],
77
+ },
78
+ { headers: corsHeaders }
79
+ );
80
+
81
+ case "/health":
82
+ return Response.json({ status: "ok" }, { headers: corsHeaders });
83
+
84
+ case "/scan":
85
+ return handleScan(request, env);
86
+
87
+ case "/orders":
88
+ return handleOrders(request, env);
89
+
90
+ case "/positions":
91
+ return handlePositions(request, env);
92
+
93
+ case "/balance":
94
+ return handleBalance(env);
95
+
96
+ case "/stats":
97
+ return handleStats(env);
98
+
99
+ case "/strategies":
100
+ return Response.json(
101
+ { strategies: listStrategies() },
102
+ { headers: corsHeaders }
103
+ );
104
+
105
+ default:
106
+ // Scheduler routes
107
+ if (url.pathname.startsWith("/scheduler")) {
108
+ return handleScheduler(request, env, url);
109
+ }
110
+
111
+ return Response.json(
112
+ { error: "Not found" },
113
+ { status: 404, headers: corsHeaders }
114
+ );
115
+ }
116
+ } catch (error) {
117
+ const message = error instanceof Error ? error.message : "Internal error";
118
+ return Response.json(
119
+ { error: message },
120
+ { status: 500, headers: corsHeaders }
121
+ );
122
+ }
123
+ },
124
+ };
125
+
126
+ // =============================================================================
127
+ // Route Handlers
128
+ // =============================================================================
129
+
130
+ async function handleScan(request: Request, env: Env): Promise<Response> {
131
+ if (request.method !== "POST") {
132
+ return Response.json(
133
+ { error: "Method not allowed" },
134
+ { status: 405, headers: corsHeaders }
135
+ );
136
+ }
137
+
138
+ const strategy = getStrategy<Env>(env.STRATEGY_NAME);
139
+ const result = await runScanner(strategy, env);
140
+
141
+ return Response.json(result, { headers: corsHeaders });
142
+ }
143
+
144
+ async function handleOrders(request: Request, env: Env): Promise<Response> {
145
+ const url = new URL(request.url);
146
+ const ordersId = env.ORDERS.idFromName("main");
147
+ const ordersStub = env.ORDERS.get(ordersId);
148
+
149
+ const status = url.searchParams.get("status");
150
+ const path = status === "pending" ? "/list-pending" : "/list-all";
151
+
152
+ const response = await ordersStub.fetch(new Request(`http://do${path}`));
153
+ const data = await response.json();
154
+
155
+ return Response.json(data, { headers: corsHeaders });
156
+ }
157
+
158
+ async function handlePositions(request: Request, env: Env): Promise<Response> {
159
+ const url = new URL(request.url);
160
+ const positionsId = env.POSITIONS.idFromName("main");
161
+ const positionsStub = env.POSITIONS.get(positionsId);
162
+
163
+ const status = url.searchParams.get("status");
164
+ const path = status === "open" ? "/list-open" : "/list-all";
165
+
166
+ const response = await positionsStub.fetch(new Request(`http://do${path}`));
167
+ const data = await response.json();
168
+
169
+ return Response.json(data, { headers: corsHeaders });
170
+ }
171
+
172
+ async function handleBalance(env: Env): Promise<Response> {
173
+ const orderManager = createOrderManagerFromEnv(env);
174
+ const balance = await orderManager.getBalance();
175
+
176
+ return Response.json(
177
+ {
178
+ balance,
179
+ mode: env.MODE,
180
+ },
181
+ { headers: corsHeaders }
182
+ );
183
+ }
184
+
185
+ async function handleStats(env: Env): Promise<Response> {
186
+ const positionsId = env.POSITIONS.idFromName("main");
187
+ const positionsStub = env.POSITIONS.get(positionsId);
188
+
189
+ const response = await positionsStub.fetch(new Request("http://do/stats"));
190
+ const stats = (await response.json()) as Record<string, unknown>;
191
+
192
+ return Response.json(
193
+ {
194
+ strategy: env.STRATEGY_NAME,
195
+ mode: env.MODE,
196
+ ...stats,
197
+ },
198
+ { headers: corsHeaders }
199
+ );
200
+ }
201
+
202
+ async function handleScheduler(
203
+ request: Request,
204
+ env: Env,
205
+ url: URL
206
+ ): Promise<Response> {
207
+ const schedulerId = env.SCHEDULER.idFromName("main");
208
+ const schedulerStub = env.SCHEDULER.get(schedulerId);
209
+
210
+ const subPath = url.pathname.replace("/scheduler", "") || "/status";
211
+
212
+ const response = await schedulerStub.fetch(
213
+ new Request(`http://do${subPath}${url.search}`, {
214
+ method: request.method,
215
+ body: request.body,
216
+ })
217
+ );
218
+
219
+ const data = await response.json();
220
+ return Response.json(data, { headers: corsHeaders });
221
+ }
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Scanner Module
3
+ *
4
+ * Orchestrates the scanning process:
5
+ * 1. Calls strategy.scan() to find signals
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)
9
+ */
10
+
11
+ import type { Env } from "./types.js";
12
+ import {
13
+ type Strategy,
14
+ type StrategyContext,
15
+ type Order,
16
+ type NewOrder,
17
+ MarketDataClient,
18
+ createOrderManagerFromEnv,
19
+ } from "polymarket-trading-sdk";
20
+
21
+ export interface ScanResult {
22
+ signalsFound: number;
23
+ ordersPlaced: number;
24
+ errors: string[];
25
+ }
26
+
27
+ /**
28
+ * Run a full scan using the given strategy.
29
+ */
30
+ export async function runScanner(
31
+ strategy: Strategy<Env>,
32
+ env: Env
33
+ ): Promise<ScanResult> {
34
+ const result: ScanResult = {
35
+ signalsFound: 0,
36
+ ordersPlaced: 0,
37
+ errors: [],
38
+ };
39
+
40
+ // Create clients
41
+ const marketData = new MarketDataClient({
42
+ clobApi: env.CLOB_API,
43
+ gammaApi: env.GAMMA_API,
44
+ });
45
+
46
+ // OrderManager routes to Lambda (live) or simulates (paper)
47
+ const orderManager = createOrderManagerFromEnv(env);
48
+
49
+ // Get strategy config
50
+ const config = strategy.getConfig?.(env) ?? {};
51
+
52
+ // Create context
53
+ const ctx: StrategyContext<Env> = {
54
+ marketData,
55
+ config,
56
+ env,
57
+ };
58
+
59
+ // Get Durable Object stubs
60
+ const ordersId = env.ORDERS.idFromName("main");
61
+ const ordersStub = env.ORDERS.get(ordersId);
62
+
63
+ try {
64
+ // 1. Scan for signals
65
+ const signals = await strategy.scan(ctx);
66
+ result.signalsFound = signals.length;
67
+
68
+ if (signals.length === 0) {
69
+ return result;
70
+ }
71
+
72
+ // 2. Get pending orders for dedup
73
+ const pendingOrdersResponse = await ordersStub.fetch(
74
+ new Request("http://do/list-pending")
75
+ );
76
+ const pendingOrders = (await pendingOrdersResponse.json()) as Order[];
77
+
78
+ // 3. Process each signal
79
+ for (const signal of signals) {
80
+ try {
81
+ // Check dedup
82
+ const hasPending = pendingOrders.some(
83
+ (o) => o.slug === signal.slug && o.label === signal.label
84
+ );
85
+
86
+ if (hasPending) {
87
+ continue; // Skip - already have pending order
88
+ }
89
+
90
+ // Custom shouldPlace check
91
+ if (strategy.shouldPlace && !strategy.shouldPlace(signal, pendingOrders)) {
92
+ continue;
93
+ }
94
+
95
+ // Get market data for pricing
96
+ const data = await marketData.getMarketData(signal.tokenId);
97
+
98
+ // Calculate price
99
+ const price = strategy.calculatePrice(signal, data, config);
100
+
101
+ if (price === null) {
102
+ continue; // Strategy decided not to place
103
+ }
104
+
105
+ // Determine order size
106
+ const size = (config.orderSize as number) ?? 5;
107
+
108
+ // Place order via OrderManager (handles live vs paper routing)
109
+ const orderResult = await orderManager.placeOrder({
110
+ slug: signal.slug,
111
+ label: signal.label,
112
+ tokenId: signal.tokenId,
113
+ side: signal.side,
114
+ price,
115
+ size,
116
+ });
117
+
118
+ if (!orderResult.success) {
119
+ result.errors.push(
120
+ `Failed to place order for ${signal.label}: ${orderResult.error}`
121
+ );
122
+ continue;
123
+ }
124
+
125
+ // Record in DO
126
+ await ordersStub.fetch(
127
+ new Request("http://do/create", {
128
+ method: "POST",
129
+ body: JSON.stringify({
130
+ slug: signal.slug,
131
+ label: signal.label,
132
+ tokenId: signal.tokenId,
133
+ side: signal.side,
134
+ price,
135
+ size,
136
+ clobOrderId: orderResult.clobOrderId,
137
+ } satisfies NewOrder & { clobOrderId?: string }),
138
+ })
139
+ );
140
+
141
+ result.ordersPlaced++;
142
+ } catch (error) {
143
+ const msg =
144
+ error instanceof Error
145
+ ? error.message
146
+ : `Error processing signal ${signal.label}`;
147
+ result.errors.push(msg);
148
+ }
149
+ }
150
+ } catch (error) {
151
+ const msg = error instanceof Error ? error.message : "Scan failed";
152
+ result.errors.push(msg);
153
+ }
154
+
155
+ return result;
156
+ }