Skill: guohongbin-git/polymarket-arbitrage-cn (version 1.0.0)

--- name: polymarket-arbitrage-cn description: "Polymarket 套利 | Polymarket Arbitrage. 预测市场套利机会 | Prediction market arbitrage opportunities. 自动发现价格差异 | Auto discover price differences. 触发词：Polymarket、预测市场、套利、arbitrage." metadata: openclaw: emoji: 📊 fork-of: "https://clawhub.ai" --- # Polymarket Arbitrage Find and execute arbitrage opportunities on Polymarket prediction markets. ## Quick Start ### 1. Paper Trading (Recommended First Step) Run a single scan to see current opportunities: ```bash cd skills/polymarket-arbitrage pip install requests beautifulsoup4 python scripts/monitor.py --once --min-edge 3.0 ``` View results in `polymarket_data/arbs.json` ### 2. Continuous Monitoring Monitor every 5 minutes and alert on new opportunities: ```bash python scripts/monitor.py --interval 300 --min-edge 3.0 ``` Stop with `Ctrl+C` ### 3. Understanding Results Each detected arbitrage includes: - **net_profit_pct**: Edge after 2% fees - **risk_score**: 0-100, lower is better - **volume**: Market liquidity - **action**: What to do (buy/sell all outcomes) Good opportunities: - Net profit: 3-5%+ - Risk score: <50 - Volume: $1M+ - Type: `math_arb_buy` (safer) ## Arbitrage Types Detected ### Math Arbitrage (Primary Focus) **Type A: Buy All Outcomes** (prob sum < 100%) - Safest type - Guaranteed profit if executable - Example: 48% + 45% = 93% → 7% edge, ~5% net after fees **Type B: Sell All Outcomes** (prob sum > 100%) - Riskier (requires liquidity) - Need capital to collateralize - Avoid until experienced See `references/arbitrage_types.md` for detailed examples and strategies. ### Cross-Market Arbitrage Same event priced differently across markets (not yet implemented - requires semantic matching). ### Orderbook Arbitrage Requires real-time orderbook data (homepage shows midpoints, not executable prices). ## Scripts ### fetch_markets.py Scrape Polymarket homepage for active markets. ```bash python scripts/fetch_markets.py --output markets.json --min-volume 50000 ``` Returns JSON with market probabilities, volumes, and metadata. ### detect_arbitrage.py Analyze markets for arbitrage opportunities. ```bash python scripts/detect_arbitrage.py markets.json --min-edge 3.0 --output arbs.json ``` Accounts for: - 2% taker fees (per leg) - Multi-outcome fee multiplication - Risk scoring ### monitor.py Continuous monitoring with alerting. ```bash python scripts/monitor.py --interval 300 --min-edge 3.0 [--alert-webhook URL] ``` Features: - Fetches markets every interval - Detects arbitrage - Alerts on NEW opportunities only (deduplicates) - Saves state to `polymarket_data/` ## Workflow Phases ### Phase 1: Paper Trading (1-2 weeks) **Goal:** Understand opportunity frequency and quality 1. Run monitor 2-3x per day 2. Log opportunities in spreadsheet 3. Check if they're still available when you look 4. Calculate what profit would have been **Decision point:** If seeing 3-5 good opportunities per week, proceed to Phase 2. ### Phase 2: Micro Testing ($50-100 CAD) **Goal:** Learn platform mechanics 1. Create Polymarket account 2. Deposit $50-100 in USDC 3. Manual trades only (no automation) 4. Max $5-10 per opportunity 5. Track every trade in spreadsheet **Decision point:** If profitable after 20+ trades, proceed to Phase 3. ### Phase 3: Scale Up ($500 CAD) **Goal:** Increase position sizes 1. Increase bankroll to $500 2. Max 5% per trade ($25) 3. Still manual execution 4. Implement strict risk management ### Phase 4: Automation (Future) Requires: - Wallet integration (private key management) - Polymarket API or browser automation - Execution logic - Monitoring infrastructure **Only consider after consistently profitable manual trading.** See `references/getting_started.md` for detailed setup instructions. ## Risk Management ### Critical Rules 1. **Maximum position size:** 5% of bankroll per opportunity 2. **Minimum edge:** 3% net (after fees) 3. **Daily loss limit:** 10% of bankroll 4. **Focus on buy arbs:** Avoid sell-side until experienced ### Red Flags - Edge >10% (likely stale data) - Volume <$100k (liquidity risk) - Probabilities recently updated (arb might close) - Sell-side arbs (capital + liquidity requirements) ## Fee Structure Polymarket charges: - **Maker fee:** 0% - **Taker fee:** 2% **Conservative assumption:** 2% per leg (assume taker) **Breakeven calculation:** - 2-outcome market: 2% × 2 = 4% gross edge needed - 3-outcome market: 2% × 3 = 6% gross edge needed - N-outcome market: 2% × N gross edge needed **Target:** 3-5% NET profit (after fees) ## Common Issues ### "High edge but disappeared" Homepage probabilities are stale or represent midpoints, not executable prices. This is normal. Real arbs disappear in seconds. ### "Can't execute at displayed price" Liquidity issue. Low-volume markets show misleading probabilities. Stick to $1M+ volume markets. ### "Edge is too small after fees" Increase `--min-edge` threshold. Try 4-5% for more conservative filtering. ## Files and Data All monitoring data stored in `./polymarket_data/`: - `markets.json` - Latest market scan - `arbs.json` - Detected opportunities - `alert_state.json` - Deduplication state (which arbs already alerted) ## Advanced Topics ### Telegram Integration (Future) Pass webhook URL to monitor script for alerts: ```bash python scripts/monitor.py --alert-webhook "https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>" ``` ### Position Sizing For a 2-outcome math arb with probabilities p₁ and p₂ where p₁ + p₂ < 100%: **Optimal allocation:** - Bet on outcome 1: (100% / p₁) / [(100%/p₁) + (100%/p₂)] of capital - Bet on outcome 2: (100% / p₂) / [(100%/p₁) + (100%/p₂)] of capital This ensures equal profit regardless of which outcome wins. **Simplified rule:** For small edges, split capital evenly across outcomes. ### Execution Speed Arbs disappear fast. If planning automation: - Use websocket connections (not polling) - Place limit orders simultaneously - Have capital pre-deposited - Monitor gas fees on Polygon ## Resources - **Polymarket:** https://polymarket.com - **Documentation:** https://docs.polymarket.com - **API (if available):** Check Polymarket docs - **Community:** Polymarket Discord ## Support For skill issues: - Check `references/arbitrage_types.md` for strategy details - Check `references/getting_started.md` for setup help - Review output files in `polymarket_data/` - Ensure dependencies installed: `pip install requests beautifulsoup4`

path: _meta.json
content: ownerId: kn7897pc660bd33e16504neb5x80exda
slug: polymarket-arbitrage
version: 0.1.0
publishedAt: 1770156353777

path: arbs.json
content: {
  "detected_at": "2026-02-01T16:16:45.236185",
  "min_edge": 0.0,
  "fee_assumption": 2.0,
  "arbitrage_count": 0,
  "arbitrages": []
}

path: polymarket_data/arbs.json
content: {"detected_at": "2026-02-01T16:16:27.572597", "min_edge": 3.0, "fee_assumption": 2.0, "arbitrage_count": 0, "arbitrages": []}

path: polymarket_data/markets.json
content: {"fetched_at": "2026-02-01T16:16:27.458605", "market_count": 6, "markets": [{"market_id": "nfl-sea-ne-2026-02-08", "title": "Seahawks69%Patriots32%", "url": "https://polymarket.com/event/nfl-sea-ne-2026-02-08", "probabilities": [69.0, 32.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 101.0, "fetched_at": "2026-02-01T16:16:27.454855"}, {"market_id": "cs2-furia-mglz-2026-02-01", "title": "0FURIA73%0TheMongolz28%", "url": "https://polymarket.com/event/cs2-furia-mglz-2026-02-01", "probabilities": [73.0, 28.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 101.0, "fetched_at": "2026-02-01T16:16:27.455329"}, {"market_id": "cs2-fut-g2-2026-02-01", "title": "0FUT Esports64%0G237%", "url": "https://polymarket.com/event/cs2-fut-g2-2026-02-01", "probabilities": [64.0, 37.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 101.0, "fetched_at": "2026-02-01T16:16:27.455544"}, {"market_id": "lol-shft-navi-2026-02-01", "title": "0Shifters38%0Natus Vincere63%", "url": "https://polymarket.com/event/lol-shft-navi-2026-02-01", "probabilities": [38.0, 63.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 101.0, "fetched_at": "2026-02-01T16:16:27.455745"}, {"market_id": "lol-sen-dig-2026-02-01", "title": "Sentinels100%Dignitas0%", "url": "https://polymarket.com/event/lol-sen-dig-2026-02-01", "probabilities": [100.0, 0.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 100.0, "fetched_at": "2026-02-01T16:16:27.457382"}, {"market_id": "lol-jdg-al-2026-01-31", "title": "JD Gaming100%Anyone's Legend1%", "url": "https://polymarket.com/event/lol-jdg-al-2026-01-31", "probabilities": [100.0, 1.0], "volume": 0, "volume_str": "", "outcome_count": 2, "prob_sum": 101.0, "fetched_at": "2026-02-01T16:16:27.457628"}]}

path: references/arbitrage_types.md
content: # Arbitrage Types on Polymarket

## Overview

Three main types of arbitrage opportunities exist on prediction markets:

1. **Math Arbitrage** - Probabilities in multi-outcome markets don't sum to 100%
2. **Cross-Market Arbitrage** - Same event priced differently across markets
3. **Orderbook Arbitrage** - Bid/ask spread inefficiencies

---

## 1. Math Arbitrage

### How It Works

In a properly priced market, the sum of all outcome probabilities should equal 100%. When it doesn't, there's an arbitrage opportunity.

### Type A: Probabilities Sum < 100% (Buy All)

**Example:**
- Outcome A: 45%
- Outcome B: 48%
- Total: 93%

**Strategy:** Buy all outcomes
- Cost: $0.93 (45¢ + 48¢)
- Payout: $1.00 (one outcome must win)
- Gross Profit: $0.07 (7%)
- Fees: ~$0.02 (2% × 2 legs = 4%)
- Net Profit: $0.05 (5%)

**Risk:** Very low - guaranteed profit if you can execute at displayed prices

### Type B: Probabilities Sum > 100% (Sell All)

**Example:**
- Outcome A: 58%
- Outcome B: 47%
- Total: 105%

**Strategy:** Sell all outcomes (be the market maker)
- Premium Received: $1.05 (58¢ + 47¢)
- Max Loss: $1.00 (you pay out the winner)
- Gross Profit: $0.05 (5%)
- Fees: ~$0.04 (2% × 2 legs)
- Net Profit: $0.01 (1%)

**Risk:** High - requires liquidity on both sides, and you need collateral

**Why it's riskier:**
- Selling requires finding buyers at your price
- You need capital upfront to collateralize the potential payout
- Slippage can eat the edge quickly

---

## 2. Cross-Market Arbitrage

### How It Works

The same event is priced differently in separate markets.

**Example:**
- Market 1: "Will Bitcoin hit $90k in February?" → Yes: 25%
- Market 2: "Bitcoin price in February" → Hits $90k: 18%

**Strategy:**
- Buy "Yes" at 18% in Market 2
- Sell "Yes" at 25% in Market 1 (or buy "No" at 75%)
- Lock in 7% profit

**Challenges:**
- Hard to detect automatically (requires semantic matching)
- Markets may have different resolution criteria
- Edge often too small after fees

---

## 3. Orderbook Arbitrage

### How It Works

Exploit bid/ask spread inefficiencies in the live orderbook.

**Example:**
- Bid for Outcome A: 48%
- Ask for Outcome B: 51%
- If A and B are the only outcomes, total = 99%

**Strategy:**
- Buy A at 48%, Buy B at 51%
- Total cost: 99¢
- Payout: $1.00
- Profit: 1% (minus fees)

**Challenges:**
- Requires real-time orderbook data (not just homepage probabilities)
- Opportunities disappear in milliseconds
- Need fast execution (bots compete here)
- Homepage percentages are often midpoints, not executable prices

---

## Fee Considerations

Polymarket charges approximately:
- **Maker fee:** 0% (no fee for limit orders)
- **Taker fee:** 2% (market orders)

**Conservative assumption:** 2% total fees per round-trip

**Breakeven threshold:**
- For 2-outcome markets: Need >4% gross edge (2% fee × 2 legs)
- For 3-outcome markets: Need >6% gross edge
- For 4-outcome markets: Need >8% gross edge

**Recommended minimum edge:** 3-5% net (after fees) to account for slippage

---

## Risk Factors

1. **Stale Data** - Homepage prices may lag orderbook
2. **Liquidity** - Can you actually fill the order at displayed price?
3. **Slippage** - Price moves when you place large orders
4. **Smart Money** - You're competing against professional arb bots
5. **Resolution Risk** - Markets may resolve differently than expected
6. **Withdrawal Friction** - Getting money on/off Polymarket takes time

---

## Best Practices

1. **Start with paper trading** - Track opportunities without real money
2. **Focus on high-volume markets** - Better liquidity, less slippage
3. **Require significant edge** - 3%+ net profit minimum
4. **Avoid sell-side arbs initially** - Stick to buying all outcomes
5. **Monitor execution** - Homepage odds ≠ executable prices
6. **Factor in gas fees** - Blockchain transactions cost money


path: references/getting_started.md
content: # Getting Started with Polymarket Arbitrage

## Phase 1: Paper Trading (Recommended)

Before risking real money, monitor markets to understand:
- How often arbs appear
- How quickly they disappear
- Typical edge sizes
- Which market types are most profitable

### Running the Monitor

```bash
cd skills/polymarket-arbitrage

# Install dependencies
pip install requests beautifulsoup4

# Test single run
python scripts/monitor.py --once --min-edge 3.0

# Continuous monitoring (every 5 minutes)
python scripts/monitor.py --interval 300 --min-edge 3.0
```

### Interpreting Results

Good opportunities:
- **Net Profit:** 3-5% or higher
- **Volume:** $1M+ (better liquidity)
- **Risk Score:** <50 (lower is better)
- **Type:** `math_arb_buy` (safer than sell)

Warning signs:
- **Very high edge (>10%):** Likely stale data or error
- **Low volume (<$100k):** Liquidity risk
- **Sell arbs:** Require capital and liquidity

---

## Phase 2: Setting Up a Polymarket Account

### Prerequisites

1. **Crypto Wallet:** MetaMask or WalletConnect
2. **USDC:** Polymarket uses USDC (stablecoin on Polygon network)
3. **Small test amount:** Start with $50-100 CAD

### Steps

1. Go to [polymarket.com](https://polymarket.com)
2. Connect your wallet (MetaMask recommended)
3. Bridge funds to Polygon network (if needed)
4. Buy USDC (or bridge from Ethereum mainnet)
5. Deposit USDC to Polymarket

**Important:** Gas fees on Polygon are low (~$0.01), but bridging from Ethereum can cost $5-50 depending on congestion.

---

## Phase 3: Manual Trading

Once you've:
- Monitored for 1-2 weeks
- Seen consistent opportunities
- Understand the patterns

Start with **manual trading**:

1. Run monitor to find opportunities
2. Review the arbitrage details
3. Manually verify on Polymarket website
4. Execute trades if prices still valid
5. Track P&L in a spreadsheet

**Why manual first?**
- Learn the platform mechanics
- Verify the detection logic is sound
- Avoid automated losses from bugs
- Build confidence

---

## Phase 4: Automated Trading (Future)

Once profitable with manual trading, automation makes sense.

**Requirements:**
- Polymarket API access or browser automation
- Wallet integration (private key management)
- Order execution logic
- Monitoring and alerting

**Warning:** Automated trading introduces new risks:
- Bug could lose money quickly
- API rate limits
- Execution failures
- Smart contract risks

---

## Risk Management Guidelines

### Position Sizing

- **Never risk more than 5% of bankroll on one arb**
- Example: $500 bankroll → max $25 per opportunity
- This allows for 20 independent opportunities

### Edge Requirements

- **Minimum net edge:** 3% after fees
- **Preferred edge:** 4-5%+
- Higher edge = more margin for error

### Loss Limits

- **Daily loss limit:** 10% of bankroll
- Example: $500 bankroll → stop at -$50/day
- Prevents catastrophic losses from bugs or bad data

### Diversification

- Don't put all capital in one market category
- Spread across politics, sports, crypto, etc.
- Reduces correlation risk

---

## Common Pitfalls

1. **Chasing stale arbs** - Homepage prices lag the orderbook
2. **Ignoring liquidity** - Can't fill large orders at displayed price
3. **Forgetting fees** - 2% × N outcomes adds up fast
4. **Overconfidence** - Small sample sizes can be misleading
5. **Not tracking P&L** - Need accurate records for tax and learning

---

## Recommended Workflow

**Week 1-2:** Paper trading
- Run monitor 2-3x/day
- Log all opportunities in a spreadsheet
- Track which ones would have been profitable

**Week 3-4:** Micro testing ($50-100)
- Manual trades only
- Max $5-10 per opportunity
- Focus on learning, not profit

**Month 2+:** Scale up gradually
- Increase bankroll if profitable
- Still manual execution
- Keep detailed records

**Month 3+:** Consider automation
- Only if consistently profitable (>10% monthly ROI)
- Start with small limits
- Monitor closely

---

## Tools and Resources

### Essential

- **Python 3.8+** for running scripts
- **Spreadsheet** for P&L tracking
- **Telegram** for alerts (optional)

### Helpful

- **Polymarket Discord** - Community insights
- **DeFi news** - Market-moving events
- **Gas tracker** - Polygon gas prices

### Advanced (Future)

- **Polymarket API** - Direct order placement
- **Browser automation** - Selenium/Playwright
- **Database** - PostgreSQL for historical tracking

---

## Next Steps

1. **Install dependencies:** `pip install requests beautifulsoup4`
2. **Run first scan:** `python scripts/monitor.py --once`
3. **Review results:** Check `polymarket_data/arbs.json`
4. **Paper trade:** Run monitor daily for 1-2 weeks
5. **Decide:** Continue if opportunities look promising

Questions? Check `references/arbitrage_types.md` for strategy details.


path: scripts/detect_arbitrage.py
content: #!/usr/bin/env python3
"""
Detect arbitrage opportunities in Polymarket markets.

Usage:
    python detect_arbitrage.py markets.json [--min-edge 3.0] [--output arbs.json]

Types of arbitrage detected:
    1. Math Arb: Multi-outcome markets where probabilities don't sum to 100%
    2. Cross-Market Arb: Same event in different markets with contradictory odds
    3. Orderbook Arb: Bid/ask spreads with profitable crossover (requires orderbook data)
"""

import json
import sys
import argparse
from datetime import datetime


# Polymarket fee structure
MAKER_FEE = 0.00  # 0% maker fee
TAKER_FEE = 0.02  # 2% taker fee
TOTAL_FEES = 0.02  # Conservative estimate (assume taker fees)


def load_markets(filepath):
    """Load markets from JSON file."""
    try:
        with open(filepath, 'r') as f:
            data = json.load(f)
            return data.get('markets', [])
    except Exception as e:
        print(f"Error loading markets: {e}", file=sys.stderr)
        sys.exit(1)


def detect_math_arb(markets, min_edge=3.0):
    """
    Detect math arbitrage in multi-outcome markets.
    
    If probabilities sum to <100%, we can buy all outcomes profitably.
    If probabilities sum to >100%, we can sell all outcomes profitably (requires liquidity).
    
    Args:
        markets: List of market dicts
        min_edge: Minimum edge percentage after fees
    
    Returns:
        List of arbitrage opportunities
    """
    arbs = []
    
    for market in markets:
        # Only consider multi-outcome markets (2+ outcomes)
        if market['outcome_count'] < 2:
            continue
        
        prob_sum = market['prob_sum']
        
        # Type 1: Probabilities sum to less than 100% (buy all outcomes)
        if prob_sum < 100:
            implied_profit_pct = 100 - prob_sum
            
            # Account for fees (need to pay fees on all legs)
            net_profit_pct = implied_profit_pct - (TOTAL_FEES * 100 * market['outcome_count'])
            
            if net_profit_pct >= min_edge:
                arbs.append({
                    'type': 'math_arb_buy',
                    'market_id': market['market_id'],
                    'title': market['title'],
                    'url': market['url'],
                    'prob_sum': prob_sum,
                    'implied_profit_pct': implied_profit_pct,
                    'net_profit_pct': net_profit_pct,
                    'volume': market['volume'],
                    'probabilities': market['probabilities'],
                    'action': 'BUY all outcomes',
                    'risk_score': calculate_risk_score(market, net_profit_pct)
                })
        
        # Type 2: Probabilities sum to more than 100% (sell all outcomes)
        elif prob_sum > 100:
            implied_profit_pct = prob_sum - 100
            net_profit_pct = implied_profit_pct - (TOTAL_FEES * 100 * market['outcome_count'])
            
            if net_profit_pct >= min_edge:
                arbs.append({
                    'type': 'math_arb_sell',
                    'market_id': market['market_id'],
                    'title': market['title'],
                    'url': market['url'],
                    'prob_sum': prob_sum,
                    'implied_profit_pct': implied_profit_pct,
                    'net_profit_pct': net_profit_pct,
                    'volume': market['volume'],
                    'probabilities': market['probabilities'],
                    'action': 'SELL all outcomes (requires liquidity)',
                    'risk_score': calculate_risk_score(market, net_profit_pct, is_sell=True)
                })
    
    return arbs


def calculate_risk_score(market, net_profit_pct, is_sell=False):
    """
    Calculate risk score for an arbitrage opportunity.
    
    Lower score = lower risk
    
    Factors:
        - Volume (higher = lower risk)
        - Edge size (higher = lower risk usually, but could indicate stale data)
        - Sell vs Buy (sell is riskier due to liquidity requirements)
    """
    score = 50  # Base score
    
    # Volume factor (higher volume = lower risk)
    if market['volume'] > 10_000_000:
        score -= 20
    elif market['volume'] > 1_000_000:
        score -= 10
    elif market['volume'] < 100_000:
        score += 20
    
    # Edge factor (very high edge might be stale/wrong data)
    if net_profit_pct > 10:
        score += 15  # Suspiciously high edge
    elif net_profit_pct > 5:
        score += 5
    elif net_profit_pct < 3.5:
        score -= 5  # Tight edge, might close quickly
    
    # Sell arbs are riskier (liquidity requirements)
    if is_sell:
        score += 25
    
    return max(0, min(100, score))


def detect_cross_market_arb(markets, min_edge=3.0):
    """
    Detect cross-market arbitrage (same event, different markets).
    
    This requires identifying markets about the same event.
    For MVP, we'll use simple keyword matching.
    """
    # TODO: Implement cross-market detection
    # This requires more sophisticated market matching
    return []


def main():
    parser = argparse.ArgumentParser(description='Detect Polymarket arbitrage')
    parser.add_argument('markets_file', help='Input markets JSON file')
    parser.add_argument('--min-edge', type=float, default=3.0, 
                        help='Minimum edge percentage after fees (default: 3.0)')
    parser.add_argument('--output', default='arbs.json', help='Output JSON file')
    parser.add_argument('--pretty', action='store_true', help='Pretty print JSON')
    
    args = parser.parse_args()
    
    print(f"Loading markets from {args.markets_file}...", file=sys.stderr)
    markets = load_markets(args.markets_file)
    
    print(f"Analyzing {len(markets)} markets for arbitrage...", file=sys.stderr)
    print(f"Minimum edge: {args.min_edge}% (after {TOTAL_FEES*100}% fees)", file=sys.stderr)
    
    # Detect different types of arbitrage
    math_arbs = detect_math_arb(markets, args.min_edge)
    cross_arbs = detect_cross_market_arb(markets, args.min_edge)
    
    all_arbs = math_arbs + cross_arbs
    
    # Sort by net profit percentage (highest first)
    all_arbs.sort(key=lambda x: x['net_profit_pct'], reverse=True)
    
    print(f"\nFound {len(all_arbs)} arbitrage opportunities:", file=sys.stderr)
    print(f"  - Math arb (buy): {len([a for a in math_arbs if a['type'] == 'math_arb_buy'])}", file=sys.stderr)
    print(f"  - Math arb (sell): {len([a for a in math_arbs if a['type'] == 'math_arb_sell'])}", file=sys.stderr)
    
    # Write output
    output_data = {
        'detected_at': datetime.utcnow().isoformat(),
        'min_edge': args.min_edge,
        'fee_assumption': TOTAL_FEES * 100,
        'arbitrage_count': len(all_arbs),
        'arbitrages': all_arbs
    }
    
    with open(args.output, 'w') as f:
        if args.pretty:
            json.dump(output_data, f, indent=2)
        else:
            json.dump(output_data, f)
    
    print(f"\nSaved to {args.output}", file=sys.stderr)
    
    # Print summary
    if all_arbs:
        print("\n=== TOP ARBITRAGE OPPORTUNITIES ===", file=sys.stderr)
        for i, arb in enumerate(all_arbs[:5], 1):
            print(f"\n{i}. {arb['title'][:60]}...", file=sys.stderr)
            print(f"   Type: {arb['type']}", file=sys.stderr)
            print(f"   Net Profit: {arb['net_profit_pct']:.2f}%", file=sys.stderr)
            print(f"   Volume: ${arb['volume']:,}", file=sys.stderr)
            print(f"   Risk Score: {arb['risk_score']}/100", file=sys.stderr)
            print(f"   Action: {arb['action']}", file=sys.stderr)
    
    print(json.dumps({
        'success': True,
        'arbitrage_count': len(all_arbs),
        'output_file': args.output
    }))


if __name__ == '__main__':
    main()


path: scripts/fetch_markets.py
content: #!/usr/bin/env python3
"""
Fetch active markets from Polymarket.

Usage:
    python fetch_markets.py [--output markets.json] [--min-volume 1000]

Output:
    JSON file containing market data with probabilities and volumes
"""

import json
import sys
import argparse
from datetime import datetime

try:
    import requests
    from bs4 import BeautifulSoup
except ImportError:
    print("Error: Missing dependencies. Install with:", file=sys.stderr)
    print("  pip install requests beautifulsoup4", file=sys.stderr)
    sys.exit(1)


def fetch_polymarket_homepage():
    """Fetch and parse the Polymarket homepage."""
    url = "https://polymarket.com"
    headers = {
        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36'
    }
    
    try:
        response = requests.get(url, headers=headers, timeout=10)
        response.raise_for_status()
        return response.text
    except requests.RequestException as e:
        print(f"Error fetching Polymarket: {e}", file=sys.stderr)
        sys.exit(1)


def parse_market_volume(volume_str):
    """Convert volume string like '$8m' to integer."""
    if not volume_str:
        return 0
    
    volume_str = volume_str.strip().lower().replace('$', '').replace(',', '')
    
    multipliers = {'k': 1000, 'm': 1_000_000, 'b': 1_000_000_000}
    
    for suffix, mult in multipliers.items():
        if suffix in volume_str:
            try:
                return int(float(volume_str.replace(suffix, '')) * mult)
            except ValueError:
                return 0
    
    try:
        return int(float(volume_str))
    except ValueError:
        return 0


def extract_markets(html):
    """Extract market data from Polymarket HTML."""
    soup = BeautifulSoup(html, 'html.parser')
    markets = []
    
    # Find all market links
    market_links = soup.find_all('a', href=lambda x: x and '/event/' in x)
    
    for link in market_links:
        href = link.get('href', '')
        if not href.startswith('/event/'):
            continue
        
        # Extract market ID from URL
        market_id = href.replace('/event/', '').split('/')[0]
        
        # Get market title (try to find it in the link or parent)
        title = link.get_text(strip=True)
        if not title or len(title) < 5:
            # Try parent elements
            parent = link.parent
            if parent:
                title = parent.get_text(strip=True)[:200]
        
        # Look for probabilities (percentage signs)
        prob_elements = link.find_all(string=lambda x: x and '%' in str(x))
        probabilities = []
        for prob in prob_elements:
            try:
                prob_val = float(str(prob).replace('%', '').strip())
                probabilities.append(prob_val)
            except ValueError:
                continue
        
        # Look for volume
        volume_str = ''
        vol_elements = link.find_all(string=lambda x: x and 'Vol' in str(x))
        if vol_elements:
            volume_str = vol_elements[0].strip()
        
        volume = parse_market_volume(volume_str)
        
        if title and probabilities:
            markets.append({
                'market_id': market_id,
                'title': title,
                'url': f'https://polymarket.com{href}',
                'probabilities': probabilities,
                'volume': volume,
                'volume_str': volume_str,
                'outcome_count': len(probabilities),
                'prob_sum': sum(probabilities),
                'fetched_at': datetime.utcnow().isoformat()
            })
    
    return markets


def filter_markets(markets, min_volume=0):
    """Filter markets by minimum volume."""
    if min_volume > 0:
        markets = [m for m in markets if m['volume'] >= min_volume]
    
    # Remove duplicates (same market_id)
    seen = set()
    unique_markets = []
    for m in markets:
        if m['market_id'] not in seen:
            seen.add(m['market_id'])
            unique_markets.append(m)
    
    return unique_markets


def main():
    parser = argparse.ArgumentParser(description='Fetch Polymarket markets')
    parser.add_argument('--output', default='markets.json', help='Output JSON file')
    parser.add_argument('--min-volume', type=int, default=0, help='Minimum volume filter')
    parser.add_argument('--pretty', action='store_true', help='Pretty print JSON')
    
    args = parser.parse_args()
    
    print("Fetching Polymarket homepage...", file=sys.stderr)
    html = fetch_polymarket_homepage()
    
    print("Parsing markets...", file=sys.stderr)
    markets = extract_markets(html)
    
    print(f"Found {len(markets)} markets", file=sys.stderr)
    
    markets = filter_markets(markets, args.min_volume)
    
    print(f"After filtering: {len(markets)} markets", file=sys.stderr)
    
    # Write output
    output_data = {
        'fetched_at': datetime.utcnow().isoformat(),
        'market_count': len(markets),
        'markets': markets
    }
    
    with open(args.output, 'w') as f:
        if args.pretty:
            json.dump(output_data, f, indent=2)
        else:
            json.dump(output_data, f)
    
    print(f"Saved to {args.output}", file=sys.stderr)
    
    # Print summary to stdout
    print(json.dumps({
        'success': True,
        'market_count': len(markets),
        'output_file': args.output
    }))


if __name__ == '__main__':
    main()


path: scripts/monitor.py
content: #!/usr/bin/env python3
"""
Continuous monitoring for Polymarket arbitrage opportunities.

Usage:
    python monitor.py [--interval 300] [--min-edge 3.0] [--alert-webhook URL]

Features:
    - Continuous market scanning
    - Arbitrage detection
    - Alerts for new opportunities
    - Persistence of results
"""

import json
import time
import sys
import argparse
import subprocess
from datetime import datetime
from pathlib import Path


def run_command(cmd, description=""):
    """Run a shell command and return the output."""
    if description:
        print(f"[{datetime.now().strftime('%H:%M:%S')}] {description}", file=sys.stderr)
    
    try:
        result = subprocess.run(
            cmd,
            shell=True,
            capture_output=True,
            text=True,
            timeout=60
        )
        
        if result.returncode != 0:
            print(f"Error: {result.stderr}", file=sys.stderr)
            return None
        
        return result.stdout
    except subprocess.TimeoutExpired:
        print(f"Timeout running: {cmd}", file=sys.stderr)
        return None
    except Exception as e:
        print(f"Exception: {e}", file=sys.stderr)
        return None


def load_json_file(filepath):
    """Load JSON file safely."""
    try:
        with open(filepath, 'r') as f:
            return json.load(f)
    except Exception as e:
        print(f"Error loading {filepath}: {e}", file=sys.stderr)
        return None


def save_alert_state(filepath, arb_ids):
    """Save alerted arbitrage IDs to avoid duplicate alerts."""
    try:
        with open(filepath, 'w') as f:
            json.dump({'alerted_ids': list(arb_ids), 'updated_at': datetime.utcnow().isoformat()}, f)
    except Exception as e:
        print(f"Error saving alert state: {e}", file=sys.stderr)


def load_alert_state(filepath):
    """Load previously alerted arbitrage IDs."""
    if not Path(filepath).exists():
        return set()
    
    data = load_json_file(filepath)
    if data:
        return set(data.get('alerted_ids', []))
    return set()


def send_alert(arb, webhook_url=None):
    """Send alert for arbitrage opportunity."""
    message = f"""
🚨 ARBITRAGE OPPORTUNITY DETECTED

{arb['title'][:80]}

Type: {arb['type']}
Net Profit: {arb['net_profit_pct']:.2f}% (after fees)
Volume: ${arb['volume']:,}
Risk Score: {arb['risk_score']}/100

Action: {arb['action']}

URL: {arb['url']}

Probabilities: {arb['probabilities']}
Sum: {arb['prob_sum']}%
"""
    
    print(message, file=sys.stderr)
    
    # TODO: Implement webhook alerts (Telegram, Discord, etc.)
    if webhook_url:
        print(f"[ALERT] Would send to webhook: {webhook_url}", file=sys.stderr)


def monitor_loop(interval, min_edge, alert_webhook, data_dir):
    """Main monitoring loop."""
    data_dir = Path(data_dir)
    data_dir.mkdir(parents=True, exist_ok=True)
    
    markets_file = data_dir / 'markets.json'
    arbs_file = data_dir / 'arbs.json'
    alert_state_file = data_dir / 'alert_state.json'
    
    alerted_ids = load_alert_state(alert_state_file)
    
    print(f"Starting Polymarket arbitrage monitor", file=sys.stderr)
    print(f"  Interval: {interval}s", file=sys.stderr)
    print(f"  Min Edge: {min_edge}%", file=sys.stderr)
    print(f"  Data Dir: {data_dir}", file=sys.stderr)
    print(f"  Previously alerted: {len(alerted_ids)} opportunities", file=sys.stderr)
    print("", file=sys.stderr)
    
    iteration = 0
    
    while True:
        iteration += 1
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        
        print(f"\n{'='*60}", file=sys.stderr)
        print(f"[{timestamp}] Iteration #{iteration}", file=sys.stderr)
        print(f"{'='*60}", file=sys.stderr)
        
        # Step 1: Fetch markets
        script_dir = Path(__file__).parent
        fetch_cmd = f"python3 {script_dir}/fetch_markets.py --output {markets_file} --min-volume 50000"
        run_command(fetch_cmd, "Fetching markets...")
        
        # Step 2: Detect arbitrage
        detect_cmd = f"python3 {script_dir}/detect_arbitrage.py {markets_file} --min-edge {min_edge} --output {arbs_file}"
        run_command(detect_cmd, "Detecting arbitrage...")
        
        # Step 3: Load results
        arbs_data = load_json_file(arbs_file)
        
        if arbs_data:
            arbs = arbs_data.get('arbitrages', [])
            
            print(f"\nFound {len(arbs)} arbitrage opportunities", file=sys.stderr)
            
            # Check for new opportunities
            new_arbs = []
            for arb in arbs:
                arb_id = f"{arb['market_id']}_{arb['type']}"
                if arb_id not in alerted_ids:
                    new_arbs.append(arb)
                    alerted_ids.add(arb_id)
            
            if new_arbs:
                print(f"\n🔔 {len(new_arbs)} NEW opportunities!", file=sys.stderr)
                for arb in new_arbs:
                    send_alert(arb, alert_webhook)
                
                # Save alert state
                save_alert_state(alert_state_file, alerted_ids)
            else:
                print("No new opportunities (all previously seen)", file=sys.stderr)
        
        # Wait for next iteration
        print(f"\nSleeping for {interval}s...", file=sys.stderr)
        time.sleep(interval)


def main():
    parser = argparse.ArgumentParser(description='Monitor Polymarket for arbitrage')
    parser.add_argument('--interval', type=int, default=300, 
                        help='Scan interval in seconds (default: 300)')
    parser.add_argument('--min-edge', type=float, default=3.0, 
                        help='Minimum edge percentage (default: 3.0)')
    parser.add_argument('--alert-webhook', type=str, 
                        help='Webhook URL for alerts')
    parser.add_argument('--data-dir', default='./polymarket_data', 
                        help='Directory for data files (default: ./polymarket_data)')
    parser.add_argument('--once', action='store_true', 
                        help='Run once and exit (no loop)')
    
    args = parser.parse_args()
    
    if args.once:
        # Single run mode (for testing)
        data_dir = Path(args.data_dir)
        data_dir.mkdir(parents=True, exist_ok=True)
        
        script_dir = Path(__file__).parent
        markets_file = data_dir / 'markets.json'
        arbs_file = data_dir / 'arbs.json'
        
        run_command(f"python3 {script_dir}/fetch_markets.py --output {markets_file}")
        run_command(f"python3 {script_dir}/detect_arbitrage.py {markets_file} --min-edge {args.min_edge} --output {arbs_file}")
        
        arbs_data = load_json_file(arbs_file)
        if arbs_data:
            print(json.dumps(arbs_data, indent=2))
    else:
        # Continuous monitoring
        try:
            monitor_loop(args.interval, args.min_edge, args.alert_webhook, args.data_dir)
        except KeyboardInterrupt:
            print("\n\nMonitoring stopped by user", file=sys.stderr)
            sys.exit(0)


if __name__ == '__main__':
    main()


path: test_markets.json
content: {
  "fetched_at": "2026-02-01T16:16:33.730241",
  "market_count": 6,
  "markets": [
    {
      "market_id": "nfl-sea-ne-2026-02-08",
      "title": "Seahawks69%Patriots32%",
      "url": "https://polymarket.com/event/nfl-sea-ne-2026-02-08",
      "probabilities": [
        69.0,
        32.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 101.0,
      "fetched_at": "2026-02-01T16:16:33.726382"
    },
    {
      "market_id": "cs2-furia-mglz-2026-02-01",
      "title": "0FURIA73%0TheMongolz28%",
      "url": "https://polymarket.com/event/cs2-furia-mglz-2026-02-01",
      "probabilities": [
        73.0,
        28.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 101.0,
      "fetched_at": "2026-02-01T16:16:33.726830"
    },
    {
      "market_id": "cs2-fut-g2-2026-02-01",
      "title": "0FUT Esports64%0G237%",
      "url": "https://polymarket.com/event/cs2-fut-g2-2026-02-01",
      "probabilities": [
        64.0,
        37.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 101.0,
      "fetched_at": "2026-02-01T16:16:33.727042"
    },
    {
      "market_id": "lol-shft-navi-2026-02-01",
      "title": "0Shifters38%0Natus Vincere63%",
      "url": "https://polymarket.com/event/lol-shft-navi-2026-02-01",
      "probabilities": [
        38.0,
        63.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 101.0,
      "fetched_at": "2026-02-01T16:16:33.727254"
    },
    {
      "market_id": "lol-sen-dig-2026-02-01",
      "title": "Sentinels100%Dignitas0%",
      "url": "https://polymarket.com/event/lol-sen-dig-2026-02-01",
      "probabilities": [
        100.0,
        0.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 100.0,
      "fetched_at": "2026-02-01T16:16:33.729025"
    },
    {
      "market_id": "lol-jdg-al-2026-01-31",
      "title": "JD Gaming100%Anyone's Legend1%",
      "url": "https://polymarket.com/event/lol-jdg-al-2026-01-31",
      "probabilities": [
        100.0,
        1.0
      ],
      "volume": 0,
      "volume_str": "",
      "outcome_count": 2,
      "prob_sum": 101.0,
      "fetched_at": "2026-02-01T16:16:33.729268"
    }
  ]
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)