polly-gamba 1.0.22 → 1.0.27

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.
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ClaudeTrader = void 0;
7
7
  const child_process_1 = require("child_process");
8
8
  const fs_1 = require("fs");
9
+ const path_1 = require("path");
9
10
  const readline_1 = require("readline");
10
11
  const ioredis_1 = __importDefault(require("ioredis"));
11
12
  function findClaudeBin() {
@@ -29,6 +30,7 @@ function findClaudeBin() {
29
30
  }
30
31
  const CLAUDE_BIN = findClaudeBin();
31
32
  const CLAUDE_TOKEN = process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY || '';
33
+ const PAPER_MODE = process.env.PAPER_MODE === 'true' || process.env.PAPER_MODE === '1';
32
34
  class ClaudeTrader {
33
35
  proc = null;
34
36
  redis;
@@ -66,6 +68,11 @@ class ClaudeTrader {
66
68
  '--verbose',
67
69
  '--dangerously-skip-permissions',
68
70
  ];
71
+ // Explicitly load MCP config — --print mode doesn't auto-discover settings.json
72
+ const settingsPath = (0, path_1.join)(this.config.cwd, 'settings.json');
73
+ if ((0, fs_1.existsSync)(settingsPath)) {
74
+ args.push('--mcp-config', settingsPath);
75
+ }
69
76
  if (this.config.model) {
70
77
  args.push('--model', this.config.model);
71
78
  }
@@ -94,12 +101,12 @@ class ClaudeTrader {
94
101
  type: 'user',
95
102
  message: {
96
103
  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
-
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
+ ${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' : ''}
99
106
  TOOLS: place_order, skip_all, get_budget_status
100
107
  RULES:
101
108
  - 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.
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.
103
110
  - YES is underpriced → BUY YES. NO is underpriced (YES overpriced) → BUY NO.
104
111
  - $10 USDC per trade (small size, many bets).
105
112
  - NO cap on number of trades — bet every market where you see any edge.
@@ -178,7 +185,7 @@ Call place_order on any market you have edge on. $10 per trade. No cap.`;
178
185
  const prefix = this.config.redisPrefix;
179
186
  await this.redis.lpush(`${prefix}:scans`, JSON.stringify({ scanId, markets_count: markets.length, ts: Date.now() }));
180
187
  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));
188
+ const tradeable = markets.filter(m => m.tokens?.some(t => t.price >= 0.10 && t.price <= 0.90));
182
189
  const prompt = `## Autonomous Scan ${scanId}
183
190
 
184
191
  Time: ${new Date().toISOString()}
@@ -192,7 +199,21 @@ ${tradeable.map((m, i) => `### ${i + 1}. ${m.question}
192
199
  - Tokens: ${m.tokens?.map(t => `${t.outcome}@${t.price}`).join(', ') || 'none'}
193
200
  ${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('\n\n')}
194
201
 
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.`;
202
+ ## HORIZON DISCIPLINE (required applied before edge check):
203
+ - Markets resolving WITHIN 6 months: ${PAPER_MODE ? '>3% edge required (PAPER MODE — be aggressive)' : 'standard >5% edge required'}.
204
+ - Markets resolving 6–12 months out: require >${PAPER_MODE ? '7' : '10'}% edge. Long-horizon bets tie up capital without near-term price catalysts.
205
+ - Markets resolving MORE than 12 months out: require >${PAPER_MODE ? '12' : '15'}% edge. Only trade if the mispricing is extreme and obvious.
206
+ - If the end date is unknown or ambiguous, treat it as >12 months. Do NOT guess short horizon.
207
+ - These thresholds stack: if the market already has an open position, do not add unless it meets the horizon threshold AND the 20%-discount re-entry rule.
208
+
209
+ ## CONCENTRATION DISCIPLINE (required — applied before edge check):
210
+ - Count your existing open positions by underlying theme. A "theme" is the single real-world event that resolves the bet (e.g. "GTA VI release", "Hungary 2026 election", "NBA 2026 Finals").
211
+ - MAX 2 open positions per underlying theme. If you already have 2+ open positions in the same theme, SKIP all new markets in that theme regardless of apparent edge.
212
+ - Correlation examples: "Will Jesus return before GTA VI", "Will BTC hit $1M before GTA VI", "Will China invade Taiwan before GTA VI" — all three resolve based on GTA VI's release date. They are the SAME theme.
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
+ - If you're uncertain whether a market correlates to an existing theme, assume it does and skip.
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.`;
196
217
  const msg = JSON.stringify({
197
218
  type: 'user',
198
219
  message: { role: 'user', content: prompt }
@@ -273,6 +294,7 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
273
294
  3. Your side of the position is priced below 10% — HARD CLOSE. The market has strongly repriced against you. Do NOT override this with "thesis not disproven yet." At <10%, expected value of holding is near zero.
274
295
  4. Position is down >50% — HARD CLOSE regardless of thesis. Cut losses. No exceptions.
275
296
  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
+ 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.
276
298
 
277
299
  HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).`;
278
300
  const msg = JSON.stringify({
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Core business logic for paper trading.
3
+ * Extracted so it can be unit-tested independently of the MCP server.
4
+ */
5
+ export interface Position {
6
+ market_id: string;
7
+ market_question: string;
8
+ outcome: string;
9
+ side: string;
10
+ size_usdc: number;
11
+ price: number;
12
+ ts: number;
13
+ status: 'open' | 'closed';
14
+ exit_price?: number;
15
+ pnl?: number;
16
+ closed_at?: number;
17
+ reason?: string;
18
+ exit_trigger?: string;
19
+ reasoning?: string;
20
+ new_catalyst?: string | null;
21
+ token_id?: string;
22
+ }
23
+ export interface PlaceOrderParams {
24
+ market_id: string;
25
+ outcome: string;
26
+ size_usdc: number;
27
+ price: number;
28
+ new_catalyst?: string;
29
+ }
30
+ export declare const TOTAL_BUDGET = 500;
31
+ export declare const MARKET_CAP = 100;
32
+ /**
33
+ * Validate a place_order request against the current set of open positions.
34
+ * Returns an error message string if validation fails, or null if OK.
35
+ */
36
+ export declare function validatePlaceOrder(params: PlaceOrderParams, openPositions: Position[]): string | null;
37
+ /**
38
+ * Compute P&L for closing a position at a given exit price.
39
+ * Returns null if the entry price is zero or invalid (prevents division by zero).
40
+ *
41
+ * Formula: shares = size_usdc / entry_price; pnl = shares * exit_price - size_usdc
42
+ */
43
+ export declare function computePnl(position: Position, exitPrice: number): number | null;
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * Core business logic for paper trading.
4
+ * Extracted so it can be unit-tested independently of the MCP server.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.MARKET_CAP = exports.TOTAL_BUDGET = void 0;
8
+ exports.validatePlaceOrder = validatePlaceOrder;
9
+ exports.computePnl = computePnl;
10
+ exports.TOTAL_BUDGET = 500;
11
+ exports.MARKET_CAP = 100;
12
+ /**
13
+ * Validate a place_order request against the current set of open positions.
14
+ * Returns an error message string if validation fails, or null if OK.
15
+ */
16
+ function validatePlaceOrder(params, openPositions) {
17
+ // Input validation — guard against bad LLM output
18
+ if (params.size_usdc == null ||
19
+ typeof params.size_usdc !== 'number' ||
20
+ isNaN(params.size_usdc) ||
21
+ params.size_usdc <= 0) {
22
+ return `Error: size_usdc must be a positive number. Got: ${params.size_usdc}`;
23
+ }
24
+ if (params.price == null ||
25
+ typeof params.price !== 'number' ||
26
+ isNaN(params.price) ||
27
+ params.price <= 0 ||
28
+ params.price > 1) {
29
+ return `Error: price must be in range (0, 1]. Got: ${params.price}`;
30
+ }
31
+ const sameMarketPositions = openPositions.filter(p => p.market_id === params.market_id);
32
+ // Check 1: Existing position for same market requires new_catalyst
33
+ if (sameMarketPositions.length > 0 && !params.new_catalyst) {
34
+ return `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`;
35
+ }
36
+ // Check 1b: Same-outcome re-entry requires ≥20% price improvement
37
+ const sameOutcomePositions = sameMarketPositions.filter(p => String(p.outcome).toLowerCase() === String(params.outcome).toLowerCase());
38
+ if (sameOutcomePositions.length > 0) {
39
+ const lastEntry = Math.max(...sameOutcomePositions.map(p => p.price || 0));
40
+ const requiredPrice = lastEntry * 0.80;
41
+ if (params.price > requiredPrice) {
42
+ return `Error: Re-entry blocked. Last ${params.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${params.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`;
43
+ }
44
+ }
45
+ // Check 2: $500 total budget cap
46
+ const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
47
+ if (totalDeployed + params.size_usdc > exports.TOTAL_BUDGET) {
48
+ const available = Math.max(0, exports.TOTAL_BUDGET - totalDeployed);
49
+ return `Error: Budget cap reached. $${exports.TOTAL_BUDGET} paper budget. Currently deployed: $${totalDeployed.toFixed(2)}. Available: $${available.toFixed(2)}.`;
50
+ }
51
+ // Check 3: $100 per-market concentration cap
52
+ const marketDeployed = sameMarketPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
53
+ if (marketDeployed + params.size_usdc > exports.MARKET_CAP) {
54
+ return `Error: Single-market cap: max $${exports.MARKET_CAP} per market (20% of $${exports.TOTAL_BUDGET} budget). Already deployed $${marketDeployed.toFixed(2)} in this market.`;
55
+ }
56
+ return null; // valid
57
+ }
58
+ /**
59
+ * Compute P&L for closing a position at a given exit price.
60
+ * Returns null if the entry price is zero or invalid (prevents division by zero).
61
+ *
62
+ * Formula: shares = size_usdc / entry_price; pnl = shares * exit_price - size_usdc
63
+ */
64
+ function computePnl(position, exitPrice) {
65
+ if (!position.price || position.price <= 0 || isNaN(position.price)) {
66
+ return null;
67
+ }
68
+ const shares = position.size_usdc / position.price;
69
+ return shares * exitPrice - position.size_usdc;
70
+ }
71
+ //# sourceMappingURL=paper-trade.js.map
@@ -8,6 +8,7 @@ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
8
8
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
9
  const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
10
10
  const ioredis_1 = __importDefault(require("ioredis"));
11
+ const paper_trade_1 = require("./lib/paper-trade");
11
12
  const REDIS_PREFIX = process.env.POLLY_REDIS_PREFIX || 'polly';
12
13
  const redis = new ioredis_1.default(process.env.REDIS_URL || 'redis://localhost:6379', {
13
14
  retryStrategy: (times) => Math.min(times * 500, 5000),
@@ -110,8 +111,6 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
110
111
  const ts = Date.now();
111
112
  if (name === 'place_order') {
112
113
  const a = args;
113
- const TOTAL_BUDGET = 500;
114
- const MARKET_CAP = 100;
115
114
  // Load all open positions for enforcement checks
116
115
  const rawPositions = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1);
117
116
  const closedIds = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`));
@@ -127,50 +126,10 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
127
126
  return false;
128
127
  return !closedIds.has(`${p.market_id}_${p.outcome}_${p.ts}`);
129
128
  });
130
- // Check 1: Existing position for same market_id requires new_catalyst
131
- const sameMarketPositions = openPositions.filter((p) => 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
- // Check 1b: Same-outcome re-entry requires ≥20% price improvement (prevents pyramiding into losers)
141
- const sameOutcomePositions = sameMarketPositions.filter((p) => String(p.outcome).toLowerCase() === String(a.outcome).toLowerCase());
142
- if (sameOutcomePositions.length > 0) {
143
- const lastEntry = Math.max(...sameOutcomePositions.map((p) => p.price || 0));
144
- const requiredPrice = lastEntry * 0.80;
145
- if (a.price > requiredPrice) {
146
- return {
147
- content: [{
148
- type: 'text',
149
- text: `Error: Re-entry blocked. Last ${a.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${a.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`
150
- }]
151
- };
152
- }
153
- }
154
- // Check 2: $500 total budget cap
155
- const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
156
- if (totalDeployed + a.size_usdc > TOTAL_BUDGET) {
157
- const available = Math.max(0, TOTAL_BUDGET - totalDeployed);
158
- return {
159
- content: [{
160
- type: 'text',
161
- text: `Error: Budget cap reached. $500 paper budget. Currently deployed: $${totalDeployed.toFixed(2)}. Available: $${available.toFixed(2)}.`
162
- }]
163
- };
164
- }
165
- // Check 3: $100 per-market concentration cap
166
- const marketDeployed = sameMarketPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
167
- if (marketDeployed + a.size_usdc > MARKET_CAP) {
168
- return {
169
- content: [{
170
- type: 'text',
171
- text: `Error: Single-market cap: max $100 per market (20% of $500 budget). Already deployed $${marketDeployed.toFixed(2)} in this market.`
172
- }]
173
- };
129
+ // Validate using extracted business logic (includes input validation + budget checks)
130
+ 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
+ if (validationError) {
132
+ return { content: [{ type: 'text', text: validationError }] };
174
133
  }
175
134
  const position = {
176
135
  market_id: a.market_id,
@@ -239,6 +198,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
239
198
  if (name === 'close_position') {
240
199
  const a = args;
241
200
  const raw = await redis.lrange(`${REDIS_PREFIX}:positions`, 0, -1);
201
+ const closedIdsForClose = new Set(await redis.smembers(`${REDIS_PREFIX}:closed_ids`));
242
202
  const positions = raw.map(r => {
243
203
  try {
244
204
  return JSON.parse(r);
@@ -247,15 +207,24 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
247
207
  return null;
248
208
  }
249
209
  }).filter(Boolean);
250
- // Find matching open position
251
- const match = positions.find((p) => p.market_id === a.market_id &&
252
- p.outcome?.toLowerCase() === String(a.outcome).toLowerCase() &&
253
- p.status !== 'closed');
210
+ // Find matching open position — must check BOTH status field AND closedIds set
211
+ // (hard stops add to closedIds but cannot update the list entry's status field)
212
+ const match = positions.find((p) => {
213
+ if (p.market_id !== a.market_id)
214
+ return false;
215
+ if (p.outcome?.toLowerCase() !== String(a.outcome).toLowerCase())
216
+ return false;
217
+ if (p.status === 'closed')
218
+ return false;
219
+ return !closedIdsForClose.has(`${p.market_id}_${p.outcome}_${p.ts}`);
220
+ });
254
221
  if (!match) {
255
222
  return { content: [{ type: 'text', text: `No open position found for market ${a.market_id} outcome ${a.outcome}` }] };
256
223
  }
257
- const shares = match.size_usdc / match.price;
258
- const pnl = shares * a.exit_price - match.size_usdc;
224
+ const pnl = (0, paper_trade_1.computePnl)(match, a.exit_price);
225
+ if (pnl === null) {
226
+ return { content: [{ type: 'text', text: `Error: Cannot compute P&L — entry price is zero or invalid for position ${a.market_id} ${a.outcome}` }] };
227
+ }
259
228
  const closed = {
260
229
  ...match,
261
230
  status: 'closed',
@@ -65,8 +65,10 @@ class PositionMonitor {
65
65
  const reviewCandidates = [];
66
66
  for (const pos of toCheck) {
67
67
  const currentPrice = marketPriceCache.get(`${pos.market_id}|${pos.outcome}`) ?? null;
68
- if (!currentPrice)
68
+ if (!currentPrice) {
69
+ console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — skipping review (market may be resolved or delisted)`);
69
70
  continue;
71
+ }
70
72
  const entryPrice = pos.price;
71
73
  const gain = (currentPrice.price - entryPrice) / entryPrice;
72
74
  const hoursToEnd = (new Date(currentPrice.endDate).getTime() - Date.now()) / (1000 * 60 * 60);
@@ -127,6 +129,10 @@ class PositionMonitor {
127
129
  const closedIds = await this.redis.smembers(`${prefix}:closed_ids`);
128
130
  if (closedIds.includes(posKey))
129
131
  return;
132
+ if (!match.price || match.price <= 0) {
133
+ console.error(`[position-monitor] HARD STOP skipped for "${String(match.market_question).slice(0, 50)}" — entry price is zero/invalid`);
134
+ return;
135
+ }
130
136
  const shares = match.size_usdc / match.price;
131
137
  const pnl = shares * candidate.current_price - match.size_usdc;
132
138
  const closed = { ...match, status: 'closed', exit_price: candidate.current_price, pnl, closed_at: Date.now(), reason: 'stop_loss' };
@@ -0,0 +1 @@
1
+ export {};