<?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: Pavel Kazantsev</title>
    <description>The latest articles on DEV Community by Pavel Kazantsev (@pavel_kkkkazantsev).</description>
    <link>https://dev.to/pavel_kkkkazantsev</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%2F4134503%2Ff205ee49-57b0-4bad-9418-b47e90ef04a5.png</url>
      <title>DEV Community: Pavel Kazantsev</title>
      <link>https://dev.to/pavel_kkkkazantsev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pavel_kkkkazantsev"/>
    <language>en</language>
    <item>
      <title>My crypto backtest had a sharpe of 13.4. Then I added trading costs</title>
      <dc:creator>Pavel Kazantsev</dc:creator>
      <pubDate>Wed, 23 Sep 2026 19:53:44 +0000</pubDate>
      <link>https://dev.to/pavel_kkkkazantsev/my-crypto-backtest-had-a-sharpe-of-134-then-i-added-trading-costs-5718</link>
      <guid>https://dev.to/pavel_kkkkazantsev/my-crypto-backtest-had-a-sharpe-of-134-then-i-added-trading-costs-5718</guid>
      <description>&lt;p&gt;I was not trying to prove a grand theory about crypto. I was testing simple, falsifiable baselines and looking for one result worth a second experiment.&lt;/p&gt;

&lt;p&gt;Cross-sectional reversal looked almost too clean: Sharpe &lt;code&gt;13.44&lt;/code&gt; before transaction costs, with positive returns in &lt;code&gt;67 / 76&lt;/code&gt; reported out-of-sample windows. Then I charged the same signal for the trading it required. At &lt;code&gt;10 bps&lt;/code&gt; per side, net Sharpe fell to &lt;code&gt;-40.13&lt;/code&gt;, and not one of those 76 windows remained positive.&lt;/p&gt;

&lt;p&gt;That is not a typo. I genuinely did not see it coming — the pre-cost numbers looked clean enough that I spent twenty minutes checking whether I had made an error somewhere before I accepted the result. It is what can happen when a short-lived pattern asks for a new portfolio every 15 minutes.&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%2F7c3rvxpqurwa6rmw4owm.jpg" 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%2F7c3rvxpqurwa6rmw4owm.jpg" alt="The HFM cost and funding implementation open during the experiment" width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The less glamorous part of the experiment: execution delay, turnover, fees, slippage, and funding inside the same evaluation path.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually tested
&lt;/h2&gt;

&lt;p&gt;This article covers one intentionally narrow reproduction of H3, a cross-sectional reversal baseline, on cached Bybit USDT perpetual data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;15-minute bars from January 2021 to the pre-lockbox boundary in September 2025;&lt;/li&gt;
&lt;li&gt;an eight-bar, or two-hour, return signal;&lt;/li&gt;
&lt;li&gt;90-day formation periods followed by 21-day OOS periods;&lt;/li&gt;
&lt;li&gt;76 non-overlapping OOS windows, rolled every 21 days;&lt;/li&gt;
&lt;li&gt;causal volatility targeting at 10% annualized volatility, using a 96-bar trailing estimate and a 3× leverage cap;&lt;/li&gt;
&lt;li&gt;a sealed lockbox beginning September 1, 2025, which was not opened for this article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A window counts as positive when the sum of its per-bar returns is greater than zero. That is the exact definition used by the evaluator for the window fractions below.&lt;/p&gt;

&lt;p&gt;The current cache contains &lt;code&gt;451&lt;/code&gt; symbol directories. That is not the same thing as a properly reconstructed point-in-time liquid universe. The panel uses the histories available in the cache, but the current reproduction does not apply a historical liquidity screen. Survivorship and availability bias therefore remain possible. This result is a development diagnostic, not evidence of a deployable alpha.&lt;/p&gt;

&lt;h2&gt;
  
  
  The signal was deliberately boring
&lt;/h2&gt;

&lt;p&gt;At each timestamp, H3 measures every available asset's return over the previous eight bars, standardizes those returns across the cross-section, and reverses the sign.&lt;/p&gt;

&lt;p&gt;Recent relative losers receive positive weights. Recent relative winners receive negative weights. The vector is scaled to unit gross exposure before portfolio-level volatility targeting.&lt;/p&gt;

&lt;p&gt;The core signal is short enough to inspect:&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;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lookback_bars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;long&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpivot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;symbols&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;variable_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;symbol&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;drop_nulls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;long&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="n"&gt;standard_deviation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;std&lt;/span&gt;&lt;span class="p"&gt;(),&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="nb"&gt;long&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with_columns&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard_deviation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mean&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standard_deviation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;gross_signal&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gross&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;with_columns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gross_signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signal&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;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gross_signal&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;otherwise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The production implementation also handles an empty cross-section and zero variance. No model is fitted here. There is no feature search hidden behind the result.&lt;/p&gt;

&lt;p&gt;A weight computed at time &lt;code&gt;t&lt;/code&gt; becomes live at &lt;code&gt;t+1&lt;/code&gt;. The evaluator enforces that delay centrally:&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;def&lt;/span&gt; &lt;span class="nf"&gt;live&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Expr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without the shift, the backtest would earn the same closing-bar return used to calculate the signal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number that made me stop
&lt;/h2&gt;

&lt;p&gt;With fees and slippage set to zero, the current run produced the following.&lt;/p&gt;

&lt;p&gt;I am not a professional quant. I run these experiments on my own time, publish the results as I go, and try to document what actually happens rather than what I hoped would happen. When I saw &lt;code&gt;13.44&lt;/code&gt; I let myself get briefly excited. That was a mistake.&lt;/p&gt;

&lt;p&gt;The zero-transaction-cost baseline still includes funding PnL and is not a pure gross-price-return series. Funding belongs to holding a perpetual position rather than transaction execution, so &lt;code&gt;13.44&lt;/code&gt; is the baseline Sharpe before fees and slippage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Zero-transaction-cost baseline&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Annualized Sharpe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;13.44&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive OOS windows&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;67 / 76&lt;/code&gt; (&lt;code&gt;88.2%&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maximum drawdown&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-11.62%&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Summed absolute turnover&lt;/td&gt;
&lt;td&gt;&lt;code&gt;29,451&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is the warning. A huge pre-cost Sharpe and huge turnover can be two descriptions of the same fragile result.&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%2Fozurgzf7d8d1yghpza40.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%2Fozurgzf7d8d1yghpza40.png" alt="The same signal before and after trading costs" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The comparison above is controlled: both bars come from the same code revision, data fingerprint, walk-forward windows, signal weights, and normalization. Only the cost model changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the backtest pay its bill
&lt;/h2&gt;

&lt;p&gt;Gross PnL uses execution-shifted weights. Turnover is the sum of absolute changes in live positions. Fees and slippage are fixed basis-point charges on turnover; funding is aligned separately with the long/short sign.&lt;/p&gt;

&lt;p&gt;The accounting identity is explicit:&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;fees&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fee_bps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;span class="n"&gt;slippage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;turnover&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;slippage_bps&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gross&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;fees&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;slippage&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;funding&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a rejection model, not an execution simulator. It does not model spread variation, order-book depth, queue position, partial fills, market impact, or adverse selection.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;6 bps&lt;/code&gt; fees plus &lt;code&gt;4 bps&lt;/code&gt; slippage per side, the same every-bar signal produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Net result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Annualized Sharpe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-40.13&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Positive OOS windows&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost / gross alpha&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4.00×&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Total return&lt;/td&gt;
&lt;td&gt;approximately &lt;code&gt;-100%&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I was hoping the cost model would hurt but leave something survivable. It did not. At this point I considered just not publishing — a Sharpe of &lt;code&gt;-40.13&lt;/code&gt; is not a great headline. But the result is real, the methodology is documented, and a clean failure is more useful to me than a buried one.&lt;/p&gt;

&lt;p&gt;The extreme Sharpe is less mysterious than it looks. A continuously refreshed cross-sectional portfolio creates persistent turnover. A relatively stable negative cost stream, annualized from 15-minute observations, can generate an absurdly negative ratio.&lt;/p&gt;

&lt;p&gt;The useful result is not the theatrical &lt;code&gt;-40.13&lt;/code&gt;. It is that the modeled trading bill was four times the gross PnL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Slowing the strategy down
&lt;/h2&gt;

&lt;p&gt;The obvious response was to hold weights longer. Under a deliberately pessimistic &lt;code&gt;15 bps&lt;/code&gt; per-side scenario, net Sharpe improved as turnover fell:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rebalance interval&lt;/th&gt;
&lt;th&gt;Net Sharpe&lt;/th&gt;
&lt;th&gt;Positive OOS windows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Every bar / 15 minutes&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-66.00&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every 16 bars / 4 hours&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-16.24&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0 / 76&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every 96 bars / 1 day&lt;/td&gt;
&lt;td&gt;&lt;code&gt;-4.45&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10 / 76&lt;/code&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%2Fn6w94js6ccj29c475iz8.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%2Fn6w94js6ccj29c475iz8.png" alt="Cost sensitivity as the signal is rebalanced less often" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The friendliest tested corner used daily rebalancing and &lt;code&gt;3 bps&lt;/code&gt; per side. It reached a Sharpe of &lt;code&gt;-0.01&lt;/code&gt;, with &lt;code&gt;39 / 76&lt;/code&gt; positive windows. A &lt;code&gt;7.5 bps&lt;/code&gt; daily scenario produced &lt;code&gt;-1.70&lt;/code&gt;, with &lt;code&gt;27 / 76&lt;/code&gt; positive windows.&lt;/p&gt;

&lt;p&gt;Slower trading removed much of the damage, but it did not reveal a robust net edge. The signal decayed while it waited. And the maker-style scenario is especially optimistic because this vectorized test says nothing about queue position or whether a passive order would actually fill.&lt;/p&gt;

&lt;h2&gt;
  
  
  What failed, and what did not
&lt;/h2&gt;

&lt;p&gt;The experiment does not establish that “crypto mean reverts.” It shows a strong pre-transaction-cost short-horizon reversal pattern inside this particular development sample and cache. It also shows that the naive implementation cannot capture that pattern after even simple modeled friction.&lt;/p&gt;

&lt;p&gt;Those are different conclusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Research lead:&lt;/strong&gt; relative two-hour moves contain a repeatable pre-cost pattern in this sample.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed strategy:&lt;/strong&gt; resizing the book continuously consumes more than the pattern earns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open engineering problem:&lt;/strong&gt; reduce turnover without waiting so long that the signal disappears.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That leaves specific follow-ups: threshold entries, hysteresis bands, cost-aware sizing, historical universe reconstruction, and a fill model for passive execution. None is assumed to work. They are simply better questions than adding more indicators to the same high-turnover baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduction boundary
&lt;/h2&gt;

&lt;p&gt;The internal HFM repository reproduces the article slice with two commands once the market-data cache exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python scripts/reproduce_round1_h3_article.py
&lt;span class="nv"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src python scripts/make_article_h3_charts.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first command rebuilds the panel and 76 walk-forward windows, evaluates all seven cost/rebalance scenarios, and writes machine-readable results. It also records the SHA-256 fingerprint of &lt;code&gt;1,442&lt;/code&gt; cached files (&lt;code&gt;880,708,217&lt;/code&gt; bytes), the hashes of every relevant source module, and the Python and Polars versions. The second command creates both figures directly from that result file.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/icesurf666/quant-research-notes" rel="noopener noreferrer"&gt;public repository&lt;/a&gt; contains the article, result snapshot, figures, minimal signal/cost code, deterministic tests, and a manifest that fails CI if any frozen artifact drifts. It does not distribute the market-data cache, so a reader can reproduce the mechanics and verify the published evidence package, but cannot independently regenerate the historical metrics without sourcing the data.&lt;/p&gt;

&lt;p&gt;Before treating the numbers as anything stronger than development evidence, keep four limits attached:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The cached-symbol universe is not a point-in-time liquidity universe and may contain survivorship or availability bias.&lt;/li&gt;
&lt;li&gt;Trading costs are fixed-bps assumptions, not observed live fills; maker scenarios omit queue and fill risk.&lt;/li&gt;
&lt;li&gt;The sealed lockbox was not evaluated, and these are backtest results rather than live returns.&lt;/li&gt;
&lt;li&gt;The 76 OOS windows do not overlap, but they are not 76 independent market regimes.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;I started the experiment hoping to find an edge. What survived was a better research rule: turnover belongs inside the hypothesis, not in the cleanup after a backtest looks good.&lt;/p&gt;

&lt;p&gt;A pre-cost Sharpe is not a strategy. It is a claim before execution. The strategy begins with what remains after the position delay, turnover, fees, slippage, funding, universe construction, and untouched data have all had their turn.&lt;/p&gt;

&lt;p&gt;In this run, almost nothing remained.&lt;/p&gt;

&lt;p&gt;If you have watched a promising backtest collapse once execution entered the model, what did the damage: turnover, spread, fill probability, market impact, or something less obvious?&lt;/p&gt;

&lt;p&gt;Results file SHA-256: &lt;a href="https://github.com/icesurf666/quant-research-notes/blob/main/studies/round1-cross-sectional-reversal/evidence/results.json" rel="noopener noreferrer"&gt;&lt;code&gt;fd1f506549b8f43ea33029acb3da760dbc88478f12f0122cf980bc725e6e85a9&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>datascience</category>
      <category>quant</category>
      <category>web3</category>
    </item>
  </channel>
</rss>
