This Polymarket arbitrage trading bot is designed for high-frequency execution during the final seconds of each 5-minute BTC and ETH prediction market epoch. The bot continuously monitors price differences between Chainlink oracle pricing and Polymarket market prices to identify short-term arbitrage and trading opportunities.
Rather than relying on a single market signal, the bot requires confirmation from both BTC and ETH before opening a position. This dual-market confirmation helps reduce false signals and improves trade quality during highly volatile market conditions.
Before executing a trade, the bot evaluates several risk filters, including market liquidity, bid-ask spread, repeated price confirmation, and execution conditions. When all requirements are satisfied, the bot automatically enters either a YES or NO position with low-latency execution.
The strategy focuses on rapid trade management instead of holding positions. After entry, the bot exits quickly using strict risk controls such as time-based exits, volatility protection, and emergency crash detection to minimize exposure before the next trading epoch begins.
Polymarket CLOB V2 bot for BTC and ETH 5-minute Up/Down epoch markets. See V2_MIGRATION.md.
Main runtime files:
src/polybot5m/engine.py — cycle orchestration, schedule, redeemsrc/polybot5m/execution/executor.py — per-market monitor loop, merged wave logssrc/polybot5m/execution/exit_strategy.py — EntryStrategyCoordinator (buy1/buy2 + risk exits)liquidity_maker)MONITOR until epoch end (entries + risk exits)
→ [optional] REDEEM after delay
→ next cycle
/book for YES/NO, optional CLOB user WS (fills), Chainlink strike/spot, strategy logic.redeem_delay_seconds (off by default).Trading and logging run on btc and eth only.
price_feed)| Setting | Purpose |
|---|---|
provider: chainlink |
Strike at epoch start (Polymarket resolution family) |
spot_provider: chainlink |
Live spot for spot_minus_strike and [STRIKE_SPOT] logs |
chainlink_spot_poll_interval_s |
How often spot is refreshed (default 0.15) |
Chainlink credentials belong in env vars (POLYBOT5MBES_PRICE_FEED__CHAINLINK__STREAMS_USER_ID, __STREAMS_SECRET), not committed YAML.
schedule)When schedule.enabled: true, the bot sleeps outside configured UTC windows (default Mon–Sat, two windows per day). Default config has schedule.enabled: false for 24/7 operation.
Schedule gates the entire bot (no monitor cycles while sleeping). It does not gate buy1/buy2 independently — if the bot is running, strategies evaluate on every monitor tick inside the epoch window.
| Strategy | Symbol | Window (t_minus) | Action |
|---|---|---|---|
| buy1 | btc | 60s → 8s | GTC BUY btc YES or NO |
| buy2 | eth | 60s → 8s | GTC BUY eth YES or NO |
| risk1 | btc or eth | after fill | GTC SELL (stop loss vs entry) |
| risk2 | btc or eth | after fill | GTC SELL (time stop) |
| risk3 | btc or eth | after fill | GTC SELL (low bid) |
buy1 and buy2 are independent — both can fire in the same epoch. Each strategy fires at most once per epoch (first side that satisfies monitoring_cycles consecutive ticks). There is no priority between buy1 and buy2.
After a successful risk1, risk2, or risk3 exit (position flat), no new buy1 (btc) or buy2 (eth) on that symbol for the rest of the epoch.
All must pass on every monitoring tick:
trigger_time_end_sec ≤ t_minus ≤ trigger_time_start_sec (uses min remaining time across btc/eth).|spot_minus_strike_btc|, |averge_spot_minus_btc| (epoch average), and |spot_minus_strike_eth| each above configured mins.spot_minus_strike_btc > 0, both averge_spot_minus_btc and spot_minus_strike_eth must be > 0; if < 0, both must be < 0.< max_spread and best_ask > min_best_ask.best_bid > other_symbol_min_best_bid.monitoring_cycles ticks (default 2); first side (YES or NO) to qualify wins and locks that outcome.Order: GTC limit buy at buy_limit_price (default 0.99) for shares (default 5). On order accept, that strategy is marked done for the epoch. FAK no-match retry logic still applies if order_type: FAK is configured; other rejections abort the strategy for the epoch.
After BUY1_ORDER / BUY2_ORDER, fill is confirmed when:
MATCHED / MINED / CONFIRMED) on the bought token, and/orrem_YES / rem_NO increases above the pre-order baseline (paper mode).Logs BUY1_FILL / BUY2_FILL with average fill price and size. Risk monitoring starts only after fill confirmation.
After a confirmed buy fill on btc or eth:
best_bid < fill_price - loss_offset for monitoring_cycles ticks → sell all position shares at best_bid − sell_offset (GTC).max_sell_attempts sell tries (retried on subsequent monitor ticks while exit_busy).Default: enabled: false.
After a confirmed buy fill:
hold_timeout_sec after the first fill timestamp → sell at best_bid − sell_offset (GTC).max_sell_attempts tries.Default: enabled: true, hold_timeout_sec: 10.
After a confirmed buy fill:
best_bid < bid_below for monitoring_cycles ticks → sell at best_bid − sell_offset (GTC).max_sell_attempts tries.Default: enabled: true, bid_below: 0.03.
Priority each tick: risk1 → risk2 → risk3 (first match wins).
RISK1_DONE / RISK2_DONE / RISK3_DONE with flat balance), that symbol is blocked for buy1/buy2 until the next epoch.reset_btc_epoch_stats().config/default.yaml)bot.paper_trading: true (paper) / false (live) — primary mode switch for Windows exe and YAML-only runsbot.dry_run: false (when paper_trading: true, dry_run is ignored)
liquidity_maker.epoch: 5m
liquidity_maker.symbols: [btc, eth]liquidity_maker.cycles: 0 (run forever)liquidity_maker.stagger_delay_seconds: 0liquidity_maker.redeem_enabled: falseliquidity_maker.redeem_async_enabled: trueliquidity_maker.redeem_delay_seconds: 150liquidity_maker.redeem_per_symbol_gap_seconds: 10liquidity_maker.redeem_max_retries: 5liquidity_maker.monitor_poll_interval_s: 0.15liquidity_maker.monitor_rest_book_timeout_s: 3.0liquidity_maker.monitor_balance_refresh_timeout_s: 3.0liquidity_maker.monitor_balance_force_refresh_min_s: 1.0liquidity_maker.monitor_wave_collect_timeout_s: 3.0liquidity_maker.monitor_user_ws_enabled: true (recommended for live fill detection)liquidity_maker.monitor_balance_poll_interval_s: 2.0liquidity_maker.monitor_log_interval_s: 0.15liquidity_maker.monitor_verbose_seconds_before_end: 165liquidity_maker.log_strike_spot_interval_s: 0.15liquidity_maker.spot_minus_strike_difference_rate_lookback_s: 0 (difference_rate not logged)liquidity_maker.trading_process_jsonl: exports/trading_process.jsonlliquidity_maker.trading_process_log_mode: trades (full optional)Merged wave console output per tick:
⏰t_minus=…
[btc/5m] [STRIKE_SPOT] …
[eth/5m] [STRIKE_SPOT] …
[btc/5m] [MARKET_TICK] …
[eth/5m] [MARKET_TICK] …
enabled: truetrigger_time_start_sec / trigger_time_end_sec: 45 / 3max_spread: 0.1, min_best_ask: 0.90spot_minus_strike_btc_abs_min: 40, averge_spot_minus_btc_abs_min: 18, spot_minus_strike_eth_abs_min: 1.0other_symbol_min_best_bid: 0.90monitoring_cycles: 2, shares: 5, buy_limit_price: 0.99, order_type: GTCenabled: truetrigger_time_start_sec / trigger_time_end_sec: 45 / 3spot_minus_strike_btc_abs_min: 55 (stricter than buy1), spot_minus_strike_eth_abs_min: 1.5risk1: enabled: false, loss_offset: 0.20, monitoring_cycles: 3, sell_offset: 0.05, max_sell_attempts: 5risk2: enabled: true, hold_timeout_sec: 10, sell_offset: 0.10, max_sell_attempts: 5risk3: enabled: true, bid_below: 0.03, monitoring_cycles: 3, sell_offset: 0.02, max_sell_attempts: 5schedule.enabled: false (set true to restrict hours)schedule.weekdays: Mon–Sat04:00–11:01, 18:00–22:01config/default.yaml — set bot.paper_trading (true = paper, false = live)..env.example to .env and fill wallet / API / Chainlink secrets.POLY-BTC-ETH-BOT.exe or run from Command Prompt in that folder for Windows.POLY-BTC-ETH-BOT-Mac.exe in that folder for Mac.Expected layout:
Polymarket-BTC-ETH-Bot/
POLY-BTC-ETH-BOT.exe
config/default.yaml
.env
logs/
exports/
The exe reads config/default.yaml next to it and uses bot.paper_trading for paper vs live (no --paper flag needed). CLI flags on Linux still override YAML when passed.
cd Polymarket-BTC-ETH-Bot
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e ".[relayer]"
cp .env.example .env # wallet, CLOB API, builder keys, Chainlink creds
Run:
polybot5m run --paper
Or set bot.paper_trading: true in config/default.yaml and run:
polybot5m run
Without install:
PYTHONPATH=src python3 -m polybot5m.cli run --paper
On a Windows machine with Python 3.11+:
cd Polymarket-BTC-ETH-Bot
.\build\build_windows.ps1
This writes POLY-BTC-ETH-BOT.exe in the repo root. Rebuild after code changes; ship the exe with config/default.yaml and .env.example (users add their own .env).
polybot5m run # live mode (or paper if bot.paper_trading: true in YAML)
polybot5m run --dry-run # no on-chain redeem tx
polybot5m run --paper # paper execution mode (overrides YAML)
polybot5m run -c config/default.yaml --paper
polybot5m run --cycles 5
polybot5m-backtest --help
Paper vs live in config/default.yaml:
bot:
paper_trading: true # paper — simulated fills, safe for testing
# paper_trading: false # live — real CLOB orders (requires .env wallet + API keys)
--paper on the CLI forces paper mode even when YAML says false. Env override: POLYBOT5MBES_BOT__PAPER_TRADING=true.
Config overrides use POLYBOT5MBES_ with __ nesting, for example:
POLYBOT5MBES_EXECUTION__RPC_URLPOLYBOT5MBES_BUY1__ENABLED=falsePOLYBOT5MBES_RISK2__ENABLED=falsePOLYBOT5MBES_SCHEDULE__ENABLED=falsePOLYBOT5MBES_PRICE_FEED__CHAINLINK__STREAMS_USER_IDPOLYBOT5MBES_PRICE_FEED__CHAINLINK__STREAMS_SECRETConsole tags: [MARKET_TICK], [STRIKE_SPOT], strategy events.
Strategy JSONL events (phases BUY1, BUY2, RISK1, RISK2, RISK3):
| Event | Meaning |
|---|---|
BUY1_TRIGGER / BUY2_TRIGGER |
Entry conditions met, sending order |
BUY1_ORDER / BUY2_ORDER |
Order accepted, awaiting fill |
BUY1_FILL / BUY2_FILL |
Fill confirmed |
BUY1_FAK_NO_MATCH / BUY2_FAK_NO_MATCH |
FAK had no match; may retry |
BUY1_ABORT / BUY2_ABORT |
Order rejected (non-FAK-no-match) |
RISK1_TRIGGER / RISK2_TRIGGER / RISK3_TRIGGER |
Risk exit started |
RISK*_ORDER / RISK*_RETRY |
Sell order placed |
RISK*_DONE |
Position flat; symbol blocked for rest of epoch |
RISK*_FAIL / RISK*_RETRY_FAIL |
Sell attempt rejected |
Export path: liquidity_maker.trading_process_jsonl. Optional file logging: bot.log_file + bot.log_timestamp_name.
```bash
python3 -c "
from polybot5m.config import load_config
c = load_config('config/default.yaml')
print('markets:', [(m.symbol, m.epoch) for m in c.liquidity_maker.markets])
print('spot:', c.price_feed.spot_provider, 'strike:', c.price_feed.provider)
print('paper_trading:', c.bot.paper_trading)
print('buy1:', c.buy1.enabled, 'buy
2:', c.buy2.enabled)
print('risk1:', c.risk1.enabled, 'risk2:', c.risk2.enabled, 'risk3:', c.risk3.enabled)
print('redeem:', c.li
$ claude mcp add Polymarket-Arbitrage-Trading-Bot \
-- python -m otcore.mcp_server <graph>