Automated trading bot for Polymarket's BTC 15-minute binary prediction markets. Combines XGBoost/LightGBM ML ensemble, 10 technical indicators, smart order routing, and real-time risk management.
Stack: React 19 + Vite 7 (dashboard) · Node.js + PM2 (bot) · XGBoost + LightGBM (ML) · Polymarket CLOB API
| Date | Change |
|---|---|
| Mar 29 | Fix: Limit orders on weekends were always blocked — weekend filter incorrectly treated intentionally-omitted mlConfidence (limit path uses its own 60% ML gate) as "ML unavailable". Now only blocks when model is truly not loaded. |
| Mar 29 | Fix: Signal stability flip gate (MAX_FLIPS_TO_ENTER) now scales with POLL_INTERVAL_MS. At 50ms polling, stable signals produce 4–7 micro-oscillation flips in 15s; the old hardcoded limit of 3 blocked valid FOK entries. Now: ≤100ms → 10, ≤1s → 6, >1s → 3. |
| Mar 28 | Fix: PTB (Price to Beat) mismatch on bot restart — added schedulePtbPageUpgrade() which retries at +15min and +30min to upgrade PTB from approximate source to exact finalPrice once Polymarket publishes it. |
| Mar 28 | Fix: PTB consensus buffer widened 0.05% → 0.3% to restore limit order fill rate after PTB accuracy improvements. |
| Mar 28 | Fix: Profit target baseline symmetry — daily baseline now resets correctly on WIB date boundary. |
| Mar 29 | Revert: ML rolled back to v16 (84.07% acc, 0.9248 AUC) — v20 underperformed due to choppy BTC regime. v16's 180-day training window with 86% real Polymarket labels remains the most reliable. |
Every 15 minutes, Polymarket runs a binary market: "Will BTC be higher in 15 minutes?" You bet YES or NO at the current market price (e.g., 65¢ for YES), and collect $1.00 if correct.
This bot automates that process — it reads BTC price data from 4 live sources, runs an ML ensemble to predict direction, calculates edge vs. market price, and places orders if conditions are favorable. It runs 24/7 and manages its own risk.
- ML Ensemble (v16) — XGBoost + LightGBM, Platt-calibrated, trained on 45K Polymarket markets (86% real labels)
- 15 Trade Filters — ML confidence, spread gates, VPIN, session quality, blackout hours, and more
- Smart Order Router — decides between FOK (instant) and Limit orders (passive, better price)
- Kelly Sizing — confidence-tiered fractional Kelly with hard bankroll caps
- Cut-Loss System — 13-gate evaluator; philosophy: hold to settlement wins 87.5% of the time
- Auto-Retrain Pipeline — weekly ML retraining with Optuna HPO, quality gates, auto-rollback
- Concept Drift Detection — CUSUM + hard threshold alerts when model degrades in real time
- AI Agent (optional) — OpenRouter-powered post-trade analysis and self-optimization
- Telegram Alerts — every trade, circuit breaker, daily P&L summary
- React Dashboard — 12 live panels: signals, positions, ML confidence, bet sizing, accuracy
Binance BTC Price (WebSocket) ─┐
Polymarket Orderbook (WebSocket) ─┤
Chainlink Oracle (WebSocket) ─────┤──→ Signal Computation ──→ 15 Trade Filters
Polymarket LiveData (WebSocket) ──┘ │
10 TA Indicators
ML Ensemble (XGB+LGB)
Regime Detection
│
┌────────▼────────┐
│ Edge Engine │
│ phase-based │
│ thresholds │
└────────┬────────┘
│
┌────────▼────────┐
│ Order Router │
│ FOK / LIMIT │
└────────┬────────┘
│
Polymarket CLOB API
(place → monitor → settle)
Every ~50ms the bot:
- Fetches real-time BTC price + Polymarket market data
- Computes 10 technical indicators + ML prediction
- Detects market regime (trending / choppy / mean-reverting)
- Calculates edge (model probability − market price)
- Applies 15 trade filters
- Routes order: LIMIT (passive entry) · FOK (immediate) · WAIT
- Monitors position: cut-loss gates, settlement detection
- Broadcasts full state to React dashboard via WebSocket
┌─ Bot Status ─────────────────────────────┐ ┌─ ML Engine ──────────────────────────┐
│ RUNNING │ DRY_RUN: OFF │ Bankroll $104 │ │ XGBoost: UP 82% LightGBM: UP 79% │
│ Poll #48291 Daily P&L: +$3.42 │ │ Ensemble: UP 81.5% HIGH confidence │
└──────────────────────────────────────────┘ └──────────────────────────────────────┘
┌─ Price Card ─────────────────────────────┐ ┌─ Edge Engine ────────────────────────┐
│ BTC $87,234 ▲ +0.3% │ Chainlink: OK │ │ Edge: 16.5% Prob: 81.5% │
│ Market: will-btc-go-up-by-1usd-... │ │ Regime: TRENDING Phase: EARLY │
│ ████████████░░░ 11:42 remaining │ │ RECOMMEND: UP ✓ ENTER │
└──────────────────────────────────────────┘ └──────────────────────────────────────┘
| Requirement | Version | Notes |
|---|---|---|
| Node.js | >= 25 | Tested on 25.1.0; required for native .ts/.mts/.cts execution |
| Python | >= 3.10 | ML retraining only (3.13.0 tested) |
| PM2 | Latest | npm install -g pm2 |
Polymarket Requirements:
- Polygon wallet (EOA) with USDC.e balance
- Recommended starting balance: $50+ USDC.e on Polygon
- No ETH needed — Polymarket uses gasless relays
# 1. Clone
git clone https://github.com/masterputra169/polymarket-15-minutes.git
cd polymarket-15-minutes/frontend
# 2. Install frontend dependencies
npm install
# 3. Install bot dependencies
cd bot && npm install && cd ..
# 4. Install PM2
npm install -g pm2
# 5. Create bot config
cp bot/.env.example bot/.env # then edit with your valuesIf
.env.exampledoesn't exist, createbot/.envmanually — see Configuration.
node -e "
const { ethers } = require('ethers');
const w = ethers.Wallet.createRandom();
console.log('Address: ', w.address);
console.log('Private Key:', w.privateKey);
"- Save private key to
bot/.envasPOLYMARKET_PRIVATE_KEY=0x... - Send USDC.e to this address on Polygon network
- API credentials are auto-derived on first start
Polymarket's web UI creates a Gnosis Safe proxy:
- Find your proxy address: Polymarket UI → Settings → Wallet
- Set
POLYMARKET_PROXY_ADDRESS=0x...in.env - Set
POLYMARKET_PRIVATE_KEY=0x...(the EOA controlling the proxy)
Create bot/.env — here are the essential variables:
# Wallet
POLYMARKET_PRIVATE_KEY=0x... # Your Polygon EOA private key
POLYMARKET_PROXY_ADDRESS= # Gnosis Safe address (if using web UI wallet)
# Bankroll
BANKROLL=50 # Starting capital in USD
DRY_RUN=true # ALWAYS start with true!
# Execution
POLL_INTERVAL_MS=50 # Poll frequency (ms)
LOG_LEVEL=info
# Dashboard status WebSocket
STATUS_BIND_HOST=127.0.0.1 # Safe default: local machine only
STATUS_PORT=3099
# STATUS_AUTH_TOKEN=change-me # Required if you expose STATUS_BIND_HOST=0.0.0.0# Circuit breakers — bot halts + 4hr cooldown when triggered
MAX_DAILY_LOSS_PCT=15 # Halt if daily loss >= 15%
MAX_CONSECUTIVE_LOSSES=7 # Halt after 7 straight losses
MAX_DRAWDOWN_PCT=30 # Halt if peak drawdown >= 30%
# Bet sizing
MAX_BET_AMOUNT_USD=2.50 # Hard cap per trade (raise as bankroll grows)# Cut-loss (recommended: keep enabled)
CUT_LOSS_ENABLED=true
CUT_LOSS_MIN_HOLD_SEC=720 # Hold at least 12 min before cutting
CUT_LOSS_MIN_TOKEN_DROP_PCT=70 # Only cut on catastrophic drops (70%)
# Take-profit (recommended: disabled — settlement beats early exit)
TAKE_PROFIT_ENABLED=false
# Limit orders — passive entry at better prices
LIMIT_ORDER_ENABLED=true
# Recovery buy — re-enter after cut-loss if signal recovers
RECOVERY_BUY_ENABLED=true
# Pre-market LONG — daily UP entry at 09:00-09:15 EST
PREMARKET_LONG_ENABLED=false
PREMARKET_LONG_RISK_PCT=0.05 # 5% bankroll per trade# Telegram
TELEGRAM_BOT_TOKEN= # From @BotFather
TELEGRAM_CHAT_ID= # Your chat ID
TELEGRAM_NOTIFY_TRADES=trueMETENGINE_ENABLED=false
SOLANA_PRIVATE_KEY= # Base58 Solana keypair for x402 paymentsAI_AGENT_ENABLED=false
OPENROUTER_API_KEY=sk-or-...
AI_MODEL=google/gemini-2-flash # Or any OpenRouter modelDRIFT_WINDOW=50 # Rolling window (trades) to evaluate accuracy
DRIFT_MIN_TRADES=30 # Min trades before check activates
DRIFT_WR_DROP_PP=15 # Alert if accuracy drops 15pp from baseline
DRIFT_AUTO_RETRAIN=false # Set true to auto-trigger retrain on drift# Ensure DRY_RUN=true in bot/.env, then:
pm2 start ecosystem.config.cts
pm2 logs polymarket-botExpected output:
[Bot] CLOB client initialized (dry-run mode)
[Bot] ML models loaded: XGBoost v16 + LightGBM v16
[Bot] WebSocket streams connected: Binance · CLOB · PolyLive · Chainlink
[Bot] Poll #1 | BTC $87,234 | UP 81% | Edge 16.5% | ENTER (DRY)
# In bot/.env:
DRY_RUN=false
MAX_BET_AMOUNT_USD=2.50 # Start small
pm2 restart polymarket-bot
pm2 logs polymarket-botpm2 start ecosystem.config.cts # Start bot + frontend dashboard
pm2 logs polymarket-bot # Bot live logs (Ctrl+C to exit)
pm2 logs frontend # Frontend live logs
pm2 logs polymarket-bot --lines 200 # Last 200 lines
pm2 stop polymarket-bot # Graceful stop bot
pm2 restart polymarket-bot # Restart bot
pm2 monit # CPU/memory monitor
pm2 status # Process listThe dashboard starts automatically with PM2 alongside the bot. Open:
http://localhost:3010
The dashboard connects to the bot via WebSocket on port 3099. By default the bot binds this port to 127.0.0.1.
For LAN/mobile dashboard access, set STATUS_BIND_HOST=0.0.0.0 and STATUS_AUTH_TOKEN=<random-long-token> in bot/.env, restart PM2 with --update-env, then open the dashboard with ?botStatusToken=<same-token> once. The frontend stores that token in browser localStorage for later reconnects.
The order router picks execution mode based on 7 rules:
| Condition | Action |
|---|---|
| ML >= 85% + price <= 65¢ | FOK (immediate) |
| ML 65–84% + price <= 62¢ + wide spread | Limit order (passive) |
| ML < 62% or no edge | WAIT |
Places GTD orders at target price (typically 55–62¢ vs. market 65–75¢):
- Auto-cancels after 7 min if unfilled → falls back to FOK
- Max 2 attempts per market (anti-loop)
- At 58¢ entry: only needs 58% WR to break even vs. 72% at 72¢ FOK
Daily UP entry during 09:00–09:15 EST:
- Exploits pre-NYSE open volatility
- 1 trade per day maximum
- Enable with
PREMARKET_LONG_ENABLED=true
Re-enters after cut-loss if signal stabilizes:
- 10s baseline + 30s monitoring period
- Requires ML still agrees, token rising or stable
- Reduced position size (anti-revenge sizing)
| Trigger | Default | Action |
|---|---|---|
| Daily loss ≥ 15% | MAX_DAILY_LOSS_PCT=15 |
Halt + 4hr cooldown |
| Peak drawdown ≥ 30% | MAX_DRAWDOWN_PCT=30 |
Halt + 4hr cooldown |
| 7 consecutive losses | MAX_CONSECUTIVE_LOSSES=7 |
Halt + 4hr cooldown |
| Win rate < 30% (rolling) | Automatic | Halt |
"Hold to settlement wins 87.5% of the time."
The bot only exits early in extreme scenarios:
- Token drops ≥ 70% (catastrophic collapse)
- ML flips direction with ≥ 92% confidence
- Minimum 12-minute hold before any cut
Data from 300+ live trades shows settlement WR of 87.5% vs. cut-loss WR of 23.3%. Early selling destroys edge.
Every entry passes all 15 gates:
| # | Filter | Purpose |
|---|---|---|
| 1 | ML confidence ≥ 62% | Only act on high-confidence signals |
| 2 | Price not 45–55¢ | Avoid coin-flip zone |
| 3 | ATR volatility check | Avoid dead markets |
| 4 | Time window 0.75–14.5 min | Avoid too-early or too-late entries |
| 5 | Post-loss cooldown | Prevent revenge trading |
| 6 | Max 2 trades per market | Anti-overtrading |
| 7 | Weekend ML gate | Block on Sat/Sun if ML model not loaded or confidence < 65% |
| 8 | Edge ceiling < 20% | Avoid mispriced markets |
| 9 | Counter-trend momentum | Don't fight strong momentum |
| 10 | Blackout hours 16:00–23:00 ET | Skip historically low-WR hours |
| 11 | Regime strictness | Tighter thresholds in choppy markets |
| 12 | Spread < 8% | Avoid illiquid markets |
| 13 | ML rolling accuracy | Pause if model degrades |
| 14 | VPIN informed flow | Block if smart money detected |
| 15 | Spread widening | Block if sudden liquidity withdrawal |
| Metric | Value |
|---|---|
| Test Accuracy | 84.07% |
| Test AUC | 0.9248 |
| Holdout Accuracy | 94.12% |
| Training window | 180 days |
| Real Polymarket labels | 86% |
| Training samples | 45,336 |
| Ensemble weights | XGBoost 0.75, LightGBM 0.25 |
| Features | 79 (54 base + 25 engineered) |
| Calibration | Platt scaling on logits |
| At ≥80% confidence | 99.3% WR (79.4% coverage) |
v16 uses a 180-day training window — enough data for robust regime coverage without diluting real Polymarket labels. v20 (120d, 34K samples) was rolled back after underperforming in the current market regime.
- BTC Price: Returns (1m/5m/15m/30m/1h/4h), Z-score, momentum
- Technical: RSI, MACD, VWAP, Bollinger Bands, ATR, Heiken Ashi, EMA Cross, StochRSI
- Polymarket: Token price, bid/ask spread, time to settlement, orderbook imbalance
- Volume: Delta, funding rate, VPIN estimate
- Regime: Choppy/trending/mean-revert classification
The bot includes a full retraining pipeline with quality gates and auto-rollback:
cd backtest/ml_training
# 1. Update market data + enrich CLOB tick prices
node fetchFreshMarkets.mts --days 60 --lookup ./polymarket_lookup.json
# 2. Generate training data
node generateTrainingData.mts --days 120 --polymarket-lookup ./polymarket_lookup.json
# 3. Train with Optuna HPO (150 trials)
python trainXGBoost_v3.py --input training_data.csv --tune --tune-trials 150
# 4. Backtest
python backtestPnL.py --threshold-sweep
# 5. Deploy (if quality gates pass)
cp output/xgboost_model.json ../../public/ml/
cp output/lightgbm_model.json ../../public/ml/
cp output/norm_browser.json ../../public/ml/fetchFreshMarkets.mts uses multiple Polymarket Gamma discovery strategies: configured series_id, search terms, recent closed events, and a timestamp slug sweep fallback. Optional overrides:
node fetchFreshMarkets.mts --days 60 --lookup ./polymarket_lookup.json --series-ids 10192 --search "Bitcoin Up or Down,BTC Up or Down"If local DNS resolves Polymarket domains to an ISP filtering host, the fetcher uses DNS-over-HTTPS automatically for Polymarket market-data requests:
node fetchFreshMarkets.mts --days 60 --lookup ./polymarket_lookup.json --dns-mode autoOr configure auto-retrain — runs weekly (Sunday 3 AM UTC) via PM2:
RETRAIN_DAY_OF_WEEK=0
RETRAIN_HOUR_UTC=3
RETRAIN_DAYS=120
RETRAIN_ENRICH_DAYS=60
RETRAIN_FETCH_PRICES=true
RETRAIN_REQUIRE_FRESH_DATA=true
RETRAIN_LOOKUP_MAX_STALE_DAYS=7
RETRAIN_TUNE_TRIALS=100With RETRAIN_REQUIRE_FRESH_DATA=true, retraining fails closed if Polymarket/Gamma/CLOB data cannot be refreshed or if the latest tick-price-enriched market is stale. This prevents deploying a model retrained on old market regimes.
The bot monitors model performance in real time. If live accuracy drops significantly from baseline:
- Telegram alert sent immediately
- CUSUM algorithm detects gradual drift early
- Optional: auto-triggers retraining (
DRIFT_AUTO_RETRAIN=true)
- Create a bot via @BotFather →
/newbot - Get your
chat_id:https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates - Set in
.env:TELEGRAM_BOT_TOKEN=123456789:ABC-... TELEGRAM_CHAT_ID=987654321 TELEGRAM_NOTIFY_TRADES=true
| Event | Severity |
|---|---|
| Trade placed (buy/sell) | Info |
| Limit order filled/cancelled | Info |
| Settlement result (win/loss) | Info |
| Daily P&L summary | Info |
| Cut-loss triggered | Warning |
| Circuit breaker halt | Critical |
| Concept drift detected | Warning |
pm2 logs polymarket-bot --lines 50Common causes:
POLYMARKET_PRIVATE_KEY is required→ checkbot/.envexists and has the keyCannot find module→ runcd bot && npm installFailed to load XGBoost model→ ensurepublic/ml/xgboost_model.jsonexists
pm2 stop polymarket-bot
# Edit bot/data/state.json — set bankroll, peakBankroll, startOfDayBankroll to actual USDC.e balance
pm2 restart polymarket-bot- Check
pm2 status— bothpolymarket-botandfrontendmust be running - Verify port 3099 is not blocked on the machine running the bot
- Dashboard connects to
ws://localhost:3099by default; for LAN access useSTATUS_BIND_HOST=0.0.0.0plusSTATUS_AUTH_TOKEN - If
frontendshowserrored:pm2 delete frontend && pm2 start ecosystem.config.cts --only frontend
pm2 stop polymarket-bot
# Edit bot/data/state.json → update all bankroll fields to your actual on-chain balance
pm2 restart polymarket-botfrontend/
├── src/ # React 19 dashboard
│ ├── App.tsx # Root, per-panel useMemo data slicing
│ ├── components/ # 12 dashboard panels
│ ├── engines/ # Browser-side ML + decision engines
│ │ ├── Mlpredictor.ts # XGBoost tree traversal (Float64Array)
│ │ ├── edge.ts # Phase-based edge thresholds
│ │ ├── asymmetricBet.ts # Kelly fraction sizing
│ │ └── regime.ts # Choppy/trending/mean-revert classifier
│ ├── indicators/ # 10 TA functions (RSI, MACD, VWAP, ...)
│ ├── hooks/ # useBotData, useCountdown, useClock
│ └── config.ts # Frontend parameters + polyFeeRate()
│
├── bot/ # Node.js trading bot (PM2-managed)
│ ├── index.ts # Entry point + startup sequence
│ └── src/
│ ├── loop.ts # Main poll loop (~2000 lines)
│ ├── config.ts # BOT_CONFIG from .env
│ ├── statusServer.ts # WebSocket broadcast server :3099
│ ├── autoRetrain.ts # Weekly ML retraining orchestrator
│ ├── engines/
│ │ ├── signalComputation.ts # All indicators per poll
│ │ ├── tradePipeline.ts # Order execution + Kelly sizing
│ │ ├── orderRouter.ts # 7-rule LIMIT/FOK/WAIT decision
│ │ ├── limitOrderManager.ts # GTD order lifecycle
│ │ └── settlement.ts # Settlement detection + P&L
│ ├── trading/
│ │ ├── positionTracker.ts # Bankroll + position state
│ │ ├── cutLoss.ts # 13-gate cut-loss evaluator
│ │ └── recoveryBuy.ts # Re-entry after cut-loss
│ ├── safety/
│ │ ├── tradeFilters.ts # 15 entry filters
│ │ └── guards.ts # Circuit breaker
│ └── monitoring/
│ ├── perfMonitor.ts # Rolling win rate + daily P&L
│ ├── driftDetector.ts # CUSUM concept drift detection
│ ├── rollbackMonitor.ts # Post-deploy WR monitor
│ └── notifier.ts # Telegram + Discord alerts
│
├── public/ml/ # Deployed ML models
│ ├── xgboost_model.json # XGBoost ensemble (v16)
│ ├── lightgbm_model.json # LightGBM ensemble (v16)
│ └── norm_browser.json # Feature normalization params
│
├── backtest/ml_training/ # ML training pipeline
│ ├── trainXGBoost_v3.py # Main trainer (Optuna HPO)
│ ├── generateTrainingData.mts # Feature engineering
│ ├── backtestPnL.py # Threshold sweep backtest
│ └── quickUpdateLookup.py # Scrape recent Polymarket markets
│
├── ecosystem.config.cts # PM2: bot + frontend processes
├── vite.config.ts # Dev server + CORS proxies
└── package.json
- Always start with
DRY_RUN=true— verify everything works before going live - Never run
node bot/index.tsdirectly — always use PM2 (handles.envloading) - The bot does not need ETH — Polymarket uses gasless relays on Polygon
- Settlement beats early exit — the cut-loss is intentionally conservative by design
- Blackout hours 16:00–23:00 ET — bot automatically skips historically low-WR hours
- Binance FAPI and Bybit are blocked in some regions — funding rate defaults to neutral (harmless)
Pull requests welcome. For major changes, open an issue first.
When contributing:
- No TypeScript — vanilla JavaScript/JSX with ES modules
- No linter configured — keep style consistent with surrounding code
- Test with
DRY_RUN=truebefore submitting
This software is for educational and personal use only. Trading prediction markets involves significant financial risk. Past performance does not guarantee future results. The authors are not responsible for any financial losses. Trade at your own risk.
MIT License — see LICENSE for details.