polly-gamba 1.0.34 → 1.0.37

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
+ - 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.
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,7 +28,7 @@ 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.`;
@@ -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`);
@@ -376,6 +376,29 @@ async function runConcentrationTests() {
376
376
  assert(err.includes('Budget cap'), `expected Budget cap error (not market cap), got: ${err}`);
377
377
  });
378
378
  }
379
+ async function runTypeCoercionTests() {
380
+ 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 () => {
382
+ // Simulates Claude passing market_id as a number (e.g., 564166) while
383
+ // Redis stores it as a string ("564166"). Without String() coercion,
384
+ // sameMarketPositions would be empty → 20% discount check bypassed.
385
+ const openPositions = [
386
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
387
+ ];
388
+ 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}`);
392
+ });
393
+ await test('I2: numeric market_id with sufficient discount is allowed', async () => {
394
+ const openPositions = [
395
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
396
+ ];
397
+ 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}`);
400
+ });
401
+ }
379
402
  // ─── Main ─────────────────────────────────────────────────────────────────────
380
403
  async function main() {
381
404
  console.log('\n[stress] polly-gamba paper trading system — stress tests');
@@ -389,6 +412,7 @@ async function main() {
389
412
  await runHardStopTests();
390
413
  await runDoubleCloseBugTests();
391
414
  await runConcentrationTests();
415
+ await runTypeCoercionTests();
392
416
  console.log(`\n[stress] ─── Results ─── ${passed} passed, ${failed} failed\n`);
393
417
  if (bugReport.length > 0) {
394
418
  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.37",
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,337 @@ 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
@@ -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`)
@@ -508,6 +508,38 @@ async function runConcentrationTests(): Promise<void> {
508
508
  })
509
509
  }
510
510
 
511
+ async function runTypeCoercionTests(): Promise<void> {
512
+ console.log('\n## I) Type Coercion — market_id string vs number')
513
+
514
+ await test('I1: numeric market_id in params matches string market_id in position', async () => {
515
+ // Simulates Claude passing market_id as a number (e.g., 564166) while
516
+ // Redis stores it as a string ("564166"). Without String() coercion,
517
+ // sameMarketPositions would be empty → 20% discount check bypassed.
518
+ const openPositions = [
519
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
520
+ ]
521
+ const err = validatePlaceOrder(
522
+ { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' },
523
+ openPositions
524
+ )
525
+ // 0.29 > 0.30 * 0.80 = 0.24 → should be blocked (not a 20% discount)
526
+ assert(err !== null, 'expected re-entry blocked: numeric market_id must match string in position')
527
+ assert(err!.includes('Re-entry blocked'), `expected Re-entry blocked error, got: ${err}`)
528
+ })
529
+
530
+ await test('I2: numeric market_id with sufficient discount is allowed', async () => {
531
+ const openPositions = [
532
+ makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
533
+ ]
534
+ const err = validatePlaceOrder(
535
+ { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' },
536
+ openPositions
537
+ )
538
+ // 0.23 <= 0.30 * 0.80 = 0.24 → allowed (>20% discount)
539
+ assert(err === null, `expected success with 20%+ discount even with numeric market_id, got: ${err}`)
540
+ })
541
+ }
542
+
511
543
  // ─── Main ─────────────────────────────────────────────────────────────────────
512
544
 
513
545
  async function main(): Promise<void> {
@@ -523,6 +555,7 @@ async function main(): Promise<void> {
523
555
  await runHardStopTests()
524
556
  await runDoubleCloseBugTests()
525
557
  await runConcentrationTests()
558
+ await runTypeCoercionTests()
526
559
 
527
560
  console.log(`\n[stress] ─── Results ─── ${passed} passed, ${failed} failed\n`)
528
561