<?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: Bryan Woods</title>
    <description>The latest articles on DEV Community by Bryan Woods (@rookai).</description>
    <link>https://dev.to/rookai</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%2F4038813%2F9c8ee328-a8dd-402d-b3e6-82324e62f57b.png</url>
      <title>DEV Community: Bryan Woods</title>
      <link>https://dev.to/rookai</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rookai"/>
    <language>en</language>
    <item>
      <title>Three Bugs, One Pattern: How My Trading Bot Put Stop-Losses Below Entries on Short Trades</title>
      <dc:creator>Bryan Woods</dc:creator>
      <pubDate>Mon, 20 Jul 2026 21:19:38 +0000</pubDate>
      <link>https://dev.to/rookai/three-bugs-one-pattern-how-my-trading-bot-put-stop-losses-below-entries-on-short-trades-57hc</link>
      <guid>https://dev.to/rookai/three-bugs-one-pattern-how-my-trading-bot-put-stop-losses-below-entries-on-short-trades-57hc</guid>
      <description>&lt;p&gt;&lt;strong&gt;By BDubs · AI Rook Trading Engine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My trading engine has a feature called "FVG anchoring." When a trade moves in your favor, the engine looks for a Fair Value Gap (a structural support/resistance zone from price action theory) and anchors the stop-loss just beyond it. This widens the stop from a tight breakeven level to a structurally meaningful one — giving the trade room to breathe while still protecting capital.&lt;/p&gt;

&lt;p&gt;It worked great for long trades. For short trades, it did the exact opposite: it placed the stop-loss below the entry price. A stop below your entry on a short means the trade can lose money before the stop even triggers. Three separate bugs, all in the same code path, all caused by the same mistake: the short-trade logic was written as if it were a long trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context
&lt;/h2&gt;

&lt;p&gt;The Rook Engine manages trade exits in phases. Phase 1 is the initial breakeven guard. Phase 2 tries to anchor the stop to a reverse FVG. Phase 3 switches to trailing. The FVG anchoring code lives across two files: &lt;code&gt;fvg-detector.js&lt;/code&gt; (finds the FVG) and &lt;code&gt;exit-manager.js&lt;/code&gt; (uses it to set the stop).&lt;/p&gt;

&lt;p&gt;The bugs only manifested on short trades because the engine had been primarily backtested and paper-traded on longs. The short path was never properly validated until a live S10 short entry at $75,359 got its stop anchored to $75,354 — five points &lt;strong&gt;below&lt;/strong&gt; entry. The trade was stopped out immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: Wrong FVG Type for Shorts
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;findReverseFVG()&lt;/code&gt; function finds the nearest structural zone to anchor the stop. For longs, you want a bearish FVG above price (resistance). For shorts, you want… also a bearish FVG above price (resistance). The code was finding a &lt;strong&gt;bullish&lt;/strong&gt; FVG &lt;strong&gt;below&lt;/strong&gt; price instead:&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;// BEFORE — finds bullish FVG below price (wrong for shorts!)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&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="s1"&gt;bullish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;top&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bullish FVG below price is a support zone. Placing a stop just above support when you're short is like placing a stop below entry — it gives the trade no protection at all.&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;// AFTER — finds bearish FVG above price (resistance above entry)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;active&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&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="s1"&gt;bearish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&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;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bottom&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="nx"&gt;bottom&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For shorts, the stop goes just above the bearish FVG's top. If price returns to that supply zone, the short thesis is invalidated. That's correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: Flipped Validation Guard
&lt;/h2&gt;

&lt;p&gt;Even if Bug 1 was somehow acceptable, the validation guard in &lt;code&gt;exit-manager.js&lt;/code&gt; was inverted. It was supposed to reject anchors that weren't above entry for shorts:&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;// BEFORE — rejects valid anchors, accepts invalid ones&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;anchoredSL&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reads: "if the anchored stop is above entry, reject it." For a short trade, the stop MUST be above entry. This guard was doing the exact opposite — rejecting every valid anchor and accepting every invalid one.&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;// AFTER — rejects anchors at or below entry (correct)&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;anchoredSL&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entry_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One character change: &lt;code&gt;&amp;gt;&lt;/code&gt; → &lt;code&gt;&amp;lt;=&lt;/code&gt;. The kind of bug that makes you question every comparison operator you've ever written.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: FVG Anchor Fires Before Breakeven Activates
&lt;/h2&gt;

&lt;p&gt;The phase transition from Phase 1 to Phase 2 could trigger on the very first candle close, even if the trade had never moved in the engine's favor. This means the FVG anchor could collapse the stop before the trade had any breathing room:&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;// BEFORE — no guard, fires on first candle close regardless&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchorResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tryFVGAnchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&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 javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AFTER — requires breakeven to have activated first&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;phase&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;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;be_activated&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;anchorResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tryFVGAnchor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;candles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;currentPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix: check &lt;code&gt;state.be_activated&lt;/code&gt; before attempting the FVG anchor. The trade must have moved in our favor and hit breakeven before we start restructuring the stop. Same guard applies to the Phase 3 skip path.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;All three bugs share a root cause: the short-trade code path was either copied from the long-trade logic without proper inversion, or never written at all and assumed to "just work." In trading systems, direction matters. Long and short are not symmetric — they're mirror images, and every comparison, every filter, every guard needs to reflect that.&lt;/p&gt;

&lt;p&gt;The commit (&lt;a href="https://github.com/Ai-Rook/rook-engine/commit/6207318" rel="noopener noreferrer"&gt;6207318&lt;/a&gt;) touches two files, 22 insertions, 10 deletions. Three bugs, one pattern, one lesson.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Test both directions.&lt;/strong&gt; If your trading code handles longs and shorts, test the short path as rigorously as the long path. They're not symmetric.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read your comparison operators aloud.&lt;/strong&gt; &lt;code&gt;if (anchoredSL &amp;gt; entry) return null&lt;/code&gt; — say it out loud. Does "reject if above entry" make sense for a short trade? If you have to think about it twice, the operator is probably wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase gates need preconditions.&lt;/strong&gt; Don't let exit management phases advance until the trade has proven itself. Breakeven activation should be a hard gate before any stop restructuring.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This fix was part of commit &lt;a href="https://github.com/Ai-Rook/rook-engine/commit/6207318" rel="noopener noreferrer"&gt;6207318&lt;/a&gt; in the &lt;a href="https://github.com/Ai-Rook/rook-engine" rel="noopener noreferrer"&gt;Rook Engine&lt;/a&gt; — an open-source algorithmic trading system.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
    </item>
    <item>
      <title>MMT Killed My WebSocket Every 5 Minutes — A 6-Character Fix</title>
      <dc:creator>Bryan Woods</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:54:26 +0000</pubDate>
      <link>https://dev.to/rookai/mmt-killed-my-websocket-every-5-minutes-a-6-character-fix-170o</link>
      <guid>https://dev.to/rookai/mmt-killed-my-websocket-every-5-minutes-a-6-character-fix-170o</guid>
      <description>&lt;p&gt;&lt;strong&gt;By BDubs · AI Rook Trading Engine&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I run an algorithmic trading engine that depends on a real-time WebSocket feed from MMT (a crypto market data provider). For weeks, my bot kept going dark. No crash. No error in my logs. Just… silence. The WebSocket would die with a &lt;code&gt;1011 read_error&lt;/code&gt;, and I'd have to restart the whole thing.&lt;/p&gt;

&lt;p&gt;The root cause was a single number that was wrong by a factor of 2.5.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Symptom
&lt;/h2&gt;

&lt;p&gt;Every 5 to 6 minutes, the MMT WebSocket connection would close. The engine would reconnect, but the reconnect cycle burned through rate limits and caused gaps in candle data. In live trading, gaps in data mean missed signals. Missed signals mean missed entries.&lt;/p&gt;

&lt;p&gt;I assumed MMT was rate-limiting me. I assumed my code had a memory leak. I assumed the VPS network was unstable.&lt;/p&gt;

&lt;p&gt;It was none of those things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Root Cause
&lt;/h2&gt;

&lt;p&gt;MMT's server closes idle WebSocket sessions after approximately 300 seconds (5 minutes). My engine had a "keepalive" mechanism that re-subscribed to the &lt;code&gt;stats&lt;/code&gt; channel every &lt;strong&gt;10 minutes&lt;/strong&gt;:&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;// BEFORE — keepalive fires every 10 minutes&lt;/span&gt;
&lt;span class="nf"&gt;setInterval&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;try&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;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readyState&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ws&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="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;subscribe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stats&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EXCHANGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;symbol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYMBOL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TF&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;catch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* non-fatal */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// every 10 minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The math is brutal: the server kills the connection at ~5 minutes. My keepalive fires at 10 minutes. The connection is already dead by then.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;stats&lt;/code&gt; channel was chosen as the keepalive because it's lightweight — just funding rates, liquidation counts, and mark price. But re-subscribing to a channel on a dead socket is useless. What I needed was a keepalive that fires &lt;strong&gt;before&lt;/strong&gt; the server's timeout, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Change one number:&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;// AFTER — keepalive fires every 4 minutes (before 300s timeout)&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// every 4 min (MMT server closes idle sessions at ~5min)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Six characters changed: &lt;code&gt;10&lt;/code&gt; → &lt;code&gt;4&lt;/code&gt;. The keepalive now fires every 240 seconds, comfortably ahead of the server's ~300-second idle timeout. The connection stays alive. No more &lt;code&gt;1011 read_error&lt;/code&gt;. No more reconnect loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is a Great Bug Smash Story
&lt;/h2&gt;

&lt;p&gt;Because it's the kind of bug that wastes days. When a WebSocket dies silently, you don't know if the problem is your code, your network, your server, or your provider. You start debugging in every direction except the one that matters.&lt;/p&gt;

&lt;p&gt;The fix was a single constant. The investigation could have been hours of &lt;code&gt;tcpdump&lt;/code&gt;, proxy logs, and support tickets. Instead it was: read the provider docs, find the idle timeout, compare it to your keepalive interval.&lt;/p&gt;

&lt;p&gt;The difference between &lt;code&gt;10 * 60 * 1000&lt;/code&gt; and &lt;code&gt;4 * 60 * 1000&lt;/code&gt; is the difference between a trading engine that stays online and one that goes dark every five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Always know your provider's idle timeout.&lt;/strong&gt; If you're using WebSockets for anything critical, the first thing to check is how long the server keeps idle connections alive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your keepalive interval must be shorter than the server's timeout.&lt;/strong&gt; Not equal. Shorter. With margin. If the server kills at 300 seconds, fire at 240, not 300.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent failures are the worst.&lt;/strong&gt; A WebSocket &lt;code&gt;1011 read_error&lt;/code&gt; doesn't crash your process. It just stops working. Monitor connection state, not just process state.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This fix was part of commit &lt;a href="https://github.com/Ai-Rook/rook-engine/commit/c12d52c" rel="noopener noreferrer"&gt;c12d52c&lt;/a&gt; in the &lt;a href="https://github.com/Ai-Rook/rook-engine" rel="noopener noreferrer"&gt;Rook Engine&lt;/a&gt; — an open-source algorithmic trading system. The full diff is two lines.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>debugging</category>
    </item>
  </channel>
</rss>
