polly-gamba 1.0.34 → 1.0.38

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.
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
  }
@@ -28,19 +28,17 @@ function validatePlaceOrder(params, openPositions) {
28
28
  params.price > 1) {
29
29
  return `Error: price must be in range (0, 1]. Got: ${params.price}`;
30
30
  }
31
- const sameMarketPositions = openPositions.filter(p => p.market_id === params.market_id);
31
+ const sameMarketPositions = openPositions.filter(p => String(p.market_id) === String(params.market_id));
32
32
  // Check 1: Existing position for same market requires new_catalyst
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);
@@ -126,19 +126,21 @@ 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
+ // Normalize market_id to string to prevent type-mismatch bypasses
130
+ const marketId = String(a.market_id);
129
131
  // Block re-entry into previously closed market+outcome combos
130
- const marketOutcomePrefix = `${a.market_id}_${a.outcome}_`;
132
+ const marketOutcomePrefix = `${marketId}_${a.outcome}_`;
131
133
  const wasClosedBefore = Array.from(closedIds).some(id => id.startsWith(marketOutcomePrefix));
132
134
  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.` }] };
135
+ return { content: [{ type: 'text', text: `Error: Re-entry blocked. This market+outcome (${marketId} ${a.outcome}) was previously closed. Polly-gamba does not re-enter previously exited positions to prevent repeated losses on the same bet.` }] };
134
136
  }
135
137
  // Validate using extracted business logic (includes input validation + budget checks)
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);
138
+ const validationError = (0, paper_trade_1.validatePlaceOrder)({ market_id: marketId, outcome: a.outcome, size_usdc: a.size_usdc, price: a.price, new_catalyst: a.new_catalyst }, openPositions);
137
139
  if (validationError) {
138
140
  return { content: [{ type: 'text', text: validationError }] };
139
141
  }
140
142
  const position = {
141
- market_id: a.market_id,
143
+ market_id: marketId,
142
144
  market_question: a.market_question,
143
145
  token_id: a.token_id,
144
146
  outcome: a.outcome,
@@ -230,7 +232,7 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
230
232
  // Find matching open position — must check BOTH status field AND closedIds set
231
233
  // (hard stops add to closedIds but cannot update the list entry's status field)
232
234
  const match = positions.find((p) => {
233
- if (p.market_id !== a.market_id)
235
+ if (String(p.market_id) !== String(a.market_id))
234
236
  return false;
235
237
  if (p.outcome?.toLowerCase() !== String(a.outcome).toLowerCase())
236
238
  return false;
@@ -71,20 +71,34 @@ class PositionMonitor {
71
71
  }
72
72
  // Build review candidates with current price data for Claude
73
73
  const reviewCandidates = [];
74
- // Track positions with no price data that have been held >24h — likely resolved/delisted
74
+ // Track positions with no price data that have been CONTINUOUSLY unavailable for >24h — likely resolved/delisted
75
+ // We use a Redis hash to record when price data first went missing per position key.
76
+ // A transient API outage should NOT trigger closure — only sustained unavailability counts.
75
77
  const noPriceDataStale = [];
78
+ const noPriceSinceKey = `${this.prefix}:no_price_since`;
76
79
  for (const pos of toCheck) {
77
80
  const currentPrice = marketPriceCache.get(`${pos.market_id}|${pos.outcome}`) ?? null;
81
+ const posKey = `${pos.market_id}_${pos.outcome}_${pos.ts}`;
78
82
  if (!currentPrice) {
79
- const hoursHeld = (Date.now() - pos.ts) / (1000 * 60 * 60);
80
- if (hoursHeld >= 24) {
81
- noPriceDataStale.push(pos);
83
+ // Record when we first noticed missing price data for this position
84
+ const existing = await this.redis.hget(noPriceSinceKey, posKey);
85
+ if (!existing) {
86
+ await this.redis.hset(noPriceSinceKey, posKey, String(Date.now()));
87
+ console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — first miss recorded, watching (market may be resolved or delisted)`);
82
88
  }
83
89
  else {
84
- console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} skipping review (market may be resolved or delisted)`);
90
+ const hoursMissing = (Date.now() - parseInt(existing, 10)) / (1000 * 60 * 60);
91
+ if (hoursMissing >= 24) {
92
+ noPriceDataStale.push(pos);
93
+ }
94
+ else {
95
+ console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — missing ${hoursMissing.toFixed(1)}h (closes after 24h continuous)`);
96
+ }
85
97
  }
86
98
  continue;
87
99
  }
100
+ // Price data available — clear any no-price-since tracker
101
+ await this.redis.hdel(noPriceSinceKey, posKey);
88
102
  const entryPrice = pos.price;
89
103
  const gain = (currentPrice.price - entryPrice) / entryPrice;
90
104
  const hoursToEnd = (new Date(currentPrice.endDate).getTime() - Date.now()) / (1000 * 60 * 60);
@@ -115,11 +129,12 @@ class PositionMonitor {
115
129
  continue;
116
130
  const closed = { ...pos, status: 'closed', exit_price: pos.price, pnl: 0, closed_at: Date.now(), reason: 'market_unavailable' };
117
131
  await this.redis.sadd(`${this.prefix}:closed_ids`, posKey);
132
+ await this.redis.hdel(noPriceSinceKey, posKey); // clean up tracker
118
133
  await this.redis.lpush(`${this.prefix}:closed_positions`, JSON.stringify(closed));
119
134
  await this.redis.ltrim(`${this.prefix}:closed_positions`, 0, 9999);
120
135
  await this.redis.lpush(`${this.prefix}:log`, JSON.stringify({ type: 'position_closed', data: closed, ts: Date.now(), trigger: 'market_unavailable' }));
121
136
  await this.redis.ltrim(`${this.prefix}:log`, 0, 9999);
122
- console.log(`[position-monitor] CLOSED [market_unavailable] "${String(pos.market_question).slice(0, 50)}" ${pos.outcome} — no price data for >24h, freeing capital`);
137
+ console.log(`[position-monitor] CLOSED [market_unavailable] "${String(pos.market_question).slice(0, 50)}" ${pos.outcome} — no price data for >24h continuously, freeing capital`);
123
138
  }
124
139
  if (noPriceDataStale.length > 0) {
125
140
  console.log(`[position-monitor] freed ${noPriceDataStale.length} stale no-price-data positions`);
@@ -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 })];
@@ -376,6 +374,28 @@ async function runConcentrationTests() {
376
374
  assert(err.includes('Budget cap'), `expected Budget cap error (not market cap), got: ${err}`);
377
375
  });
378
376
  }
377
+ async function runTypeCoercionTests() {
378
+ console.log('\n## I) Type Coercion — market_id string vs number');
379
+ await test('I1: numeric market_id in params matches string market_id in position — same-direction blocked', async () => {
380
+ // Simulates Claude passing market_id as a number (e.g., 564166) while
381
+ // Redis stores it as a string ("564166"). Without String() coercion,
382
+ // sameMarketPositions would be empty → same-direction block bypassed.
383
+ const openPositions = [
384
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
385
+ ];
386
+ const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' }, openPositions);
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}`);
389
+ });
390
+ await test('I2: numeric market_id same-direction re-entry always blocked regardless of discount', async () => {
391
+ const openPositions = [
392
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
393
+ ];
394
+ const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' }, openPositions);
395
+ // Same-direction averaging is never allowed, any discount
396
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`);
397
+ });
398
+ }
379
399
  // ─── Main ─────────────────────────────────────────────────────────────────────
380
400
  async function main() {
381
401
  console.log('\n[stress] polly-gamba paper trading system — stress tests');
@@ -389,6 +409,7 @@ async function main() {
389
409
  await runHardStopTests();
390
410
  await runDoubleCloseBugTests();
391
411
  await runConcentrationTests();
412
+ await runTypeCoercionTests();
392
413
  console.log(`\n[stress] ─── Results ─── ${passed} passed, ${failed} failed\n`);
393
414
  if (bugReport.length > 0) {
394
415
  console.log('[stress] Bug findings:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-gamba",
3
- "version": "1.0.34",
3
+ "version": "1.0.38",
4
4
  "description": "Coinbase price signal → Claude brain → Polymarket CLOB execution",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/service.log CHANGED
@@ -2469,3 +2469,548 @@ npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact
2469
2469
  [position-monitor] WARNING: no price data for 562828 NO — skipping review (market may be resolved or delisted)
2470
2470
  [position-monitor] WARNING: no price data for 559651 NO — skipping review (market may be resolved or delisted)
2471
2471
  [position-monitor] checked=24 review_candidates=3 hard_stops=0 (moved>5% or <72h expiry)
2472
+ [polly-gamba] Starting paper trading service
2473
+ [polly-gamba] Claude cwd: /Users/feral/polly-gamba
2474
+ [polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
2475
+ [coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
2476
+ [polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
2477
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2478
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2479
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2480
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2481
+ [coinbase-ws] Connected
2482
+ [gamma] Loaded 494 markets (filtered from 500)
2483
+ [scan] 20 high-quality markets for autonomous review
2484
+ [gamma] Loaded 494 markets (filtered from 500)
2485
+ [expiring] 0 expiring markets for review
2486
+ [expiring] no expiring markets found this cycle
2487
+ [position-monitor] checked=24 review_candidates=5 hard_stops=0 (moved>5% or <72h expiry)
2488
+ [claude-trader:anthropic] ready
2489
+ [claude-trader:ollama] ready
2490
+ [claude-trader:expiring] ready
2491
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2492
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2493
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2494
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2495
+ [expiring] failed to fetch markets: fetch failed
2496
+ [gamma] Loaded 494 markets (filtered from 500)
2497
+ [scan] 20 high-quality markets for autonomous review
2498
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2499
+ [position-monitor] CLOSED [market_unavailable] "Will Spain win the 2026 FIFA World Cup?" YES — no price data for >24h, freeing capital
2500
+ [position-monitor] CLOSED [market_unavailable] "Will Arsenal win the 2025-26 Champions League?" NO — no price data for >24h, freeing capital
2501
+ [position-monitor] CLOSED [market_unavailable] "Will Jesus Christ return before GTA VI?" NO — no price data for >24h, freeing capital
2502
+ [position-monitor] CLOSED [market_unavailable] "Will Scottie Scheffler win the 2026 Masters tourna" YES — no price data for >24h, freeing capital
2503
+ [position-monitor] CLOSED [market_unavailable] "Will France win the 2026 FIFA World Cup?" YES — no price data for >24h, freeing capital
2504
+ [position-monitor] CLOSED [market_unavailable] "Will the San Antonio Spurs win the 2026 NBA Finals" NO — no price data for >24h, freeing capital
2505
+ [position-monitor] freed 6 stale no-price-data positions
2506
+ [position-monitor] checked=24 review_candidates=3 hard_stops=0 (moved>5% or <72h expiry)
2507
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2508
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2509
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2510
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2511
+ [scan] failed to fetch markets: fetch failed
2512
+ [expiring] failed to fetch markets: fetch failed
2513
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2514
+ [position-monitor] CLOSED [market_unavailable] "Will Gavin Newsom win the 2028 Democratic presiden" NO — no price data for >24h, freeing capital
2515
+ [position-monitor] CLOSED [market_unavailable] "Zelenskyy out as Ukraine president by end of 2026?" NO — no price data for >24h, freeing capital
2516
+ [position-monitor] CLOSED [market_unavailable] "Will Bayern Munich win the 2025–26 Champions Leagu" NO — no price data for >24h, freeing capital
2517
+ [position-monitor] CLOSED [market_unavailable] "Will bitcoin hit $1m before GTA VI?" NO — no price data for >24h, freeing capital
2518
+ [position-monitor] CLOSED [market_unavailable] "Xi Jinping out before 2027?" NO — no price data for >24h, freeing capital
2519
+ [position-monitor] CLOSED [market_unavailable] "Will China invade Taiwan by end of 2026?" NO — no price data for >24h, freeing capital
2520
+ [position-monitor] freed 6 stale no-price-data positions
2521
+ [position-monitor] checked=18 review_candidates=3 hard_stops=0 (moved>5% or <72h expiry)
2522
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2523
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2524
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2525
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2526
+ [scan] failed to fetch markets: fetch failed
2527
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2528
+ [position-monitor] CLOSED [market_unavailable] "Will the Boston Celtics win the 2026 NBA Finals?" YES — no price data for >24h, freeing capital
2529
+ [position-monitor] CLOSED [market_unavailable] "2026 Balance of Power: D Senate, D House" NO — no price data for >24h, freeing capital
2530
+ [position-monitor] CLOSED [market_unavailable] "Will Manchester City win the 2025–26 English Premi" NO — no price data for >24h, freeing capital
2531
+ [position-monitor] CLOSED [market_unavailable] "Putin out as President of Russia by December 31, 2" NO — no price data for >24h, freeing capital
2532
+ [position-monitor] CLOSED [market_unavailable] "Will Gavin Newsom win the 2028 US Presidential Ele" NO — no price data for >24h, freeing capital
2533
+ [position-monitor] CLOSED [market_unavailable] "Will Marco Rubio win the 2028 Republican president" NO — no price data for >24h, freeing capital
2534
+ [position-monitor] CLOSED [market_unavailable] "Will J.D. Vance win the 2028 Republican presidenti" YES — no price data for >24h, freeing capital
2535
+ [position-monitor] CLOSED [market_unavailable] "Will the next Prime Minister of Hungary be Péter M" NO — no price data for >24h, freeing capital
2536
+ [position-monitor] CLOSED [market_unavailable] "Will the next Prime Minister of Hungary be Viktor " YES — no price data for >24h, freeing capital
2537
+ [position-monitor] CLOSED [market_unavailable] "Russia x Ukraine ceasefire by end of 2026?" NO — no price data for >24h, freeing capital
2538
+ [position-monitor] CLOSED [market_unavailable] "Will John Cornyn win the 2026 Texas Republican Pri" YES — no price data for >24h, freeing capital
2539
+ [position-monitor] freed 11 stale no-price-data positions
2540
+ [position-monitor] checked=12 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2541
+ [gamma] Loaded 494 markets (filtered from 500)
2542
+ [expiring] 0 expiring markets for review
2543
+ [expiring] no expiring markets found this cycle
2544
+ [signal] ETH up 0.50% @ $2,146.9 over 55s
2545
+ [signal] ETH up 0.50% @ $2,146.9
2546
+ [match] 20 markets for ETH signal
2547
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2548
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2549
+ [scan] failed to fetch markets: fetch failed
2550
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2551
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2552
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2553
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2554
+ [gamma] Loaded 494 markets (filtered from 500)
2555
+ [expiring] 0 expiring markets for review
2556
+ [expiring] no expiring markets found this cycle
2557
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2558
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2559
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2560
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2561
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2562
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2563
+ [scan] failed to fetch markets: fetch failed
2564
+ [expiring] failed to fetch markets: fetch failed
2565
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2566
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2567
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2568
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2569
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2570
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2571
+ [expiring] failed to fetch markets: fetch failed
2572
+ [gamma] Loaded 494 markets (filtered from 500)
2573
+ [scan] 18 high-quality markets for autonomous review
2574
+ [signal] BTC up 0.50% @ $70,106.04 over 14s
2575
+ [signal] BTC up 0.50% @ $70,106.04
2576
+ [match] 1 markets for BTC signal
2577
+ [signal] ETH up 0.50% @ $2,164.32 over 9s
2578
+ [signal] ETH up 0.50% @ $2,164.32
2579
+ [match] 20 markets for ETH signal
2580
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2581
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2582
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2583
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2584
+ [scan] failed to fetch markets: fetch failed
2585
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2586
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2587
+ [expiring] failed to fetch markets: fetch failed
2588
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2589
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2590
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2591
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2592
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2593
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2594
+ [scan] failed to fetch markets: fetch failed
2595
+ [gamma] Loaded 494 markets (filtered from 500)
2596
+ [expiring] 0 expiring markets for review
2597
+ [expiring] no expiring markets found this cycle
2598
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2599
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2600
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2601
+ [scan] failed to fetch markets: fetch failed
2602
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2603
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2604
+ [expiring] failed to fetch markets: fetch failed
2605
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2606
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2607
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2608
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2609
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2610
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2611
+ [expiring] failed to fetch markets: fetch failed
2612
+ [gamma] Loaded 494 markets (filtered from 500)
2613
+ [scan] 18 high-quality markets for autonomous review
2614
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2615
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2616
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2617
+ [scan] failed to fetch markets: fetch failed
2618
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2619
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2620
+ [gamma] Loaded 495 markets (filtered from 500)
2621
+ [expiring] 0 expiring markets for review
2622
+ [expiring] no expiring markets found this cycle
2623
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2624
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2625
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2626
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2627
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2628
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2629
+ [expiring] failed to fetch markets: fetch failed
2630
+ [gamma] Loaded 495 markets (filtered from 500)
2631
+ [scan] 19 high-quality markets for autonomous review
2632
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2633
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2634
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2635
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2636
+ [scan] failed to fetch markets: fetch failed
2637
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2638
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2639
+ [gamma] Loaded 495 markets (filtered from 500)
2640
+ [expiring] 0 expiring markets for review
2641
+ [expiring] no expiring markets found this cycle
2642
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2643
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2644
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2645
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2646
+ [gamma] Loaded 495 markets (filtered from 500)
2647
+ [scan] 21 high-quality markets for autonomous review
2648
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2649
+ [expiring] 0 expiring markets for review
2650
+ [expiring] no expiring markets found this cycle
2651
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2652
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2653
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2654
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2655
+ [scan] failed to fetch markets: fetch failed
2656
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2657
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2658
+ [gamma] Loaded 495 markets (filtered from 500)
2659
+ [expiring] 0 expiring markets for review
2660
+ [expiring] no expiring markets found this cycle
2661
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2662
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2663
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2664
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2665
+ [scan] failed to fetch markets: fetch failed
2666
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2667
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2668
+ [gamma] Loaded 495 markets (filtered from 500)
2669
+ [expiring] 0 expiring markets for review
2670
+ [expiring] no expiring markets found this cycle
2671
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2672
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2673
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2674
+ [scan] failed to fetch markets: fetch failed
2675
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2676
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2677
+ [expiring] failed to fetch markets: fetch failed
2678
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2679
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2680
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2681
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2682
+ [gamma] Loaded 495 markets (filtered from 500)
2683
+ [scan] 20 high-quality markets for autonomous review
2684
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2685
+ [expiring] 0 expiring markets for review
2686
+ [expiring] no expiring markets found this cycle
2687
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2688
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2689
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2690
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2691
+ [gamma] Loaded 495 markets (filtered from 500)
2692
+ [scan] 20 high-quality markets for autonomous review
2693
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2694
+ [expiring] 0 expiring markets for review
2695
+ [expiring] no expiring markets found this cycle
2696
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2697
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2698
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2699
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2700
+ [gamma] Loaded 495 markets (filtered from 500)
2701
+ [scan] 18 high-quality markets for autonomous review
2702
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2703
+ [expiring] 0 expiring markets for review
2704
+ [expiring] no expiring markets found this cycle
2705
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2706
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2707
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2708
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2709
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2710
+ [position-monitor] CLOSED [market_unavailable] "Will Spain win the 2026 FIFA World Cup?" YES — no price data for >24h, freeing capital
2711
+ [position-monitor] CLOSED [market_unavailable] "Will Arsenal win the 2025-26 Champions League?" NO — no price data for >24h, freeing capital
2712
+ [position-monitor] CLOSED [market_unavailable] "Will Jesus Christ return before GTA VI?" NO — no price data for >24h, freeing capital
2713
+ [position-monitor] CLOSED [market_unavailable] "Will Scottie Scheffler win the 2026 Masters tourna" YES — no price data for >24h, freeing capital
2714
+ [position-monitor] CLOSED [market_unavailable] "Will the San Antonio Spurs win the 2026 NBA Finals" NO — no price data for >24h, freeing capital
2715
+ [position-monitor] CLOSED [market_unavailable] "Xi Jinping out before 2027?" NO — no price data for >24h, freeing capital
2716
+ [position-monitor] CLOSED [market_unavailable] "Will China invade Taiwan by end of 2026?" NO — no price data for >24h, freeing capital
2717
+ [position-monitor] CLOSED [market_unavailable] "Putin out as President of Russia by December 31, 2" NO — no price data for >24h, freeing capital
2718
+ [position-monitor] CLOSED [market_unavailable] "Will Gavin Newsom win the 2028 US Presidential Ele" NO — no price data for >24h, freeing capital
2719
+ [position-monitor] CLOSED [market_unavailable] "Will Marco Rubio win the 2028 Republican president" NO — no price data for >24h, freeing capital
2720
+ [position-monitor] CLOSED [market_unavailable] "Will J.D. Vance win the 2028 Republican presidenti" YES — no price data for >24h, freeing capital
2721
+ [position-monitor] CLOSED [market_unavailable] "Will the next Prime Minister of Hungary be Péter M" NO — no price data for >24h, freeing capital
2722
+ [position-monitor] CLOSED [market_unavailable] "Will the next Prime Minister of Hungary be Viktor " YES — no price data for >24h, freeing capital
2723
+ [position-monitor] CLOSED [market_unavailable] "Russia x Ukraine ceasefire by end of 2026?" NO — no price data for >24h, freeing capital
2724
+ [position-monitor] CLOSED [market_unavailable] "Will John Cornyn win the 2026 Texas Republican Pri" YES — no price data for >24h, freeing capital
2725
+ [position-monitor] freed 15 stale no-price-data positions
2726
+ [position-monitor] checked=24 review_candidates=2 hard_stops=0 (moved>5% or <72h expiry)
2727
+ [gamma] Loaded 494 markets (filtered from 500)
2728
+ [scan] 18 high-quality markets for autonomous review
2729
+ [gamma] Loaded 495 markets (filtered from 500)
2730
+ [expiring] 0 expiring markets for review
2731
+ [expiring] no expiring markets found this cycle
2732
+ [position-monitor] checked=9 review_candidates=2 hard_stops=0 (moved>5% or <72h expiry)
2733
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2734
+ [scan] 18 high-quality markets for autonomous review
2735
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2736
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2737
+ [expiring] failed to fetch markets: fetch failed
2738
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2739
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2740
+ [scan] failed to fetch markets: fetch failed
2741
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2742
+ [position-monitor] CLOSED [market_unavailable] "Will Gavin Newsom win the 2028 Democratic presiden" NO — no price data for >24h, freeing capital
2743
+ [position-monitor] CLOSED [market_unavailable] "Will France win the 2026 FIFA World Cup?" YES — no price data for >24h, freeing capital
2744
+ [position-monitor] CLOSED [market_unavailable] "Zelenskyy out as Ukraine president by end of 2026?" NO — no price data for >24h, freeing capital
2745
+ [position-monitor] CLOSED [market_unavailable] "Will bitcoin hit $1m before GTA VI?" NO — no price data for >24h, freeing capital
2746
+ [position-monitor] CLOSED [market_unavailable] "2026 Balance of Power: D Senate, D House" NO — no price data for >24h, freeing capital
2747
+ [position-monitor] CLOSED [market_unavailable] "Will Manchester City win the 2025–26 English Premi" NO — no price data for >24h, freeing capital
2748
+ [position-monitor] freed 6 stale no-price-data positions
2749
+ [position-monitor] checked=9 review_candidates=1 hard_stops=0 (moved>5% or <72h expiry)
2750
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2751
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2752
+ [expiring] failed to fetch markets: fetch failed
2753
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2754
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2755
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2756
+ [position-monitor] CLOSED [market_unavailable] "Will Bayern Munich win the 2025–26 Champions Leagu" NO — no price data for >24h, freeing capital
2757
+ [position-monitor] CLOSED [market_unavailable] "Will the Boston Celtics win the 2026 NBA Finals?" YES — no price data for >24h, freeing capital
2758
+ [position-monitor] freed 2 stale no-price-data positions
2759
+ [position-monitor] checked=3 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2760
+ [gamma] Loaded 494 markets (filtered from 500)
2761
+ [scan] 16 high-quality markets for autonomous review
2762
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2763
+ [expiring] 0 expiring markets for review
2764
+ [expiring] no expiring markets found this cycle
2765
+ [signal] ETH up 0.50% @ $2,155.82 over 60s
2766
+ [signal] ETH up 0.50% @ $2,155.82
2767
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2768
+ [gamma] Loaded 484 markets (filtered from 500)
2769
+ [match] 20 markets for ETH signal
2770
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2771
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2772
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2773
+ [scan] 16 high-quality markets for autonomous review
2774
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2775
+ [expiring] 0 expiring markets for review
2776
+ [expiring] no expiring markets found this cycle
2777
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2778
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2779
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2780
+ [scan] failed to fetch markets: fetch failed
2781
+ [signal] ETH up 0.50% @ $2,156.26 over 39s
2782
+ [signal] ETH up 0.50% @ $2,156.26
2783
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2784
+ [gamma] Loaded 484 markets (filtered from 500)
2785
+ [match] 20 markets for ETH signal
2786
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2787
+ [expiring] 0 expiring markets for review
2788
+ [expiring] no expiring markets found this cycle
2789
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2790
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2791
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2792
+ [scan] failed to fetch markets: fetch failed
2793
+ [signal] ETH down -0.50% @ $2,148 over 60s
2794
+ [signal] ETH down 0.50% @ $2,148
2795
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2796
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2797
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2798
+ [expiring] failed to fetch markets: fetch failed
2799
+ [error] failed to fetch markets: fetch failed
2800
+ [position-monitor] WARNING: no price data for 553830 NO — skipping review (market may be resolved or delisted)
2801
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2802
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2803
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2804
+ [gamma] Loaded 493 markets (filtered from 500)
2805
+ [scan] 16 high-quality markets for autonomous review
2806
+ [polly-gamba] Starting paper trading service
2807
+ [polly-gamba] Claude cwd: /Users/feral/polly-gamba
2808
+ [polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
2809
+ [coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
2810
+ [polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
2811
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2812
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2813
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2814
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2815
+ [position-monitor] checked=1 review_candidates=0 hard_stops=0 (moved>5% or <72h expiry)
2816
+ [coinbase-ws] Connected
2817
+ [gamma] Loaded 493 markets (filtered from 500)
2818
+ [scan] 16 high-quality markets for autonomous review
2819
+ [gamma] Loaded 493 markets (filtered from 500)
2820
+ [expiring] 0 expiring markets for review
2821
+ [expiring] no expiring markets found this cycle
2822
+ [claude-trader:anthropic] ready
2823
+ [claude-trader:ollama] ready
2824
+ [claude-trader:expiring] ready
2825
+ [polly-gamba] Starting paper trading service
2826
+ [polly-gamba] Claude cwd: /Users/feral/polly-gamba
2827
+ [polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
2828
+ [coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
2829
+ [polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
2830
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2831
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2832
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2833
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2834
+ [gamma] Loaded 493 markets (filtered from 500)
2835
+ [expiring] 0 expiring markets for review
2836
+ [expiring] no expiring markets found this cycle
2837
+ [gamma] Loaded 493 markets (filtered from 500)
2838
+ [scan] 16 high-quality markets for autonomous review
2839
+ [coinbase-ws] Connected
2840
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2841
+ [claude-trader:anthropic] ready
2842
+ [claude-trader:ollama] ready
2843
+ [claude-trader:expiring] ready
2844
+ [polly-gamba] Starting paper trading service
2845
+ [polly-gamba] Claude cwd: /Users/feral/polly-gamba
2846
+ [polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
2847
+ [coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
2848
+ [polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
2849
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2850
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2851
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2852
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2853
+ [gamma] Loaded 493 markets (filtered from 500)
2854
+ [expiring] 0 expiring markets for review
2855
+ [expiring] no expiring markets found this cycle
2856
+ [gamma] Loaded 493 markets (filtered from 500)
2857
+ [scan] 16 high-quality markets for autonomous review
2858
+ [coinbase-ws] Connected
2859
+ [claude-trader:anthropic] ready
2860
+ [claude-trader:ollama] ready
2861
+ [claude-trader:expiring] ready
2862
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2863
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2864
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2865
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2866
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2867
+ [gamma] Loaded 494 markets (filtered from 500)
2868
+ [scan] 16 high-quality markets for autonomous review
2869
+ [gamma] Loaded 494 markets (filtered from 500)
2870
+ [expiring] 0 expiring markets for review
2871
+ [expiring] no expiring markets found this cycle
2872
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2873
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2874
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2875
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2876
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2877
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2878
+ [gamma] Loaded 494 markets (filtered from 500)
2879
+ [expiring] 0 expiring markets for review
2880
+ [expiring] no expiring markets found this cycle
2881
+ [gamma] Loaded 494 markets (filtered from 500)
2882
+ [scan] 15 high-quality markets for autonomous review
2883
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2884
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2885
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2886
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2887
+ [expiring] failed to fetch markets: fetch failed
2888
+ [position-monitor] WARNING: no price data for 553830 NO — first miss recorded, watching (market may be resolved or delisted)
2889
+ [position-monitor] WARNING: no price data for 558934 YES — first miss recorded, watching (market may be resolved or delisted)
2890
+ [position-monitor] WARNING: no price data for 566140 NO — first miss recorded, watching (market may be resolved or delisted)
2891
+ [position-monitor] WARNING: no price data for 540819 NO — first miss recorded, watching (market may be resolved or delisted)
2892
+ [position-monitor] WARNING: no price data for 568629 YES — first miss recorded, watching (market may be resolved or delisted)
2893
+ [position-monitor] WARNING: no price data for 566142 NO — first miss recorded, watching (market may be resolved or delisted)
2894
+ [position-monitor] WARNING: no price data for 562828 NO — first miss recorded, watching (market may be resolved or delisted)
2895
+ [position-monitor] WARNING: no price data for 559651 NO — first miss recorded, watching (market may be resolved or delisted)
2896
+ [position-monitor] WARNING: no price data for 561975 NO — first miss recorded, watching (market may be resolved or delisted)
2897
+ [position-monitor] WARNING: no price data for 567561 NO — first miss recorded, watching (market may be resolved or delisted)
2898
+ [position-monitor] WARNING: no price data for 567687 NO — first miss recorded, watching (market may be resolved or delisted)
2899
+ [position-monitor] checked=24 review_candidates=3 hard_stops=0 (moved>5% or <72h expiry)
2900
+ [gamma] Loaded 494 markets (filtered from 500)
2901
+ [scan] 15 high-quality markets for autonomous review
2902
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2903
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2904
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2905
+ [gamma] Loaded 494 markets (filtered from 500)
2906
+ [scan] 15 high-quality markets for autonomous review
2907
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2908
+ [expiring] 0 expiring markets for review
2909
+ [expiring] no expiring markets found this cycle
2910
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2911
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2912
+ [scan] failed to fetch markets: fetch failed
2913
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2914
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2915
+ [expiring] failed to fetch markets: fetch failed
2916
+ [position-monitor] WARNING: no price data for 566140 NO — first miss recorded, watching (market may be resolved or delisted)
2917
+ [position-monitor] WARNING: no price data for 540819 NO — first miss recorded, watching (market may be resolved or delisted)
2918
+ [position-monitor] WARNING: no price data for 568629 YES — first miss recorded, watching (market may be resolved or delisted)
2919
+ [position-monitor] WARNING: no price data for 562828 NO — first miss recorded, watching (market may be resolved or delisted)
2920
+ [position-monitor] WARNING: no price data for 561974 YES — first miss recorded, watching (market may be resolved or delisted)
2921
+ [position-monitor] checked=24 review_candidates=5 hard_stops=0 (moved>5% or <72h expiry)
2922
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2923
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2924
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2925
+ [gamma] Loaded 494 markets (filtered from 500)
2926
+ [scan] 15 high-quality markets for autonomous review
2927
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2928
+ [expiring] 0 expiring markets for review
2929
+ [expiring] no expiring markets found this cycle
2930
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2931
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2932
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2933
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2934
+ [expiring] failed to fetch markets: fetch failed
2935
+ [scan] failed to fetch markets: fetch failed
2936
+ [position-monitor] WARNING: no price data for 553830 NO — first miss recorded, watching (market may be resolved or delisted)
2937
+ [position-monitor] WARNING: no price data for 558934 YES — first miss recorded, watching (market may be resolved or delisted)
2938
+ [position-monitor] WARNING: no price data for 566140 NO — first miss recorded, watching (market may be resolved or delisted)
2939
+ [position-monitor] WARNING: no price data for 540819 NO — first miss recorded, watching (market may be resolved or delisted)
2940
+ [position-monitor] WARNING: no price data for 568629 YES — first miss recorded, watching (market may be resolved or delisted)
2941
+ [position-monitor] WARNING: no price data for 559652 NO — first miss recorded, watching (market may be resolved or delisted)
2942
+ [position-monitor] WARNING: no price data for 558936 YES — first miss recorded, watching (market may be resolved or delisted)
2943
+ [position-monitor] WARNING: no price data for 553866 NO — first miss recorded, watching (market may be resolved or delisted)
2944
+ [position-monitor] WARNING: no price data for 567689 NO — first miss recorded, watching (market may be resolved or delisted)
2945
+ [position-monitor] WARNING: no price data for 566142 NO — first miss recorded, watching (market may be resolved or delisted)
2946
+ [position-monitor] checked=24 review_candidates=4 hard_stops=0 (moved>5% or <72h expiry)
2947
+ [signal] ETH down -0.50% @ $2,148.31 over 59s
2948
+ [signal] ETH down 0.50% @ $2,148.31
2949
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2950
+ [error] failed to fetch markets: fetch failed
2951
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2952
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2953
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2954
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2955
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2956
+ [gamma] Loaded 494 markets (filtered from 500)
2957
+ [scan] 15 high-quality markets for autonomous review
2958
+ [gamma] Loaded 494 markets (filtered from 500)
2959
+ [expiring] 0 expiring markets for review
2960
+ [expiring] no expiring markets found this cycle
2961
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2962
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2963
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2964
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2965
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2966
+ [gamma] Loaded 494 markets (filtered from 500)
2967
+ [scan] 15 high-quality markets for autonomous review
2968
+ [gamma] Loaded 494 markets (filtered from 500)
2969
+ [expiring] 0 expiring markets for review
2970
+ [expiring] no expiring markets found this cycle
2971
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2972
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2973
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2974
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2975
+ [expiring] failed to fetch markets: fetch failed
2976
+ [scan] failed to fetch markets: fetch failed
2977
+ [position-monitor] WARNING: no price data for 558934 YES — first miss recorded, watching (market may be resolved or delisted)
2978
+ [position-monitor] WARNING: no price data for 566140 NO — first miss recorded, watching (market may be resolved or delisted)
2979
+ [position-monitor] WARNING: no price data for 540819 NO — first miss recorded, watching (market may be resolved or delisted)
2980
+ [position-monitor] WARNING: no price data for 568629 YES — first miss recorded, watching (market may be resolved or delisted)
2981
+ [position-monitor] WARNING: no price data for 553866 NO — first miss recorded, watching (market may be resolved or delisted)
2982
+ [position-monitor] WARNING: no price data for 567689 NO — first miss recorded, watching (market may be resolved or delisted)
2983
+ [position-monitor] WARNING: no price data for 566142 NO — first miss recorded, watching (market may be resolved or delisted)
2984
+ [position-monitor] WARNING: no price data for 562828 NO — first miss recorded, watching (market may be resolved or delisted)
2985
+ [position-monitor] WARNING: no price data for 566188 NO — first miss recorded, watching (market may be resolved or delisted)
2986
+ [position-monitor] WARNING: no price data for 559651 NO — first miss recorded, watching (market may be resolved or delisted)
2987
+ [position-monitor] checked=24 review_candidates=5 hard_stops=0 (moved>5% or <72h expiry)
2988
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
2989
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
2990
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2991
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
2992
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2993
+ [gamma] Loaded 495 markets (filtered from 500)
2994
+ [scan] 15 high-quality markets for autonomous review
2995
+ [gamma] Loaded 495 markets (filtered from 500)
2996
+ [expiring] 0 expiring markets for review
2997
+ [expiring] no expiring markets found this cycle
2998
+ [polly-gamba] Starting paper trading service
2999
+ [polly-gamba] Claude cwd: /Users/feral/polly-gamba
3000
+ [polly-gamba] Expiring trader cwd: /Users/feral/polly-gamba-expiring
3001
+ [coinbase-ws] Connecting to wss://ws-feed.exchange.coinbase.com
3002
+ [polly-gamba] Listening for BTC/ETH price signals (threshold: 0.5% in 60s)...
3003
+ [scan] fetching high-quality markets (vol24h>$50k, liq>$50k, price 0.10-0.90)
3004
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
3005
+ [expiring] fetching expiring markets (closing within 72h, vol24h>$10k, liq>$10k, price 0.05-0.95)
3006
+ [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
3007
+ [gamma] Loaded 494 markets (filtered from 500)
3008
+ [expiring] 0 expiring markets for review
3009
+ [expiring] no expiring markets found this cycle
3010
+ [coinbase-ws] Connected
3011
+ [gamma] Loaded 494 markets (filtered from 500)
3012
+ [scan] 15 high-quality markets for autonomous review
3013
+ [claude-trader:anthropic] ready
3014
+ [claude-trader:ollama] ready
3015
+ [claude-trader:expiring] ready
3016
+ [position-monitor] checked=24 review_candidates=6 hard_stops=0 (moved>5% or <72h expiry)
@@ -132,7 +132,7 @@ RULES:
132
132
 
133
133
  ## POSITION DISCIPLINE:
134
134
  - Max $100 per market (20% of $500 budget). The MCP enforces this — don't fight it.
135
- - 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.
135
+ - 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.
136
136
  - 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."
137
137
  - 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.`
138
138
  }
@@ -67,16 +67,14 @@ export function validatePlaceOrder(
67
67
  return `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`
68
68
  }
69
69
 
70
- // Check 1b: Same-outcome re-entry requires ≥20% price improvement
70
+ // Check 1b: Same-outcome re-entry is NEVER allowed while position is open.
71
+ // Averaging down compounds losses (see: Wembanyama -$7.18 incident).
72
+ // Close the existing position first, then re-enter if still compelling.
71
73
  const sameOutcomePositions = sameMarketPositions.filter(p =>
72
74
  String(p.outcome).toLowerCase() === String(params.outcome).toLowerCase()
73
75
  )
74
76
  if (sameOutcomePositions.length > 0) {
75
- const lastEntry = Math.max(...sameOutcomePositions.map(p => p.price || 0))
76
- const requiredPrice = lastEntry * 0.80
77
- if (params.price > requiredPrice) {
78
- 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.`
79
- }
77
+ 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.`
80
78
  }
81
79
 
82
80
  // Check 2: $500 total budget cap
@@ -106,20 +106,34 @@ export class PositionMonitor {
106
106
  reasoning?: string
107
107
  }> = []
108
108
 
109
- // Track positions with no price data that have been held >24h — likely resolved/delisted
109
+ // Track positions with no price data that have been CONTINUOUSLY unavailable for >24h — likely resolved/delisted
110
+ // We use a Redis hash to record when price data first went missing per position key.
111
+ // A transient API outage should NOT trigger closure — only sustained unavailability counts.
110
112
  const noPriceDataStale: Position[] = []
113
+ const noPriceSinceKey = `${this.prefix}:no_price_since`
111
114
  for (const pos of toCheck) {
112
115
  const currentPrice = marketPriceCache.get(`${pos.market_id}|${pos.outcome}`) ?? null
116
+ const posKey = `${pos.market_id}_${pos.outcome}_${pos.ts}`
113
117
  if (!currentPrice) {
114
- const hoursHeld = (Date.now() - pos.ts) / (1000 * 60 * 60)
115
- if (hoursHeld >= 24) {
116
- noPriceDataStale.push(pos)
118
+ // Record when we first noticed missing price data for this position
119
+ const existing = await this.redis.hget(noPriceSinceKey, posKey)
120
+ if (!existing) {
121
+ await this.redis.hset(noPriceSinceKey, posKey, String(Date.now()))
122
+ console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — first miss recorded, watching (market may be resolved or delisted)`)
117
123
  } else {
118
- console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} skipping review (market may be resolved or delisted)`)
124
+ const hoursMissing = (Date.now() - parseInt(existing, 10)) / (1000 * 60 * 60)
125
+ if (hoursMissing >= 24) {
126
+ noPriceDataStale.push(pos)
127
+ } else {
128
+ console.warn(`[position-monitor] WARNING: no price data for ${pos.market_id} ${pos.outcome} — missing ${hoursMissing.toFixed(1)}h (closes after 24h continuous)`)
129
+ }
119
130
  }
120
131
  continue
121
132
  }
122
133
 
134
+ // Price data available — clear any no-price-since tracker
135
+ await this.redis.hdel(noPriceSinceKey, posKey)
136
+
123
137
  const entryPrice = pos.price
124
138
  const gain = (currentPrice.price - entryPrice) / entryPrice
125
139
  const hoursToEnd = (new Date(currentPrice.endDate).getTime() - Date.now()) / (1000 * 60 * 60)
@@ -153,11 +167,12 @@ export class PositionMonitor {
153
167
 
154
168
  const closed = { ...pos, status: 'closed', exit_price: pos.price, pnl: 0, closed_at: Date.now(), reason: 'market_unavailable' }
155
169
  await this.redis.sadd(`${this.prefix}:closed_ids`, posKey)
170
+ await this.redis.hdel(noPriceSinceKey, posKey) // clean up tracker
156
171
  await this.redis.lpush(`${this.prefix}:closed_positions`, JSON.stringify(closed))
157
172
  await this.redis.ltrim(`${this.prefix}:closed_positions`, 0, 9999)
158
173
  await this.redis.lpush(`${this.prefix}:log`, JSON.stringify({ type: 'position_closed', data: closed, ts: Date.now(), trigger: 'market_unavailable' }))
159
174
  await this.redis.ltrim(`${this.prefix}:log`, 0, 9999)
160
- console.log(`[position-monitor] CLOSED [market_unavailable] "${String(pos.market_question).slice(0, 50)}" ${pos.outcome} — no price data for >24h, freeing capital`)
175
+ console.log(`[position-monitor] CLOSED [market_unavailable] "${String(pos.market_question).slice(0, 50)}" ${pos.outcome} — no price data for >24h continuously, freeing capital`)
161
176
  }
162
177
  if (noPriceDataStale.length > 0) {
163
178
  console.log(`[position-monitor] freed ${noPriceDataStale.length} stale no-price-data positions`)
@@ -234,44 +234,42 @@ async function runDuplicateTests(): Promise<void> {
234
234
  assert(err!.includes('new_catalyst'), `expected new_catalyst required, got: ${err}`)
235
235
  })
236
236
 
237
- await test('D3: re-entry with catalyst but <20% price discount — blocked', async () => {
237
+ await test('D3: same-outcome re-entry always blocked (even with catalyst, any discount)', async () => {
238
238
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })]
239
- // 0.5 × 0.80 = 0.40; price 0.45 is not ≤ 0.40
240
239
  const err = validatePlaceOrder(
241
240
  { market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.45, new_catalyst: 'Breaking news' },
242
241
  openPositions
243
242
  )
244
- assert(err !== null, 'expected price discount error')
245
- assert(err!.includes('Re-entry blocked'), `expected Re-entry blocked, got: ${err}`)
246
- assert(err!.includes('0.4000'), `expected required price 0.4000, got: ${err}`)
243
+ assert(err !== null, 'expected same-direction block')
244
+ assert(err!.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`)
247
245
  })
248
246
 
249
- await test('D4: re-entry with catalyst AND ≥20% discount allowed', async () => {
247
+ await test('D4: same-outcome re-entry blocked even with 24% discount + catalyst', async () => {
250
248
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })]
251
- // 0.5 × 0.80 = 0.40; price 0.38 is ≤ 0.40 ✓
252
249
  const err = validatePlaceOrder(
253
250
  { market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.38, new_catalyst: 'Breaking news' },
254
251
  openPositions
255
252
  )
256
- assert(err === null, `expected success with 24% discount + catalyst, got: ${err}`)
253
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`)
254
+ assert(err!.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`)
257
255
  })
258
256
 
259
- await test('D5: re-entry at exactly 20% discount (boundary) allowed', async () => {
257
+ await test('D5: same-outcome re-entry at exactly 20% discount — blocked (no longer allowed)', async () => {
260
258
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })]
261
259
  const err = validatePlaceOrder(
262
260
  { market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.40, new_catalyst: 'News' },
263
261
  openPositions
264
262
  )
265
- assert(err === null, `expected success at exactly 0.40 (20% below 0.50), got: ${err}`)
263
+ assert(err !== null, `expected block: averaging down is never allowed, got: ${err}`)
266
264
  })
267
265
 
268
- await test('D6: re-entry at 20% + epsilon above threshold — blocked', async () => {
266
+ await test('D6: same-outcome re-entry at any price — blocked', async () => {
269
267
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })]
270
268
  const err = validatePlaceOrder(
271
- { market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.401, new_catalyst: 'News' },
269
+ { market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.10, new_catalyst: 'News' },
272
270
  openPositions
273
271
  )
274
- assert(err !== null, `expected block at 0.401 (just above 0.40 threshold), got: ${err}`)
272
+ assert(err !== null, `expected block regardless of price, got: ${err}`)
275
273
  })
276
274
 
277
275
  await test('D7: opposite-outcome (NO) with catalyst — no price check needed', async () => {
@@ -508,6 +506,37 @@ async function runConcentrationTests(): Promise<void> {
508
506
  })
509
507
  }
510
508
 
509
+ async function runTypeCoercionTests(): Promise<void> {
510
+ console.log('\n## I) Type Coercion — market_id string vs number')
511
+
512
+ await test('I1: numeric market_id in params matches string market_id in position — same-direction blocked', async () => {
513
+ // Simulates Claude passing market_id as a number (e.g., 564166) while
514
+ // Redis stores it as a string ("564166"). Without String() coercion,
515
+ // sameMarketPositions would be empty → same-direction block bypassed.
516
+ const openPositions = [
517
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
518
+ ]
519
+ const err = validatePlaceOrder(
520
+ { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' },
521
+ openPositions
522
+ )
523
+ assert(err !== null, 'expected same-direction block: numeric market_id must match string in position')
524
+ assert(err!.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`)
525
+ })
526
+
527
+ await test('I2: numeric market_id same-direction re-entry always blocked regardless of discount', async () => {
528
+ const openPositions = [
529
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
530
+ ]
531
+ const err = validatePlaceOrder(
532
+ { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' },
533
+ openPositions
534
+ )
535
+ // Same-direction averaging is never allowed, any discount
536
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`)
537
+ })
538
+ }
539
+
511
540
  // ─── Main ─────────────────────────────────────────────────────────────────────
512
541
 
513
542
  async function main(): Promise<void> {
@@ -523,6 +552,7 @@ async function main(): Promise<void> {
523
552
  await runHardStopTests()
524
553
  await runDoubleCloseBugTests()
525
554
  await runConcentrationTests()
555
+ await runTypeCoercionTests()
526
556
 
527
557
  console.log(`\n[stress] ─── Results ─── ${passed} passed, ${failed} failed\n`)
528
558