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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-gamba",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Coinbase price signal → Claude brain → Polymarket CLOB execution",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -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 position = { ...a, ts, status: 'open', pnl: null }
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