<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jonathan Peterson</title>
    <description>The latest articles on DEV Community by Jonathan Peterson (@jonathanpetersonn).</description>
    <link>https://dev.to/jonathanpetersonn</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3822335%2F21831712-de84-4094-94be-31d823b99c96.jpg</url>
      <title>DEV Community: Jonathan Peterson</title>
      <link>https://dev.to/jonathanpetersonn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jonathanpetersonn"/>
    <language>en</language>
    <item>
      <title>I tapped into a public WebSocket feed and found a consistent pricing gap on Polymarket hiding in plain sight.</title>
      <dc:creator>Jonathan Peterson</dc:creator>
      <pubDate>Sat, 28 Mar 2026 10:20:36 +0000</pubDate>
      <link>https://dev.to/jonathanpetersonn/i-tapped-into-a-public-websocket-feed-and-found-a-consistent-pricing-gap-on-polymarket-hiding-in-5h0k</link>
      <guid>https://dev.to/jonathanpetersonn/i-tapped-into-a-public-websocket-feed-and-found-a-consistent-pricing-gap-on-polymarket-hiding-in-5h0k</guid>
      <description>&lt;p&gt;Check it out: &lt;a href="https://github.com/JonathanPetersonn/oracle-lag-sniper" rel="noopener noreferrer"&gt;https://github.com/JonathanPetersonn/oracle-lag-sniper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here’s the setup:&lt;/p&gt;

&lt;p&gt;Two live data streams running side by side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One from the Chainlink oracle that settles Polymarket’s 15-minute crypto markets&lt;/li&gt;
&lt;li&gt;One from the Polymarket order book itself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the difference between them is where things get interesting.&lt;/p&gt;

&lt;p&gt;The oracle updates almost instantly.&lt;br&gt;
The order book takes ~55 seconds to catch up.&lt;/p&gt;

&lt;p&gt;That means for nearly a full minute, tokens are trading on stale information — even though the “true” settlement price is already publicly available.&lt;/p&gt;

&lt;p&gt;Anyone can access that feed. It’s not hidden.&lt;br&gt;
It just consistently runs ahead of the market it decides.&lt;/p&gt;

&lt;p&gt;At first, it feels like a bug.&lt;/p&gt;

&lt;p&gt;It’s not.&lt;/p&gt;

&lt;p&gt;It’s just a rare case where a market inefficiency is clean, measurable, and sitting out in the open.&lt;/p&gt;

&lt;p&gt;So I built a bot to act on it — about 1,400 lines of Python.&lt;/p&gt;

&lt;p&gt;The strategy itself is almost boring:&lt;/p&gt;

&lt;p&gt;On every oracle tick, check three conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;price moved &amp;gt; 0.07% from market open  
time remaining &amp;gt; 5 minutes  
token price &amp;lt; $0.62  
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three are true → buy.&lt;/p&gt;

&lt;p&gt;Then wait for settlement: either $1 payout or a loss.&lt;/p&gt;

&lt;p&gt;Simple logic. The real challenge was making it actually work in real time.&lt;/p&gt;

&lt;p&gt;This isn’t a script you run once — it’s a system that has to stay alive.&lt;/p&gt;

&lt;p&gt;You need one process that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maintains a live WebSocket with sub-second updates&lt;/li&gt;
&lt;li&gt;Tracks 16 overlapping markets&lt;/li&gt;
&lt;li&gt;Evaluates signals on every tick&lt;/li&gt;
&lt;li&gt;Places orders without blocking the feed&lt;/li&gt;
&lt;li&gt;Sends Telegram alerts&lt;/li&gt;
&lt;li&gt;Persists state for crash recovery&lt;/li&gt;
&lt;li&gt;Monitors itself for failures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All at once.&lt;/p&gt;

&lt;p&gt;Since it’s almost entirely IO-bound, &lt;code&gt;asyncio&lt;/code&gt; was the natural fit.&lt;/p&gt;

&lt;p&gt;Seven concurrent tasks, one event loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;market_lifecycle_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;signal_evaluation_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="n"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;state_persist_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;redeem_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;sanity_check_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No threads. No locks. Just coordinated async tasks.&lt;/p&gt;

&lt;p&gt;The hardest bug I hit wasn’t logic — it was silence.&lt;/p&gt;

&lt;p&gt;Zombie WebSocket connections.&lt;/p&gt;

&lt;p&gt;Everything &lt;em&gt;looked&lt;/em&gt; healthy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connection open&lt;/li&gt;
&lt;li&gt;Ping/pong working&lt;/li&gt;
&lt;li&gt;No exceptions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But price data had stopped flowing.&lt;/p&gt;

&lt;p&gt;The bot just… idled. Quietly. Indefinitely.&lt;/p&gt;

&lt;p&gt;Timeouts didn’t catch it because heartbeat frames were still coming in.&lt;/p&gt;

&lt;p&gt;The fix was subtle: track the timestamp of the last &lt;em&gt;real&lt;/em&gt; price update using a monotonic clock. If that goes stale, kill the connection and force a reconnect.&lt;/p&gt;

&lt;p&gt;That one took multiple evenings to track down.&lt;/p&gt;

&lt;p&gt;Another painful lesson: silent failures outside the core system.&lt;/p&gt;

&lt;p&gt;I once ran the bot for 20 minutes thinking everything was perfect — only to realize all Telegram notifications were failing due to a wrong chat ID.&lt;/p&gt;

&lt;p&gt;No errors. No warnings. Just missing visibility.&lt;/p&gt;

&lt;p&gt;Now the bot validates everything upfront:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calls Telegram &lt;code&gt;getMe&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sends a test message&lt;/li&gt;
&lt;li&gt;Verifies API keys&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If anything is wrong, it fails immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Telegram chat_id=123456 is invalid.  
Tip: send a message to your bot, then use getUpdates to find your chat_id.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the data side:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;8,876 resolved markets&lt;/li&gt;
&lt;li&gt;146,000 price points&lt;/li&gt;
&lt;li&gt;5,017 trades triggered&lt;/li&gt;
&lt;li&gt;61.4% win rate across BTC, ETH, XRP, SOL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried hard to break it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Out-of-sample date splits&lt;/li&gt;
&lt;li&gt;Parameter sweeps&lt;/li&gt;
&lt;li&gt;Doubled fees&lt;/li&gt;
&lt;li&gt;Day-by-day analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It held up every time.&lt;/p&gt;

&lt;p&gt;But let’s keep expectations grounded.&lt;/p&gt;

&lt;p&gt;Liquidity is thin.&lt;br&gt;
Execution won’t be perfect.&lt;br&gt;
Other bots are already competing.&lt;br&gt;
And this edge will compress as it gets discovered.&lt;/p&gt;

&lt;p&gt;This isn’t a magic money machine.&lt;/p&gt;

&lt;p&gt;It’s a small, repeatable inefficiency — and a good example of what happens when public data moves faster than the market built on top of it.&lt;/p&gt;

&lt;p&gt;I’ve open-sourced the whole thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bot&lt;/li&gt;
&lt;li&gt;Backtests&lt;/li&gt;
&lt;li&gt;Data&lt;/li&gt;
&lt;li&gt;Live demo mode (no API keys needed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://github.com/JonathanPetersonn/oracle-lag-sniper" rel="noopener noreferrer"&gt;https://github.com/JonathanPetersonn/oracle-lag-sniper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you’ve built similar real-time asyncio systems, I’d love to hear how you approached it.&lt;/p&gt;

&lt;p&gt;This pattern — many long-lived, stateful tasks sharing a single event loop — feels common, but surprisingly under-documented in the wild.&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>cryptocurrency</category>
      <category>showdev</category>
      <category>web3</category>
    </item>
    <item>
      <title>I connected to a public WebSocket feed and found mispriced tokens on Polymarket</title>
      <dc:creator>Jonathan Peterson</dc:creator>
      <pubDate>Wed, 18 Mar 2026 22:35:32 +0000</pubDate>
      <link>https://dev.to/jonathanpetersonn/i-connected-to-a-public-websocket-feed-and-found-mispriced-tokens-on-polymarket-1931</link>
      <guid>https://dev.to/jonathanpetersonn/i-connected-to-a-public-websocket-feed-and-found-mispriced-tokens-on-polymarket-1931</guid>
      <description>&lt;p&gt;Open two WebSocket connections. One to the Chainlink oracle that settles Polymarket 15 minute crypto markets. One to the Polymarket order book.&lt;/p&gt;

&lt;p&gt;Watch them side by side.&lt;/p&gt;

&lt;p&gt;The oracle updates in under a second. The order book takes 55 seconds to reflect the same move. For almost a full minute tokens are sitting there priced on stale data.&lt;/p&gt;

&lt;p&gt;The settlement source is public. Anyone can connect. And it consistently runs almost a minute ahead of the market it settles.&lt;/p&gt;

&lt;p&gt;That felt like a bug at first. It is not. It is just how markets work. But usually you cannot measure it this cleanly.&lt;/p&gt;

&lt;p&gt;So I wrote about 1,400 lines of Python to trade on it.&lt;/p&gt;

&lt;p&gt;The core logic is pretty simple. On every oracle price tick, check three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;price moved &amp;gt; 0.07% from market open
time remaining &amp;gt; 5 minutes
token price &amp;lt; $0.62
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three true? Buy the cheap side. Wait for settlement. Collect $1 or lose your stake.&lt;/p&gt;

&lt;p&gt;The interesting part was not the strategy. It was making this actually run reliably.&lt;/p&gt;

&lt;p&gt;You need a single process that holds open a WebSocket receiving sub second updates, tracks 16 overlapping markets, evaluates signals on every tick, places orders without blocking the price feed, sends Telegram notifications, persists state for crash recovery, and monitors its own health. All at the same time.&lt;/p&gt;

&lt;p&gt;asyncio was the obvious choice. Almost zero CPU work, everything is IO bound. Seven concurrent tasks in one event loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;market_lifecycle_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;signal_evaluation_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="n"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;state_persist_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;redeem_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
    &lt;span class="nf"&gt;sanity_check_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No threads. No locks. No multiprocessing.&lt;/p&gt;

&lt;p&gt;The nastiest bug I hit was zombie WebSocket connections. Connection stays alive, ping pong works, no exceptions thrown. But price data silently stops flowing. Your bot just sits there doing absolutely nothing and looks healthy the whole time.&lt;/p&gt;

&lt;p&gt;A recv timeout does not help because heartbeat frames still arrive. I ended up tracking the last real price timestamp on monotonic clock and checking it on every timeout. If real data has not arrived in too long, kill the connection and force reconnect.&lt;/p&gt;

&lt;p&gt;I lost multiple evenings to this before I figured it out.&lt;/p&gt;

&lt;p&gt;Another thing that burned me. Start the bot. Logs look perfect. Run it for 20 minutes. Then discover the Telegram chat id was wrong and every notification silently failed. You had no idea anything was off.&lt;/p&gt;

&lt;p&gt;Now the bot verifies every external credential before entering the main loop. Hits Telegram getMe, sends a test message, checks Polymarket API keys. If something is wrong you see it in the first 2 seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Telegram chat_id=123456 is invalid.
Tip: send any message to @your_bot first, then use getUpdates to find your chat_id.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backtest ran across 8,876 resolved markets and 146,000 price points. 5,017 trades flagged. 61.4% win rate across BTC, ETH, XRP and SOL.&lt;/p&gt;

&lt;p&gt;I tried 7 different ways to break it. Date split, parameter grid search, doubled fees, daily breakdown. None of them killed it.&lt;/p&gt;

&lt;p&gt;But let me be real. The book is thin, fills will be harder than the backtest assumes, and the lag will shrink over time. This is not a retirement plan.&lt;/p&gt;

&lt;p&gt;The whole thing is open source. Demo mode connects to live data and paper trades without any API keys.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/JonathanPetersonn/oracle-lag-sniper" rel="noopener noreferrer"&gt;https://github.com/JonathanPetersonn/oracle-lag-sniper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have built similar real time asyncio systems I genuinely want to know how you structured them. This "many concurrent long lived tasks sharing state" pattern feels common but I could not find good open source references when I was building this.&lt;/p&gt;

</description>
      <category>websockets</category>
      <category>python</category>
      <category>trading</category>
      <category>asyncio</category>
    </item>
    <item>
      <title>Building a Real-Time Oracle Latency Bot for Polymarket with Python and asyncio</title>
      <dc:creator>Jonathan Peterson</dc:creator>
      <pubDate>Sat, 14 Mar 2026 09:45:50 +0000</pubDate>
      <link>https://dev.to/jonathanpetersonn/building-a-real-time-oracle-latency-bot-for-polymarket-with-python-and-asyncio-3gpg</link>
      <guid>https://dev.to/jonathanpetersonn/building-a-real-time-oracle-latency-bot-for-polymarket-with-python-and-asyncio-3gpg</guid>
      <description>&lt;p&gt;I recently open sourced a trading bot that exploits a timing gap on Polymarket 15 minute crypto markets. I am not going to rehash the strategy here (there is a &lt;a href="https://github.com/JonathanPetersonn/oracle-lag-sniper/blob/main/RESEARCH.md" rel="noopener noreferrer"&gt;full writeup in the repo&lt;/a&gt; if you are curious), but I do want to talk about the engineering side because I ran into some fun problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the bot actually needs to do
&lt;/h2&gt;

&lt;p&gt;So here is the list of things happening at the same time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hold open a WebSocket to a price oracle that pushes updates every fraction of a second&lt;/li&gt;
&lt;li&gt;Keep track of like 16 overlapping 15 minute markets at once&lt;/li&gt;
&lt;li&gt;Check trading signals on every single price tick&lt;/li&gt;
&lt;li&gt;Place orders, watch for fills, handle settlement&lt;/li&gt;
&lt;li&gt;Never drop a price update, even while placing an order&lt;/li&gt;
&lt;li&gt;If it crashes, pick up where it left off&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And all of this runs in one process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I went with asyncio
&lt;/h2&gt;

&lt;p&gt;My first thought was threads. But then I actually looked at what the bot does and almost all of it is just waiting. Waiting for WebSocket frames, waiting for HTTP responses, waiting on timers. There is basically no CPU work happening.&lt;/p&gt;

&lt;p&gt;asyncio fits this perfectly. One event loop, no locks, no thread safety headaches.&lt;/p&gt;

&lt;p&gt;The main loop kicks off about 7 tasks that all run at the same time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="n"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;           &lt;span class="c1"&gt;# WebSocket price feed
&lt;/span&gt;    &lt;span class="nf"&gt;market_lifecycle_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;     &lt;span class="c1"&gt;# market tracking
&lt;/span&gt;    &lt;span class="nf"&gt;signal_evaluation_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;    &lt;span class="c1"&gt;# trade signals
&lt;/span&gt;    &lt;span class="n"&gt;telegram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;# notifications
&lt;/span&gt;    &lt;span class="nf"&gt;state_persist_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;        &lt;span class="c1"&gt;# crash recovery
&lt;/span&gt;    &lt;span class="nf"&gt;redeem_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;               &lt;span class="c1"&gt;# auto redeem winners
&lt;/span&gt;    &lt;span class="nf"&gt;sanity_check_loop&lt;/span&gt;&lt;span class="p"&gt;(...),&lt;/span&gt;         &lt;span class="c1"&gt;# API health checks
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;They are all independent. The oracle task fires up first and then the market and signal loops wait a few seconds before starting so the price buffer has some data in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detecting zombie connections
&lt;/h2&gt;

&lt;p&gt;This one took me a while to figure out. The WebSocket connection stays alive, ping pong works fine, everything looks healthy. But no actual price data is coming through. The upstream just quietly stops sending.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;recv()&lt;/code&gt; timeout does not catch this because you still get heartbeat frames. The connection is technically alive, just useless.&lt;/p&gt;

&lt;p&gt;So I track the last time a real price came in using monotonic clock and check it on every timeout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_set&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TimeoutError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;since_last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_price_ts&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;since_last&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;STALL_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# connected but no real data, force reconnect
&lt;/span&gt;            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sounds obvious in hindsight but I spent an embarrassing amount of time debugging "why did the bot just sit there doing nothing for 20 minutes" before I added this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two oracle backends, same interface
&lt;/h2&gt;

&lt;p&gt;The bot can pull prices from two different sources:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ChainlinkOracle&lt;/strong&gt; connects directly to Chainlink Data Streams. HMAC auth, raw binary price reports. This is the "real" source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PolymarketOracle&lt;/strong&gt; uses Polymarket public RTDS relay. No authentication, JSON messages. Free and good enough for demo mode and live trading.&lt;/p&gt;

&lt;p&gt;Both write into the same &lt;code&gt;OracleBuffer&lt;/code&gt;. The signal evaluation loop has no idea which one is running and does not care. You switch between them with one env var.&lt;/p&gt;

&lt;p&gt;One quirk with the RTDS source: it needs you to send a PING string every 5 seconds. Not a WebSocket level ping, an actual text message that says PING. So I spin up a separate task just for that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ping_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_ping_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shutdown_event&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# main recv loop
&lt;/span&gt;&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ping_task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Verifying credentials before anything starts
&lt;/h2&gt;

&lt;p&gt;This was born out of pure frustration. I would start the bot, everything looks great, logs are clean. Then 10 minutes later I realize my Telegram token was wrong because a notification silently failed and I had no idea.&lt;/p&gt;

&lt;p&gt;Now the bot hits the Telegram API with &lt;code&gt;getMe&lt;/code&gt; and sends a test message before it even enters the main loop. Same thing for Polymarket API keys. If something is off you know immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERROR: Telegram chat_id=123456 is invalid. Response: Forbidden: bots can't send messages to bots
Tip: send any message to @your_bot first, then use getUpdates to find your chat_id.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tiny change but it saved me so much time after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bisect trick for the price buffer
&lt;/h2&gt;

&lt;p&gt;Prices are stored in a per asset circular buffer (just a &lt;code&gt;deque&lt;/code&gt; with maxlen). The signal loop constantly needs to look up "what was the oracle price when this market opened?" which means searching by timestamp.&lt;/p&gt;

&lt;p&gt;The obvious tool is &lt;code&gt;bisect_right&lt;/code&gt; but it does not work on a deque because there is no &lt;code&gt;__getitem__&lt;/code&gt; on the timestamp component. I could copy to a list every time but that felt gross on a hot path.&lt;/p&gt;

&lt;p&gt;Instead I wrote a tiny wrapper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;_DequeTimestampKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;__slots__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_buf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__len__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_buf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__getitem__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# timestamp component
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;O(log n) lookups, zero allocations. Probably my favorite 10 lines in the whole codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash recovery
&lt;/h2&gt;

&lt;p&gt;State gets dumped to a JSON file every 30 seconds. Open positions, risk counters, daily P&amp;amp;L, kill switch status. When the bot restarts it reads that file and picks up where it left off. Any markets that expired while it was down get resolved on the next cycle.&lt;/p&gt;

&lt;p&gt;Nothing fancy but it means I can restart the bot mid session without losing track of anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it performed
&lt;/h2&gt;

&lt;p&gt;61.4% win rate over 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 split by date.&lt;/p&gt;

&lt;p&gt;Full source is here: &lt;a href="https://github.com/JonathanPetersonn/oracle-lag-sniper" rel="noopener noreferrer"&gt;https://github.com/JonathanPetersonn/oracle-lag-sniper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would genuinely love to hear how other people have structured similar systems. The "bunch of long lived concurrent tasks sharing state" pattern comes up a lot but I have not found many open source projects that do it well to learn from.&lt;/p&gt;

</description>
      <category>python</category>
      <category>asyncio</category>
      <category>websockets</category>
      <category>trading</category>
    </item>
  </channel>
</rss>
