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.
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
- package/templates/worker/package.json +20 -0
- package/templates/worker/src/durable-objects/orders.ts +214 -0
- package/templates/worker/src/durable-objects/positions.ts +247 -0
- package/templates/worker/src/durable-objects/scheduler.ts +291 -0
- package/templates/worker/src/index.ts +221 -0
- package/templates/worker/src/scanner.ts +156 -0
- package/templates/worker/src/strategies/{{strategy-name}}.ts +119 -0
- package/templates/worker/src/types.ts +29 -0
- package/templates/worker/tsconfig.json +15 -0
- package/templates/worker/wrangler.jsonc +42 -0
|
@@ -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
|
+
}
|