polly-gamba 1.0.27 → 1.0.32

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.
@@ -103,7 +103,7 @@ class ClaudeTrader {
103
103
  role: 'user',
104
104
  content: `You are a Polymarket ${PAPER_MODE ? 'PAPER TRADING (no real money)' : '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.
105
105
  ${PAPER_MODE ? '\n⚠️ PAPER MODE: This is a test environment. Be AGGRESSIVE — trade more, skip less. Lower your threshold to 3% edge for any market resolving within 6 months.\n' : ''}
106
- TOOLS: place_order, skip_all, get_budget_status
106
+ TOOLS: place_order, skip_all, get_budget_status, get_positions
107
107
  RULES:
108
108
  - Output ONLY tool calls. Zero prose.
109
109
  - For EVERY market in the list: if current price differs from your estimated fair probability by more than ${PAPER_MODE ? '3%' : '5%'}, place a trade.
@@ -117,7 +117,7 @@ RULES:
117
117
  - Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
118
118
  - To add to an existing position with the SAME outcome: (1) you MUST cite a specific new catalyst (news published in last 24h, not price movement), AND (2) the current price must be ≥20% below your last entry price (e.g. if you bought YES at 0.15, new entry only valid at ≤0.12). The MCP enforces this — attempts at a smaller discount will be rejected. Price dipping alone is NOT a catalyst. New information is a catalyst.
119
119
  - 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."
120
- - Call get_budget_status at the start of each scan to know available capital.`
120
+ - Call get_budget_status AND get_positions at the start of each scan to know available capital and your current open positions. get_positions returns {"positions": [...], "closed_markets": [...]}. You MUST review BOTH before trading to apply concentration AND closed-market discipline correctly.`
121
121
  }
122
122
  }));
123
123
  this.ready = true;
@@ -213,7 +213,21 @@ ${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('
213
213
  - Similarly: "Will Orbán be PM" and "Will Magyar be PM" are both the Hungary 2026 election — treat as same theme (you may hold both as a hedge pair, but do not add a third Hungary position).
214
214
  - If you're uncertain whether a market correlates to an existing theme, assume it does and skip.
215
215
 
216
- For EVERY market above: apply horizon discipline AND concentration discipline first, then if price differs from your fair probability by the required edge, place a trade ($10 USDC). Call skip_all only if you have zero opinion on all markets.`;
216
+ ## CLOSED MARKET DISCIPLINE (absolute rule never override):
217
+ - get_positions returns a JSON object with two keys: "positions" (open) and "closed_markets" (previously exited).
218
+ - NEVER place a new order for any market_id+outcome combination that appears in closed_markets. This is enforced server-side and will be rejected anyway.
219
+ - The reason: re-entering a market you already exited is a sign of anchoring bias. If the thesis was wrong once, sportsbook odds movement is not sufficient justification. Accept the loss and redeploy capital elsewhere.
220
+
221
+ ## SPORTSBOOK CROSS-CHECK DISCIPLINE (apply when estimating fair value):
222
+ - Sportsbooks measure betting DEMAND, not calibrated probability. Polymarket is often better calibrated on liquid markets (>$50k volume).
223
+ - When fair value is primarily derived from sportsbook lines (e.g. "DraftKings has X at +225 implying 30%"), apply a 50% haircut to the apparent cross-market edge. Example: sportsbooks imply 30%, Polymarket shows 15% → effective edge = (30%×0.8 − 15%) = 9pp, not 15pp.
224
+ - Sportsbook-derived estimates MUST exceed the horizon threshold AFTER the 50% haircut. Do not enter based purely on sportsbook arbitrage at small gaps.
225
+ - Anchor fair value on base rates and world knowledge first; use sportsbooks to adjust by ±5pp only. If you cannot defend the estimate without the sportsbook line, the edge is too speculative.
226
+ - Exception: if both Polymarket AND sportsbooks agree with your base-rate analysis, the convergence strengthens the thesis.
227
+
228
+ STEP 1: Call get_positions to see your current open positions AND closed_markets — required before evaluating any market.
229
+ STEP 2: For each market, apply horizon discipline, concentration discipline, AND closed market discipline (using the data from step 1).
230
+ STEP 3: If price differs from your fair probability by the required edge (accounting for sportsbook haircut if applicable), place a trade ($10 USDC). Call skip_all only if you have zero opinion on all markets.`;
217
231
  const msg = JSON.stringify({
218
232
  type: 'user',
219
233
  message: { role: 'user', content: prompt }
@@ -295,6 +309,8 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
295
309
  4. Position is down >50% — HARD CLOSE regardless of thesis. Cut losses. No exceptions.
296
310
  5. You have 2+ open positions in the SAME market with the SAME outcome — close the NEWER entry (higher ts value) regardless of P&L. Duplicate same-outcome positions double exposure without incremental edge. Keep the original entry.
297
311
  6. Position is UP >25% from entry AND has been held >48h — CLOSE to lock in profits. The market has moved significantly in your favor; gains are now vulnerable to reversion. Use take_profit reason. Exception: if the resolution event is within 7 days AND the position is still clearly on the right side, you may hold until resolution.
312
+ 7. STALE POSITION: Position held >7 days AND price has moved <5% from entry AND resolution event is >30 days away — CLOSE to redeploy capital. A position that doesn't move over 7 days means the mispricing wasn't as large as estimated. Cut it, accept the small loss/gain, and redeploy to markets with active price movement.
313
+ 8. CONCENTRATION VIOLATION: Count open positions by underlying theme (the single real-world event that resolves all of them). Examples: "Will Jesus return before GTA VI", "Will BTC hit $1M before GTA VI", "Will China invade Taiwan before GTA VI" all share the theme "GTA VI release date". Similarly "Will Spurs win NBA Finals", "Will OKC win NBA Finals", "Will Celtics win NBA Finals" all share the theme "2026 NBA Finals". If you have 3+ positions in the same theme, close the one with the smallest current edge (current price closest to 50/50 or furthest from your target) to reduce to 2 positions. Apply this BEFORE checking other rules.
298
314
 
299
315
  HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`;
300
316
  const msg = JSON.stringify({
@@ -126,6 +126,12 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
126
126
  return false;
127
127
  return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`);
128
128
  });
129
+ // Block re-entry into previously closed market+outcome combos
130
+ const marketOutcomePrefix = `${a.market_id}_${a.outcome}_`;
131
+ const wasClosedBefore = Array.from(closedIds).some(id => id.startsWith(marketOutcomePrefix));
132
+ if (wasClosedBefore) {
133
+ return { content: [{ type: 'text', text: `Error: Re-entry blocked. This market+outcome (${a.market_id} ${a.outcome}) was previously closed. Polly-gamba does not re-enter previously exited positions to prevent repeated losses on the same bet.` }] };
134
+ }
129
135
  // Validate using extracted business logic (includes input validation + budget checks)
130
136
  const validationError = (0, paper_trade_1.validatePlaceOrder)({ market_id: a.market_id, outcome: a.outcome, size_usdc: a.size_usdc, price: a.price, new_catalyst: a.new_catalyst }, openPositions);
131
137
  if (validationError) {
@@ -191,8 +197,22 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
191
197
  const key = `${p.market_id}_${p.outcome}_${p.ts}`;
192
198
  return !closedIds.has(key);
193
199
  });
200
+ // Expose previously-closed market+outcome combos so Claude can avoid re-entry
201
+ const closedMarkets = Array.from(closedIds).map(key => {
202
+ const parts = key.split('_');
203
+ return { market_id: parts[0], outcome: parts[1] };
204
+ });
205
+ // Deduplicate
206
+ const seen = new Set();
207
+ const uniqueClosedMarkets = closedMarkets.filter(m => {
208
+ const k = `${m.market_id}_${m.outcome}`;
209
+ if (seen.has(k))
210
+ return false;
211
+ seen.add(k);
212
+ return true;
213
+ });
194
214
  return {
195
- content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }]
215
+ content: [{ type: 'text', text: JSON.stringify({ positions, closed_markets: uniqueClosedMarkets }, null, 2) }]
196
216
  };
197
217
  }
198
218
  if (name === 'close_position') {
@@ -52,15 +52,23 @@ class PositionMonitor {
52
52
  return;
53
53
  }
54
54
  // Deduplicate market fetches keyed by marketId_outcome
55
+ // Throttle to 5 concurrent fetches to avoid Gamma API rate limiting
55
56
  const marketPriceCache = new Map();
56
57
  const uniqueKeys = [...new Set(toCheck.map(p => `${p.market_id}|${p.outcome}`))];
57
- await Promise.all(uniqueKeys.map(async (key) => {
58
- const sep = key.indexOf('|');
59
- const marketId = key.slice(0, sep);
60
- const outcome = key.slice(sep + 1);
61
- const result = await this.fetchCurrentPrice(marketId, outcome).catch(() => null);
62
- marketPriceCache.set(key, result);
63
- }));
58
+ const BATCH_SIZE = 5;
59
+ for (let i = 0; i < uniqueKeys.length; i += BATCH_SIZE) {
60
+ const batch = uniqueKeys.slice(i, i + BATCH_SIZE);
61
+ await Promise.all(batch.map(async (key) => {
62
+ const sep = key.indexOf('|');
63
+ const marketId = key.slice(0, sep);
64
+ const outcome = key.slice(sep + 1);
65
+ const result = await this.fetchCurrentPrice(marketId, outcome).catch(() => null);
66
+ marketPriceCache.set(key, result);
67
+ }));
68
+ if (i + BATCH_SIZE < uniqueKeys.length) {
69
+ await new Promise(resolve => setTimeout(resolve, 200));
70
+ }
71
+ }
64
72
  // Build review candidates with current price data for Claude
65
73
  const reviewCandidates = [];
66
74
  for (const pos of toCheck) {
@@ -72,8 +80,10 @@ class PositionMonitor {
72
80
  const entryPrice = pos.price;
73
81
  const gain = (currentPrice.price - entryPrice) / entryPrice;
74
82
  const hoursToEnd = (new Date(currentPrice.endDate).getTime() - Date.now()) / (1000 * 60 * 60);
75
- // Only send to Claude if something notable: moved >5% either way, or <72h to expiry
76
- if (Math.abs(gain) >= 0.05 || hoursToEnd <= 72) {
83
+ const hoursHeld = (Date.now() - pos.ts) / (1000 * 60 * 60);
84
+ const isStale = hoursHeld >= 168 && Math.abs(gain) < 0.05; // held >7d with <5% movement
85
+ // Send to Claude if: moved >5% either way, <72h to expiry, or stale (>7d no movement)
86
+ if (Math.abs(gain) >= 0.05 || hoursToEnd <= 72 || isStale) {
77
87
  reviewCandidates.push({
78
88
  market_id: pos.market_id,
79
89
  market_question: pos.market_question,
@@ -147,7 +157,9 @@ class PositionMonitor {
147
157
  }
148
158
  async fetchCurrentPrice(marketId, outcome) {
149
159
  try {
150
- const res = await fetch(`https://gamma-api.polymarket.com/markets/${marketId}`);
160
+ const res = await fetch(`https://gamma-api.polymarket.com/markets/${marketId}`, {
161
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; polly-gamba/1.0)' }
162
+ });
151
163
  if (!res.ok)
152
164
  return null;
153
165
  const data = await res.json();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-gamba",
3
- "version": "1.0.27",
3
+ "version": "1.0.32",
4
4
  "description": "Coinbase price signal → Claude brain → Polymarket CLOB execution",
5
5
  "main": "dist/index.js",
6
6
  "bin": {