Skill: johny0920/polymarket-arbitrage (version 0.1.0)

--- name: polymarket-arbitrage description: Monitor and execute arbitrage opportunities on Polymarket prediction markets. Detects math arbitrage (multi-outcome probability mismatches), cross-market arbitrage (same event different prices), and orderbook inefficiencies. Use when user wants to find or trade Polymarket arbitrage, monitor prediction markets for opportunities, or implement automated trading strategies. Includes risk management, P&L tracking, and alerting. --- # 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: 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: clean | SkillSpector:  ()