polly-gamba 1.0.3 → 1.0.4
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/claude-trader.d.ts +36 -0
- package/dist/claude-trader.js +247 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +132 -0
- package/dist/markets/clob.d.ts +32 -0
- package/dist/markets/clob.js +85 -0
- package/dist/markets/gamma.d.ts +22 -0
- package/dist/markets/gamma.js +113 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +181 -0
- package/dist/mcp-server.d.ts +2 -0
- package/dist/mcp-server.js +361 -0
- package/dist/position-monitor.d.ts +22 -0
- package/dist/position-monitor.js +138 -0
- package/dist/signals/coinbase-ws.d.ts +23 -0
- package/dist/signals/coinbase-ws.js +120 -0
- package/dist/test/smoke.d.ts +1 -0
- package/dist/test/smoke.js +86 -0
- package/package.json +1 -1
- package/src/mcp-server.ts +2 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { PriceSignal } from './signals/coinbase-ws';
|
|
2
|
+
import { Market } from './markets/gamma';
|
|
3
|
+
export interface TraderConfig {
|
|
4
|
+
cwd: string;
|
|
5
|
+
redisPrefix: string;
|
|
6
|
+
model?: string;
|
|
7
|
+
envOverrides?: Record<string, string>;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class ClaudeTrader {
|
|
11
|
+
private proc;
|
|
12
|
+
private redis;
|
|
13
|
+
private ready;
|
|
14
|
+
private queue;
|
|
15
|
+
private config;
|
|
16
|
+
constructor(config: TraderConfig);
|
|
17
|
+
start(): void;
|
|
18
|
+
private handleOutputLine;
|
|
19
|
+
private sendRaw;
|
|
20
|
+
private flushQueue;
|
|
21
|
+
onSignal(signal: PriceSignal, markets: Market[]): Promise<void>;
|
|
22
|
+
onAutonomousScan(markets: Market[]): Promise<void>;
|
|
23
|
+
onPositionReview(positions: Array<{
|
|
24
|
+
market_id: string;
|
|
25
|
+
market_question: string;
|
|
26
|
+
outcome: string;
|
|
27
|
+
side: string;
|
|
28
|
+
price: number;
|
|
29
|
+
size_usdc: number;
|
|
30
|
+
exit_trigger?: string;
|
|
31
|
+
ts: number;
|
|
32
|
+
current_price?: number;
|
|
33
|
+
hours_to_expiry?: number;
|
|
34
|
+
}>): Promise<void>;
|
|
35
|
+
private log;
|
|
36
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ClaudeTrader = void 0;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const fs_1 = require("fs");
|
|
9
|
+
const readline_1 = require("readline");
|
|
10
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
11
|
+
function findClaudeBin() {
|
|
12
|
+
const candidates = [
|
|
13
|
+
process.env.CLAUDE_BIN,
|
|
14
|
+
'/Users/feral/.npm-global/bin/claude',
|
|
15
|
+
'/usr/local/bin/claude',
|
|
16
|
+
'/opt/homebrew/bin/claude',
|
|
17
|
+
].filter(Boolean);
|
|
18
|
+
try {
|
|
19
|
+
const fromWhich = (0, child_process_1.execSync)('which claude 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
20
|
+
if (fromWhich)
|
|
21
|
+
candidates.unshift(fromWhich);
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
if ((0, fs_1.existsSync)(p))
|
|
26
|
+
return p;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`claude binary not found. Tried: ${candidates.join(', ')}. Set CLAUDE_BIN env var.`);
|
|
29
|
+
}
|
|
30
|
+
const CLAUDE_BIN = findClaudeBin();
|
|
31
|
+
const CLAUDE_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY || '';
|
|
32
|
+
class ClaudeTrader {
|
|
33
|
+
proc = null;
|
|
34
|
+
redis;
|
|
35
|
+
ready = false;
|
|
36
|
+
queue = [];
|
|
37
|
+
config;
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.redis = new ioredis_1.default(process.env.REDIS_URL || 'redis://localhost:6379', {
|
|
41
|
+
retryStrategy: (times) => Math.min(times * 500, 5000),
|
|
42
|
+
maxRetriesPerRequest: null,
|
|
43
|
+
enableReadyCheck: false,
|
|
44
|
+
});
|
|
45
|
+
this.redis.on('error', (e) => console.error(`[redis:${config.label}]`, e.message));
|
|
46
|
+
}
|
|
47
|
+
start() {
|
|
48
|
+
const env = {
|
|
49
|
+
...process.env,
|
|
50
|
+
};
|
|
51
|
+
if (CLAUDE_TOKEN) {
|
|
52
|
+
if (CLAUDE_TOKEN.startsWith('sk-ant-oat')) {
|
|
53
|
+
env.CLAUDE_CODE_OAUTH_TOKEN = CLAUDE_TOKEN;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
env.ANTHROPIC_API_KEY = CLAUDE_TOKEN;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (this.config.envOverrides) {
|
|
60
|
+
Object.assign(env, this.config.envOverrides);
|
|
61
|
+
}
|
|
62
|
+
const args = [
|
|
63
|
+
'--output-format', 'stream-json',
|
|
64
|
+
'--input-format', 'stream-json',
|
|
65
|
+
'--print',
|
|
66
|
+
'--verbose',
|
|
67
|
+
'--dangerously-skip-permissions',
|
|
68
|
+
];
|
|
69
|
+
if (this.config.model) {
|
|
70
|
+
args.push('--model', this.config.model);
|
|
71
|
+
}
|
|
72
|
+
this.proc = (0, child_process_1.spawn)(CLAUDE_BIN, args, { cwd: this.config.cwd, env });
|
|
73
|
+
const rl = (0, readline_1.createInterface)({ input: this.proc.stdout });
|
|
74
|
+
rl.on('line', (line) => this.handleOutputLine(line));
|
|
75
|
+
this.proc.stderr?.on('data', (d) => {
|
|
76
|
+
const s = d.toString();
|
|
77
|
+
if (s.trim())
|
|
78
|
+
this.log('stderr', { text: s.trim() }).catch(() => { });
|
|
79
|
+
});
|
|
80
|
+
this.proc.on('close', (code) => {
|
|
81
|
+
console.error(`[claude-trader:${this.config.label}] process closed (code=${code}) — restarting in 5s`);
|
|
82
|
+
this.log('process', { event: 'closed', code }).catch(() => { });
|
|
83
|
+
this.proc = null;
|
|
84
|
+
this.ready = false;
|
|
85
|
+
setTimeout(() => this.start(), 5000);
|
|
86
|
+
});
|
|
87
|
+
this.proc.on('error', (err) => {
|
|
88
|
+
console.error(`[claude-trader:${this.config.label}] spawn error:`, err.message);
|
|
89
|
+
this.log('process', { event: 'error', message: err.message }).catch(() => { });
|
|
90
|
+
});
|
|
91
|
+
// Send system context after brief startup delay
|
|
92
|
+
setTimeout(() => {
|
|
93
|
+
this.sendRaw(JSON.stringify({
|
|
94
|
+
type: 'user',
|
|
95
|
+
message: {
|
|
96
|
+
role: 'user',
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
TOOLS: place_order, skip_all, get_budget_status
|
|
100
|
+
RULES:
|
|
101
|
+
- Output ONLY tool calls. Zero prose.
|
|
102
|
+
- For EVERY market in the list: if current price differs from your estimated fair probability by more than 5%, place a trade.
|
|
103
|
+
- YES is underpriced → BUY YES. NO is underpriced (YES overpriced) → BUY NO.
|
|
104
|
+
- $10 USDC per trade (small size, many bets).
|
|
105
|
+
- NO cap on number of trades — bet every market where you see any edge.
|
|
106
|
+
- Only skip_all if you genuinely have zero opinion on any market (rare).
|
|
107
|
+
- Use your world knowledge: sports standings, political context, recent events, base rates.
|
|
108
|
+
|
|
109
|
+
## POSITION DISCIPLINE:
|
|
110
|
+
- Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
|
|
111
|
+
- To add to an existing position: you MUST cite a specific new catalyst (news published in last 24h, not price movement). Price dipping is NOT a catalyst. Price rising is NOT a catalyst. New information is a catalyst.
|
|
112
|
+
- exit_trigger is required on every trade. Be specific: "Exit when price hits 0.X" or "Exit when [specific news event]" — not "when narrative converges."
|
|
113
|
+
- Call get_budget_status at the start of each scan to know available capital.`
|
|
114
|
+
}
|
|
115
|
+
}));
|
|
116
|
+
this.ready = true;
|
|
117
|
+
this.flushQueue();
|
|
118
|
+
console.log(`[claude-trader:${this.config.label}] ready`);
|
|
119
|
+
}, 2000);
|
|
120
|
+
}
|
|
121
|
+
handleOutputLine(line) {
|
|
122
|
+
if (!line.trim())
|
|
123
|
+
return;
|
|
124
|
+
try {
|
|
125
|
+
const msg = JSON.parse(line);
|
|
126
|
+
this.log('claude_output', msg).catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
this.log('claude_raw', { text: line }).catch(() => { });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
sendRaw(json) {
|
|
133
|
+
if (this.proc?.stdin?.writable) {
|
|
134
|
+
this.proc.stdin.write(json + '\n');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
flushQueue() {
|
|
138
|
+
while (this.queue.length && this.ready) {
|
|
139
|
+
this.sendRaw(this.queue.shift());
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async onSignal(signal, markets) {
|
|
143
|
+
const signalId = signal.id || Math.random().toString(36).slice(2);
|
|
144
|
+
const pct = signal.pct_change;
|
|
145
|
+
const prefix = this.config.redisPrefix;
|
|
146
|
+
// Log signal to Redis
|
|
147
|
+
await this.redis.lpush(`${prefix}:signals`, JSON.stringify({ ...signal, id: signalId, markets_count: markets.length }));
|
|
148
|
+
await this.redis.ltrim(`${prefix}:signals`, 0, 9999);
|
|
149
|
+
await this.redis.set(`${prefix}:signal:${signalId}`, JSON.stringify({ signal, markets }), 'EX', 86400);
|
|
150
|
+
const prompt = `## Signal ${signalId}
|
|
151
|
+
|
|
152
|
+
**${signal.asset} ${signal.direction === 'up' ? '+' : '-'}${(Math.abs(pct) * 100).toFixed(2)}%** @ $${signal.price.toFixed(2)}
|
|
153
|
+
Window: ${signal.window_secs}s | Time: ${new Date(signal.signal_ts).toISOString()}
|
|
154
|
+
|
|
155
|
+
## Relevant Polymarket Markets (${markets.length} found)
|
|
156
|
+
|
|
157
|
+
${markets.map((m, i) => `### ${i + 1}. ${m.question}
|
|
158
|
+
- Volume: $${(m.volume || 0).toLocaleString()}
|
|
159
|
+
- Liquidity: $${(m.liquidity || 0).toLocaleString()}
|
|
160
|
+
- Market ID: ${m.id}
|
|
161
|
+
- Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
|
|
162
|
+
${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
|
|
163
|
+
|
|
164
|
+
Call place_order on any market you have edge on. $10 per trade. No cap.`;
|
|
165
|
+
const msg = JSON.stringify({
|
|
166
|
+
type: 'user',
|
|
167
|
+
message: { role: 'user', content: prompt }
|
|
168
|
+
});
|
|
169
|
+
if (this.ready) {
|
|
170
|
+
this.sendRaw(msg);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
this.queue.push(msg);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async onAutonomousScan(markets) {
|
|
177
|
+
const scanId = Math.random().toString(36).slice(2);
|
|
178
|
+
const prefix = this.config.redisPrefix;
|
|
179
|
+
await this.redis.lpush(`${prefix}:scans`, JSON.stringify({ scanId, markets_count: markets.length, ts: Date.now() }));
|
|
180
|
+
await this.redis.ltrim(`${prefix}:scans`, 0, 9999);
|
|
181
|
+
const tradeable = markets.filter(m => m.tokens?.some(t => t.price > 0.02 && t.price < 0.98));
|
|
182
|
+
const prompt = `## Autonomous Scan ${scanId}
|
|
183
|
+
|
|
184
|
+
Time: ${new Date().toISOString()}
|
|
185
|
+
|
|
186
|
+
## High-Quality Markets (${tradeable.length} found — vol24h>$50k, liq>$50k, price 0.10-0.90)
|
|
187
|
+
|
|
188
|
+
${tradeable.map((m, i) => `### ${i + 1}. ${m.question}
|
|
189
|
+
- Volume 24h: $${(m.volume24hr || 0).toLocaleString()}
|
|
190
|
+
- Liquidity: $${(m.liquidity || 0).toLocaleString()}
|
|
191
|
+
- Market ID: ${m.id}
|
|
192
|
+
- Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
|
|
193
|
+
${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
|
|
194
|
+
|
|
195
|
+
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.`;
|
|
196
|
+
const msg = JSON.stringify({
|
|
197
|
+
type: 'user',
|
|
198
|
+
message: { role: 'user', content: prompt }
|
|
199
|
+
});
|
|
200
|
+
if (this.ready) {
|
|
201
|
+
this.sendRaw(msg);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this.queue.push(msg);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async onPositionReview(positions) {
|
|
208
|
+
const prefix = this.config.redisPrefix;
|
|
209
|
+
await this.redis.lpush(`${prefix}:reviews`, JSON.stringify({ ts: Date.now(), positions_count: positions.length }));
|
|
210
|
+
await this.redis.ltrim(`${prefix}:reviews`, 0, 9999);
|
|
211
|
+
const positionLines = positions.map(p => {
|
|
212
|
+
const gainPct = p.current_price != null && p.price > 0
|
|
213
|
+
? (((p.current_price - p.price) / p.price) * 100).toFixed(1)
|
|
214
|
+
: 'N/A';
|
|
215
|
+
return `### ${p.market_id} ${p.market_question}
|
|
216
|
+
- Side: ${p.side} ${p.outcome} | Entry: ${p.price} | Now: ${p.current_price ?? 'N/A'} | Gain: ${gainPct}%
|
|
217
|
+
- Exit trigger: ${p.exit_trigger || '(none set)'}
|
|
218
|
+
- Size: $${p.size_usdc} | Hours to expiry: ${p.hours_to_expiry ?? 'N/A'}h`;
|
|
219
|
+
}).join('\n\n');
|
|
220
|
+
const prompt = `## Position Review — ${new Date().toISOString()}
|
|
221
|
+
|
|
222
|
+
Review each open position against its stated exit trigger. Call close_position for any position that has hit its exit condition.
|
|
223
|
+
|
|
224
|
+
${positionLines}
|
|
225
|
+
|
|
226
|
+
For each position that has reached its exit_trigger condition: call close_position immediately.
|
|
227
|
+
For positions still within bounds: do nothing (no output needed).`;
|
|
228
|
+
const msg = JSON.stringify({
|
|
229
|
+
type: 'user',
|
|
230
|
+
message: { role: 'user', content: prompt }
|
|
231
|
+
});
|
|
232
|
+
if (this.ready) {
|
|
233
|
+
this.sendRaw(msg);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
this.queue.push(msg);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async log(type, data) {
|
|
240
|
+
const prefix = this.config.redisPrefix;
|
|
241
|
+
const entry = JSON.stringify({ type, data, ts: Date.now(), model: this.config.label });
|
|
242
|
+
await this.redis.lpush(`${prefix}:log`, entry);
|
|
243
|
+
await this.redis.ltrim(`${prefix}:log`, 0, 9999);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
exports.ClaudeTrader = ClaudeTrader;
|
|
247
|
+
//# sourceMappingURL=claude-trader.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const coinbase_ws_1 = require("./signals/coinbase-ws");
|
|
37
|
+
const gamma_1 = require("./markets/gamma");
|
|
38
|
+
const claude_trader_1 = require("./claude-trader");
|
|
39
|
+
const position_monitor_1 = require("./position-monitor");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const CWD = process.env.POLLY_CWD || path.join(os.homedir(), 'polly-gamba');
|
|
43
|
+
async function main() {
|
|
44
|
+
console.log('[polly-gamba] Starting paper trading service');
|
|
45
|
+
console.log(`[polly-gamba] Claude cwd: ${CWD}`);
|
|
46
|
+
const anthropicTrader = new claude_trader_1.ClaudeTrader({ cwd: CWD, redisPrefix: 'polly', label: 'anthropic' });
|
|
47
|
+
const ollamaTrader = new claude_trader_1.ClaudeTrader({
|
|
48
|
+
cwd: CWD,
|
|
49
|
+
redisPrefix: 'polly:ollama',
|
|
50
|
+
label: 'ollama',
|
|
51
|
+
model: 'glm-4.7-flash',
|
|
52
|
+
envOverrides: {
|
|
53
|
+
ANTHROPIC_BASE_URL: 'http://localhost:11434',
|
|
54
|
+
ANTHROPIC_AUTH_TOKEN: 'ollama',
|
|
55
|
+
ANTHROPIC_API_KEY: 'ollama',
|
|
56
|
+
USER_TYPE: 'ant',
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const signals = new coinbase_ws_1.CoinbaseSignalDetector();
|
|
60
|
+
const monitor = new position_monitor_1.PositionMonitor(process.env.REDIS_URL || 'redis://localhost:6379', 'polly');
|
|
61
|
+
anthropicTrader.start();
|
|
62
|
+
ollamaTrader.start();
|
|
63
|
+
signals.on('signal', (signal) => {
|
|
64
|
+
console.log(`[signal] ${signal.asset} ${signal.direction} ${(Math.abs(signal.pct_change) * 100).toFixed(2)}% @ $${signal.price.toLocaleString()}`);
|
|
65
|
+
handleSignal(signal, anthropicTrader, ollamaTrader).catch((e) => {
|
|
66
|
+
console.error('[error] signal handling failed:', e.message);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
signals.start();
|
|
70
|
+
console.log('[polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...');
|
|
71
|
+
// Position monitor: check every 15 minutes for exits
|
|
72
|
+
const SCAN_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
|
73
|
+
monitor.checkPositions().catch((e) => console.error('[monitor]', e.message));
|
|
74
|
+
setInterval(() => {
|
|
75
|
+
monitor.checkPositions().catch((e) => console.error('[monitor]', e.message));
|
|
76
|
+
}, SCAN_INTERVAL_MS);
|
|
77
|
+
// Autonomous scan on startup and every 15 minutes
|
|
78
|
+
autonomousScan(anthropicTrader, ollamaTrader).catch((e) => {
|
|
79
|
+
console.error('[error] autonomous scan failed:', e.message);
|
|
80
|
+
});
|
|
81
|
+
setInterval(() => {
|
|
82
|
+
autonomousScan(anthropicTrader, ollamaTrader).catch((e) => {
|
|
83
|
+
console.error('[error] autonomous scan failed:', e.message);
|
|
84
|
+
});
|
|
85
|
+
}, SCAN_INTERVAL_MS);
|
|
86
|
+
process.on('SIGINT', () => {
|
|
87
|
+
console.log('\n[polly-gamba] Shutting down...');
|
|
88
|
+
signals.stop();
|
|
89
|
+
monitor.stop();
|
|
90
|
+
process.exit(0);
|
|
91
|
+
});
|
|
92
|
+
process.on('SIGTERM', () => {
|
|
93
|
+
signals.stop();
|
|
94
|
+
monitor.stop();
|
|
95
|
+
process.exit(0);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function autonomousScan(...traders) {
|
|
99
|
+
console.log('[scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)');
|
|
100
|
+
let markets;
|
|
101
|
+
try {
|
|
102
|
+
markets = await (0, gamma_1.fetchHighQualityMarkets)();
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
console.error('[scan] failed to fetch markets:', e.message);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(`[scan] ${markets.length} high-quality markets for autonomous review`);
|
|
109
|
+
await Promise.all(traders.map(t => t.onAutonomousScan(markets)));
|
|
110
|
+
}
|
|
111
|
+
async function handleSignal(signal, ...traders) {
|
|
112
|
+
let markets;
|
|
113
|
+
try {
|
|
114
|
+
markets = await (0, gamma_1.fetchActiveMarkets)(10_000, 1_000);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
console.error('[error] failed to fetch markets:', e.message);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const relevant = (0, gamma_1.filterBySignal)(signal.asset, markets);
|
|
121
|
+
console.log(`[match] ${relevant.length} markets for ${signal.asset} signal`);
|
|
122
|
+
if (relevant.length === 0) {
|
|
123
|
+
console.log('[match] no relevant markets found');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await Promise.all(traders.map(t => t.onSignal(signal, relevant)));
|
|
127
|
+
}
|
|
128
|
+
main().catch((err) => {
|
|
129
|
+
console.error('[polly-gamba] Fatal:', err);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
|
132
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface OrderbookLevel {
|
|
2
|
+
price: string;
|
|
3
|
+
size: string;
|
|
4
|
+
}
|
|
5
|
+
export interface Orderbook {
|
|
6
|
+
market: string;
|
|
7
|
+
asset_id: string;
|
|
8
|
+
bids: OrderbookLevel[];
|
|
9
|
+
asks: OrderbookLevel[];
|
|
10
|
+
spread?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface PlaceOrderParams {
|
|
13
|
+
token_id: string;
|
|
14
|
+
side: 'BUY' | 'SELL';
|
|
15
|
+
size_usdc: number;
|
|
16
|
+
price: number;
|
|
17
|
+
dry_run?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface OrderResult {
|
|
20
|
+
dry_run: boolean;
|
|
21
|
+
token_id: string;
|
|
22
|
+
side: 'BUY' | 'SELL';
|
|
23
|
+
size_usdc: number;
|
|
24
|
+
price: number;
|
|
25
|
+
estimated_shares: number;
|
|
26
|
+
order_id?: string;
|
|
27
|
+
logged_at: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function getOrderbook(tokenId: string): Promise<Orderbook>;
|
|
30
|
+
export declare function getPrice(tokenId: string, side: 'buy' | 'sell'): Promise<number>;
|
|
31
|
+
export declare function getPrices(tokenIds: string[]): Promise<Map<string, number>>;
|
|
32
|
+
export declare function placeOrder(params: PlaceOrderParams): Promise<OrderResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getOrderbook = getOrderbook;
|
|
4
|
+
exports.getPrice = getPrice;
|
|
5
|
+
exports.getPrices = getPrices;
|
|
6
|
+
exports.placeOrder = placeOrder;
|
|
7
|
+
const CLOB_BASE = 'https://clob.polymarket.com';
|
|
8
|
+
async function getOrderbook(tokenId) {
|
|
9
|
+
const url = `${CLOB_BASE}/book?token_id=${encodeURIComponent(tokenId)}`;
|
|
10
|
+
const res = await fetch(url);
|
|
11
|
+
if (!res.ok) {
|
|
12
|
+
throw new Error(`CLOB getOrderbook error: ${res.status} ${res.statusText}`);
|
|
13
|
+
}
|
|
14
|
+
const data = await res.json();
|
|
15
|
+
const bids = (data.bids ?? []).slice(0, 10);
|
|
16
|
+
const asks = (data.asks ?? []).slice(0, 10);
|
|
17
|
+
let spread;
|
|
18
|
+
if (bids.length > 0 && asks.length > 0) {
|
|
19
|
+
spread = parseFloat(asks[0].price) - parseFloat(bids[0].price);
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
market: data.market ?? '',
|
|
23
|
+
asset_id: data.asset_id ?? tokenId,
|
|
24
|
+
bids,
|
|
25
|
+
asks,
|
|
26
|
+
spread,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async function getPrice(tokenId, side) {
|
|
30
|
+
const url = `${CLOB_BASE}/price?token_id=${encodeURIComponent(tokenId)}&side=${side}`;
|
|
31
|
+
const res = await fetch(url);
|
|
32
|
+
if (!res.ok) {
|
|
33
|
+
throw new Error(`CLOB getPrice error: ${res.status} ${res.statusText}`);
|
|
34
|
+
}
|
|
35
|
+
const data = await res.json();
|
|
36
|
+
return parseFloat(data.price ?? data) ?? 0;
|
|
37
|
+
}
|
|
38
|
+
async function getPrices(tokenIds) {
|
|
39
|
+
const prices = new Map();
|
|
40
|
+
// Batch with concurrency limit of 5
|
|
41
|
+
const chunks = [];
|
|
42
|
+
for (let i = 0; i < tokenIds.length; i += 5) {
|
|
43
|
+
chunks.push(tokenIds.slice(i, i + 5));
|
|
44
|
+
}
|
|
45
|
+
for (const chunk of chunks) {
|
|
46
|
+
await Promise.all(chunk.map(async (id) => {
|
|
47
|
+
try {
|
|
48
|
+
const price = await getPrice(id, 'buy');
|
|
49
|
+
prices.set(id, price);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
// skip failed lookups
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
return prices;
|
|
57
|
+
}
|
|
58
|
+
async function placeOrder(params) {
|
|
59
|
+
const { token_id, side, size_usdc, price, dry_run = true } = params;
|
|
60
|
+
const estimated_shares = price > 0 ? size_usdc / price : 0;
|
|
61
|
+
const result = {
|
|
62
|
+
dry_run,
|
|
63
|
+
token_id,
|
|
64
|
+
side,
|
|
65
|
+
size_usdc,
|
|
66
|
+
price,
|
|
67
|
+
estimated_shares,
|
|
68
|
+
logged_at: Date.now(),
|
|
69
|
+
};
|
|
70
|
+
if (dry_run) {
|
|
71
|
+
console.log(`[clob/dry-run] WOULD PLACE ORDER: ${side} ${size_usdc} USDC @ ${price} ` +
|
|
72
|
+
`(~${estimated_shares.toFixed(2)} shares) token=${token_id}`);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
// Real order placement — requires CLOB auth (L1/L2 headers)
|
|
76
|
+
// Auth implementation deferred — set POLYMARKET_KEY env var when ready
|
|
77
|
+
const apiKey = process.env.POLYMARKET_API_KEY;
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
throw new Error('POLYMARKET_API_KEY not set — cannot place real orders');
|
|
80
|
+
}
|
|
81
|
+
// TODO: implement full CLOB auth (EIP-712 L1 + L2 API key headers)
|
|
82
|
+
// See: https://docs.polymarket.com/#authentication
|
|
83
|
+
throw new Error('Real order placement not yet implemented — use dry_run=true');
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=clob.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface MarketToken {
|
|
2
|
+
token_id: string;
|
|
3
|
+
outcome: string;
|
|
4
|
+
price: number;
|
|
5
|
+
}
|
|
6
|
+
export interface Market {
|
|
7
|
+
id: string;
|
|
8
|
+
conditionId: string;
|
|
9
|
+
question: string;
|
|
10
|
+
description: string;
|
|
11
|
+
volume: number;
|
|
12
|
+
volume24hr: number;
|
|
13
|
+
liquidity: number;
|
|
14
|
+
tokens: MarketToken[];
|
|
15
|
+
active: boolean;
|
|
16
|
+
closed: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function fetchActiveMarkets(minVolume?: number, minLiquidity?: number): Promise<Market[]>;
|
|
19
|
+
export declare function fetchHighQualityMarkets(minVolume24h?: number, minLiquidity?: number, minPrice?: number, maxPrice?: number): Promise<Market[]>;
|
|
20
|
+
export declare function fetchMarket(id: string): Promise<Market | null>;
|
|
21
|
+
export declare function clearCache(): void;
|
|
22
|
+
export declare function filterBySignal(asset: string, markets: Market[]): Market[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchActiveMarkets = fetchActiveMarkets;
|
|
4
|
+
exports.fetchHighQualityMarkets = fetchHighQualityMarkets;
|
|
5
|
+
exports.fetchMarket = fetchMarket;
|
|
6
|
+
exports.clearCache = clearCache;
|
|
7
|
+
exports.filterBySignal = filterBySignal;
|
|
8
|
+
const GAMMA_BASE = 'https://gamma-api.polymarket.com';
|
|
9
|
+
const CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
10
|
+
let cache = null;
|
|
11
|
+
function parseNumber(val) {
|
|
12
|
+
if (typeof val === 'number')
|
|
13
|
+
return val;
|
|
14
|
+
if (typeof val === 'string')
|
|
15
|
+
return parseFloat(val) || 0;
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
function parseTokens(raw) {
|
|
19
|
+
if (!Array.isArray(raw))
|
|
20
|
+
return [];
|
|
21
|
+
return raw.map((t) => ({
|
|
22
|
+
token_id: t.token_id ?? t.tokenId ?? '',
|
|
23
|
+
outcome: t.outcome ?? '',
|
|
24
|
+
price: parseNumber(t.price),
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
function parseMarket(raw) {
|
|
28
|
+
// Gamma API returns outcomes/prices as JSON strings, not a tokens array
|
|
29
|
+
let tokens = parseTokens(raw.tokens);
|
|
30
|
+
if (tokens.length === 0 && raw.outcomes) {
|
|
31
|
+
try {
|
|
32
|
+
const outcomes = JSON.parse(raw.outcomes);
|
|
33
|
+
const prices = JSON.parse(raw.outcomePrices || '[]');
|
|
34
|
+
tokens = outcomes.map((outcome, i) => ({
|
|
35
|
+
token_id: '',
|
|
36
|
+
outcome,
|
|
37
|
+
price: parseFloat(prices[i]) || 0,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
catch { }
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
id: String(raw.id ?? ''),
|
|
44
|
+
conditionId: String(raw.conditionId ?? raw.condition_id ?? ''),
|
|
45
|
+
question: String(raw.question ?? ''),
|
|
46
|
+
description: String(raw.description ?? ''),
|
|
47
|
+
volume: parseNumber(raw.volume),
|
|
48
|
+
volume24hr: parseNumber(raw.volume24hr),
|
|
49
|
+
liquidity: parseNumber(raw.liquidity),
|
|
50
|
+
tokens,
|
|
51
|
+
active: Boolean(raw.active),
|
|
52
|
+
closed: Boolean(raw.closed),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function fetchActiveMarkets(minVolume = 10_000, minLiquidity = 1_000) {
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
if (cache && now - cache.ts < CACHE_TTL_MS) {
|
|
58
|
+
return cache.markets;
|
|
59
|
+
}
|
|
60
|
+
const url = `${GAMMA_BASE}/markets?active=true&closed=false&limit=500`;
|
|
61
|
+
console.log(`[gamma] Fetching active markets from ${url}`);
|
|
62
|
+
const res = await fetch(url);
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`Gamma API error: ${res.status} ${res.statusText}`);
|
|
65
|
+
}
|
|
66
|
+
const raw = await res.json();
|
|
67
|
+
const data = Array.isArray(raw) ? raw : [];
|
|
68
|
+
const markets = data
|
|
69
|
+
.map(parseMarket)
|
|
70
|
+
.filter(m => m.active && !m.closed && m.volume >= minVolume && m.liquidity >= minLiquidity);
|
|
71
|
+
console.log(`[gamma] Loaded ${markets.length} markets (filtered from ${data.length})`);
|
|
72
|
+
cache = { markets, ts: now };
|
|
73
|
+
return markets;
|
|
74
|
+
}
|
|
75
|
+
async function fetchHighQualityMarkets(minVolume24h = 50_000, minLiquidity = 50_000, minPrice = 0.10, maxPrice = 0.90) {
|
|
76
|
+
const all = await fetchActiveMarkets(1_000, 1_000);
|
|
77
|
+
return all
|
|
78
|
+
.filter(m => {
|
|
79
|
+
const vol24 = m.volume24hr ?? 0;
|
|
80
|
+
const liq = m.liquidity ?? 0;
|
|
81
|
+
const hasUncertainty = m.tokens?.some(t => t.price >= minPrice && t.price <= maxPrice);
|
|
82
|
+
return vol24 >= minVolume24h && liq >= minLiquidity && hasUncertainty;
|
|
83
|
+
})
|
|
84
|
+
.sort((a, b) => (b.volume24hr ?? 0) - (a.volume24hr ?? 0));
|
|
85
|
+
}
|
|
86
|
+
async function fetchMarket(id) {
|
|
87
|
+
const url = `${GAMMA_BASE}/markets/${id}`;
|
|
88
|
+
const res = await fetch(url);
|
|
89
|
+
if (!res.ok)
|
|
90
|
+
return null;
|
|
91
|
+
const data = await res.json();
|
|
92
|
+
return parseMarket(data);
|
|
93
|
+
}
|
|
94
|
+
function clearCache() {
|
|
95
|
+
cache = null;
|
|
96
|
+
}
|
|
97
|
+
const KEYWORDS = {
|
|
98
|
+
BTC: ['bitcoin', 'btc', 'crypto', 'cryptocurrency'],
|
|
99
|
+
ETH: ['ethereum', 'eth', 'ether', 'defi'],
|
|
100
|
+
};
|
|
101
|
+
function filterBySignal(asset, markets) {
|
|
102
|
+
const kw = KEYWORDS[asset] || [];
|
|
103
|
+
const scored = markets
|
|
104
|
+
.map(m => {
|
|
105
|
+
const text = `${m.question} ${m.description || ''}`.toLowerCase();
|
|
106
|
+
const score = kw.reduce((s, k) => s + (text.includes(k) ? 1 : 0), 0);
|
|
107
|
+
return { m, score };
|
|
108
|
+
})
|
|
109
|
+
.filter(x => x.score > 0)
|
|
110
|
+
.sort((a, b) => b.score - a.score || b.m.volume - a.m.volume);
|
|
111
|
+
return scored.slice(0, 20).map(x => x.m);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=gamma.js.map
|