<?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: ict_edge</title>
    <description>The latest articles on DEV Community by ict_edge (@ict_edge).</description>
    <link>https://dev.to/ict_edge</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%2F4173959%2Ff0235a27-2b85-43d2-91a1-311947083dcf.png</url>
      <title>DEV Community: ict_edge</title>
      <link>https://dev.to/ict_edge</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ict_edge"/>
    <language>en</language>
    <item>
      <title>Don't let a new signal engine write fake trades: a warm-up flag for trading alert bots</title>
      <dc:creator>ict_edge</dc:creator>
      <pubDate>Fri, 09 Oct 2026 17:48:43 +0000</pubDate>
      <link>https://dev.to/ict_edge/dont-let-a-new-signal-engine-write-fake-trades-a-warm-up-flag-for-trading-alert-bots-a25</link>
      <guid>https://dev.to/ict_edge/dont-let-a-new-signal-engine-write-fake-trades-a-warm-up-flag-for-trading-alert-bots-a25</guid>
      <description>&lt;p&gt;I run a small trading-alerts project, and this week I added eight new signal engines to a bot that was already running. The code was the easy part. The hard part was making sure the statistics stayed honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug you only see once
&lt;/h2&gt;

&lt;p&gt;A signal engine reads recent candles and says "there is a setup". When you deploy a new engine on a live bot, its very first cycle looks at the last few hours of history, finds setups that formed &lt;em&gt;before the engine existed&lt;/em&gt;, and happily records them as trades.&lt;/p&gt;

&lt;p&gt;Those trades never happened in real time. Nobody could have acted on them. But they now sit in your journal, they count in your win rate, and every comparison you make afterwards is polluted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: version the engines, skip the first cycle
&lt;/h2&gt;

&lt;p&gt;The bot keeps a small persistent state file. I store an engines version in it:&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;ENGINES_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v3&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;run_cycle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# True only on the first cycle after a new engine set is deployed
&lt;/span&gt;    &lt;span class="n"&gt;warm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&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;_engines&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;ENGINES_VERSION&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;engines&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bars&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="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;engine %s failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                      &lt;span class="c1"&gt;# one broken engine must not stop the others
&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;warm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                      &lt;span class="c1"&gt;# warm-up: compute, but record nothing
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;age_seconds&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;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_age&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                      &lt;span class="c1"&gt;# stale setup: too late to be actionable
&lt;/span&gt;        &lt;span class="n"&gt;journal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_engines&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;ENGINES_VERSION&lt;/span&gt;   &lt;span class="c1"&gt;# persisted, so a restart does not re-trigger warm-up
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The marker is persisted.&lt;/strong&gt; If it only lived in memory, every container restart would silently skip a cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The marker is written after the loop.&lt;/strong&gt; A crash in the middle means the next start is still "warm", which is the safe direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warm-up is not enough on its own.&lt;/strong&gt; An age guard per engine (a few minutes for fast setups, a bit more for slower ones) also rejects setups that were detected late, for example after a feed hiccup.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the deploy, I checked the journal: zero trades from the new engines, even though they were already running. That is the whole test, and it is worth automating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engines that depend on the clock are hard to test offline
&lt;/h2&gt;

&lt;p&gt;Some engines are tied to a session (an opening range, for instance). They use the current wall-clock time to decide whether the range window has closed. That means you cannot replay last week's candles through them and get a meaningful answer.&lt;/p&gt;

&lt;p&gt;What worked for me: test the plumbing with a fake feed in the right format, check that the engine runs without errors and produces no output outside its window, and accept that the first real validation happens on the first real session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller lessons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Isolate every engine&lt;/strong&gt; in its own try/except. A bug in one detector should cost you one detector, not the whole cycle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't share strategy code between bots.&lt;/strong&gt; Copying a file feels wasteful until a fix in one bot silently changes another bot's behaviour. Each bot gets its own copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep alerts off while comparing engines.&lt;/strong&gt; Eight new engines firing notifications on day one is noise, and noise hides the signal you are trying to measure.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this does not tell you
&lt;/h2&gt;

&lt;p&gt;Nothing here says any strategy makes money. It is about measurement hygiene: if your journal contains trades that could not have been taken, your comparisons are meaningless, whatever the strategies are. Trading carries a real risk of loss, and this is engineering, not financial advice.&lt;/p&gt;

&lt;p&gt;I build &lt;a href="https://trading-ict-smc.com" rel="noopener noreferrer"&gt;ICT Edge&lt;/a&gt;, a trading-alerts tool, which is where this comes from.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I drafted this post with AI assistance and reviewed and edited it myself.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>trading</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
