A Python copy-trading bot that monitors the most successful traders on Polymarket, identifies their new trades in real time, and copies them with conservative, range-relative risk management designed specifically for prediction markets.
Only ~7.6% of Polymarket wallets are profitable, and academic research (Gómez-Cram, Guo, Jensen & Kung, SSRN #6617059, Apr 2026) finds that the ~3% of accounts with genuine, persistent skill earn it largely by reacting to public news faster than the market — an edge tied to speed of execution, not just strategy, that a bot mirroring trades seconds later is not guaranteed to inherit. This bot identifies historically strong traders via risk-adjusted scoring (not raw PnL) and mirrors their entries with tighter, range-relative risk controls, but treat leaderboard rank as a candidate filter, not a proven, copyable edge — validate with paper mode and your own measurement before risking real capital. See PROFITABILITY_ANALYSIS_JUNE_2026.md for a full analysis.
Paper mode is the only supported runtime mode. polymarket_copier.main::run_bot fails closed for --mode live because this build uses unsupported Polymarket CLOB V1 orders. Do not fund a wallet or configure live credentials for this build.
The remaining readiness discussion describes prerequisites for a future, separately reviewed live client; it is not a path to enable the current build. It still does not prove profitability outside paper mode.
Official Polymarket docs currently recommend py-clob-client-v2 and the deposit-wallet signature_type=3 flow for new API users; this repo's live client still uses py-clob-client>=0.34,<1.0, so current-doc auth parity is not complete.
Any future live client requires a venue-specific legal review, a held-out backtest with positive net expectancy after spread/slippage/fees/latency/no-fills, realistic paper/live execution reports from order-book snapshots, and a minimal-fund proof of the exact SDK/auth path. US/Georgia operators should treat the international CLOB endpoint as a venue mismatch, not a config problem.
1. DISCOVER Fetch the Polymarket leaderboard, score traders by
a weighted sum of capped Sharpe, consistency, and recency
(not raw PnL)
Dual-window filter: traders must rank in both all-time
and recent 30-day windows to qualify
2. MONITOR Poll tracked wallets every 8s for new BUY trades
WebSocket feeds real-time prices for open positions
Per-wallet activity cache reduces redundant API calls
3. COPY Mirror entries at a flat 50% of source size, capped at 2%
of bankroll (Kelly-capable, but disabled by default — see below)
Skip if price moved >2%, volume <$5K, or market resolves <24h
Fee + spread checked before copying
4. MANAGE Range-relative TP/SL (not flat %), trailing stop,
per-market 8% exposure cap, daily loss circuit breaker
In-memory position cache for zero-latency WS exits
5. EXIT Automated exits on TP, SL, trailing stop, time exit,
resolution blackout, or daily loss limit
Source-exit mirroring when tracked trader exits
Polymarket tokens are bounded in [0, 1]. Flat-percentage TP/SL breaks at extremes:
| Entry | Naive 15% TP | Range-Relative TP | Range-Relative SL |
|---|---|---|---|
| $0.20 | $0.23 | $0.52 (40% of $0.80 upside) | $0.15 |
| $0.50 | $0.575 | $0.70 (40% of $0.50 upside) | $0.375 |
| $0.82 | $0.943 | $0.892 (40% of $0.18 upside) | $0.748 |
| $0.97 | $1.12 (impossible) | $1.00 (clamped) | $0.94 |
Traders are ranked by the weighted sum (4.0 * Sharpe_proxy + 3.5 * Consistency + 2.5 * Recency_weight) / 10, not a product and not raw PnL. This filters out lucky concentrated bettors in favor of consistently profitable traders across many markets. Additional guards:
- Sharpe proxy capped at 3.0 and shrunk for small samples to prevent outlier amplification
- Traders must rank in both the all-time and trailing 30-day leaderboard windows
- Expectancy / profit-factor weighted instead of raw win rate to avoid favorite-buyer bias
Position sizes default to a flat 50% of source size (size_multiplier), capped at 2% of bankroll per trade. Setting kelly_enabled: true in config.yaml switches sizing to fractional Kelly. Once a trader has at least kelly_min_trades bot-closed trades, sizing uses that trader's observed portfolio win rate. Before that sample exists, kelly_seed_from_tracker: true can seed from tracker mean ROI with time decay. Kelly is off by default because both paths inherit measurement bias (see PROFITABILITY_ANALYSIS_JUNE_2026.md §4.2–4.3); the 2% hard cap bounds the damage either way.
- WebSocket feeds real-time prices for positions we hold (sub-second latency for exits)
- REST polling detects new trades from tracked wallets (the WS API doesn't filter by wallet)
- Shared
aiohttp.ClientSessionwith keep-alive across all API clients eliminates redundant TLS handshakes
polymarket_copier/
├── main.py # Async CLI entrypoint, startup sequence, supervisor loops
├── config.py # Pydantic v2 settings from .env + config.yaml
├── api/
│ ├── data_client.py # Polymarket Data API (leaderboard, wallet activity)
│ ├── gamma_client.py # Gamma API (markets, resolve times, prices)
│ └── clob_client.py # CLOB API (order placement, depth checks, paper fills)
├── core/
│ ├── tracker.py # Trader discovery, dual-window scoring, activity cache
│ ├── monitor.py # WebSocket + REST trade/price monitor, WS reconnect
│ ├── copier.py # Copy-trade decision engine, entry/exit lock guards
│ ├── risk_manager.py # Range-relative TP/SL, exposure caps, circuit breakers
│ ├── portfolio.py # SQLite (WAL mode) position persistence, composite indexes
│ ├── sizing.py # Fractional Kelly criterion sizing
│ └── metrics.py # Optional Prometheus metrics (prometheus_client)
├── models/
│ └── types.py # Pydantic v2 models (Market, Order)
└── utils/
├── addresses.py # Ethereum address normalization (lowercase at all ingestion points)
└── logger.py # Structured JSON logging for downstream analysis
- Python 3.10+ (CI runs 3.11 and 3.12)
- No wallet or private key is required for supported paper mode
git clone https://github.com/CGFixIT/PolyMarket_Mimic_Trader.git
cd PolyMarket_Mimic_Trader
pip install -r requirements.txtcp .env.example .envFor paper mode, no private key or API credentials are needed. Keep any future credentials in an untracked .env; never commit them:
POLY_PRIVATE_KEY= # Your Polygon wallet private key (live mode only)
POLY_API_KEY= # L2 API key (auto-derived if blank)
POLY_API_SECRET= # L2 API secret
POLY_API_PASSPHRASE= # L2 API passphrase
POLY_SIGNATURE_TYPE= # Deposit-wallet signing type, if required
POLY_FUNDER= # Deposit-wallet funder address, if required
BANKROLL=500 # Starting bankroll in USDCAll trading parameters are in config.yaml. The defaults are conservative:
| Parameter | Default | Description |
|---|---|---|
size_multiplier |
0.5 | Copy at 50% of source trade size (Kelly overrides when enabled) |
max_trade_pct |
0.02 | Max 2% of bankroll per trade |
tp_range_fraction |
0.40 | Take profit at 40% of remaining upside |
sl_range_fraction |
0.25 | Stop loss at 25% of remaining downside |
trailing_stop_fraction |
0.40 | Give back 40% of the run-up from entry, floored at hard SL |
max_market_exposure_pct |
0.08 | Max 8% of bankroll in any single market |
max_trader_allocation |
0.05 | Max 5% of bankroll copied from any single trader |
daily_loss_limit_pct |
0.03 | Halt all trading after 3% daily loss (resets at UTC midnight) |
resolution_blackout_hours |
24 | Never enter markets resolving within 24h |
max_concurrent_positions |
10 | Maximum open positions at once |
max_trade_age_seconds |
12 | Skip trades older than this at detection |
cooldown_after_losses |
3 | Pause new entries after this many consecutive losses |
cooldown_minutes |
60 | Length of the post-loss cooldown |
kelly_enabled |
false | Enable fractional Kelly sizing (requires ≥50 closed trades to activate) |
kelly_min_trades |
50 | Minimum closed trades before Kelly activates |
kelly_fraction_multiplier |
0.25 | Fraction of full Kelly to use (0.25 = quarter-Kelly) |
fail_closed_on_missing_data |
true | Skip a copy when market metadata or price can't be verified |
mirror_source_exits |
true | Exit when the tracked trader exits (SOURCE_EXIT) |
paper_fill_slippage_pct |
0.005 | Half-spread slippage in paper mode (~0.5%) |
paper_taker_fee_rate |
0.08 | Fallback taker fee RATE (used only when live CLOB/Gamma fee data is unavailable); actual fee is price-shaped: rate * price * (1 - price). 0.08 gives a 2% peak at p=0.5, above every real category cap |
metrics_enabled |
false | Enable Prometheus metrics scrape endpoint |
metrics_port |
9090 | Port for Prometheus scrape endpoint |
Paper mode (no real trades, logs what would execute):
python -m polymarket_copier.main --mode paperLive mode is intentionally disabled. python -m polymarket_copier.main --mode live
raises ConfigError before creating an order session. Do not bypass that boundary;
any future live-client work requires separate review of the SDK, auth, venue, legal,
and execution-readiness assumptions above.
The bot enforces multiple layers of protection:
- Range-relative TP/SL — thresholds adapt to token price within [0, 1]
- Trailing stop — locks in profit as price rises, never drops below hard SL
- Time exit — closes stale positions after 48h if price barely moved
- Per-market exposure cap — max 8% of bankroll in any single market
- Per-trader allocation cap — max 5% of bankroll copied from any single trader
- Daily loss circuit breaker — halts all trading after 3% daily loss; resets at UTC midnight
- Resolution blackout — never enters markets resolving within 24h
- Pre-trade depth check — verifies ask-side liquidity before BUY orders (live mode)
- Per-trader drawdown stop — stops copying a trader after cumulative -8% session loss
- Cooldown — pauses new entries after 3 consecutive losing trades
- Staleness gate — skips trades older than 12s at detection
- Fail-closed gating — skips a copy when market metadata or price can't be verified
- Cold-start guard — first poll per wallet seeds a baseline; no copies from the backlog
- Source exit mirroring — exits when the tracked trader exits (aligns holding period)
- Rate-limited poll path —
AsyncLimitergates REST polls to prevent 429s - Realistic paper fills — paper mode applies slippage + taker fee
- Concurrent exit lock — per-position
asyncio.Lockprevents double-SELL race between WebSocket tick and poll sweep - Entry TOCTOU lock — global
asyncio.Lockaround position-count check + open prevents simultaneous wallet polls from both passing the cap - Fee-aware sizing — expected round-trip fee deducted from edge before Kelly sizing
- Wallet address normalization — all Ethereum addresses lowercased at ingestion to prevent case-mixing dict lookup misses
# Run the CI test suite
pytest -v -m "not integration"
# Run only the integration tests
pytest tests/test_integration.py -v
# Run only the risk manager tests
pytest tests/test_risk_manager.py -v
# Run chaos/resilience tests
pytest tests/test_chaos.py -v
# With coverage report
pytest --cov=polymarket_copier --cov-report=term-missingThe test suite includes:
- Unit tests for every module (config, models, API clients, risk manager, tracker, portfolio, copier, monitor, sizing, addresses)
- Integration tests for the full
TradeMonitor → CopyTradercallback chain - Chaos tests — network errors, API 429/500s, malformed data, concurrent exits, WS death + recovery
- All tests run offline with mocked API responses
Polymarket Data API Polymarket CLOB WebSocket
│ │
▼ ▼
TradeMonitor._poll_loop() TradeMonitor._ws_loop()
(detect new trades from (real-time price feed for
tracked wallets, 8s + jitter) subscribed token positions)
│ │
▼ ▼
CopyTrader.handle_trade_event() CopyTrader.handle_price_tick()
(validate, size, place order) (evaluate TP/SL/trail/time exit)
│ │
▼ ▼
RiskManager.build_position() RiskManager.evaluate()
(compute range-relative TP/SL, (check all exit conditions,
enforce exposure cap) update trailing peak)
│ │
▼ ▼
ClobClient.place_order() ClobClient.place_order() [SELL]
(BUY with depth check) (exit the position)
│ │
▼ ▼
PortfolioManager (SQLite) PortfolioManager (SQLite)
(persist open position) (mark closed, record PnL)
- Load config + logger → init
RiskManager,PortfolioManager, sharedaiohttp.ClientSession - Rehydrate market/trader exposure from DB open positions (single DB fetch, passed to both rehydration steps)
- Fetch top traders via
TrackerClient.refresh()(dual-window leaderboard, activity cache) - Launch concurrent tasks:
monitor.run(),rebalance_loop(),exit_check_loop(),metrics_loop(),heartbeat_watchdog(),shutdown_watcher() - SIGINT/SIGTERM → set
shutdown_event→ cancel tasks → print portfolio summary → exit
| API | Base URL | Auth | Purpose |
|---|---|---|---|
| Data API | data-api.polymarket.com |
None | Leaderboard, wallet activity |
| Gamma API | gamma-api.polymarket.com |
None | Market discovery, resolve times |
| CLOB API | clob.polymarket.com |
L1/L2 | Order placement, midpoint prices |
| CLOB WebSocket | ws-subscriptions-clob.polymarket.com |
None | Real-time price feeds |
When metrics_enabled: true in config.yaml, a Prometheus scrape endpoint is exposed on metrics_port (default 9090). Available metrics:
copybot_bankroll_usd— current bankrollcopybot_daily_pnl_usd— today's realized PnLcopybot_open_positions— count of open positionscopybot_open_unrealized_pnl_usd— conservative unrealized PnLcopybot_total_exposure_usd— deployed USDC across open marketscopybot_copies_skipped_total— copies skipped by reason labelcopybot_exits_total— exits by reason label
Structured JSON log events are emitted on the data logger channel for downstream analysis:
position_opened,position_closed— entry/exit with price, size, PnLcopy_skipped— every skipped copy with a stable reason codecircuit_breaker_tripped— daily loss limit hittrader_demoted— trader removed from pool after drawdown
This software is provided for educational and research purposes. Trading on prediction markets carries financial risk. Past performance of copied traders does not guarantee future results. Use paper mode with this build. The authors are not responsible for any financial losses incurred through use of this software.
MIT