Production-ready TypeScript bot for trading Polymarket BTC 5-minute and 15-minute fast markets using Binance BTCUSDT momentum lag detection and market odds mispricing.
This bot watches the live Binance BTCUSDT 1-minute kline stream and, when it detects a significant price move (0.35%β0.80%) combined with an elevated volume spike, checks whether the corresponding Polymarket fast market is still mispriced. If the YES/NO odds haven't caught up to the Binance signal, it enters a trade and manages the position with take-profit, stop-loss, and force-close logic.
Keywords: Polymarket trading bot, Binance momentum bot, prediction market arbitrage, BTC fast market bot, TypeScript trading bot.
| Condition | Threshold |
|---|---|
| BTC 1m momentum | +0.35% to +0.80% |
| YES price | < 0.47 |
| Volume ratio | > 1.5Γ average |
| Candle colour streak | Last 2 green |
| Condition | Threshold |
|---|---|
| BTC 1m momentum | β0.35% to β0.80% |
| YES price | > 0.56 |
| Volume ratio | > 1.5Γ average |
| Candle colour streak | Last 2 red |
- Less than 60 seconds remaining
- Spread wider than 6Β’
- Liquidity below $50
- Momentum outside the configured band
- Market already priced in
| Trigger | Threshold |
|---|---|
| Take profit | +5Β’ per share |
| Stop loss | β3Β’ per share |
| Force close | 15 s before expiry |
- Max daily loss: $50 (configurable)
- Max concurrent positions: 3
- Cooldown after 3 consecutive losses: 15 minutes
| Package | Role |
|---|---|
ws |
Binance WebSocket kline stream |
axios |
REST calls (Binance, Polymarket CLOB, Simmer) |
zod |
Environment config validation |
pino + pino-pretty |
Structured logging |
dotenv |
.env loading |
typescript + tsx |
TypeScript compilation and dev runner |
src/
βββ config/
β βββ index.ts Zod-validated env config
βββ exchanges/
β βββ binance.ts WebSocket client + REST warm-up
β βββ polymarket.ts Market discovery + CLOB order books
βββ strategies/
β βββ momentum.ts 1m/3m/5m momentum + volume ratio + streak
β βββ mispricing.ts Entry/exit signal generation
βββ risk/
β βββ manager.ts Daily loss, position limits, cooldown
βββ execution/
β βββ orders.ts Simmer SDK buy/sell + portfolio balance
β βββ positions.ts In-memory position tracker + exit loop
βββ backtest/
β βββ runner.ts Historical candle replay + P&L simulation
βββ utils/
β βββ types.ts All shared TypeScript types
β βββ logger.ts Pino singleton
β βββ storage.ts Append-only JSONL trade log
βββ index.ts Main bot loop + backtest entry point
- Node.js β₯ 18
- A Simmer account with a funded Polymarket wallet
- Your
SIMMER_API_KEYfrom simmer.markets/dashboard β SDK tab
cd polymarket-momentum-mispricing-bot
npm installcp .env.example .env
# Edit .env and set SIMMER_API_KEY# Dry run (no real trades β default)
npm run dry-run
# Live trading
npm run live
# Backtest on historical data
npm run backtest
# Development mode with auto-restart
npm run devnpm run build
npm start| Script | Command | Description |
|---|---|---|
dev |
tsx src/index.ts |
Dev run with live TypeScript compilation |
dry-run |
DRY_RUN=true tsx src/index.ts |
Signal detection only, no orders |
live |
DRY_RUN=false tsx src/index.ts |
Live trading |
backtest |
BACKTEST_MODE=true tsx src/index.ts |
Historical replay |
build |
tsc |
Compile to dist/ |
start |
node dist/index.js |
Run compiled build |
typecheck |
tsc --noEmit |
Type-check without output |
See .env.example for the full list with descriptions. Key variables:
| Variable | Default | Description |
|---|---|---|
SIMMER_API_KEY |
β | Required for live trading |
DRY_RUN |
true |
false to enable real orders |
POSITION_SIZE_USD |
10 |
USD per trade |
MOMENTUM_MIN_PCT |
0.35 |
Min 1m BTC move % |
MOMENTUM_MAX_PCT |
0.80 |
Max 1m BTC move % |
YES_BUY_MAX_PRICE |
0.47 |
Buy YES below this |
YES_SELL_MIN_PRICE |
0.56 |
Buy NO above this |
MAX_DAILY_LOSS_USD |
50 |
Daily loss circuit breaker |
BACKTEST_START_DATE |
2025-01-01 |
Backtest start |
BACKTEST_END_DATE |
2025-01-31 |
Backtest end |
All trades are appended to data/trades.jsonl (configurable via TRADES_FILE).
Each line is a complete JSON TradeRecord. Example:
Each line in data/trades.jsonl is one complete trade:
{"id":"3f2a1b4c-8d9e-4f5a-b6c7-d8e9f0a1b2c3","timestamp":"2026-04-30T02:43:05.000Z","market":"Will BTC be higher at 2:50PM ET? (Apr 30, 2026 - 2:45PM to 2:50PM ET)","conditionId":"0x9f3c4a2b1d0e8f7a6c5b4d3e2f1a0b9c8d7e6f5a","side":"YES","entryPrice":0.44,"exitPrice":0.492,"shares":22.7,"cost":10.0,"pnl":1.1804,"status":"closed","exitReason":"take_profit","entryTime":"2026-04-30T02:43:05.000Z","exitTime":"2026-04-30T02:44:18.000Z","secondsHeld":73,"momentum1m":0.463,"momentum3m":0.581,"momentum5m":0.744,"volumeRatio":2.34,"candleStreak":3,"window":"5m","dryRun":false}
{"id":"7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d","timestamp":"2026-04-30T02:51:44.000Z","market":"Will BTC be higher at 3:00PM ET? (Apr 30, 2026 - 2:45PM to 3:00PM ET)","conditionId":"0x1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b","side":"NO","entryPrice":0.41,"exitPrice":0.379,"shares":24.4,"cost":10.0,"pnl":-0.7564,"status":"closed","exitReason":"stop_loss","entryTime":"2026-04-30T02:51:44.000Z","exitTime":"2026-04-30T02:53:01.000Z","secondsHeld":77,"momentum1m":-0.512,"momentum3m":-0.643,"momentum5m":-0.791,"volumeRatio":1.87,"candleStreak":-2,"window":"15m","dryRun":false}
{"id":"b1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e","timestamp":"2026-04-30T03:05:22.000Z","market":"Will BTC be higher at 3:10PM ET? (Apr 30, 2026 - 3:05PM to 3:10PM ET)","conditionId":"0x2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c","side":"YES","entryPrice":0.43,"exitPrice":0.48,"shares":23.3,"cost":10.0,"pnl":1.163,"status":"closed","exitReason":"take_profit","entryTime":"2026-04-30T03:05:22.000Z","exitTime":"2026-04-30T03:06:47.000Z","secondsHeld":85,"momentum1m":0.391,"momentum3m":0.512,"momentum5m":0.623,"volumeRatio":1.92,"candleStreak":2,"window":"5m","dryRun":false}Session stats after 3 trades:
| # | Side | Entry | Exit | Shares | Cost | P&L | Reason | Time Held |
|---|---|---|---|---|---|---|---|---|
| 1 | YES | $0.440 | $0.492 | 22.7 | $10.00 | +$1.18 | take_profit | 73 s |
| 2 | NO | $0.410 | $0.379 | 24.4 | $10.00 | β$0.76 | stop_loss | 77 s |
| 3 | YES | $0.430 | $0.480 | 23.3 | $10.00 | +$1.16 | take_profit | 85 s |
| $30.00 | +$1.59 |
02:41:08 INFO: π Execution: direct CLOB (private key loaded)
address: "0xA1b2C3d4E5f6A7b8C9d0E1f2A3b4C5d6E7f8A9b0"
mode: "clob"
02:41:08 INFO: π polymarket-momentum-mispricing-bot starting [DRY RUN]
mode: "clob"
dryRun: true
trade5m: true
trade15m: true
momentumMin: 0.35
momentumMax: 0.8
positionSize: 10
maxDaily: 50
02:41:08 INFO: Connecting to Binance WebSocket...
02:41:09 INFO: Binance WebSocket connected
symbol: "BTCUSDT"
stream: "btcusdt@kline_1m"
02:41:09 INFO: Warming up candle buffer
symbol: "BTCUSDT"
limit: 60
02:41:10 INFO: Candle buffer ready
count: 60
priceNow: 94821.32
02:41:10 INFO: Bot running β press Ctrl+C to stop
02:41:10 DEBUG: Skip: 1m momentum 0.112% < min 0.35% (too weak)
window: "5m"
yes: "0.510"
no: "0.490"
secs: "247"
02:41:15 DEBUG: Skip: Volume ratio 1.21x < min 1.5x
window: "5m"
yes: "0.480"
no: "0.520"
secs: "242"
02:41:20 DEBUG: Skip: 1m momentum 0.093% < min 0.35% (too weak)
window: "15m"
yes: "0.503"
no: "0.497"
secs: "731"
02:43:05 INFO: π― Entry signal: YES on 5m market
window: "5m"
side: "YES"
confidence: "0.71"
reason: "BTC β²0.463% | YES @ 0.440 | vol 2.34x | streak +3"
yes: "0.440"
no: "0.560"
secs: "187"
momentum: "β² 1m:+0.463% 3m:+0.581% 5m:+0.744% vol:2.34x streak:+3 $94,971.84"
02:43:05 INFO: [DRY RUN] BUY simulated
marketId: "0x9f3c4a2b1d0e8f7a6c5b4d3e2f1a0b9c8d7e6f5a"
side: "YES"
amountUsd: 10
fillPrice: 0.44
dryShares: "22.7"
mode: "clob"
02:43:05 INFO: Position opened
id: "3f2a1b4c-8d9e-4f5a-b6c7-d8e9f0a1b2c3"
side: "YES"
question: "Will BTC be higher at 2:50PM ET? (Apr 30..."
entryPrice: "0.440"
shares: "22.7"
cost: "10.00"
02:44:18 INFO: β
Position exit β take_profit
id: "3f2a1b4c-8d9e-4f5a-b6c7-d8e9f0a1b2c3"
side: "YES"
reason: "take_profit"
entryPrice: "0.440"
exitPrice: "0.492"
pnlPerShare: "0.052"
pnl: "1.1804"
shares: "22.7"
02:44:18 INFO: [DRY RUN] SELL simulated
marketId: "0x9f3c4a2b1d0e8f7a6c5b4d3e2f1a0b9c8d7e6f5a"
side: "YES"
shares: 22.7
currentPrice: 0.492
mode: "clob"
02:44:18 INFO: RiskManager: position closed
pnl: "1.1804"
dailyPnl: "1.1804"
openCount: 0
consecutiveLosses: 0
02:51:44 INFO: π― Entry signal: NO on 15m market
window: "15m"
side: "NO"
confidence: "0.68"
reason: "BTC βΌ0.512% | YES @ 0.590 (HIGH) | vol 1.87x | streak -2"
yes: "0.590"
no: "0.410"
secs: "634"
momentum: "βΌ 1m:-0.512% 3m:-0.643% 5m:-0.791% vol:1.87x streak:-2 $94,487.22"
02:51:44 INFO: [DRY RUN] BUY simulated
side: "NO"
amountUsd: 10
fillPrice: 0.41
dryShares: "24.4"
02:51:44 INFO: Position opened
id: "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
side: "NO"
entryPrice: "0.410"
shares: "24.4"
cost: "10.00"
02:53:01 INFO: β Position exit β stop_loss
id: "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
side: "NO"
entryPrice: "0.410"
exitPrice: "0.379"
pnlPerShare: "-0.031"
pnl: "-0.7564"
02:53:01 INFO: RiskManager: position closed
pnl: "-0.7564"
dailyPnl: "0.4240"
consecutiveLosses: 1
02:55:00 INFO: π Status
risk: "Daily P&L: $0.42 | Open: 0 | Trades: 2 (1W/1L 50.0%) | Streak-losses: 1"
btcPrice: "94,512.80"
storedTrades: 2
storedPnl: "0.4240"
tradeFile: "data/trades.jsonl"
03:18:22 WARN: Loss streak threshold reached β entering cooldown
consecutiveLosses: 3
cooldownMinutes: 15
cooldownUntil: "2026-04-30T03:33:22.000Z"
03:18:30 WARN: β Risk gate blocked trade
reason: "Cooldown active β 831s remaining"
The backtest mode fetches real Binance BTCUSDT 1-minute candles and replays them through the strategy. Since historical Polymarket market data is not publicly available, synthetic YES/NO prices are used for entry conditions (configurable via BACKTEST_ENTRY_PRICE). Market resolution is exact β it uses the actual BTC price direction during each window.
# Backtest January 2025
BACKTEST_START_DATE=2025-01-01 BACKTEST_END_DATE=2025-01-31 npm run backtestββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Starting backtest
start: "2025-01-01"
end: "2025-01-31"
startingBalance: 1000
Candle data loaded
totalCandles: 44640
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π BACKTEST SUMMARY β polymarket-momentum-mispricing-bot
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Period: 2025-01-01 β 2025-01-31
Starting capital $1000.00
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Signals scanned: 4176
Trades executed: 312
Wins: 181 Losses: 131
Win rate: 58.0%
Avg P&L/trade: $0.0623
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
Net P&L: $19.44
Final balance: $1019.44
ROI: +1.94%
Max drawdown: 3.21%
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Two execution backends β selected by EXECUTION_MODE in .env:
Your private key signs EIP-712 orders directly on the Polymarket CTF Exchange contract (Polygon). No intermediary needed.
Bot β EIP-712 sign (ethers v6) β Polymarket CLOB API β Order filled
(clob.polymarket.com)
POST /auth/api-keyβ obtain session credentials (signed once, cached)POST /orderβ submit FOK (Fill-or-Kill) buy or sell orderGET /balance-allowanceβ USDC balance
Simmer acts as an execution proxy and handles CLOB signing for you.
Bot β Simmer API β Polymarket CLOB β Order filled
(api.simmer.markets)
POST /api/sdk/markets/importβ register a marketPOST /api/sdk/tradeβ buy/sell an outcomeGET /api/sdk/portfolioβ account balance
- Always test with
DRY_RUN=truefirst. Watch the logs for signal quality before enabling live trading. - Start with a small
POSITION_SIZE_USD(e.g., $5β$10). - The bot does not guarantee profits. Polymarket fast markets are highly competitive.
- Keep
MAX_DAILY_LOSS_USDset to a comfortable level you can afford to lose. - The Simmer API is a third-party service β review their terms before use.
MIT