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
package/dist/index.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import fs from "fs-extra";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { execSync } from "child_process";
|
|
8
|
+
import pc from "picocolors";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
var __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
var TEMPLATE_DIR = path.resolve(__dirname, "..", "templates", "worker");
|
|
12
|
+
function toKebabCase(str) {
|
|
13
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
|
|
14
|
+
}
|
|
15
|
+
function toPascalCase(str) {
|
|
16
|
+
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
17
|
+
}
|
|
18
|
+
function toCamelCase(str) {
|
|
19
|
+
const pascal = toPascalCase(str);
|
|
20
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
21
|
+
}
|
|
22
|
+
async function replaceInFile(filePath, replacements) {
|
|
23
|
+
let content = await fs.readFile(filePath, "utf-8");
|
|
24
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
25
|
+
content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"), value);
|
|
26
|
+
}
|
|
27
|
+
await fs.writeFile(filePath, content);
|
|
28
|
+
}
|
|
29
|
+
async function replaceInFiles(dir, replacements) {
|
|
30
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const fullPath = path.join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
await replaceInFiles(fullPath, replacements);
|
|
35
|
+
} else if (entry.isFile()) {
|
|
36
|
+
const ext = path.extname(entry.name);
|
|
37
|
+
if ([".ts", ".js", ".json", ".jsonc", ".md", ".txt", ""].includes(ext)) {
|
|
38
|
+
await replaceInFile(fullPath, replacements);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function renameFiles(dir, replacements) {
|
|
44
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const fullPath = path.join(dir, entry.name);
|
|
47
|
+
if (entry.isDirectory()) {
|
|
48
|
+
await renameFiles(fullPath, replacements);
|
|
49
|
+
}
|
|
50
|
+
let newName = entry.name;
|
|
51
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
52
|
+
if (newName.includes(key)) {
|
|
53
|
+
newName = newName.replace(key, value);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (newName !== entry.name) {
|
|
57
|
+
const newPath = path.join(dir, newName);
|
|
58
|
+
await fs.rename(fullPath, newPath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function create(name, options) {
|
|
63
|
+
const targetDir = path.resolve(process.cwd(), name);
|
|
64
|
+
const strategyName = toKebabCase(name);
|
|
65
|
+
const strategyClassName = toPascalCase(name);
|
|
66
|
+
const strategyVarName = toCamelCase(name);
|
|
67
|
+
if (await fs.pathExists(targetDir)) {
|
|
68
|
+
console.error(pc.red(`Error: Directory "${name}" already exists.`));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!await fs.pathExists(TEMPLATE_DIR)) {
|
|
72
|
+
console.error(pc.red(`Error: Template not found at ${TEMPLATE_DIR}`));
|
|
73
|
+
console.error(pc.yellow("This may be a development installation issue."));
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
console.log();
|
|
77
|
+
console.log(pc.cyan(`Creating Polymarket strategy: ${pc.bold(name)}`));
|
|
78
|
+
console.log();
|
|
79
|
+
console.log(` ${pc.green("\u2713")} Copying template...`);
|
|
80
|
+
await fs.copy(TEMPLATE_DIR, targetDir);
|
|
81
|
+
console.log(` ${pc.green("\u2713")} Configuring project...`);
|
|
82
|
+
const replacements = {
|
|
83
|
+
"{{name}}": name,
|
|
84
|
+
"{{strategy-name}}": strategyName,
|
|
85
|
+
"{{StrategyName}}": strategyClassName,
|
|
86
|
+
"{{strategyName}}": strategyVarName
|
|
87
|
+
};
|
|
88
|
+
await replaceInFiles(targetDir, replacements);
|
|
89
|
+
await renameFiles(targetDir, {
|
|
90
|
+
"{{strategy-name}}": strategyName
|
|
91
|
+
});
|
|
92
|
+
if (options.install) {
|
|
93
|
+
console.log(` ${pc.green("\u2713")} Installing dependencies...`);
|
|
94
|
+
try {
|
|
95
|
+
execSync("pnpm install", { cwd: targetDir, stdio: "pipe" });
|
|
96
|
+
} catch {
|
|
97
|
+
try {
|
|
98
|
+
execSync("npm install", { cwd: targetDir, stdio: "pipe" });
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.log(pc.yellow(" Could not install dependencies automatically."));
|
|
101
|
+
console.log(pc.yellow(" Run 'npm install' or 'pnpm install' manually."));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (options.git) {
|
|
106
|
+
console.log(` ${pc.green("\u2713")} Initializing git repository...`);
|
|
107
|
+
try {
|
|
108
|
+
execSync("git init", { cwd: targetDir, stdio: "pipe" });
|
|
109
|
+
execSync("git add -A", { cwd: targetDir, stdio: "pipe" });
|
|
110
|
+
} catch {
|
|
111
|
+
console.log(pc.yellow(" Could not initialize git repository."));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
console.log(pc.green("Done! ") + pc.bold(`Created ${name}`));
|
|
116
|
+
console.log();
|
|
117
|
+
console.log("Next steps:");
|
|
118
|
+
console.log();
|
|
119
|
+
console.log(pc.cyan(` cd ${name}`));
|
|
120
|
+
console.log(pc.cyan(` # Edit src/strategies/${strategyName}.ts`));
|
|
121
|
+
console.log(pc.cyan(" # Configure wrangler.jsonc"));
|
|
122
|
+
console.log(pc.cyan(" wrangler deploy"));
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
program.name("create-polymarket-strategy").description("Create a new Polymarket trading strategy project").version("0.1.0").argument("<name>", "Project name").option("--no-git", "Skip git initialization").option("--no-install", "Skip dependency installation").action(create);
|
|
126
|
+
program.parse();
|
|
127
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { program } from \"commander\";\nimport fs from \"fs-extra\";\nimport path from \"path\";\nimport { execSync } from \"child_process\";\nimport pc from \"picocolors\";\nimport { fileURLToPath } from \"url\";\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// Template directory is at the package root, not in dist\nconst TEMPLATE_DIR = path.resolve(__dirname, \"..\", \"templates\", \"worker\");\n\ninterface Options {\n git: boolean;\n install: boolean;\n}\n\nfunction toKebabCase(str: string): string {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/[\\s_]+/g, \"-\")\n .toLowerCase();\n}\n\nfunction toPascalCase(str: string): string {\n return str\n .split(/[-_\\s]+/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(\"\");\n}\n\nfunction toCamelCase(str: string): string {\n const pascal = toPascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\nasync function replaceInFile(\n filePath: string,\n replacements: Record<string, string>\n): Promise<void> {\n let content = await fs.readFile(filePath, \"utf-8\");\n for (const [key, value] of Object.entries(replacements)) {\n content = content.replace(new RegExp(key.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\"), \"g\"), value);\n }\n await fs.writeFile(filePath, content);\n}\n\nasync function replaceInFiles(\n dir: string,\n replacements: Record<string, string>\n): Promise<void> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n await replaceInFiles(fullPath, replacements);\n } else if (entry.isFile()) {\n // Only process text files\n const ext = path.extname(entry.name);\n if ([\".ts\", \".js\", \".json\", \".jsonc\", \".md\", \".txt\", \"\"].includes(ext)) {\n await replaceInFile(fullPath, replacements);\n }\n }\n }\n}\n\nasync function renameFiles(\n dir: string,\n replacements: Record<string, string>\n): Promise<void> {\n const entries = await fs.readdir(dir, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n await renameFiles(fullPath, replacements);\n }\n\n // Check if filename needs renaming\n let newName = entry.name;\n for (const [key, value] of Object.entries(replacements)) {\n if (newName.includes(key)) {\n newName = newName.replace(key, value);\n }\n }\n\n if (newName !== entry.name) {\n const newPath = path.join(dir, newName);\n await fs.rename(fullPath, newPath);\n }\n }\n}\n\nasync function create(name: string, options: Options): Promise<void> {\n const targetDir = path.resolve(process.cwd(), name);\n const strategyName = toKebabCase(name);\n const strategyClassName = toPascalCase(name);\n const strategyVarName = toCamelCase(name);\n\n // Check if target exists\n if (await fs.pathExists(targetDir)) {\n console.error(pc.red(`Error: Directory \"${name}\" already exists.`));\n process.exit(1);\n }\n\n // Check if template exists\n if (!(await fs.pathExists(TEMPLATE_DIR))) {\n console.error(pc.red(`Error: Template not found at ${TEMPLATE_DIR}`));\n console.error(pc.yellow(\"This may be a development installation issue.\"));\n process.exit(1);\n }\n\n console.log();\n console.log(pc.cyan(`Creating Polymarket strategy: ${pc.bold(name)}`));\n console.log();\n\n // Copy template\n console.log(` ${pc.green(\"✓\")} Copying template...`);\n await fs.copy(TEMPLATE_DIR, targetDir);\n\n // Replace placeholders\n console.log(` ${pc.green(\"✓\")} Configuring project...`);\n const replacements = {\n \"{{name}}\": name,\n \"{{strategy-name}}\": strategyName,\n \"{{StrategyName}}\": strategyClassName,\n \"{{strategyName}}\": strategyVarName,\n };\n await replaceInFiles(targetDir, replacements);\n await renameFiles(targetDir, {\n \"{{strategy-name}}\": strategyName,\n });\n\n // Install dependencies\n if (options.install) {\n console.log(` ${pc.green(\"✓\")} Installing dependencies...`);\n try {\n execSync(\"pnpm install\", { cwd: targetDir, stdio: \"pipe\" });\n } catch {\n try {\n execSync(\"npm install\", { cwd: targetDir, stdio: \"pipe\" });\n } catch (e) {\n console.log(pc.yellow(\" Could not install dependencies automatically.\"));\n console.log(pc.yellow(\" Run 'npm install' or 'pnpm install' manually.\"));\n }\n }\n }\n\n // Initialize git\n if (options.git) {\n console.log(` ${pc.green(\"✓\")} Initializing git repository...`);\n try {\n execSync(\"git init\", { cwd: targetDir, stdio: \"pipe\" });\n execSync(\"git add -A\", { cwd: targetDir, stdio: \"pipe\" });\n } catch {\n console.log(pc.yellow(\" Could not initialize git repository.\"));\n }\n }\n\n // Success message\n console.log();\n console.log(pc.green(\"Done! \") + pc.bold(`Created ${name}`));\n console.log();\n console.log(\"Next steps:\");\n console.log();\n console.log(pc.cyan(` cd ${name}`));\n console.log(pc.cyan(` # Edit src/strategies/${strategyName}.ts`));\n console.log(pc.cyan(\" # Configure wrangler.jsonc\"));\n console.log(pc.cyan(\" wrangler deploy\"));\n console.log();\n}\n\nprogram\n .name(\"create-polymarket-strategy\")\n .description(\"Create a new Polymarket trading strategy project\")\n .version(\"0.1.0\")\n .argument(\"<name>\", \"Project name\")\n .option(\"--no-git\", \"Skip git initialization\")\n .option(\"--no-install\", \"Skip dependency installation\")\n .action(create);\n\nprogram.parse();\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-polymarket-strategy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Polymarket trading strategy project",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-polymarket-strategy": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"prepublishOnly": "pnpm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^12.0.0",
|
|
21
|
+
"fs-extra": "^11.2.0",
|
|
22
|
+
"picocolors": "^1.1.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/fs-extra": "^11.0.4",
|
|
26
|
+
"@types/node": "^22.0.0",
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "^5.7.0"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"polymarket",
|
|
32
|
+
"trading",
|
|
33
|
+
"create",
|
|
34
|
+
"cli",
|
|
35
|
+
"scaffold"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{name}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"tail": "wrangler tail",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"polymarket-trading-sdk": "^0.1.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@cloudflare/workers-types": "^4.20240620.0",
|
|
17
|
+
"typescript": "^5.7.0",
|
|
18
|
+
"wrangler": "^4.0.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Orders Durable Object
|
|
3
|
+
*
|
|
4
|
+
* SQL-backed persistent storage for order tracking.
|
|
5
|
+
* Handles deduplication and order lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Order, NewOrder } from "polymarket-trading-sdk";
|
|
9
|
+
|
|
10
|
+
export class OrdersDO implements DurableObject {
|
|
11
|
+
private sql: SqlStorage;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private state: DurableObjectState,
|
|
15
|
+
private env: unknown
|
|
16
|
+
) {
|
|
17
|
+
this.sql = state.storage.sql;
|
|
18
|
+
this.initSchema();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private initSchema(): void {
|
|
22
|
+
this.sql.exec(`
|
|
23
|
+
CREATE TABLE IF NOT EXISTS orders (
|
|
24
|
+
id TEXT PRIMARY KEY,
|
|
25
|
+
slug TEXT NOT NULL,
|
|
26
|
+
label TEXT NOT NULL,
|
|
27
|
+
token_id TEXT NOT NULL,
|
|
28
|
+
clob_order_id TEXT,
|
|
29
|
+
side TEXT NOT NULL,
|
|
30
|
+
price REAL NOT NULL,
|
|
31
|
+
size REAL NOT NULL,
|
|
32
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
33
|
+
fill_price REAL,
|
|
34
|
+
created_at TEXT NOT NULL,
|
|
35
|
+
filled_at TEXT,
|
|
36
|
+
cancel_reason TEXT
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_orders_slug_label ON orders(slug, label);
|
|
41
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_orders_pending_unique
|
|
42
|
+
ON orders(slug, label) WHERE status = 'pending';
|
|
43
|
+
`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetch(request: Request): Promise<Response> {
|
|
47
|
+
const url = new URL(request.url);
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
switch (url.pathname) {
|
|
51
|
+
case "/create":
|
|
52
|
+
return this.handleCreate(request);
|
|
53
|
+
case "/get-pending":
|
|
54
|
+
return this.handleGetPending(url);
|
|
55
|
+
case "/list-pending":
|
|
56
|
+
return this.handleListPending();
|
|
57
|
+
case "/list-all":
|
|
58
|
+
return this.handleListAll();
|
|
59
|
+
case "/mark-filled":
|
|
60
|
+
return this.handleMarkFilled(request);
|
|
61
|
+
case "/mark-cancelled":
|
|
62
|
+
return this.handleMarkCancelled(request);
|
|
63
|
+
case "/get":
|
|
64
|
+
return this.handleGet(url);
|
|
65
|
+
default:
|
|
66
|
+
return new Response("Not found", { status: 404 });
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
70
|
+
return Response.json({ error: message }, { status: 500 });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async handleCreate(request: Request): Promise<Response> {
|
|
75
|
+
const body = (await request.json()) as NewOrder & { clobOrderId?: string };
|
|
76
|
+
|
|
77
|
+
const id = crypto.randomUUID();
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
this.sql.exec(
|
|
82
|
+
`INSERT INTO orders (
|
|
83
|
+
id, slug, label, token_id, clob_order_id, side, price, size, status, created_at
|
|
84
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?)`,
|
|
85
|
+
id,
|
|
86
|
+
body.slug,
|
|
87
|
+
body.label,
|
|
88
|
+
body.tokenId,
|
|
89
|
+
body.clobOrderId || null,
|
|
90
|
+
body.side,
|
|
91
|
+
body.price,
|
|
92
|
+
body.size,
|
|
93
|
+
now
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return Response.json({ id });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (
|
|
99
|
+
error instanceof Error &&
|
|
100
|
+
error.message.includes("UNIQUE constraint")
|
|
101
|
+
) {
|
|
102
|
+
return Response.json(
|
|
103
|
+
{ error: "Pending order already exists for this market/outcome" },
|
|
104
|
+
{ status: 409 }
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private handleGetPending(url: URL): Response {
|
|
112
|
+
const slug = url.searchParams.get("slug");
|
|
113
|
+
const label = url.searchParams.get("label");
|
|
114
|
+
|
|
115
|
+
if (!slug || !label) {
|
|
116
|
+
return Response.json(
|
|
117
|
+
{ error: "slug and label required" },
|
|
118
|
+
{ status: 400 }
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = this.sql.exec(
|
|
123
|
+
`SELECT * FROM orders WHERE slug = ? AND label = ? AND status = 'pending' LIMIT 1`,
|
|
124
|
+
slug,
|
|
125
|
+
label
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const rows = result.toArray();
|
|
129
|
+
if (rows.length === 0) {
|
|
130
|
+
return Response.json(null);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Response.json(this.rowToOrder(rows[0]));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private handleListPending(): Response {
|
|
137
|
+
const result = this.sql.exec(
|
|
138
|
+
`SELECT * FROM orders WHERE status = 'pending' ORDER BY created_at DESC`
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const orders = result.toArray().map((row) => this.rowToOrder(row));
|
|
142
|
+
return Response.json(orders);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private handleListAll(): Response {
|
|
146
|
+
const result = this.sql.exec(
|
|
147
|
+
`SELECT * FROM orders ORDER BY created_at DESC LIMIT 100`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const orders = result.toArray().map((row) => this.rowToOrder(row));
|
|
151
|
+
return Response.json(orders);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private async handleMarkFilled(request: Request): Promise<Response> {
|
|
155
|
+
const body = (await request.json()) as { id: string; fillPrice: number };
|
|
156
|
+
const now = new Date().toISOString();
|
|
157
|
+
|
|
158
|
+
this.sql.exec(
|
|
159
|
+
`UPDATE orders SET status = 'filled', fill_price = ?, filled_at = ? WHERE id = ?`,
|
|
160
|
+
body.fillPrice,
|
|
161
|
+
now,
|
|
162
|
+
body.id
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
return Response.json({ success: true });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async handleMarkCancelled(request: Request): Promise<Response> {
|
|
169
|
+
const body = (await request.json()) as { id: string; reason?: string };
|
|
170
|
+
|
|
171
|
+
this.sql.exec(
|
|
172
|
+
`UPDATE orders SET status = 'cancelled', cancel_reason = ? WHERE id = ?`,
|
|
173
|
+
body.reason || null,
|
|
174
|
+
body.id
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return Response.json({ success: true });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private handleGet(url: URL): Response {
|
|
181
|
+
const id = url.searchParams.get("id");
|
|
182
|
+
|
|
183
|
+
if (!id) {
|
|
184
|
+
return Response.json({ error: "id required" }, { status: 400 });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const result = this.sql.exec(`SELECT * FROM orders WHERE id = ?`, id);
|
|
188
|
+
const rows = result.toArray();
|
|
189
|
+
|
|
190
|
+
if (rows.length === 0) {
|
|
191
|
+
return Response.json(null);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Response.json(this.rowToOrder(rows[0]));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private rowToOrder(row: Record<string, unknown>): Order {
|
|
198
|
+
return {
|
|
199
|
+
id: row.id as string,
|
|
200
|
+
slug: row.slug as string,
|
|
201
|
+
label: row.label as string,
|
|
202
|
+
tokenId: row.token_id as string,
|
|
203
|
+
clobOrderId: row.clob_order_id as string | undefined,
|
|
204
|
+
side: row.side as "buy" | "sell",
|
|
205
|
+
price: row.price as number,
|
|
206
|
+
size: row.size as number,
|
|
207
|
+
status: row.status as "pending" | "filled" | "cancelled",
|
|
208
|
+
fillPrice: row.fill_price as number | undefined,
|
|
209
|
+
createdAt: row.created_at as string,
|
|
210
|
+
filledAt: row.filled_at as string | undefined,
|
|
211
|
+
cancelReason: row.cancel_reason as string | undefined,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positions Durable Object
|
|
3
|
+
*
|
|
4
|
+
* SQL-backed persistent storage for position tracking.
|
|
5
|
+
* Tracks open positions and calculates P&L.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Position } from "polymarket-trading-sdk";
|
|
9
|
+
|
|
10
|
+
interface NewPosition {
|
|
11
|
+
orderId: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
label: string;
|
|
14
|
+
entryPrice: number;
|
|
15
|
+
size: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Stats {
|
|
19
|
+
total: number;
|
|
20
|
+
open: number;
|
|
21
|
+
won: number;
|
|
22
|
+
lost: number;
|
|
23
|
+
winRate: number;
|
|
24
|
+
totalPnl: number;
|
|
25
|
+
totalInvested: number;
|
|
26
|
+
roi: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class PositionsDO implements DurableObject {
|
|
30
|
+
private sql: SqlStorage;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private state: DurableObjectState,
|
|
34
|
+
private env: unknown
|
|
35
|
+
) {
|
|
36
|
+
this.sql = state.storage.sql;
|
|
37
|
+
this.initSchema();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private initSchema(): void {
|
|
41
|
+
this.sql.exec(`
|
|
42
|
+
CREATE TABLE IF NOT EXISTS positions (
|
|
43
|
+
id TEXT PRIMARY KEY,
|
|
44
|
+
order_id TEXT NOT NULL,
|
|
45
|
+
slug TEXT NOT NULL,
|
|
46
|
+
label TEXT NOT NULL,
|
|
47
|
+
entry_price REAL NOT NULL,
|
|
48
|
+
size REAL NOT NULL,
|
|
49
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
50
|
+
pnl REAL,
|
|
51
|
+
opened_at TEXT NOT NULL,
|
|
52
|
+
closed_at TEXT
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_positions_status ON positions(status);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_positions_order_id ON positions(order_id);
|
|
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 "/create":
|
|
66
|
+
return this.handleCreate(request);
|
|
67
|
+
case "/list-open":
|
|
68
|
+
return this.handleListOpen();
|
|
69
|
+
case "/list-all":
|
|
70
|
+
return this.handleListAll();
|
|
71
|
+
case "/close":
|
|
72
|
+
return this.handleClose(request);
|
|
73
|
+
case "/get":
|
|
74
|
+
return this.handleGet(url);
|
|
75
|
+
case "/stats":
|
|
76
|
+
return this.handleStats();
|
|
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
|
+
private async handleCreate(request: Request): Promise<Response> {
|
|
87
|
+
const body = (await request.json()) as NewPosition;
|
|
88
|
+
|
|
89
|
+
const id = crypto.randomUUID();
|
|
90
|
+
const now = new Date().toISOString();
|
|
91
|
+
|
|
92
|
+
this.sql.exec(
|
|
93
|
+
`INSERT INTO positions (
|
|
94
|
+
id, order_id, slug, label, entry_price, size, status, opened_at
|
|
95
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'open', ?)`,
|
|
96
|
+
id,
|
|
97
|
+
body.orderId,
|
|
98
|
+
body.slug,
|
|
99
|
+
body.label,
|
|
100
|
+
body.entryPrice,
|
|
101
|
+
body.size,
|
|
102
|
+
now
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return Response.json({ id });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private handleListOpen(): Response {
|
|
109
|
+
const result = this.sql.exec(
|
|
110
|
+
`SELECT * FROM positions WHERE status = 'open' ORDER BY opened_at DESC`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const positions = result.toArray().map((row) => this.rowToPosition(row));
|
|
114
|
+
return Response.json(positions);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private handleListAll(): Response {
|
|
118
|
+
const result = this.sql.exec(
|
|
119
|
+
`SELECT * FROM positions ORDER BY opened_at DESC LIMIT 100`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const positions = result.toArray().map((row) => this.rowToPosition(row));
|
|
123
|
+
return Response.json(positions);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async handleClose(request: Request): Promise<Response> {
|
|
127
|
+
const body = (await request.json()) as {
|
|
128
|
+
id: string;
|
|
129
|
+
won: boolean;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const posResult = this.sql.exec(
|
|
133
|
+
`SELECT * FROM positions WHERE id = ?`,
|
|
134
|
+
body.id
|
|
135
|
+
);
|
|
136
|
+
const rows = posResult.toArray();
|
|
137
|
+
|
|
138
|
+
if (rows.length === 0) {
|
|
139
|
+
return Response.json({ error: "Position not found" }, { status: 404 });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const pos = rows[0];
|
|
143
|
+
const entryPrice = pos.entry_price as number;
|
|
144
|
+
const size = pos.size as number;
|
|
145
|
+
|
|
146
|
+
const pnl = body.won
|
|
147
|
+
? size * (1 - entryPrice)
|
|
148
|
+
: -size * entryPrice;
|
|
149
|
+
|
|
150
|
+
const now = new Date().toISOString();
|
|
151
|
+
const status = body.won ? "won" : "lost";
|
|
152
|
+
|
|
153
|
+
this.sql.exec(
|
|
154
|
+
`UPDATE positions SET status = ?, pnl = ?, closed_at = ? WHERE id = ?`,
|
|
155
|
+
status,
|
|
156
|
+
pnl,
|
|
157
|
+
now,
|
|
158
|
+
body.id
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return Response.json({ success: true, pnl });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private handleGet(url: URL): Response {
|
|
165
|
+
const id = url.searchParams.get("id");
|
|
166
|
+
|
|
167
|
+
if (!id) {
|
|
168
|
+
return Response.json({ error: "id required" }, { status: 400 });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const result = this.sql.exec(`SELECT * FROM positions WHERE id = ?`, id);
|
|
172
|
+
const rows = result.toArray();
|
|
173
|
+
|
|
174
|
+
if (rows.length === 0) {
|
|
175
|
+
return Response.json(null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return Response.json(this.rowToPosition(rows[0]));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private handleStats(): Response {
|
|
182
|
+
const result = this.sql.exec(`
|
|
183
|
+
SELECT
|
|
184
|
+
COUNT(*) as total,
|
|
185
|
+
SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open,
|
|
186
|
+
SUM(CASE WHEN status = 'won' THEN 1 ELSE 0 END) as won,
|
|
187
|
+
SUM(CASE WHEN status = 'lost' THEN 1 ELSE 0 END) as lost,
|
|
188
|
+
SUM(COALESCE(pnl, 0)) as total_pnl,
|
|
189
|
+
SUM(size * entry_price) as total_invested
|
|
190
|
+
FROM positions
|
|
191
|
+
`);
|
|
192
|
+
|
|
193
|
+
const rows = result.toArray();
|
|
194
|
+
if (rows.length === 0) {
|
|
195
|
+
return Response.json({
|
|
196
|
+
total: 0,
|
|
197
|
+
open: 0,
|
|
198
|
+
won: 0,
|
|
199
|
+
lost: 0,
|
|
200
|
+
winRate: 0,
|
|
201
|
+
totalPnl: 0,
|
|
202
|
+
totalInvested: 0,
|
|
203
|
+
roi: 0,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const row = rows[0];
|
|
208
|
+
const total = (row.total as number) || 0;
|
|
209
|
+
const open = (row.open as number) || 0;
|
|
210
|
+
const won = (row.won as number) || 0;
|
|
211
|
+
const lost = (row.lost as number) || 0;
|
|
212
|
+
const totalPnl = (row.total_pnl as number) || 0;
|
|
213
|
+
const totalInvested = (row.total_invested as number) || 0;
|
|
214
|
+
|
|
215
|
+
const closed = won + lost;
|
|
216
|
+
const winRate = closed > 0 ? won / closed : 0;
|
|
217
|
+
const roi = totalInvested > 0 ? totalPnl / totalInvested : 0;
|
|
218
|
+
|
|
219
|
+
const stats: Stats = {
|
|
220
|
+
total,
|
|
221
|
+
open,
|
|
222
|
+
won,
|
|
223
|
+
lost,
|
|
224
|
+
winRate,
|
|
225
|
+
totalPnl,
|
|
226
|
+
totalInvested,
|
|
227
|
+
roi,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return Response.json(stats);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private rowToPosition(row: Record<string, unknown>): Position {
|
|
234
|
+
return {
|
|
235
|
+
id: row.id as string,
|
|
236
|
+
orderId: row.order_id as string,
|
|
237
|
+
slug: row.slug as string,
|
|
238
|
+
label: row.label as string,
|
|
239
|
+
entryPrice: row.entry_price as number,
|
|
240
|
+
size: row.size as number,
|
|
241
|
+
status: row.status as "open" | "won" | "lost",
|
|
242
|
+
pnl: row.pnl as number | undefined,
|
|
243
|
+
openedAt: row.opened_at as string,
|
|
244
|
+
closedAt: row.closed_at as string | undefined,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|