<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>回测 on 远思码技术实验室</title><link>https://yuansima.com/tags/%E5%9B%9E%E6%B5%8B/</link><description>Recent content in 回测 on 远思码技术实验室</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Mon, 19 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://yuansima.com/tags/%E5%9B%9E%E6%B5%8B/index.xml" rel="self" type="application/rss+xml"/><item><title>双均线与动量轮动策略实战：参数稳健性比收益率更重要</title><link>https://yuansima.com/p/ma-momentum-rotation-strategy/</link><pubDate>Mon, 19 Oct 2026 21:00:00 +0800</pubDate><guid>https://yuansima.com/p/ma-momentum-rotation-strategy/</guid><description>&lt;p&gt;&lt;em&gt;远思为径，码动价值。欢迎来到远思码技术实验室的最新技术实践记录。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;有了&lt;a class="link" href="https://yuansima.com/p/vectorized-backtest-engine/" &gt;回测引擎&lt;/a&gt;，第一件想做的事一定是&amp;quot;跑一个看起来能赚钱的策略&amp;quot;。实验室也不例外，但我们刻意选了&lt;strong&gt;最简单、最没有故事性&lt;/strong&gt;的两个策略来做基准，因为它们最不容易过拟合，也最容易暴露研究流程里的问题。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重要声明：本文所有回测结果均为实验室内部示例，使用的历史区间有限、未考虑全部交易限制，不构成任何投资建议。&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="一策略-a双均线择时"&gt;一、策略 A：双均线择时
&lt;/h2&gt;&lt;p&gt;逻辑烂大街：短均线上穿长均线满仓，下穿空仓。它的价值不在于赚钱，而在于给我们一个&amp;quot;&lt;strong&gt;基准尺子&lt;/strong&gt;&amp;quot;——任何复杂策略如果连它都打不过，就没必要继续。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# strategies/ma_cross.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ma_cross_signal&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, fast: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, slow: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;返回目标仓位序列：1 满仓 / 0 空仓。只在收盘后生成，次日生效。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ma_fast &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(fast)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ma_slow &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(slow)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sig &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (ma_fast &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; ma_slow)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;astype(float)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 均线未成型期间不持仓&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sig[ma_slow&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isna()] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; sig
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;把它接到引擎上只需要构造 &lt;code&gt;target_w&lt;/code&gt; 矩阵：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# strategies/run_ma.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; backtest.engine &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BacktestConfig, run_backtest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; strategies.ma_cross &lt;span style="color:#f92672"&gt;import&lt;/span&gt; ma_cross_signal
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;run_ma_backtest&lt;/span&gt;(symbol: str, start: str, end: str,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fast: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, slow: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; dict:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;from&lt;/span&gt; src.data.loader &lt;span style="color:#f92672"&gt;import&lt;/span&gt; load_bars
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; load_bars([symbol], start, end, adjust&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;qfq&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pivot(index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, values&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;close&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pivot(index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, values&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct &lt;span style="color:#f92672"&gt;=&lt;/span&gt; load_bars([symbol], start, end, adjust&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;none&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pct&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pivot(index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, values&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;pct_chg&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sig &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ma_cross_signal(close[symbol], fast, slow)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame({symbol: sig}, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 引擎内部会做 shift(1)：sig 在 T 日收盘生成，T+1 日承担收益&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run_backtest(close, volume, target_w, pct_chg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;pct)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;metrics
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实验室以沪深 300 ETF（510300）为标的，2019-01 ~ 2025-06 的结果（示例）：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;参数 (fast, slow)&lt;/th&gt;
 &lt;th&gt;年化收益&lt;/th&gt;
 &lt;th&gt;夏普&lt;/th&gt;
 &lt;th&gt;最大回撤&lt;/th&gt;
 &lt;th&gt;年化换手&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;(20, 60)&lt;/td&gt;
 &lt;td&gt;7.2%&lt;/td&gt;
 &lt;td&gt;0.62&lt;/td&gt;
 &lt;td&gt;-18.4%&lt;/td&gt;
 &lt;td&gt;2.1 倍&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;(10, 30)&lt;/td&gt;
 &lt;td&gt;5.8%&lt;/td&gt;
 &lt;td&gt;0.49&lt;/td&gt;
 &lt;td&gt;-21.7%&lt;/td&gt;
 &lt;td&gt;4.6 倍&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;(30, 120)&lt;/td&gt;
 &lt;td&gt;6.9%&lt;/td&gt;
 &lt;td&gt;0.66&lt;/td&gt;
 &lt;td&gt;-16.2%&lt;/td&gt;
 &lt;td&gt;0.9 倍&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;买入持有&lt;/td&gt;
 &lt;td&gt;3.4%&lt;/td&gt;
 &lt;td&gt;0.28&lt;/td&gt;
 &lt;td&gt;-42.1%&lt;/td&gt;
 &lt;td&gt;0&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有价值的结论只有一个：&lt;strong&gt;择时把最大回撤从 -42% 压到 -18%&lt;/strong&gt;，代价是年化收益只从 3.4% 提到 7.2%。这才是双均线的真实贡献——它是个风险管理工具，不是收益引擎。&lt;/p&gt;
&lt;h2 id="二策略-betf-动量轮动"&gt;二、策略 B：ETF 动量轮动
&lt;/h2&gt;&lt;p&gt;第二个策略稍微进阶：在一个 ETF 池里按动量排名，只持有最强的几只。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# strategies/momentum_rotation.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; __future__ &lt;span style="color:#f92672"&gt;import&lt;/span&gt; annotations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; numpy &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; np
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;momentum_score&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, lookback: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vol_window: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;风险调整动量：过去 N 日收益 / 过去 N 日波动。截面排名用。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ret &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close &lt;span style="color:#f92672"&gt;/&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(lookback) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pct_change()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(vol_window)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;std()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; (ret &lt;span style="color:#f92672"&gt;/&lt;/span&gt; vol)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;replace([np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;inf, &lt;span style="color:#f92672"&gt;-&lt;/span&gt;np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;inf], np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;build_target_weights&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, score: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; top_k: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, rebalance: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; min_score: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;月末选动量最强的 top_k 只等权持有，其余现金。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; rebalance=&amp;#39;M&amp;#39; 表示只在每月最后一个交易日调仓，中间日权重保持不变。
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; idx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; rebalance &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 每月最后一个交易日。这里统一转成 Timestamp，避免 datetime64 与 Timestamp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 在 set 里哈希不一致导致&amp;#34;一个调仓日都没命中&amp;#34;的静默 bug。&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; month_end &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(idx, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;idx)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(idx&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_period(&lt;span style="color:#e6db74"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;max()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rebal_set &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Timestamp(d)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;normalize() &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; d &lt;span style="color:#f92672"&gt;in&lt;/span&gt; month_end}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rebal_set &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Timestamp(d)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;normalize() &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; d &lt;span style="color:#f92672"&gt;in&lt;/span&gt; idx}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;idx, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; date, row &lt;span style="color:#f92672"&gt;in&lt;/span&gt; score&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iterrows():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Timestamp(date)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;normalize() &lt;span style="color:#f92672"&gt;in&lt;/span&gt; rebal_set:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; row&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dropna()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 只买动量为正的标的，否则空仓&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; valid[valid &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; min_score]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_values(ascending&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; picks &lt;span style="color:#f92672"&gt;=&lt;/span&gt; valid&lt;span style="color:#f92672"&gt;.&lt;/span&gt;head(top_k)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; len(picks):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_w[picks] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; top_k
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; last_w
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; target
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ETF 池选了六只，覆盖股、债、商品、海外：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;POOL &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;510300&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;沪深300ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;510500&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;中证500ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;159915&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;创业板ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;513100&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;纳指ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;518880&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;黄金ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;511010&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;国债ETF&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;2019-01 ~ 2025-06，&lt;code&gt;lookback=20, top_k=2, 月度调仓&lt;/code&gt; 的示例结果：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;指标&lt;/th&gt;
 &lt;th&gt;动量轮动&lt;/th&gt;
 &lt;th&gt;沪深300 买入持有&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;年化收益&lt;/td&gt;
 &lt;td&gt;11.8%&lt;/td&gt;
 &lt;td&gt;3.4%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;年化波动&lt;/td&gt;
 &lt;td&gt;14.2%&lt;/td&gt;
 &lt;td&gt;21.5%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;夏普&lt;/td&gt;
 &lt;td&gt;0.79&lt;/td&gt;
 &lt;td&gt;0.28&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;最大回撤&lt;/td&gt;
 &lt;td&gt;-19.6%&lt;/td&gt;
 &lt;td&gt;-42.1%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Calmar&lt;/td&gt;
 &lt;td&gt;0.60&lt;/td&gt;
 &lt;td&gt;0.08&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;年化换手&lt;/td&gt;
 &lt;td&gt;8.4 倍&lt;/td&gt;
 &lt;td&gt;0&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;日胜率&lt;/td&gt;
 &lt;td&gt;53.1%&lt;/td&gt;
 &lt;td&gt;48.6%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分年度看，它并不每年都赢：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;年份&lt;/th&gt;
 &lt;th&gt;轮动&lt;/th&gt;
 &lt;th&gt;沪深300&lt;/th&gt;
 &lt;th&gt;差异&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;2019&lt;/td&gt;
 &lt;td&gt;+21.4%&lt;/td&gt;
 &lt;td&gt;+36.1%&lt;/td&gt;
 &lt;td&gt;-14.7%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2020&lt;/td&gt;
 &lt;td&gt;+28.6%&lt;/td&gt;
 &lt;td&gt;+27.2%&lt;/td&gt;
 &lt;td&gt;+1.4%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2021&lt;/td&gt;
 &lt;td&gt;+16.3%&lt;/td&gt;
 &lt;td&gt;-5.2%&lt;/td&gt;
 &lt;td&gt;+21.5%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2022&lt;/td&gt;
 &lt;td&gt;-8.9%&lt;/td&gt;
 &lt;td&gt;-21.6%&lt;/td&gt;
 &lt;td&gt;+12.7%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2023&lt;/td&gt;
 &lt;td&gt;+5.2%&lt;/td&gt;
 &lt;td&gt;-11.4%&lt;/td&gt;
 &lt;td&gt;+16.6%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2024&lt;/td&gt;
 &lt;td&gt;+14.7%&lt;/td&gt;
 &lt;td&gt;+14.7%&lt;/td&gt;
 &lt;td&gt;0.0%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;2025H1&lt;/td&gt;
 &lt;td&gt;+6.1%&lt;/td&gt;
 &lt;td&gt;+2.8%&lt;/td&gt;
 &lt;td&gt;+3.3%&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;2019 年跑输 15 个点&lt;/strong&gt;，原因是牛市里持有单一最强动量 ETF，反而在板块轮动中来回挨打。这个&amp;quot;缺陷&amp;quot;贯穿了后面所有讨论：&lt;strong&gt;动量策略在趋势延续时赚钱，在急切换时亏钱&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="三参数稳健性热力图比单点更好看也更诚实"&gt;三、参数稳健性：热力图比单点更好看也更诚实
&lt;/h2&gt;&lt;p&gt;实验室的规矩是：&lt;strong&gt;任何策略都不允许只看一组参数的结果&lt;/strong&gt;。于是把 &lt;code&gt;lookback × 调仓周期&lt;/code&gt; 打成一张热力图。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# scripts/grid_robustness.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; itertools
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; strategies.momentum_rotation &lt;span style="color:#f92672"&gt;import&lt;/span&gt; build_target_weights, momentum_score
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; strategies.run_ma &lt;span style="color:#f92672"&gt;import&lt;/span&gt; run_ma_backtest &lt;span style="color:#75715e"&gt;# noqa: F401 (示意：复用取数/引擎)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;grid_search&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, volume: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, pct: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rows &lt;span style="color:#f92672"&gt;=&lt;/span&gt; []
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; lookback, top_k, rebal &lt;span style="color:#f92672"&gt;in&lt;/span&gt; itertools&lt;span style="color:#f92672"&gt;.&lt;/span&gt;product([&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;20&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;], [&lt;span style="color:#e6db74"&gt;&amp;#34;M&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;W&amp;#34;&lt;/span&gt;]):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; score &lt;span style="color:#f92672"&gt;=&lt;/span&gt; momentum_score(close, lookback&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lookback)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; build_target_weights(close, score, top_k&lt;span style="color:#f92672"&gt;=&lt;/span&gt;top_k, rebalance&lt;span style="color:#f92672"&gt;=&lt;/span&gt;rebal)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run_backtest(close, volume, w, pct_chg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;pct)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rows&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append({&lt;span style="color:#e6db74"&gt;&amp;#34;lookback&amp;#34;&lt;/span&gt;: lookback, &lt;span style="color:#e6db74"&gt;&amp;#34;top_k&amp;#34;&lt;/span&gt;: top_k, &lt;span style="color:#e6db74"&gt;&amp;#34;rebal&amp;#34;&lt;/span&gt;: rebal,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;**&lt;/span&gt;{k: res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;metrics[k] &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; k &lt;span style="color:#f92672"&gt;in&lt;/span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#34;年化收益&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;最大回撤&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;年化换手&amp;#34;&lt;/span&gt;)}})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(rows)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;robustness_table&lt;/span&gt;(df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;汇总：多少比例的参数组合夏普 &amp;gt; 0.5，多少 &amp;gt; 0.3。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame({
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;gt;0.8&amp;#34;&lt;/span&gt;: [(df[&lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.8&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;gt;0.5&amp;#34;&lt;/span&gt;: [(df[&lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;gt;0.3&amp;#34;&lt;/span&gt;: [(df[&lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.3&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;夏普中位数&amp;#34;&lt;/span&gt;: [df[&lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;median()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;最差夏普&amp;#34;&lt;/span&gt;: [df[&lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;min()],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;24 组参数的实际分布（示例）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;夏普中位数 &lt;strong&gt;0.61&lt;/strong&gt;，最好 0.88，最差 0.12；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;18/24 组夏普 &amp;gt; 0.4&lt;/strong&gt;，说明策略不是靠某一组魔法参数撑起来的；&lt;/li&gt;
&lt;li&gt;但 &lt;code&gt;top_k=1&lt;/code&gt; 的 8 组里有 5 组夏普 &amp;lt; 0.4，&lt;strong&gt;集中度过高是这个策略的脆弱点&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断标准很朴素：&lt;strong&gt;如果参数热力图上有大片&amp;quot;深坑&amp;quot;，说明策略在拟合噪声；如果是一片&amp;quot;高原&amp;quot;，参数不敏感，策略逻辑本身站得住&lt;/strong&gt;。上面这张表勉强算高原。&lt;/p&gt;
&lt;h2 id="四成本敏感性把假设打回原形"&gt;四、成本敏感性：把假设打回原形
&lt;/h2&gt;&lt;p&gt;回测里最贵的不是滑点，是乐观。把成本参数逐级调高，看策略还剩多少：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;成本假设&lt;/th&gt;
 &lt;th&gt;年化收益&lt;/th&gt;
 &lt;th&gt;夏普&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;佣金万三 + 无滑点&lt;/td&gt;
 &lt;td&gt;13.2%&lt;/td&gt;
 &lt;td&gt;0.88&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;佣金万三 + 单边 0.1% 滑点&lt;/td&gt;
 &lt;td&gt;11.8%&lt;/td&gt;
 &lt;td&gt;0.79&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;佣金万三 + 单边 0.2% 滑点&lt;/td&gt;
 &lt;td&gt;10.4%&lt;/td&gt;
 &lt;td&gt;0.70&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;再加 0.1% 冲击成本&lt;/td&gt;
 &lt;td&gt;8.1%&lt;/td&gt;
 &lt;td&gt;0.55&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;极端：单边 0.5%&lt;/td&gt;
 &lt;td&gt;3.2%&lt;/td&gt;
 &lt;td&gt;0.22&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;换手 8.4 倍的策略，成本每提高 0.1%，年化就掉 1.3~1.4 个点&lt;/strong&gt;。这是 ETF 轮动真实的脆弱面——它需要每月真金白银地换仓。&lt;/p&gt;
&lt;p&gt;也正因为这个原因，实验室的实盘版本改成了&amp;quot;&lt;strong&gt;只在新信号与当前持仓差异超过一定阈值时才调仓&lt;/strong&gt;&amp;quot;，把年化换手从 8.4 倍压到 4 倍左右，年化收益只掉了 0.8 个点。这类工程细节，永远比再找一个因子更值钱。&lt;/p&gt;
&lt;h2 id="五几个只在实盘阶段才暴露的问题"&gt;五、几个只在实盘阶段才暴露的问题
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ETF 溢价与流动性&lt;/strong&gt;。纳指 ETF（513100）长期有溢价，回测里按净值成交的假设在实盘会被吃掉 1~3%。更麻烦的是它偶尔&lt;strong&gt;限购&lt;/strong&gt;，大资金根本买不进去。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;上市时间不同&lt;/strong&gt;。创业板 ETF 2011 年就有，纳指 ETF 2013 年上市。如果起始日期按最长的算，早期回测里那些&amp;quot;缺失标的&amp;quot;会被当成持续下跌处理，动量排名就失真了。正确做法是&lt;strong&gt;只在当期有数据的标的里排名&lt;/strong&gt;（&lt;code&gt;dropna()&lt;/code&gt; 已经处理，但要检查是不是真的这么做的）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;幸存者偏差&lt;/strong&gt;。ETF 池是按今天的名单选的。2019 年时并没有现在这么多行业 ETF，如果回测里混进了&amp;quot;因为后来表现好才被记住的品种&amp;quot;，结论就不可信。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分红与拆分&lt;/strong&gt;。ETF 分红会导致价格跳空，未做复权的动量会误判趋势反转。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六结论策略的意义在于暴露流程问题"&gt;六、结论：策略的意义在于暴露流程问题
&lt;/h2&gt;&lt;p&gt;两个策略跑完，实验室得到的收获不是&amp;quot;找到了一条年化 12% 的曲线&amp;quot;，而是三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;择时工具的价值主要在压回撤&lt;/strong&gt;，不要在它身上指望超额收益；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;参数稳健性比单点最优重要一个数量级&lt;/strong&gt;，热力图上的高原才是真信号；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;换手是隐形税&lt;/strong&gt;，任何策略的第一版都应该先算&amp;quot;成本翻倍后还剩多少&amp;quot;。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下一阶段，我们从&amp;quot;规则策略&amp;quot;转向&amp;quot;&lt;strong&gt;机器学习选股&lt;/strong&gt;&amp;quot;——用 &lt;a class="link" href="https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/" &gt;LightGBM&lt;/a&gt; 做截面收益预测，那里会碰到比参数网格更危险的东西：信息泄露。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。回测里每一条平滑的曲线，背后都藏着一堆没写进报告的前提。&lt;/p&gt;</description></item><item><title>向量化回测引擎：从零实现一个可信的日频回测框架</title><link>https://yuansima.com/p/vectorized-backtest-engine/</link><pubDate>Sat, 17 Oct 2026 20:40:00 +0800</pubDate><guid>https://yuansima.com/p/vectorized-backtest-engine/</guid><description>&lt;p&gt;&lt;em&gt;远思为径，码动价值。欢迎来到远思码技术实验室的最新技术实践记录。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;回测是量化研究里&lt;strong&gt;最容易产生幻觉的一步&lt;/strong&gt;。同样一段策略逻辑，用不同框架、不同参数假设跑出来的年化收益率可以差出十几个点，而绝大多数差异都藏在三件小事里：成交价、交易限制、成本。&lt;/p&gt;
&lt;p&gt;实验室的结论是：&lt;strong&gt;先自己写一个能看懂每一行的引擎&lt;/strong&gt;，再去用现成框架。这篇就是那个引擎，全部代码不到 200 行。&lt;/p&gt;
&lt;h2 id="一引擎的契约"&gt;一、引擎的契约
&lt;/h2&gt;&lt;p&gt;在写代码之前，先把输入输出定死，这是防止自己作弊的第一步：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;输入&lt;/th&gt;
 &lt;th&gt;形状&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;close&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;T×N&lt;/td&gt;
 &lt;td&gt;复权收盘价矩阵&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;volume&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;T×N&lt;/td&gt;
 &lt;td&gt;成交量矩阵（用于判断可交易性）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;pct_chg&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;T×N&lt;/td&gt;
 &lt;td&gt;当日涨跌幅，用于判断涨跌停&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;target_w&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;T×N&lt;/td&gt;
 &lt;td&gt;&lt;strong&gt;T 日收盘后&lt;/strong&gt;决定的下一期目标权重，行和 ≤ 1，余下为现金&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;输出&lt;/th&gt;
 &lt;th&gt;含义&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;nav&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;净值曲线（起点为 1）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;holdings&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;每日实际持仓权重、换手、成本明细&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键约定只有一条：&lt;strong&gt;&lt;code&gt;target_w&lt;/code&gt; 的第 t 行，是从第 t 日收盘之后开始生效的权重&lt;/strong&gt;。引擎内部自己做 &lt;code&gt;shift(1)&lt;/code&gt; 来实现&amp;quot;信号滞后一期&amp;quot;，策略代码里严禁再手动 shift，否则会重复滞后。&lt;/p&gt;
&lt;h2 id="二骨架收益漂移成本"&gt;二、骨架：收益、漂移、成本
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# backtest/engine.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; __future__ &lt;span style="color:#f92672"&gt;import&lt;/span&gt; annotations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; itertools
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; dataclasses &lt;span style="color:#f92672"&gt;import&lt;/span&gt; dataclass, field
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; numpy &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; np
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BacktestConfig&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fee_rate: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0003&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 佣金，双边&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; stamp_duty: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0005&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 印花税，仅卖出&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; slippage: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0010&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 单边滑点&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; impact_coef: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.0015&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 冲击成本系数（相对换手）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; rf: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.02&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 无风险利率（年化）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; periods: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;244&lt;/span&gt; &lt;span style="color:#75715e"&gt;# 年化交易日&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;@dataclass&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;BacktestResult&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nav: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; daily_return: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cost: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; holdings: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metrics: dict &lt;span style="color:#f92672"&gt;=&lt;/span&gt; field(default_factory&lt;span style="color:#f92672"&gt;=&lt;/span&gt;dict)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核心循环全部用矩阵运算完成，一行 &lt;code&gt;for&lt;/code&gt; 都不需要：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;run_backtest&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_w: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct_chg: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg: BacktestConfig &lt;span style="color:#f92672"&gt;|&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; BacktestResult:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; cfg &lt;span style="color:#f92672"&gt;or&lt;/span&gt; BacktestConfig()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close, volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;align(volume, join&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;inner&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reindex(index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ret &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pct_change()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# 第 t 日收益，close_{t-1} -&amp;gt; close_t&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 1. 可交易性：停牌(volume=0) / 涨跌停 / 缺失价 ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tradable &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (volume &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna() &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; (target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; pct_chg &lt;span style="color:#f92672"&gt;is&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pct_chg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reindex_like(tradable)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 主板 ±10%、创业板/科创板 ±20%，用绝对阈值统一近似：≥9.8% 视为涨停不可买&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; limit_up &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pct &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;9.8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; limit_down &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pct &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;9.8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 目标权重 &amp;gt; 0 且涨停 -&amp;gt; 买不进；目标权重 &amp;lt; 0 且跌停 -&amp;gt; 卖不出&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; can_buy &lt;span style="color:#f92672"&gt;=&lt;/span&gt; tradable &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;limit_up
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; can_sell &lt;span style="color:#f92672"&gt;=&lt;/span&gt; tradable &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;limit_down
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w_prev &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; blocked_buy &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (w_prev&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isna() &lt;span style="color:#f92672"&gt;|&lt;/span&gt; (w_prev &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; (target_w &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;can_buy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; blocked_sell &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (w_prev &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; (target_w &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#f92672"&gt;~&lt;/span&gt;can_sell
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mask(blocked_buy, &lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mask(blocked_sell, w_prev)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 2. 归一化：不足 1 的部分视为现金 ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 允许做多为主；若行和 &amp;gt; 1 则等比压缩，避免隐式杠杆&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gross &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;replace(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scale &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;where(gross &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; gross, &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; target_w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mul(pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(scale, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index), axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 3. 实际持仓 = 上一期目标权重（信号滞后一期） ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w_hold &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 4. 毛收益：今日持仓 × 今日收益 ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gross_ret &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (w_hold &lt;span style="color:#f92672"&gt;*&lt;/span&gt; ret)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 5. 权重漂移：今日收盘后各资产权重自然变化 ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; denom &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; gross_ret)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;replace(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w_drift &lt;span style="color:#f92672"&gt;=&lt;/span&gt; w_hold&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mul(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; ret, axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;div(denom, axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# ---- 6. 换手与成本：今日收盘调仓，相对漂移权重 ----&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; delta &lt;span style="color:#f92672"&gt;=&lt;/span&gt; target_w &lt;span style="color:#f92672"&gt;-&lt;/span&gt; w_drift
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover_buy &lt;span style="color:#f92672"&gt;=&lt;/span&gt; delta&lt;span style="color:#f92672"&gt;.&lt;/span&gt;clip(lower&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# 买入额占比&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover_sell &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#f92672"&gt;-&lt;/span&gt;delta)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;clip(lower&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# 卖出额占比&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover &lt;span style="color:#f92672"&gt;=&lt;/span&gt; turnover_buy &lt;span style="color:#f92672"&gt;+&lt;/span&gt; turnover_sell
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cost &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (turnover_buy &lt;span style="color:#f92672"&gt;*&lt;/span&gt; (cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fee_rate &lt;span style="color:#f92672"&gt;+&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;slippage)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; turnover_sell &lt;span style="color:#f92672"&gt;*&lt;/span&gt; (cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fee_rate &lt;span style="color:#f92672"&gt;+&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;slippage &lt;span style="color:#f92672"&gt;+&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stamp_duty)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; turnover &lt;span style="color:#f92672"&gt;*&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;impact_coef &lt;span style="color:#f92672"&gt;*&lt;/span&gt; turnover) &lt;span style="color:#75715e"&gt;# 冲击成本近似平方项&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; port_ret &lt;span style="color:#f92672"&gt;=&lt;/span&gt; gross_ret &lt;span style="color:#f92672"&gt;-&lt;/span&gt; cost
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nav &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; port_ret)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cumprod()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; BacktestResult(nav&lt;span style="color:#f92672"&gt;=&lt;/span&gt;nav, daily_return&lt;span style="color:#f92672"&gt;=&lt;/span&gt;port_ret, turnover&lt;span style="color:#f92672"&gt;=&lt;/span&gt;turnover,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cost&lt;span style="color:#f92672"&gt;=&lt;/span&gt;cost, holdings&lt;span style="color:#f92672"&gt;=&lt;/span&gt;w_hold)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;metrics &lt;span style="color:#f92672"&gt;=&lt;/span&gt; performance(nav, port_ret, turnover, cfg)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; res
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有几处设计值得展开说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;w_hold = target_w.shift(1)&lt;/code&gt;&lt;/strong&gt; 就是&amp;quot;信号滞后一期&amp;quot;的全部实现。策略在 T 日收盘后生成信号，T+1 日才能享受（或承受）收益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;漂移权重 &lt;code&gt;w_drift&lt;/code&gt;&lt;/strong&gt; 常被忽略。如果你 T 日满仓买入，T+1 日股票涨了 5%，那么 T+1 收盘时它在组合里的权重是 5% 之后的自然权重，不是当初的目标权重。调仓换手必须相对它算，否则每次调仓的换手都会被高估。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;冲击成本写成 &lt;code&gt;turnover * coef * turnover&lt;/code&gt;&lt;/strong&gt;，即随换手平方增长。这比线性模型更贴近现实：大额调仓的边际成本上升。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="三绩效指标"&gt;三、绩效指标
&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;performance&lt;/span&gt;(nav: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, ret: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, turnover: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg: BacktestConfig) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; dict:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; n &lt;span style="color:#f92672"&gt;=&lt;/span&gt; len(nav)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; n &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;or&lt;/span&gt; nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;error&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;样本不足或净值归零&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ann &lt;span style="color:#f92672"&gt;=&lt;/span&gt; nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;**&lt;/span&gt; (cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;periods &lt;span style="color:#f92672"&gt;/&lt;/span&gt; n) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ret&lt;span style="color:#f92672"&gt;.&lt;/span&gt;std(ddof&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#f92672"&gt;*&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sqrt(cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;periods)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sharpe &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (ann &lt;span style="color:#f92672"&gt;-&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rf) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; vol &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; vol &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; nav &lt;span style="color:#f92672"&gt;/&lt;/span&gt; nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cummax() &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mdd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; dd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;min()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; calmar &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ann &lt;span style="color:#f92672"&gt;/&lt;/span&gt; abs(mdd) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; mdd &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 最大回撤持续天数（净值在水面之下的最长连续区间）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; underwater &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (dd &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;astype(int)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_flat &lt;span style="color:#f92672"&gt;=&lt;/span&gt; max((len(list(g)) &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; k, g &lt;span style="color:#f92672"&gt;in&lt;/span&gt; itertools&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(underwater) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; k &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; default&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; win &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (ret &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; avg_turnover &lt;span style="color:#f92672"&gt;=&lt;/span&gt; turnover&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;累计收益&amp;#34;&lt;/span&gt;: round(float(nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;年化收益&amp;#34;&lt;/span&gt;: round(float(ann), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;年化波动&amp;#34;&lt;/span&gt;: round(float(vol), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;夏普&amp;#34;&lt;/span&gt;: round(float(sharpe), &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;最大回撤&amp;#34;&lt;/span&gt;: round(float(mdd), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;最大回撤天数&amp;#34;&lt;/span&gt;: int(max_flat),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;Calmar&amp;#34;&lt;/span&gt;: round(float(calmar), &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;日胜率&amp;#34;&lt;/span&gt;: round(float(win), &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;日均换手&amp;#34;&lt;/span&gt;: round(float(avg_turnover), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;年化换手&amp;#34;&lt;/span&gt;: round(float(avg_turnover &lt;span style="color:#f92672"&gt;*&lt;/span&gt; cfg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;periods), &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;交易日数&amp;#34;&lt;/span&gt;: int(n),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里 &lt;code&gt;日均换手 × 244 = 年化换手&lt;/code&gt; 是个粗略但足够用的口径。&lt;strong&gt;日频策略年化换手超过 30 倍，成本假设就必须认真对待&lt;/strong&gt;，不能再用&amp;quot;万一三&amp;quot;敷衍过去。&lt;/p&gt;
&lt;h2 id="四把正确性钉死三个单元测试"&gt;四、把正确性钉死：三个单元测试
&lt;/h2&gt;&lt;p&gt;引擎写完不测，等于没写。实验室用三个手算案例做回归测试，任何改动都必须过：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# tests/test_engine.py&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; numpy &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; np
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;import&lt;/span&gt; pandas &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;from&lt;/span&gt; backtest.engine &lt;span style="color:#f92672"&gt;import&lt;/span&gt; BacktestConfig, run_backtest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;_frames&lt;/span&gt;(ret_rows):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;由收益矩阵构造价格矩阵（起点 10 元）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(ret_rows))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cumprod() &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(&lt;span style="color:#ae81ff"&gt;1e6&lt;/span&gt;, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; close, volume
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_single_stock_buy_and_hold&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 两天，每天涨 10%。T日收盘建仓 -&amp;gt; T+1 才吃到收益&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close, volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _frames([[&lt;span style="color:#ae81ff"&gt;0.10&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.10&lt;/span&gt;]])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame([[&lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;]], index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run_backtest(close, volume, w, cfg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;BacktestConfig(fee_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, stamp_duty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; slippage&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, impact_coef&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 第一天无持仓（w 被 shift），第二天满仓吃 10%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; abs(res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1e-12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; abs(res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nav&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.10&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1e-12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_cost_is_charged_on_turnover&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close, volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _frames([[&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;]])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame([[&lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.0&lt;/span&gt;]], index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; cfg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; BacktestConfig(fee_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.001&lt;/span&gt;, stamp_duty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.001&lt;/span&gt;, slippage&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, impact_coef&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run_backtest(close, volume, w, cfg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;cfg)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 第一天：w.shift 后仍为 0 -&amp;gt; 无持仓；第二天持仓 1 后立刻清仓？&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 实际：hold 恒为 [0, 1]，故第二天收盘换手 = 1，卖出成本 = 1*(佣金0.001+印花税0.001)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;turnover&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1.0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; abs(res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cost&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;] &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.002&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1e-12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;def&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;test_weights_never_imply_leverage&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close, volume &lt;span style="color:#f92672"&gt;=&lt;/span&gt; _frames([[&lt;span style="color:#ae81ff"&gt;0.01&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.01&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.01&lt;/span&gt;]], )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; w &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame([[&lt;span style="color:#ae81ff"&gt;0.8&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.8&lt;/span&gt;], [&lt;span style="color:#ae81ff"&gt;0.8&lt;/span&gt;]], index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; res &lt;span style="color:#f92672"&gt;=&lt;/span&gt; run_backtest(close, volume, w, cfg&lt;span style="color:#f92672"&gt;=&lt;/span&gt;BacktestConfig(fee_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, stamp_duty&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; slippage&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, impact_coef&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;assert&lt;/span&gt; (res&lt;span style="color:#f92672"&gt;.&lt;/span&gt;holdings&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1e-9&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;all()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第二个测试看起来有点绕，但它锁住了一个非常具体的语义：&lt;strong&gt;换手发生在&amp;quot;目标权重与漂移权重不一致&amp;quot;的那一天，成本当天计提&lt;/strong&gt;。这个语义一旦写错，所有策略的成本都会被系统性低估。&lt;/p&gt;
&lt;h2 id="五什么时候该换成事件驱动框架"&gt;五、什么时候该换成事件驱动框架
&lt;/h2&gt;&lt;p&gt;向量化引擎的假设是：&lt;strong&gt;同一时点所有股票都能按收盘价成交，且份额可以无限细分&lt;/strong&gt;。它在下列场景会失真：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;场景&lt;/th&gt;
 &lt;th&gt;向量化引擎的问题&lt;/th&gt;
 &lt;th&gt;建议&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;分钟级 / 盘口撮合&lt;/td&gt;
 &lt;td&gt;无法表达排队、部分成交&lt;/td&gt;
 &lt;td&gt;事件驱动（vn.py / backtrader / 自研撮合）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;资金量大于标的日成交额的 1%&lt;/td&gt;
 &lt;td&gt;冲击成本模型过于乐观&lt;/td&gt;
 &lt;td&gt;引入成交量参与率约束&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;需要盘中撤单、止损单&lt;/td&gt;
 &lt;td&gt;无订单生命周期&lt;/td&gt;
 &lt;td&gt;事件驱动&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;组合再平衡涉及资金约束（现金不足）&lt;/td&gt;
 &lt;td&gt;权重假设掩盖了资金不足&lt;/td&gt;
 &lt;td&gt;加现金约束层&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实验室的日常研究 90% 用向量化，因为要跑几千组参数；只有在策略进入仿真阶段，才用事件驱动复核一遍。&lt;strong&gt;先用快而粗糙的尺子找方向，再用慢而精确的尺子确认&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="六一个常见误用"&gt;六、一个常见误用
&lt;/h2&gt;&lt;p&gt;有人会把 &lt;code&gt;target_w&lt;/code&gt; 直接设成&amp;quot;今天涨得最好的股票&amp;quot;，然后跑出 500% 的年化——因为他在 T 日收盘时用了 T 日的数据去选股，而引擎的 &lt;code&gt;shift(1)&lt;/code&gt; 让这份&amp;quot;未来信息&amp;quot;合法化了。&lt;/p&gt;
&lt;p&gt;判断方法很简单：&lt;strong&gt;把 &lt;code&gt;shift(1)&lt;/code&gt; 改成 &lt;code&gt;shift(2)&lt;/code&gt;，如果年化收益暴跌，说明策略严重依赖即时信息，值得警惕&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;到这里我们有了两样东西：可检验的&lt;a class="link" href="https://yuansima.com/p/quant-factor-engineering/" &gt;因子&lt;/a&gt;，和可信的&lt;a class="link" href="https://yuansima.com/p/vectorized-backtest-engine/" &gt;尺子&lt;/a&gt;。下一篇用它们跑第一个真实策略——双均线与动量轮动的组合，顺带把参数稳健性检验做完。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。回测是用来证伪的，不是用来给自己发奖金的。&lt;/p&gt;</description></item></channel></rss>