polly-gamba 1.0.37 → 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. CRITICAL: Sportsbook odds movement is NOT a valid new catalyst — if your original thesis cited sportsbook lines and the proposed add-on also cites updated sportsbook odds, that is the same bet twice. Only factual real-world events qualify (injury, official announcement, standings change, political development). Sportsbook-only re-entry is prohibited even with a large price discount.
118
+ - Same-direction re-entry is NEVER allowed while a position is open. You CANNOT average down into an existing open position. The MCP enforces this hard block. If you still believe in the thesis after a price drop, close the existing position first, then re-enter fresh next scan.
119
119
  - exit_trigger is required on every trade. Be specific: "Exit when price hits 0.X" or "Exit when [specific news event]" — not "when narrative converges."
120
120
  - Call get_budget_status AND get_positions at the start of each scan to know available capital and your current open positions. get_positions returns {"positions": [...], "closed_markets": [...]}. You MUST review BOTH before trading to apply concentration AND closed-market discipline correctly.`
121
121
  }
@@ -33,14 +33,12 @@ function validatePlaceOrder(params, openPositions) {
33
33
  if (sameMarketPositions.length > 0 && !params.new_catalyst) {
34
34
  return `Error: Position already exists for this market. Provide new_catalyst (specific new information from last 24h) to add.`;
35
35
  }
36
- // Check 1b: Same-outcome re-entry requires ≥20% price improvement
36
+ // Check 1b: Same-outcome re-entry is NEVER allowed while position is open.
37
+ // Averaging down compounds losses (see: Wembanyama -$7.18 incident).
38
+ // Close the existing position first, then re-enter if still compelling.
37
39
  const sameOutcomePositions = sameMarketPositions.filter(p => String(p.outcome).toLowerCase() === String(params.outcome).toLowerCase());
38
40
  if (sameOutcomePositions.length > 0) {
39
- const lastEntry = Math.max(...sameOutcomePositions.map(p => p.price || 0));
40
- const requiredPrice = lastEntry * 0.80;
41
- if (params.price > requiredPrice) {
42
- return `Error: Re-entry blocked. Last ${params.outcome} entry at ${lastEntry.toFixed(4)}. Require price ≤ ${requiredPrice.toFixed(4)} (20% better) to add same-direction position. Current price ${params.price.toFixed(4)} is too close to last entry. Wait for a larger dislocation before averaging.`;
43
- }
41
+ return `Error: Same-direction position already open for ${params.market_id} ${params.outcome}. Cannot average into an existing open position — this compounds losses. Close the current position first before re-entering.`;
44
42
  }
45
43
  // Check 2: $500 total budget cap
46
44
  const totalDeployed = openPositions.reduce((s, p) => s + (p.size_usdc || 0), 0);
@@ -182,29 +182,27 @@ async function runDuplicateTests() {
182
182
  assert(err !== null, 'expected same-market error for NO when YES exists');
183
183
  assert(err.includes('new_catalyst'), `expected new_catalyst required, got: ${err}`);
184
184
  });
185
- await test('D3: re-entry with catalyst but <20% price discount — blocked', async () => {
185
+ await test('D3: same-outcome re-entry always blocked (even with catalyst, any discount)', async () => {
186
186
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
187
- // 0.5 × 0.80 = 0.40; price 0.45 is not ≤ 0.40
188
187
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.45, new_catalyst: 'Breaking news' }, openPositions);
189
- assert(err !== null, 'expected price discount error');
190
- assert(err.includes('Re-entry blocked'), `expected Re-entry blocked, got: ${err}`);
191
- assert(err.includes('0.4000'), `expected required price 0.4000, got: ${err}`);
188
+ assert(err !== null, 'expected same-direction block');
189
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
192
190
  });
193
- await test('D4: re-entry with catalyst AND ≥20% discount allowed', async () => {
191
+ await test('D4: same-outcome re-entry blocked even with 24% discount + catalyst', async () => {
194
192
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
195
- // 0.5 × 0.80 = 0.40; price 0.38 is ≤ 0.40 ✓
196
193
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.38, new_catalyst: 'Breaking news' }, openPositions);
197
- assert(err === null, `expected success with 24% discount + catalyst, got: ${err}`);
194
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`);
195
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
198
196
  });
199
- await test('D5: re-entry at exactly 20% discount (boundary) allowed', async () => {
197
+ await test('D5: same-outcome re-entry at exactly 20% discount — blocked (no longer allowed)', async () => {
200
198
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
201
199
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.40, new_catalyst: 'News' }, openPositions);
202
- assert(err === null, `expected success at exactly 0.40 (20% below 0.50), got: ${err}`);
200
+ assert(err !== null, `expected block: averaging down is never allowed, got: ${err}`);
203
201
  });
204
- await test('D6: re-entry at 20% + epsilon above threshold — blocked', async () => {
202
+ await test('D6: same-outcome re-entry at any price — blocked', async () => {
205
203
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
206
- const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.401, new_catalyst: 'News' }, openPositions);
207
- assert(err !== null, `expected block at 0.401 (just above 0.40 threshold), got: ${err}`);
204
+ const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 'dup_mkt', outcome: 'YES', size_usdc: 10, price: 0.10, new_catalyst: 'News' }, openPositions);
205
+ assert(err !== null, `expected block regardless of price, got: ${err}`);
208
206
  });
209
207
  await test('D7: opposite-outcome (NO) with catalyst — no price check needed', async () => {
210
208
  const openPositions = [makePosition({ market_id: 'dup_mkt', outcome: 'YES', price: 0.5 })];
@@ -378,25 +376,24 @@ async function runConcentrationTests() {
378
376
  }
379
377
  async function runTypeCoercionTests() {
380
378
  console.log('\n## I) Type Coercion — market_id string vs number');
381
- await test('I1: numeric market_id in params matches string market_id in position', async () => {
379
+ await test('I1: numeric market_id in params matches string market_id in position — same-direction blocked', async () => {
382
380
  // Simulates Claude passing market_id as a number (e.g., 564166) while
383
381
  // Redis stores it as a string ("564166"). Without String() coercion,
384
- // sameMarketPositions would be empty → 20% discount check bypassed.
382
+ // sameMarketPositions would be empty → same-direction block bypassed.
385
383
  const openPositions = [
386
384
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
387
385
  ];
388
386
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' }, openPositions);
389
- // 0.29 > 0.30 * 0.80 = 0.24 should be blocked (not a 20% discount)
390
- assert(err !== null, 'expected re-entry blocked: numeric market_id must match string in position');
391
- assert(err.includes('Re-entry blocked'), `expected Re-entry blocked error, got: ${err}`);
387
+ assert(err !== null, 'expected same-direction block: numeric market_id must match string in position');
388
+ assert(err.includes('Same-direction position already open'), `expected same-direction error, got: ${err}`);
392
389
  });
393
- await test('I2: numeric market_id with sufficient discount is allowed', async () => {
390
+ await test('I2: numeric market_id same-direction re-entry always blocked regardless of discount', async () => {
394
391
  const openPositions = [
395
392
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
396
393
  ];
397
394
  const err = (0, paper_trade_1.validatePlaceOrder)({ market_id: 564166, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' }, openPositions);
398
- // 0.23 <= 0.30 * 0.80 = 0.24 → allowed (>20% discount)
399
- assert(err === null, `expected success with 20%+ discount even with numeric market_id, got: ${err}`);
395
+ // Same-direction averaging is never allowed, any discount
396
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`);
400
397
  });
401
398
  }
402
399
  // ─── Main ─────────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "polly-gamba",
3
- "version": "1.0.37",
3
+ "version": "1.0.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
@@ -2803,3 +2803,214 @@ npm warn deprecated prebuild-install@7.1.3: No longer maintained. Please contact
2803
2803
  [gamma] Fetching active markets from https://gamma-api.polymarket.com/markets?active=true&closed=false&limit=500
2804
2804
  [gamma] Loaded 493 markets (filtered from 500)
2805
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
@@ -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 () => {
@@ -511,10 +509,10 @@ async function runConcentrationTests(): Promise<void> {
511
509
  async function runTypeCoercionTests(): Promise<void> {
512
510
  console.log('\n## I) Type Coercion — market_id string vs number')
513
511
 
514
- await test('I1: numeric market_id in params matches string market_id in position', async () => {
512
+ await test('I1: numeric market_id in params matches string market_id in position — same-direction blocked', async () => {
515
513
  // Simulates Claude passing market_id as a number (e.g., 564166) while
516
514
  // Redis stores it as a string ("564166"). Without String() coercion,
517
- // sameMarketPositions would be empty → 20% discount check bypassed.
515
+ // sameMarketPositions would be empty → same-direction block bypassed.
518
516
  const openPositions = [
519
517
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
520
518
  ]
@@ -522,12 +520,11 @@ async function runTypeCoercionTests(): Promise<void> {
522
520
  { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.29, new_catalyst: 'New result' },
523
521
  openPositions
524
522
  )
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}`)
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}`)
528
525
  })
529
526
 
530
- await test('I2: numeric market_id with sufficient discount is allowed', async () => {
527
+ await test('I2: numeric market_id same-direction re-entry always blocked regardless of discount', async () => {
531
528
  const openPositions = [
532
529
  makePosition({ market_id: '564166', outcome: 'YES', price: 0.30, ts: 1 })
533
530
  ]
@@ -535,8 +532,8 @@ async function runTypeCoercionTests(): Promise<void> {
535
532
  { market_id: 564166 as any, outcome: 'YES', size_usdc: 10, price: 0.23, new_catalyst: 'Sportsbook update' },
536
533
  openPositions
537
534
  )
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}`)
535
+ // Same-direction averaging is never allowed, any discount
536
+ assert(err !== null, `expected block on same-direction open position, got: ${err}`)
540
537
  })
541
538
  }
542
539