polly-gamba 1.0.37 → 1.0.40

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.
@@ -115,7 +115,7 @@ RULES:
115
115
 
116
116
  ## POSITION DISCIPLINE:
117
117
  - Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
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. CRITICAL: Sportsbook odds movement is NOT a valid new catalyst — if your original thesis cited sportsbook lines and the proposed add-on also cites updated sportsbook odds, that is the same bet twice. Only factual real-world events qualify (injury, official announcement, standings change, political development). Sportsbook-only re-entry is prohibited even with a large price discount.
118
+ - Same-direction re-entry is NEVER allowed while a position is open. You CANNOT average down into an existing open position. The MCP enforces this hard block. If you still believe in the thesis after a price drop, close the existing position first, then re-enter fresh next scan.
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
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
  }
@@ -211,6 +211,7 @@ ${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('
211
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
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
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
+ - IMPORTANT: All 2028 US election markets share one theme — "2028 US election". This includes: Dem primary nominees (Newsom, Harris, etc.), Rep primary nominees (Vance, Rubio, etc.), and the general election winner. Do NOT hold more than 2 positions across this entire cycle.
214
215
  - If you're uncertain whether a market correlates to an existing theme, assume it does and skip.
215
216
 
216
217
  ## CLOSED MARKET DISCIPLINE (absolute rule — never override):
@@ -225,8 +226,8 @@ ${m.description ? `- Description: ${m.description.slice(0, 200)}` : ''}`).join('
225
226
  - 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
227
  - Exception: if both Polymarket AND sportsbooks agree with your base-rate analysis, the convergence strengthens the thesis.
227
228
 
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).
229
+ STEP 1: Call get_positions. Then output a THEME CONCENTRATION TABLE — list every theme with ≥1 open position, showing count/max (e.g. "2028 US election: 3/2 FULL", "GTA VI: 2/2 FULL", "Hungary 2026 election: 1/2"). Any theme showing FULL must be skipped entirely. You MUST output this table before evaluating any market.
230
+ STEP 2: For each market, check your theme concentration table first. If the market's theme is FULL, skip it immediately. Otherwise apply horizon discipline, closed market discipline, and edge check.
230
231
  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.`;
231
232
  const msg = JSON.stringify({
232
233
  type: 'user',
@@ -298,7 +299,12 @@ For EVERY market above: if price differs from fair probability by >8%, place a t
298
299
  }).join('\n\n');
299
300
  const prompt = `## Position Review — ${new Date().toISOString()}
300
301
 
301
- Review each open position. For each position: check whether the exit trigger condition has been met AND use your world knowledge (recent news, current standings, sportsbook odds) to assess whether the original thesis still holds.
302
+ ## STEP 0 CONCENTRATION AUDIT (do this BEFORE reviewing individual positions):
303
+ Call get_positions to get the FULL list of all open positions. Count positions by underlying theme (see Rule 8 definition below). If ANY theme has 3+ open positions, immediately close the excess ones (starting with the one whose current price is closest to 50/50) until the theme has ≤2 positions. Do NOT wait — close them now before proceeding. If you have 4 positions in a theme, you must close 2.
304
+
305
+ ## Review Candidates (${positionLines.split('###').length - 1} positions that moved >5% or expire within 72h):
306
+
307
+ Review each candidate below. For each: check whether the exit trigger has been met AND use world knowledge to assess thesis.
302
308
 
303
309
  ${positionLines}
304
310
 
@@ -310,7 +316,7 @@ CLOSE RULES — apply ALL that match, no thesis override allowed:
310
316
  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.
311
317
  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
318
  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.
319
+ 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". IMPORTANT: All positions tied to the 2028 US election cycle share one theme — "2028 US election". This includes: 2028 Dem primary nominees (Newsom, etc.), 2028 Rep primary nominees (Vance, Rubio, etc.), and the 2028 general election winner — regardless of which stage of the race resolves each market. If you have 3+ positions in the same theme: close positions one by one (starting with the one whose current price is closest to 50/50) until you have ≤2 in that theme. If you have 4, you must close 2. If you have 5, close 3. Do NOT stop after closing just one if the theme still has 3+ positions. Apply this BEFORE checking other rules. This check requires the FULL position list — which is why Step 0 mandates calling get_positions first.
314
320
  9. SPORTSBOOK THESIS DECAY: If the original reasoning cited a sportsbook cross-reference (e.g. "sportsbooks imply X%, Polymarket shows Y%") AND the position is currently down >20%: estimate current sportsbook consensus from your world knowledge. If the original sportsbook-Polymarket gap has narrowed by >60% (e.g. original 15pp gap is now ≤6pp), the arbitrage thesis is exhausted — CLOSE with reason "thesis_decayed". Do not continue holding through hard stops when the original edge is structurally gone.
315
321
 
316
322
  HOLD RULES: If NONE of the close rules apply and exit trigger NOT triggered, do nothing (no output needed).
@@ -33,14 +33,12 @@ function validatePlaceOrder(params, openPositions) {
33
33
  if (sameMarketPositions.length > 0 && !params.new_catalyst) {
34
34
  return `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`;
35
35
  }
36
- // Check 1b: Same-outcome re-entry requires ≥20% price improvement
36
+ // Check 1b: Same-outcome re-entry is NEVER allowed while position is open.
37
+ // Averaging down compounds losses (see: Wembanyama -$7.18 incident).
38
+ // Close the existing position first, then re-enter if still compelling.
37
39
  const sameOutcomePositions = sameMarketPositions.filter(p => String(p.outcome).toLowerCase() === String(params.outcome).toLowerCase());
38
40
  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
- }
41
+ return `Error: Same-direction position already open for ${params.market_id} ${params.outcome}. Cannot average into an existing open position — this compounds losses. Close the current position first before re-entering.`;
44
42
  }
45
43
  // Check 2: $500 total budget cap
46
44
  const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
@@ -182,29 +182,27 @@ async function runDuplicateTests() {
182
182
  assert(err !== null, 'expected same-market error for NO when YES exists');
183
183
  assert(err.includes('new_catalyst'), `expected new_catalyst required, got: ${err}`);
184
184
  });
185
- await test('D3: re-entry with catalyst but <20% price discount — blocked', async () => {
185
+ await test('D3: same-outcome re-entry always blocked (even with catalyst, any discount)', async () => {
186
186
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
187
- // 0.5 × 0.80 = 0.40; price 0.45 is not ≤ 0.40
188
187
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.45, new_catalyst: 'Breaking news' }, openPositions);
189
- assert(err !== null, 'expected price discount error');
190
- assert(err.includes('Re-entry blocked'), `expected Re-entry blocked, got: ${err}`);
191
- assert(err.includes('0.4000'), `expected required price 0.4000, got: ${err}`);
188
+ assert(err !== null, 'expected same-direction block');
189
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
192
190
  });
193
- await test('D4: re-entry with catalyst AND ≥20% discount allowed', async () => {
191
+ await test('D4: same-outcome re-entry blocked even with 24% discount + catalyst', async () => {
194
192
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
195
- // 0.5 × 0.80 = 0.40; price 0.38 is ≤ 0.40 ✓
196
193
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.38, new_catalyst: 'Breaking news' }, openPositions);
197
- assert(err === null, `expected success with 24% discount + catalyst, got: ${err}`);
194
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`);
195
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
198
196
  });
199
- await test('D5: re-entry at exactly 20% discount (boundary) allowed', async () => {
197
+ await test('D5: same-outcome re-entry at exactly 20% discount — blocked (no longer allowed)', async () => {
200
198
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
201
199
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.40, new_catalyst: 'News' }, openPositions);
202
- assert(err === null, `expected success at exactly 0.40 (20% below 0.50), got: ${err}`);
200
+ assert(err !== null, `expected block: averaging down is never allowed, got: ${err}`);
203
201
  });
204
- await test('D6: re-entry at 20% + epsilon above threshold — blocked', async () => {
202
+ await test('D6: same-outcome re-entry at any price — blocked', async () => {
205
203
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
206
- const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.401, new_catalyst: 'News' }, openPositions);
207
- assert(err !== null, `expected block at 0.401 (just above 0.40 threshold), got: ${err}`);
204
+ const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.10, new_catalyst: 'News' }, openPositions);
205
+ assert(err !== null, `expected block regardless of price, got: ${err}`);
208
206
  });
209
207
  await test('D7: opposite-outcome (NO) with catalyst — no price check needed', async () => {
210
208
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
@@ -378,25 +376,24 @@ async function runConcentrationTests() {
378
376
  }
379
377
  async function runTypeCoercionTests() {
380
378
  console.log('\n## I) Type Coercion — market_id string vs number');
381
- await test('I1: numeric market_id in params matches string market_id in position', async () => {
379
+ await test('I1: numeric market_id in params matches string market_id in position — same-direction blocked', async () => {
382
380
  // Simulates Claude passing market_id as a number (e.g., 564166) while
383
381
  // Redis stores it as a string ("564166"). Without String() coercion,
384
- // sameMarketPositions would be empty → 20% discount check bypassed.
382
+ // sameMarketPositions would be empty → same-direction block bypassed.
385
383
  const openPositions = [
386
384
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
387
385
  ];
388
386
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' }, openPositions);
389
- // 0.29 > 0.30 * 0.80 = 0.24 should be blocked (not a 20% discount)
390
- assert(err !== null, 'expected re-entry blocked: numeric market_id must match string in position');
391
- assert(err.includes('Re-entry blocked'), `expected Re-entry blocked error, got: ${err}`);
387
+ assert(err !== null, 'expected same-direction block: numeric market_id must match string in position');
388
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
392
389
  });
393
- await test('I2: numeric market_id with sufficient discount is allowed', async () => {
390
+ await test('I2: numeric market_id same-direction re-entry always blocked regardless of discount', async () => {
394
391
  const openPositions = [
395
392
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
396
393
  ];
397
394
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' }, openPositions);
398
- // 0.23 <= 0.30 * 0.80 = 0.24 → allowed (>20% discount)
399
- assert(err === null, `expected success with 20%+ discount even with numeric market_id, got: ${err}`);
395
+ // Same-direction averaging is never allowed, any discount
396
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`);
400
397
  });
401
398
  }
402
399
  // ─── Main ─────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-gamba",
3
- "version": "1.0.37",
3
+ "version": "1.0.40",
4
4
  "description": "Coinbase price signal → Claude brain → Polymarket CLOB execution",
5
5
  "main": "dist/index.js",
6
6
  "bin": {