@gonzih/polymarket-arb 1.0.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/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # polymarket-arb
2
+
3
+ Claude-powered Polymarket arbitrage bot. Monitors BTC/ETH price feeds, detects 30-second momentum signals, finds matching Polymarket contracts expiring within 20 minutes, and uses Claude AI to assess each trade opportunity before entering.
4
+
5
+ ## How it works
6
+
7
+ 1. **Price feeds** — connects to Binance WebSocket (with Coinbase fallback on 451 geo-block) for real-time BTC/ETH prices
8
+ 2. **Signal detection** — fires when |30s momentum| > 0.35%
9
+ 3. **Contract discovery** — queries Polymarket GraphQL for open BTC/ETH up/down contracts expiring within 20 minutes
10
+ 4. **Claude analysis** — sends signal + contract data to `claude-haiku-4-5-20251001` for confidence score, Kelly fraction, and reasoning
11
+ 5. **Trade entry** — only enters if Claude confidence > 0.65; sizes position using Kelly Criterion (capped at 10%)
12
+ 6. **Risk management** — daily loss limit -20%, total drawdown kill switch -40%
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -g @gonzih/polymarket-arb
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ # Paper trading (default, safe)
24
+ ANTHROPIC_API_KEY=sk-... polymarket-arb --paper
25
+
26
+ # Live trading
27
+ ANTHROPIC_API_KEY=sk-... POLYMARKET_API_KEY=... POLYMARKET_SECRET=... polymarket-arb --live
28
+ ```
29
+
30
+ ## Environment variables
31
+
32
+ | Variable | Required | Default | Description |
33
+ |----------|----------|---------|-------------|
34
+ | `ANTHROPIC_API_KEY` | Yes | — | Claude API key |
35
+ | `POLYMARKET_API_KEY` | Live only | — | Polymarket CLOB API key |
36
+ | `POLYMARKET_SECRET` | Live only | — | Polymarket API secret |
37
+ | `PAPER_MODE` | No | `true` | Set `false` for live trading |
38
+ | `LOG_DIR` | No | `~/.polymarket-arb` | Log directory |
39
+
40
+ ## Logs
41
+
42
+ Structured JSON logs written to `~/.polymarket-arb/trades.jsonl`. Each line is a JSON object with `timestamp`, `level`, and event-specific fields.
43
+
44
+ ## macOS daemon (launchd)
45
+
46
+ A launchd plist is included (`com.polymarket-arb.plist`). Install:
47
+
48
+ ```bash
49
+ # Edit REPLACE_ME with your API key first
50
+ cp com.polymarket-arb.plist ~/Library/LaunchAgents/
51
+ launchctl load ~/Library/LaunchAgents/com.polymarket-arb.plist
52
+ ```
53
+
54
+ ## Architecture
55
+
56
+ ```
57
+ src/
58
+ index.ts — CLI entry point
59
+ feeds.ts — Binance + Coinbase WebSocket managers
60
+ signal.ts — 30s momentum calculation
61
+ polymarket.ts — GraphQL contract discovery + CLOB order placement
62
+ claude.ts — Anthropic SDK integration, trade analysis
63
+ kelly.ts — Kelly Criterion position sizing, risk limits
64
+ logger.ts — Structured JSON logging
65
+ daemon.ts — Main bot loop, kill switch logic
66
+ ```
67
+
68
+ ## License
69
+
70
+ MIT
@@ -0,0 +1,10 @@
1
+ import type { MomentumSignal } from "./signal.js";
2
+ import type { Contract } from "./polymarket.js";
3
+ export type TradeAnalysis = {
4
+ confidence: number;
5
+ kelly_fraction: number;
6
+ reasoning: string;
7
+ enter: boolean;
8
+ };
9
+ export declare function analyzeTradeOpportunity(signal: MomentumSignal, contract: Contract): Promise<TradeAnalysis | null>;
10
+ //# sourceMappingURL=claude.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.d.ts","sourceRoot":"","sources":["../src/claude.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAKhD,MAAM,MAAM,aAAa,GAAG;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAaF,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CA0D/B"}
package/dist/claude.js ADDED
@@ -0,0 +1,68 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { log } from "./logger.js";
3
+ const MODEL = "claude-haiku-4-5-20251001";
4
+ let client = null;
5
+ function getClient() {
6
+ if (!client) {
7
+ const apiKey = process.env.ANTHROPIC_API_KEY;
8
+ if (!apiKey)
9
+ throw new Error("ANTHROPIC_API_KEY is required");
10
+ client = new Anthropic({ apiKey });
11
+ }
12
+ return client;
13
+ }
14
+ export async function analyzeTradeOpportunity(signal, contract) {
15
+ const momentumPct = (signal.momentum * 100).toFixed(3);
16
+ const direction = signal.direction === "down" ? "dropped" : "rose";
17
+ const contractOdds = contract.direction === "down" ? contract.yesPrice : contract.yesPrice;
18
+ const minutesToExpiry = Math.round((contract.expiresAt - Date.now()) / 60_000);
19
+ // Implied real probability based on momentum direction matching contract
20
+ const impliedProb = signal.direction === contract.direction ? 0.75 : 0.35;
21
+ const impliedEdge = Math.abs(impliedProb - contractOdds);
22
+ const prompt = `You are a prediction market arbitrage assistant. Analyze this trade opportunity and respond in JSON only.
23
+
24
+ Signal: ${signal.symbol} ${direction} ${momentumPct}% in 30 seconds on crypto exchange
25
+ Polymarket contract: "${contract.question}" currently priced at ${(contractOdds * 100).toFixed(1)}% (${contractOdds.toFixed(2)})
26
+ Implied edge: ${(impliedEdge * 100).toFixed(1)} percentage points (estimated real probability ~${(impliedProb * 100).toFixed(0)}%)
27
+ Minutes to expiry: ${minutesToExpiry}
28
+ Recent price ticks: [${signal.recentTicks.join(", ")}]
29
+ Current price: ${signal.currentPrice}
30
+
31
+ Assess: Does the momentum signal justify a bet on this contract? Consider momentum strength, edge size, time to expiry, and signal-contract alignment.
32
+
33
+ Respond with JSON only:
34
+ {"confidence": 0.0-1.0, "kelly_fraction": 0.0-0.1, "reasoning": "brief explanation", "enter": true/false}`;
35
+ try {
36
+ const anthropic = getClient();
37
+ const message = await anthropic.messages.create({
38
+ model: MODEL,
39
+ max_tokens: 256,
40
+ messages: [{ role: "user", content: prompt }],
41
+ });
42
+ const content = message.content[0];
43
+ if (content.type !== "text") {
44
+ log("warn", { source: "claude", event: "unexpected_response_type" });
45
+ return null;
46
+ }
47
+ // Extract JSON from response (may have surrounding text)
48
+ const jsonMatch = content.text.match(/\{[^}]+\}/s);
49
+ if (!jsonMatch) {
50
+ log("warn", { source: "claude", event: "no_json_in_response", text: content.text });
51
+ return null;
52
+ }
53
+ const analysis = JSON.parse(jsonMatch[0]);
54
+ log("info", {
55
+ source: "claude",
56
+ event: "analysis",
57
+ symbol: signal.symbol,
58
+ contract: contract.question,
59
+ ...analysis,
60
+ });
61
+ return analysis;
62
+ }
63
+ catch (err) {
64
+ log("error", { source: "claude", event: "analysis_error", error: String(err) });
65
+ return null;
66
+ }
67
+ }
68
+ //# sourceMappingURL=claude.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../src/claude.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export declare class TradingDaemon {
2
+ private feeds;
3
+ private signals;
4
+ private risk;
5
+ private paperMode;
6
+ private lastSignal;
7
+ private dayResetTimer;
8
+ constructor(paperMode?: boolean);
9
+ start(): void;
10
+ stop(): void;
11
+ private scheduleDayReset;
12
+ }
13
+ //# sourceMappingURL=daemon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAWA,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,OAAO,CAAsB;IACrC,OAAO,CAAC,IAAI,CAAoC;IAChD,OAAO,CAAC,SAAS,CAAU;IAC3B,OAAO,CAAC,UAAU,CAAkC;IACpD,OAAO,CAAC,aAAa,CAA+B;gBAExC,SAAS,UAAO;IAK5B,KAAK,IAAI,IAAI;IAwFb,IAAI,IAAI,IAAI;IAMZ,OAAO,CAAC,gBAAgB;CAWzB"}
package/dist/daemon.js ADDED
@@ -0,0 +1,112 @@
1
+ import { FeedManager } from "./feeds.js";
2
+ import { SignalEngine } from "./signal.js";
3
+ import { fetchContracts, placeOrder } from "./polymarket.js";
4
+ import { analyzeTradeOpportunity } from "./claude.js";
5
+ import { RiskManager } from "./kelly.js";
6
+ import { log } from "./logger.js";
7
+ const CLAUDE_CONFIDENCE_THRESHOLD = 0.65;
8
+ const INITIAL_BALANCE = 1000; // paper trading starts with $1000
9
+ const SIGNAL_COOLDOWN_MS = 60_000; // don't re-fire same symbol within 1 min
10
+ export class TradingDaemon {
11
+ feeds = new FeedManager();
12
+ signals = new SignalEngine();
13
+ risk = new RiskManager(INITIAL_BALANCE);
14
+ paperMode;
15
+ lastSignal = new Map();
16
+ dayResetTimer = null;
17
+ constructor(paperMode = true) {
18
+ this.paperMode = paperMode;
19
+ log("info", { event: "daemon_start", paperMode, balance: INITIAL_BALANCE });
20
+ }
21
+ start() {
22
+ this.feeds.onPrice(async (tick) => {
23
+ const signal = this.signals.addTick(tick);
24
+ if (!signal)
25
+ return;
26
+ // Cooldown check
27
+ const lastFired = this.lastSignal.get(signal.symbol) ?? 0;
28
+ if (Date.now() - lastFired < SIGNAL_COOLDOWN_MS)
29
+ return;
30
+ // Risk limits
31
+ const limits = this.risk.checkLimits();
32
+ if (!limits.ok) {
33
+ log("warn", { event: "trading_halted", reason: limits.reason });
34
+ this.stop();
35
+ return;
36
+ }
37
+ this.lastSignal.set(signal.symbol, Date.now());
38
+ log("info", {
39
+ event: "signal_fired",
40
+ symbol: signal.symbol,
41
+ direction: signal.direction,
42
+ momentum: (signal.momentum * 100).toFixed(3) + "%",
43
+ price: signal.currentPrice,
44
+ });
45
+ // Find matching contracts
46
+ const contracts = await fetchContracts(signal.symbol);
47
+ const matching = contracts.filter((c) => c.direction === signal.direction);
48
+ if (matching.length === 0) {
49
+ log("info", { event: "no_matching_contracts", symbol: signal.symbol });
50
+ return;
51
+ }
52
+ // Process best contract (closest expiry)
53
+ const contract = matching.sort((a, b) => a.expiresAt - b.expiresAt)[0];
54
+ // Ask Claude for analysis
55
+ const analysis = await analyzeTradeOpportunity(signal, contract);
56
+ if (!analysis)
57
+ return;
58
+ if (!analysis.enter || analysis.confidence < CLAUDE_CONFIDENCE_THRESHOLD) {
59
+ log("info", {
60
+ event: "trade_skipped",
61
+ reason: "claude_confidence_below_threshold",
62
+ confidence: analysis.confidence,
63
+ threshold: CLAUDE_CONFIDENCE_THRESHOLD,
64
+ reasoning: analysis.reasoning,
65
+ });
66
+ return;
67
+ }
68
+ // Size the position
69
+ const price = contract.yesPrice;
70
+ const size = this.risk.sizePosition(analysis.kelly_fraction, price);
71
+ if (size <= 0) {
72
+ log("info", { event: "trade_skipped", reason: "zero_size" });
73
+ return;
74
+ }
75
+ log("trade", {
76
+ event: "entering_trade",
77
+ symbol: signal.symbol,
78
+ contract: contract.question,
79
+ direction: signal.direction,
80
+ size,
81
+ price,
82
+ confidence: analysis.confidence,
83
+ kellyFraction: analysis.kelly_fraction,
84
+ reasoning: analysis.reasoning,
85
+ expiresAt: new Date(contract.expiresAt).toISOString(),
86
+ paperMode: this.paperMode,
87
+ });
88
+ await placeOrder(contract, "YES", size, price, this.paperMode);
89
+ });
90
+ this.feeds.start();
91
+ // Reset daily P&L at midnight
92
+ this.scheduleDayReset();
93
+ log("info", { event: "feeds_started" });
94
+ }
95
+ stop() {
96
+ this.feeds.stop();
97
+ if (this.dayResetTimer)
98
+ clearTimeout(this.dayResetTimer);
99
+ log("info", { event: "daemon_stopped" });
100
+ }
101
+ scheduleDayReset() {
102
+ const now = new Date();
103
+ const midnight = new Date(now);
104
+ midnight.setHours(24, 0, 0, 0);
105
+ const msUntilMidnight = midnight.getTime() - now.getTime();
106
+ this.dayResetTimer = setTimeout(() => {
107
+ this.risk.resetDay();
108
+ this.scheduleDayReset();
109
+ }, msUntilMidnight);
110
+ }
111
+ }
112
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.js","sourceRoot":"","sources":["../src/daemon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAElC,MAAM,2BAA2B,GAAG,IAAI,CAAC;AACzC,MAAM,eAAe,GAAG,IAAI,CAAC,CAAC,kCAAkC;AAChE,MAAM,kBAAkB,GAAG,MAAM,CAAC,CAAC,yCAAyC;AAE5E,MAAM,OAAO,aAAa;IAChB,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC;IAC1B,OAAO,GAAG,IAAI,YAAY,EAAE,CAAC;IAC7B,IAAI,GAAG,IAAI,WAAW,CAAC,eAAe,CAAC,CAAC;IACxC,SAAS,CAAU;IACnB,UAAU,GAAwB,IAAI,GAAG,EAAE,CAAC;IAC5C,aAAa,GAA0B,IAAI,CAAC;IAEpD,YAAY,SAAS,GAAG,IAAI;QAC1B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;IAC9E,CAAC;IAED,KAAK;QACH,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;YAChC,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,CAAC,MAAM;gBAAE,OAAO;YAEpB,iBAAiB;YACjB,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YAC1D,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,GAAG,kBAAkB;gBAAE,OAAO;YAExD,cAAc;YACd,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;gBACf,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;gBAChE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACZ,OAAO;YACT,CAAC;YAED,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YAE/C,GAAG,CAAC,MAAM,EAAE;gBACV,KAAK,EAAE,cAAc;gBACrB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,QAAQ,EAAE,CAAC,MAAM,CAAC,QAAQ,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG;gBAClD,KAAK,EAAE,MAAM,CAAC,YAAY;aAC3B,CAAC,CAAC;YAEH,0BAA0B;YAC1B,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YACtD,MAAM,QAAQ,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS,CAAC,CAAC;YAE3E,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,uBAAuB,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;gBACvE,OAAO;YACT,CAAC;YAED,yCAAyC;YACzC,MAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YAEvE,0BAA0B;YAC1B,MAAM,QAAQ,GAAG,MAAM,uBAAuB,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YACjE,IAAI,CAAC,QAAQ;gBAAE,OAAO;YAEtB,IAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,UAAU,GAAG,2BAA2B,EAAE,CAAC;gBACzE,GAAG,CAAC,MAAM,EAAE;oBACV,KAAK,EAAE,eAAe;oBACtB,MAAM,EAAE,mCAAmC;oBAC3C,UAAU,EAAE,QAAQ,CAAC,UAAU;oBAC/B,SAAS,EAAE,2BAA2B;oBACtC,SAAS,EAAE,QAAQ,CAAC,SAAS;iBAC9B,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,oBAAoB;YACpB,MAAM,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC;YAChC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;YAEpE,IAAI,IAAI,IAAI,CAAC,EAAE,CAAC;gBACd,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC,CAAC;gBAC7D,OAAO;YACT,CAAC;YAED,GAAG,CAAC,OAAO,EAAE;gBACX,KAAK,EAAE,gBAAgB;gBACvB,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,QAAQ,CAAC,QAAQ;gBAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,IAAI;gBACJ,KAAK;gBACL,UAAU,EAAE,QAAQ,CAAC,UAAU;gBAC/B,aAAa,EAAE,QAAQ,CAAC,cAAc;gBACtC,SAAS,EAAE,QAAQ,CAAC,SAAS;gBAC7B,SAAS,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE;gBACrD,SAAS,EAAE,IAAI,CAAC,SAAS;aAC1B,CAAC,CAAC;YAEH,MAAM,UAAU,CAAC,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACjE,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;QAEnB,8BAA8B;QAC9B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAExB,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI;QACF,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAClB,IAAI,IAAI,CAAC,aAAa;YAAE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC3C,CAAC;IAEO,gBAAgB;QACtB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/B,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;QAE3D,IAAI,CAAC,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE;YACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACrB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,CAAC,EAAE,eAAe,CAAC,CAAC;IACtB,CAAC;CACF"}
@@ -0,0 +1,23 @@
1
+ export type PriceTick = {
2
+ symbol: "BTC" | "ETH";
3
+ price: number;
4
+ timestamp: number;
5
+ source: "binance" | "coinbase";
6
+ };
7
+ export type PriceUpdateHandler = (tick: PriceTick) => void;
8
+ export declare class FeedManager {
9
+ private handlers;
10
+ private binanceWs;
11
+ private coinbaseWs;
12
+ private binanceBlocked;
13
+ private binanceRetryTimer;
14
+ private stopped;
15
+ onPrice(handler: PriceUpdateHandler): void;
16
+ private emit;
17
+ start(): void;
18
+ stop(): void;
19
+ private connectBinance;
20
+ private scheduleBindanceRetry;
21
+ private connectCoinbase;
22
+ }
23
+ //# sourceMappingURL=feeds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.d.ts","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,SAAS,GAAG,UAAU,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;AAS3D,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAA4B;IAC5C,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,UAAU,CAA0B;IAC5C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA+B;IACxD,OAAO,CAAC,OAAO,CAAS;IAExB,OAAO,CAAC,OAAO,EAAE,kBAAkB,GAAG,IAAI;IAI1C,OAAO,CAAC,IAAI;IAIZ,KAAK,IAAI,IAAI;IAKb,IAAI,IAAI,IAAI;IAOZ,OAAO,CAAC,cAAc;IAoDtB,OAAO,CAAC,qBAAqB;IAQ7B,OAAO,CAAC,eAAe;CA+CxB"}
package/dist/feeds.js ADDED
@@ -0,0 +1,134 @@
1
+ import WebSocket from "ws";
2
+ import { log } from "./logger.js";
3
+ const BINANCE_WS = "wss://stream.binance.com:9443/stream?streams=btcusdt@kline_1m/ethusdt@kline_1m";
4
+ const COINBASE_WS = "wss://advanced-trade-ws.coinbase.com";
5
+ // Retry Binance every 6 hours after 451 block
6
+ const BINANCE_RETRY_MS = 6 * 60 * 60 * 1000;
7
+ export class FeedManager {
8
+ handlers = [];
9
+ binanceWs = null;
10
+ coinbaseWs = null;
11
+ binanceBlocked = false;
12
+ binanceRetryTimer = null;
13
+ stopped = false;
14
+ onPrice(handler) {
15
+ this.handlers.push(handler);
16
+ }
17
+ emit(tick) {
18
+ for (const h of this.handlers)
19
+ h(tick);
20
+ }
21
+ start() {
22
+ this.connectBinance();
23
+ this.connectCoinbase();
24
+ }
25
+ stop() {
26
+ this.stopped = true;
27
+ if (this.binanceRetryTimer)
28
+ clearTimeout(this.binanceRetryTimer);
29
+ this.binanceWs?.close();
30
+ this.coinbaseWs?.close();
31
+ }
32
+ connectBinance() {
33
+ if (this.stopped || this.binanceBlocked)
34
+ return;
35
+ const ws = new WebSocket(BINANCE_WS);
36
+ this.binanceWs = ws;
37
+ ws.on("open", () => {
38
+ log("info", { source: "binance", event: "connected" });
39
+ });
40
+ ws.on("message", (raw) => {
41
+ try {
42
+ const msg = JSON.parse(raw.toString());
43
+ const stream = msg.stream ?? "";
44
+ const kline = msg.data?.k;
45
+ if (!kline || !kline.x)
46
+ return; // not closed candle — use last close price anyway
47
+ const symbol = stream.startsWith("btc") ? "BTC" : "ETH";
48
+ const price = parseFloat(kline.c);
49
+ this.emit({ symbol, price, timestamp: Date.now(), source: "binance" });
50
+ }
51
+ catch {
52
+ // ignore malformed
53
+ }
54
+ });
55
+ ws.on("error", (err) => {
56
+ // Check for 451 geo-block
57
+ if (err.message?.includes("451") || err.code === "451") {
58
+ log("warn", { source: "binance", event: "geo_blocked_451", message: err.message });
59
+ this.binanceBlocked = true;
60
+ ws.close();
61
+ this.scheduleBindanceRetry();
62
+ }
63
+ else {
64
+ log("warn", { source: "binance", event: "error", message: err.message });
65
+ }
66
+ });
67
+ ws.on("unexpected-response", (_req, res) => {
68
+ if (res.statusCode === 451) {
69
+ log("warn", { source: "binance", event: "geo_blocked_451", statusCode: 451 });
70
+ this.binanceBlocked = true;
71
+ ws.close();
72
+ this.scheduleBindanceRetry();
73
+ }
74
+ });
75
+ ws.on("close", () => {
76
+ if (this.stopped || this.binanceBlocked)
77
+ return;
78
+ log("info", { source: "binance", event: "reconnecting" });
79
+ setTimeout(() => this.connectBinance(), 5000);
80
+ });
81
+ }
82
+ scheduleBindanceRetry() {
83
+ log("info", { source: "binance", event: "retry_scheduled_6h" });
84
+ this.binanceRetryTimer = setTimeout(() => {
85
+ this.binanceBlocked = false;
86
+ this.connectBinance();
87
+ }, BINANCE_RETRY_MS);
88
+ }
89
+ connectCoinbase() {
90
+ if (this.stopped)
91
+ return;
92
+ const ws = new WebSocket(COINBASE_WS);
93
+ this.coinbaseWs = ws;
94
+ ws.on("open", () => {
95
+ log("info", { source: "coinbase", event: "connected" });
96
+ ws.send(JSON.stringify({
97
+ type: "subscribe",
98
+ product_ids: ["BTC-USD", "ETH-USD"],
99
+ channel: "market_trades",
100
+ }));
101
+ });
102
+ ws.on("message", (raw) => {
103
+ try {
104
+ const msg = JSON.parse(raw.toString());
105
+ if (msg.channel !== "market_trades")
106
+ return;
107
+ const events = msg.events ?? [];
108
+ for (const event of events) {
109
+ const trades = event.trades ?? [];
110
+ for (const trade of trades) {
111
+ const symbol = trade.product_id.startsWith("BTC") ? "BTC" : "ETH";
112
+ const price = parseFloat(trade.price);
113
+ if (!isNaN(price)) {
114
+ this.emit({ symbol, price, timestamp: Date.now(), source: "coinbase" });
115
+ }
116
+ }
117
+ }
118
+ }
119
+ catch {
120
+ // ignore malformed
121
+ }
122
+ });
123
+ ws.on("error", (err) => {
124
+ log("warn", { source: "coinbase", event: "error", message: err.message });
125
+ });
126
+ ws.on("close", () => {
127
+ if (this.stopped)
128
+ return;
129
+ log("info", { source: "coinbase", event: "reconnecting" });
130
+ setTimeout(() => this.connectCoinbase(), 5000);
131
+ });
132
+ }
133
+ }
134
+ //# sourceMappingURL=feeds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"feeds.js","sourceRoot":"","sources":["../src/feeds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,60 @@
1
+ #!/usr/bin/env node
2
+ import { TradingDaemon } from "./daemon.js";
3
+ import { log, logDir } from "./logger.js";
4
+ const args = process.argv.slice(2);
5
+ if (args.includes("--help") || args.includes("-h")) {
6
+ console.log(`polymarket-arb — Claude-powered prediction market arbitrage bot
7
+
8
+ Usage: polymarket-arb [options]
9
+
10
+ Options:
11
+ --paper Paper trading mode (default, no real orders)
12
+ --live Live trading mode (requires POLYMARKET_API_KEY + POLYMARKET_SECRET)
13
+ --log-dir Override log directory (default: ~/.polymarket-arb)
14
+ --help Show this help
15
+
16
+ Environment variables:
17
+ ANTHROPIC_API_KEY Required — Claude API key for trade analysis
18
+ POLYMARKET_API_KEY Required for live trading
19
+ POLYMARKET_SECRET Required for live trading
20
+ PAPER_MODE=true Default paper mode (overridden by --live flag)
21
+ LOG_DIR Log directory path
22
+
23
+ Log directory: ${logDir()}
24
+ `);
25
+ process.exit(0);
26
+ }
27
+ // Determine paper mode
28
+ const paperMode = args.includes("--live")
29
+ ? false
30
+ : args.includes("--paper")
31
+ ? true
32
+ : process.env.PAPER_MODE !== "false";
33
+ if (!paperMode && !process.env.POLYMARKET_API_KEY) {
34
+ console.error("ERROR: --live mode requires POLYMARKET_API_KEY environment variable");
35
+ process.exit(1);
36
+ }
37
+ if (!process.env.ANTHROPIC_API_KEY) {
38
+ console.error("ERROR: ANTHROPIC_API_KEY environment variable is required");
39
+ process.exit(1);
40
+ }
41
+ log("info", {
42
+ event: "startup",
43
+ mode: paperMode ? "paper" : "live",
44
+ logDir: logDir(),
45
+ nodeVersion: process.version,
46
+ });
47
+ const daemon = new TradingDaemon(paperMode);
48
+ daemon.start();
49
+ // Graceful shutdown
50
+ process.on("SIGTERM", () => {
51
+ log("info", { event: "sigterm_received" });
52
+ daemon.stop();
53
+ process.exit(0);
54
+ });
55
+ process.on("SIGINT", () => {
56
+ log("info", { event: "sigint_received" });
57
+ daemon.stop();
58
+ process.exit(0);
59
+ });
60
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAEnC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnD,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;;;;;;iBAiBG,MAAM,EAAE;CACxB,CAAC,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,uBAAuB;AACvB,MAAM,SAAS,GAAG,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvC,CAAC,CAAC,KAAK;IACP,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QAC1B,CAAC,CAAC,IAAI;QACN,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,KAAK,OAAO,CAAC;AAEvC,IAAI,CAAC,SAAS,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;IAClD,OAAO,CAAC,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,EAAE,CAAC;IACnC,OAAO,CAAC,KAAK,CAAC,2DAA2D,CAAC,CAAC;IAC3E,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,GAAG,CAAC,MAAM,EAAE;IACV,KAAK,EAAE,SAAS;IAChB,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;IAClC,MAAM,EAAE,MAAM,EAAE;IAChB,WAAW,EAAE,OAAO,CAAC,OAAO;CAC7B,CAAC,CAAC;AAEH,MAAM,MAAM,GAAG,IAAI,aAAa,CAAC,SAAS,CAAC,CAAC;AAC5C,MAAM,CAAC,KAAK,EAAE,CAAC;AAEf,oBAAoB;AACpB,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;IACzB,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC3C,MAAM,CAAC,IAAI,EAAE,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC;AAEH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE;IACxB,GAAG,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC1C,MAAM,CAAC,IAAI,EAAE,CAAC;IACd,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,17 @@
1
+ export declare class RiskManager {
2
+ private portfolio;
3
+ private startOfDayPortfolio;
4
+ private peakPortfolio;
5
+ private halted;
6
+ constructor(initialBalance: number);
7
+ isHalted(): boolean;
8
+ getPortfolio(): number;
9
+ checkLimits(): {
10
+ ok: boolean;
11
+ reason?: string;
12
+ };
13
+ sizePosition(kellyFraction: number, price: number): number;
14
+ recordTrade(pnl: number): void;
15
+ resetDay(): void;
16
+ }
17
+ //# sourceMappingURL=kelly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kelly.d.ts","sourceRoot":"","sources":["../src/kelly.ts"],"names":[],"mappings":"AAOA,qBAAa,WAAW;IACtB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,MAAM,CAAS;gBAEX,cAAc,EAAE,MAAM;IAMlC,QAAQ,IAAI,OAAO;IAInB,YAAY,IAAI,MAAM;IAItB,WAAW,IAAI;QAAE,EAAE,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;IAiB/C,YAAY,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM;IAW1D,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAa9B,QAAQ,IAAI,IAAI;CAIjB"}
package/dist/kelly.js ADDED
@@ -0,0 +1,61 @@
1
+ import { log } from "./logger.js";
2
+ const MAX_KELLY = 0.10; // cap at 10%
3
+ const MAX_POSITION = 0.08; // 8% of portfolio
4
+ const DAILY_LOSS_LIMIT = -0.20; // -20%
5
+ const TOTAL_DRAWDOWN_LIMIT = -0.40; // -40%
6
+ export class RiskManager {
7
+ portfolio;
8
+ startOfDayPortfolio;
9
+ peakPortfolio;
10
+ halted = false;
11
+ constructor(initialBalance) {
12
+ this.portfolio = initialBalance;
13
+ this.startOfDayPortfolio = initialBalance;
14
+ this.peakPortfolio = initialBalance;
15
+ }
16
+ isHalted() {
17
+ return this.halted;
18
+ }
19
+ getPortfolio() {
20
+ return this.portfolio;
21
+ }
22
+ checkLimits() {
23
+ const dailyReturn = (this.portfolio - this.startOfDayPortfolio) / this.startOfDayPortfolio;
24
+ const drawdown = (this.portfolio - this.peakPortfolio) / this.peakPortfolio;
25
+ if (dailyReturn <= DAILY_LOSS_LIMIT) {
26
+ this.halted = true;
27
+ return { ok: false, reason: `Daily loss limit hit: ${(dailyReturn * 100).toFixed(1)}%` };
28
+ }
29
+ if (drawdown <= TOTAL_DRAWDOWN_LIMIT) {
30
+ this.halted = true;
31
+ return { ok: false, reason: `Total drawdown kill switch: ${(drawdown * 100).toFixed(1)}%` };
32
+ }
33
+ return { ok: true };
34
+ }
35
+ sizePosition(kellyFraction, price) {
36
+ const cappedKelly = Math.min(kellyFraction, MAX_KELLY);
37
+ const maxByPortfolio = this.portfolio * MAX_POSITION;
38
+ const kellySize = this.portfolio * cappedKelly;
39
+ const size = Math.min(kellySize, maxByPortfolio);
40
+ // Convert dollars to number of contracts (each contract is $1 face value at price)
41
+ const contracts = price > 0 ? size / price : 0;
42
+ return Math.floor(contracts * 100) / 100; // round to 2 decimal places
43
+ }
44
+ recordTrade(pnl) {
45
+ this.portfolio += pnl;
46
+ if (this.portfolio > this.peakPortfolio) {
47
+ this.peakPortfolio = this.portfolio;
48
+ }
49
+ log("info", {
50
+ event: "portfolio_update",
51
+ portfolio: this.portfolio,
52
+ pnl,
53
+ peakPortfolio: this.peakPortfolio,
54
+ });
55
+ }
56
+ resetDay() {
57
+ this.startOfDayPortfolio = this.portfolio;
58
+ log("info", { event: "day_reset", portfolio: this.portfolio });
59
+ }
60
+ }
61
+ //# sourceMappingURL=kelly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kelly.js","sourceRoot":"","sources":["../src/kelly.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function log(level: "info" | "warn" | "error" | "trade", data: Record<string, unknown>): void;
2
+ export declare function logDir(): string;
3
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAcA,wBAAgB,GAAG,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAKnG;AAED,wBAAgB,MAAM,IAAI,MAAM,CAE/B"}
package/dist/logger.js ADDED
@@ -0,0 +1,20 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ const LOG_DIR = process.env.LOG_DIR
5
+ ? path.resolve(process.env.LOG_DIR)
6
+ : path.join(os.homedir(), ".polymarket-arb");
7
+ if (!fs.existsSync(LOG_DIR)) {
8
+ fs.mkdirSync(LOG_DIR, { recursive: true });
9
+ }
10
+ const logFile = path.join(LOG_DIR, "trades.jsonl");
11
+ export function log(level, data) {
12
+ const entry = { timestamp: new Date().toISOString(), level, ...data };
13
+ const line = JSON.stringify(entry);
14
+ console.log(line);
15
+ fs.appendFileSync(logFile, line + "\n");
16
+ }
17
+ export function logDir() {
18
+ return LOG_DIR;
19
+ }
20
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO;IACjC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IACnC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,iBAAiB,CAAC,CAAC;AAE/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;IAC5B,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;AAC7C,CAAC;AAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;AAEnD,MAAM,UAAU,GAAG,CAAC,KAA0C,EAAE,IAA6B;IAC3F,MAAM,KAAK,GAAG,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,IAAI,EAAE,CAAC;IACtE,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACnC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,16 @@
1
+ export type Contract = {
2
+ id: string;
3
+ question: string;
4
+ expiresAt: number;
5
+ yesPrice: number;
6
+ noPrice: number;
7
+ symbol: "BTC" | "ETH";
8
+ direction: "up" | "down";
9
+ };
10
+ export type OrderResult = {
11
+ orderId: string;
12
+ status: string;
13
+ };
14
+ export declare function fetchContracts(symbol: "BTC" | "ETH"): Promise<Contract[]>;
15
+ export declare function placeOrder(contract: Contract, side: "YES" | "NO", size: number, price: number, paperMode: boolean): Promise<OrderResult | null>;
16
+ //# sourceMappingURL=polymarket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polymarket.d.ts","sourceRoot":"","sources":["../src/polymarket.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AA6BF,wBAAsB,cAAc,CAAC,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA0D/E;AAED,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,KAAK,GAAG,IAAI,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,OAAO,GACjB,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAmD7B"}
@@ -0,0 +1,123 @@
1
+ import { log } from "./logger.js";
2
+ const GAMMA_API = "https://gamma-api.polymarket.com";
3
+ const CLOB_API = "https://clob.polymarket.com";
4
+ const GRAPHQL_QUERY = `
5
+ query OpenContracts($symbol: String!) {
6
+ markets(
7
+ where: {
8
+ active: true,
9
+ closed: false,
10
+ question_contains: $symbol
11
+ },
12
+ orderBy: endDate,
13
+ orderDirection: asc
14
+ ) {
15
+ id
16
+ question
17
+ endDate
18
+ outcomePrices
19
+ outcomes
20
+ }
21
+ }
22
+ `;
23
+ function parseDirection(question) {
24
+ const q = question.toLowerCase();
25
+ if (q.includes("higher") || q.includes("above") || q.includes("exceed"))
26
+ return "up";
27
+ if (q.includes("lower") || q.includes("below") || q.includes("drop") || q.includes("fall"))
28
+ return "down";
29
+ return null;
30
+ }
31
+ export async function fetchContracts(symbol) {
32
+ const now = Date.now();
33
+ const maxExpiry = now + 20 * 60 * 1000; // 20 min from now
34
+ try {
35
+ const res = await fetch(`${GAMMA_API}/graphql`, {
36
+ method: "POST",
37
+ headers: { "Content-Type": "application/json" },
38
+ body: JSON.stringify({
39
+ query: GRAPHQL_QUERY,
40
+ variables: { symbol },
41
+ }),
42
+ });
43
+ if (!res.ok) {
44
+ log("warn", { source: "polymarket", event: "graphql_error", status: res.status });
45
+ return [];
46
+ }
47
+ const data = (await res.json());
48
+ const markets = data?.data?.markets ?? [];
49
+ const contracts = [];
50
+ for (const m of markets) {
51
+ const expiresAt = new Date(m.endDate).getTime();
52
+ if (expiresAt > maxExpiry || expiresAt < now)
53
+ continue;
54
+ const direction = parseDirection(m.question);
55
+ if (!direction)
56
+ continue;
57
+ const prices = m.outcomePrices?.map(Number) ?? [0.5, 0.5];
58
+ contracts.push({
59
+ id: m.id,
60
+ question: m.question,
61
+ expiresAt,
62
+ yesPrice: prices[0] ?? 0.5,
63
+ noPrice: prices[1] ?? 0.5,
64
+ symbol,
65
+ direction,
66
+ });
67
+ }
68
+ return contracts;
69
+ }
70
+ catch (err) {
71
+ log("warn", { source: "polymarket", event: "fetch_error", error: String(err) });
72
+ return [];
73
+ }
74
+ }
75
+ export async function placeOrder(contract, side, size, price, paperMode) {
76
+ const order = {
77
+ marketId: contract.id,
78
+ side,
79
+ size,
80
+ price,
81
+ orderType: "LIMIT",
82
+ };
83
+ if (paperMode) {
84
+ log("trade", {
85
+ event: "paper_order",
86
+ contract: contract.question,
87
+ side,
88
+ size,
89
+ price,
90
+ expiresAt: new Date(contract.expiresAt).toISOString(),
91
+ });
92
+ return { orderId: `paper-${Date.now()}`, status: "paper" };
93
+ }
94
+ const apiKey = process.env.POLYMARKET_API_KEY;
95
+ const secret = process.env.POLYMARKET_SECRET;
96
+ if (!apiKey || !secret) {
97
+ log("error", { event: "missing_creds", message: "POLYMARKET_API_KEY and POLYMARKET_SECRET required for live trading" });
98
+ return null;
99
+ }
100
+ try {
101
+ const res = await fetch(`${CLOB_API}/order`, {
102
+ method: "POST",
103
+ headers: {
104
+ "Content-Type": "application/json",
105
+ Authorization: `Bearer ${apiKey}`,
106
+ "X-Secret": secret,
107
+ },
108
+ body: JSON.stringify(order),
109
+ });
110
+ if (!res.ok) {
111
+ log("error", { source: "polymarket", event: "order_error", status: res.status });
112
+ return null;
113
+ }
114
+ const result = (await res.json());
115
+ log("trade", { event: "order_placed", orderId: result.orderId, ...order });
116
+ return result;
117
+ }
118
+ catch (err) {
119
+ log("error", { source: "polymarket", event: "order_exception", error: String(err) });
120
+ return null;
121
+ }
122
+ }
123
+ //# sourceMappingURL=polymarket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polymarket.js","sourceRoot":"","sources":["../src/polymarket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { PriceTick } from "./feeds.js";
2
+ export type MomentumSignal = {
3
+ symbol: "BTC" | "ETH";
4
+ direction: "up" | "down";
5
+ momentum: number;
6
+ currentPrice: number;
7
+ recentTicks: number[];
8
+ timestamp: number;
9
+ };
10
+ export declare class SignalEngine {
11
+ private ticks;
12
+ addTick(tick: PriceTick): MomentumSignal | null;
13
+ private evaluate;
14
+ getRecentTicks(symbol: "BTC" | "ETH", n?: number): number[];
15
+ }
16
+ //# sourceMappingURL=signal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.d.ts","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,KAAK,GAAG,KAAK,CAAC;IACtB,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAMF,qBAAa,YAAY;IACvB,OAAO,CAAC,KAAK,CAAuC;IAEpD,OAAO,CAAC,IAAI,EAAE,SAAS,GAAG,cAAc,GAAG,IAAI;IAe/C,OAAO,CAAC,QAAQ;IA2BhB,cAAc,CAAC,MAAM,EAAE,KAAK,GAAG,KAAK,EAAE,CAAC,SAAI,GAAG,MAAM,EAAE;CAIvD"}
package/dist/signal.js ADDED
@@ -0,0 +1,48 @@
1
+ const WINDOW_MS = 30_000; // 30 seconds
2
+ const MOMENTUM_THRESHOLD = 0.0035; // 0.35%
3
+ const MAX_TICKS = 20; // keep last N ticks per symbol for history
4
+ export class SignalEngine {
5
+ ticks = new Map();
6
+ addTick(tick) {
7
+ const key = tick.symbol;
8
+ if (!this.ticks.has(key))
9
+ this.ticks.set(key, []);
10
+ const arr = this.ticks.get(key);
11
+ arr.push(tick);
12
+ // trim to keep only recent + last MAX_TICKS
13
+ const cutoff = Date.now() - WINDOW_MS * 2;
14
+ while (arr.length > 0 && arr[0].timestamp < cutoff && arr.length > MAX_TICKS) {
15
+ arr.shift();
16
+ }
17
+ return this.evaluate(tick.symbol);
18
+ }
19
+ evaluate(symbol) {
20
+ const arr = this.ticks.get(symbol);
21
+ if (!arr || arr.length < 2)
22
+ return null;
23
+ const now = Date.now();
24
+ const windowStart = now - WINDOW_MS;
25
+ const recent = arr.filter((t) => t.timestamp >= windowStart);
26
+ const all = arr.slice(-MAX_TICKS);
27
+ if (recent.length < 2)
28
+ return null;
29
+ const oldest = recent[0].price;
30
+ const newest = recent[recent.length - 1].price;
31
+ const momentum = (newest - oldest) / oldest;
32
+ if (Math.abs(momentum) < MOMENTUM_THRESHOLD)
33
+ return null;
34
+ return {
35
+ symbol,
36
+ direction: momentum > 0 ? "up" : "down",
37
+ momentum,
38
+ currentPrice: newest,
39
+ recentTicks: all.map((t) => t.price).slice(-5),
40
+ timestamp: now,
41
+ };
42
+ }
43
+ getRecentTicks(symbol, n = 5) {
44
+ const arr = this.ticks.get(symbol) ?? [];
45
+ return arr.slice(-n).map((t) => t.price);
46
+ }
47
+ }
48
+ //# sourceMappingURL=signal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signal.js","sourceRoot":"","sources":["../src/signal.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@gonzih/polymarket-arb",
3
+ "version": "1.0.0",
4
+ "description": "Claude-powered Polymarket arbitrage bot — BTC/ETH momentum signal + AI trade analysis",
5
+ "keywords": [
6
+ "polymarket",
7
+ "arbitrage",
8
+ "trading",
9
+ "crypto",
10
+ "defi",
11
+ "btc",
12
+ "eth",
13
+ "claude",
14
+ "anthropic"
15
+ ],
16
+ "homepage": "https://github.com/Gonzih/polymarket-arb",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/Gonzih/polymarket-arb.git"
20
+ },
21
+ "license": "MIT",
22
+ "type": "module",
23
+ "bin": {
24
+ "polymarket-arb": "./dist/index.js"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "scripts": {
28
+ "build": "tsc",
29
+ "start": "node dist/index.js",
30
+ "dev": "tsx src/index.ts",
31
+ "test": "node dist/index.js --help"
32
+ },
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "files": [
37
+ "dist/",
38
+ "README.md"
39
+ ],
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "dependencies": {
44
+ "@anthropic-ai/sdk": "0.39.0",
45
+ "ws": "8.18.1"
46
+ },
47
+ "devDependencies": {
48
+ "@types/ws": "8.5.14",
49
+ "@types/node": "22.13.14",
50
+ "typescript": "5.8.2",
51
+ "tsx": "4.19.3"
52
+ }
53
+ }