polly-gamba 1.0.2 → 1.0.3
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/package.json +1 -1
- package/src/claude-trader.ts +56 -2
- package/src/mcp-server.ts +118 -4
package/package.json
CHANGED
package/src/claude-trader.ts
CHANGED
|
@@ -112,7 +112,7 @@ export class ClaudeTrader {
|
|
|
112
112
|
role: 'user',
|
|
113
113
|
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.
|
|
114
114
|
|
|
115
|
-
TOOLS: place_order, skip_all
|
|
115
|
+
TOOLS: place_order, skip_all, get_budget_status
|
|
116
116
|
RULES:
|
|
117
117
|
- Output ONLY tool calls. Zero prose.
|
|
118
118
|
- For EVERY market in the list: if current price differs from your estimated fair probability by more than 5%, place a trade.
|
|
@@ -120,7 +120,13 @@ RULES:
|
|
|
120
120
|
- $10 USDC per trade (small size, many bets).
|
|
121
121
|
- NO cap on number of trades — bet every market where you see any edge.
|
|
122
122
|
- Only skip_all if you genuinely have zero opinion on any market (rare).
|
|
123
|
-
- Use your world knowledge: sports standings, political context, recent events, base rates
|
|
123
|
+
- Use your world knowledge: sports standings, political context, recent events, base rates.
|
|
124
|
+
|
|
125
|
+
## POSITION DISCIPLINE:
|
|
126
|
+
- Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
|
|
127
|
+
- 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.
|
|
128
|
+
- 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."
|
|
129
|
+
- Call get_budget_status at the start of each scan to know available capital.`
|
|
124
130
|
}
|
|
125
131
|
}))
|
|
126
132
|
this.ready = true
|
|
@@ -227,6 +233,54 @@ For EVERY market above: if price differs from your fair probability by >5%, plac
|
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
235
|
|
|
236
|
+
async onPositionReview(positions: Array<{
|
|
237
|
+
market_id: string
|
|
238
|
+
market_question: string
|
|
239
|
+
outcome: string
|
|
240
|
+
side: string
|
|
241
|
+
price: number
|
|
242
|
+
size_usdc: number
|
|
243
|
+
exit_trigger?: string
|
|
244
|
+
ts: number
|
|
245
|
+
current_price?: number
|
|
246
|
+
hours_to_expiry?: number
|
|
247
|
+
}>) {
|
|
248
|
+
const prefix = this.config.redisPrefix
|
|
249
|
+
|
|
250
|
+
await this.redis.lpush(`${prefix}:reviews`, JSON.stringify({ ts: Date.now(), positions_count: positions.length }))
|
|
251
|
+
await this.redis.ltrim(`${prefix}:reviews`, 0, 9999)
|
|
252
|
+
|
|
253
|
+
const positionLines = positions.map(p => {
|
|
254
|
+
const gainPct = p.current_price != null && p.price > 0
|
|
255
|
+
? (((p.current_price - p.price) / p.price) * 100).toFixed(1)
|
|
256
|
+
: 'N/A'
|
|
257
|
+
return `### ${p.market_id} ${p.market_question}
|
|
258
|
+
- Side: ${p.side} ${p.outcome} | Entry: ${p.price} | Now: ${p.current_price ?? 'N/A'} | Gain: ${gainPct}%
|
|
259
|
+
- Exit trigger: ${p.exit_trigger || '(none set)'}
|
|
260
|
+
- Size: $${p.size_usdc} | Hours to expiry: ${p.hours_to_expiry ?? 'N/A'}h`
|
|
261
|
+
}).join('\n\n')
|
|
262
|
+
|
|
263
|
+
const prompt = `## Position Review — ${new Date().toISOString()}
|
|
264
|
+
|
|
265
|
+
Review each open position against its stated exit trigger. Call close_position for any position that has hit its exit condition.
|
|
266
|
+
|
|
267
|
+
${positionLines}
|
|
268
|
+
|
|
269
|
+
For each position that has reached its exit_trigger condition: call close_position immediately.
|
|
270
|
+
For positions still within bounds: do nothing (no output needed).`
|
|
271
|
+
|
|
272
|
+
const msg = JSON.stringify({
|
|
273
|
+
type: 'user',
|
|
274
|
+
message: { role: 'user', content: prompt }
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
if (this.ready) {
|
|
278
|
+
this.sendRaw(msg)
|
|
279
|
+
} else {
|
|
280
|
+
this.queue.push(msg)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
230
284
|
private async log(type: string, data: any) {
|
|
231
285
|
const prefix = this.config.redisPrefix
|
|
232
286
|
const entry = JSON.stringify({ type, data, ts: Date.now(), model: this.config.label })
|
package/src/mcp-server.ts
CHANGED
|
@@ -33,9 +33,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
33
33
|
side: { type: 'string', enum: ['BUY', 'SELL'] },
|
|
34
34
|
size_usdc: { type: 'number', description: 'Size in USDC (virtual)' },
|
|
35
35
|
price: { type: 'number', description: 'Current price (0-1)' },
|
|
36
|
-
reasoning: { type: 'string', description: 'Why you are making this trade' }
|
|
36
|
+
reasoning: { type: 'string', description: 'Why you are making this trade' },
|
|
37
|
+
exit_trigger: { type: 'string', description: 'Specific condition that would cause an exit (e.g. "price hits 0.25 or Wemby drops off MVP ladder top 3")' },
|
|
38
|
+
new_catalyst: { type: 'string', description: 'Required if a position already exists for this market_id. Must be specific new information published in last 24h — not price movement.' }
|
|
37
39
|
},
|
|
38
|
-
required: ['market_id', 'market_question', 'outcome', 'side', 'size_usdc', 'price', 'reasoning']
|
|
40
|
+
required: ['market_id', 'market_question', 'outcome', 'side', 'size_usdc', 'price', 'reasoning', 'exit_trigger']
|
|
39
41
|
}
|
|
40
42
|
},
|
|
41
43
|
{
|
|
@@ -93,6 +95,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
93
95
|
properties: {},
|
|
94
96
|
required: []
|
|
95
97
|
}
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'get_budget_status',
|
|
101
|
+
description: 'Returns current $500 paper budget status: deployed capital, available capital, and per-market breakdown.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {},
|
|
105
|
+
required: []
|
|
106
|
+
}
|
|
96
107
|
}
|
|
97
108
|
]
|
|
98
109
|
}))
|
|
@@ -103,7 +114,68 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
103
114
|
|
|
104
115
|
if (name === 'place_order') {
|
|
105
116
|
const a = args as any
|
|
106
|
-
const
|
|
117
|
+
const TOTAL_BUDGET = 500
|
|
118
|
+
const MARKET_CAP = 100
|
|
119
|
+
|
|
120
|
+
// Load all open positions for enforcement checks
|
|
121
|
+
const rawPositions = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1)
|
|
122
|
+
const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`))
|
|
123
|
+
const openPositions = rawPositions.map(r => {
|
|
124
|
+
try { return JSON.parse(r) } catch { return null }
|
|
125
|
+
}).filter(Boolean).filter((p: any) => {
|
|
126
|
+
if (p.status === 'closed') return false
|
|
127
|
+
return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
// Check 1: Existing position for same market_id requires new_catalyst
|
|
131
|
+
const sameMarketPositions = openPositions.filter((p: any) => p.market_id === a.market_id)
|
|
132
|
+
if (sameMarketPositions.length > 0 && !a.new_catalyst) {
|
|
133
|
+
return {
|
|
134
|
+
content: [{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`
|
|
137
|
+
}]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check 2: $500 total budget cap
|
|
142
|
+
const totalDeployed = openPositions.reduce((s: number, p: any) => s + (p.size_usdc || 0), 0)
|
|
143
|
+
if (totalDeployed + a.size_usdc > TOTAL_BUDGET) {
|
|
144
|
+
const available = Math.max(0, TOTAL_BUDGET - totalDeployed)
|
|
145
|
+
return {
|
|
146
|
+
content: [{
|
|
147
|
+
type: 'text',
|
|
148
|
+
text: `Error: Budget cap reached. $500 paper budget. Currently deployed: $${totalDeployed.toFixed(2)}. Available: $${available.toFixed(2)}.`
|
|
149
|
+
}]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check 3: $100 per-market concentration cap
|
|
154
|
+
const marketDeployed = sameMarketPositions.reduce((s: number, p: any) => s + (p.size_usdc || 0), 0)
|
|
155
|
+
if (marketDeployed + a.size_usdc > MARKET_CAP) {
|
|
156
|
+
return {
|
|
157
|
+
content: [{
|
|
158
|
+
type: 'text',
|
|
159
|
+
text: `Error: Single-market cap: max $100 per market (20% of $500 budget). Already deployed $${marketDeployed.toFixed(2)} in this market.`
|
|
160
|
+
}]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const position = {
|
|
165
|
+
market_id: a.market_id,
|
|
166
|
+
market_question: a.market_question,
|
|
167
|
+
token_id: a.token_id,
|
|
168
|
+
outcome: a.outcome,
|
|
169
|
+
side: a.side,
|
|
170
|
+
size_usdc: a.size_usdc,
|
|
171
|
+
price: a.price,
|
|
172
|
+
reasoning: a.reasoning,
|
|
173
|
+
exit_trigger: a.exit_trigger,
|
|
174
|
+
new_catalyst: a.new_catalyst || null,
|
|
175
|
+
ts,
|
|
176
|
+
status: 'open',
|
|
177
|
+
pnl: null,
|
|
178
|
+
}
|
|
107
179
|
await redis.lpush(`${REDIS_PREFIX}:positions`, JSON.stringify(position))
|
|
108
180
|
await redis.ltrim(`${REDIS_PREFIX}:positions`, 0, 9999)
|
|
109
181
|
await redis.publish(`${REDIS_PREFIX}:events`, JSON.stringify({ type: 'trade', ...position }))
|
|
@@ -111,7 +183,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
111
183
|
return {
|
|
112
184
|
content: [{
|
|
113
185
|
type: 'text',
|
|
114
|
-
text: `Paper trade logged: ${a.side} ${a.outcome} on "${a.market_question}" @ ${a.price} for $${a.size_usdc} USDC. Position recorded in Redis.`
|
|
186
|
+
text: `Paper trade logged: ${a.side} ${a.outcome} on "${a.market_question}" @ ${a.price} for $${a.size_usdc} USDC. Exit trigger: ${a.exit_trigger}. Position recorded in Redis.`
|
|
115
187
|
}]
|
|
116
188
|
}
|
|
117
189
|
}
|
|
@@ -218,6 +290,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
218
290
|
byReason[r] = (byReason[r] || 0) + 1
|
|
219
291
|
}
|
|
220
292
|
|
|
293
|
+
const TOTAL_BUDGET = 500
|
|
221
294
|
const summary = {
|
|
222
295
|
open_positions: openPositions.length,
|
|
223
296
|
closed_positions: closedPositions.length,
|
|
@@ -225,11 +298,52 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
225
298
|
total_realized_pnl: Math.round(totalRealized * 100) / 100,
|
|
226
299
|
win_rate: Math.round(winRate * 100) / 100,
|
|
227
300
|
by_reason: byReason,
|
|
301
|
+
budget_total: TOTAL_BUDGET,
|
|
302
|
+
budget_deployed: Math.round(totalInvested * 100) / 100,
|
|
303
|
+
budget_available: Math.round((TOTAL_BUDGET - totalInvested) * 100) / 100,
|
|
228
304
|
}
|
|
229
305
|
|
|
230
306
|
return { content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }] }
|
|
231
307
|
}
|
|
232
308
|
|
|
309
|
+
if (name === 'get_budget_status') {
|
|
310
|
+
const TOTAL_BUDGET = 500
|
|
311
|
+
const rawPositions = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1)
|
|
312
|
+
const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`))
|
|
313
|
+
const openPositions = rawPositions.map(r => {
|
|
314
|
+
try { return JSON.parse(r) } catch { return null }
|
|
315
|
+
}).filter(Boolean).filter((p: any) => {
|
|
316
|
+
if (p.status === 'closed') return false
|
|
317
|
+
return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`)
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const deployed = openPositions.reduce((s: number, p: any) => s + (p.size_usdc || 0), 0)
|
|
321
|
+
|
|
322
|
+
// Per-market breakdown
|
|
323
|
+
const perMarket: Record<string, { market_question: string; total_deployed: number }> = {}
|
|
324
|
+
for (const p of openPositions as any[]) {
|
|
325
|
+
if (!perMarket[p.market_id]) {
|
|
326
|
+
perMarket[p.market_id] = { market_question: p.market_question || p.market_id, total_deployed: 0 }
|
|
327
|
+
}
|
|
328
|
+
perMarket[p.market_id].total_deployed += p.size_usdc || 0
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Round per-market values
|
|
332
|
+
for (const key of Object.keys(perMarket)) {
|
|
333
|
+
perMarket[key].total_deployed = Math.round(perMarket[key].total_deployed * 100) / 100
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const status = {
|
|
337
|
+
total_budget: TOTAL_BUDGET,
|
|
338
|
+
deployed: Math.round(deployed * 100) / 100,
|
|
339
|
+
available: Math.round((TOTAL_BUDGET - deployed) * 100) / 100,
|
|
340
|
+
positions_count: openPositions.length,
|
|
341
|
+
per_market: perMarket,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }
|
|
345
|
+
}
|
|
346
|
+
|
|
233
347
|
throw new Error(`Unknown tool: ${name}`)
|
|
234
348
|
})
|
|
235
349
|
|