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.
- package/dist/claude-trader.js +19 -3
- package/dist/mcp-server.js +21 -1
- package/dist/position-monitor.js +22 -10
- package/package.json +1 -1
- package/service.log +1029 -0
- package/src/claude-trader.ts +19 -3
- package/src/mcp-server.ts +21 -1
- package/src/position-monitor.ts +23 -10
package/dist/claude-trader.js
CHANGED
|
@@ -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
|
-
|
|
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({
|
package/dist/mcp-server.js
CHANGED
|
@@ -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') {
|
package/dist/position-monitor.js
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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();
|