Build a Python trading bot with Claude AI, Polymarket CLOB integration, and live Polymarket TWAP (Chainlink) signals. Paper trading to production in one guide.
TL;DR
I built a production algorithmic trading system using Python, Claude Code for development, Polymarket’s CLOB API for execution, and Polymarket’s Real-Time Data Streaming (RTDS) Chainlink TWAP feeds for signals. This guide covers the full architecture: signal generation from 30s/60s TWAP updates, position management, exit strategies, and the paper-to-live progression that prevented costly mistakes.
Key takeaways
01 A production trading bot needs five core modules: signal generation, position execution, exit management, state persistence, and health monitoring
02 Claude Code wrote 95% of the 4,000-line codebase autonomously, including the asyncio event loop, database layer, and deployment scripts
03 Paper trading with at least 30 signals before going live prevented two strategy failures that would have cost real money
04 The biggest technical challenge was not the trading logic but state recovery after process restarts
05 Polymarket’s CLOB API requires careful order book analysis because liquidity varies dramatically between markets — and TWAP-resolved markets demand even tighter depth checks around resolution windows
Why this matters
I built a production algorithmic trading system using Python, Claude Code for development, Polymarket’s CLOB API for execution, and Polymarket’s public RTDS WebSocket for live Chainlink TWAP price updates (30-second and 60-second windows). This guide covers the full architecture: signal generation from TWAP streams, position management, exit strategies, and the paper-to-live progression that prevented costly mistakes.
A production Python trading bot needs five core modules to run continuously without losing state: signal generation, position management, exit strategies, state persistence, and health monitoring. I built this system using Python and Claude Code across a 4,000-line codebase, with Polymarket RTDS Chainlink TWAP feeds for signals and the CLOB for execution. Paper trading at least 30 signals over 6 weeks before going live prevented two strategy failures — a liquidity trap near TWAP settlement windows and a signal-timing issue during low-liquidity hours — that would have cost real money.
System Architecture Overview
When I started building a trading bot, I expected the hard part to be the trading logic. It wasn’t. The hard part was building a system that could run continuously for weeks without losing state, crashing silently, or entering impossible positions — especially once Polymarket switched crypto up/down markets to TWAP-based resolution (5-minute markets on 30s TWAP, 15-minute and 4-hour markets on 60s TWAP).
A production trading bot needs five core modules that work together:
- Signal Generation — Monitors live Chainlink TWAP updates via Polymarket RTDS and generates trade signals
- Position Management — Executes trades, tracks holdings, and prevents overlapping positions
- Exit Strategies — Knows when to close positions and takes profits or cuts losses
- State Persistence — Survives process crashes, power failures, and restarts
- Health Monitoring — Detects stuck orders, orphaned positions, and API failures
Each module can fail independently, so the system needs to handle partial failures gracefully. A TWAP signal can fail without crashing position management. An API call can timeout without losing the position state. The monitoring system watches everything and alerts when something goes wrong.
The entire codebase is about 4,000 lines of Python. Claude Code wrote 95% of it, including the most complex parts: the asyncio event loop, the database schema and queries, and the deployment scripts.
Note: The repo is private for now, but I'm planning to open source the core trading logic once it's hardened further.
How Does Signal Generation Work?
Signals are the input to the entire system. My signals come from Polymarket’s RTDS Chainlink TWAP feeds (no Chainlink credentials required).
Polymarket RTDS delivers real-time 30-second and 60-second Chainlink TWAP updates over a public WebSocket. I subscribe to topics such as crypto_prices_twap_thirty and crypto_prices_twap_sixty for pairs like btc/usd, eth/usd, etc. I watch consecutive TWAP prints and detect momentum breakouts when the latest TWAP moves more than 2-sigma relative to the recent window of TWAP values. When a TWAP breakout occurs, I look for corresponding Polymarket up/down prediction markets that are still underpriced relative to the new TWAP trajectory.
The key insight is that prediction-market prices still lag the official TWAP updates by 15–60 seconds in many cases, creating a small window to position ahead of the eventual resolution source.
The TWAP Momentum Window
Here’s how the signal generation actually works in code (using the official polymarket-client):
from polymarket import AsyncPublicClient
from polymarket.streams import CryptoPricesChainlinkTwapSpec
import asyncio
from collections import deque
async def detect_twap_breakout(twap_history: deque, current_twap: float) -> float | None:
"""
Watch recent Chainlink TWAP values (30s or 60s window) and detect 2-sigma breakouts.
Returns signal strength (0-1) if breakout detected.
"""
if len(twap_history) < 20:
return None
values = list(twap_history)
mean = sum(values) / len(values)
variance = sum((x - mean) ** 2 for x in values) / len(values)
std_dev = variance ** 0.5
if std_dev == 0:
return None
z_score = (current_twap - mean) / std_dev
if z_score > 2.0: # Upside breakout on TWAP
return min(1.0, z_score / 3.0) # Cap at 3-sigma
return None
# Example subscription loop
async def stream_twap_signals():
history = deque(maxlen=20)
async with AsyncPublicClient() as client:
async with await client.subscribe(
CryptoPricesChainlinkTwapSpec(window_seconds=30, symbols=["btc/usd"])
) as stream:
async for event in stream:
twap = float(event.payload.value)
history.append(twap)
strength = await detect_twap_breakout(history, twap)
if strength:
# Emit signal to position manager queue
await signal_queue.put({
"symbol": event.payload.symbol,
"twap": twap,
"window_s": event.payload.window_seconds,
"strength": strength,
"ts": event.payload.timestamp
})
When a signal fires, the system calculates how many standard deviations the latest TWAP has moved above the recent mean of TWAP prints. A 2-sigma TWAP move is rare; when it lines up with an underpriced Polymarket market that resolves on the same TWAP feed, the edge becomes real.
The efficiency gap exists because:
- Official Chainlink TWAP updates arrive via RTDS (fast and authoritative)
- Retail prediction-market traders react more slowly to the new TWAP level
- Markets that resolve on the exact same 30s/60s TWAP still show temporary mispricing in the order book
This isn’t an edge that lasts forever. As more bots subscribe to the same public RTDS feed, the 15–60 second window compresses. But for now, it’s consistent enough to trade.
Signals feed into a queue. The position manager processes signals one at a time, ensuring we never accidentally open two positions on the same market.
How Does Position Management Work?
Once a TWAP signal arrives, the position manager decides: do we take this trade, or skip it?
The decision logic checks:
- Do we already have a position in this market? If yes, skip.
- Is the order book deep enough to execute at reasonable prices? If no, skip.
- Has this market been active for more than 24 hours? Recent markets are illiquid.
- Are we at our maximum concurrent positions? If yes, skip.
- Is the market’s resolution window (5m / 15m / 4h) still far enough from the TWAP settlement window that we have room to exit?
If all checks pass, the position manager places a limit order on the Polymarket CLOB. The CLOB is Polymarket’s central limit order book. It’s lower latency than the REST API but requires understanding the order book structure — especially critical now that resolution is driven by the same TWAP feeds we are monitoring.
The position manager tracks every open position in SQLite. Each position stores:
- Market ID and outcome tokens
- Entry price and quantity
- Timestamp and signal strength (from TWAP z-score)
- Current mark-to-market value
- Status (open, closing, closed)
- Linked TWAP window (30s or 60s) and symbol
This database survives process restarts. On startup, the position manager reads the database and reconstructs the exact state it was in before the crash.
What Exit Strategies Actually Work?
The hardest part of algorithmic trading is exits. Most retail traders focus on entries but skip profitable or know when to close positions. It’s the difference between “cool idea” and “actual profit.”
I use three exit types:
- Profit target — Close 50% of position at 2% profit, rest at 5% profit
- Stop loss — Close entire position if it drops 3% below entry
- Time decay / TWAP proximity — If a position hasn’t moved in 4 hours, or if the market is approaching its TWAP resolution window with no edge left, close it (markets that aren’t moving are wasting capital)
The exit manager runs every minute, checks all open positions against the latest RTDS TWAP and market mid, and executes exits that meet criteria. It places exit orders as limit orders too, so we get the best available prices.
The Math on Asymmetric Position Sizing
Here’s why the split profit target works. Say I enter a position at $0.45 on a binary market with $100 per trade:
Winning trades (55% of the time):
- Exit 50% at $0.459 (2% profit): +$0.90
- Exit remaining 50% at $0.4725 (5% profit): +$2.25
- Total on winner: +$3.15 (3.15% return on $100)
Losing trades (45% of the time):
- Stop loss hits at $0.4365 (3% below entry): -$3.00
- Total on loser: -$3.00 (-3% return on $100)
Expected value calculation:
EV = (0.55 × $3.15) + (0.45 × -$3.00)
EV = $1.73 + (-$1.35)
EV = +$0.38 per $100 bet
That’s positive EV at a 55% win rate. Drop to 54% and the math flips negative. This is why signal quality (TWAP z-score strength) matters more than quantity. A 60% win rate turns $0.38 per $100 into $0.60 per $100. Signal strength compounds.
The split exit structure protects against reversal. If the market hits 2% and I’ve already cashed out half, the second half can either hit 5% or get stopped out. Either way, I’ve locked 50% of the winning outcome. That’s risk management.
The asymmetry is deliberate. I lose 3% on losers but capture 2-5% on winners because prices on TWAP-resolved markets still bounce. A tight 1% stop gets triggered by noise. A 3% stop lets the position breathe.
State Tracking for Reliable Exits
The position database tracks exit status for each open position:
CREATE TABLE positions (
id INTEGER PRIMARY KEY,
market_id TEXT,
entry_price REAL,
entry_qty INTEGER,
entry_time TEXT,
stop_loss_price REAL, -- 0.4365 for 0.45 entry
target_one_price REAL, -- 0.459 (2% profit)
target_two_price REAL, -- 0.4725 (5% profit)
exit_status TEXT, -- 'open', 'half_closed', 'closing', 'closed'
target_one_filled_at TEXT,
target_two_filled_at TEXT,
twap_window INTEGER, -- 30 or 60
linked_symbol TEXT -- e.g. 'btc/usd'
);
On every minute, the exit manager reads the latest RTDS TWAP and current market price and compares against these thresholds. When target one hits, it updates exit_status to ‘half_closed’ and records the fill time. This prevents double-exits and ensures the second half of the position doesn’t exit prematurely.
Limit orders are crucial here. Market orders on Polymarket can slip 0.5-1% depending on order book depth. A limit order at target_one_price of $0.459 sits on the book until filled. If the market only reaches $0.458, the order stays open. If it bounces to $0.461, it fills at $0.459 (better than market order at $0.461). Over 100 trades, limit order precision saves 0.3-0.5% in aggregate slippage.
The real lesson: don’t set stop losses too tight on TWAP-resolved prediction markets. Binary outcomes mean prices bounce around more than equity markets. A 1% stop gets triggered by noise. 3% gives the position room to breathe while still protecting against real reversals.
The trickiest exit is time decay near the TWAP window. As the market approaches resolution, prices converge hard to the TWAP path. If I’m long and the market isn’t moving with the TWAP, the time decay works against me. Exiting stale positions frees capital for new TWAP signals.
One thing that surprised me: the time-decay / TWAP-proximity exit generated more total profit than the pure profit-target exit. Not because individual exits were bigger, but because freeing stale capital meant the bot could take 2-3 more trades per day. Capital velocity matters more than any single trade’s P&L.
How Do You Transition from Paper Trading to Live?
I paper-traded for 6 weeks before going live. Paper trading means simulating trades without real money, just tracking P&L against live RTDS TWAP feeds and CLOB order books.
Paper trading revealed two strategy failures:
- Liquidity trap near TWAP windows — The culprit was insidious: entry prices looked good because I was catching markets right after a TWAP update, when stale limit orders still sat on the books. But exit? Nightmare. On markets with under $50K order book depth, especially in the final minutes before the 30s/60s TWAP settlement, I’d win the entry at $0.45 then get forced out at $0.42 because no one was buying. The tight spread at entry reversed hard at exit. Across 30 paper trades, this cost pattern showed up 7 times. Each time: entry P&L looked profitable (+2-3%), but the exit slippage erased it. One trade: up $2.25 on 50% exit at 2% profit, then the final 50% couldn’t execute near target. Ended up closing at $0.41 (-4% from entry) because the order book evaporated near the TWAP window. That single trade went from +$3 to -$1. Multiply that by 7 failed exits across the paper period, and I’m looking at roughly $2,000 in prevented losses once I added the liquidity + proximity check.
The fix was a simple gate before position entry:
async def check_market_liquidity_and_twap_window(market_id: str, twap_window: int) -> bool:
"""
Only enter if order book has sufficient depth AND we are not too close to the TWAP resolution window.
Skip if spread > 1% of mid-price or total depth < threshold.
"""
order_book = await polymarket.get_order_book(market_id)
best_bid = order_book['bids'][0]['price']
best_ask = order_book['asks'][0]['price']
mid = (best_bid + best_ask) / 2
spread_pct = ((best_ask - best_bid) / mid) * 100
total_depth = sum(qty for _, qty in order_book['bids'][:5]) + \
sum(qty for _, qty in order_book['asks'][:5])
if spread_pct > 1.0: # Spread too wide
return False
if total_depth < 500: # Not enough size to exit
return False
# Extra guard: skip if market closes within 2× the TWAP window
if seconds_to_resolution(market_id) < (twap_window * 2):
return False
return True
This single filter would have prevented 7 bad trades and saved ~$2,000 in real losses.
- Signal timing on thin TWAP books — The second failure was time-dependent. TWAP signals arriving during European market hours (roughly 2-8 AM UTC, when US traders sleep) got filled at punishment prices. Same TWAP move, same market, but the order book was thin and slow-moving. I’d get a 30s/60s TWAP breakout at 5 AM UTC. By the time I placed the order, retail traders on Polymarket hadn’t woken up yet. Bid-ask spread was 0.5-1% instead of 0.2%. Fills were 200-300 basis points worse than signals arriving during US peak hours (12pm-11pm UTC). Over the 30 paper trades, only 6 arrived during European dead hours, but those 6 had 0.5-1% worse fills than identical TWAP signals during US hours. That’s roughly $1,500 in prevented slippage if I’d filtered those out.
The time-of-day filter was even simpler:
async def is_peak_trading_hour() -> bool:
"""
Only process TWAP signals during peak US trading hours.
12pm-11pm UTC captures most US market activity.
"""
now_utc = datetime.datetime.utcnow()
current_hour = now_utc.hour
# Peak hours: 12pm-11pm UTC (7am-6pm EST)
if 12 <= current_hour < 23:
return True
return False
async def should_process_signal(signal: dict) -> bool:
if not await is_peak_trading_hour():
return False # Skip this TWAP signal
return True
That’s it. One hour check prevented $1,500 in unnecessary slippage by avoiding markets where the book moves like molasses around live TWAP updates.
Both failures would have cost real money live. The 6-week cost in time was worth it.
I ran at least 30 paper trades before going live. That’s the minimum to see the major failure modes once you’re trading against the official Chainlink TWAP feeds. Anything less and you’re just guessing.
Find us on:
- Youtube: https://youtube.com/@bosonax
- Telegram: https://t.me/bosonax
- X: https://x.com/xxniiinxx
Top comments (0)