Skip to content

hu553in/polybot

Polybot

CI

Polymarket BTC/ETH Up/Down 5m trading bot with Chainlink-aligned price analysis, executable CLOB quotes, paper/live modes, risk limits, SQLite history, and Telegram notifications.

The bot is built for short-window binary markets where the settlement line is the window open price. It chooses a direction from the current move, estimates whether the market price is mispriced after fees, and enters only when signal, execution quality, timing, and risk gates all pass.

⚠️ Financial warning

This bot does not guarantee profit. It can lose money, including all allocated capital. Do not trade live with money you cannot afford to lose.

What it does

  • Tracks BTC and ETH 5-minute Up/Down markets on Polymarket.
  • Uses Gamma for market metadata and CLOB token winners for official settlement data.
  • Uses a fresh CLOB WebSocket order-book cache first, with REST order-book fallback for executable buy VWAP.
  • Uses Chainlink RTDS for runtime open/current/close prices and Binance spot/futures as context data.
  • Scores the move with delta, ATR, micro-momentum, volume heat, multi-timeframe confirmation, higher-timeframe trend, funding bias, market regime, and BTC/ETH correlation.
  • Applies price, edge, quote-age, liquidity, spread, depth, push-risk, timing, and risk guards before entry.
  • Supports paper and live modes.
  • Stores snapshots, decisions, trades, and settlements in SQLite when storage is enabled.
  • Sends Telegram alerts for startup, entries, execution blocks/failures, risk blocks, settlements, and summaries.
  • Adds Telegram /summary and /wallet commands when Telegram is configured.
  • Keeps Telegram notifications event-based; ordinary console logs and debug noise are not mirrored to chat.

Components

  • cli: Typer commands: run, doctor, healthcheck, show-config, and export
  • bot: market loop, entry window handling, decisions, execution, settlement
  • analysis: price sourcing, delta direction, ATR, momentum, volume, higher timeframes, funding
  • probability: transparent heuristic probability model
  • calibration: rolling isotonic probability calibration from settled trades
  • strategy: dynamic delta skip, required edge, and position sizing helpers
  • polymarket: Gamma market lookup, CLOB official resolution lookup, CLOB WS/REST order-book quotes
  • executor: live CLOB buy execution and live order checks
  • risk: run/day/notional/consecutive-loss, drawdown, VaR, and expected shortfall limits
  • storage: SQLite persistence and migrations
  • logging: plain/Rich or structured JSON console logs, Telegram notifications, and run summary

Architecture

flowchart LR
    G["Gamma markets"] --> B["Bot loop"]
    CL["Chainlink RTDS"] --> A["Analysis"]
    BN["Binance candles and funding"] --> A
    CLOB["CLOB WebSocket and REST order book"] --> Q["Executable quote"]
    B --> A
    B --> Q
    A --> D["Decision gates"]
    Q --> D
    D --> R["Risk limits"]
    R --> X["Paper or live buy"]
    X --> S["SQLite, logs, Telegram"]
    G --> O["Official settlement"]
    O --> S
Loading

Trading lifecycle

  1. Compute the next 5-minute close and the corresponding market slug.
  2. Capture the Chainlink window open when possible. Missing or stale Chainlink data pauses entries. The default open/price freshness windows allow normal RTDS publication lag, while still rejecting genuinely old ticks. At a close boundary, the next window open is captured before settling the just-closed window, so settlement latency cannot make the next market miss its price-to-beat.
  3. Wake shortly before close and poll only inside the configured entry window.
  4. Fetch BTC and ETH market state in parallel.
  5. Analyze each market against the settlement line and apply the BTC/ETH correlation filter.
  6. Build a decision from signal quality, execution quote, timing, edge, and risk limits.
  7. Optionally wait for a better edge until the forced-decision cutoff.
  8. Enter once per market slug when every gate passes.
  9. After market close, fetch official settlement and update realized result metrics. If the official result and a fresh Chainlink close tick are both unavailable, keep traded slugs pending and retry instead of finalizing an UNKNOWN result.

Strategy

Direction and base signal

The settlement line is the window open price. If the current price is above it, the considered side is Up; if below it, the side is Down. The absolute move from the open is the main signal.

Very small moves are skipped through POLYBOT_DELTA_SKIP. That threshold is dynamic:

  • late in the window it is multiplied by POLYBOT_DELTA_SKIP_LATE_MULTIPLIER
  • as ATR ratio rises, it is discounted by POLYBOT_DELTA_SKIP_ATR_DISCOUNT_*
  • in high-volatility candles it is multiplied by POLYBOT_DELTA_SKIP_HIGH_VOL_MULTIPLIER

This keeps noise out early in the window while still allowing stronger late moves to pass.

Late relaxed entries have one extra guard: if the move is still below POLYBOT_LATE_WEAK_SIGNAL_DELTA_MAX, the bot requires at least POLYBOT_LATE_WEAK_SIGNAL_MIN_MOMENTUM_SCORE unless volume is strongly aligned. This blocks fragile last-seconds near-line trades where a small reversal can flip the result.

Probability model

The model is a transparent heuristic, not a trained model. It starts from an interpolated probability based on delta strength:

  • POLYBOT_DELTA_SKIP to POLYBOT_DELTA_WEAK
  • POLYBOT_DELTA_WEAK to POLYBOT_DELTA_STRONG
  • POLYBOT_DELTA_STRONG to POLYBOT_DELTA_VERY_STRONG

Then it adjusts probability with context:

  • source quality: stale Chainlink source age is penalized
  • ATR: overheated candles and high ATR ratios reduce confidence
  • micro-momentum: recent 1-minute candles are volume-weighted; acceleration can add confidence
  • volume heat: unusually high aligned Binance volume can add confidence
  • multi-timeframe confirmation: the live 5-minute candle can add confidence or penalize a counter-signal
  • higher timeframes: 15-minute and 1-hour candles can confirm or penalize the 5-minute impulse
  • funding bias: Binance futures funding is treated as a directional crowding signal
  • regime: trending and strong-trending regimes add confidence; high-volatility regimes penalize confidence
  • correlation: BTC/ETH disagreement or a weak peer move dynamically penalizes the weaker signal

When SQLite contains enough settled trades, the final probability is passed through a rolling isotonic calibration curve. This does not make the model predictive by itself; it only shrinks repeated overconfidence or underconfidence observed in the bot's own settled history. Calibration is mode-specific, so live execution is not tuned from paper fills. The bot refreshes that calibrator after settled trades, so a long-running process can adapt without a restart.

Edge and price gates

The entry price is the executable buy VWAP from CLOB asks. Gamma prices are metadata only and are never used as entry quotes.

For each candidate, the bot computes:

breakeven = fee-adjusted entry price
edge = probability - breakeven

The trade must pass:

  • POLYBOT_MIN_CONFIDENCE
  • dynamic required confidence after late-entry and ATR discounts
  • dynamic required edge from POLYBOT_MIN_EDGE
  • POLYBOT_MIN_EDGE_FLOOR after late-entry and ATR discounts
  • per-asset POLYBOT_BTC_ENTRY_PRICE_MIN / POLYBOT_ETH_ENTRY_PRICE_MIN
  • POLYBOT_ENTRY_PRICE_MAX

The bot also estimates near-line push risk. A tiny move can pass the late dynamic delta threshold, but still be rejected if the configured push-risk estimate is too high.

Weak moves just above the dynamic delta threshold are probability-capped before edge math. This prevents momentum/volume bonuses from turning a barely-off-the-line setup into a falsely strong 95%+ signal. Expensive entries also pay a small high-price premium: near the top of the binary price range, the required edge and confidence are slightly higher because one tick of slippage or one small reversal matters more.

If dynamic entry is enabled, a barely valid early trade can be delayed until either edge or early-window delta is clearly stronger, or until the forced-decision cutoff is reached.

Execution quality guards

Before entry, the bot checks that the market is tradable enough for the configured amount:

  • minimum market liquidity
  • known bid/ask spread
  • max spread
  • minimum book depth in shares
  • maximum order book levels consumed
  • maximum VWAP slippage versus best ask
  • fresh CLOB quote age
  • side-collapse guard when the considered side quote suddenly drops
  • top-of-book bid/ask depth imbalance as a probability bonus or penalty
  • one trade attempt per slug by default

Live mode repeats the edge check against the actual live order price before submitting the order. It can also use a stricter live-only VWAP slippage cap through POLYBOT_LIVE_MAX_VWAP_TO_BEST_ASK_BPS. Transient live order submit failures can be retried with a small exponential backoff; rejected or ambiguous orders are not blindly retried. The live taker FAK/FOK BUY flow uses the CLOB market-order API and sends the pUSD amount, matching Polymarket's UI-style cash sizing.

Position sizing

Position size starts from POLYBOT_AMOUNT and changes with edge quality:

  • low edge near the threshold uses POLYBOT_POSITION_SIZE_MIN_MULTIPLIER
  • strong edge can scale toward POLYBOT_POSITION_SIZE_MAX_MULTIPLIER
  • strong-trending regime can scale size up, within the cap
  • high ATR or high-volatility regime reduces size
  • near-line distance reduces size when the move barely cleared the dynamic delta threshold
  • late-window entries can slightly increase size
  • fractional Kelly can cap the computed size using configured bankroll and fraction
  • correlated same-window BTC/ETH exposure can reduce the second same-direction trade

All sizing is still bounded by the risk limits below.

Risk controls

  • POLYBOT_MAX_TRADES: max trades per process run
  • POLYBOT_MAX_TRADES_PER_DAY: max trades per UTC day
  • POLYBOT_MAX_DAILY_NOTIONAL: max daily pUSD notional
  • POLYBOT_MAX_CONSECUTIVE_LOSSES: stop after a configured losing streak
  • POLYBOT_MAX_DRAWDOWN_PCT: pause entries after realized drawdown exceeds the configured budget
  • POLYBOT_VAR_*: block entries when historical tail loss exceeds the configured VaR threshold
  • POLYBOT_EXPECTED_SHORTFALL_MAX_LOSS: block entries when average tail loss is too large
  • POLYBOT_MAX_CORRELATED_EXPOSURE: cap same-window, same-direction BTC/ETH exposure
  • POLYBOT_LIVE_PRIVATE_KEY and POLYBOT_LIVE_WALLET: required only for live execution

The current live strategy is entry-and-settle. It does not actively sell positions before market resolution.

Quick start

Install dependencies, create a local config, inspect it, check external services, and start the bot. With no explicit mode set, run uses paper mode.

make install-deps
cp .env.example .env
uv run polybot show-config
uv run polybot doctor
uv run polybot run

Modes

The two modes are mutually exclusive. With nothing set, the bot defaults to paper so a forgotten env var cannot accidentally place real orders.

Behavior paper live
Calls real CLOB execution no yes
Records TradeRecord to storage yes yes
Writes Snapshot / Decision rows yes yes
Entry price quote VWAP taker limit
Requires POLYBOT_LIVE_PRIVATE_KEY + POLYBOT_LIVE_WALLET no yes

When to pick which:

  • paper: realistic strategy simulation against live market data; default for research.
  • live: actually submits orders. Repeats the edge check against the live order price before posting and can use a stricter live-only VWAP slippage cap.

CLI commands

uv run polybot run           # start the trading loop in the configured mode
uv run polybot doctor        # health check: gamma, clob, clob-ws, RTDS, storage, live env, telegram
uv run polybot healthcheck   # Docker-safe local heartbeat check; exits 0 when fresh, 1 otherwise
uv run polybot show-config   # print every effective setting with its env name (secrets masked)
uv run polybot export        # export all stored trades as CSV to stdout

doctor exits non-zero if any check fails, so it is safe to use as a pre-flight gate in CI or deploy scripts. Run it before switching to live mode.

healthcheck is intentionally smaller than doctor: it only reads the local heartbeat file that the running bot updates in POLYBOT_STORAGE_DATA_DIR, including the last local CLOB WebSocket state. It does not call Polymarket, Binance, Chainlink, or Telegram, so transient third-party outages do not restart the container.

Operational notes

  • Live rollout: keep live mode off until paper metrics look stable.
  • Storage data directory: POLYBOT_STORAGE_DATA_DIR is created automatically when the parent path is writable. The bot stores polybot.sqlite3 and polybot.health.json inside it, so Docker deployments should mount a writable volume and set POLYBOT_STORAGE_DATA_DIR to that mount path.
  • Live wallet setup: new Polymarket accounts commonly use the POLY_1271 / deposit-wallet flow. In live mode, POLYBOT_LIVE_WALLET must be the CLOB funder wallet for the configured private key, and POLYBOT_LIVE_SIGNATURE_TYPE must match that wallet type. For the current deposit-wallet flow this is usually POLYBOT_LIVE_SIGNATURE_TYPE=3.
  • pUSD balance and allowances: doctor checks CLOB-reported pUSD balance and spender allowances. If it reports clob-live: balance/allowance insufficient, the bot cannot trade from the configured wallet yet. Fix the private-key/wallet/signature-type combination first, then approve/sync pUSD allowances before running live.
  • CLOB API key creation warning: the client may print Could not create api key while deriving an existing key. If subsequent CLOB calls work and doctor passes, that line is not a blocker.
  • Probability calibration warm-up: until SQLite has at least POLYBOT_PROBABILITY_CALIBRATION_MIN_SAMPLES settled trades (default 30), the calibrator is a no-op and probabilities come straight from the heuristic. Expect win rate to be noisy for the first ~30 settled trades in the current mode.
  • Chainlink RTDS session recycle: the Polymarket RTDS websocket forces a disconnect after about 120 minutes. The bot pre-emptively recycles the connection at POLYBOT_CHAINLINK_MAX_SESSION_SECONDS (default 105 min), but only at a moment at least POLYBOT_CHAINLINK_RECONNECT_SAFE_DISTANCE_SECONDS (default 30 s) away from any 5-minute close, so the recycle never lands on a settlement boundary. If RTDS ticks become stale, the bot logs the stale/recovery cycle and reconnects the websocket. Telegram is only notified when the stale episode lasts at least POLYBOT_CHAINLINK_TELEGRAM_STALE_SECONDS (default 120 s; 0 disables RTDS Telegram alerts). Entries also pause for a short recovery cooldown after a stale episode so the first fresh tick after reconnect is not treated as an immediately stable feed.
  • CLOB WebSocket cache: the hot path uses the public market-channel websocket for active token books. If the cache is empty or stale, the bot falls back to REST when enabled; stale quotes never become tradable entries. doctor and healthcheck report whether the stream is started, connected, subscribed, and receiving fresh book messages.
  • Settlement source: the bot prefers Polymarket's official result and uses its own close tick if the official one is not available yet. If neither is usable at close, settlement remains pending and is retried on later cycles. Stored trades carry the settlement_source and settlement-line context so you can tell which path resolved each trade.
  • State retention: in-memory caches and slug sets are pruned to the last POLYBOT_STATE_RETENTION_WINDOWS windows, so a long-running process does not grow forever. SQLite history is unbounded.
  • Migrations and backup: storage applies migrations on startup via yoyo. By default a timestamped *.bak-... is created next to the SQLite file before any migration is applied; disable with POLYBOT_STORAGE_BACKUP_BEFORE_MIGRATION=false.
  • Stop and resume: Ctrl-C requests a graceful stop, finishes the current cycle, prints the run summary, and exits with code 130. Restarting the process re-reads recent trades from SQLite to reseed risk drawdown and calibration from the current mode.
  • Telegram visibility: when Telegram is configured, the bot sends startup, entries, results, actionable runtime alerts, and a periodic run summary. Short RTDS stale/recovered cycles stay in logs by default. Set POLYBOT_TELEGRAM_SUMMARY_INTERVAL_SECONDS=0 to disable the periodic summary.

Snapshot one-line log key

Each in-window iteration prints a compact line per market. Reading it left to right:

ETH 47s Down@0.515 U:0.020 D:0.515 px:cl:2313.29 beat:2315.20 o:cl q:ws d:0.0824% p:93.2% be:53.3% e:+39.9% age:120ms qage:80ms s:0.42 skip: ...
Token Meaning
ETH 47s asset and seconds left in the close window
Down@0.515 Polymarket side considered and its current quote
U:0.020 D:0.515 Up/Down market quotes side by side
px: current price source: cl chainlink, -- none
beat: settlement line (window open price)
o: window-open source: cl chainlink, -- missing
q: execution quote source: ws, clob, or -- none
d: absolute move from window open in percent
p: model probability
be: fee-adjusted breakeven probability
e: edge = probability - breakeven
age: current-price source age in milliseconds
qage: execution quote age or REST quote latency in milliseconds
s: compact signal-strength score for logging and sizing context
trailing tag enter, waiting-entry, monitor/no-dir: ..., or skip: ...

Set POLYBOT_LOG_FORMAT=json to emit one JSON object per log event instead of Rich text. The same snapshot data is kept as structured fields, for example crypto, seconds_left, pm_side, pm_price, up_price, down_price, current_price, window_open, quote_source, delta_pct, confidence, breakeven, edge, source_age_ms, quote_age_ms, and signal_strength. Log source and severity are structured as component and level; plain mode renders them as readable prefixes. Telegram still receives only event notifications, not debug/snapshot noise.

Configuration

All runtime settings are environment variables with the POLYBOT_ prefix. .env is loaded automatically, and .env.example contains every available setting with its default or effective default value.

Use uv run polybot show-config to see the parsed values. Non-empty secrets are masked.

Trade export is intentionally CLI-only instead of env-driven:

uv run polybot export
uv run polybot export --output-file exports/polybot-trades.csv
uv run polybot export --since 2026-05-08T00:00:00Z
uv run polybot export --current-run
uv run polybot export --format summary

export reads SQLite in read-only mode and never applies migrations or creates the database. CSV rows include the executable quote context used at entry (quote_source, quote_age_ms, best bid/ask, book imbalance, and levels used). Missing, damaged, and incompatible databases fail with explicit errors.

Development

make install-deps
make lint
make check-types
make test
make check

Releases

No releases published

Packages

 
 
 

Contributors

Languages