polly-gamba 1.0.1
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 +130 -0
- package/package.json +35 -0
- package/src/claude-trader.ts +237 -0
- package/src/index.ts +116 -0
- package/src/markets/clob.ts +127 -0
- package/src/markets/gamma.ts +146 -0
- package/src/mcp/server.ts +199 -0
- package/src/mcp-server.ts +245 -0
- package/src/position-monitor.ts +170 -0
- package/src/signals/coinbase-ws.ts +146 -0
- package/src/test/smoke.ts +93 -0
- package/tsconfig.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# polly-gamba
|
|
2
|
+
|
|
3
|
+
**Signal propagation arbitrage:** Coinbase price move → Polymarket bet, before the humans do.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────────┐ signal ┌───────────────────┐ match ┌──────────────────┐
|
|
7
|
+
│ Coinbase WS │ ──────────→ │ Signal Router │ ─────────→ │ Polymarket CLOB │
|
|
8
|
+
│ BTC/ETH ticker │ │ (Claude brain) │ │ Order execution │
|
|
9
|
+
└─────────────────┘ └───────────────────┘ └──────────────────┘
|
|
10
|
+
↑ ↑ ↑
|
|
11
|
+
0.5% move in keyword + heuristic dry-run by default
|
|
12
|
+
60s window market matching (set API key for live)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Why this works
|
|
16
|
+
|
|
17
|
+
Human reaction time from seeing a BTC pump on Coinbase to placing a Polymarket bet: **5–30 minutes**.
|
|
18
|
+
|
|
19
|
+
That's the edge. We don't front-run algos — we front-run *people*. Crypto price signal → find correlated prediction markets → place before the crowd arrives.
|
|
20
|
+
|
|
21
|
+
News doesn't matter. Price signal does.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
| Component | File | Purpose |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| Coinbase WS | `src/signals/coinbase-ws.ts` | Real-time BTC/ETH ticker, threshold detection |
|
|
28
|
+
| Gamma API | `src/markets/gamma.ts` | Fetch active Polymarket markets, 10-min cache |
|
|
29
|
+
| CLOB API | `src/markets/clob.ts` | Orderbook reads, price queries, order placement |
|
|
30
|
+
| MCP Server | `src/mcp/server.ts` | Polymarket tools for Claude Code integration |
|
|
31
|
+
| Main loop | `src/index.ts` | Orchestration, latency tracking, dry-run |
|
|
32
|
+
|
|
33
|
+
## Quick start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
|
|
38
|
+
# Run the signal → match → execute pipeline (dry-run mode)
|
|
39
|
+
npx ts-node src/index.ts
|
|
40
|
+
|
|
41
|
+
# Run as MCP server (for Claude Code)
|
|
42
|
+
npx ts-node src/mcp/server.ts
|
|
43
|
+
|
|
44
|
+
# Run smoke tests
|
|
45
|
+
npm test
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Signal detection
|
|
49
|
+
|
|
50
|
+
`coinbase-ws.ts` connects to Coinbase Advanced Trade WebSocket (no auth required):
|
|
51
|
+
- Subscribes to `ticker` channel for `BTC-USD` and `ETH-USD`
|
|
52
|
+
- Maintains a 60-second rolling price window
|
|
53
|
+
- Fires a `PriceSignal` when price moves ≥ 0.5% within the window
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
interface PriceSignal {
|
|
57
|
+
asset: 'BTC' | 'ETH'
|
|
58
|
+
direction: 'up' | 'down'
|
|
59
|
+
pct_change: number // e.g. 0.0213 for +2.13%
|
|
60
|
+
price: number // current USD price
|
|
61
|
+
window_secs: number // actual window duration
|
|
62
|
+
signal_ts: number // Date.now() at detection
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Market matching
|
|
67
|
+
|
|
68
|
+
On signal, the system:
|
|
69
|
+
1. Fetches active Polymarket markets (Gamma API, cached 10 min)
|
|
70
|
+
2. Filters for volume > $10k, liquidity > $1k
|
|
71
|
+
3. Keyword-matches market questions against the signal asset
|
|
72
|
+
4. Logs matched markets + latency breakdown
|
|
73
|
+
|
|
74
|
+
## MCP Tools (Claude integration)
|
|
75
|
+
|
|
76
|
+
Add to `~/.claude/claude_desktop_config.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"polymarket": {
|
|
82
|
+
"command": "npx",
|
|
83
|
+
"args": ["ts-node", "/path/to/polly-gamba/src/mcp/server.ts"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Available tools:
|
|
90
|
+
- **`list_markets`** — fetch active markets with optional keyword filter
|
|
91
|
+
- **`get_orderbook`** — CLOB orderbook for a token
|
|
92
|
+
- **`get_market_price`** — buy/sell price (0–1 = probability)
|
|
93
|
+
- **`assess_signal`** — find markets affected by a price signal
|
|
94
|
+
- **`place_order`** — place order (dry-run default)
|
|
95
|
+
|
|
96
|
+
## Latency tracking
|
|
97
|
+
|
|
98
|
+
Every signal logs:
|
|
99
|
+
```
|
|
100
|
+
signal_ts → when the price move was detected
|
|
101
|
+
match_ts → when market matching completed
|
|
102
|
+
execute_ts → when dry-run/order was logged
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Target: signal → execute in < 500ms.
|
|
106
|
+
|
|
107
|
+
## Config
|
|
108
|
+
|
|
109
|
+
| Env var | Required | Description |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `POLYMARKET_API_KEY` | No (for now) | CLOB API key for live order placement |
|
|
112
|
+
|
|
113
|
+
## Dry-run mode
|
|
114
|
+
|
|
115
|
+
All order placement is dry-run by default — logs `WOULD PLACE ORDER: ...` without touching the CLOB. Set `dry_run: false` and `POLYMARKET_API_KEY` for live execution.
|
|
116
|
+
|
|
117
|
+
## Next steps
|
|
118
|
+
|
|
119
|
+
1. **CLOB auth** — implement EIP-712 L1 + L2 API key header signing
|
|
120
|
+
2. **Position sizing** — Kelly criterion on signal strength × market liquidity
|
|
121
|
+
3. **Redis signal bus** — decouple signal detection from execution
|
|
122
|
+
4. **Backtesting** — replay historical Coinbase ticks against historical Polymarket prices
|
|
123
|
+
5. **Claude reasoning** — pipe signals through Claude for semantic market scoring
|
|
124
|
+
|
|
125
|
+
## Build
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm run build # tsc → dist/
|
|
129
|
+
npm test # smoke tests (live API, read-only)
|
|
130
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "polly-gamba",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Coinbase price signal → Claude brain → Polymarket CLOB execution",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "ts-node src/index.ts",
|
|
8
|
+
"mcp": "ts-node src/mcp/server.ts",
|
|
9
|
+
"mcp-paper": "ts-node src/mcp-server.ts",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"test": "ts-node src/test/smoke.ts"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/gonzih/polly-gamba.git"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/gonzih/polly-gamba/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/gonzih/polly-gamba#readme",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
26
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
27
|
+
"@types/node": "^25.5.0",
|
|
28
|
+
"@types/ws": "^8.18.1",
|
|
29
|
+
"better-sqlite3": "^12.8.0",
|
|
30
|
+
"ioredis": "^5.10.1",
|
|
31
|
+
"ts-node": "^10.9.2",
|
|
32
|
+
"typescript": "^6.0.2",
|
|
33
|
+
"ws": "^8.20.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { spawn, ChildProcess, execSync } from 'child_process'
|
|
2
|
+
import { existsSync } from 'fs'
|
|
3
|
+
import { createInterface } from 'readline'
|
|
4
|
+
import Redis from 'ioredis'
|
|
5
|
+
import { PriceSignal } from './signals/coinbase-ws'
|
|
6
|
+
import { Market } from './markets/gamma'
|
|
7
|
+
|
|
8
|
+
function findClaudeBin(): string {
|
|
9
|
+
const candidates = [
|
|
10
|
+
process.env.CLAUDE_BIN,
|
|
11
|
+
'/Users/feral/.npm-global/bin/claude',
|
|
12
|
+
'/usr/local/bin/claude',
|
|
13
|
+
'/opt/homebrew/bin/claude',
|
|
14
|
+
].filter(Boolean) as string[]
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
const fromWhich = execSync('which claude 2>/dev/null', { encoding: 'utf8' }).trim()
|
|
18
|
+
if (fromWhich) candidates.unshift(fromWhich)
|
|
19
|
+
} catch {}
|
|
20
|
+
|
|
21
|
+
for (const p of candidates) {
|
|
22
|
+
if (existsSync(p)) return p
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`claude binary not found. Tried: ${candidates.join(', ')}. Set CLAUDE_BIN env var.`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CLAUDE_BIN = findClaudeBin()
|
|
28
|
+
const CLAUDE_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY || ''
|
|
29
|
+
|
|
30
|
+
export interface TraderConfig {
|
|
31
|
+
cwd: string
|
|
32
|
+
redisPrefix: string
|
|
33
|
+
model?: string
|
|
34
|
+
envOverrides?: Record<string, string>
|
|
35
|
+
label: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class ClaudeTrader {
|
|
39
|
+
private proc: ChildProcess | null = null
|
|
40
|
+
private redis: Redis
|
|
41
|
+
private ready = false
|
|
42
|
+
private queue: string[] = []
|
|
43
|
+
private config: TraderConfig
|
|
44
|
+
|
|
45
|
+
constructor(config: TraderConfig) {
|
|
46
|
+
this.config = config
|
|
47
|
+
this.redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
|
|
48
|
+
retryStrategy: (times) => Math.min(times * 500, 5000),
|
|
49
|
+
maxRetriesPerRequest: null,
|
|
50
|
+
enableReadyCheck: false,
|
|
51
|
+
})
|
|
52
|
+
this.redis.on('error', (e) => console.error(`[redis:${config.label}]`, e.message))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
start() {
|
|
56
|
+
const env: Record<string, string> = {
|
|
57
|
+
...(process.env as Record<string, string>),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (CLAUDE_TOKEN) {
|
|
61
|
+
if (CLAUDE_TOKEN.startsWith('sk-ant-oat')) {
|
|
62
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = CLAUDE_TOKEN
|
|
63
|
+
} else {
|
|
64
|
+
env.ANTHROPIC_API_KEY = CLAUDE_TOKEN
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (this.config.envOverrides) {
|
|
69
|
+
Object.assign(env, this.config.envOverrides)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const args = [
|
|
73
|
+
'--continue',
|
|
74
|
+
'--output-format', 'stream-json',
|
|
75
|
+
'--input-format', 'stream-json',
|
|
76
|
+
'--print',
|
|
77
|
+
'--verbose',
|
|
78
|
+
'--dangerously-skip-permissions',
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
if (this.config.model) {
|
|
82
|
+
args.push('--model', this.config.model)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.proc = spawn(CLAUDE_BIN, args, { cwd: this.config.cwd, env })
|
|
86
|
+
|
|
87
|
+
const rl = createInterface({ input: this.proc.stdout! })
|
|
88
|
+
rl.on('line', (line) => this.handleOutputLine(line))
|
|
89
|
+
|
|
90
|
+
this.proc.stderr?.on('data', (d: Buffer) => {
|
|
91
|
+
const s = d.toString()
|
|
92
|
+
if (s.trim()) this.log('stderr', { text: s.trim() }).catch(() => {})
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
this.proc.on('close', (code: number | null) => {
|
|
96
|
+
console.error(`[claude-trader:${this.config.label}] process closed (code=${code}) — restarting in 5s`)
|
|
97
|
+
this.log('process', { event: 'closed', code }).catch(() => {})
|
|
98
|
+
this.proc = null
|
|
99
|
+
this.ready = false
|
|
100
|
+
setTimeout(() => this.start(), 5000)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
this.proc.on('error', (err: Error) => {
|
|
104
|
+
console.error(`[claude-trader:${this.config.label}] spawn error:`, err.message)
|
|
105
|
+
this.log('process', { event: 'error', message: err.message }).catch(() => {})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Send system context after brief startup delay
|
|
109
|
+
setTimeout(() => {
|
|
110
|
+
this.sendRaw(JSON.stringify({
|
|
111
|
+
type: 'user',
|
|
112
|
+
message: {
|
|
113
|
+
role: 'user',
|
|
114
|
+
content: `You are a Polymarket paper trader running a high-volume moneyball strategy. Your job is to place paper trades on EVERY market where you have ANY opinion on fair value — even slight.
|
|
115
|
+
|
|
116
|
+
TOOLS: place_order, skip_all
|
|
117
|
+
RULES:
|
|
118
|
+
- Output ONLY tool calls. Zero prose.
|
|
119
|
+
- For EVERY market in the list: if current price differs from your estimated fair probability by more than 5%, place a trade.
|
|
120
|
+
- YES is underpriced → BUY YES. NO is underpriced (YES overpriced) → BUY NO.
|
|
121
|
+
- $10 USDC per trade (small size, many bets).
|
|
122
|
+
- NO cap on number of trades — bet every market where you see any edge.
|
|
123
|
+
- Only skip_all if you genuinely have zero opinion on any market (rare).
|
|
124
|
+
- Use your world knowledge: sports standings, political context, recent events, base rates.`
|
|
125
|
+
}
|
|
126
|
+
}))
|
|
127
|
+
this.ready = true
|
|
128
|
+
this.flushQueue()
|
|
129
|
+
console.log(`[claude-trader:${this.config.label}] ready`)
|
|
130
|
+
}, 2000)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private handleOutputLine(line: string) {
|
|
134
|
+
if (!line.trim()) return
|
|
135
|
+
try {
|
|
136
|
+
const msg = JSON.parse(line)
|
|
137
|
+
this.log('claude_output', msg).catch(() => {})
|
|
138
|
+
} catch {
|
|
139
|
+
this.log('claude_raw', { text: line }).catch(() => {})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private sendRaw(json: string) {
|
|
144
|
+
if (this.proc?.stdin?.writable) {
|
|
145
|
+
this.proc.stdin.write(json + '\n')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private flushQueue() {
|
|
150
|
+
while (this.queue.length && this.ready) {
|
|
151
|
+
this.sendRaw(this.queue.shift()!)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async onSignal(signal: PriceSignal, markets: Market[]) {
|
|
156
|
+
const signalId = (signal as any).id || Math.random().toString(36).slice(2)
|
|
157
|
+
const pct = signal.pct_change
|
|
158
|
+
const prefix = this.config.redisPrefix
|
|
159
|
+
|
|
160
|
+
// Log signal to Redis
|
|
161
|
+
await this.redis.lpush(`${prefix}:signals`, JSON.stringify({ ...signal, id: signalId, markets_count: markets.length }))
|
|
162
|
+
await this.redis.ltrim(`${prefix}:signals`, 0, 9999)
|
|
163
|
+
await this.redis.set(`${prefix}:signal:${signalId}`, JSON.stringify({ signal, markets }), 'EX', 86400)
|
|
164
|
+
|
|
165
|
+
const prompt = `## Signal ${signalId}
|
|
166
|
+
|
|
167
|
+
**${signal.asset} ${signal.direction === 'up' ? '+' : '-'}${(Math.abs(pct) * 100).toFixed(2)}%** @ $${signal.price.toFixed(2)}
|
|
168
|
+
Window: ${signal.window_secs}s | Time: ${new Date(signal.signal_ts).toISOString()}
|
|
169
|
+
|
|
170
|
+
## Relevant Polymarket Markets (${markets.length} found)
|
|
171
|
+
|
|
172
|
+
${markets.map((m, i) => `### ${i + 1}. ${m.question}
|
|
173
|
+
- Volume: $${(m.volume || 0).toLocaleString()}
|
|
174
|
+
- Liquidity: $${(m.liquidity || 0).toLocaleString()}
|
|
175
|
+
- Market ID: ${m.id}
|
|
176
|
+
- Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
|
|
177
|
+
${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
|
|
178
|
+
|
|
179
|
+
Call place_order on any market you have edge on. $10 per trade. No cap.`
|
|
180
|
+
|
|
181
|
+
const msg = JSON.stringify({
|
|
182
|
+
type: 'user',
|
|
183
|
+
message: { role: 'user', content: prompt }
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if (this.ready) {
|
|
187
|
+
this.sendRaw(msg)
|
|
188
|
+
} else {
|
|
189
|
+
this.queue.push(msg)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async onAutonomousScan(markets: Market[]) {
|
|
194
|
+
const scanId = Math.random().toString(36).slice(2)
|
|
195
|
+
const prefix = this.config.redisPrefix
|
|
196
|
+
|
|
197
|
+
await this.redis.lpush(`${prefix}:scans`, JSON.stringify({ scanId, markets_count: markets.length, ts: Date.now() }))
|
|
198
|
+
await this.redis.ltrim(`${prefix}:scans`, 0, 9999)
|
|
199
|
+
|
|
200
|
+
const tradeable = markets.filter(m =>
|
|
201
|
+
m.tokens?.some(t => t.price > 0.02 && t.price < 0.98)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
const prompt = `## Autonomous Scan ${scanId}
|
|
205
|
+
|
|
206
|
+
Time: ${new Date().toISOString()}
|
|
207
|
+
|
|
208
|
+
## High-Quality Markets (${tradeable.length} found — vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
209
|
+
|
|
210
|
+
${tradeable.map((m, i) => `### ${i + 1}. ${m.question}
|
|
211
|
+
- Volume 24h: $${(m.volume24hr || 0).toLocaleString()}
|
|
212
|
+
- Liquidity: $${(m.liquidity || 0).toLocaleString()}
|
|
213
|
+
- Market ID: ${m.id}
|
|
214
|
+
- Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
|
|
215
|
+
${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
|
|
216
|
+
|
|
217
|
+
For EVERY market above: if price differs from your fair probability by >5%, place a trade ($10 USDC). Call skip_all only if you have zero opinion on all markets.`
|
|
218
|
+
|
|
219
|
+
const msg = JSON.stringify({
|
|
220
|
+
type: 'user',
|
|
221
|
+
message: { role: 'user', content: prompt }
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
if (this.ready) {
|
|
225
|
+
this.sendRaw(msg)
|
|
226
|
+
} else {
|
|
227
|
+
this.queue.push(msg)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private async log(type: string, data: any) {
|
|
232
|
+
const prefix = this.config.redisPrefix
|
|
233
|
+
const entry = JSON.stringify({ type, data, ts: Date.now(), model: this.config.label })
|
|
234
|
+
await this.redis.lpush(`${prefix}:log`, entry)
|
|
235
|
+
await this.redis.ltrim(`${prefix}:log`, 0, 9999)
|
|
236
|
+
}
|
|
237
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { CoinbaseSignalDetector, PriceSignal } from './signals/coinbase-ws'
|
|
2
|
+
import { fetchActiveMarkets, fetchHighQualityMarkets, filterBySignal } from './markets/gamma'
|
|
3
|
+
import { ClaudeTrader } from './claude-trader'
|
|
4
|
+
import { PositionMonitor } from './position-monitor'
|
|
5
|
+
import * as path from 'path'
|
|
6
|
+
import * as os from 'os'
|
|
7
|
+
|
|
8
|
+
const CWD = process.env.POLLY_CWD || path.join(os.homedir(), 'polly-gamba')
|
|
9
|
+
|
|
10
|
+
async function main(): Promise<void> {
|
|
11
|
+
console.log('[polly-gamba] Starting paper trading service')
|
|
12
|
+
console.log(`[polly-gamba] Claude cwd: ${CWD}`)
|
|
13
|
+
|
|
14
|
+
const anthropicTrader = new ClaudeTrader({ cwd: CWD, redisPrefix: 'polly', label: 'anthropic' })
|
|
15
|
+
|
|
16
|
+
const ollamaTrader = new ClaudeTrader({
|
|
17
|
+
cwd: CWD,
|
|
18
|
+
redisPrefix: 'polly:ollama',
|
|
19
|
+
label: 'ollama',
|
|
20
|
+
model: 'glm-4.7-flash',
|
|
21
|
+
envOverrides: {
|
|
22
|
+
ANTHROPIC_BASE_URL: 'http://localhost:11434',
|
|
23
|
+
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
|
24
|
+
ANTHROPIC_API_KEY: 'ollama',
|
|
25
|
+
USER_TYPE: 'ant',
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const signals = new CoinbaseSignalDetector()
|
|
30
|
+
|
|
31
|
+
const monitor = new PositionMonitor(
|
|
32
|
+
process.env.REDIS_URL || 'redis://localhost:6379',
|
|
33
|
+
'polly'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
anthropicTrader.start()
|
|
37
|
+
ollamaTrader.start()
|
|
38
|
+
|
|
39
|
+
signals.on('signal', (signal: PriceSignal) => {
|
|
40
|
+
console.log(`[signal] ${signal.asset} ${signal.direction} ${(Math.abs(signal.pct_change) * 100).toFixed(2)}% @ $${signal.price.toLocaleString()}`)
|
|
41
|
+
handleSignal(signal, anthropicTrader, ollamaTrader).catch((e: Error) => {
|
|
42
|
+
console.error('[error] signal handling failed:', e.message)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
signals.start()
|
|
47
|
+
console.log('[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...')
|
|
48
|
+
|
|
49
|
+
// Position monitor: check every 15 minutes for exits
|
|
50
|
+
const SCAN_INTERVAL_MS = 15 * 60 * 1000 // 15 minutes
|
|
51
|
+
monitor.checkPositions().catch((e: Error) => console.error('[monitor]', e.message))
|
|
52
|
+
setInterval(() => {
|
|
53
|
+
monitor.checkPositions().catch((e: Error) => console.error('[monitor]', e.message))
|
|
54
|
+
}, SCAN_INTERVAL_MS)
|
|
55
|
+
|
|
56
|
+
// Autonomous scan on startup and every 15 minutes
|
|
57
|
+
autonomousScan(anthropicTrader, ollamaTrader).catch((e: Error) => {
|
|
58
|
+
console.error('[error] autonomous scan failed:', e.message)
|
|
59
|
+
})
|
|
60
|
+
setInterval(() => {
|
|
61
|
+
autonomousScan(anthropicTrader, ollamaTrader).catch((e: Error) => {
|
|
62
|
+
console.error('[error] autonomous scan failed:', e.message)
|
|
63
|
+
})
|
|
64
|
+
}, SCAN_INTERVAL_MS)
|
|
65
|
+
|
|
66
|
+
process.on('SIGINT', () => {
|
|
67
|
+
console.log('\n[polly-gamba] Shutting down...')
|
|
68
|
+
signals.stop()
|
|
69
|
+
monitor.stop()
|
|
70
|
+
process.exit(0)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
process.on('SIGTERM', () => {
|
|
74
|
+
signals.stop()
|
|
75
|
+
monitor.stop()
|
|
76
|
+
process.exit(0)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function autonomousScan(...traders: ClaudeTrader[]): Promise<void> {
|
|
81
|
+
console.log('[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)')
|
|
82
|
+
let markets
|
|
83
|
+
try {
|
|
84
|
+
markets = await fetchHighQualityMarkets()
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
console.error('[scan] failed to fetch markets:', e.message)
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
console.log(`[scan] ${markets.length} high-quality markets for autonomous review`)
|
|
90
|
+
await Promise.all(traders.map(t => t.onAutonomousScan(markets)))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function handleSignal(signal: PriceSignal, ...traders: ClaudeTrader[]): Promise<void> {
|
|
94
|
+
let markets
|
|
95
|
+
try {
|
|
96
|
+
markets = await fetchActiveMarkets(10_000, 1_000)
|
|
97
|
+
} catch (e: any) {
|
|
98
|
+
console.error('[error] failed to fetch markets:', e.message)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const relevant = filterBySignal(signal.asset, markets)
|
|
103
|
+
console.log(`[match] ${relevant.length} markets for ${signal.asset} signal`)
|
|
104
|
+
|
|
105
|
+
if (relevant.length === 0) {
|
|
106
|
+
console.log('[match] no relevant markets found')
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
await Promise.all(traders.map(t => t.onSignal(signal, relevant)))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
main().catch((err) => {
|
|
114
|
+
console.error('[polly-gamba] Fatal:', err)
|
|
115
|
+
process.exit(1)
|
|
116
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const CLOB_BASE = 'https://clob.polymarket.com';
|
|
2
|
+
|
|
3
|
+
export interface OrderbookLevel {
|
|
4
|
+
price: string;
|
|
5
|
+
size: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface Orderbook {
|
|
9
|
+
market: string;
|
|
10
|
+
asset_id: string;
|
|
11
|
+
bids: OrderbookLevel[];
|
|
12
|
+
asks: OrderbookLevel[];
|
|
13
|
+
spread?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PlaceOrderParams {
|
|
17
|
+
token_id: string;
|
|
18
|
+
side: 'BUY' | 'SELL';
|
|
19
|
+
size_usdc: number;
|
|
20
|
+
price: number; // 0-1 range
|
|
21
|
+
dry_run?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OrderResult {
|
|
25
|
+
dry_run: boolean;
|
|
26
|
+
token_id: string;
|
|
27
|
+
side: 'BUY' | 'SELL';
|
|
28
|
+
size_usdc: number;
|
|
29
|
+
price: number;
|
|
30
|
+
estimated_shares: number;
|
|
31
|
+
order_id?: string;
|
|
32
|
+
logged_at: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function getOrderbook(tokenId: string): Promise<Orderbook> {
|
|
36
|
+
const url = `${CLOB_BASE}/book?token_id=${encodeURIComponent(tokenId)}`;
|
|
37
|
+
const res = await fetch(url);
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
throw new Error(`CLOB getOrderbook error: ${res.status} ${res.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
const data: any = await res.json();
|
|
42
|
+
|
|
43
|
+
const bids: OrderbookLevel[] = (data.bids ?? []).slice(0, 10);
|
|
44
|
+
const asks: OrderbookLevel[] = (data.asks ?? []).slice(0, 10);
|
|
45
|
+
|
|
46
|
+
let spread: number | undefined;
|
|
47
|
+
if (bids.length > 0 && asks.length > 0) {
|
|
48
|
+
spread = parseFloat(asks[0].price) - parseFloat(bids[0].price);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
market: data.market ?? '',
|
|
53
|
+
asset_id: data.asset_id ?? tokenId,
|
|
54
|
+
bids,
|
|
55
|
+
asks,
|
|
56
|
+
spread,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getPrice(tokenId: string, side: 'buy' | 'sell'): Promise<number> {
|
|
61
|
+
const url = `${CLOB_BASE}/price?token_id=${encodeURIComponent(tokenId)}&side=${side}`;
|
|
62
|
+
const res = await fetch(url);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`CLOB getPrice error: ${res.status} ${res.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
const data: any = await res.json();
|
|
67
|
+
return parseFloat(data.price ?? data) ?? 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function getPrices(tokenIds: string[]): Promise<Map<string, number>> {
|
|
71
|
+
const prices = new Map<string, number>();
|
|
72
|
+
|
|
73
|
+
// Batch with concurrency limit of 5
|
|
74
|
+
const chunks: string[][] = [];
|
|
75
|
+
for (let i = 0; i < tokenIds.length; i += 5) {
|
|
76
|
+
chunks.push(tokenIds.slice(i, i + 5));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
for (const chunk of chunks) {
|
|
80
|
+
await Promise.all(
|
|
81
|
+
chunk.map(async (id) => {
|
|
82
|
+
try {
|
|
83
|
+
const price = await getPrice(id, 'buy');
|
|
84
|
+
prices.set(id, price);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// skip failed lookups
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return prices;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function placeOrder(params: PlaceOrderParams): Promise<OrderResult> {
|
|
96
|
+
const { token_id, side, size_usdc, price, dry_run = true } = params;
|
|
97
|
+
const estimated_shares = price > 0 ? size_usdc / price : 0;
|
|
98
|
+
|
|
99
|
+
const result: OrderResult = {
|
|
100
|
+
dry_run,
|
|
101
|
+
token_id,
|
|
102
|
+
side,
|
|
103
|
+
size_usdc,
|
|
104
|
+
price,
|
|
105
|
+
estimated_shares,
|
|
106
|
+
logged_at: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
if (dry_run) {
|
|
110
|
+
console.log(
|
|
111
|
+
`[clob/dry-run] WOULD PLACE ORDER: ${side} ${size_usdc} USDC @ ${price} ` +
|
|
112
|
+
`(~${estimated_shares.toFixed(2)} shares) token=${token_id}`
|
|
113
|
+
);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Real order placement — requires CLOB auth (L1/L2 headers)
|
|
118
|
+
// Auth implementation deferred — set POLYMARKET_KEY env var when ready
|
|
119
|
+
const apiKey = process.env.POLYMARKET_API_KEY;
|
|
120
|
+
if (!apiKey) {
|
|
121
|
+
throw new Error('POLYMARKET_API_KEY not set — cannot place real orders');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// TODO: implement full CLOB auth (EIP-712 L1 + L2 API key headers)
|
|
125
|
+
// See: https://docs.polymarket.com/#authentication
|
|
126
|
+
throw new Error('Real order placement not yet implemented — use dry_run=true');
|
|
127
|
+
}
|