<?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: Northmark</title>
    <description>The latest articles on DEV Community by Northmark (@northmark).</description>
    <link>https://dev.to/northmark</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%2F4037859%2Feb8fcdd5-95f2-4f5f-b677-9afd550818ac.png</url>
      <title>DEV Community: Northmark</title>
      <link>https://dev.to/northmark</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/northmark"/>
    <language>en</language>
    <item>
      <title>My trading system has a 73.5% win rate and loses money. Here is the diagnostic that found it.</title>
      <dc:creator>Northmark</dc:creator>
      <pubDate>Mon, 20 Jul 2026 11:17:51 +0000</pubDate>
      <link>https://dev.to/northmark/my-trading-system-has-a-735-win-rate-and-loses-money-here-is-the-diagnostic-that-found-it-2faa</link>
      <guid>https://dev.to/northmark/my-trading-system-has-a-735-win-rate-and-loses-money-here-is-the-diagnostic-that-found-it-2faa</guid>
      <description>&lt;p&gt;I spent a few days auditing a live FX system I built, over 16 years of history. The result was negative in a way I did not expect, and the diagnostic that found it is general enough to be worth writing up — it applies to any parameter search, not just trading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;17 configurations, 4-hour entry bars, exits simulated on 1-hour bars, 2010-05-28 .. 2026-07-08 (16.1y), 7641 trades. Crucially I fed the &lt;em&gt;unmodified production functions&lt;/em&gt; historical CSV instead of a live data feed, so the code path being tested is the code path that runs live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: the simulation charged costs to the wrong place
&lt;/h2&gt;

&lt;p&gt;Platform OHLC bars are BID prices. The backtest placed entry at the bar close, put the take-profit and stop-loss at close ± n·ATR, checked touches against bid highs and lows, then subtracted the spread &lt;strong&gt;from the final P&amp;amp;L&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Live, a long position fills at the ASK, the bracket sits relative to that ask, and it closes on the BID. So live needs price to travel one extra spread to reach the target, and reaches the stop one spread sooner.&lt;/p&gt;

&lt;p&gt;Subtracting cost from the &lt;em&gt;result&lt;/em&gt; is not the same as charging it to the &lt;em&gt;trigger&lt;/em&gt;. The first changes how much you win. The second changes &lt;strong&gt;which trades win at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Measured: &lt;strong&gt;2–5 percentage points of win rate&lt;/strong&gt;, always against you.&lt;/p&gt;

&lt;p&gt;This generalises: if your simulation applies costs as a post-hoc adjustment rather than modelling the mechanism that generates them, your event counts are wrong, not just your totals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: the win rate was a design choice, not evidence
&lt;/h2&gt;

&lt;p&gt;Targets were 0.5–0.8×ATR, stops 1.5–2.0×ATR. That is a reward:risk of 0.25–0.4, which fixes the breakeven win rate by arithmetic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;breakeven = SL / (TP + SL) = 75–82%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Out-of-sample: &lt;strong&gt;4947 trades, 73.5% win rate, -4669.9 pips, -0.944 pips per trade.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 73.5% win rate that loses money is not a paradox. Small targets hit often. That is what small targets do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The diagnostic worth stealing
&lt;/h2&gt;

&lt;p&gt;Before re-optimising anything, I asked whether the parameter surface was &lt;strong&gt;learnable at all&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For each walk-forward fold: evaluate the &lt;em&gt;entire&lt;/em&gt; parameter grid on the training window and on the test window, then take the Spearman rank correlation between the two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if rho &amp;gt; 0  -&amp;gt; training rank predicts test rank; selection is meaningful
if rho ~ 0  -&amp;gt; the surface is noise; NO selection rule can help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That second line is the valuable part. It separates &lt;em&gt;"my optimiser is bad"&lt;/em&gt; from &lt;em&gt;"there is nothing here"&lt;/em&gt; — two situations that look identical from the outside and demand opposite responses.&lt;/p&gt;

&lt;p&gt;Result across 119 folds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;median rho: &lt;strong&gt;-0.024&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;mean rho: -0.03, 95% CI [-0.09, 0.03] — &lt;strong&gt;includes zero&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;folds with rho &amp;gt; 0: 47.9% — a coin flip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Zero information. Consistent with that, walk-forward optimisation with plateau smoothing produced &lt;em&gt;worse&lt;/em&gt; out-of-sample results than the parameters I never touched, and selected the grid boundary in 11 of 17 configs — the signature of an optimiser with nothing to grip.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I checked whether the effect exists anywhere
&lt;/h2&gt;

&lt;p&gt;38 instruments, non-overlapping holding periods, volatility-scaled, pooled by asset class so 1,140 tests collapse to 6, split first-half vs second-half:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;asset class&lt;/th&gt;
&lt;th&gt;Sharpe @0bp&lt;/th&gt;
&lt;th&gt;breakeven cost&lt;/th&gt;
&lt;th&gt;1st half&lt;/th&gt;
&lt;th&gt;2nd half&lt;/th&gt;
&lt;th&gt;stable&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FX major&lt;/td&gt;
&lt;td&gt;+0.08&lt;/td&gt;
&lt;td&gt;3bp&lt;/td&gt;
&lt;td&gt;+0.06&lt;/td&gt;
&lt;td&gt;-0.09&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FX cross&lt;/td&gt;
&lt;td&gt;+0.02&lt;/td&gt;
&lt;td&gt;1bp&lt;/td&gt;
&lt;td&gt;-0.05&lt;/td&gt;
&lt;td&gt;-0.06&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Index&lt;/td&gt;
&lt;td&gt;+0.07&lt;/td&gt;
&lt;td&gt;5bp&lt;/td&gt;
&lt;td&gt;+0.04&lt;/td&gt;
&lt;td&gt;+0.02&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Metal&lt;/td&gt;
&lt;td&gt;+0.12&lt;/td&gt;
&lt;td&gt;12bp&lt;/td&gt;
&lt;td&gt;+0.13&lt;/td&gt;
&lt;td&gt;+0.03&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Energy&lt;/td&gt;
&lt;td&gt;+0.13&lt;/td&gt;
&lt;td&gt;23bp&lt;/td&gt;
&lt;td&gt;+0.15&lt;/td&gt;
&lt;td&gt;-0.02&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crypto&lt;/td&gt;
&lt;td&gt;+0.42&lt;/td&gt;
&lt;td&gt;100bp&lt;/td&gt;
&lt;td&gt;+0.54&lt;/td&gt;
&lt;td&gt;+0.15&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;Two things I would not have guessed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Breakeven transaction cost is more decision-useful than Sharpe.&lt;/strong&gt; FX majors break even at ~3bp and crosses at ~1bp — both &lt;em&gt;below&lt;/em&gt; the actual spread. The question "is there an edge" is settled before you write any strategy code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The significant result was the fake one.&lt;/strong&gt; Energy shows t = 3.73 on the full sample. It is flat-to-negative in the second half. Without the split it would have looked like the best finding in the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual lesson
&lt;/h2&gt;

&lt;p&gt;Build the falsifier before the strategy.&lt;/p&gt;

&lt;p&gt;The reason a long series of configurations looked "validated" is that no component in the pipeline had the job of saying &lt;em&gt;nothing here&lt;/em&gt;. Every part was designed to find something, so it found something, every time.&lt;/p&gt;

&lt;p&gt;The rank-correlation test above took an afternoon and invalidated months of parameter work — including the one lead I was most confident about. That is the test doing its job, and it is the cheapest insurance I have added to a research process in a long time.&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>finance</category>
      <category>algorithms</category>
    </item>
  </channel>
</rss>
