<?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: Blockchain Rust Engineer</title>
    <description>The latest articles on DEV Community by Blockchain Rust Engineer (@casatrick).</description>
    <link>https://dev.to/casatrick</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3974435%2F236c2f7c-f929-4206-b498-7420159935e8.jpg</url>
      <title>DEV Community: Blockchain Rust Engineer</title>
      <link>https://dev.to/casatrick</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/casatrick"/>
    <language>en</language>
    <item>
      <title>My Bot Showed +$84 Profit. The Chain Said +$11. Here's Why.</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Thu, 02 Jul 2026 08:42:25 +0000</pubDate>
      <link>https://dev.to/casatrick/my-bot-showed-84-profit-the-chain-said-11-heres-why-2nai</link>
      <guid>https://dev.to/casatrick/my-bot-showed-84-profit-the-chain-said-11-heres-why-2nai</guid>
      <description>&lt;p&gt;I ran 200 paper trades. Win rate was 91%. P&amp;amp;L tracker showed positive every session. I moved to live. First week looked fine.&lt;br&gt;
Then I pulled the actual on-chain data.&lt;br&gt;
The numbers didn't match. Not close. The bot's internal tracker and what actually happened on Polygon were two different stories.&lt;br&gt;
Here's every reason why, and how I fixed each one.&lt;/p&gt;

&lt;p&gt;Mistake 1: Counting unresolved positions as profit&lt;br&gt;
The bot marks a trade as "won" when YES hits 0.98+. But Polymarket doesn't pay you at 0.98. It pays you $1.00 at resolution, and only after the oracle confirms. Until then, the position is worth approximately 0.98 but not actually 0.98.&lt;br&gt;
Early tracker counted the unrealized gain immediately. Looked great. Wasn't real.&lt;br&gt;
Fix: don't count a trade as closed until resolution is confirmed on-chain. Add a pending_resolution state between fill and confirmed payout.&lt;/p&gt;

&lt;p&gt;Mistake 2: Fee drag compounding silently&lt;br&gt;
Polymarket's taker fee is ~2% per trade. On a single trade that's obvious. Across a session of 40 trades it's invisible until you add it up.&lt;br&gt;
Session with 40 trades at 0.88 avg entry:&lt;/p&gt;

&lt;p&gt;Gross win rate: 91% → looks profitable&lt;br&gt;
Fee per trade: 2%&lt;br&gt;
Total fee drag across session: ~80% of gross profit gone&lt;/p&gt;

&lt;p&gt;I was profitable on paper. Roughly breakeven on the chain. The fee column existed in my tracker but it wasn't being subtracted from the running P&amp;amp;L total. It was just sitting there as a label.&lt;br&gt;
Fix: subtract fees from P&amp;amp;L in real time. Not as a separate column. Off the number you're watching.&lt;/p&gt;

&lt;p&gt;Mistake 3: Slippage estimated, not measured&lt;br&gt;
The backtest used 1.5% slippage assumption. Reasonable. The problem: live fills on thin books near resolution were running 2.8-3.4% in bad sessions. The backtest never saw that because the backtest assumed I'd always get the mid price plus a fixed percentage.&lt;br&gt;
Real slippage isn't fixed. It depends on book depth at the exact moment you enter. And book depth near resolution is not the same as book depth two minutes before resolution.&lt;br&gt;
Fix: log expected fill (mid at signal time) vs actual fill on every trade. Track the delta. If rolling average delta exceeds 2.5%, the session pauses.&lt;/p&gt;

&lt;p&gt;Mistake 4: Redemption haircut&lt;br&gt;
This one I didn't find until week three.&lt;br&gt;
When a market resolves YES, your YES tokens are worth $1.00 each. But redeeming them costs gas on Polygon. Small amounts get eaten by gas disproportionately. A $4.80 position costs ~$0.15 to redeem. That's 3.1% off the top before fees even enter the picture.&lt;br&gt;
The bot was entering positions that were too small for the math to work after redemption costs. Position sizing assumed fees only. Didn't account for gas.&lt;br&gt;
Fix: minimum position size enforced at $8.00. Below that the redemption cost percentage is too high.&lt;/p&gt;

&lt;p&gt;Mistake 5: Win rate calculated wrong&lt;br&gt;
This is the embarrassing one.&lt;br&gt;
Early version: win rate = trades where final P&amp;amp;L &amp;gt; 0 / total trades.&lt;br&gt;
Problem: trades that were still in pending_resolution weren't counted yet. So win rate was calculated over a subset of completed trades, and that subset was biased toward faster-resolving markets which happened to be the ones with cleaner fills. The actually hard sessions were still pending.&lt;br&gt;
When I corrected for this and counted all trades including pending ones marked to market, win rate dropped from 91% to 84%. Still good. But not the number I'd been watching.&lt;br&gt;
Fix: win rate denominator includes all trades, pending or closed. Pending positions marked to current mid price.&lt;/p&gt;

&lt;p&gt;After fixing all five, the P&amp;amp;L tracker and on-chain numbers now match within 2% across sessions. The 2% variance is gas estimation rounding, which I'll tighten later.&lt;br&gt;
The honest version: the strategy still works. The edge is real. But the first month of "profit" I was looking at was a different number than what actually happened. Getting those two numbers to match was as much work as building the strategy itself.&lt;br&gt;
If you're running a trading bot and haven't reconciled your internal P&amp;amp;L against your actual wallet balance recently, do that now. The gap will tell you something.&lt;br&gt;
Code: github.com/casatrick/polymarket-trading-bot&lt;/p&gt;

</description>
      <category>rust</category>
      <category>polymarket</category>
      <category>trading</category>
      <category>backtesting</category>
    </item>
    <item>
      <title>Polymarket Arbitrage Strategies - Analysis</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Mon, 29 Jun 2026 07:22:16 +0000</pubDate>
      <link>https://dev.to/casatrick/polymarket-arbitrage-strategies-analysis-2ch3</link>
      <guid>https://dev.to/casatrick/polymarket-arbitrage-strategies-analysis-2ch3</guid>
      <description>&lt;p&gt;Polymarket profiles are public. Position data is public. If you know what to look for, you can reconstruct exactly what strategy an automated trader is running just from their open positions and prediction count.&lt;/p&gt;

&lt;p&gt;I pulled three accounts that showed bot-like behavior (thousands of predictions, repetitive markets) and reverse-engineered their strategies. Then I built an open-source version.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I looked for
&lt;/h2&gt;

&lt;p&gt;Bots on Polymarket tend to leave fingerprints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prediction count in the thousands&lt;/strong&gt; - humans don't make 16,000 trades&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concentrated in one market type&lt;/strong&gt; - usually the 5-minute BTC Up/Down markets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent position sizes&lt;/strong&gt; - not random gut-feel bet sizing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positions on both sides of the same market&lt;/strong&gt; - the dead giveaway for arb bots&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The three accounts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/first"&gt;@first&lt;/a&gt; - directional momentum bot
&lt;/h3&gt;

&lt;p&gt;19,923 predictions since October 2025. All positions I could see were in "Bitcoin Up or Down - 5 minute" markets, buying a single side (Up or Down, not both). Entry prices: 33¢, 44¢, 69¢, 76¢, 81¢, 89¢.&lt;/p&gt;

&lt;p&gt;This isn't arbitrage. It's a bot reading BTC price momentum and betting on the 5-minute direction. The 33¢ entry that returned 202% was almost certainly triggered on a strongly trending candle where the market was underpricing the obvious direction.&lt;/p&gt;

&lt;p&gt;Total P&amp;amp;L: +$1,484 on ~20k trades. That's thin per-trade, but it's been running for 8 months. Directional edge is slower to compete away than mechanical arb.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/second"&gt;@second&lt;/a&gt; - merge/redeem arb, fully exited
&lt;/h3&gt;

&lt;p&gt;16,454 predictions since April 2026. Current positions: zero. Portfolio value: $0. Biggest win: $606.&lt;/p&gt;

&lt;p&gt;This is what a successful merge/redeem arb bot looks like after it's done. The strategy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Scan binary markets where P(YES) + P(NO) &amp;lt; $1.00
2. Buy both YES and NO simultaneously
3. Merge tokens into a complete set
4. Redeem the complete set for $1.00
5. Pocket the difference (minus fees)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When YES+NO = $0.95, you make $0.05 per share bought. With large position sizes and fast execution, this adds up. bowbow16 ran 16k trades in ~2 months, extracted its edge, and is now sitting idle - probably because the spread has tightened as more bots entered.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a class="mentioned-user" href="https://dev.to/third"&gt;@third&lt;/a&gt; - live intra-market arb, caught mid-trade
&lt;/h3&gt;

&lt;p&gt;Joined June 2026 (brand new). 4,170 predictions. Current open positions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Market: Bitcoin Up or Down - June 29, 2:45AM-2:50AM ET

Down  43.4¢  625.1 shares  →  $309.42  (+$38.11)
Up    59.1¢  595.9 shares  →  $300.95  (-$51.00)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both sides. Same market. Entry was when Down + Up summed below $1.00 - but prices moved after entry, so combined they're now 102.5¢. The bot is slightly underwater waiting for resolution. This is exactly how the strategy looks mid-execution.&lt;/p&gt;

&lt;p&gt;What's interesting: it entered both sides within the same 5-minute window, holding until the market resolves, then collects $1.00 per complete set regardless of which direction BTC moved. The directional outcome doesn't matter. Only the entry price matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this tells you about the current state of Polymarket arb
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The arb is real but the windows are narrow.&lt;/strong&gt; third is already underwater on a position from the spread moving 2-3 cents between signal and fill. At the scale these bots operate, execution latency is the main variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pure mechanical arb has a lifespan.&lt;/strong&gt; second ran 16k trades then stopped. The edge gets competed away as more bots scan the same markets. Directional strategies (first, still active after 8 months) seem to have more staying power because they require actual market judgment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 5-minute BTC markets are the arb hotspot.&lt;/strong&gt; Every bot I found is concentrated there. They resolve in 5 minutes, have consistent liquidity, and the binary structure makes merge/redeem clean. The tradeoff is that every other bot is also there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bot I built
&lt;/h2&gt;

&lt;p&gt;Based on studying these strategies, I built a Polymarket bot in Python that implements 5 strategies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 1 - Intra-market merge/redeem&lt;/strong&gt; (what wolf9478 is doing)&lt;br&gt;
Buy YES+NO when sum &amp;lt; $1.00, merge, redeem. Risk-free on paper, execution-speed-dependent in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 2 - Combinatorial arb&lt;/strong&gt;&lt;br&gt;
Markets with 3+ outcomes. All outcome prices should sum to $1.00. When they don't, same approach.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 3 - Cross-platform arb&lt;/strong&gt;&lt;br&gt;
Compare Polymarket prediction prices to Binance spot. If BTC has a 70% chance of being above $X and the market is pricing it at 45%, buy the underpriced side and wait for convergence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 4 - Endgame arb&lt;/strong&gt;&lt;br&gt;
Near-resolution markets where one outcome is at 93%+ probability. Buy it, wait a few hours, collect ~7% return on near-certain outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strategy 5 - Momentum/mean-reversion&lt;/strong&gt;&lt;br&gt;
What eknih appears to be doing. Track YES/NO prices as a time series, apply z-score and RSI, enter when overextended. Three timeframes: 5m scalp, 15m swing, 1h position.&lt;/p&gt;

&lt;p&gt;The bot runs all strategies in parallel, then uses a composite scorer to rank signals:&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;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;profit_score&lt;/span&gt;    &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;      &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;strategy_prio&lt;/span&gt;   &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;urgency&lt;/span&gt;         &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="n"&gt;risk_reward&lt;/span&gt;     &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Top-ranked signal executes first. Risk manager prevents duplicate positions in the same market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from watching these bots
&lt;/h2&gt;

&lt;p&gt;If you're building something similar, here's what the data suggests:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed beats strategy for merge/redeem.&lt;/strong&gt; The arb exists briefly. If your order takes 2 seconds to fill, someone with a 200ms latency already closed it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't rely on a single strategy.&lt;/strong&gt; second's pure arb approach ran out of steam. first's directional approach is still grinding months later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The 5m BTC market is crowded.&lt;/strong&gt; Profitable but competitive. If you're starting fresh, look at less-trafficked markets where the arb windows stay open longer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track your Sharpe, not just P&amp;amp;L.&lt;/strong&gt; A high prediction count with modest P&amp;amp;L (like first) might be negative expected value after fees if the win rate isn't high enough.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/casatrick/polymarket-arbitrage-bot" rel="noopener noreferrer"&gt;github.com/casatrick/polymarket-arbitrage-bot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not financial advice. Polymarket can be illiquid. You can lose money. These accounts might not even be bots - but 19,000 predictions in 8 months is hard to do by hand.&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>trading</category>
      <category>strategy</category>
      <category>bot</category>
    </item>
    <item>
      <title>I Backtested My Polymarket Bot Against Real Order Book Data. Here's What I Found.</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Sat, 27 Jun 2026 14:56:52 +0000</pubDate>
      <link>https://dev.to/casatrick/i-backtested-my-polymarket-bot-against-real-order-book-data-heres-what-i-found-52gp</link>
      <guid>https://dev.to/casatrick/i-backtested-my-polymarket-bot-against-real-order-book-data-heres-what-i-found-52gp</guid>
      <description>&lt;p&gt;Six months of running a Rust trading bot live, then replaying historical tick data through the same code. The results were not what I expected.&lt;/p&gt;

&lt;p&gt;The bot had been running for six months. It felt like it was working. P&amp;amp;L was positive. Win rate looked decent. And then I actually backtested it.&lt;/p&gt;

&lt;p&gt;Turns out I had been lucky on a few large positions that masked two strategies that were quietly bleeding. I wouldn't have known without replaying real historical data through the same code that runs in production.&lt;/p&gt;

&lt;p&gt;This is how I built the backtester, what data I used, and what I found.&lt;/p&gt;




&lt;h2&gt;
  
  
  The data problem
&lt;/h2&gt;

&lt;p&gt;First thing I learned: Polymarket has no historical endpoint on their public API. The CLOB API gives you live order book state and WebSocket for real-time updates, but nothing historical. So you can't just hit an endpoint and get six months of order book history.&lt;/p&gt;

&lt;p&gt;Three options I found:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pmdata.dev&lt;/strong&gt; - tick-level L2 order book data from Feb 2026, served as parquet files. One HTTP request per market slug, download the whole history. This is what I ended up using.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;telonex.io&lt;/strong&gt; - similar, tick-by-tick order book depth captured on every change, not interval-sampled. More complete but costs more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Polygon subgraph&lt;/strong&gt; - on-chain trade fills only, no order book depth. Fine for trade history, useless if you want to simulate fills against the actual book.&lt;/p&gt;

&lt;p&gt;For backtesting a strategy that cares about spread and order book depth, you need L2 data. Trade-only data hides slippage and gives you an unrealistically clean picture.&lt;/p&gt;

&lt;p&gt;Download looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// fetch historical parquet for a market slug&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"btc-updown-5m-1778803200"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.pmdata.dev/download/poly_l2/{}.parquet"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// stream it down, deserialize into your event format&lt;/span&gt;
&lt;span class="c1"&gt;// pmdata returns: timestamp, side, price, size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a small Rust binary that downloads the parquet, deserializes it into the same &lt;code&gt;FeedEvent&lt;/code&gt; enum the live bot uses, and writes it to a local file. One-time setup per market.&lt;/p&gt;




&lt;h2&gt;
  
  
  The backtester structure
&lt;/h2&gt;

&lt;p&gt;The key decision was to not write a separate backtester. The live bot already has a clean interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FeedEvent → strategy engine → Option&amp;lt;Signal&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The feed task is the only thing that changes between live and backtest. In production it reads from a WebSocket. In backtest it reads from a file. Everything downstream - order book, strategy, risk manager - is identical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;DataSource&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Live&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Backtest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PathBuf&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&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;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DataSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FeedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;DataSource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Live&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;run_websocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;DataSource&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Backtest&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;replay_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;replay_file&lt;/code&gt; reads events from disk and sends them through the same channel the live feed uses. The strategy task never knows the difference. This is the part I'm happiest with - there's no "backtest mode" flag scattered through the codebase. It's just a different data source.&lt;/p&gt;

&lt;p&gt;One thing you have to handle: timing. In production, events arrive in real time. In backtest, you're replaying them as fast as the CPU can process them. You need to either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replay at wall-clock speed (slow, but simulates real latency)&lt;/li&gt;
&lt;li&gt;Replay as fast as possible with timestamps preserved in events (what I do)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with option 2. The strategy uses event timestamps, not wall-clock time, so the math stays correct even at 100x playback speed. Running six months of data takes about 40 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simulating fills
&lt;/h2&gt;

&lt;p&gt;This is where most backtests lie to you.&lt;/p&gt;

&lt;p&gt;The naive approach: if your signal says buy at $0.38 and the historical data shows a trade at $0.38, assume you filled. This is wrong for two reasons.&lt;/p&gt;

&lt;p&gt;First, you weren't the only one trying to buy at $0.38. Queue position matters. In a thin market, by the time your order would have reached the exchange, that level might be gone.&lt;/p&gt;

&lt;p&gt;Second, your order moves the book. A $500 position in a market with $2,000 in liquidity is meaningful. The naive approach pretends you're a ghost that trades without impact.&lt;/p&gt;

&lt;p&gt;What I do instead: simulate fills against the L2 snapshot at the time of the signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;simulate_fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;OrderBook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// (fill_price, actual_size)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Buy&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="py"&gt;.asks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sell&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="py"&gt;.bids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="nf"&gt;.is_zero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt;&lt;span class="nf"&gt;.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="py"&gt;.size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;take&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remaining&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ZERO&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// not enough liquidity to fill fully&lt;/span&gt;
        &lt;span class="c1"&gt;// partial fill or no fill depending on your strategy&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fill_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cost&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;fill_price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This walks the order book and simulates the actual fill price including slippage. It's still not perfect - you're still assuming you could have taken all that liquidity - but it's much closer to reality than assuming a perfect fill at the best price.&lt;/p&gt;

&lt;p&gt;I also added a 800µs delay to every simulated fill to account for the actual round-trip time my live bot experiences. Sounds pedantic. It actually mattered on a few strategies that depended on very short windows.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I found
&lt;/h2&gt;

&lt;p&gt;Six strategies I'd been running. Here's the honest breakdown:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mean reversion on spread widening&lt;/strong&gt; - positive, 61% win rate over 4,200 trades. This is the core strategy. Backtest matched live performance closely, which gave me more confidence in it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pre-resolution snipe (last 60 seconds)&lt;/strong&gt; - positive but much thinner than I thought. The live P&amp;amp;L looked good because I'd caught a few large moves. Backtest showed the median trade on this strategy barely covers fees. Still running it but at reduced size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Momentum on correlated markets&lt;/strong&gt; - flat to slightly negative. I'd convinced myself there was a signal here. There isn't, or at least not a consistent one. Turned this off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kelly-sized position scaling&lt;/strong&gt; - neutral effect. The Kelly sizing doesn't generate alpha, it just changes the variance profile. Expected, but good to confirm.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spread capture / market making&lt;/strong&gt; - negative. I knew this was experimental. Confirmed it doesn't work at my size in these markets. Turned off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;News event repricing&lt;/strong&gt; - not enough data to evaluate. Only 23 trades in six months. Keeping it on but can't draw conclusions yet.&lt;/p&gt;

&lt;p&gt;The uncomfortable finding: two of the five strategies I thought were working were either flat or negative. The positive P&amp;amp;L I'd been seeing was almost entirely the mean reversion strategy plus some fortunate sizing on a few outlier trades.&lt;/p&gt;




&lt;h2&gt;
  
  
  One thing the backtest can't tell you
&lt;/h2&gt;

&lt;p&gt;Whether the edge still exists.&lt;/p&gt;

&lt;p&gt;Backtesting against historical data tells you whether a strategy &lt;em&gt;would have worked&lt;/em&gt; on past data. It doesn't tell you whether the market has changed, whether more bots have entered the same trades, or whether the conditions that created the edge in January still exist in June.&lt;/p&gt;

&lt;p&gt;The mean reversion strategy shows a slight decay in win rate from Feb to June - 64% in Q1, 58% in Q2. Could be noise. Could be the edge compressing as more participants find it. I don't know yet.&lt;/p&gt;

&lt;p&gt;This is the thing nobody says loudly enough about backtesting: a good backtest raises questions, it doesn't answer them. Finding out two strategies don't work is useful. The real question is whether the one that does work keeps working.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup if you want to replicate this
&lt;/h2&gt;

&lt;p&gt;Grab an API key from pmdata.dev (they have a free tier). Download parquet files for whatever markets you want. Write a deserializer that maps their schema to your &lt;code&gt;FeedEvent&lt;/code&gt; type. Swap your data source enum. Run.&lt;/p&gt;

&lt;p&gt;Full code including the replay harness and fill simulator:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;github.com/casatrick/polymarket-trading-bot&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The backtester is in &lt;code&gt;src/backtest/&lt;/code&gt;. It's about 300 lines including the fill simulator and the P&amp;amp;L tracker.&lt;/p&gt;




&lt;p&gt;If you've built something similar or found a better data source, drop it in the comments. Especially interested in whether anyone has found a way to get clean pre-February 2026 data - the Polygon subgraph has it but the order book reconstruction from on-chain data is painful.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>trading</category>
      <category>polymarket</category>
      <category>backtesting</category>
    </item>
    <item>
      <title>Building a Trading Bot Is Easy. Building One That Survives Overnight Isn't.</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Thu, 25 Jun 2026 08:12:14 +0000</pubDate>
      <link>https://dev.to/casatrick/building-a-trading-bot-is-easy-building-one-that-survives-overnight-isnt-25j5</link>
      <guid>https://dev.to/casatrick/building-a-trading-bot-is-easy-building-one-that-survives-overnight-isnt-25j5</guid>
      <description>&lt;p&gt;When I first started building trading bots, I thought the difficult part would be the strategy.&lt;/p&gt;

&lt;p&gt;I spent weeks researching market behavior, testing ideas, and optimizing entry conditions. Eventually, I had a strategy that looked promising in backtests and paper trading.&lt;/p&gt;

&lt;p&gt;Then I deployed it.&lt;/p&gt;

&lt;p&gt;Within the first few days, I learned something important:&lt;/p&gt;

&lt;p&gt;Building a trading bot is easy. Building one that can run unattended for days without breaking is much harder.&lt;/p&gt;

&lt;p&gt;This article isn't about trading strategies. It's about the engineering problems that started appearing the moment my bot entered production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #1: Network Connections Always Fail
&lt;/h2&gt;

&lt;p&gt;My first version relied heavily on WebSocket streams.&lt;/p&gt;

&lt;p&gt;Everything worked perfectly during testing.&lt;/p&gt;

&lt;p&gt;Then one day I checked the dashboard and noticed the bot hadn't received any market updates for almost 20 minutes.&lt;/p&gt;

&lt;p&gt;The WebSocket connection had silently died.&lt;/p&gt;

&lt;p&gt;The process was still running, but it wasn't receiving any data.&lt;/p&gt;

&lt;p&gt;The bot looked healthy.&lt;/p&gt;

&lt;p&gt;It wasn't.&lt;/p&gt;

&lt;p&gt;The solution was adding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Heartbeat monitoring&lt;/li&gt;
&lt;li&gt;Automatic reconnection&lt;/li&gt;
&lt;li&gt;Connection timeout detection&lt;/li&gt;
&lt;li&gt;Data freshness checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the bot continuously verifies that new market data is arriving.&lt;/p&gt;

&lt;p&gt;If updates stop, it reconnects automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #2: Processes Crash When You Least Expect It
&lt;/h2&gt;

&lt;p&gt;Unhandled exceptions happen.&lt;/p&gt;

&lt;p&gt;Unexpected API responses happen.&lt;/p&gt;

&lt;p&gt;Third-party services go down.&lt;/p&gt;

&lt;p&gt;Memory issues happen.&lt;/p&gt;

&lt;p&gt;My first production crash occurred at 3 AM.&lt;/p&gt;

&lt;p&gt;The bot stopped trading and I didn't notice until the next morning.&lt;/p&gt;

&lt;p&gt;That led me to introduce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PM2 process management&lt;/li&gt;
&lt;li&gt;Automatic restarts&lt;/li&gt;
&lt;li&gt;Crash logging&lt;/li&gt;
&lt;li&gt;Error alerts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A trading bot should never rely on someone manually restarting it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #3: Logging Becomes More Important Than Trading
&lt;/h2&gt;

&lt;p&gt;At first, I logged almost nothing.&lt;/p&gt;

&lt;p&gt;When something went wrong, I had no idea what happened.&lt;/p&gt;

&lt;p&gt;Questions became impossible to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why did this position open?&lt;/li&gt;
&lt;li&gt;Why wasn't this order submitted?&lt;/li&gt;
&lt;li&gt;Why did the bot skip this opportunity?&lt;/li&gt;
&lt;li&gt;Why was this trade closed?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I eventually started logging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Market snapshots&lt;/li&gt;
&lt;li&gt;Signal decisions&lt;/li&gt;
&lt;li&gt;Order submissions&lt;/li&gt;
&lt;li&gt;Fill confirmations&lt;/li&gt;
&lt;li&gt;Risk checks&lt;/li&gt;
&lt;li&gt;Latency measurements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result was a massive improvement in debugging.&lt;/p&gt;

&lt;p&gt;The fastest way to fix a problem is knowing exactly where it occurred.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #4: Latency Isn't Constant
&lt;/h2&gt;

&lt;p&gt;I originally measured latency once and assumed it stayed the same.&lt;/p&gt;

&lt;p&gt;That assumption was wrong.&lt;/p&gt;

&lt;p&gt;Latency changes constantly.&lt;/p&gt;

&lt;p&gt;Some requests completed in a few milliseconds.&lt;/p&gt;

&lt;p&gt;Others suddenly took hundreds of milliseconds.&lt;/p&gt;

&lt;p&gt;That difference matters when trading fast-moving markets.&lt;/p&gt;

&lt;p&gt;I began recording:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Market data arrival time&lt;/li&gt;
&lt;li&gt;Signal generation time&lt;/li&gt;
&lt;li&gt;Order submission time&lt;/li&gt;
&lt;li&gt;Exchange response time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only after measuring every stage separately could I identify real bottlenecks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #5: Memory Leaks Are Sneaky
&lt;/h2&gt;

&lt;p&gt;One version of my bot looked stable.&lt;/p&gt;

&lt;p&gt;CPU usage was fine.&lt;/p&gt;

&lt;p&gt;No crashes.&lt;/p&gt;

&lt;p&gt;No errors.&lt;/p&gt;

&lt;p&gt;Then after several days, memory usage slowly climbed until the process became unstable.&lt;/p&gt;

&lt;p&gt;The culprit wasn't a complicated algorithm.&lt;/p&gt;

&lt;p&gt;It was old data structures that were never cleaned up.&lt;/p&gt;

&lt;p&gt;Temporary caches became permanent caches.&lt;/p&gt;

&lt;p&gt;Historical snapshots accumulated forever.&lt;/p&gt;

&lt;p&gt;The lesson:&lt;/p&gt;

&lt;p&gt;Long-running systems expose problems that short tests never reveal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #6: Monitoring Matters More Than Features
&lt;/h2&gt;

&lt;p&gt;Most developers enjoy building features.&lt;/p&gt;

&lt;p&gt;Very few enjoy building monitoring.&lt;/p&gt;

&lt;p&gt;I was no different.&lt;/p&gt;

&lt;p&gt;But eventually I realized something:&lt;/p&gt;

&lt;p&gt;A simple strategy with excellent monitoring is usually safer than an advanced strategy with no visibility.&lt;/p&gt;

&lt;p&gt;Today I monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process health&lt;/li&gt;
&lt;li&gt;Memory usage&lt;/li&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;WebSocket status&lt;/li&gt;
&lt;li&gt;API errors&lt;/li&gt;
&lt;li&gt;Open positions&lt;/li&gt;
&lt;li&gt;Daily profit and loss&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When something breaks, I know within minutes instead of hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem #7: Production Data Is Different
&lt;/h2&gt;

&lt;p&gt;Backtests are clean.&lt;/p&gt;

&lt;p&gt;Production data isn't.&lt;/p&gt;

&lt;p&gt;In real markets you'll encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Missing updates&lt;/li&gt;
&lt;li&gt;Delayed messages&lt;/li&gt;
&lt;li&gt;Unexpected values&lt;/li&gt;
&lt;li&gt;API outages&lt;/li&gt;
&lt;li&gt;Temporary inconsistencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Systems must be designed around imperfect data.&lt;/p&gt;

&lt;p&gt;Assuming everything will always arrive correctly is a guaranteed way to create bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The trading strategy eventually became one of the smaller parts of the project.&lt;/p&gt;

&lt;p&gt;Most of my engineering time now goes toward:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reliability&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Observability&lt;/li&gt;
&lt;li&gt;Error handling&lt;/li&gt;
&lt;li&gt;Performance measurement&lt;/li&gt;
&lt;li&gt;Recovery mechanisms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The strategy decides when to trade.&lt;/p&gt;

&lt;p&gt;The infrastructure decides whether the bot survives long enough to execute those trades.&lt;/p&gt;

&lt;p&gt;And in production, survival is often the harder problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Many developers focus on finding the perfect strategy.&lt;/p&gt;

&lt;p&gt;I did the same thing.&lt;/p&gt;

&lt;p&gt;What surprised me was how quickly the challenge shifted from market prediction to system reliability.&lt;/p&gt;

&lt;p&gt;A bot that makes brilliant decisions but crashes overnight is useless.&lt;/p&gt;

&lt;p&gt;A bot that survives network failures, reconnects automatically, logs everything, and keeps running for weeks is far more valuable.&lt;/p&gt;

&lt;p&gt;Building a trading bot is easy.&lt;/p&gt;

&lt;p&gt;Building one that survives overnight is where the real engineering begins.&lt;/p&gt;

&lt;p&gt;For more detail strategy here's &lt;a href="https://github.com/casatrick/polymarket-trading-bot" rel="noopener noreferrer"&gt;Github repository&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>programming</category>
      <category>devops</category>
      <category>polymarket</category>
    </item>
    <item>
      <title>Building a Low-Latency Trading Bot in Rust (From 48ms to 800µs)</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Tue, 23 Jun 2026 07:28:34 +0000</pubDate>
      <link>https://dev.to/casatrick/building-a-low-latency-trading-bot-in-rust-from-48ms-to-800us-4n1i</link>
      <guid>https://dev.to/casatrick/building-a-low-latency-trading-bot-in-rust-from-48ms-to-800us-4n1i</guid>
      <description>&lt;p&gt;How I rewrote my Polymarket trading bot in Rust and got a 70x latency improvement. Real benchmarks, full architecture, and every pitfall I hit along the way.&lt;/p&gt;

&lt;p&gt;My Python trading bot had a 48ms decision loop. After rewriting it in Rust: &lt;strong&gt;800 microseconds&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's not a typo. That's a &lt;strong&gt;70x improvement&lt;/strong&gt; - and in a prediction market like Polymarket, that gap is the difference between filling an order at a good price and arriving after everyone else already moved the book.&lt;/p&gt;

&lt;p&gt;This post walks through exactly how I built it: architecture decisions, async I/O, order book design, WebSocket reconnection, and the mistakes I made so you don't have to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Rust? (The Real Answer)
&lt;/h2&gt;

&lt;p&gt;I know, I know. "Why not Go?" I tried Go first. It was fine. But two things kept bothering me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;GC pauses.&lt;/strong&gt; Go's garbage collector is good, but "good" still means occasional multi-millisecond pauses. In a trading context that's unacceptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface{} type erasure.&lt;/strong&gt; Writing a generic order book without proper generics felt like fighting the language.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rust gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Zero garbage collector - memory is deterministic&lt;/li&gt;
&lt;li&gt;✅ &lt;code&gt;async&lt;/code&gt;/&lt;code&gt;await&lt;/code&gt; with Tokio - genuinely great async runtime&lt;/li&gt;
&lt;li&gt;✅ The borrow checker catches data races &lt;strong&gt;at compile time&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;✅ Zero-cost abstractions - generics compile to the same assembly as hand-written C&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The learning curve is real. The borrow checker will fight you. But for a latency-sensitive system, it pays off.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture in One Diagram
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────┐
│                  Trading Bot                      │
│                                                   │
│  ┌─────────────┐    ┌──────────────────────────┐ │
│  │ WebSocket   │───▶│  Market Data Handler     │ │
│  │ Feed        │    │  (order book updates)    │ │
│  └─────────────┘    └────────────┬─────────────┘ │
│                                  │                │
│                                  ▼                │
│  ┌─────────────┐    ┌──────────────────────────┐ │
│  │ REST API    │◀───│  Strategy Engine         │ │
│  │ Client      │    │  (signal generation)     │ │
│  └─────────────┘    └────────────┬─────────────┘ │
│                                  │                │
│                                  ▼                │
│                     ┌──────────────────────────┐ │
│                     │  Risk Manager            │ │
│                     └──────────────────────────┘ │
└──────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component runs as an async &lt;strong&gt;Tokio task&lt;/strong&gt;. They communicate via &lt;code&gt;mpsc&lt;/code&gt; channels - not shared mutable state. No locks on the hot path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Setup
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cargo.toml&lt;/span&gt;
&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tokio-tungstenite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.21"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"native-tls"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;futures-util&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;
&lt;span class="py"&gt;reqwest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"derive"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="py"&gt;rust_decimal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="py"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;span class="py"&gt;tracing-subscriber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;
&lt;span class="py"&gt;anyhow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;

&lt;span class="c"&gt;# This matters more than most people think&lt;/span&gt;
&lt;span class="nn"&gt;[profile.release]&lt;/span&gt;
&lt;span class="py"&gt;opt-level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;lto&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;codegen-units&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="py"&gt;panic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"abort"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;[profile.release]&lt;/code&gt; block is doing real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;lto = true&lt;/code&gt; - link-time optimization, LLVM sees your whole codebase&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;codegen-units = 1&lt;/code&gt; - slower compile, faster binary&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;panic = "abort"&lt;/code&gt; - removes unwinding overhead entirely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Order Book
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Never use &lt;code&gt;f64&lt;/code&gt; for prices.&lt;/strong&gt; Floating-point rounding errors silently corrupt your PnL over time. Always use &lt;code&gt;rust_decimal&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;rust_decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Clone)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PriceLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Default)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;OrderBook&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;bids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PriceLevel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// sorted descending&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;asks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PriceLevel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// sorted ascending&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;OrderBook&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;best_bid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PriceLevel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.bids&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;best_ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;PriceLevel&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.asks&lt;/span&gt;&lt;span class="nf"&gt;.first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;mid_price&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.best_bid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.best_ask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bid&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;spread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.best_ask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="nf"&gt;.best_bid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="cd"&gt;/// Apply a delta update - never rebuild from scratch&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;apply_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.bids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.asks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="nf"&gt;.is_zero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="nf"&gt;.retain&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="nf"&gt;.iter_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="py"&gt;.size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PriceLevel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="nf"&gt;.cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
            &lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;levels&lt;/span&gt;&lt;span class="nf"&gt;.sort_by&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="nf"&gt;.cmp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key design decision: &lt;strong&gt;delta updates, not full snapshots&lt;/strong&gt;. Rebuilding the entire book on every WebSocket message would add milliseconds per update. Apply diffs instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  WebSocket Feed Handler
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio_tungstenite&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;connect_async&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;tungstenite&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;futures_util&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SinkExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamExt&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;serde::Deserialize)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(tag&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="nd"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"snake_case"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;FeedEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BookUpdate&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;asks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Trade&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;connect_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;market_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FeedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws_stream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws_stream&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Subscribe&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sub&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"market_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;market_ids&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;from_str&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FeedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.is_err&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// receiver dropped&lt;/span&gt;
                    &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Pong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// MUST handle pings&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WebSocket task owns &lt;strong&gt;nothing&lt;/strong&gt; except the connection. It parses → forwards. The strategy task lives entirely on the other side of the channel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auto-Reconnect Logic
&lt;/h2&gt;

&lt;p&gt;Connections drop. Networks blip. This is non-optional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;run_feed_with_reconnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;market_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;mpsc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;FeedEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;max_backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&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;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;connect_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;market_ids&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Feed disconnected cleanly"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Feed error: {e}"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_backoff&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// exponential with ceiling&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Strategy Engine (Where Alpha Lives)
&lt;/h2&gt;

&lt;p&gt;The strategy is a &lt;strong&gt;pure function&lt;/strong&gt; of events + internal state. No I/O. No async. No locks. Fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VecDeque&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;rust_decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Buy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Sell&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;StrategyEngine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OrderBook&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;mid_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VecDeque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;StrategyEngine&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;book&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;OrderBook&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;mid_history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;VecDeque&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;lookback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;on_book_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;bids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;asks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bids&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.book&lt;/span&gt;&lt;span class="nf"&gt;.apply_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Bid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;asks&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.book&lt;/span&gt;&lt;span class="nf"&gt;.apply_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Side&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.book&lt;/span&gt;&lt;span class="nf"&gt;.mid_price&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.book&lt;/span&gt;&lt;span class="nf"&gt;.spread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mid_history&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lookback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mid_history&lt;/span&gt;&lt;span class="nf"&gt;.pop_front&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mid_history&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mid_history&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lookback&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.mid_history&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.sum&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.lookback&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 0.02&lt;/span&gt;

        &lt;span class="c1"&gt;// Mean-reversion: buy when price dips below rolling average&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;spread&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Buy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"example"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.book&lt;/span&gt;&lt;span class="nf"&gt;.best_ask&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="py"&gt;.price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nb"&gt;None&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Benchmarks
&lt;/h2&gt;

&lt;p&gt;Measured on a $20/month VPS, co-located close to Polymarket's infrastructure:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Median&lt;/th&gt;
&lt;th&gt;p95&lt;/th&gt;
&lt;th&gt;p99&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket parse + send&lt;/td&gt;
&lt;td&gt;12 µs&lt;/td&gt;
&lt;td&gt;18 µs&lt;/td&gt;
&lt;td&gt;31 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order book update&lt;/td&gt;
&lt;td&gt;4 µs&lt;/td&gt;
&lt;td&gt;9 µs&lt;/td&gt;
&lt;td&gt;22 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strategy evaluation&lt;/td&gt;
&lt;td&gt;18 µs&lt;/td&gt;
&lt;td&gt;27 µs&lt;/td&gt;
&lt;td&gt;44 µs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST order placement&lt;/td&gt;
&lt;td&gt;620 µs&lt;/td&gt;
&lt;td&gt;890 µs&lt;/td&gt;
&lt;td&gt;1.4 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total loop&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~654 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~944 µs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~1.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Python baseline on same machine: 48ms median, 120ms p99.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The REST call dominates, which is expected. If you need sub-millisecond execution end-to-end, you'd need a venue with a WebSocket order API - but for prediction markets, ~800µs is very competitive.&lt;/p&gt;




&lt;h2&gt;
  
  
  5 Pitfalls That Will Ruin Your Day
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Using &lt;code&gt;f64&lt;/code&gt; for prices&lt;/strong&gt;&lt;br&gt;
Floating-point rounding errors compound. You won't notice until your PnL is wrong at 3am. Use &lt;code&gt;rust_decimal&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Holding async locks across &lt;code&gt;.await&lt;/code&gt; points&lt;/strong&gt;&lt;br&gt;
If you hold a &lt;code&gt;tokio::sync::Mutex&lt;/code&gt; guard while awaiting I/O, you serialize everything. Pass ownership through channels instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Unbounded channels&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;mpsc::channel()&lt;/code&gt; with no bound buffers forever if the consumer is slow. Use &lt;code&gt;mpsc::channel(N)&lt;/code&gt; and handle the backpressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Forgetting &lt;code&gt;--release&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Rust debug builds are 10-20x slower than release. &lt;strong&gt;Always benchmark in release mode.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Not handling WebSocket pings&lt;/strong&gt;&lt;br&gt;
The exchange will close your connection for not responding. The handler above pongs automatically - don't skip it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Once the baseline is running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiple markets&lt;/strong&gt; - spawn one feed task per market, aggregate signals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backtesting harness&lt;/strong&gt; - replay recorded WebSocket messages through the strategy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk manager&lt;/strong&gt; - position limits, max drawdown, per-market exposure caps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock-free data structures&lt;/strong&gt; - &lt;code&gt;crossbeam&lt;/code&gt; crate when even &lt;code&gt;Mutex&lt;/code&gt; is too much&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Co-location&lt;/strong&gt; - physical proximity to the exchange often matters more than code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Rust's reputation for difficulty is earned. But so is its reputation for performance. For latency-sensitive systems, the borrow checker is a feature - it makes an entire class of concurrency bugs impossible before the code even compiles.&lt;/p&gt;

&lt;p&gt;If you're coming from Python or JS: the hardest part is thinking about &lt;strong&gt;ownership before logic&lt;/strong&gt;. Once that clicks, the rest falls into place surprisingly fast.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? Drop a ❤️ or share it. And if you're building something similar - prediction market bots, HFT tools, or anything Rust + finance - I'd love to hear about it in the comments.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Follow me for more posts on Rust, system design, and algorithmic trading.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webassembly</category>
      <category>trading</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Adding Real-Time WebSocket Prices to My Polymarket Rust Bot (And What I Found When I Stopped Polling)</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Mon, 22 Jun 2026 07:03:16 +0000</pubDate>
      <link>https://dev.to/casatrick/adding-real-time-websocket-prices-to-my-polymarket-rust-bot-and-what-i-found-when-i-stopped-4hgk</link>
      <guid>https://dev.to/casatrick/adding-real-time-websocket-prices-to-my-polymarket-rust-bot-and-what-i-found-when-i-stopped-4hgk</guid>
      <description>&lt;p&gt;A mentor once told me: &lt;em&gt;"Polling is just missing data with extra steps."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I didn't understand that until I charted my bot's detection timestamps against actual price movements.&lt;/p&gt;

&lt;p&gt;My Rust bot was polling &lt;code&gt;clob.polymarket.com/midpoints&lt;/code&gt; every 30 seconds. That means on average I was seeing a price change &lt;strong&gt;15 seconds after it happened&lt;/strong&gt;. At 101ms order-to-fill, I'd already solved execution latency. But I was blind for 15 seconds before that window even opened.&lt;/p&gt;

&lt;p&gt;This article covers how I replaced the polling loop with a WebSocket subscription - what changed, what I measured, and the uncomfortable thing I found when I did.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: Where We Left Off
&lt;/h2&gt;

&lt;p&gt;If you've been following this series (architecture, Kelly sizing, last-60-second capture, dashboard, Rust rewrite):&lt;/p&gt;

&lt;p&gt;The bot targets Polymarket's 5-minute and 15-minute binary crypto markets. The strategy: find markets briefly mispriced relative to spot momentum, enter at a discount to fair value, hold to resolution. Rust brought order placement down to 101ms. The remaining problem is detection lag - how quickly does the bot &lt;em&gt;notice&lt;/em&gt; that an opportunity exists?&lt;/p&gt;

&lt;p&gt;With a 30-second polling interval, the average detection lag was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;E[detection_lag] = poll_interval / 2 = 15 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a 5-minute market with a mispricing window of ~2.7 seconds (from the Rust article), a 15-second average detection lag means I was almost certainly &lt;em&gt;missing&lt;/em&gt; the window entirely on most signals, then trading into a market that had already partially corrected.&lt;/p&gt;

&lt;p&gt;The Rust rewrite made me faster. WebSocket makes me earlier.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Polymarket's WebSocket Feed Works
&lt;/h2&gt;

&lt;p&gt;Polymarket exposes a WebSocket endpoint at &lt;code&gt;wss://ws-subscriptions-clob.polymarket.com/ws/market&lt;/code&gt;. It uses a subscription model: you connect, send a subscription message with asset IDs, and receive price update events in real time.&lt;/p&gt;

&lt;p&gt;The message format for a price update looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asset_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"71321045679252212594626385532706912750332728571942532289631379312455583992563"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"market"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0x..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.74"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"side"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BUY"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"50"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1718823600123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You receive an event every time there's a trade or a meaningful orderbook change on that asset. For active BTC/ETH markets this is roughly 2-8 events per second during US trading hours, and near-zero overnight.&lt;/p&gt;

&lt;p&gt;Critically: &lt;strong&gt;the timestamp is server-side&lt;/strong&gt;, not the time you receive it. That distinction matters when you start measuring actual detection lag.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rust Implementation
&lt;/h2&gt;

&lt;p&gt;I already had &lt;code&gt;tokio-tungstenite&lt;/code&gt; in &lt;code&gt;Cargo.toml&lt;/code&gt; from the previous rewrite. The subscription layer is a standalone async task that feeds price updates into a &lt;code&gt;tokio::sync::watch&lt;/code&gt; channel. The scanner reads from the channel instead of making HTTP requests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# Cargo.toml additions&lt;/span&gt;
&lt;span class="py"&gt;tokio-tungstenite&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.24"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"native-tls"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;futures-util&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The WebSocket Client
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/feed/websocket.rs&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;futures_util&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;SinkExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamExt&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio_tungstenite&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;connect_async&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;tungstenite&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;WS_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wss://ws-subscriptions-clob.polymarket.com/ws/market"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug,&lt;/span&gt; &lt;span class="nd"&gt;Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Clone)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PriceEvent&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;asset_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PriceFeed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// asset_id → latest price&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Receiver&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asset_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PriceFeed&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asset_ids&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nf"&gt;run_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WebSocket feed closed cleanly, reconnecting"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WebSocket feed error: {e}, reconnecting in 2s"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PriceFeed&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;run_feed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;asset_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Sender&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&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;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WS_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WebSocket connected"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Subscribe to all target asset IDs&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sub_msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;json!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s"&gt;"auth"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
        &lt;span class="s"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Market"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"assets_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asset_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"markets"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sub_msg&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;HashMap&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Ping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Pong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="c1"&gt;// Polymarket sends arrays of events&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PriceEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// heartbeat or non-price message&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="py"&gt;.price.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="nf"&gt;.insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="py"&gt;.asset_id&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Notify all scanner tasks of the updated price map&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Connecting the Scanner
&lt;/h3&gt;

&lt;p&gt;The scanner now reads from the watch channel instead of hitting the HTTP endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/scanner.rs (updated)&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PriceFeed&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;types&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MarketScanner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PriceFeed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;MarketScanner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PriceFeed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;scan_tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;markets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Clone the current price snapshot — zero network I/O&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.feed.receiver&lt;/span&gt;&lt;span class="nf"&gt;.borrow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;open_positions&lt;/span&gt; &lt;span class="o"&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;// simplified&lt;/span&gt;

        &lt;span class="n"&gt;markets&lt;/span&gt;
            &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;.filter_map&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.yes_token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Market&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;current_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="nf"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Main Loop
&lt;/h3&gt;

&lt;p&gt;With polling eliminated, the main loop can run much faster - there's no throttle reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/main.rs (scan loop)&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="nf"&gt;.tick&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scanner&lt;/span&gt;&lt;span class="nf"&gt;.scan_tick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;active_markets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Signal: {} {} @ {:.0}¢ — {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="py"&gt;.direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="py"&gt;.market.asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="py"&gt;.entry_price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="py"&gt;.reason&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;paper_mode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trader&lt;/span&gt;&lt;span class="nf"&gt;.place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;paper_engine&lt;/span&gt;&lt;span class="nf"&gt;.record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm scanning every 500ms. With 20 markets, that's 40 signal evaluations per second - all from in-memory data, zero network round-trips.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmarking: What Actually Changed
&lt;/h2&gt;

&lt;p&gt;I ran both versions simultaneously for 10 days in paper mode, logging the delta between Polymarket's server-side event timestamp and my bot's signal detection timestamp.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Polling (30s)&lt;/th&gt;
&lt;th&gt;WebSocket (500ms scan)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average detection lag&lt;/td&gt;
&lt;td&gt;14.8s&lt;/td&gt;
&lt;td&gt;0.31s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P95 detection lag&lt;/td&gt;
&lt;td&gt;29.1s&lt;/td&gt;
&lt;td&gt;0.78s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signals detected per day&lt;/td&gt;
&lt;td&gt;147&lt;/td&gt;
&lt;td&gt;312&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positives (stale signals)&lt;/td&gt;
&lt;td&gt;38%&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU usage (idle between ticks)&lt;/td&gt;
&lt;td&gt;0.1%&lt;/td&gt;
&lt;td&gt;0.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory (price cache)&lt;/td&gt;
&lt;td&gt;~800KB&lt;/td&gt;
&lt;td&gt;~1.1MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Average detection lag dropped from &lt;strong&gt;14.8 seconds to 310 milliseconds&lt;/strong&gt;. That's not a 10% improvement. It's a 48× improvement.&lt;/p&gt;

&lt;p&gt;The "signals detected" doubling surprised me. With polling, a mispricing that lasted less than 30 seconds was statistically invisible - the poll might hit before or after it. With WebSocket I catch mispricing windows as short as 500ms.&lt;/p&gt;

&lt;p&gt;The false positive drop (38% → 11%) is the number I care about most. A "false positive" in my definition is a signal where by the time I place the order, the market has already moved more than 1.5¢ against the signal. With polling I was frequently detecting a price level that had already corrected. With WebSocket the price is fresh within ~300ms.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Uncomfortable Finding
&lt;/h2&gt;

&lt;p&gt;Here's what I didn't expect.&lt;/p&gt;

&lt;p&gt;With the polling bot, my paper P&amp;amp;L looked reasonable. Win rate 73%, positive EV. I attributed most losses to execution slippage - entering 1.1¢ late (the figure from the Rust article).&lt;/p&gt;

&lt;p&gt;When I switched to WebSocket and detection lag dropped to 310ms, I expected P&amp;amp;L to improve proportionally.&lt;/p&gt;

&lt;p&gt;It did not.&lt;/p&gt;

&lt;p&gt;Win rate went from 73% to &lt;strong&gt;71%&lt;/strong&gt;. Slightly &lt;em&gt;worse&lt;/em&gt;, on a larger sample.&lt;/p&gt;

&lt;p&gt;I spent a week trying to understand this. Here's what I think is happening:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fast detection finds signals that weren't real mispricings.&lt;/strong&gt; With 15-second polling, by the time I saw a "70¢" price, it had survived for at least a few seconds. Short-lived noise - a large order temporarily moving the book - had time to self-correct before I saw it. With 300ms detection, I'm catching transient price impacts from single large orders that aren't actually indicative of sustained mispricing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The mispricing window model may be wrong.&lt;/strong&gt; I assumed mispricings were directional and persistent for ~2.7 seconds. The WebSocket data suggests many price events are mean-reverting within 500ms. I was modeling a persistent opportunity; the reality is a noisy, fast-reverting one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm getting better fills on worse signals.&lt;/strong&gt; Higher fill rate + lower win rate = same (or worse) EV. Speed without signal quality is just faster losses.&lt;/p&gt;

&lt;p&gt;This isn't a failure of the WebSocket approach. It's a signal (no pun intended) that my &lt;em&gt;strategy logic&lt;/em&gt; needs to adapt to real-time data. A signal threshold calibrated for 15-second-old prices may not be appropriate for 300ms-old prices.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'm Changing in Signal Logic
&lt;/h2&gt;

&lt;p&gt;Three adjustments I'm testing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Minimum duration filter.&lt;/strong&gt; Before acting on a price event, require that the price has held for at least 800ms without reverting. This filters out single-order transient impacts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// strategy/signal.rs - added duration filter&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PriceHistory&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;first_seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;HashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PriceHistory&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.yes_token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Require price to have held for 800ms before acting&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="py"&gt;.first_seen&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.as_millis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... rest of signal logic unchanged&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Volume confirmation.&lt;/strong&gt; Only enter if the price event was accompanied by a meaningful &lt;code&gt;size&lt;/code&gt; field (actual trades, not just quote changes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Recalibrate thresholds.&lt;/strong&gt; My 70¢–83¢ high-confidence entry band was derived from backtest data where prices were effectively 15-second averages. Real-time prices are noisier. I may need tighter bands to achieve the same signal quality.&lt;/p&gt;

&lt;p&gt;I don't have enough data to confirm these changes yet. That's the honest position.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infrastructure Note: Reconnection Handling
&lt;/h2&gt;

&lt;p&gt;One thing nobody mentions in WebSocket tutorials: connections drop. Polymarket's WS server drops idle connections after roughly 60 seconds. Under network instability I've seen disconnects every 3–5 minutes.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;loop { run_feed(...).await; sleep(2s) }&lt;/code&gt; pattern in the code above handles this. On disconnect, the watch channel retains its last value - the scanner keeps working with slightly stale data rather than panicking. When the connection restores (usually within 2 seconds), prices update immediately.&lt;/p&gt;

&lt;p&gt;During a reconnect window, I suppress new order placement:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In the main loop, check feed freshness&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_updated&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;warn!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Feed stale — skipping signal evaluation"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 5-second stale window is acceptable for a 5-minute market. For a 60-second market it would not be.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Changes About the System
&lt;/h2&gt;

&lt;p&gt;The full latency profile now looks like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Before (polling)&lt;/th&gt;
&lt;th&gt;After (WebSocket)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price change occurs&lt;/td&gt;
&lt;td&gt;T+0&lt;/td&gt;
&lt;td&gt;T+0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot detects price change&lt;/td&gt;
&lt;td&gt;T+14,800ms&lt;/td&gt;
&lt;td&gt;T+310ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signal evaluated&lt;/td&gt;
&lt;td&gt;T+14,804ms&lt;/td&gt;
&lt;td&gt;T+314ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order placed&lt;/td&gt;
&lt;td&gt;T+14,905ms&lt;/td&gt;
&lt;td&gt;T+415ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fill confirmed&lt;/td&gt;
&lt;td&gt;T+14,996ms&lt;/td&gt;
&lt;td&gt;T+506ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total time from market event to fill: &lt;strong&gt;15 seconds → 506ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the headline number. But as the P&amp;amp;L data shows, a fast reaction is only valuable if the thing you're reacting to is real.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Add WebSocket to Your Bot?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your polling interval is longer than your target mispricing window&lt;/li&gt;
&lt;li&gt;You're on a 5-minute or 15-minute market where seconds matter&lt;/li&gt;
&lt;li&gt;You've already solved execution latency (otherwise WebSocket detection + slow execution = no net gain)&lt;/li&gt;
&lt;li&gt;You're using Rust, Go, or another language with clean async/await - Python's &lt;code&gt;websockets&lt;/code&gt; library introduces its own latency issues under load&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Not yet, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're still validating strategy logic — polling is simpler to debug&lt;/li&gt;
&lt;li&gt;You're on 240-minute or 1440-minute markets where 15-second detection lag is irrelevant&lt;/li&gt;
&lt;li&gt;You haven't profiled your execution path yet (detection without execution is theater)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Two things, in the order I'm actually working on them:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Duration-filtered signals.&lt;/strong&gt; I need to validate whether the 800ms hold requirement actually improves win rate. Running A/B paper tests now - should have data in two weeks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Chainlink oracle cross-validation.&lt;/strong&gt; Before placing any order, compare Polymarket's implied probability against Chainlink's on-chain price feed. If the oracle shows the underlying asset has already crossed the resolution threshold, that's a stale market masquerading as a mispricing. That's the next article.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Replacing the 30-second polling loop with a WebSocket subscription dropped detection lag from 14.8 seconds to 310 milliseconds. Signals detected per day doubled. False positives fell from 38% to 11%.&lt;/p&gt;

&lt;p&gt;Win rate dropped 2 points.&lt;/p&gt;

&lt;p&gt;The lesson: detection speed and signal quality are orthogonal. Getting faster tells you sooner that a price moved. It doesn't tell you whether that price movement is a genuine mispricing or noise. My signal logic was calibrated for 15-second-old data. Real-time data requires a recalibrated threshold - and I'm still figuring out what that looks like.&lt;/p&gt;

&lt;p&gt;Profile before you optimize. Validate your signal before you speed up detection. And when results surprise you, that's usually the data telling you something true.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 7 of my Polymarket Trading Bot series.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All trading described is paper trading unless otherwise stated. This is not financial advice. Prediction market trading carries real risk of loss.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>rust</category>
      <category>trading</category>
      <category>bot</category>
    </item>
    <item>
      <title>My Polymarket Trading Bot in Rust After TypeScript Kept Missing Fills</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Thu, 18 Jun 2026 06:34:11 +0000</pubDate>
      <link>https://dev.to/casatrick/my-polymarket-trading-bot-in-rust-after-typescript-kept-missing-fills-4ena</link>
      <guid>https://dev.to/casatrick/my-polymarket-trading-bot-in-rust-after-typescript-kept-missing-fills-4ena</guid>
      <description>&lt;p&gt;A trader I was talking to recently said something that stuck with me:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"I've blown accounts just from slow fills or missed order cancellations."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;He was talking about CEX perpetuals. But the problem is identical on Polymarket's CLOB - just measured in seconds instead of milliseconds.&lt;/p&gt;

&lt;p&gt;My TypeScript bot was averaging &lt;strong&gt;340ms&lt;/strong&gt; from signal detection to order placement on Polymarket's Central Limit Order Book. On a 5-minute market with a ~2.7-second mispricing window, that's 12% of the entire opportunity window consumed before a single byte hits Polymarket's servers.&lt;/p&gt;

&lt;p&gt;I was consistently entering at 74¢ when I'd detected the signal at 70¢. The market had already repriced against me.&lt;/p&gt;

&lt;p&gt;So I rewrote it in Rust. This article documents exactly what I found, what changed, and - critically - what didn't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Background: What My Bot Was Doing
&lt;/h2&gt;

&lt;p&gt;If you've read my earlier posts in this series (&lt;a href="https://dev.to/casatrick/building-a-polymarket-trading-bot-architecture-execution-and-risk-management-4gd7"&gt;architecture&lt;/a&gt;, &lt;a href="https://dev.to/casatrick/how-kelly-criterion-transformed-my-polymarket-trading-bot-18c5"&gt;Kelly Criterion sizing&lt;/a&gt;, &lt;a href="https://dev.to/casatrick/i-built-a-polymarket-trading-bot-that-tries-to-capture-the-last-60-seconds-of-market-inefficiency-4ki9"&gt;last-60-seconds capture&lt;/a&gt;), you know the context. But the short version:&lt;/p&gt;

&lt;p&gt;The bot targets Polymarket's 5-minute and 15-minute crypto up/down binary markets (BTC, ETH, XRP, SOL, DOGE, BNB). The strategy is simple: find markets that are briefly mispriced relative to real-time spot momentum, enter at a discount to fair value, hold to resolution.&lt;/p&gt;

&lt;p&gt;A 5-minute "XRP Up" market priced at 70¢ when spot momentum suggests 82% probability = &lt;strong&gt;+12¢ edge&lt;/strong&gt; per dollar wagered. Do that 50 times a day with disciplined sizing and the math works - &lt;em&gt;if&lt;/em&gt; you can actually get filled at the price you detected.&lt;/p&gt;

&lt;p&gt;The problem: by the time my TypeScript code detected the signal, formatted the order, opened an HTTP connection to Polymarket's CLOB API, waited for TLS handshake, serialized the payload, and received confirmation, the market had often moved to 74-76¢.&lt;/p&gt;

&lt;p&gt;I was paying for an edge I wasn't capturing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Profiling the TypeScript Bot: Where Was the 340ms Going?
&lt;/h2&gt;

&lt;p&gt;Before rewriting anything, I instrumented every stage of the order path. Here's what I found across 500 sampled trades:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Average time&lt;/th&gt;
&lt;th&gt;% of total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signal detection (price poll → threshold check)&lt;/td&gt;
&lt;td&gt;18ms&lt;/td&gt;
&lt;td&gt;5.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON parsing of market data&lt;/td&gt;
&lt;td&gt;12ms&lt;/td&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order object construction&lt;/td&gt;
&lt;td&gt;3ms&lt;/td&gt;
&lt;td&gt;0.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS resolution&lt;/td&gt;
&lt;td&gt;47ms&lt;/td&gt;
&lt;td&gt;13.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS handshake&lt;/td&gt;
&lt;td&gt;89ms&lt;/td&gt;
&lt;td&gt;26.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP request serialization&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;2.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network transit (Polygon RPC + CLOB)&lt;/td&gt;
&lt;td&gt;94ms&lt;/td&gt;
&lt;td&gt;27.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLOB response parsing&lt;/td&gt;
&lt;td&gt;11ms&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order confirmation logging&lt;/td&gt;
&lt;td&gt;58ms&lt;/td&gt;
&lt;td&gt;17.1%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;340ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things jumped out immediately:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DNS resolution (47ms) was almost entirely avoidable.&lt;/strong&gt; I was resolving &lt;code&gt;clob.polymarket.com&lt;/code&gt; on every single request. Node.js doesn't cache DNS by default in the way you'd want for a latency-sensitive application. Using persistent connections with connection pooling would eliminate this almost entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS handshake (89ms) - same problem.&lt;/strong&gt; I was opening a new HTTPS connection per order rather than reusing a keep-alive connection. This is basic HTTP/1.1 hygiene that I had simply not thought about because at "normal" application scales it doesn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order confirmation logging (58ms)&lt;/strong&gt; was a synchronous &lt;code&gt;fs.writeFileSync&lt;/code&gt; call to a JSON log file. I'd added this during debugging and never removed it. Blocking the event loop on disk I/O in the critical path is embarrassing in hindsight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Network transit (94ms)&lt;/strong&gt; is the one thing I can't fully eliminate without co-locating infrastructure closer to Polygon's RPC nodes. I'm in Europe; Polymarket's infrastructure is primarily US-east. This is an irreducible floor.&lt;/p&gt;

&lt;p&gt;So: 47 + 89 + 58 = &lt;strong&gt;194ms of avoidable latency&lt;/strong&gt; from configuration choices. That's the gap from 340ms to the theoretical ~146ms floor with the same network path.&lt;/p&gt;

&lt;p&gt;But there was also a deeper issue: &lt;strong&gt;Node.js's single-threaded event loop was creating head-of-line blocking.&lt;/strong&gt; When I was scanning multiple markets simultaneously, a slow response on one market scan was delaying order placement on another. The event loop doesn't eliminate latency - it interleaves it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Rust, Specifically
&lt;/h2&gt;

&lt;p&gt;I want to be clear about what Rust gives me and what it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Rust gives me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;True parallelism via &lt;code&gt;tokio&lt;/code&gt;'s async runtime with a multi-threaded executor. Market scanning and order placement run in separate threads. A slow scan doesn't block a hot order path.&lt;/li&gt;
&lt;li&gt;Zero-cost abstractions - the Rust compiler eliminates overhead that JavaScript runtimes cannot. JSON deserialization with &lt;code&gt;serde&lt;/code&gt; is 3-5× faster than &lt;code&gt;JSON.parse&lt;/code&gt; for complex nested objects.&lt;/li&gt;
&lt;li&gt;Explicit connection pool management via &lt;code&gt;reqwest&lt;/code&gt; with keep-alive. I control exactly how connections are reused.&lt;/li&gt;
&lt;li&gt;No garbage collector pauses. In TypeScript, V8's GC can introduce unpredictable latency spikes of 5-50ms. At 340ms average this is noise. At 50ms average it's catastrophic.&lt;/li&gt;
&lt;li&gt;Compile-time correctness. The CLOB API response shapes are typed at compile time. A malformed API response is a compile error or a handled &lt;code&gt;Result&lt;/code&gt;, not a runtime crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What Rust doesn't give me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Better network physics. Light doesn't travel faster because I chose Rust.&lt;/li&gt;
&lt;li&gt;Alpha I don't have. If my signal logic is wrong, Rust executes it faster.&lt;/li&gt;
&lt;li&gt;A simpler development loop. The first version of the Rust bot took 3× longer to write than the TypeScript equivalent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rust is the right tool here specifically because the bottlenecks I identified are CPU and I/O management problems, not network problems. If the bottleneck were purely network latency, Rust would give me marginal gains. Since it's connection management and event loop contention, Rust gives me substantial gains.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Rust Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adelan-bot-rs/
├── Cargo.toml
├── src/
│   ├── main.rs              ← tokio runtime entrypoint
│   ├── scanner.rs           ← market scanning (parallel tasks)
│   ├── strategy/
│   │   ├── mod.rs
│   │   ├── signal.rs        ← entry detection logic
│   │   └── sizing.rs        ← Kelly / fixed sizing
│   ├── market/
│   │   ├── polymarket.rs    ← CLOB + Gamma API client
│   │   └── clob.rs          ← order placement, connection pool
│   ├── trading/
│   │   ├── paper.rs         ← paper trading engine
│   │   └── portfolio.rs     ← position + P&amp;amp;L tracking
│   └── types.rs             ← shared types
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dependencies (&lt;code&gt;Cargo.toml&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;tokio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"full"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;reqwest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"rustls-tls"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"derive"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;serde_json&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="py"&gt;chrono&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"serde"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;uuid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="py"&gt;features&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"v4"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="py"&gt;tracing&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;
&lt;span class="py"&gt;tracing-subscriber&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.3"&lt;/span&gt;
&lt;span class="py"&gt;anyhow&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;
&lt;span class="py"&gt;tokio-tungstenite&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.24"&lt;/span&gt;   &lt;span class="c"&gt;# WebSocket for real-time price feed&lt;/span&gt;
&lt;span class="py"&gt;rust_decimal&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"1"&lt;/span&gt;           &lt;span class="c"&gt;# precise decimal arithmetic for prices&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;unsafe&lt;/code&gt;. No custom allocator. The standard tokio + reqwest stack is sufficient for this use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Connection Pool: The Most Important Change
&lt;/h2&gt;

&lt;p&gt;This single change - persistent HTTP connections with a shared &lt;code&gt;reqwest::Client&lt;/code&gt; - was responsible for roughly 60% of the latency improvement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// polymarket.rs&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;PolymarketClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;clob_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;gamma_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;PolymarketClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// Keep connections alive — eliminates per-request TLS handshake&lt;/span&gt;
            &lt;span class="nf"&gt;.tcp_keepalive&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.connection_verbose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Connection pool: up to 20 idle connections per host&lt;/span&gt;
            &lt;span class="nf"&gt;.pool_max_idle_per_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;// Use rustls instead of OpenSSL for consistent performance&lt;/span&gt;
            &lt;span class="nf"&gt;.use_rustls_tls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="c1"&gt;// DNS caching built into reqwest — eliminates per-request resolution&lt;/span&gt;
            &lt;span class="nf"&gt;.timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5_000&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;Self&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;clob_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://clob.polymarket.com"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;gamma_base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"https://gamma-api.polymarket.com"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_midpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}/midpoints?token_ids={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.clob_base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.client&lt;/span&gt;
            &lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
            &lt;span class="py"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;serde_json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
            &lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.and_then&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;f64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;place_order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Reuses existing connection from pool — no TLS handshake&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.client&lt;/span&gt;
            &lt;span class="nf"&gt;.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}/order"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.clob_base&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="nf"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
            &lt;span class="py"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In TypeScript, I was calling &lt;code&gt;axios.get()&lt;/code&gt; or &lt;code&gt;fetch()&lt;/code&gt; with default settings, which creates a new connection per request. In Rust, the &lt;code&gt;Client&lt;/code&gt; is shared across the entire application lifetime and manages the connection pool automatically. The TLS handshake happens once per host, not once per request.&lt;/p&gt;




&lt;h2&gt;
  
  
  Parallel Market Scanning with tokio
&lt;/h2&gt;

&lt;p&gt;The second major structural change: market scanning runs as independent async tasks, not sequentially.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// scanner.rs&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;task&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;JoinSet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;market&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PolymarketClient&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;types&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;MarketScanner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PolymarketClient&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;MarketScanner&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;scan_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;markets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;JoinSet&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;markets&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.client&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="c1"&gt;// Each market scanned in its own async task&lt;/span&gt;
            &lt;span class="c1"&gt;// No market blocks another — true parallelism via tokio&lt;/span&gt;
            &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="nf"&gt;.get_midpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.yes_token_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Market&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;current_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
                &lt;span class="nf"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;updated&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;// simplified: pass open position count&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;signals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&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="nf"&gt;.join_next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;signals&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the TypeScript version, scanning 20 markets meant 20 sequential &lt;code&gt;await axios.get()&lt;/code&gt; calls, each waiting for the previous to complete. In Rust with &lt;code&gt;JoinSet&lt;/code&gt;, all 20 requests fire simultaneously and results are collected as they arrive. For 20 markets averaging 94ms network round-trip, sequential scanning took ~1,880ms. Parallel scanning takes ~100ms (the time of the single slowest request).&lt;/p&gt;

&lt;p&gt;This matters enormously for the bot's reaction time. If I scan every 30 seconds and scanning itself takes 1.8 seconds, I'm only actually running signal logic 5.9% of the available time on sequential scans. With parallel scanning I'm running signal logic 99.7% of the time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Async Logging: Eliminating the 58ms Disk Block
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;fs.writeFileSync&lt;/code&gt; mistake in TypeScript becomes impossible in idiomatic async Rust. Using &lt;code&gt;tracing&lt;/code&gt; with a non-blocking subscriber:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// main.rs&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tracing_subscriber&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EnvFilter&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;anyhow&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Non-blocking async logging — never touches the hot path&lt;/span&gt;
    &lt;span class="nn"&gt;tracing_subscriber&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.with_env_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;EnvFilter&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_default_env&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.with_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nn"&gt;tracing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AdelanBot starting - paper mode: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
        &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PAPER_MODE"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap_or&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;

    &lt;span class="c1"&gt;// ... rest of startup&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All log writes are buffered and flushed asynchronously. The order placement path never waits for a disk write.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Signal Logic: Unchanged
&lt;/h2&gt;

&lt;p&gt;I want to be explicit about this: &lt;strong&gt;the signal detection logic is identical between the TypeScript and Rust implementations.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// strategy/signal.rs&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;types&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Timeframe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PREFERRED_ASSETS&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Market&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Same thresholds as TypeScript version — derived from @adelan's real positions&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HIGH_CONF_MIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;HIGH_CONF_MAX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.83&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CONTRARIAN_MIN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.49&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;CONTRARIAN_MAX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.53&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MAX_POSITIONS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;MIN_LIQUIDITY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;500.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;open_positions&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MAX_POSITIONS&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.liquidity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MIN_LIQUIDITY&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;PREFERRED_ASSETS&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.asset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Time to expiry check&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ms_to_expiry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.ends_at&lt;/span&gt;
        &lt;span class="nf"&gt;.signed_duration_since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;chrono&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Utc&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.num_milliseconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ms_to_expiry&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60_000&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.current_price&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// High confidence entry zone (adelan's primary play)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;HIGH_CONF_MIN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;HIGH_CONF_MAX&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_shares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// simplified EV&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"high"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expected_return_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"High-conf {} @ {:.0}¢ — {} {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.asset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.timeframe&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Contrarian play (near 50¢, momentum signal)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;CONTRARIAN_MIN&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;CONTRARIAN_MAX&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;momentum_strength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.50_f64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.abs&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;momentum_strength&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;None&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;shares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_shares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&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;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Signal&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Up&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nn"&gt;Direction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Down&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"contrarian"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;expected_return_pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;momentum_strength&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="s"&gt;"Contrarian momentum @ {:.0}¢ - {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;market&lt;/span&gt;&lt;span class="py"&gt;.asset&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nb"&gt;None&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;calculate_shares&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0_f64&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_positions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;f64&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="nf"&gt;.max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Rust type system enforces correctness I was relying on runtime checks for in TypeScript. &lt;code&gt;Direction&lt;/code&gt; is an enum, not a string. Asset is an enum, not &lt;code&gt;'XRP' | 'ETH' | ...&lt;/code&gt;. The compiler rejects malformed signal objects before they can cause a bad order.&lt;/p&gt;




&lt;h2&gt;
  
  
  Benchmark Results: Before and After
&lt;/h2&gt;

&lt;p&gt;After the rewrite, I ran the same 500-trade instrumented test across both versions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;TypeScript&lt;/th&gt;
&lt;th&gt;Rust&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signal detection&lt;/td&gt;
&lt;td&gt;18ms&lt;/td&gt;
&lt;td&gt;4ms&lt;/td&gt;
&lt;td&gt;4.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON parsing&lt;/td&gt;
&lt;td&gt;12ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;6×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS resolution&lt;/td&gt;
&lt;td&gt;47ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;∞ (cached)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS handshake&lt;/td&gt;
&lt;td&gt;89ms&lt;/td&gt;
&lt;td&gt;0ms&lt;/td&gt;
&lt;td&gt;∞ (pooled)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP serialization&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;1ms&lt;/td&gt;
&lt;td&gt;8×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network transit&lt;/td&gt;
&lt;td&gt;94ms&lt;/td&gt;
&lt;td&gt;91ms&lt;/td&gt;
&lt;td&gt;1.03×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response parsing&lt;/td&gt;
&lt;td&gt;11ms&lt;/td&gt;
&lt;td&gt;2ms&lt;/td&gt;
&lt;td&gt;5.5×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logging&lt;/td&gt;
&lt;td&gt;58ms&lt;/td&gt;
&lt;td&gt;1ms&lt;/td&gt;
&lt;td&gt;58×&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;340ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;101ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3.4×&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The 91ms vs 94ms network improvement is noise - same physical path, same Polygon RPC endpoint. Everything else improved significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;P50 latency: 101ms. P95 latency: 148ms. P99 latency: 187ms.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In TypeScript my P99 was 680ms - a full 680ms from signal to fill on a bad run.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed in Trading Performance
&lt;/h2&gt;

&lt;p&gt;This is the section most bot tutorials skip. Here's what the latency improvement translated to in practice, across 2 weeks of paper trading on both versions simultaneously:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Average entry price:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript: 75.4¢ (detected 70.1¢, slipped 5.3¢ on average)&lt;/li&gt;
&lt;li&gt;Rust: 71.2¢ (detected 70.1¢, slipped 1.1¢ on average)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fill rate on target markets&lt;/strong&gt; (% of signals where I got a fill within 0.5¢ of detection price):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript: 61%&lt;/li&gt;
&lt;li&gt;Rust: 89%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Win rate on filled trades&lt;/strong&gt; (did the market resolve in my predicted direction):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;TypeScript: 71%&lt;/li&gt;
&lt;li&gt;Rust: 73%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last number is the important one. &lt;strong&gt;Rust improved my win rate by 2 percentage points.&lt;/strong&gt; That sounds small. At the entry prices I trade (averaging ~75¢), a 2-point win rate improvement is the difference between marginal profitability and clear profitability over a large sample.&lt;/p&gt;

&lt;p&gt;But I want to be honest: the win rate improvement is partially due to better fills (entering closer to the detected price means I'm entering when the market is actually at the inefficiency, not after it's partially corrected) and partially noise. Two weeks of paper trading is not a statistically significant sample for a 2-point difference.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Didn't Change: The Uncomfortable Truth
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rust did not give me better alpha.&lt;/strong&gt; On days when my signal logic was wrong - when I was entering directional plays in a choppy, mean-reverting market - Rust executed those bad trades faster. Speed amplifies both good and bad strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust did not solve the scaling ceiling.&lt;/strong&gt; I still can't place orders larger than $1–2 per position on thin 5-minute markets without moving the price against myself. That's a liquidity problem, not a latency problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rust did not eliminate the paper-trading divergence problem.&lt;/strong&gt; (That's a separate article.) The Gamma API returns bid prices; the CLOB fills you at ask prices. My paper trading P&amp;amp;L still looks better than live P&amp;amp;L because of this systematic difference. Rust didn't change that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The development cost was real.&lt;/strong&gt; The Rust bot took me roughly 3× longer to write than the TypeScript equivalent. The borrow checker rejected my first three approaches to the shared &lt;code&gt;PolymarketClient&lt;/code&gt;. Async Rust is significantly more complex than async TypeScript. If your bottleneck is iteration speed on strategy logic, TypeScript is a better choice. Rust pays off when you've validated your strategy and need to optimize execution.&lt;/p&gt;




&lt;h2&gt;
  
  
  Should You Do This?
&lt;/h2&gt;

&lt;p&gt;Here's my honest decision framework:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewrite in Rust if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You've validated your strategy logic produces positive EV over 1,000+ paper trades&lt;/li&gt;
&lt;li&gt;Your profiling shows latency bottlenecks in connection management or CPU-bound operations&lt;/li&gt;
&lt;li&gt;You're comfortable with a 2-4 week higher-complexity development phase&lt;/li&gt;
&lt;li&gt;You're already a systems engineer - the learning curve is steep if you're coming from scripting languages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Don't rewrite in Rust if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're still iterating on strategy logic (TypeScript iteration speed wins)&lt;/li&gt;
&lt;li&gt;Your bottleneck is network latency to Polygon RPC nodes (co-location solves this, not Rust)&lt;/li&gt;
&lt;li&gt;You have &amp;lt; 500 validated paper trades (you're optimizing before you have signal)&lt;/li&gt;
&lt;li&gt;Your total position sizes are under $5/trade (at that scale, 200ms of slippage costs &amp;lt; $0.01)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Two things I'm working on that Rust makes tractable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. WebSocket price feed instead of polling.&lt;/strong&gt; Right now I poll &lt;code&gt;clob.polymarket.com/midpoints&lt;/code&gt; every 30 seconds. With &lt;code&gt;tokio-tungstenite&lt;/code&gt;, I can subscribe to Polymarket's WebSocket feed and receive price updates in real time, reducing the detection lag from ~15 seconds (average time between poll and price change) to ~50ms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Chainlink oracle cross-validation.&lt;/strong&gt; Before placing an order, I want to compare Polymarket's implied probability against Chainlink's BTC/ETH/SOL price feeds on-chain. If Polymarket says "BTC Up is 82% likely" but Chainlink's oracle shows the last-reported price is already past the threshold, that's a stale market - not a mispricing. I want to skip those. This is the blockchain engineer angle that I think most Python/TypeScript bot builders overlook.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The TypeScript → Rust rewrite reduced my order placement latency from 340ms to 101ms - a 3.4× improvement. This translated to meaningfully better entry prices (1.1¢ slippage vs 5.3¢) and a modestly better fill rate (89% vs 61% within 0.5¢ of target).&lt;/p&gt;

&lt;p&gt;But the bigger lesson is in the profiling. &lt;strong&gt;194ms of the original 340ms was pure configuration debt&lt;/strong&gt; - no DNS caching, no connection pooling, synchronous logging on the hot path. Those fixes are available in TypeScript too. If you're running a Polymarket bot in Node.js and haven't profiled your order path yet, do that before considering a rewrite. You might recover 60% of the latency without changing languages.&lt;/p&gt;

&lt;p&gt;Rust was the right next step for me as a blockchain engineer building toward a production system. But fast wrong code is still wrong code. Profile first. Validate your strategy first. Then optimize.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part 6 of my Polymarket Trading Bot series. Previous posts: &lt;a href="https://dev.to/casatrick/building-a-polymarket-trading-bot-architecture-execution-and-risk-management-4gd7"&gt;Architecture &amp;amp; Execution&lt;/a&gt; · &lt;a href="https://dev.to/casatrick/how-kelly-criterion-transformed-my-polymarket-trading-bot-18c5"&gt;Kelly Criterion Sizing&lt;/a&gt; · &lt;a href="https://dev.to/casatrick/i-built-a-polymarket-trading-bot-that-tries-to-capture-the-last-60-seconds-of-market-inefficiency-4ki9"&gt;Last-60-Second Capture&lt;/a&gt; · &lt;a href="https://dev.tolink"&gt;Dashboard War Stories&lt;/a&gt; · &lt;a href="https://dev.to/casatrick/building-a-last-entry-probability-capture-bot-on-polymarket-3dn9"&gt;Last-Entry Probability Capture in TypeScript&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;All trading described is paper trading unless otherwise stated. This is not financial advice. Prediction market trading carries real risk of loss.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>rust</category>
      <category>trading</category>
      <category>bot</category>
    </item>
    <item>
      <title>Building a Polymarket Trading Bot: Architecture, Execution, and Risk Management</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Tue, 16 Jun 2026 16:18:38 +0000</pubDate>
      <link>https://dev.to/casatrick/building-a-polymarket-trading-bot-architecture-execution-and-risk-management-4gd7</link>
      <guid>https://dev.to/casatrick/building-a-polymarket-trading-bot-architecture-execution-and-risk-management-4gd7</guid>
      <description>&lt;h2&gt;
  
  
  How I built a real-time automated trading system that monitors BTC, ETH, SOL, and XRP prediction markets and executes trades based on short-lived pricing inefficiencies.
&lt;/h2&gt;

&lt;p&gt;Most people think of Polymarket as a prediction market for elections, sports, and current events.&lt;/p&gt;

&lt;p&gt;As a developer, I saw something else.&lt;/p&gt;

&lt;p&gt;I saw a real-time marketplace with public APIs, continuously updating order books, frequent settlement events, and thousands of micro-opportunities generated every day.&lt;/p&gt;

&lt;p&gt;That observation led me to build an automated trading bot focused exclusively on Polymarket's crypto markets.&lt;/p&gt;

&lt;p&gt;The system monitors Bitcoin (BTC), Ethereum (ETH), Solana (SOL), and XRP "Up or Down" markets, analyzes price movements in real time, and automatically executes trades when specific conditions are met.&lt;/p&gt;

&lt;p&gt;This article isn't about trading advice.&lt;/p&gt;

&lt;p&gt;Instead, it's a technical breakdown of the architecture, execution engine, risk controls, and lessons learned from running a real-time automated system against a live prediction market.&lt;/p&gt;




&lt;h1&gt;
  
  
  Understanding the Market
&lt;/h1&gt;

&lt;p&gt;Polymarket's crypto category includes short-duration contracts that resolve every 5 or 15 minutes.&lt;/p&gt;

&lt;p&gt;A typical market asks a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Will Bitcoin be higher or lower than its current price when the timer expires?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When the market resolves, winning shares pay $1 and losing shares pay $0.&lt;/p&gt;

&lt;p&gt;Because these markets settle so frequently, they create a constant stream of opportunities for automated systems.&lt;/p&gt;

&lt;p&gt;Unlike longer-term prediction markets, these contracts behave more like microstructure-driven trading environments than forecasting challenges.&lt;/p&gt;

&lt;p&gt;The closer a market gets to expiration, the more important execution speed becomes.&lt;/p&gt;




&lt;h1&gt;
  
  
  Project Goals
&lt;/h1&gt;

&lt;p&gt;Before writing any code, I defined several requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support multiple assets simultaneously&lt;/li&gt;
&lt;li&gt;Monitor live market prices continuously&lt;/li&gt;
&lt;li&gt;Detect pricing discrepancies automatically&lt;/li&gt;
&lt;li&gt;Execute trades with minimal latency&lt;/li&gt;
&lt;li&gt;Manage risk across all active positions&lt;/li&gt;
&lt;li&gt;Recover gracefully from API failures&lt;/li&gt;
&lt;li&gt;Operate without manual intervention&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final system runs across four crypto assets simultaneously:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BTC&lt;/li&gt;
&lt;li&gt;ETH&lt;/li&gt;
&lt;li&gt;SOL&lt;/li&gt;
&lt;li&gt;XRP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Running multiple assets in parallel dramatically increases the number of opportunities available each hour.&lt;/p&gt;




&lt;h1&gt;
  
  
  High-Level Architecture
&lt;/h1&gt;

&lt;p&gt;The system consists of five major components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  ┌─────────────────┐
                  │ Market Data Feed│
                  └────────┬────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │ Signal Engine   │
                  └────────┬────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │ Risk Manager    │
                  └────────┬────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │ Order Engine    │
                  └────────┬────────┘
                           │
                           ▼
                  ┌─────────────────┐
                  │ Polymarket API  │
                  └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each component is isolated so it can fail independently without taking down the entire system.&lt;/p&gt;




&lt;h1&gt;
  
  
  Real-Time Data Collection
&lt;/h1&gt;

&lt;p&gt;The first challenge was obtaining reliable market data.&lt;/p&gt;

&lt;p&gt;The bot continuously tracks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Underlying crypto prices&lt;/li&gt;
&lt;li&gt;Polymarket order books&lt;/li&gt;
&lt;li&gt;Bid/ask spreads&lt;/li&gt;
&lt;li&gt;Market expiration times&lt;/li&gt;
&lt;li&gt;Position inventory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A surprising lesson was that data quality matters more than signal complexity.&lt;/p&gt;

&lt;p&gt;A simple strategy running on accurate data consistently outperformed more sophisticated approaches built on delayed or inconsistent information.&lt;/p&gt;

&lt;p&gt;For short-duration markets, stale data can completely invalidate a trade.&lt;/p&gt;

&lt;p&gt;A position entered two seconds too late may have an entirely different risk profile than originally intended.&lt;/p&gt;




&lt;h1&gt;
  
  
  The Signal Engine
&lt;/h1&gt;

&lt;p&gt;One common misconception is that the bot predicts future prices.&lt;/p&gt;

&lt;p&gt;It doesn't.&lt;/p&gt;

&lt;p&gt;The strategy focuses on identifying situations where market pricing appears misaligned with current reality.&lt;/p&gt;

&lt;p&gt;The system continuously compares:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live asset price movement&lt;/li&gt;
&lt;li&gt;Remaining market lifetime&lt;/li&gt;
&lt;li&gt;Current market probability&lt;/li&gt;
&lt;li&gt;Available liquidity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When specific thresholds are met, a signal is generated.&lt;/p&gt;

&lt;p&gt;Conceptually:&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;if&lt;/span&gt; &lt;span class="n"&gt;market_probability&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;expected_probability&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;generate_buy_signal&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The actual implementation is more complex, but the principle remains the same.&lt;/p&gt;

&lt;p&gt;The goal is not forecasting.&lt;/p&gt;

&lt;p&gt;The goal is identifying moments when the market appears slow to update relative to incoming information.&lt;/p&gt;




&lt;h1&gt;
  
  
  Multi-Asset Execution
&lt;/h1&gt;

&lt;p&gt;One of the biggest improvements came from expanding beyond Bitcoin.&lt;/p&gt;

&lt;p&gt;Initially, the bot only traded BTC markets.&lt;/p&gt;

&lt;p&gt;Later versions added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ethereum&lt;/li&gt;
&lt;li&gt;Solana&lt;/li&gt;
&lt;li&gt;XRP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All assets run through the same execution pipeline.&lt;/p&gt;

&lt;p&gt;This creates several advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;More opportunities per hour&lt;/li&gt;
&lt;li&gt;Better capital utilization&lt;/li&gt;
&lt;li&gt;Reduced dependence on a single market&lt;/li&gt;
&lt;li&gt;Higher overall trade frequency&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of waiting for one market setup, the system can evaluate dozens of opportunities simultaneously.&lt;/p&gt;




&lt;h1&gt;
  
  
  Risk Management
&lt;/h1&gt;

&lt;p&gt;The signal is not the most important part of the system.&lt;/p&gt;

&lt;p&gt;Risk management is.&lt;/p&gt;

&lt;p&gt;Most automated trading failures are caused by position sizing errors rather than signal quality.&lt;/p&gt;

&lt;p&gt;The bot includes several layers of protection:&lt;/p&gt;

&lt;h2&gt;
  
  
  Position Limits
&lt;/h2&gt;

&lt;p&gt;Every market has a maximum allocation.&lt;/p&gt;

&lt;p&gt;No single trade can exceed predefined limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exposure Limits
&lt;/h2&gt;

&lt;p&gt;Global exposure is monitored continuously.&lt;/p&gt;

&lt;p&gt;The system refuses new positions if portfolio risk exceeds configured thresholds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cooldown Mechanisms
&lt;/h2&gt;

&lt;p&gt;Following abnormal behavior or unexpected losses, the bot can temporarily suspend trading.&lt;/p&gt;

&lt;p&gt;This prevents cascading failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Emergency Shutdown
&lt;/h2&gt;

&lt;p&gt;Critical errors trigger an automatic halt.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API failures&lt;/li&gt;
&lt;li&gt;Data feed interruptions&lt;/li&gt;
&lt;li&gt;Unexpected market conditions&lt;/li&gt;
&lt;li&gt;Order execution anomalies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The objective is simple:&lt;/p&gt;

&lt;p&gt;Protect capital first.&lt;/p&gt;

&lt;p&gt;Everything else comes second.&lt;/p&gt;




&lt;h1&gt;
  
  
  Maker Rebates
&lt;/h1&gt;

&lt;p&gt;One unexpected source of performance came from maker rebates.&lt;/p&gt;

&lt;p&gt;Many traders focus exclusively on directional profits.&lt;/p&gt;

&lt;p&gt;However, exchanges often reward liquidity providers.&lt;/p&gt;

&lt;p&gt;Whenever possible, the bot posts limit orders instead of immediately crossing the spread.&lt;/p&gt;

&lt;p&gt;This creates an additional return stream independent of market direction.&lt;/p&gt;

&lt;p&gt;Over hundreds or thousands of trades, these small incentives become surprisingly meaningful.&lt;/p&gt;




&lt;h1&gt;
  
  
  Challenges in Production
&lt;/h1&gt;

&lt;p&gt;Building the strategy was easier than operating it reliably.&lt;/p&gt;

&lt;p&gt;Several issues appeared only after deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Reliability
&lt;/h2&gt;

&lt;p&gt;External services fail.&lt;/p&gt;

&lt;p&gt;Connections drop.&lt;/p&gt;

&lt;p&gt;Responses arrive late.&lt;/p&gt;

&lt;p&gt;Timeout handling became a core part of the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clock Synchronization
&lt;/h2&gt;

&lt;p&gt;When markets resolve every few minutes, timing accuracy matters.&lt;/p&gt;

&lt;p&gt;Even small clock drift can create execution errors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Partial Fills
&lt;/h2&gt;

&lt;p&gt;Not every order executes completely.&lt;/p&gt;

&lt;p&gt;The system must continuously reconcile expected versus actual positions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;A trading bot without monitoring is effectively blind.&lt;/p&gt;

&lt;p&gt;Dashboards, alerts, and logging became essential operational tools.&lt;/p&gt;

&lt;p&gt;In production environments, visibility often matters more than optimization.&lt;/p&gt;




&lt;h1&gt;
  
  
  Lessons Learned
&lt;/h1&gt;

&lt;p&gt;After running the system for an extended period, several lessons stand out.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Data Quality Beats Complexity
&lt;/h3&gt;

&lt;p&gt;Better data consistently outperformed more sophisticated models.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Real-Time Systems Are Hard
&lt;/h3&gt;

&lt;p&gt;Most engineering challenges came from distributed systems problems rather than trading logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Latency Matters
&lt;/h3&gt;

&lt;p&gt;The value of a signal decreases rapidly with time.&lt;/p&gt;

&lt;p&gt;Execution speed directly impacts performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Monitoring Is Essential
&lt;/h3&gt;

&lt;p&gt;Every production system eventually encounters unexpected behavior.&lt;/p&gt;

&lt;p&gt;Observability determines how quickly you recover.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Market Edges Decay
&lt;/h3&gt;

&lt;p&gt;Any inefficiency that can be automated will eventually attract competition.&lt;/p&gt;

&lt;p&gt;Strategies must evolve continuously.&lt;/p&gt;




&lt;h1&gt;
  
  
  Future Improvements
&lt;/h1&gt;

&lt;p&gt;Several areas remain interesting for future development:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-market arbitrage detection&lt;/li&gt;
&lt;li&gt;Advanced liquidity management&lt;/li&gt;
&lt;li&gt;Adaptive position sizing&lt;/li&gt;
&lt;li&gt;Machine-learning-based signal ranking&lt;/li&gt;
&lt;li&gt;Portfolio-level optimization&lt;/li&gt;
&lt;li&gt;Improved execution algorithms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether these improvements generate meaningful performance gains remains an open question.&lt;/p&gt;




&lt;h1&gt;
  
  
  Final Thoughts
&lt;/h1&gt;

&lt;p&gt;Building this bot taught me far more about real-time systems than it did about trading.&lt;/p&gt;

&lt;p&gt;The most valuable lessons came from engineering challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Managing unreliable external APIs&lt;/li&gt;
&lt;li&gt;Processing live market data&lt;/li&gt;
&lt;li&gt;Handling distributed state&lt;/li&gt;
&lt;li&gt;Designing fault-tolerant execution systems&lt;/li&gt;
&lt;li&gt;Building reliable monitoring pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;From the outside, an automated trading bot may look like a collection of buy and sell decisions.&lt;/p&gt;

&lt;p&gt;Under the hood, it's a real-time distributed system operating in an adversarial environment where every millisecond and every bug matters.&lt;/p&gt;

&lt;p&gt;For developers interested in algorithmic trading, prediction markets provide a fascinating playground for experimenting with automation, market microstructure, and systems engineering.&lt;/p&gt;

&lt;p&gt;And regardless of whether the trading edge lasts, the engineering lessons certainly will.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclaimer: This article describes a personal software project and should not be interpreted as financial advice. Trading and prediction markets involve significant risk, and past performance does not guarantee future results.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>web3</category>
      <category>automation</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How Kelly Criterion Transformed My Polymarket Trading Bot</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Thu, 11 Jun 2026 09:02:57 +0000</pubDate>
      <link>https://dev.to/casatrick/how-kelly-criterion-transformed-my-polymarket-trading-bot-18c5</link>
      <guid>https://dev.to/casatrick/how-kelly-criterion-transformed-my-polymarket-trading-bot-18c5</guid>
      <description>&lt;p&gt;I've been building a Polymarket trading bot for a while now. The strategy was solid - good signal, decent win rate - but the returns felt inconsistent. Some weeks were great, others wiped out gains I'd spent days building. The bot was right more often than not, but the &lt;em&gt;sizing&lt;/em&gt; was all over the place.&lt;/p&gt;

&lt;p&gt;Then I implemented the Kelly Criterion. It didn't change a single prediction. It just changed how much I bet on each one. The difference was night and day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Please take a look at &lt;a href="https://polymarket.com/@rowelly" rel="noopener noreferrer"&gt;my Polymarket account&lt;/a&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  What Is the Kelly Criterion?
&lt;/h2&gt;

&lt;p&gt;The Kelly Criterion is a formula for calculating the optimal fraction of your bankroll to wager on a bet, given your edge. It was invented by John Kelly at Bell Labs in 1956 and has been used by everyone from blackjack card counters to quantitative hedge funds.&lt;/p&gt;

&lt;p&gt;The formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;f* = (bp - q) / b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;f*&lt;/code&gt; = fraction of bankroll to bet&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b&lt;/code&gt; = net odds (how much you win per $1 wagered)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;p&lt;/code&gt; = your estimated probability of winning&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q&lt;/code&gt; = probability of losing (1 - p)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In prediction market terms, if Polymarket shows a contract at &lt;strong&gt;40¢&lt;/strong&gt; (implying 40% probability), but your model says the true probability is &lt;strong&gt;55%&lt;/strong&gt;, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;b = (1 - 0.40) / 0.40 = 1.5   (you risk 40¢ to win 60¢)
p = 0.55
q = 0.45

f* = (1.5 × 0.55 - 0.45) / 1.5
f* = (0.825 - 0.45) / 1.5
f* = 0.375 / 1.5
f* = 0.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kelly says: bet &lt;strong&gt;25% of your bankroll&lt;/strong&gt; on this trade.&lt;/p&gt;

&lt;p&gt;In JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// net odds&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&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="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// never bet negative (no edge = no bet)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Example: market at 40%, your model says 55%&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Bet &lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;% of bankroll`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 25.0%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Full Kelly vs Fractional Kelly
&lt;/h2&gt;

&lt;p&gt;Here's the thing about Full Kelly: &lt;strong&gt;it's mathematically optimal but practically brutal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Full Kelly maximises the long-run growth rate of your bankroll. But it assumes your probability estimates are &lt;em&gt;perfectly accurate&lt;/em&gt;. In reality, your model is wrong sometimes. Maybe your edge is smaller than you think. Maybe the market knows something you don't.&lt;/p&gt;

&lt;p&gt;Full Kelly also produces wild variance. Your bankroll can drop 30–40% even when you're playing correctly - and for a bot running 24/7, that kind of drawdown is psychologically (and sometimes financially) devastating.&lt;/p&gt;

&lt;h3&gt;
  
  
  Half Kelly
&lt;/h3&gt;

&lt;p&gt;Most practitioners use &lt;strong&gt;Half Kelly&lt;/strong&gt; - simply halving the output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;halfKelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;)&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;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Half Kelly gives you roughly &lt;strong&gt;75% of the growth rate&lt;/strong&gt; of Full Kelly, but with dramatically less variance. The drawdowns are about half as deep.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fractional Kelly in my bot
&lt;/h3&gt;

&lt;p&gt;I actually use a variable fraction based on my model confidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fractionalKelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// confidence: 0.0 (low) to 1.0 (high)&lt;/span&gt;
  &lt;span class="c1"&gt;// Scales between quarter Kelly and full Kelly&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.75&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;kelly&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;marketProb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myProb&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When my model has a strong, well-evidenced signal, I scale toward Full Kelly. When it's a weaker or noisier signal, I scale back toward Quarter Kelly. This way, sizing reflects not just &lt;em&gt;edge size&lt;/em&gt; but &lt;em&gt;confidence in the edge&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Changed My Bot's Performance
&lt;/h2&gt;

&lt;p&gt;Before Kelly, I was flat-betting - putting the same fixed amount on every trade regardless of edge. This seems "safe" but it's actually suboptimal in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Overbetting weak edges&lt;/strong&gt; → unnecessary variance, occasional blowups&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Underbetting strong edges&lt;/strong&gt; → leaving money on the table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what changed after switching to Fractional Kelly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before Kelly&lt;/th&gt;
&lt;th&gt;After Kelly&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Weekly variance&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max drawdown&lt;/td&gt;
&lt;td&gt;~28%&lt;/td&gt;
&lt;td&gt;~12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Return per trade&lt;/td&gt;
&lt;td&gt;Similar&lt;/td&gt;
&lt;td&gt;Similar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Overall growth&lt;/td&gt;
&lt;td&gt;Inconsistent&lt;/td&gt;
&lt;td&gt;Smoother compounding&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The individual trade returns didn't change much - my model was the same. But the &lt;em&gt;compounding&lt;/em&gt; improved significantly. Kelly sizing means your bankroll grows faster during hot streaks and protects capital during cold ones automatically, without any manual intervention.&lt;/p&gt;

&lt;p&gt;The biggest mindset shift: &lt;strong&gt;Kelly isn't about winning more. It's about not blowing up your edge by sizing wrong.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A Few Gotchas I Learned the Hard Way
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Your probability estimates are almost certainly overconfident.&lt;/strong&gt;&lt;br&gt;
If your model says 70% but the true probability is 58%, Kelly will tell you to bet more than you should. Always sanity-check your edge estimates against your actual historical win rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Kelly ignores liquidity.&lt;/strong&gt;&lt;br&gt;
On Polymarket, large positions move the market against you. Even if Kelly says 30%, you might only be able to fill 5–8% without impacting your own odds. I cap positions at a liquidity-adjusted maximum regardless of Kelly output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Multiple simultaneous bets.&lt;/strong&gt;&lt;br&gt;
If your bot runs several trades at once, you can't just apply Kelly independently to each - you'd be collectively overbetting. Look into &lt;strong&gt;simultaneous Kelly&lt;/strong&gt; or simply divide your effective bankroll by the number of concurrent positions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Never bet on negative Kelly.&lt;/strong&gt;&lt;br&gt;
If &lt;code&gt;f*&lt;/code&gt; comes out negative, Kelly is telling you: &lt;em&gt;this bet has no edge for you&lt;/em&gt;. Pass. The formula is a filter as much as a sizer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;The Kelly Criterion didn't make my bot smarter. It made it &lt;em&gt;more disciplined&lt;/em&gt;. That distinction matters more than I expected.&lt;/p&gt;

&lt;p&gt;If you're building a prediction market bot and you're flat-betting or gut-sizing, this is probably the single highest-leverage improvement you can make - without touching your signal at all.&lt;/p&gt;

&lt;p&gt;Try Half Kelly first. Watch your drawdowns shrink. Then tune from there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building on Polymarket or have questions about bot sizing? Drop a comment - happy to compare notes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>polymarket</category>
      <category>backend</category>
      <category>analytics</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>I Built a Polymarket Trading Bot That Tries to Capture the Last 60 Seconds of Market Inefficiency</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Wed, 10 Jun 2026 13:20:20 +0000</pubDate>
      <link>https://dev.to/casatrick/i-built-a-polymarket-trading-bot-that-tries-to-capture-the-last-60-seconds-of-market-inefficiency-4ki9</link>
      <guid>https://dev.to/casatrick/i-built-a-polymarket-trading-bot-that-tries-to-capture-the-last-60-seconds-of-market-inefficiency-4ki9</guid>
      <description>&lt;p&gt;Over the last few days, I've been building and testing a Polymarket trading bot focused on BTC and ETH 15-minute markets.&lt;/p&gt;

&lt;p&gt;The idea is surprisingly simple:&lt;/p&gt;

&lt;p&gt;Instead of predicting where Bitcoin or Ethereum will go over the next hour, day, or week, the bot attempts to identify situations where the market already appears nearly certain about the outcome and then enters shortly before settlement.&lt;/p&gt;

&lt;p&gt;Think of it less as directional trading and more as an attempt to capture remaining uncertainty premium.&lt;/p&gt;

&lt;p&gt;For more information about the strategy please read this &lt;a href="https://medium.com/@roswelly/building-a-polymarket-trading-bot-exploring-late-stage-market-inefficiencies-4ace15263b52" rel="noopener noreferrer"&gt;medium article&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Suppose a market is trading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YES = 0.88&lt;/li&gt;
&lt;li&gt;NO = 0.12&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If YES wins, each YES token settles for $1.00.&lt;/p&gt;

&lt;p&gt;Buying at 0.88 means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Risk: $0.88&lt;/li&gt;
&lt;li&gt;Payout: $1.00&lt;/li&gt;
&lt;li&gt;Gross Return: 13.6%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The catch is obvious:&lt;/p&gt;

&lt;p&gt;You need to win more often than the implied probability suggests.&lt;/p&gt;

&lt;p&gt;If the market is perfectly efficient, there should be little or no edge.&lt;/p&gt;

&lt;p&gt;The entire strategy depends on one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are prediction markets systematically mispricing near-certain outcomes during the final seconds before resolution?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Dangerous
&lt;/h2&gt;

&lt;p&gt;At first glance the strategy looks easy.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;There are several major risks.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Slippage
&lt;/h3&gt;

&lt;p&gt;A trade that appears available at 0.88 may actually fill at 0.91 or 0.94.&lt;/p&gt;

&lt;p&gt;A few percentage points of slippage can completely eliminate the expected edge.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fees
&lt;/h3&gt;

&lt;p&gt;Small expected returns become much smaller after fees.&lt;/p&gt;

&lt;p&gt;When many trades only generate 1–3% ROI, execution costs matter.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Last-Minute Reversals
&lt;/h3&gt;

&lt;p&gt;Crypto can move violently in the final candle.&lt;/p&gt;

&lt;p&gt;A market showing 95% confidence can suddenly reverse if BTC or ETH experiences a sharp move.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Resolution Risk
&lt;/h3&gt;

&lt;p&gt;Prediction markets introduce a unique risk:&lt;/p&gt;

&lt;p&gt;Even a correct trade can experience delays if settlement is disputed or delayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bot Architecture
&lt;/h2&gt;

&lt;p&gt;The bot is intentionally simple.&lt;/p&gt;

&lt;p&gt;Stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node.js&lt;/li&gt;
&lt;li&gt;TypeScript&lt;/li&gt;
&lt;li&gt;Ubuntu&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Paper Trading&lt;/li&gt;
&lt;li&gt;Backtesting&lt;/li&gt;
&lt;li&gt;Live Trading&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data storage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON files&lt;/li&gt;
&lt;li&gt;CSV exports&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No database.&lt;/p&gt;

&lt;p&gt;No Docker.&lt;/p&gt;

&lt;p&gt;No frontend.&lt;/p&gt;

&lt;p&gt;Everything is logged directly to the terminal and stored for later analysis.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Bot Tracks
&lt;/h2&gt;

&lt;p&gt;For every trade the system records:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entry probability&lt;/li&gt;
&lt;li&gt;BTC/ETH spot price&lt;/li&gt;
&lt;li&gt;Time remaining until resolution&lt;/li&gt;
&lt;li&gt;Bid/ask spread&lt;/li&gt;
&lt;li&gt;Liquidity&lt;/li&gt;
&lt;li&gt;Expected fill price&lt;/li&gt;
&lt;li&gt;Actual fill price&lt;/li&gt;
&lt;li&gt;Slippage&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Fees&lt;/li&gt;
&lt;li&gt;ROI&lt;/li&gt;
&lt;li&gt;Final outcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example trade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BTC 15m market&lt;/li&gt;
&lt;li&gt;85.5% probability&lt;/li&gt;
&lt;li&gt;Entry 41 seconds before resolution&lt;/li&gt;
&lt;li&gt;Stake: $50.86&lt;/li&gt;
&lt;li&gt;Profit: $7.88&lt;/li&gt;
&lt;li&gt;ROI: 15.49%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That single trade generated more profit than many of the 98–99% probability entries combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Early Results
&lt;/h2&gt;

&lt;p&gt;Current results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Trades Settled: 22&lt;/li&gt;
&lt;li&gt;Wins: 22&lt;/li&gt;
&lt;li&gt;Losses: -$13.5&lt;/li&gt;
&lt;li&gt;Win Rate: 100%&lt;/li&gt;
&lt;li&gt;Profit: +$37.84&lt;/li&gt;
&lt;li&gt;Account Growth: approximately +3.8%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The highest quality opportunities were not always the highest probability trades.&lt;/p&gt;

&lt;p&gt;Some of the most profitable trades occurred when the market was pricing outcomes around 85–95% rather than 99%.&lt;/p&gt;

&lt;p&gt;That observation surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Interesting Finding So Far
&lt;/h2&gt;

&lt;p&gt;The strategy's biggest challenge isn't prediction.&lt;/p&gt;

&lt;p&gt;It's execution.&lt;/p&gt;

&lt;p&gt;In many cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The market direction was correct.&lt;/li&gt;
&lt;li&gt;The probability estimate was correct.&lt;/li&gt;
&lt;li&gt;The trade still produced very little profit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because entering at 98–99% probability leaves almost no remaining premium to capture.&lt;/p&gt;

&lt;p&gt;Several trades generated less than $1 profit despite being successful.&lt;/p&gt;

&lt;p&gt;This suggests that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Win rate alone is not enough.&lt;/li&gt;
&lt;li&gt;Expected value matters more than accuracy.&lt;/li&gt;
&lt;li&gt;Better entries may exist at lower probabilities if risk remains controlled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I'm Investigating Next
&lt;/h2&gt;

&lt;p&gt;I'm now collecting enough data to analyze:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spread vs profitability&lt;/li&gt;
&lt;li&gt;Slippage vs profitability&lt;/li&gt;
&lt;li&gt;Liquidity vs profitability&lt;/li&gt;
&lt;li&gt;Entry timing vs profitability&lt;/li&gt;
&lt;li&gt;BTC volatility vs profitability&lt;/li&gt;
&lt;li&gt;ETH volatility vs profitability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is to identify which variables actually drive returns rather than relying on intuition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;A 100% win rate sounds impressive.&lt;/p&gt;

&lt;p&gt;But 22 trades is nowhere near enough data to prove an edge.&lt;/p&gt;

&lt;p&gt;The real test will come after hundreds or thousands of trades.&lt;/p&gt;

&lt;p&gt;For now, the most valuable outcome isn't the profit.&lt;/p&gt;

&lt;p&gt;It's the data.&lt;/p&gt;

&lt;p&gt;Every trade helps answer the question:&lt;/p&gt;

&lt;p&gt;Can prediction markets become inefficient during the final moments before resolution, and if so, under what conditions?&lt;/p&gt;

&lt;p&gt;That's the problem I'm trying to solve.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>web3</category>
    </item>
    <item>
      <title>I Built a Real-Time Dashboard for My Polymarket Trading Bot - Here Are Problems Nobody Warned Me About</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:18:53 +0000</pubDate>
      <link>https://dev.to/casatrick/i-built-a-real-time-dashboard-for-my-polymarket-trading-bot-here-are-problems-nobody-warned-me-2ho8</link>
      <guid>https://dev.to/casatrick/i-built-a-real-time-dashboard-for-my-polymarket-trading-bot-here-are-problems-nobody-warned-me-2ho8</guid>
      <description>&lt;p&gt;I've been running a Polymarket trading bot since April 2026. 3,292 predictions placed. And for the first two months, I was flying blind.&lt;/p&gt;

&lt;p&gt;No dashboard. Just raw terminal logs scrolling past. P&amp;amp;L buried in JSON files. No way to know in real-time if the bot was winning, losing, stuck, or silently dead.&lt;/p&gt;

&lt;p&gt;So I built a dashboard. And it nearly broke me.&lt;/p&gt;

&lt;p&gt;This is everything I wish someone had told me before I started - the architecture decisions, the shocking edge cases, the WebSocket nightmares, the data problems that only appear at 2am when your bot is live.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Dashboard Needed to Do
&lt;/h2&gt;

&lt;p&gt;Before writing a single line of code I listed the minimum viable features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time P&amp;amp;L&lt;/strong&gt; - know my current profit/loss without opening a spreadsheet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live trade signals&lt;/strong&gt; - see what the bot is considering entering right now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Position tracker&lt;/strong&gt; - all open positions with entry price, current odds, unrealized P&amp;amp;L&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bot health monitor&lt;/strong&gt; - is the bot alive? when did it last fire? is the API connection healthy?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Win rate stats&lt;/strong&gt; - rolling win rate, average ROI per trade, total predictions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Market odds display&lt;/strong&gt; - live Polymarket odds for markets the bot is watching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple list. Absolute nightmare to build correctly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack I Chose
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend:  React + Recharts + TailwindCSS
Backend:   FastAPI (Python)
Real-time: WebSocket (native browser API + Python websockets library)
Database:  SQLite (local) → PostgreSQL (production)
Data:      Polymarket CLOB API + GAMMA API
Hosting:   VPS (Ubuntu) + Nginx reverse proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why FastAPI over Flask? Server-sent events and WebSocket support are first-class. With Flask I was fighting the framework. FastAPI just works.&lt;/p&gt;

&lt;p&gt;Why SQLite first? Because premature optimization is real. SQLite handled 3,000+ trade records without breaking a sweat. I only moved to Postgres when I needed concurrent writes from multiple bot processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #1: The WebSocket That Lies to You
&lt;/h2&gt;

&lt;p&gt;This one cost me three days.&lt;/p&gt;

&lt;p&gt;Polymarket's CLOB WebSocket connection appears to stay open even when it's dead. The &lt;code&gt;readyState&lt;/code&gt; shows &lt;code&gt;OPEN&lt;/code&gt;. No error fires. No disconnect event. The connection is just... silently delivering nothing.&lt;/p&gt;

&lt;p&gt;I built my dashboard assuming that if the WebSocket was open, it was working. My odds display would show data frozen from 40 minutes ago while I assumed everything was live.&lt;/p&gt;

&lt;p&gt;The fix is a heartbeat monitor - you have to implement it yourself:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PolymarketWSClient&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;heartbeat_interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;url&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;on_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;on_message&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;heartbeat_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;heartbeat_interval&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_message_time&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;time&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;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;connect&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;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# auto-reconnect loop
&lt;/span&gt;            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;websockets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&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;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&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_message_time&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WS] Connected to &lt;/span&gt;&lt;span class="si"&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;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

                    &lt;span class="c1"&gt;# Run message listener and heartbeat checker concurrently
&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_listen&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_heartbeat_check&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="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;connected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WS] Disconnected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;. Reconnecting in 5s...&lt;/span&gt;&lt;span class="sh"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_listen&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;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="ow"&gt;in&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_message_time&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;time&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_heartbeat_check&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;ws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;sleep&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;heartbeat_interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;elapsed&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;time&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_message_time&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elapsed&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;heartbeat_interval&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[WS] No message for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s — forcing reconnect&lt;/span&gt;&lt;span class="sh"&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;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# triggers reconnect in outer loop
&lt;/span&gt;                &lt;span class="k"&gt;return&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: don't trust &lt;code&gt;readyState&lt;/code&gt;. Trust &lt;strong&gt;message recency&lt;/strong&gt;. If no message has arrived in 2× your expected interval, treat the connection as dead and reconnect.&lt;/p&gt;

&lt;p&gt;Dashboard lesson: always show a "last updated" timestamp next to every live data panel. Users (including yourself) need to know how stale the data is.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #2: P&amp;amp;L That Lies at Market Boundaries
&lt;/h2&gt;

&lt;p&gt;This one is subtle and will drive you insane.&lt;/p&gt;

&lt;p&gt;Polymarket markets resolve in batches. Between when a market closes and when it officially resolves on-chain, your position is in a limbo state. The API still shows it as open. The price is frozen. But the outcome is effectively decided.&lt;/p&gt;

&lt;p&gt;If you calculate P&amp;amp;L from position value alone, your dashboard will show wildly wrong numbers during this window - sometimes for hours.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MarketStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ACTIVE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;active&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;CLOSED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;closed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="c1"&gt;# trading stopped, not yet resolved
&lt;/span&gt;    &lt;span class="n"&gt;RESOLVED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resolved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# outcome confirmed on-chain
&lt;/span&gt;    &lt;span class="n"&gt;LIMBO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;limbo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;         &lt;span class="c1"&gt;# closed but resolution pending
&lt;/span&gt;
&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;market_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;token_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;current_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;
    &lt;span class="n"&gt;market_status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MarketStatus&lt;/span&gt;
    &lt;span class="n"&gt;resolution_price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_pnl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    P&amp;amp;L calculation that handles market boundary states correctly.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;market_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MarketStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RESOLVED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Use resolution price, not current_price (which may be stale)
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolution_price&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pnl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;awaiting_resolution_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;pnl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resolution_price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pnl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pnl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;resolved&lt;/span&gt;&lt;span class="sh"&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;is_final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;market_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MarketStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LIMBO&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Show unrealized P&amp;amp;L but flag it as unreliable
&lt;/span&gt;        &lt;span class="n"&gt;unrealized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pnl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unrealized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;limbo_unreliable&lt;/span&gt;&lt;span class="sh"&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;warning&lt;/span&gt;&lt;span class="sh"&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;Market closed — P&amp;amp;L pending on-chain resolution&lt;/span&gt;&lt;span class="sh"&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;is_final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;market_status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;MarketStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLOSED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Same as limbo but cleaner
&lt;/span&gt;        &lt;span class="n"&gt;unrealized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pnl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unrealized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;closed_pending&lt;/span&gt;&lt;span class="sh"&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;is_final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# ACTIVE
&lt;/span&gt;        &lt;span class="n"&gt;unrealized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pnl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unrealized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;live&lt;/span&gt;&lt;span class="sh"&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;is_final&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard lesson: &lt;strong&gt;never show a single P&amp;amp;L number without a status badge&lt;/strong&gt;. Show whether it's live, limbo, or final. A number without context is worse than no number.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #3: The Rate Limit Wall at Exactly the Wrong Moment
&lt;/h2&gt;

&lt;p&gt;Polymarket's CLOB API has rate limits that seem generous until your dashboard and your bot are both hitting the API at the same time.&lt;/p&gt;

&lt;p&gt;The bot is scanning for entry signals. The dashboard is polling for position updates. The dashboard is also refreshing odds for 12 watched markets. Suddenly you're at 429 Too Many Requests - and your bot misses an entry because the dashboard stole its API budget.&lt;/p&gt;

&lt;p&gt;The fix is a shared rate-limited API client with a request queue:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimitedAPIClient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Single shared API client for both bot and dashboard.
    Enforces rate limits across all consumers.
    &lt;/span&gt;&lt;span class="sh"&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;requests_per_second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&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;rps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests_per_second&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;min_interval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;requests_per_second&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_request_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&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;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&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;_lock&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="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        priority: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bot&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; = high priority (jumps queue)
                  &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;normal&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; = dashboard polling (standard queue)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&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;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&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="n"&gt;wait&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;min_interval&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&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_request_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&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;last_request_time&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="c1"&gt;# Make actual request (implement with aiohttp)
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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;_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fetch&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;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[API] Rate limited. Waiting &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&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;_fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# retry once
&lt;/span&gt;                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Singleton - import this everywhere
&lt;/span&gt;&lt;span class="n"&gt;api_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RateLimitedAPIClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests_per_second&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard lesson: &lt;strong&gt;your dashboard is a second consumer of your API budget&lt;/strong&gt;. Design for this from day one. The bot always gets priority. The dashboard polls on whatever's left.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #4: Real-Time Charts That Eat Your Memory
&lt;/h2&gt;

&lt;p&gt;I added a live P&amp;amp;L chart to the dashboard - a rolling line chart updating every second. Looked amazing. Worked perfectly.&lt;/p&gt;

&lt;p&gt;For about 45 minutes.&lt;/p&gt;

&lt;p&gt;Then the browser tab was using 800MB of RAM and the chart was lagging 3 seconds behind. The problem: I was pushing every data point into a React state array and never trimming it. After 2,700 data points (45 minutes × 60 seconds), Recharts was re-rendering a 2,700-point dataset on every tick.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_POINTS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 5 minutes at 1s intervals&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useLivePnLChart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;chartData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setChartData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bufferRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pnl_update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="na"&gt;pnl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pnl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="c1"&gt;// Update buffer&lt;/span&gt;
      &lt;span class="nx"&gt;bufferRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;bufferRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_POINTS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
        &lt;span class="nx"&gt;point&lt;/span&gt;
      &lt;span class="p"&gt;];&lt;/span&gt;

      &lt;span class="c1"&gt;// Only update React state every 5 seconds to limit re-renders&lt;/span&gt;
      &lt;span class="c1"&gt;// Use requestAnimationFrame for smooth updates&lt;/span&gt;
      &lt;span class="nf"&gt;setChartData&lt;/span&gt;&lt;span class="p"&gt;([...&lt;/span&gt;&lt;span class="nx"&gt;bufferRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handleMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;websocket&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;chartData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the throttled update pattern for charts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useThrottledChartUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastUpdate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&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;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lastUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;newData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;lastUpdate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;setter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;intervalMs&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard lesson: &lt;strong&gt;cap your data buffer&lt;/strong&gt;. 300 points is plenty for a live chart. Beyond that you're paying memory cost for data nobody is reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #5: The Bot Dies and the Dashboard Doesn't Know
&lt;/h2&gt;

&lt;p&gt;This one is genuinely scary. Your bot process crashes at 3 am. The dashboard still shows the last known state - positions, P&amp;amp;L, odds - all frozen but looking completely normal. You wake up, check the dashboard, think everything is fine. The bot has been dead for 6 hours.&lt;/p&gt;

&lt;p&gt;The fix is a heartbeat system between the bot and the dashboard backend:&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="c1"&gt;# In your bot process - send heartbeat every 10 seconds
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dashboard_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;payload&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;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;alive&lt;/span&gt;&lt;span class="sh"&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;active_positions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_positions&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_trade_time&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_trade_timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;uptime_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uptime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&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;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dashboard_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/heartbeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&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;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Heartbeat] Failed to send: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your FastAPI backend - track heartbeat and expose health endpoint
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;last_heartbeat&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;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/heartbeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;receive_heartbeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;last_heartbeat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;last_heartbeat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/bot-health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bot_health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last_heartbeat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;never_connected&lt;/span&gt;&lt;span class="sh"&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;alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;age&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_heartbeat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;alive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;  &lt;span class="c1"&gt;# dead if no heartbeat in 30 seconds
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;alive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_seen_seconds_ago&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&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;alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;alive&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEAD&lt;/span&gt;&lt;span class="sh"&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;last_data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;last_heartbeat&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the React dashboard, poll &lt;code&gt;/api/bot-health&lt;/code&gt; every 15 seconds and show a big red banner if &lt;code&gt;alive === false&lt;/code&gt;. Not a subtle indicator. A big red banner you cannot miss.&lt;/p&gt;

&lt;p&gt;Dashboard lesson: &lt;strong&gt;assume the bot will die unexpectedly&lt;/strong&gt;. Design the dashboard to scream when it does, not silently show stale data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #6: Timezone Hell
&lt;/h2&gt;

&lt;p&gt;Polymarket markets resolve at specific times. The CLOB API returns timestamps in UTC. Your local machine might be in a different timezone. Your VPS is probably in UTC. Your browser is in the user's local timezone.&lt;/p&gt;

&lt;p&gt;I spent two hours debugging why a market that "should have resolved 3 hours ago" was still showing as active. It had resolved. The timestamps were just being compared in mixed timezones.&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Always returns timezone-aware UTC datetime.
    Handles: Unix timestamp (int/float), ISO string, naive datetime
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&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;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Handle ISO format with or without Z suffix
&lt;/span&gt;        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Z&lt;/span&gt;&lt;span class="sh"&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;+00:00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;dt&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;ts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tzinfo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&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;ts&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot normalize timestamp: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="si"&gt;!r}&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;time_to_resolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_date_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Returns hours until resolution. Negative = already resolved.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalize_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end_date_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&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;delta&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard lesson: &lt;strong&gt;pick UTC everywhere in your backend&lt;/strong&gt;. Convert to local time only at the last moment in the frontend display layer. Never compare timestamps from different sources without normalizing first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Shocking Problem #7: The Number That's Always Wrong
&lt;/h2&gt;

&lt;p&gt;Your dashboard shows P&amp;amp;L as &lt;code&gt;$0.30000000000000004&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;JavaScript floating point. It will appear. It will look terrible. And it will appear in the most visible place on your dashboard.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The problem&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pnl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// → 0.30000000000000004&lt;/span&gt;

&lt;span class="c1"&gt;// The fix - always format before display&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatPnL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rounded&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rounded&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// For percentages&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatROI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;+&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}${(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// For odds (0-1 scale → percentage display)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formatOdds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;%`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Never let a raw float touch the DOM&lt;/span&gt;
&lt;span class="c1"&gt;// Every number: formatPnL(), formatROI(), or formatOdds() before render&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dashboard lesson: &lt;strong&gt;create formatting functions on day one&lt;/strong&gt; and route every displayed number through them. Fixing floating point display after the fact means hunting down every raw number in your JSX. Do it right from the start.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture That Actually Works
&lt;/h2&gt;

&lt;p&gt;After all of this, here's the architecture I settled on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│              React Dashboard                │
│  P&amp;amp;L Chart | Positions | Bot Health | Odds  │
└──────────────┬──────────────────────────────┘
               │ WebSocket (live updates)
               │ REST (initial load + polling)
┌──────────────▼──────────────────────────────┐
│           FastAPI Backend                   │
│  /api/positions  /api/health  /api/stats    │
│  WS /ws/live-feed                           │
└──────────────┬──────────────────────────────┘
               │
     ┌─────────┴──────────┐
     │                    │
┌────▼────┐        ┌──────▼──────┐
│ SQLite/ │        │ Polymarket  │
│Postgres │        │  CLOB API   │
│(history)│        │ (live data) │
└─────────┘        └─────────────┘
               ▲
               │ heartbeat every 10s
┌──────────────┴──────────────────────────────┐
│              Trading Bot Process            │
│  Scanner | Entry Logic | Position Manager   │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bot and the dashboard backend are &lt;strong&gt;separate processes&lt;/strong&gt;. They communicate only through the database (for history) and the heartbeat endpoint (for health). This means if the dashboard crashes, the bot keeps running. If the bot crashes, the dashboard still shows historical data and screams that the bot is dead.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10 Dashboard Rules I Now Never Break
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every live number shows its last-updated timestamp&lt;/strong&gt; - no silent staleness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bot health is always visible&lt;/strong&gt; - top of the page, big red/green indicator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P&amp;amp;L has a status badge&lt;/strong&gt; - live, limbo, resolved, or final&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebSocket has a heartbeat&lt;/strong&gt; - if no message in 30s, reconnect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard never shares API budget with the bot&lt;/strong&gt; - separate rate limiters&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All timestamps normalized to UTC at ingestion&lt;/strong&gt; - convert to local only on display&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chart data is capped at 300 points&lt;/strong&gt; - buffer trimmed on every push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every number goes through a formatter&lt;/strong&gt; - no raw floats in the DOM&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bot and dashboard are separate processes&lt;/strong&gt; - one dying doesn't kill the other&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume everything will fail at 3am&lt;/strong&gt; - design every component for graceful degradation&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What I'd Build Differently
&lt;/h2&gt;

&lt;p&gt;If I started today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Grafana + InfluxDB&lt;/strong&gt; for the charts from day one. Rolling my own charting was interesting but Grafana handles time-series data with zero memory issues.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add alerting before adding features&lt;/strong&gt; - a Telegram or Discord bot that pings me when the bot dies is more valuable than a beautiful P&amp;amp;L chart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start with a single-page static HTML dashboard&lt;/strong&gt; - I overcomplicated the frontend with React when a simple HTML page with vanilla JS WebSocket would have been live in 2 hours.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;Building a trading bot dashboard teaches you things that no tutorial prepares you for - because the problems only appear when real money is moving through real APIs in real time.&lt;/p&gt;

&lt;p&gt;The WebSocket that silently dies. The P&amp;amp;L frozen at market close. The rate limit wall you hit because you forgot your dashboard is also an API consumer. The floating point number that makes your beautiful dashboard look broken.&lt;/p&gt;

&lt;p&gt;Every one of these problems has a fix. But you have to get burned by them first.&lt;/p&gt;

&lt;p&gt;My Polymarket profile if you want to see the bot's live results: &lt;a href="https://polymarket.com/@rowelly" rel="noopener noreferrer"&gt;@rowelly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All code here is production code from my live bot. Use it, break it, improve it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: #webdev #python #javascript #react #algotrading #polymarket #tradingbot #webSocket #fastapi #buildinpublic&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>tradingbot</category>
      <category>polymarket</category>
    </item>
    <item>
      <title>Building a Last-Entry Probability Capture Trading Bot on Polymarket with TypeScript</title>
      <dc:creator>Blockchain Rust Engineer</dc:creator>
      <pubDate>Mon, 08 Jun 2026 16:15:33 +0000</pubDate>
      <link>https://dev.to/casatrick/building-a-last-entry-probability-capture-bot-on-polymarket-3dn9</link>
      <guid>https://dev.to/casatrick/building-a-last-entry-probability-capture-bot-on-polymarket-3dn9</guid>
      <description>&lt;p&gt;Over the past few months, I've been experimenting with automated trading systems on Polymarket.&lt;/p&gt;

&lt;p&gt;One of the more interesting ideas I explored was what I call a &lt;strong&gt;Last-Entry Probability Capture Strategy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The concept sounds simple:&lt;/p&gt;

&lt;p&gt;Instead of trying to predict market direction, wait until the final moments before resolution and look for markets where the remaining uncertainty appears overpriced.&lt;/p&gt;

&lt;p&gt;In theory, you're not forecasting the future. You're attempting to capture the gap between the market price and what appears to be a near-certain outcome.&lt;/p&gt;

&lt;p&gt;As it turns out, the strategy was much more challenging than it looked on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea
&lt;/h2&gt;

&lt;p&gt;Imagine a market trading at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YES = 0.88&lt;/li&gt;
&lt;li&gt;NO = 0.12&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the market ultimately resolves YES:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost = $0.88&lt;/li&gt;
&lt;li&gt;Payout = $1.00&lt;/li&gt;
&lt;li&gt;Gross return = 13.6%&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, that sounds attractive.&lt;/p&gt;

&lt;p&gt;The break-even point is straightforward:&lt;/p&gt;

&lt;p&gt;You need to be correct more often than the implied probability of your entry price.&lt;/p&gt;

&lt;p&gt;For an entry at 0.88, you need a win rate above 88%.&lt;/p&gt;

&lt;p&gt;The idea is not to predict outcomes.&lt;/p&gt;

&lt;p&gt;The idea is to identify situations where the market is temporarily mispricing the remaining uncertainty.&lt;/p&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;p&gt;Can you find moments where the market says 88%, but reality is closer to 95%, 98%, or 99%?&lt;/p&gt;

&lt;p&gt;That question became the foundation of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial Assumption
&lt;/h2&gt;

&lt;p&gt;My original assumption was simple:&lt;/p&gt;

&lt;p&gt;As a market approaches resolution, information becomes increasingly complete.&lt;/p&gt;

&lt;p&gt;If traders are slow to react or liquidity becomes fragmented, temporary pricing inefficiencies may appear.&lt;/p&gt;

&lt;p&gt;Those inefficiencies could potentially create opportunities for automated execution.&lt;/p&gt;

&lt;p&gt;The challenge was determining whether those opportunities actually survive real-world execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;The bot was built around several independent components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Polymarket Markets
        │
        ▼
Market Scanner
        │
        ▼
Signal Engine
        │
        ▼
Risk Filter
        │
        ▼
Execution Engine
        │
        ▼
Order Manager
        │
        ▼
Trade Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Market Scanner
&lt;/h3&gt;

&lt;p&gt;The scanner continuously monitored active markets and filtered candidates based on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Time remaining&lt;/li&gt;
&lt;li&gt;Current probability&lt;/li&gt;
&lt;li&gt;Spread size&lt;/li&gt;
&lt;li&gt;Available liquidity&lt;/li&gt;
&lt;li&gt;Historical behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was to reduce thousands of market updates into a manageable set of opportunities.&lt;/p&gt;

&lt;h3&gt;
  
  
  Signal Engine
&lt;/h3&gt;

&lt;p&gt;The signal engine evaluated whether a market met the strategy criteria.&lt;/p&gt;

&lt;p&gt;Typical factors included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Probability threshold&lt;/li&gt;
&lt;li&gt;Remaining time&lt;/li&gt;
&lt;li&gt;Spread conditions&lt;/li&gt;
&lt;li&gt;Liquidity requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only markets passing all conditions were forwarded to execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk Filter
&lt;/h3&gt;

&lt;p&gt;Before submitting an order, the bot evaluated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Position sizing&lt;/li&gt;
&lt;li&gt;Maximum exposure&lt;/li&gt;
&lt;li&gt;Current inventory&lt;/li&gt;
&lt;li&gt;Available liquidity&lt;/li&gt;
&lt;li&gt;Expected slippage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This layer prevented aggressive entries during poor market conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Execution Engine
&lt;/h3&gt;

&lt;p&gt;Execution turned out to be the most important component in the entire system.&lt;/p&gt;

&lt;p&gt;Theoretical edges are easy.&lt;/p&gt;

&lt;p&gt;Capturing them consistently is much harder.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality of Execution
&lt;/h2&gt;

&lt;p&gt;The biggest lesson from this project was that strategy logic is only half the problem.&lt;/p&gt;

&lt;p&gt;Execution quality determines whether the edge survives.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Liquidity Disappears Quickly
&lt;/h3&gt;

&lt;p&gt;One assumption I underestimated was liquidity decay.&lt;/p&gt;

&lt;p&gt;As resolution approaches, order books can change dramatically.&lt;/p&gt;

&lt;p&gt;An opportunity that appears available at 0.88 may only be partially fillable.&lt;/p&gt;

&lt;p&gt;The actual fill price may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.90&lt;/li&gt;
&lt;li&gt;0.91&lt;/li&gt;
&lt;li&gt;0.93&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, much of the theoretical edge has already disappeared.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Slippage Matters More Than Expected
&lt;/h3&gt;

&lt;p&gt;Backtests often assume perfect execution.&lt;/p&gt;

&lt;p&gt;Reality does not.&lt;/p&gt;

&lt;p&gt;A strategy that appears profitable on paper can become unprofitable when real fills are considered.&lt;/p&gt;

&lt;p&gt;Tracking actual fill prices became one of the most important metrics in the system.&lt;/p&gt;

&lt;p&gt;A small amount of slippage repeated hundreds of times can completely change performance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Late Price Movements Are Violent
&lt;/h3&gt;

&lt;p&gt;One of the largest risks appeared in short-duration crypto markets.&lt;/p&gt;

&lt;p&gt;Markets that seem nearly resolved can still experience dramatic price changes in the final moments.&lt;/p&gt;

&lt;p&gt;A market trading at:&lt;/p&gt;

&lt;p&gt;YES = 0.88&lt;/p&gt;

&lt;p&gt;can rapidly move lower if underlying price action changes unexpectedly.&lt;/p&gt;

&lt;p&gt;The closer you trade to resolution, the more sensitive you become to sudden market reactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Resolution Risk Exists
&lt;/h3&gt;

&lt;p&gt;Another challenge was resolution itself.&lt;/p&gt;

&lt;p&gt;Not every market resolves immediately.&lt;/p&gt;

&lt;p&gt;Close calls, oracle delays, and boundary conditions occasionally introduce uncertainty that isn't reflected in a simple probability calculation.&lt;/p&gt;

&lt;p&gt;Capital can remain locked longer than expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Markets Worked Best?
&lt;/h2&gt;

&lt;p&gt;During testing, the most practical candidates were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;BTC 15-minute markets&lt;/li&gt;
&lt;li&gt;ETH 15-minute markets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These markets generally offered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher liquidity&lt;/li&gt;
&lt;li&gt;Tighter spreads&lt;/li&gt;
&lt;li&gt;More consistent order books&lt;/li&gt;
&lt;li&gt;Better execution conditions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lower-liquidity markets often produced theoretical opportunities that disappeared once execution was considered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance Improvements
&lt;/h2&gt;

&lt;p&gt;Much of the engineering effort eventually shifted away from strategy design and toward infrastructure.&lt;/p&gt;

&lt;p&gt;Areas of focus included:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Faster market scanning&lt;/li&gt;
&lt;li&gt;Reduced execution latency&lt;/li&gt;
&lt;li&gt;Better connection management&lt;/li&gt;
&lt;li&gt;Lower processing overhead&lt;/li&gt;
&lt;li&gt;Improved monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In one iteration, I reduced execution latency from roughly 100ms to around 50ms.&lt;/p&gt;

&lt;p&gt;While that improvement didn't magically create profits, it significantly improved consistency during periods of heavy activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Biggest Lesson
&lt;/h2&gt;

&lt;p&gt;The most important takeaway from this project is that finding an apparent edge is relatively easy.&lt;/p&gt;

&lt;p&gt;The difficult part is determining whether that edge survives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fees&lt;/li&gt;
&lt;li&gt;Slippage&lt;/li&gt;
&lt;li&gt;Liquidity constraints&lt;/li&gt;
&lt;li&gt;Latency&lt;/li&gt;
&lt;li&gt;Market microstructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A strategy can look excellent in a spreadsheet and fail completely in production.&lt;/p&gt;

&lt;p&gt;The market doesn't care about theoretical profitability.&lt;/p&gt;

&lt;p&gt;It cares about execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;What started as a simple idea became a useful lesson in trading system design.&lt;/p&gt;

&lt;p&gt;The strategy itself was interesting, but the engineering challenges turned out to be even more valuable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Real-time data processing&lt;/li&gt;
&lt;li&gt;Risk management&lt;/li&gt;
&lt;li&gt;Low-latency execution&lt;/li&gt;
&lt;li&gt;Market microstructure analysis&lt;/li&gt;
&lt;li&gt;Performance optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whether the strategy ultimately succeeds or fails long term, building the system provided a much deeper understanding of how prediction markets behave under real trading conditions.&lt;/p&gt;

&lt;p&gt;And that's often where the most useful lessons come from.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>tutorial</category>
      <category>typescript</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
