<?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: 王伟</title>
    <description>The latest articles on DEV Community by 王伟 (@wei-backtest).</description>
    <link>https://dev.to/wei-backtest</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%2F4198694%2F866d0ccd-ecfb-4592-b4e0-7fe050f96373.png</url>
      <title>DEV Community: 王伟</title>
      <link>https://dev.to/wei-backtest</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wei-backtest"/>
    <language>en</language>
    <item>
      <title>I Costed Futures Like Crypto, and Half the Loss Was Made Up</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Sat, 31 Oct 2026 01:00:04 +0000</pubDate>
      <link>https://dev.to/wei-backtest/i-costed-futures-like-crypto-and-half-the-loss-was-made-up-368k</link>
      <guid>https://dev.to/wei-backtest/i-costed-futures-like-crypto-and-half-the-loss-was-made-up-368k</guid>
      <description>&lt;p&gt;&lt;em&gt;A cost model is not a detail. It can be larger than the effect you are trying to measure.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I ran a 1-minute futures strategy and it lost 94% of its capital. That looked like a decisive answer: the strategy does not work.&lt;/p&gt;

&lt;p&gt;It was not decisive. &lt;strong&gt;Half of that loss was invented by my cost model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqr7mkmrqxf4pou2ljlcr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqr7mkmrqxf4pou2ljlcr.png" alt="Cost assumption vs measurement" width="799" height="329"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;I used &lt;strong&gt;5 basis points per side&lt;/strong&gt;, which is 10 bp round trip. That number came from crypto, where I had been working. It felt conservative: &lt;em&gt;surely paying more cost than reality is the safe direction.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That intuition is wrong, and not mildly wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually costs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instrument&lt;/th&gt;
&lt;th&gt;measured round trip&lt;/th&gt;
&lt;th&gt;assumption was&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NQ (Nasdaq futures)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.30 bp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;33× too high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MGC (micro gold)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.55 bp&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18× too high&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTC perpetual&lt;/td&gt;
&lt;td&gt;10.2 bp&lt;/td&gt;
&lt;td&gt;about right&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Note the last row. The number I used was correct — &lt;strong&gt;for the asset class I took it from&lt;/strong&gt;. That is the trap: a cost assumption is a fact about a specific market, and it does not travel.&lt;/p&gt;

&lt;p&gt;Futures on a liquid index are extraordinarily cheap to trade. Crypto perpetuals are not. They differ by more than an order of magnitude, and I applied the expensive one to the cheap market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it mattered more than usual
&lt;/h2&gt;

&lt;p&gt;Here is the part that makes this a systemic problem rather than a slip.&lt;/p&gt;

&lt;p&gt;With 1-minute entries, the &lt;strong&gt;per-trade risk&lt;/strong&gt; on this strategy was only about &lt;strong&gt;6.2 bp&lt;/strong&gt;. So the cost-to-risk ratio was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 bp cost ÷ 6.2 bp risk  ≈ 162%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was charging more than one and a half times the risk of the trade as friction. The result was that every trade lost &lt;strong&gt;3.16R&lt;/strong&gt; on average, of which &lt;strong&gt;2.16R was invented fees&lt;/strong&gt; and only 1.0R was the strategy.&lt;/p&gt;

&lt;p&gt;That is not a strategy being measured. That is a cost model being measured.&lt;/p&gt;

&lt;p&gt;Correcting the number to 0.30 bp moved the same run from &lt;strong&gt;−94.0% to −45.5%&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule of thumb
&lt;/h2&gt;

&lt;p&gt;Before rejecting a strategy on cost grounds, compute one ratio:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;round-trip cost ÷ per-trade risk&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If it exceeds about &lt;strong&gt;5%&lt;/strong&gt;, stop. Do not conclude the strategy is bad — conclude that &lt;strong&gt;you do not know your costs&lt;/strong&gt;, and go measure them. A ratio above 100% (as mine was) means the cost term dominates the result entirely and the backtest is telling you about your assumptions, not about the market.&lt;/p&gt;

&lt;p&gt;The corollary is just as important: a strategy whose edge is small relative to friction is not necessarily broken. It may be &lt;strong&gt;correctly measured and simply too small&lt;/strong&gt; — which is a different conclusion with a different next step (find cheaper execution, trade less often, or move to a higher timeframe) and not "throw it away".&lt;/p&gt;

&lt;h2&gt;
  
  
  How to measure instead of guess
&lt;/h2&gt;

&lt;p&gt;I did not have a perfect cost model afterwards either. What I had was something better than a guess:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Use the exchange's own fee schedule&lt;/strong&gt;, not a number from another asset class.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the spread separately.&lt;/strong&gt; It is usually larger than commission on liquid futures and it varies by time of day. Sample it, don't assume it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity-check against a fill-quality sample.&lt;/strong&gt; Take 50 real fills, compare your simulated price to the actual one. The gap is your slippage estimate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State the number and its source in the results.&lt;/strong&gt; "Costs: 0.30 bp round trip, measured from NQ fill samples over the sample period" is a claim someone can audit. "5 bp" is a habit.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The uncomfortable asymmetry
&lt;/h2&gt;

&lt;p&gt;An optimistic cost assumption makes a bad strategy look good — and you will find out in production, expensively.&lt;/p&gt;

&lt;p&gt;A pessimistic cost assumption makes a &lt;strong&gt;good&lt;/strong&gt; strategy look bad — and you will never find out, because you will have moved on. That failure is silent. It cost me a line of research, and I only noticed because a 94% loss looked too clean to be real.&lt;/p&gt;

&lt;p&gt;So the discipline is symmetric: &lt;strong&gt;do not treat high costs as the safe direction.&lt;/strong&gt; Both directions are errors, and the pessimistic one is the one nobody catches for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The cost-per-risk check is one of the static checks in a small open-source linter (&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT, no dependencies). It flags a fee column that is missing or zero, and reports cost against per-trade risk so the ratio is visible before you draw conclusions.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you would rather have this as a download than a repo: the same tool, plus the&lt;br&gt;
demo trade logs (one with five bugs planted, one clean), is *&lt;/em&gt;$0** on Gumroad —&lt;br&gt;
pay what you want, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>backtesting</category>
      <category>futures</category>
      <category>python</category>
    </item>
    <item>
      <title>One Missing Call Site Turned -5.7% Into +17.9%</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Fri, 30 Oct 2026 01:00:03 +0000</pubDate>
      <link>https://dev.to/wei-backtest/one-missing-call-site-turned-57-into-179-jbe</link>
      <guid>https://dev.to/wei-backtest/one-missing-call-site-turned-57-into-179-jbe</guid>
      <description>&lt;p&gt;&lt;em&gt;A one-line structural bug, worth 23.6 points of return and most of a 74% win rate.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I want to start with the number, because the number is the point:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkxwle7ccw0nsimv9ajd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpkxwle7ccw0nsimv9ajd.png" alt="The same strategy, before and after" width="800" height="331"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;−5.7% when exits are checked every bar. +17.9% when they are only checked on signal bars.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same strategy, same data, same period, same costs. The difference is where one function was called from.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;My backtester had a loop that looked roughly like this:&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;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# &amp;lt;- entry AND exit logic live in here
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;execute()&lt;/code&gt; contained both the entry and the exit check. Putting the exit check inside a function that only gets called on signal bars means &lt;strong&gt;the exit is only evaluated on bars where a signal fired&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On every other bar, the position sat there. Stops were not checked. Targets were not checked. Time-based exits were not checked. The strategy effectively had permission to hold through anything, as long as no new signal appeared.&lt;/p&gt;

&lt;p&gt;The fix is not a change to the strategy at all:&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;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bar&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&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;signal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&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;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;strategy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;manage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;# &amp;lt;- exits, every bar, unconditionally
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire change: move exit management out of the signal branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it flatters you
&lt;/h2&gt;

&lt;p&gt;This one is directionally biased, and the bias is not subtle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Losers get held longer than they should.&lt;/strong&gt; A position that would have been stopped out keeps living until the next signal happens to fire. Sometimes it recovers. Sometimes it recovers &lt;em&gt;because&lt;/em&gt; you were not checking — which is not a strategy, it is a delay.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The win rate inflates.&lt;/strong&gt; My original run reported a &lt;strong&gt;74.1% win rate&lt;/strong&gt;. Fixing the call site moved it by &lt;strong&gt;6 to 8 points&lt;/strong&gt;. That is the difference between "this looks like a professional system" and "this looks like a coin flip with costs".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It shows up as fewer trades reaching their stop.&lt;/strong&gt; If you log exits, you will see stop-loss exits that cluster in time — they fire in bursts on signal bars instead of being spread evenly. That is the fingerprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mechanism is worth stating plainly: &lt;strong&gt;you were not simulating a strategy, you were simulating a strategy plus a free option to defer every adverse outcome.&lt;/strong&gt; That option is worth a lot, and it is worth it in the direction you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find this in your own code
&lt;/h2&gt;

&lt;p&gt;Three checks, in increasing order of effort:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Count exits by type over time.&lt;/strong&gt; If stop-outs cluster on the same bars as entries, your exits are gated by the signal path. They should be roughly uniform in time, driven by price, not by signal timing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Search for the phrase you are about to write.&lt;/strong&gt; In almost every backtester, exits end up inside a function the entry path also calls. Grep for &lt;code&gt;execute&lt;/code&gt;, &lt;code&gt;handle_bar&lt;/code&gt;, &lt;code&gt;on_bar&lt;/code&gt;, &lt;code&gt;step&lt;/code&gt; — and ask, for each call site, "does this run on every bar or only sometimes?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Print the bar index of every exit and every entry, then diff.&lt;/strong&gt; If the sets of indices overlap far more than chance, exits are coupled to signals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The structural fix beats the test.&lt;/strong&gt; Make exit management a function that is called &lt;strong&gt;unconditionally, from the main loop, once per bar&lt;/strong&gt;, and make it impossible to call it from the signal branch. If the API shape permits the bug, the bug will come back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost me
&lt;/h2&gt;

&lt;p&gt;The strategy was negative before the fix. That is the uncomfortable part: this bug &lt;strong&gt;kept a losing strategy alive&lt;/strong&gt; for months. If I had run it honestly I would have dropped it, and I would have been right to.&lt;/p&gt;

&lt;p&gt;Bugs that make results worse get found. Bugs that make results better get &lt;strong&gt;shipped&lt;/strong&gt;. This is the structural asymmetry of backtesting: your attention is drawn to things that look broken, and a false positive looks like progress.&lt;/p&gt;

&lt;p&gt;Which is why the check has to be mechanical instead of attentional. I could not have caught this by staring harder at the strategy — the strategy was fine. I caught it by asking a question that had nothing to do with signals: &lt;em&gt;where is this function called from?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The general shape
&lt;/h2&gt;

&lt;p&gt;This belongs to a family I keep running into: &lt;strong&gt;the backtester's control flow is itself a modelling assumption.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You think you are modelling a strategy. You are actually modelling a strategy &lt;em&gt;inside a simulation harness&lt;/em&gt;, and the harness decides when your code gets to see the world. Every asymmetry in that decision — which bars you get called on, whether you see a bar before or after it closes, whether the exit path is privileged — silently becomes part of the strategy.&lt;/p&gt;

&lt;p&gt;So when an audit turns up nothing in the strategy, the next place to look is the harness. Mine was wrong in a way no amount of strategy review could have found.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The call-site check is one of the static checks in a small open-source linter (&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT, no dependencies). It can flag exits that cluster with entries; it cannot read your control flow, which is why the structural fix matters more than the check.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;There is also a *&lt;/em&gt;$0** download on Gumroad that bundles the tool with the demo&lt;br&gt;
logs, if you want something to test against before pointing it at your own data —&lt;br&gt;
name your price, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>backtesting</category>
      <category>python</category>
      <category>debugging</category>
    </item>
    <item>
      <title>"Enough Trades" Is Not a Number, It Is a Ratio</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Thu, 29 Oct 2026 01:00:04 +0000</pubDate>
      <link>https://dev.to/wei-backtest/enough-trades-is-not-a-number-it-is-a-ratio-40dh</link>
      <guid>https://dev.to/wei-backtest/enough-trades-is-not-a-number-it-is-a-ratio-40dh</guid>
      <description>&lt;p&gt;&lt;em&gt;Two configs, same strategy, same data. One becomes provable in under a year. The other cannot be proven in a working lifetime.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;"Am I overfitting?" gets all the attention. The more common problem is quieter and harder to fix: &lt;strong&gt;you do not have enough data to tell, and no amount of cleverness changes that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had two configurations of the same breakout strategy. Same code, same decade, different instrument and parameter set.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;trades&lt;/th&gt;
&lt;th&gt;per-trade vol&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gold, k=0.75, 1:1&lt;/td&gt;
&lt;td&gt;2,352&lt;/td&gt;
&lt;td&gt;0.75%&lt;/td&gt;
&lt;td&gt;2.93&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTC, k=0.50, 4R&lt;/td&gt;
&lt;td&gt;1,506&lt;/td&gt;
&lt;td&gt;2.16%&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both looked reasonable. Both had positive expectancy. One was nearly provable; the other was not going to be provable at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The formula
&lt;/h2&gt;

&lt;p&gt;There is a closed form for the sample size you need to reach a given t-statistic, and it has no free parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;n = (3 × per-trade volatility ÷ per-trade edge)²
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it as: the number of trades you need is the square of three divided by your &lt;strong&gt;signal-to-noise ratio&lt;/strong&gt;. Not "how many trades did I run". Not "how many years of data". The ratio.&lt;/p&gt;

&lt;p&gt;That is why "I have 1,500 trades, that's plenty" is not a statement about evidence. 1,500 trades with noisy returns is worth less than 300 with clean ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Applying it
&lt;/h2&gt;

&lt;p&gt;From the table, the observed t and n give the ratio directly: &lt;code&gt;edge/vol = t / sqrt(n)&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;t&lt;/th&gt;
&lt;th&gt;n&lt;/th&gt;
&lt;th&gt;edge/vol&lt;/th&gt;
&lt;th&gt;trades needed&lt;/th&gt;
&lt;th&gt;still needed&lt;/th&gt;
&lt;th&gt;at this rate&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gold&lt;/td&gt;
&lt;td&gt;2.93&lt;/td&gt;
&lt;td&gt;2,352&lt;/td&gt;
&lt;td&gt;0.060&lt;/td&gt;
&lt;td&gt;2,466&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;114&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~0.9 yr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BTC&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;td&gt;1,506&lt;/td&gt;
&lt;td&gt;0.028&lt;/td&gt;
&lt;td&gt;11,202&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9,696&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~37 yr&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjcrssii8fyav3e6rocr9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjcrssii8fyav3e6rocr9.png" alt="Sample size is a ratio" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gold needs about nine more months of trading. BTC needs thirty-seven years — and that assumes the edge and the volatility both stay exactly where they are, which is the optimistic case.&lt;/p&gt;

&lt;p&gt;Both are the same strategy. The difference is that BTC's per-trade volatility is &lt;strong&gt;2.9×&lt;/strong&gt; gold's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why noise dominates silently
&lt;/h2&gt;

&lt;p&gt;Look at the right panel of that figure: &lt;code&gt;n&lt;/code&gt; grows with the &lt;em&gt;square&lt;/em&gt; of the inverse ratio. Halve your signal-to-noise and you need four times the data. Drop it to a third and you need nine times.&lt;/p&gt;

&lt;p&gt;BTC's ratio is less than half of gold's, so the requirement is roughly &lt;code&gt;(0.060/0.028)² ≈ 4.5×&lt;/code&gt; more trades — and BTC also produces more trades per year, so the year count should improve. It does not, because the ratio entered as a square while the trade frequency is only linear. Frequency cannot outrun a squared penalty.&lt;/p&gt;

&lt;p&gt;This is why a strategy with a great-looking annualised return can be the &lt;strong&gt;worst&lt;/strong&gt; candidate for deployment. BTC had the highest annualised figure of anything I tested and the weakest evidence by a wide margin. The return was real in the sample. The evidence that it would repeat was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I do differently now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Compute the required n before the strategy is finished.&lt;/strong&gt; If the answer is thirty-seven years, I want to know that on day one, not after six months of work. The calculation takes one line and it has changed which projects I start.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Treat "needs a lifetime" as a rejection.&lt;/strong&gt; Not "promising but unproven". If the required sample is longer than the instrument's plausible lifespan, the hypothesis is &lt;strong&gt;unfalsifiable in practice&lt;/strong&gt;, and an unfalsifiable strategy is indistinguishable from a random one for as long as I would be trading it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Prefer high signal-to-noise instruments for a first test.&lt;/strong&gt; Same logic as the gold/BTC split: if you must prove something, prove it where the arithmetic permits it. Testing on the noisiest instrument first is the most expensive possible ordering — you spend the most time and learn the least.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Report n and the ratio, not just the t.&lt;/strong&gt; A t-statistic alone hides whether the result is 300 clean trades or 3,000 noisy ones. The ratio makes the difference visible in one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable corollary
&lt;/h2&gt;

&lt;p&gt;Some of the things I believed in earlier were not tested — they were &lt;strong&gt;untestable&lt;/strong&gt;, and I mistook the absence of proof for the presence of evidence.&lt;/p&gt;

&lt;p&gt;That is a worse error than a bug, because a bug can be fixed by editing a line. An unfalsifiable hypothesis can only be abandoned, and abandoning it costs whatever I already invested in it.&lt;/p&gt;

&lt;p&gt;The formula is unforgiving in a useful way. It does not care how much I like the strategy, or how plausible the story is. It just says: with this much noise and this much edge, you need this much data. If you cannot get the data, you cannot get the answer.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The fold, sizing and selection helpers, plus this arithmetic, are in a small open-source kit (&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT, no dependencies). If you have a config where the required n came out short and the strategy still failed, that is the case I want to hear about — it means the ratio was not the binding constraint.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you would rather have this as a download than a repo: the same tool, plus the&lt;br&gt;
demo trade logs (one with five bugs planted, one clean), is *&lt;/em&gt;$0** on Gumroad —&lt;br&gt;
pay what you want, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>trading</category>
      <category>backtesting</category>
      <category>python</category>
    </item>
    <item>
      <title>After 455 Parameter Sets, a t-Stat of 2.18 Means Nothing</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:44:56 +0000</pubDate>
      <link>https://dev.to/wei-backtest/after-455-parameter-sets-a-t-stat-of-218-means-nothing-k3n</link>
      <guid>https://dev.to/wei-backtest/after-455-parameter-sets-a-t-stat-of-218-means-nothing-k3n</guid>
      <description>&lt;p&gt;&lt;em&gt;The significance test you learned assumes you ran one test. You ran hundreds.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I had a strategy with a t-statistic of &lt;strong&gt;2.18&lt;/strong&gt; on 400 trades.&lt;/p&gt;

&lt;p&gt;Under the textbook reading, that is significant. The conventional threshold for 5% two-sided is about 1.96, and 2.18 clears it. I had spent weeks on the parameter grid before that number appeared, evaluating roughly &lt;strong&gt;455 parameter sets&lt;/strong&gt; along the way.&lt;/p&gt;

&lt;p&gt;So I asked the question that killed it: &lt;em&gt;if all 455 of those sets had zero edge, how big would the best of them look?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The test nobody runs
&lt;/h2&gt;

&lt;p&gt;The standard t-test answers: "if the true mean were zero, how often would a sample like mine show a t of 2.18 or larger?" That question is conditional on &lt;strong&gt;one&lt;/strong&gt; sample.&lt;/p&gt;

&lt;p&gt;I did not take one sample. I took 455, threw away 454 of them, and kept the largest. The quantity I am actually looking at is not a t-statistic from one draw — it is the &lt;strong&gt;maximum of 455 t-statistics&lt;/strong&gt;. The distribution of a maximum is nothing like the distribution of a single value.&lt;/p&gt;

&lt;p&gt;The arithmetic rule of thumb people quote is: expected false positives = &lt;code&gt;N × α&lt;/code&gt;. With 455 sets at 5%, that would predict about 22.8 sets "significant" by luck. That number is right about the &lt;em&gt;count&lt;/em&gt; and useless about the &lt;em&gt;threshold&lt;/em&gt;, because it does not tell you how big the biggest one gets.&lt;/p&gt;

&lt;p&gt;For that, simulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The simulation
&lt;/h2&gt;

&lt;p&gt;I generated 455 strategies, each with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;exactly zero edge (mean 0)&lt;/li&gt;
&lt;li&gt;400 trades, same volatility as mine&lt;/li&gt;
&lt;li&gt;the same t-statistic formula&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then recorded the &lt;strong&gt;maximum&lt;/strong&gt; t across the 455, and repeated that 2,000 times. Everything below is real output, not a sketch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftoyl3pp48mznt4zatn8r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftoyl3pp48mznt4zatn8r.png" alt="Best-of-455 null distribution" width="800" height="417"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Best-of-455 t under the null:
  median   2.99
  p90      3.51
  p95      3.71
  p99      ~4.0

P(best-of-455 t &amp;gt;= your 2.18) = 0.999
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The median best-of-455 t under &lt;strong&gt;pure zero edge&lt;/strong&gt; is &lt;strong&gt;2.99&lt;/strong&gt;. My 2.18 did not clear the median. It was below what search produces on a normal day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the bar moves so much
&lt;/h2&gt;

&lt;p&gt;The maximum of N draws grows, but slowly — roughly like &lt;code&gt;sqrt(2 log N)&lt;/code&gt; for well-behaved tails. Two consequences worth internalising:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The usual t = 3 bar is not conservative enough when N is in the hundreds.&lt;/strong&gt; People treat t = 3 as "strict". Under a 455-wide search at my sample size, the &lt;em&gt;median&lt;/em&gt; best is 2.99. You would need to clear something like the 95th percentile — 3.71 — to say anything at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The bar depends on your sample size.&lt;/strong&gt; More trades per strategy tightens the single-test distribution but does not shrink the selection effect; with 400 trades the null max still sits near 3. The two effects do not cancel, and it is easy to add trades while also adding grid points, netting out to nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest accounting
&lt;/h2&gt;

&lt;p&gt;Here is what I do now, and it is mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write down N before searching.&lt;/strong&gt; Not after. If I planned 20 sets and ended up evaluating 455, the number I report is 455.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute the selection-adjusted p-value&lt;/strong&gt;, not the naive one. Simulate the null (or use a formula) and ask what the best of N looks like at my sample size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report N alongside the t-statistic, every time.&lt;/strong&gt; A t-statistic without N is not interpretable once you have searched.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The third one is the cheapest and it is the one nobody does. "t = 2.18" sounds like a result. "t = 2.18 after 455 evaluations, where the null median is 2.99" is a different sentence entirely, and both are one line.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not mean
&lt;/h2&gt;

&lt;p&gt;It does &lt;strong&gt;not&lt;/strong&gt; mean the strategy is bad. It means the evidence I had was insufficient to distinguish it from noise produced by searching.&lt;/p&gt;

&lt;p&gt;Those are different conclusions with different next actions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Conclusion&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"The strategy has no edge"&lt;/td&gt;
&lt;td&gt;Drop it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The evidence is insufficient"&lt;/td&gt;
&lt;td&gt;Collect more data, or search less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"The edge is real but unproven"&lt;/td&gt;
&lt;td&gt;Paper-trade with small size while accumulating&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Mine was the second. The honest move was to stop adding filters (each filter is a new search, and it would need its own out-of-sample) and instead increase trades per strategy while holding the grid fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable arithmetic
&lt;/h2&gt;

&lt;p&gt;Every additional parameter set you evaluate buys you a slightly higher chance of a good-looking result, and charges you a slightly higher bar to clear. Early on, the first effect dominates: going from 1 to 10 sets is clearly worth it, you learn something. Later, the second dominates: going from 400 to 455 mostly just raises the bar.&lt;/p&gt;

&lt;p&gt;There is a crossover, and it is usually much earlier than people think. Mine was somewhere in the low tens.&lt;/p&gt;

&lt;p&gt;The number of things you tried is not a detail of your process. It is &lt;strong&gt;a parameter of your test&lt;/strong&gt;, and leaving it out is how a search gets reported as a discovery.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The simulation is part of a small open-source kit (&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT, no dependencies): &lt;code&gt;backtest_kit.py selection --tried N&lt;/code&gt; runs the null for your own N and sample size. It reports a probability, not a verdict — the verdict is yours.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you would rather have this as a download than a repo: the same tool, plus the&lt;br&gt;
demo trade logs (one with five bugs planted, one clean), is *&lt;/em&gt;$0** on Gumroad —&lt;br&gt;
pay what you want, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>trading</category>
      <category>backtesting</category>
      <category>python</category>
    </item>
    <item>
      <title>My Walk-Forward Said +29.2 Per Trade. Honest Out-of-Sample Said -4.6.</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:44:29 +0000</pubDate>
      <link>https://dev.to/wei-backtest/my-walk-forward-said-292-per-trade-honest-out-of-sample-said-46-km2</link>
      <guid>https://dev.to/wei-backtest/my-walk-forward-said-292-per-trade-honest-out-of-sample-said-46-km2</guid>
      <description>&lt;p&gt;&lt;em&gt;A parameter set that won on folds 1-3 lost on folds 4-5. Same strategy, same data.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I ran the standard walk-forward check. Three parameter sets, five sequential folds, about 400 trades. For each fold I picked the set that was best on the &lt;strong&gt;previous&lt;/strong&gt; folds only, then scored it on the fold it had never seen.&lt;/p&gt;

&lt;p&gt;Here is the whole output:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pvd3k0yemvpmmk519k3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pvd3k0yemvpmmk519k3.png" alt="Out-of-sample result per fold" width="800" height="426"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fold   picked      oos net    oos t
2      k0.75        436.5     0.88
3      k0.75        827.9     1.56
4      k0.75       -941.2    -2.51
5      k1.00       -813.2    -1.96

Stitched out-of-sample: n=107  net=-489.99  mean=-4.58  t=-0.51
In-sample best mean   :  +29.17 per trade
Optimism gap          :   33.75 per trade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameter set &lt;code&gt;k0.75&lt;/code&gt; looked best for three consecutive folds. It was the &lt;em&gt;worst&lt;/em&gt; set for the next two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;+29.17 per trade in sample. −4.58 out of sample.&lt;/strong&gt; The gap is 33.75 per trade, which is larger than the entire in-sample edge — the sign flipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "out-of-sample" actually means
&lt;/h2&gt;

&lt;p&gt;Most people treat out-of-sample as a split. You cut the data at some date, fit on the left, evaluate on the right, and call the right side honest.&lt;/p&gt;

&lt;p&gt;That is not what makes it honest. &lt;strong&gt;Out-of-sample is a procedure, not a file.&lt;/strong&gt; What makes it honest is that no choice in the evaluation was informed by the data being evaluated.&lt;/p&gt;

&lt;p&gt;Here is the distinction, concretely:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you did&lt;/th&gt;
&lt;th&gt;Is the score honest?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Optimised on 2015-2020, tested on 2021-2023, and you never looked at 2021-2023 while designing&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Optimised on 2015-2020, tested on 2021-2023, then went back and tweaked because 2021-2023 looked bad&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no&lt;/strong&gt; — that file is now in-sample&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picked the parameter that maximised the &lt;em&gt;whole&lt;/em&gt; period, then reported the second half&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;no&lt;/strong&gt; — the second half informed the pick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Picked on folds 1..i-1, scored on fold i, never revisited&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row is the one that catches people. If you choose a parameter using any statistic computed over the test period — total return, Sharpe, whether the equity curve "looked right" — the test period has leaked into the choice, and the score is in-sample no matter which file it lives in.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap is not a haircut
&lt;/h2&gt;

&lt;p&gt;The natural instinct is to treat the optimism gap as a discount: knock 20% off the in-sample number and move on.&lt;/p&gt;

&lt;p&gt;That does not work, because the gap is not proportional to the edge. It is roughly proportional to &lt;strong&gt;how much selection freedom you had&lt;/strong&gt;. In this example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;in-sample best mean: &lt;strong&gt;+29.17&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;honest out-of-sample: &lt;strong&gt;−4.58&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The gap (33.75) was &lt;em&gt;bigger than the edge&lt;/em&gt; (29.17). There was no discount that turns this back into a positive number, because the honest number is negative.&lt;/p&gt;

&lt;p&gt;When the gap exceeds the edge, what you measured was not a weak strategy. You measured the selection process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this happens
&lt;/h2&gt;

&lt;p&gt;Three mechanisms, all mundane:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Fold-to-fold variance is large.&lt;/strong&gt; A fold with 20-25 trades has a standard error of roughly &lt;code&gt;sd / sqrt(25)&lt;/code&gt;. If per-trade sd is around 100, the standard error of the mean is about 20 per trade. So a "best mean" of 29 is roughly one to two standard errors above zero — entirely reachable by luck. With three candidate sets, picking the top one at each step reliably picks the noisiest winner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Regime change is not announced.&lt;/strong&gt; The demo I used has the edge decaying after ~60% of the sample on purpose, which is a clean version of what real markets do more subtly. A parameter that suits the first regime is not obliged to suit the second.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The selection is repeated, not once.&lt;/strong&gt; You do not choose a parameter once. You choose at fold 2, again at fold 3, again at fold 4. Each choice is a fresh chance to pick the lucky one, and the errors compound across the stitched result.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to run this yourself
&lt;/h2&gt;

&lt;p&gt;You need one thing beyond a trade log: &lt;strong&gt;a column saying which parameter set produced each trade.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;entry&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;time&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;side&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;entry&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;exit&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;param&lt;/span&gt;&lt;span class="err"&gt;_&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt;
&lt;span class="ld"&gt;2024-01-02T09:30:00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;BUY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;100.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;101.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;k&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;
&lt;span class="ld"&gt;2024-01-03T14:05:00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;SELL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;101.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;100.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;k&lt;/span&gt;&lt;span class="mf"&gt;1.00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that column, the procedure is mechanical:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split trades into K sequential folds by time.&lt;/li&gt;
&lt;li&gt;For fold &lt;em&gt;i&lt;/em&gt; (starting at &lt;em&gt;i&lt;/em&gt;=2), compute each parameter set's mean on folds 1..&lt;em&gt;i&lt;/em&gt;−1.&lt;/li&gt;
&lt;li&gt;Pick the best one.&lt;/li&gt;
&lt;li&gt;Score it on fold &lt;em&gt;i&lt;/em&gt; only.&lt;/li&gt;
&lt;li&gt;Stitch the fold-&lt;em&gt;i&lt;/em&gt; results. That stitched series is your honest number.&lt;/li&gt;
&lt;li&gt;Compare it to the best in-sample mean. The difference is the gap.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python backtest_kit.py folds trades.csv &lt;span class="nt"&gt;--k&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the column, all you can do is split validation, which is weaker — it tells you about stability over time, not about selection. The tool says so rather than pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that stings
&lt;/h2&gt;

&lt;p&gt;I did not set out to fool myself. I had a parameter grid, I picked the winner on the development data, and I reported the number. That is the standard workflow, and it is exactly the workflow that produces this gap.&lt;/p&gt;

&lt;p&gt;The gap is not a sign of carelessness. It is the &lt;strong&gt;expected output&lt;/strong&gt; of selecting on noise. Which means the fix is not "be more careful" — it is to make the selection procedure mechanical and to report the stitched number, including when it is negative.&lt;/p&gt;

&lt;p&gt;The uncomfortable question is not "was my gap large". It is: &lt;strong&gt;how many strategies do I still believe in because I never computed this number for them?&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The fold walk and the gap calculation are in a small open-source kit (&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, MIT, no dependencies). If you have run this on your own grid and the gap came out smaller than the edge, I would like to see the setup — that is the interesting case.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;There is also a *&lt;/em&gt;$0** download on Gumroad that bundles the tool with the demo&lt;br&gt;
logs, if you want something to test against before pointing it at your own data —&lt;br&gt;
name your price, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>backtesting</category>
      <category>statistics</category>
      <category>python</category>
    </item>
    <item>
      <title>The One-Line Fix That Took My t-Stat From 6.22 to 1.04</title>
      <dc:creator>王伟</dc:creator>
      <pubDate>Wed, 21 Oct 2026 16:42:36 +0000</pubDate>
      <link>https://dev.to/wei-backtest/the-one-line-fix-that-took-my-t-stat-from-622-to-104-5ccl</link>
      <guid>https://dev.to/wei-backtest/the-one-line-fix-that-took-my-t-stat-from-622-to-104-5ccl</guid>
      <description>&lt;p&gt;&lt;em&gt;A breakout strategy passed all six parameter cells. It was a fill assumption, not an edge.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I had a breakout strategy on 1-minute data come in at &lt;strong&gt;t = 6.22&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Six parameter cells, all six passing. That is the kind of number that makes you start thinking about position size and funding an account. It is also the kind of number that should make you suspicious, because in ten years of doing this I have almost never seen a real edge survive a clean audit with a t-stat that high.&lt;/p&gt;

&lt;p&gt;It was not real. The strategy was fine. The &lt;strong&gt;fill assumption&lt;/strong&gt; was wrong, and it was wrong in exactly the direction that flatters you.&lt;/p&gt;

&lt;p&gt;Here is the whole story, because the same bug is probably sitting in a lot of backtesters, including yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;My breakout entry looked like this:&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;high&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&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;buy_level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buy_level&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buy_level&lt;/code&gt; came from the &lt;strong&gt;previous&lt;/strong&gt; session's high. The logic reads fine: price trades up through my level, I get filled at my level. That is what a limit order does.&lt;/p&gt;

&lt;p&gt;The problem is the gap. When the new session opens with a gap straight &lt;strong&gt;through&lt;/strong&gt; &lt;code&gt;buy_level&lt;/code&gt;, bar &lt;code&gt;j&lt;/code&gt; satisfies &lt;code&gt;high[j] &amp;gt;= buy_level&lt;/code&gt; on its very first tick — at the open. The open is already above my trigger, and I record a fill at &lt;code&gt;buy_level&lt;/code&gt;, a price that never traded in that session.&lt;/p&gt;

&lt;p&gt;I was gifting myself the gap. Systematically, on every gap-through day, in the direction that made the equity curve look better.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6ji582mrbzd87j0zcx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6ji582mrbzd87j0zcx1.png" alt="The fill that never traded" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;X&lt;/code&gt; is the recorded fill. The dotted line is that bar's low. The fill sits &lt;strong&gt;1.48 below the lowest price of the bar&lt;/strong&gt; — a price that, by construction, no one could have transacted at. The backtester did not complain, because nothing in the code knows what a plausible fill looks like.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One line:&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;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buy_level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# the worse of the two, for a long
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the bar opened above my trigger, I get the open — the price I could actually have paid. If it opened below, I get my trigger, as intended. Same for shorts with &lt;code&gt;min(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is the entire change. Not a filter, not a parameter, not a new feature.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9t8dpy9be2pb6vbnn8kh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9t8dpy9be2pb6vbnn8kh.png" alt="Before and after" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same trades, same period, same parameter grid. &lt;strong&gt;6.22 → 1.04.&lt;/strong&gt; Passing cells: &lt;strong&gt;6 of 6 → 0 of 6.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The entire edge lived in the fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is hard to catch
&lt;/h2&gt;

&lt;p&gt;Three reasons, and they compound.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It is plausible.&lt;/strong&gt; &lt;code&gt;entry = buy_level&lt;/code&gt; reads like correct code. It is the natural way to express "I got filled at my level". Nothing about it looks like a mistake, so code review slides right over it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It inflates results in the direction you want.&lt;/strong&gt; There is no error, no crash, no unusual log line. The only symptom is a nicer equity curve, which is precisely the thing you were hoping for. Your brain is not going to flag good news as a bug.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It hides in the gap.&lt;/strong&gt; If you trade a market that does not gap, you may never see it. It only appears on days where the open jumps past the trigger — and on those days it silently substitutes a favourable price for the one you would have got.&lt;/p&gt;

&lt;h2&gt;
  
  
  How often it actually bites
&lt;/h2&gt;

&lt;p&gt;I measured the share of entries where the recorded fill fell outside the bar's &lt;code&gt;[low, high]&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86e0nrulupf7ioxtjzxd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F86e0nrulupf7ioxtjzxd.png" alt="Gap fill share by instrument" width="799" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The pattern is exactly what you would expect: the more a market gaps overnight, the worse it gets. Index futures on a session-based breakout are the worst case — &lt;strong&gt;roughly one entry in six&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And note what this does to the interpretation of a backtest. On NQ, 17.4% of entries were priced at a level the market never traded. If your strategy's edge is on the order of a few basis points per trade, and one entry in six is being handed a free gap, then the measured edge is mostly the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found it
&lt;/h2&gt;

&lt;p&gt;Not by reading the code. I had read that code a dozen times.&lt;/p&gt;

&lt;p&gt;I found it by looking at &lt;strong&gt;when&lt;/strong&gt; the largest 1-minute moves happened. I ran a data-quality pass that printed the twelve biggest 1-minute jumps with their timestamps, and all twelve landed between &lt;strong&gt;18:00 and 18:05 New York time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That clustering was the tell. Twelve enormous 1-minute moves, all in a five-minute window, every time — those are not market moves. That is the session boundary. The "jumps" were cross-session gaps being recorded inside a session-labelled dataset.&lt;/p&gt;

&lt;p&gt;Once I was looking at gaps specifically, the fill assumption fell apart in about ten minutes.&lt;/p&gt;

&lt;p&gt;The general lesson here is worth more than the bug: &lt;strong&gt;when you audit a backtest, look at the time distribution of anomalies, not just their size.&lt;/strong&gt; A 2% one-minute move at a random time is a market event. A 2% one-minute move that always happens at 18:00 is a data-structure artefact, and it is telling you that your data is not shaped the way your code assumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this generalises to
&lt;/h2&gt;

&lt;p&gt;Fill assumptions are where backtests lie most often, because they are the one part of the simulation that has no external check. Your signal logic produces a number you can inspect. Your fill price is just... whatever you wrote.&lt;/p&gt;

&lt;p&gt;Three specific forms to look for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gap-through fills&lt;/strong&gt; — the one above. Trigger levels set from prior session data, filled at the trigger regardless of the open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look-ahead on exits&lt;/strong&gt; — if &lt;code&gt;execute()&lt;/code&gt; is only called on bars where a signal fired, then exits are only checked on those bars. I had this one too: fixing it moved a different strategy from &lt;strong&gt;−5.7% to +17.9%&lt;/strong&gt;, and inflated the win rate by 6–8 points. The original reported a 74.1% win rate; most of that was the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost assumptions borrowed from another asset class&lt;/strong&gt; — two futures strategies of mine were costed at 5bp per side. Actual round-trip cost was closer to 0.30bp (NQ) and 0.55bp (MGC). That is an 18–33× overstatement. Correcting it moved one of them from −94.0% to −45.5%. Rule of thumb: if &lt;code&gt;cost ÷ per-trade risk&lt;/code&gt; exceeds about 5%, suspect the cost model before you reject the strategy.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three checks you can run on your own log
&lt;/h2&gt;

&lt;p&gt;You do not need my code for these; you need your trade log and your price bars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is every fill inside its bar?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For each trade, find the bar containing the entry timestamp and check&lt;br&gt;
&lt;code&gt;low &amp;lt;= entry_price &amp;lt;= high&lt;/code&gt;. Anything outside never traded. This single check catches the gap-fill bug directly, and it is the highest-yield check I know.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Is the fill better than the bar's open?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For breakout entries, if a long fill is below the open, either a limit order filled later in the bar (fine, but then model it that way) or you have gifted yourself the gap (not fine). This one has false positives — limit orders legitimately fill better — so treat it as a prompt to look, not a verdict.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Do your anomalies cluster in time?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Print your largest N moves with timestamps and histogram the hour. If the biggest moves all sit at a fixed time of day, you are looking at session boundaries, and your session handling is probably wrong.&lt;/p&gt;
&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I turned the checks I now run into a small script, because doing them by hand every time is how you stop doing them. It is one file, standard library only, no pandas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python backtest_lint.py trades.csv &lt;span class="nt"&gt;--prices&lt;/span&gt; prices.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ynxxc0kjgr9vavu5dl8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0ynxxc0kjgr9vavu5dl8.png" alt="Real output" width="799" height="618"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That image is real output from the tool on a demo log that has five bugs deliberately planted in it. It scores 1/6 and names each one with the row number and the actual numbers. The same repo also ships a clean control that scores 6/6 — a checker that always says "fail" would be worthless, so both directions are testable before you trust it.&lt;/p&gt;

&lt;p&gt;Code: &lt;strong&gt;&lt;a href="https://github.com/wei498766-ai/backtest-lie-detector" rel="noopener noreferrer"&gt;https://github.com/wei498766-ai/backtest-lie-detector&lt;/a&gt;&lt;/strong&gt; — MIT, no dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable part
&lt;/h2&gt;

&lt;p&gt;The uncomfortable part is not that I had a bug. Everyone has bugs.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that &lt;strong&gt;the bug made the result better, and the result looked like a discovery&lt;/strong&gt;. I did not find this because I was careless. I found it because I got suspicious of a number that was too good, and then went looking for how it could be too good.&lt;/p&gt;

&lt;p&gt;That is not a scalable process. It depends on being suspicious at the right moment, about the right number — and the numbers you most want to believe are exactly the ones you are least likely to question.&lt;/p&gt;

&lt;p&gt;So the process has to be external. Cheap, mechanical checks that run whether or not you are in the mood to doubt yourself.&lt;/p&gt;

&lt;p&gt;That is the whole argument for this kind of tooling. Not that it makes you a better trader, but that it does not care what you want to be true.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you have hit something in this family — particularly anything where the backtester's assumption is invisible in the output — I would like to hear about it. The ones that do not throw errors are the ones worth collecting.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you would rather have this as a download than a repo: the same tool, plus the&lt;br&gt;
demo trade logs (one with five bugs planted, one clean), is *&lt;/em&gt;$0** on Gumroad —&lt;br&gt;
pay what you want, and $0 is a valid answer: &lt;a href="https://weiwave23.gumroad.com/l/backtest-lie-detector" rel="noopener noreferrer"&gt;https://weiwave23.gumroad.com/l/backtest-lie-detector&lt;/a&gt;*&lt;/p&gt;

</description>
      <category>trading</category>
      <category>backtesting</category>
      <category>python</category>
      <category>quant</category>
    </item>
  </channel>
</rss>
