I have been building a trading bot that exploits a timing gap on Polymarket 15 minute crypto markets. The Chainlink oracle that settles these markets updates in under a second but the order book takes about 55 seconds to react. During that gap one side is mispriced.
I am not going to go deep on the trading strategy here. There is a full research writeup in the repo (github.com/JonathanPetersonn/oracle-lag-sniper). Instead I want to walk through the technical problems I ran into and how I solved them, because some of these apply to any system that juggles multiple real time data streams.
The architecture
Everything runs in a single Python process using asyncio. About 7 long lived tasks all running concurrently:
A WebSocket connection to the price oracle that pushes updates every fraction of a second. A market lifecycle loop tracking around 16 overlapping markets. A signal evaluation loop checking conditions on every price tick. An order manager. A Telegram notification bot. A state persistence loop for crash recovery. A health check loop.
They all share state through a common oracle buffer and a market state dictionary. No threads, no locks. The workload is almost entirely IO bound so asyncio handles it naturally.
The zombie connection problem
This one cost me hours. The WebSocket connection looks perfectly healthy. Ping pong works. No errors. But price data just stops arriving. The upstream quietly stops sending and your bot sits there doing nothing thinking everything is fine.
A recv timeout does not catch it because heartbeat frames still come through. The connection is alive, just useless.
The fix was tracking the last time an actual price update arrived using monotonic clock. On every timeout I check how long it has been since real data came in. If it exceeds a threshold I kill the connection and force a reconnect.
Sounds simple but I did not think of it until after spending an embarrassing amount of time staring at logs wondering why the bot went silent for 20 minutes.
Two oracle backends, one interface
The bot supports two completely different data sources:
ChainlinkOracle connects directly to Chainlink Data Streams. HMAC authentication, binary encoded price reports. This is the raw source of truth.
PolymarketOracle uses Polymarket public relay. No auth needed, JSON messages. Free and works great for demo mode and live trading.
Both implement the same interface and write into the same OracleBuffer. The rest of the system has no idea which one is running. Switching is a single environment variable.
One fun detail about the Polymarket relay. It requires you to send a literal PING text message every 5 seconds. Not a WebSocket level ping frame, an actual string. So there is a small asyncio task that just does that in a loop alongside the main recv loop.
Credential verification at startup
I got burned by this multiple times during development. Start the bot, everything looks clean in the logs, run it for a while, then realize my Telegram chat id was wrong and I had been missing every notification.
Now before the main loop even starts the bot calls the Telegram getMe endpoint to verify the token, then sends a test message to verify the chat id. Same thing for Polymarket API keys. If anything is misconfigured you see it in the first 2 seconds, not 10 minutes later.
Bisect on a deque
Oracle prices live in a per asset circular buffer using deque with maxlen. The signal loop constantly needs to look up the price at a specific timestamp which means binary search.
Problem is bisect_right does not work on a deque because there is no getitem on the timestamp component. Copying to a list on every lookup felt wasteful on a hot path.
I wrote a tiny wrapper class that exposes len and getitem and just pulls the timestamp from each deque entry. Ten lines of code, O(log n) lookups, zero allocations. Honestly one of my favorite little tricks in the project.
Crash recovery
Every 30 seconds the bot dumps its state to a JSON file. Open positions, risk counters, daily profit and loss, kill switch status. On restart it reads the file and picks up where it left off. Markets that expired during downtime get cleaned up on the next cycle.
Nothing groundbreaking but it means I can restart the process without losing context.
Results
61.4% win rate across 5,017 backtested trades. Consistent across BTC, ETH, XRP and SOL. The backtest includes 7 falsification tests and a 60/40 in sample out of sample date split.
The whole thing is open source.
github.com/JonathanPetersonn/oracle-lag-sniper
If you have built something similar with asyncio or have thoughts on better patterns for managing a bunch of concurrent long lived tasks I would love to hear about it.
No responses yet.