<?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/categories/%E9%87%8F%E5%8C%96%E4%BA%A4%E6%98%93/</link><description>Recent content in 量化交易 on 远思码技术实验室</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 25 Oct 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://yuansima.com/categories/%E9%87%8F%E5%8C%96%E4%BA%A4%E6%98%93/index.xml" rel="self" type="application/rss+xml"/><item><title>每日选股信号的自动化：systemd timer + 企业微信推送</title><link>https://yuansima.com/p/daily-signal-automation/</link><pubDate>Sun, 25 Oct 2026 07:30:00 +0800</pubDate><guid>https://yuansima.com/p/daily-signal-automation/</guid><description>&lt;p&gt;&lt;em&gt;远思为径，码动价值。欢迎来到远思码技术实验室的最新技术实践记录。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;研究做得再漂亮，如果每天还要手动开 Jupyter 跑一遍 notebook，那它就永远只是个练习。这一篇把&lt;a class="link" href="https://yuansima.com/p/quant-trading-roadmap/" &gt;前面六篇&lt;/a&gt;串成一条&lt;strong&gt;无人值守的流水线&lt;/strong&gt;：每个交易日盘后自动取数、算因子、生成信号，并把结果推到手机上。&lt;/p&gt;
&lt;h2 id="一整条链路"&gt;一、整条链路
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐
│ 取行情 │ → │ 算因子 │ → │ 模型打分 │ → │ 组合构建 │ → │ 推送+落库│
└─────────┘ └──────────┘ └──────────┘ └──────────┘ └─────────┘
 失败 → 重试 3 次 → 仍失败则告警 → 记录死信
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;设计原则只有三条：&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;：任何一个环节失败都不会污染已有信号。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="二先判断是不是交易日"&gt;二、先判断是不是交易日
&lt;/h2&gt;&lt;p&gt;节假日不跑，是这条流水线最重要的一条规则。&lt;code&gt;systemd&lt;/code&gt; 的 &lt;code&gt;OnCalendar&lt;/code&gt; 只认星期几，不认 A 股节假日，所以判断必须写在程序里。&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;# quant/calendar.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; akshare &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; ak
&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;_cache: 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;
&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;trade_dates&lt;/span&gt;() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; list[dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date]:
&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;A 股历史交易日序列（含未来已公布日期）。首次调用约 1 秒，之后走缓存。&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;global&lt;/span&gt; _cache
&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; _cache &lt;span style="color:#f92672"&gt;is&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; df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ak&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tool_trade_date_hist_sina() &lt;span style="color:#75715e"&gt;# 列: trade_date&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; _cache &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_datetime(df[&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date
&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; list(_cache)
&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;is_trading_day&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date &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; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; day &lt;span style="color:#f92672"&gt;=&lt;/span&gt; day &lt;span style="color:#f92672"&gt;or&lt;/span&gt; dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date&lt;span style="color:#f92672"&gt;.&lt;/span&gt;today()
&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; day &lt;span style="color:#f92672"&gt;in&lt;/span&gt; trade_dates()
&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;last_trading_day&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date &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; dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date:
&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;lt;= day 的交易日（用于补跑）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; day &lt;span style="color:#f92672"&gt;=&lt;/span&gt; day &lt;span style="color:#f92672"&gt;or&lt;/span&gt; dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date&lt;span style="color:#f92672"&gt;.&lt;/span&gt;today()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dates &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [d &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; d &lt;span style="color:#f92672"&gt;in&lt;/span&gt; trade_dates() &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; d &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; day]
&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; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; dates:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;RuntimeError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;day&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 之前没有交易日数据，检查交易日历缓存&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; dates[&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;为什么不干脆只在 &lt;code&gt;OnCalendar=Mon..Fri&lt;/code&gt; 上跑？因为&lt;strong&gt;调休&lt;/strong&gt;。2026 年的春节、国庆前后都有&amp;quot;周六上班&amp;quot;的交易日，只在周一到周五跑会漏；反过来节假日又白跑。让程序判断交易日、让 timer 每天都触发一次，是最省心的组合。&lt;/p&gt;

 &lt;/blockquote&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;# quant/pipeline.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; logging
&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; sys
&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; quant.calendar &lt;span style="color:#f92672"&gt;import&lt;/span&gt; is_trading_day, last_trading_day
&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; quant.config &lt;span style="color:#f92672"&gt;import&lt;/span&gt; CFG
&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; quant.db &lt;span style="color:#f92672"&gt;import&lt;/span&gt; save_signals
&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; quant.features &lt;span style="color:#f92672"&gt;import&lt;/span&gt; build_features
&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; quant.model &lt;span style="color:#f92672"&gt;import&lt;/span&gt; load_model, score_panel
&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; quant.notify &lt;span style="color:#f92672"&gt;import&lt;/span&gt; push_wecom
&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; quant.portfolio &lt;span style="color:#f92672"&gt;import&lt;/span&gt; build_portfolio
&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;log &lt;span style="color:#f92672"&gt;=&lt;/span&gt; logging&lt;span style="color:#f92672"&gt;.&lt;/span&gt;getLogger(&lt;span style="color:#e6db74"&gt;&amp;#34;quant.pipeline&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;
&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_once&lt;/span&gt;(force: bool &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;, day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date &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; int:
&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;返回进程退出码：0 成功/跳过，1 失败。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; day &lt;span style="color:#f92672"&gt;=&lt;/span&gt; day &lt;span style="color:#f92672"&gt;or&lt;/span&gt; last_trading_day()
&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; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; force &lt;span style="color:#f92672"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; is_trading_day(day):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&lt;span style="color:#e6db74"&gt;&amp;#34;非交易日 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%s&lt;/span&gt;&lt;span style="color:#e6db74"&gt;，跳过&amp;#34;&lt;/span&gt;, day)
&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:#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; &lt;span style="color:#75715e"&gt;# 1) 取数（失败直接抛，由 systemd 的重试策略兜底）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; close, volume, pct, factor_panel &lt;span style="color:#f92672"&gt;=&lt;/span&gt; build_features(day)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&lt;span style="color:#e6db74"&gt;&amp;#34;取数完成：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 只标的，&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 个因子&amp;#34;&lt;/span&gt;, close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shape[&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;], factor_panel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shape[&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;# 2) 模型打分&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; model &lt;span style="color:#f92672"&gt;=&lt;/span&gt; load_model(CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;model_path)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; scores &lt;span style="color:#f92672"&gt;=&lt;/span&gt; score_panel(model, factor_panel)
&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) 组合构建：top 20 等权，行业偏离 &amp;lt;= 3%&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; weights &lt;span style="color:#f92672"&gt;=&lt;/span&gt; build_portfolio(scores, factor_panel[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;], top_n&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;top_n)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&lt;span style="color:#e6db74"&gt;&amp;#34;组合构建完成：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 只标的，合计权重 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%.2f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, (weights &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;sum(), weights&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum())
&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; n &lt;span style="color:#f92672"&gt;=&lt;/span&gt; save_signals(day, weights, scores)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&lt;span style="color:#e6db74"&gt;&amp;#34;信号已入库 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 条&amp;#34;&lt;/span&gt;, n)
&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; push_wecom(day, weights, scores)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&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; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;if __name__ == &amp;quot;__main__&amp;quot;&lt;/code&gt; 的入口保持极薄，方便用 &lt;code&gt;python -m quant.pipeline --force&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;# quant/__main__.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; argparse
&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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; logging
&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; sys
&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; quant.pipeline &lt;span style="color:#f92672"&gt;import&lt;/span&gt; run_once
&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;logging&lt;span style="color:#f92672"&gt;.&lt;/span&gt;basicConfig(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; level&lt;span style="color:#f92672"&gt;=&lt;/span&gt;logging&lt;span style="color:#f92672"&gt;.&lt;/span&gt;INFO,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; format&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(asctime)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(levelname)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(name)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; - &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(message)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;parser &lt;span style="color:#f92672"&gt;=&lt;/span&gt; argparse&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ArgumentParser()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;parser&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add_argument(&lt;span style="color:#e6db74"&gt;&amp;#34;--day&amp;#34;&lt;/span&gt;, type&lt;span style="color:#f92672"&gt;=&lt;/span&gt;dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fromisoformat, default&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;parser&lt;span style="color:#f92672"&gt;.&lt;/span&gt;add_argument(&lt;span style="color:#e6db74"&gt;&amp;#34;--force&amp;#34;&lt;/span&gt;, action&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;store_true&amp;#34;&lt;/span&gt;, help&lt;span style="color:#f92672"&gt;=&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;args &lt;span style="color:#f92672"&gt;=&lt;/span&gt; parser&lt;span style="color:#f92672"&gt;.&lt;/span&gt;parse_args()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sys&lt;span style="color:#f92672"&gt;.&lt;/span&gt;exit(run_once(force&lt;span style="color:#f92672"&gt;=&lt;/span&gt;args&lt;span style="color:#f92672"&gt;.&lt;/span&gt;force, day&lt;span style="color:#f92672"&gt;=&lt;/span&gt;args&lt;span style="color:#f92672"&gt;.&lt;/span&gt;day))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;IF&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;EXISTS&lt;/span&gt; daily_signal (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trade_date date &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; symbol text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; score double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; weight double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; model_ver text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; created_at timestamptz &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; now(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; (trade_date, symbol)
&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;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;# quant/db.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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 style="color:#f92672"&gt;from&lt;/span&gt; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; create_engine, text
&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; quant.config &lt;span style="color:#f92672"&gt;import&lt;/span&gt; CFG
&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;engine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; create_engine(CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dsn, pool_pre_ping&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, pool_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;UPSERT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; text(&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;&lt;span style="color:#e6db74"&gt; INSERT INTO daily_signal (trade_date, symbol, score, weight, model_ver)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VALUES (:trade_date, :symbol, :score, :weight, :model_ver)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ON CONFLICT (trade_date, symbol) DO UPDATE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SET score = EXCLUDED.score,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; weight = EXCLUDED.weight,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; model_ver = EXCLUDED.model_ver
&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;
&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;save_signals&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date, weights: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, scores: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; int:
&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:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;: day, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;: sym, &lt;span style="color:#e6db74"&gt;&amp;#34;score&amp;#34;&lt;/span&gt;: float(scores[sym]),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;weight&amp;#34;&lt;/span&gt;: float(w), &lt;span style="color:#e6db74"&gt;&amp;#34;model_ver&amp;#34;&lt;/span&gt;: CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;model_version}
&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; sym, w &lt;span style="color:#f92672"&gt;in&lt;/span&gt; weights&lt;span style="color:#f92672"&gt;.&lt;/span&gt;items() &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; w &lt;span style="color:#f92672"&gt;&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; rows:
&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:#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;with&lt;/span&gt; engine&lt;span style="color:#f92672"&gt;.&lt;/span&gt;begin() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; conn:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;execute(UPSERT, rows)
&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; len(rows)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;推送也需要幂等——同一天重复推送会把手机刷屏。用一个简单的 &lt;code&gt;push_log&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;already_pushed&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date, channel: str) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; bool:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; engine&lt;span style="color:#f92672"&gt;.&lt;/span&gt;connect() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; conn:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hit &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;execute(text(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;SELECT 1 FROM push_log WHERE trade_date = :d AND channel = :c&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;d&amp;#34;&lt;/span&gt;: day, &lt;span style="color:#e6db74"&gt;&amp;#34;c&amp;#34;&lt;/span&gt;: channel})&lt;span style="color:#f92672"&gt;.&lt;/span&gt;first()
&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; hit &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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="五推送企业微信机器人"&gt;五、推送：企业微信机器人
&lt;/h2&gt;&lt;p&gt;企业微信群机器人的 webhook 是最省事的通道——不需要申请公众号、不需要服务器回调，一条 HTTP POST 就到手机。&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;# quant/notify.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; logging
&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; time
&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; requests
&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; quant.config &lt;span style="color:#f92672"&gt;import&lt;/span&gt; CFG
&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; quant.db &lt;span style="color:#f92672"&gt;import&lt;/span&gt; already_pushed, mark_pushed
&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;log &lt;span style="color:#f92672"&gt;=&lt;/span&gt; logging&lt;span style="color:#f92672"&gt;.&lt;/span&gt;getLogger(&lt;span style="color:#e6db74"&gt;&amp;#34;quant.notify&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;
&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;_fmt_message&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date, weights, scores) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; str:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; top &lt;span style="color:#f92672"&gt;=&lt;/span&gt; weights[weights &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;sort_values(ascending&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;head(&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; lines &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;### 📈 量化信号 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;day&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isoformat()&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;gt; 模型版本 `&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;model_version&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;`，持仓 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;int((weights &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;sum())&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 只，&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;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;总仓位 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;weights&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum()&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.0%&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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;&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;for&lt;/span&gt; sym, w &lt;span style="color:#f92672"&gt;in&lt;/span&gt; top&lt;span style="color:#f92672"&gt;.&lt;/span&gt;items():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lines&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;- `&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;sym&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;` 权重 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;w&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;.1%&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;，得分 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;scores[sym]&lt;span style="color:#e6db74"&gt;:&lt;/span&gt;&lt;span style="color:#e6db74"&gt;+.3f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lines&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(&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; lines&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;lt;font color=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;comment&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;gt;本消息由远思码实验室自动生成，不构成投资建议。&amp;lt;/font&amp;gt;&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; &lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;\n&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;.&lt;/span&gt;join(lines)
&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;push_wecom&lt;/span&gt;(day: dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date, weights, scores, retries: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;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; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dry_run:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;warning(&lt;span style="color:#e6db74"&gt;&amp;#34;DRY_RUN 开启，跳过推送&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;
&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; already_pushed(day, &lt;span style="color:#e6db74"&gt;&amp;#34;wecom&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;info(&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 已推送过，跳过&amp;#34;&lt;/span&gt;, day)
&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&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; payload &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {&lt;span style="color:#e6db74"&gt;&amp;#34;msgtype&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;markdown&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;markdown&amp;#34;&lt;/span&gt;: {&lt;span style="color:#e6db74"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: _fmt_message(day, weights, scores)}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_err: &lt;span style="color:#a6e22e"&gt;Exception&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;=&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; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; attempt &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, retries &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 style="color:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; resp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; requests&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post(CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;wecom_webhook, json&lt;span style="color:#f92672"&gt;=&lt;/span&gt;payload, timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; data &lt;span style="color:#f92672"&gt;=&lt;/span&gt; resp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;json()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 企业微信的成功码是 0；频率限制是 45009&lt;/span&gt;
&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; data&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get(&lt;span style="color:#e6db74"&gt;&amp;#34;errcode&amp;#34;&lt;/span&gt;) &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; mark_pushed(day, &lt;span style="color:#e6db74"&gt;&amp;#34;wecom&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;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;RuntimeError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;企业微信返回 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;data&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Exception&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; exc: &lt;span style="color:#75715e"&gt;# noqa: BLE001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; last_err &lt;span style="color:#f92672"&gt;=&lt;/span&gt; exc
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; wait &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;**&lt;/span&gt; attempt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;warning(&lt;span style="color:#e6db74"&gt;&amp;#34;推送失败（第 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 次）：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%s&lt;/span&gt;&lt;span style="color:#e6db74"&gt;，&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%d&lt;/span&gt;&lt;span style="color:#e6db74"&gt;s 后重试&amp;#34;&lt;/span&gt;, attempt, exc, wait)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; time&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sleep(wait)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;RuntimeError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;推送最终失败：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;last_err&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&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;
&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;push_alert&lt;/span&gt;(msg: str) &lt;span style="color:#f92672"&gt;-&amp;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; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&amp;#34;告警用：单次尝试、不重试、失败只写日志（告警本身不该再触发告警）。&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;if&lt;/span&gt; CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dry_run:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;warning(&lt;span style="color:#e6db74"&gt;&amp;#34;DRY_RUN 开启，告警内容：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%s&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, msg)
&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:#66d9ef"&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; requests&lt;span style="color:#f92672"&gt;.&lt;/span&gt;post(CFG&lt;span style="color:#f92672"&gt;.&lt;/span&gt;wecom_webhook,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; json&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;msgtype&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;: {&lt;span style="color:#e6db74"&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: msg}},
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timeout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;except&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;Exception&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; exc: &lt;span style="color:#75715e"&gt;# noqa: BLE001&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; log&lt;span style="color:#f92672"&gt;.&lt;/span&gt;error(&lt;span style="color:#e6db74"&gt;&amp;#34;告警推送失败：&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%s&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;, exc)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;webhook 里有 key，&lt;strong&gt;不要写进代码库&lt;/strong&gt;。用 &lt;code&gt;EnvironmentFile&lt;/code&gt; 注入，权限 &lt;code&gt;600&lt;/code&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="六systemd为什么不用-crontab"&gt;六、systemd：为什么不用 crontab
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://yuansima.com/p/linux-crontab-guide/" &gt;以前写过 crontab 的用法&lt;/a&gt;，但这条流水线更适合 systemd，理由很具体：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;需求&lt;/th&gt;
 &lt;th&gt;cron&lt;/th&gt;
 &lt;th&gt;systemd timer&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;code&gt;sleep 30&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;After=network-online.target postgresql.service&lt;/code&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;code&gt;Restart=on-failure&lt;/code&gt; + &lt;code&gt;RestartSec&lt;/code&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;code&gt;journalctl -u&lt;/code&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;code&gt;systemctl start quant-signal.service&lt;/code&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;code&gt;Persistent=true&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;随机延迟（避免整点打爆 API）&lt;/td&gt;
 &lt;td&gt;不支持&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;RandomizedDelaySec=&lt;/code&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;code&gt;MemoryMax&lt;/code&gt; / &lt;code&gt;CPUQuota&lt;/code&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/systemd/system/quant-signal.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Daily quant signal pipeline&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Wants&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;network-online.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;After&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;network-online.target postgresql.service&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;&lt;span style="color:#a6e22e"&gt;StartLimitIntervalSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;3600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;StartLimitBurst&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;3&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;[Service]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Type&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;oneshot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;User&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;siyuan&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Group&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;siyuan&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;WorkingDirectory&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/home/siyuan/code/python/quant&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;EnvironmentFile&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/etc/quant/quant.env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ExecStart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/home/siyuan/miniconda3/envs/quant/bin/python -m quant&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 超时保护：超过 30 分钟说明卡住了&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;TimeoutStartSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;1800&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Restart&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;on-failure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RestartSec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;120&lt;/span&gt;
&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;&lt;span style="color:#a6e22e"&gt;MemoryMax&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;6G&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;CPUQuota&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;300%&lt;/span&gt;
&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;&lt;span style="color:#a6e22e"&gt;NoNewPrivileges&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;PrivateTmp&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ProtectSystem&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;strict&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ProtectHome&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;read-only&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;ReadWritePaths&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;/home/siyuan/code/python/quant/logs&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;StandardOutput&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;journal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;StandardError&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;journal&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;SyslogIdentifier&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;quant-signal&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;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;WantedBy&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;multi-user.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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-ini" data-lang="ini"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/systemd/system/quant-signal.timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;[Unit]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Description&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Run quant signal pipeline after market close&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;[Timer]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 17:30 触发；A 股 15:00 收盘，盘后数据源一般 16:30 之前更新完毕&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;OnCalendar&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;Mon..Fri 17:30:00&lt;/span&gt;
&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;&lt;span style="color:#a6e22e"&gt;Persistent&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# 随机延迟 0~10 分钟，避免每天同一秒打爆 akshare 接口&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;RandomizedDelaySec&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;600&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;Unit&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;quant-signal.service&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;[Install]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;WantedBy&lt;/span&gt;&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;timers.target&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl daemon-reload
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl enable --now quant-signal.timer
&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;# 看下次触发时间&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;systemctl list-timers quant-signal.timer
&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;# 立即手动跑一次（验证用）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;sudo systemctl start quant-signal.service
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;journalctl -u quant-signal.service -n &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt; --no-pager
&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;# 查看历史所有执行&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;journalctl -u quant-signal.service --since &lt;span style="color:#e6db74"&gt;&amp;#34;7 days ago&amp;#34;&lt;/span&gt; | grep -E &lt;span style="color:#e6db74"&gt;&amp;#34;完成|失败|跳过&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;EnvironmentFile&lt;/code&gt; 内容（权限 &lt;code&gt;600&lt;/code&gt;，属主 root）：&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/quant/quant.env&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;QUANT_DSN&lt;span style="color:#f92672"&gt;=&lt;/span&gt;postgresql+psycopg2://quant:***@127.0.0.1:5432/quant
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WECOM_WEBHOOK&lt;span style="color:#f92672"&gt;=&lt;/span&gt;https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key&lt;span style="color:#f92672"&gt;=&lt;/span&gt;***
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MODEL_VERSION&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lgb-20261020
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DRY_RUN&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="七心跳与告警让沉默也能被发现"&gt;七、心跳与告警：让沉默也能被发现
&lt;/h2&gt;&lt;p&gt;最危险的故障不是报错，而是&lt;strong&gt;什么都没发生&lt;/strong&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;# quant/heartbeat.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; text
&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; quant.db &lt;span style="color:#f92672"&gt;import&lt;/span&gt; engine
&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;beat&lt;/span&gt;(name: str, ok: bool, msg: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &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:#66d9ef"&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; engine&lt;span style="color:#f92672"&gt;.&lt;/span&gt;begin() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; conn:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;execute(text(&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;&lt;span style="color:#e6db74"&gt; INSERT INTO heartbeat (task, last_run_at, ok, msg)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VALUES (:t, now(), :ok, :msg)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ON CONFLICT (task) DO UPDATE
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; SET last_run_at = now(), ok = EXCLUDED.ok, msg = EXCLUDED.msg
&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 style="color:#e6db74"&gt;&amp;#34;t&amp;#34;&lt;/span&gt;: name, &lt;span style="color:#e6db74"&gt;&amp;#34;ok&amp;#34;&lt;/span&gt;: ok, &lt;span style="color:#e6db74"&gt;&amp;#34;msg&amp;#34;&lt;/span&gt;: msg[:&lt;span style="color:#ae81ff"&gt;500&lt;/span&gt;]})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;再挂一个每天中午的看门狗 timer：如果 &lt;code&gt;quant-signal&lt;/code&gt; 最近一次心跳早于上一个交易日 18:00，就推一条告警。&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;# quant/watchdog.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; datetime &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; dt
&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; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; text
&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; quant.calendar &lt;span style="color:#f92672"&gt;import&lt;/span&gt; last_trading_day
&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; quant.db &lt;span style="color:#f92672"&gt;import&lt;/span&gt; engine
&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; quant.notify &lt;span style="color:#f92672"&gt;import&lt;/span&gt; push_alert
&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;check&lt;/span&gt;() &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; int:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;with&lt;/span&gt; engine&lt;span style="color:#f92672"&gt;.&lt;/span&gt;connect() &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; conn:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; row &lt;span style="color:#f92672"&gt;=&lt;/span&gt; conn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;execute(text(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;SELECT last_run_at, ok FROM heartbeat WHERE task = &amp;#39;quant-signal&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;first()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; today &lt;span style="color:#f92672"&gt;=&lt;/span&gt; dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date&lt;span style="color:#f92672"&gt;.&lt;/span&gt;today()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; expect_after &lt;span style="color:#f92672"&gt;=&lt;/span&gt; dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;datetime&lt;span style="color:#f92672"&gt;.&lt;/span&gt;combine(last_trading_day(today), dt&lt;span style="color:#f92672"&gt;.&lt;/span&gt;time(&lt;span style="color:#ae81ff"&gt;18&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;if&lt;/span&gt; row &lt;span style="color:#f92672"&gt;is&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;None&lt;/span&gt; &lt;span style="color:#f92672"&gt;or&lt;/span&gt; row&lt;span style="color:#f92672"&gt;.&lt;/span&gt;last_run_at &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; expect_after:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; push_alert(&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;⚠️ 量化信号未在 &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;expect_after&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; 前完成，请检查 journalctl -u quant-signal&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; &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 style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; row&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ok:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; push_alert(&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; &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 style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="八踩过的坑"&gt;八、踩过的坑
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;OnCalendar&lt;/code&gt; 的时间是本地时区&lt;/strong&gt;，但容器里常常是 UTC。如果哪天把服务塞进容器，17:30 会变成凌晨 1:30。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WorkingDirectory&lt;/code&gt; 必须显式设置&lt;/strong&gt;，否则 &lt;code&gt;Type=oneshot&lt;/code&gt; 的工作目录是 &lt;code&gt;/&lt;/code&gt;，相对路径的配置、模型文件全部找不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ProtectHome=read-only&lt;/code&gt; 与 conda 环境&lt;/strong&gt;：conda 在某些操作下要写 &lt;code&gt;~/.cache&lt;/code&gt;，如果策略要写缓存目录，得把 &lt;code&gt;ReadWritePaths&lt;/code&gt; 加上。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;akshare 接口偶尔 503&lt;/strong&gt;。重试要带指数退避，而且要区分&amp;quot;接口挂了&amp;quot;和&amp;quot;今天本来没数据&amp;quot;——前者该重试，后者重试只会放大问题。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;推送成功 ≠ 你看到了&lt;/strong&gt;。机器人被移出群、被限流（每分钟 20 条）都会静默失败，所以 &lt;code&gt;push_log&lt;/code&gt; 与心跳两者都要有。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别在 &lt;code&gt;ExecStart&lt;/code&gt; 里用 shell 管道&lt;/strong&gt;。&lt;code&gt;ExecStart&lt;/code&gt; 不经过 shell，&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt;、&lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt; 都不会生效；需要 shell 就写包装脚本。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;这条链路跑通之后，整个量化研究才算真正闭环：&lt;strong&gt;研究 → 回测 → 上线 → 反馈&lt;/strong&gt;。每天早上八点看手机上那几条消息，比看任何回测曲线都更能暴露问题——真实的价格、真实的公告、真实的滑点，会立刻告诉你模型对不对。&lt;/p&gt;
&lt;p&gt;六篇量化系列的导航：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/quant-factor-engineering/" &gt;因子工程：从行情到可检验的 Alpha&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/timescaledb-market-data-modeling/" &gt;TimescaleDB 行情库进阶&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/vectorized-backtest-engine/" &gt;向量化回测引擎&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/ma-momentum-rotation-strategy/" &gt;双均线与动量轮动策略实战&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/" &gt;LightGBM 截面选股&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://yuansima.com/p/tft-stgnn-quant-rocm/" &gt;TFT 与 ST-GNN 的真实收益边界&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;远思为径，码动价值。让它每天自己跑起来，是量化从&amp;quot;研究&amp;quot;变成&amp;quot;系统&amp;quot;的最后一步。&lt;/p&gt;</description></item><item><title>TFT 与 ST-GNN：深度时序模型在 A 股选股中的真实收益边界</title><link>https://yuansima.com/p/tft-stgnn-quant-rocm/</link><pubDate>Fri, 23 Oct 2026 21:10:00 +0800</pubDate><guid>https://yuansima.com/p/tft-stgnn-quant-rocm/</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/lightgbm-cross-sectional-stock-prediction/" &gt;LightGBM 那篇&lt;/a&gt;的结论是：多个弱因子合成后 RankIC 0.042。想要再往上，直觉上有两条路——&lt;strong&gt;捕捉时序结构&lt;/strong&gt;（今天的形态和 20 天前有关）与&lt;strong&gt;捕捉截面结构&lt;/strong&gt;（同行业的股票会互相传染）。&lt;/p&gt;
&lt;p&gt;这两条正好对应 TFT 与 ST-GNN。这篇记录实验室在这两个模型上的实测，包括一个不太讨喜的结论：&lt;strong&gt;它们在 IC 上没有赢过 GBDT，只在组合稳定性上略有优势，而推理成本高一个数量级&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="一为什么深度模型在这类任务上不占优势"&gt;一、为什么深度模型在这类任务上不占优势
&lt;/h2&gt;&lt;p&gt;先说清楚约束条件，否则容易陷入&amp;quot;模型越大越好&amp;quot;的误区：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;样本量陷阱&lt;/strong&gt;：A 股每天约 5000 只股票 × 250 天 ≈ 125 万样本/年，听起来很多，但&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;：因子有效期 5~20 个交易日，模型学到的模式可能在下个季度就失效。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此深度模型在 A 股的正确姿势不是&amp;quot;替代 GBDT&amp;quot;，而是&amp;quot;&lt;strong&gt;提供 GBDT 看不到的那部分信息&lt;/strong&gt;&amp;quot;：时序上的形态演变、行业间的溢出。&lt;/p&gt;
&lt;h2 id="二数据张量化从面板到-样本-时间步-特征"&gt;二、数据张量化：从面板到 (样本, 时间步, 特征)
&lt;/h2&gt;&lt;p&gt;TFT 需要三维输入。构造滑动窗口时要格外小心——&lt;strong&gt;窗口内不能混入标签期之后的数据&lt;/strong&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;# ml/deep/dataset.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 style="color:#f92672"&gt;import&lt;/span&gt; torch
&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; torch.utils.data &lt;span style="color:#f92672"&gt;import&lt;/span&gt; Dataset
&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;FEATURES &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;mom_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_60&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;rev_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;vol_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;turn_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amihud&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;
&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;WindowDataset&lt;/span&gt;(Dataset):
&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;把面板数据切成 (lookback, n_features) 的时间窗，预测未来 horizon 日截面排名。
&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; 关键约定：window 截止于 t 日（含），标签是 t+1..t+horizon 的收益。
&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;
&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;__init__&lt;/span&gt;(self, panel: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, 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; lookback: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;, horizon: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, min_hist: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;250&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;horizon &lt;span style="color:#f92672"&gt;=&lt;/span&gt; horizon
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# panel: 多级索引 (trade_date, symbol)，列为特征&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dates &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sorted(panel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_level_values(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unique())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;panel &lt;span style="color:#f92672"&gt;=&lt;/span&gt; panel
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;close &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close
&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; xs, ys, ds &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; i, date &lt;span style="color:#f92672"&gt;in&lt;/span&gt; enumerate(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dates):
&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; i &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; min_hist &lt;span style="color:#f92672"&gt;or&lt;/span&gt; i &lt;span style="color:#f92672"&gt;+&lt;/span&gt; horizon &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; len(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dates):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; window &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dates[i &lt;span style="color:#f92672"&gt;-&lt;/span&gt; lookback &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;: i &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; block &lt;span style="color:#f92672"&gt;=&lt;/span&gt; panel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[window] &lt;span style="color:#75715e"&gt;# (lookback * n_symbols, n_feat)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; symbols &lt;span style="color:#f92672"&gt;=&lt;/span&gt; block&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_level_values(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unique()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; arr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stack([
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; block&lt;span style="color:#f92672"&gt;.&lt;/span&gt;xs(d, level&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;reindex(symbols)[FEATURES]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy()
&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; d &lt;span style="color:#f92672"&gt;in&lt;/span&gt; window
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ]) &lt;span style="color:#75715e"&gt;# (lookback, n_symbols, n_feat)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; arr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nan_to_num(arr, nan&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; xs&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tensor(arr&lt;span style="color:#f92672"&gt;.&lt;/span&gt;transpose(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;), dtype&lt;span style="color:#f92672"&gt;=&lt;/span&gt;torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;float32)) &lt;span style="color:#75715e"&gt;# (n_symbols, lookback, n_feat)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ds&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(date)
&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; c &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date, symbols]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; c_end &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dates[i &lt;span style="color:#f92672"&gt;+&lt;/span&gt; horizon], symbols]
&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; (c_end &lt;span style="color:#f92672"&gt;/&lt;/span&gt; c &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;rank(pct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;) &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&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:#75715e"&gt;# 截面排名标签&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ys&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tensor(ret&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy(), dtype&lt;span style="color:#f92672"&gt;=&lt;/span&gt;torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;float32))
&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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x &lt;span style="color:#f92672"&gt;=&lt;/span&gt; xs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ys
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;date_index &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ds
&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;__len__&lt;/span&gt;(self) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; int:
&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; len(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x)
&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;__getitem__&lt;/span&gt;(self, idx: int):
&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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;x[idx], self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;y[idx]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;内存是第一个坎：&lt;code&gt;n_symbols × lookback × n_features&lt;/code&gt; 的 float32 张量，5000 × 60 × 7 ≈ 8.4 MB/天。1200 天就是 10 GB。所以在 48GB UMA 的机器上，实验室的折衷是 &lt;strong&gt;lookback=60、特征 7 个、只取沪深 300 + 中证 500 成分股&lt;/strong&gt;（约 800 只），单天 1.3 MB，全量 1.5 GB，正好塞进内存。&lt;/p&gt;
&lt;h2 id="三tft可解释性才是它的卖点"&gt;三、TFT：可解释性才是它的卖点
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://github.com/jdb78/pytorch-forecasting" target="_blank" rel="noopener"
 &gt;pytorch-forecasting&lt;/a&gt; 的 TFT 把&amp;quot;变量选择网络（VSN）+ 多头注意力&amp;quot;包好了，我们只需要喂 &lt;code&gt;TimeSeriesDataSet&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;# ml/deep/tft_model.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; lightning.pytorch &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; pl
&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; pytorch_forecasting &lt;span style="color:#f92672"&gt;import&lt;/span&gt; TemporalFusionTransformer, TimeSeriesDataSet
&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; pytorch_forecasting.metrics &lt;span style="color:#f92672"&gt;import&lt;/span&gt; QuantileLoss
&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;CONFIG &lt;span style="color:#f92672"&gt;=&lt;/span&gt; dict(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_encoder_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;60&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_prediction_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;5&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; hidden_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; attention_head_size&lt;span style="color:#f92672"&gt;=&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; dropout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hidden_continuous_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; learning_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1e-3&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; batch_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;256&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_epochs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;30&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&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_dataset&lt;/span&gt;(df, predict: bool &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;, training: TimeSeriesDataSet &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; TimeSeriesDataSet:
&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; TimeSeriesDataSet(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; df,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; time_idx&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;time_idx&amp;#34;&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;&lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; group_ids&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_encoder_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;max_encoder_length&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_prediction_length&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;max_prediction_length&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; time_varying_known_reals&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;time_idx&amp;#34;&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; time_varying_unknown_reals&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;mom_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_60&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;rev_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;vol_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;turn_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amihud&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; static_categoricals&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; predict_mode&lt;span style="color:#f92672"&gt;=&lt;/span&gt;predict,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; training&lt;span style="color:#f92672"&gt;=&lt;/span&gt;training,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; allow_missing_timesteps&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; add_relative_time_idx&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&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&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_trainer&lt;/span&gt;(gpus: int &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;gt;&lt;/span&gt; pl&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Trainer:
&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; pl&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Trainer(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; max_epochs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;max_epochs&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; accelerator&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;gpu&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; gpus &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;cpu&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; precision&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;bf16-mixed&amp;#34;&lt;/span&gt;, &lt;span style="color:#75715e"&gt;# ROCm 上 bf16 比 fp16 稳定&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; gradient_clip_val&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; enable_checkpointing&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; logger&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; enable_progress_bar&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&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&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_model&lt;/span&gt;(training_ds: TimeSeriesDataSet) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; TemporalFusionTransformer:
&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; TemporalFusionTransformer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;from_dataset(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; training_ds,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; learning_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;learning_rate&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hidden_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;hidden_size&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; attention_head_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;attention_head_size&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dropout&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;dropout&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; hidden_continuous_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;CONFIG[&lt;span style="color:#e6db74"&gt;&amp;#34;hidden_continuous_size&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; output_size&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;, &lt;span style="color:#75715e"&gt;# 7 个分位数（默认 QuantileLoss）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; loss&lt;span style="color:#f92672"&gt;=&lt;/span&gt;QuantileLoss(),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; reduce_on_plateau_patience&lt;span style="color:#f92672"&gt;=&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&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;TFT 真正的价值不在精度，而在&lt;strong&gt;可解释性输出&lt;/strong&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;interpretation&lt;/span&gt;(model, trainer, dataloader) &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; raw &lt;span style="color:#f92672"&gt;=&lt;/span&gt; model&lt;span style="color:#f92672"&gt;.&lt;/span&gt;predict(dataloader, mode&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;raw&amp;#34;&lt;/span&gt;, return_x&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, trainer&lt;span style="color:#f92672"&gt;=&lt;/span&gt;trainer)
&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; model&lt;span style="color:#f92672"&gt;.&lt;/span&gt;interpret_output(raw&lt;span style="color:#f92672"&gt;.&lt;/span&gt;output, reduction&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;sum&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;# attention / 变量权重&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;interpretation&lt;/code&gt; 会给你三样东西：&lt;strong&gt;每个历史时间步的注意力权重&lt;/strong&gt;（模型在关注 5 天前还是 40 天前）、&lt;strong&gt;每个变量的重要度&lt;/strong&gt;（动量还是换手在起作用）、&lt;strong&gt;持续性权重&lt;/strong&gt;（历史形态对未来预测的持续影响）。这三样东西能反过来修正因子设计，比 IC 高 0.005 有用得多。&lt;/p&gt;
&lt;h2 id="四st-gnn把行业关系建成图"&gt;四、ST-GNN：把行业关系建成图
&lt;/h2&gt;&lt;p&gt;截面信息传播的想法很自然：某只股票异动，同行业/上下游的股票往往跟随。用图神经网络表达就是：&lt;strong&gt;特征矩阵 + 邻接矩阵 → 消息传递 → 更新的特征&lt;/strong&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;# ml/deep/stgnn.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; torch
&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; torch.nn.functional &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; F
&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; torch &lt;span style="color:#f92672"&gt;import&lt;/span&gt; nn
&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; torch_geometric.nn &lt;span style="color:#f92672"&gt;import&lt;/span&gt; GCNConv
&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_industry_adjacency&lt;/span&gt;(industry: torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tensor, n_keep: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tensor:
&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;同一行业的股票两两连边，并做行归一化。industry: (n_symbols,) 的整数标签。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&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; industry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;size(&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; same &lt;span style="color:#f92672"&gt;=&lt;/span&gt; industry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unsqueeze(&lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) &lt;span style="color:#f92672"&gt;==&lt;/span&gt; industry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unsqueeze(&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; adj &lt;span style="color:#f92672"&gt;=&lt;/span&gt; same&lt;span style="color:#f92672"&gt;.&lt;/span&gt;float()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adj&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fill_diagonal_(&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:#75715e"&gt;# 行业过大时每行只保留相似度最高的 n_keep 个（防止金融板块把噪声全拉进来）&lt;/span&gt;
&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_keep:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; topv, topi &lt;span style="color:#f92672"&gt;=&lt;/span&gt; adj&lt;span style="color:#f92672"&gt;.&lt;/span&gt;topk(min(n_keep &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, n), dim&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; keep &lt;span style="color:#f92672"&gt;=&lt;/span&gt; torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;zeros_like(adj)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; keep&lt;span style="color:#f92672"&gt;.&lt;/span&gt;scatter_(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, topi, &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; adj &lt;span style="color:#f92672"&gt;=&lt;/span&gt; adj &lt;span style="color:#f92672"&gt;*&lt;/span&gt; keep
&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; adj &lt;span style="color:#f92672"&gt;/&lt;/span&gt; adj&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum(dim&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, keepdim&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;clamp_min(&lt;span style="color:#ae81ff"&gt;1e-9&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;class&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;STGNNBlock&lt;/span&gt;(nn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Module):
&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;空间（GCN 传递截面信息）+ 时间（GRU 聚合时序）的交替堆叠。&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&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;__init__&lt;/span&gt;(self, n_feat: int, hidden: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;, edge_index: torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tensor &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; super()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;__init__&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gcn &lt;span style="color:#f92672"&gt;=&lt;/span&gt; GCNConv(n_feat, hidden)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gru &lt;span style="color:#f92672"&gt;=&lt;/span&gt; nn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;GRU(hidden, hidden, batch_first&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, num_layers&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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;head &lt;span style="color:#f92672"&gt;=&lt;/span&gt; nn&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Linear(hidden, &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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;edge_index &lt;span style="color:#f92672"&gt;=&lt;/span&gt; edge_index
&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;forward&lt;/span&gt;(self, x: torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tensor) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Tensor:
&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;x: (n_symbols, lookback, n_feat) -&amp;gt; (n_symbols,) 预测分数。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; n, t, f &lt;span style="color:#f92672"&gt;=&lt;/span&gt; x&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shape
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; feats &lt;span style="color:#f92672"&gt;=&lt;/span&gt; x&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reshape(n &lt;span style="color:#f92672"&gt;*&lt;/span&gt; t, f)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 空间聚合：每行构造 block-diagonal 的边索引&lt;/span&gt;
&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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;edge_index &lt;span style="color:#f92672"&gt;is&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; &lt;span style="color:#66d9ef"&gt;raise&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ValueError&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;edge_index 未设置&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; src, dst &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;edge_index
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ei &lt;span style="color:#f92672"&gt;=&lt;/span&gt; torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cat([src &lt;span style="color:#f92672"&gt;+&lt;/span&gt; i &lt;span style="color:#f92672"&gt;*&lt;/span&gt; n &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(t)])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;long(), torch&lt;span style="color:#f92672"&gt;.&lt;/span&gt;cat([dst &lt;span style="color:#f92672"&gt;+&lt;/span&gt; i &lt;span style="color:#f92672"&gt;*&lt;/span&gt; n &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(t)])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;long()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; h &lt;span style="color:#f92672"&gt;=&lt;/span&gt; F&lt;span style="color:#f92672"&gt;.&lt;/span&gt;relu(self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gcn(feats, ei))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; h &lt;span style="color:#f92672"&gt;=&lt;/span&gt; h&lt;span style="color:#f92672"&gt;.&lt;/span&gt;view(n, t, &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; out, _ &lt;span style="color:#f92672"&gt;=&lt;/span&gt; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;gru(h)
&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; self&lt;span style="color:#f92672"&gt;.&lt;/span&gt;head(out[:, &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;squeeze(&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;/code&gt;&lt;/pre&gt;&lt;/div&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;IC 均值&lt;/th&gt;
 &lt;th&gt;备注&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;不建图（GRU-only）&lt;/td&gt;
 &lt;td&gt;0.031&lt;/td&gt;
 &lt;td&gt;基线&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;同行业连边&lt;/td&gt;
 &lt;td&gt;0.036&lt;/td&gt;
 &lt;td&gt;稳定提升&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;行业 + 20 日收益相关性 top-3&lt;/td&gt;
 &lt;td&gt;0.038&lt;/td&gt;
 &lt;td&gt;提升但噪声变大&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;行业 + 供应链（供应商/客户）&lt;/td&gt;
 &lt;td&gt;0.033&lt;/td&gt;
 &lt;td&gt;数据稀疏，覆盖度不足&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;供应链图理论上最有价值，但 A 股的供应商客户关系披露覆盖率太低，最终 30% 的股票没有一条边。&lt;strong&gt;图的覆盖率决定了图模型的上限&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="五rocm-上的训练细节"&gt;五、ROCm 上的训练细节
&lt;/h2&gt;&lt;p&gt;&lt;a class="link" href="https://yuansima.com/p/install-llama-cpp-ubuntu-2604-rocm/" &gt;本地算力节点&lt;/a&gt;是 AMD Radeon 890M（gfx1150）+ 48GB UMA 内存，ROCm 7.2.3。要点：&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-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# PyTorch 必须装 ROCm 构建版本&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pip install torch torchvision --index-url https://download.pytorch.org/whl/rocm7.2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;pip install pytorch-forecasting pytorch-lightning torch-geometric
&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;# 核显架构白名单覆盖（890M -&amp;gt; gfx1150）&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;export HSA_OVERRIDE_GFX_VERSION&lt;span style="color:#f92672"&gt;=&lt;/span&gt;11.5.0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;python -c &lt;span style="color:#e6db74"&gt;&amp;#34;import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;precision=&amp;quot;bf16-mixed&amp;quot;&lt;/code&gt; 是必需的&lt;/strong&gt;。核显上 fp16 的 loss scaling 容易溢出，训练到第 10 个 epoch 左右突然 loss 变 NaN，我为此浪费了两天。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UMA 内存是把双刃剑&lt;/strong&gt;：48GB 共享内存意味着 batch 可以开很大，但也意味着 CPU 内存吃紧时会直接 OOM。建议 &lt;code&gt;export HSA_ENABLE_SDMA=0&lt;/code&gt; 减少内存拷贝抖动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;torch_geometric&lt;/code&gt; 的 scatter 操作在 ROCm 上比 CUDA 慢 20~30%&lt;/strong&gt;，图很大时要控制每行邻居数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实测训练时间（800 只股票 × 1200 天，lookback 60）：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;模型&lt;/th&gt;
 &lt;th&gt;单 epoch&lt;/th&gt;
 &lt;th&gt;30 epoch&lt;/th&gt;
 &lt;th&gt;显存峰值&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;LightGBM（CPU）&lt;/td&gt;
 &lt;td&gt;—&lt;/td&gt;
 &lt;td&gt;约 40 秒&lt;/td&gt;
 &lt;td&gt;—&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;TFT（hidden 64）&lt;/td&gt;
 &lt;td&gt;95 秒&lt;/td&gt;
 &lt;td&gt;约 48 分钟&lt;/td&gt;
 &lt;td&gt;3.1 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ST-GNN（4 层）&lt;/td&gt;
 &lt;td&gt;60 秒&lt;/td&gt;
 &lt;td&gt;约 30 分钟&lt;/td&gt;
 &lt;td&gt;2.4 GB&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="六结果一个不性感的对照表"&gt;六、结果：一个不性感的对照表
&lt;/h2&gt;&lt;p&gt;同一份特征、同一份样本、同一套 purged 时序切分，三个模型的表现：&lt;/p&gt;
&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;模型&lt;/th&gt;
 &lt;th&gt;RankIC&lt;/th&gt;
 &lt;th&gt;ICIR&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;LightGBM&lt;/td&gt;
 &lt;td&gt;0.042&lt;/td&gt;
 &lt;td&gt;0.31&lt;/td&gt;
 &lt;td&gt;16.4%&lt;/td&gt;
 &lt;td&gt;-12.7%&lt;/td&gt;
 &lt;td&gt;0.8 秒&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;TFT&lt;/td&gt;
 &lt;td&gt;0.038&lt;/td&gt;
 &lt;td&gt;0.29&lt;/td&gt;
 &lt;td&gt;17.9%&lt;/td&gt;
 &lt;td&gt;-9.4%&lt;/td&gt;
 &lt;td&gt;6.2 秒&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ST-GNN&lt;/td&gt;
 &lt;td&gt;0.036&lt;/td&gt;
 &lt;td&gt;0.26&lt;/td&gt;
 &lt;td&gt;15.1%&lt;/td&gt;
 &lt;td&gt;-11.8%&lt;/td&gt;
 &lt;td&gt;4.5 秒&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论得诚实地说：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;IC 上 GBDT 依然最好&lt;/strong&gt;。深度模型没有&amp;quot;发现&amp;quot;新 alpha；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TFT 的多空组合回撤更小&lt;/strong&gt;（-9.4% vs -12.7%），说明它学到的东西更&amp;quot;稳&amp;quot;，可能与时序注意力对趋势转折的敏感性有关；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;深度模型的推理成本高 5~8 倍&lt;/strong&gt;，对日频策略无所谓，但如果做分钟级就完全不可接受。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实验室最终的选择是：&lt;strong&gt;主策略用 LightGBM，TFT 的输出作为&amp;quot;风格因子&amp;quot;叠加&lt;/strong&gt;——两者相关性只有 0.6 左右，合成的 IC 能达到 0.048，这是深度模型唯一真正值钱的地方：&lt;strong&gt;它与 GBDT 的误差不相关&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="七踩坑清单"&gt;七、踩坑清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;窗口跨越停牌期&lt;/strong&gt;：停牌 3 个月后复牌，窗口里 60 个交易日中有 40 个是 NaN，&lt;code&gt;nan_to_num&lt;/code&gt; 填 0 会让模型以为&amp;quot;这只股票波动突然归零&amp;quot;。正确做法是给一个 &lt;code&gt;is_suspended&lt;/code&gt; 掩码。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;样本权重&lt;/strong&gt;：不同日期的股票数量差异很大（2015 年牛市 vs 2018 年熊市），不加权会让模型偏向样本多的时期。实验室用 &lt;code&gt;1 / 当日样本数&lt;/code&gt; 做权重。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;随机种子敏感&lt;/strong&gt;：深度模型的 IC 在不同种子下波动 ±0.006，报告结果必须给出&lt;strong&gt;多个种子的均值&lt;/strong&gt;，否则就是在挑好看的种子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;predict_mode&lt;/code&gt; 与 &lt;code&gt;training&lt;/code&gt; 参数&lt;/strong&gt;：pytorch-forecasting 里构造验证集时必须传 &lt;code&gt;training=training_dataset&lt;/code&gt;，否则特征缩放不一致，验证集 IC 会莫名其妙地低。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要用全样本拟合归一化&lt;/strong&gt;：&lt;code&gt;GroupNormalizer&lt;/code&gt; 的 &lt;code&gt;groups&lt;/code&gt; 参数决定了按什么分组归一化，用 &lt;code&gt;symbol&lt;/code&gt; 会导致每只股票各自的尺度，跨股票不可比。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;写到这里，量化这条线上的核心环节都跑通了：&lt;a class="link" href="https://yuansima.com/p/timescaledb-market-data-modeling/" &gt;数据&lt;/a&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;a class="link" href="https://yuansima.com/p/ma-momentum-rotation-strategy/" &gt;策略&lt;/a&gt; → &lt;a class="link" href="https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/" &gt;机器学习&lt;/a&gt; → &lt;a class="link" href="https://yuansima.com/p/tft-stgnn-quant-rocm/" &gt;深度模型&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;但研究做完不等于能跑。最后一篇回到工程：把这套流程变成&lt;strong&gt;每天早上 8 点自动生成信号并推送到手机&lt;/strong&gt;的服务，用 systemd timer 而不是 crontab，因为失败重试、日志与依赖管理都要靠它。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。深度模型不会给你 alpha，但它会给你更清楚的自我怀疑。&lt;/p&gt;</description></item><item><title>LightGBM 截面选股：把因子流水线变成预测模型</title><link>https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/</link><pubDate>Wed, 21 Oct 2026 20:20:00 +0800</pubDate><guid>https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/</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/ma-momentum-rotation-strategy/" &gt;上一篇文章&lt;/a&gt;的策略是&amp;quot;人工写规则&amp;quot;，参数换了就要重跑一遍。机器学习选股的思路不同：&lt;strong&gt;把多个因子交给模型，让它自己学加权方式&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;听起来很美，但实验室第一次做的时候，IC 跑到了 0.15（这个数字在 A 股日频里强得不像真的），回测年化 60%。查了两天才发现——&lt;strong&gt;标签用到了未来 5 天的收盘价，而特征里的换手率是&amp;quot;未来 5 天平均换手&amp;quot;&lt;/strong&gt;。这不是机器学习，这是拿着答案考试。&lt;/p&gt;
&lt;p&gt;这篇把正确的流程完整走一遍，重点标出那些&amp;quot;看起来没问题、其实在泄露&amp;quot;的环节。&lt;/p&gt;
&lt;h2 id="一问题定义截面回归不是时间序列预测"&gt;一、问题定义：截面回归，不是时间序列预测
&lt;/h2&gt;&lt;p&gt;先说清楚我们在预测什么：&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;对每个交易日 t，输入当期所有股票的因子截面，输出每只股票&lt;strong&gt;未来 5 个交易日收益的横截面排名&lt;/strong&gt;。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;为什么是排名而不是绝对收益？因为 A 股有系统性涨跌（beta），模型容易把&amp;quot;市场要涨&amp;quot;学成&amp;quot;所有股票都好&amp;quot;。&lt;strong&gt;去掉截面均值之后，模型只能学相对强弱&lt;/strong&gt;，这也和最终&amp;quot;买前 N 只&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;# ml/labels.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&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;forward_return&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, horizon: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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;未来 horizon 日收益。注意：这里的时间箭头是指向未来的，永远只能用于标签。&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; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(&lt;span style="color:#f92672"&gt;-&lt;/span&gt;horizon) &lt;span style="color:#f92672"&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&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;cross_section_rank&lt;/span&gt;(y: 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;截面排序归一化到 [-1, 1]，比 z-score 更抗极端值。&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; y&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rank(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, pct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;) &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &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;/code&gt;&lt;/pre&gt;&lt;/div&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;code&gt;factor_value&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;说明&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;code&gt;mom_5 / mom_20 / mom_60 / mom_120&lt;/code&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;code&gt;rev_5 / rev_20&lt;/code&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;code&gt;vol_20 / vol_60 / max_dd_60&lt;/code&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;code&gt;turn_20 / amount_20 / amihud&lt;/code&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;code&gt;pe_ttm / pb / ps_ttm&lt;/code&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;code&gt;industry&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;类别特征，LightGBM 原生支持&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键纪律：&lt;strong&gt;所有特征都必须在截面上做稳健标准化&lt;/strong&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;# ml/features.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;load_factor_panel&lt;/span&gt;(factor_names: list[str], start: str, end: str) &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;从 factor_value 取宽表并堆成 (trade_date, symbol) 长表。&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:#f92672"&gt;from&lt;/span&gt; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; create_engine
&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; engine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; create_engine(&lt;span style="color:#e6db74"&gt;&amp;#34;postgresql+psycopg2://quant:quant@127.0.0.1:5432/quant&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sql &lt;span style="color:#f92672"&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;&lt;span style="color:#e6db74"&gt; SELECT trade_date, symbol, factor_name, z
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM factor_value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE factor_name = ANY(&lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(names)s&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; AND trade_date BETWEEN &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(start)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; AND &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(end)s&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; &amp;#34;&amp;#34;&amp;#34;&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; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read_sql(sql, engine, params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;names&amp;#34;&lt;/span&gt;: factor_names, &lt;span style="color:#e6db74"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: start, &lt;span style="color:#e6db74"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: end})
&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; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;pivot_table(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;, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;], columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;factor_name&amp;#34;&lt;/span&gt;, values&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;z&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;
&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;add_industry_and_label&lt;/span&gt;(panel: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, 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; industry: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, horizon: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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;拼上行业标签与截面排序标签，并丢弃最后 horizon 天（没有未来收益）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; y &lt;span style="color:#f92672"&gt;=&lt;/span&gt; cross_section_rank(forward_return(close, horizon))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; long &lt;span style="color:#f92672"&gt;=&lt;/span&gt; y&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stack()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rename(&lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reset_index()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; long&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;label&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; ind &lt;span style="color:#f92672"&gt;=&lt;/span&gt; industry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;stack()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rename(&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reset_index()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ind&lt;span style="color:#f92672"&gt;.&lt;/span&gt;columns &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;industry&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; out &lt;span style="color:#f92672"&gt;=&lt;/span&gt; panel&lt;span style="color:#f92672"&gt;.&lt;/span&gt;reset_index()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;merge(long, on&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;], how&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; out &lt;span style="color:#f92672"&gt;=&lt;/span&gt; out&lt;span style="color:#f92672"&gt;.&lt;/span&gt;merge(ind, on&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;], how&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;left&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;# 行业标签在每个截面内部统一成 category，未知记为 &amp;#34;UNKNOWN&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; out[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; out[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;fillna(&lt;span style="color:#e6db74"&gt;&amp;#34;UNKNOWN&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;astype(&lt;span style="color:#e6db74"&gt;&amp;#34;category&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; out&lt;span style="color:#f92672"&gt;.&lt;/span&gt;dropna(subset&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="三时序切分purged-与-embargo"&gt;三、时序切分：purged 与 embargo
&lt;/h2&gt;&lt;p&gt;这是整篇文章最重要的部分。机器学习课教你 K-Fold 随机切分，&lt;strong&gt;在量化里这是灾难&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;标签是未来 5 日收益，t 日的样本和 t+3 日的样本标签区间重叠 → 训练集和验证集&amp;quot;共享答案&amp;quot;；&lt;/li&gt;
&lt;li&gt;相邻交易日的特征高度自相关 → 模型只要记住&amp;quot;这几天&amp;quot;，就能在验证集上蒙对。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;实验室用的是&lt;strong&gt;按时间切分的 purged walk-forward + embargo&lt;/strong&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;# ml/split.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; 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;purged_time_split&lt;/span&gt;(dates: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, n_splits: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;, embargo: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; list[tuple]:
&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_splits 段，训练集与验证集之间留 embargo 天缓冲。
&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; 返回 [(train_mask, valid_mask), ...]，mask 是与 dates 同长度的布尔数组。
&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; uniq &lt;span style="color:#f92672"&gt;=&lt;/span&gt; sorted(dates&lt;span style="color:#f92672"&gt;.&lt;/span&gt;unique())
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fold &lt;span style="color:#f92672"&gt;=&lt;/span&gt; len(uniq) &lt;span style="color:#f92672"&gt;//&lt;/span&gt; (n_splits &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; idx &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(range(len(uniq)), index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;uniq)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; splits &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; k &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, n_splits &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; train_end &lt;span style="color:#f92672"&gt;=&lt;/span&gt; k &lt;span style="color:#f92672"&gt;*&lt;/span&gt; fold
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid_start &lt;span style="color:#f92672"&gt;=&lt;/span&gt; train_end &lt;span style="color:#f92672"&gt;+&lt;/span&gt; embargo
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid_end &lt;span style="color:#f92672"&gt;=&lt;/span&gt; valid_start &lt;span style="color:#f92672"&gt;+&lt;/span&gt; fold
&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; valid_end &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; len(uniq):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; train_days &lt;span style="color:#f92672"&gt;=&lt;/span&gt; set(uniq[:train_end])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid_days &lt;span style="color:#f92672"&gt;=&lt;/span&gt; set(uniq[valid_start:valid_end])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; splits&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append((dates&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isin(train_days), dates&lt;span style="color:#f92672"&gt;.&lt;/span&gt;isin(valid_days)))
&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; splits
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;embargo=5&lt;/code&gt; 的含义：训练集最后 5 天的样本，其标签区间与验证集第一天重叠，必须&lt;strong&gt;整体切除&lt;/strong&gt;。少了这一步，验证集 IC 能凭空高 30%。&lt;/p&gt;
&lt;h2 id="四训练lightgbm"&gt;四、训练：LightGBM
&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;# ml/train.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; lightgbm &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; lgb
&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;FEATURES &lt;span style="color:#f92672"&gt;=&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;mom_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_60&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;mom_120&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;rev_5&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;rev_20&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;vol_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;vol_60&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;max_dd_60&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;turn_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amount_20&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amihud&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;PARAMS &lt;span style="color:#f92672"&gt;=&lt;/span&gt; dict(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; objective&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;regression_l2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; metric&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;l2&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; learning_rate&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.03&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; num_leaves&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;15&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; min_data_in_leaf&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;200&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; feature_fraction&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.75&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bagging_fraction&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;0.8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; bagging_freq&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; lambda_l2&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;5.0&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; max_depth&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; verbosity&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; seed&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;42&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; num_threads&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;8&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&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;train_model&lt;/span&gt;(df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, train_mask, valid_mask) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Booster:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; X_tr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[train_mask, FEATURES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; y_tr &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[train_mask, &lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; X_va &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[valid_mask, FEATURES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; y_va &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[valid_mask, &lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy()
&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; dtrain &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dataset(X_tr, label&lt;span style="color:#f92672"&gt;=&lt;/span&gt;y_tr, categorical_feature&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;], free_raw_data&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; dvalid &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Dataset(X_va, label&lt;span style="color:#f92672"&gt;=&lt;/span&gt;y_va, categorical_feature&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;], reference&lt;span style="color:#f92672"&gt;=&lt;/span&gt;dtrain)
&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; booster &lt;span style="color:#f92672"&gt;=&lt;/span&gt; lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;train(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PARAMS,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dtrain,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; num_boost_round&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;2000&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid_sets&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[dtrain, dvalid],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; valid_names&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;train&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;valid&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; callbacks&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;early_stopping(&lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;, verbose&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;), lgb&lt;span style="color:#f92672"&gt;.&lt;/span&gt;log_evaluation(&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; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; booster
&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;num_leaves=15 + max_depth=5 + lambda_l2=5&lt;/code&gt;&lt;/strong&gt;：截面数据的信噪比极低（IC 0.03 就算不错），复杂模型几乎必然过拟合到某几段行情；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;min_data_in_leaf=200&lt;/code&gt;&lt;/strong&gt;：一天全市场几千只股票，叶子太小就等于记住个股；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;early_stopping&lt;/code&gt; 只看验证集&lt;/strong&gt;，测试集从头到尾不参与训练决策。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="五评价不看-r只看-ic-与分层"&gt;五、评价：不看 R²，只看 IC 与分层
&lt;/h2&gt;&lt;p&gt;回归任务的 &lt;code&gt;R²&lt;/code&gt; 在这里毫无意义（0.01 也算&amp;quot;显著&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;# ml/evaluate.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; scipy.stats &lt;span style="color:#f92672"&gt;import&lt;/span&gt; spearmanr
&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;predict_panel&lt;/span&gt;(booster, 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;Series:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pred &lt;span style="color:#f92672"&gt;=&lt;/span&gt; booster&lt;span style="color:#f92672"&gt;.&lt;/span&gt;predict(df[FEATURES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;industry&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;Series(pred, index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;df&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&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;ic_by_date&lt;/span&gt;(df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, pred: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, label_col: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&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; tmp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assign(pred&lt;span style="color:#f92672"&gt;=&lt;/span&gt;pred)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ic &lt;span style="color:#f92672"&gt;=&lt;/span&gt; tmp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;apply(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;lambda&lt;/span&gt; g: spearmanr(g[&lt;span style="color:#e6db74"&gt;&amp;#34;pred&amp;#34;&lt;/span&gt;], g[label_col])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;statistic
&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(g) &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt; &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; include_groups&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; )
&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; ic&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&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;ic_stats&lt;/span&gt;(ic: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series) &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:#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;IC均值&amp;#34;&lt;/span&gt;: round(float(ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()), &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;ICIR&amp;#34;&lt;/span&gt;: round(float(ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean() &lt;span style="color:#f92672"&gt;/&lt;/span&gt; ic&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:#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;IC&amp;gt;0占比&amp;#34;&lt;/span&gt;: round(float((ic &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 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;IC衰减(后20%)&amp;#34;&lt;/span&gt;: round(float(ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;iloc[&lt;span style="color:#f92672"&gt;-&lt;/span&gt;len(ic) &lt;span style="color:#f92672"&gt;//&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;:]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()), &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&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;long_short_backtest&lt;/span&gt;(df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, pred: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, top_pct: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0.1&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;按预测分位做多空组合，返回每日收益（未计成本）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tmp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;assign(pred&lt;span style="color:#f92672"&gt;=&lt;/span&gt;pred)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; daily &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; date, g &lt;span style="color:#f92672"&gt;in&lt;/span&gt; tmp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;):
&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; max(&lt;span style="color:#ae81ff"&gt;10&lt;/span&gt;, int(len(g) &lt;span style="color:#f92672"&gt;*&lt;/span&gt; top_pct))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; g &lt;span style="color:#f92672"&gt;=&lt;/span&gt; g&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_values(&lt;span style="color:#e6db74"&gt;&amp;#34;pred&amp;#34;&lt;/span&gt;, 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; daily&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append((date, g&lt;span style="color:#f92672"&gt;.&lt;/span&gt;head(n)[&lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&lt;/span&gt;]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean() &lt;span style="color:#f92672"&gt;-&lt;/span&gt; g&lt;span style="color:#f92672"&gt;.&lt;/span&gt;tail(n)[&lt;span style="color:#e6db74"&gt;&amp;#34;label&amp;#34;&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; s &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(dict(daily))&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_index()
&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; s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实验室的实测水平（示例，2018-2025 的日频、5 日持有）：&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;RankIC 均值&lt;/td&gt;
 &lt;td&gt;0.042&lt;/td&gt;
 &lt;td&gt;可用（&amp;gt; 0.03）&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;ICIR&lt;/td&gt;
 &lt;td&gt;0.31&lt;/td&gt;
 &lt;td&gt;弱但稳定&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;IC &amp;gt; 0 占比&lt;/td&gt;
 &lt;td&gt;0.58&lt;/td&gt;
 &lt;td&gt;尚可&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;多空年化（未计成本）&lt;/td&gt;
 &lt;td&gt;16.4%&lt;/td&gt;
 &lt;td&gt;与 IC 量级相符&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;多空最大回撤&lt;/td&gt;
 &lt;td&gt;-12.7%&lt;/td&gt;
 &lt;td&gt;可接受&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;注意这里的 IC 只有 0.042，远低于我第一版的 0.15。&lt;strong&gt;0.15 不是好消息，是报警信号&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="六过拟合诊断三个必查项"&gt;六、过拟合诊断：三个必查项
&lt;/h2&gt;&lt;h3 id="61-特征重要性是否稳定"&gt;6.1 特征重要性是否稳定
&lt;/h3&gt;&lt;p&gt;同一套流程在 2020-2022 和 2023-2025 两段数据上分别训练，如果两次的 top 特征完全不同，说明模型学的是噪声。&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;top_features&lt;/span&gt;(booster, k: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;6&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&amp;gt;&lt;/span&gt; list[str]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; imp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; booster&lt;span style="color:#f92672"&gt;.&lt;/span&gt;feature_importance(importance_type&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;gain&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; names &lt;span style="color:#f92672"&gt;=&lt;/span&gt; booster&lt;span style="color:#f92672"&gt;.&lt;/span&gt;feature_name()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; order &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;argsort(imp)[::&lt;span style="color:#f92672"&gt;-&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;][:k]
&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; [names[i] &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#f92672"&gt;in&lt;/span&gt; order]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;实验室两次训练的 top 特征都是 &lt;code&gt;rev_5 / mom_20 / turn_20 / vol_20&lt;/code&gt;，只有排序在变——这才叫稳定。&lt;/p&gt;
&lt;h3 id="62-分年度-ic-是否衰减"&gt;6.2 分年度 IC 是否衰减
&lt;/h3&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;by_year &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index&lt;span style="color:#f92672"&gt;.&lt;/span&gt;str[:&lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;agg([&lt;span style="color:#e6db74"&gt;&amp;#34;mean&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;count&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 2024 年之后 IC 突然接近 0，那不是&amp;quot;市场变了&amp;quot;，更可能是因子被交易过度拥挤了。&lt;strong&gt;任何因子都有生命周期&lt;/strong&gt;，要接受它。&lt;/p&gt;
&lt;h3 id="63-shap-方向是否反常识"&gt;6.3 SHAP 方向是否反常识
&lt;/h3&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:#f92672"&gt;import&lt;/span&gt; shap
&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;explain_one_day&lt;/span&gt;(booster, df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, date: str, top_n: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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; day &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df[df[&lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;==&lt;/span&gt; date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; explainer &lt;span style="color:#f92672"&gt;=&lt;/span&gt; shap&lt;span style="color:#f92672"&gt;.&lt;/span&gt;TreeExplainer(booster)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; sv &lt;span style="color:#f92672"&gt;=&lt;/span&gt; explainer&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shap_values(day[FEATURES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;]])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; shap_df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame(sv, columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;FEATURES &lt;span style="color:#f92672"&gt;+&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;industry&amp;#34;&lt;/span&gt;], index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;day&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;return&lt;/span&gt; shap_df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;abs()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()&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 style="color:#f92672"&gt;.&lt;/span&gt;head(top_n)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 SHAP 告诉你&amp;quot;换手率越低越好&amp;quot;，而你去查因子定义发现写反了符号——&lt;strong&gt;这类 bug 在 IC 评价里看不出来，但会在实盘里原形毕露&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="七从预测到组合别忽略这一步"&gt;七、从预测到组合：别忽略这一步
&lt;/h2&gt;&lt;p&gt;模型输出的是相对分数，不是权重。从分数到组合中间还有几件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;截断极值&lt;/strong&gt;：按预测分位取前 10%，而不是简单取 top 50 只（防止小盘股霸榜）；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行业约束&lt;/strong&gt;：行业偏离不超过 3%，否则模型会集中押注某几个行业；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;权重平滑&lt;/strong&gt;：用分数排名加权（&lt;code&gt;w ∝ rank&lt;/code&gt;），比等权更能利用信息，但换手更高；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在池内选股&lt;/strong&gt;：只在沪深 300 / 中证 500 成分股里选，避免流动性黑洞。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这些环节对最终净值的影响，往往比换一个更强的模型更大。&lt;/p&gt;
&lt;h2 id="八踩坑清单"&gt;八、踩坑清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;标签泄露&lt;/strong&gt;：&lt;code&gt;forward_return&lt;/code&gt; 只能用于 &lt;code&gt;y&lt;/code&gt;，如果它不小心进入特征表（比如按 symbol merge 时列名撞车），IC 会好到不真实。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;复权口径不一致&lt;/strong&gt;：特征用前复权、标签用未复权，除权日会凭空出现 -30% 的标签。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行业分类会变&lt;/strong&gt;：申万行业每年调整，历史行业标签要用&lt;strong&gt;当期&lt;/strong&gt;分类，不能用今天的分类回填。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停牌股&lt;/strong&gt;：停牌期间没有任何成交，标签却是&amp;quot;未来 5 日收益&amp;quot;，必须按可交易性把样本剔除，否则模型会学到&amp;quot;停牌股复牌后大涨&amp;quot;的伪规律。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;groupby.apply&lt;/code&gt; 的 &lt;code&gt;include_groups&lt;/code&gt;&lt;/strong&gt;：pandas 2.2 之后行为变了，老代码会静默丢列，IC 计算直接错位。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;LightGBM 这条路的上限是清楚的：&lt;strong&gt;它能把多个弱因子合成一个稍强的信号，但不会凭空创造 alpha&lt;/strong&gt;。IC 从单因子的 0.03 提到 0.042，靠的是模型对非线性与交互的捕捉，以及行业信息的利用。&lt;/p&gt;
&lt;p&gt;如果还想往上走，就要引入序列结构——这正是下一篇的主题：用 &lt;a class="link" href="https://yuansima.com/p/tft-stgnn-quant-rocm/" &gt;TFT 与 ST-GNN&lt;/a&gt; 捕捉时序与截面图结构。那条路的坑，比 LightGBM 多得多。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。模型不会让你赚钱，只会放大你流程里的漏洞。&lt;/p&gt;</description></item><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><item><title>TimescaleDB 量化行情库进阶：分区、压缩与连续聚合</title><link>https://yuansima.com/p/timescaledb-market-data-modeling/</link><pubDate>Wed, 14 Oct 2026 21:30:00 +0800</pubDate><guid>https://yuansima.com/p/timescaledb-market-data-modeling/</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/clickhouse-vs-dolphindb/" &gt;量化数据存储选型&lt;/a&gt;那篇里，我留下了一个结论：&lt;strong&gt;冷数据归档用 ClickHouse，热数据计算交给 DolphinDB&lt;/strong&gt;。但实验室真正跑日常研究的那套库，最后选了第三个选项——&lt;strong&gt;TimescaleDB&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;原因很朴素：A 股日线一年也就一百多万行，任何数据库都扛得住；真正稀缺的是&amp;quot;&lt;strong&gt;能不能用 SQL 直接把数据拼出来&lt;/strong&gt;&amp;quot;。TimescaleDB 是 PostgreSQL 扩展，意味着我可以用窗口函数、&lt;code&gt;DISTINCT ON&lt;/code&gt;、CTE、&lt;code&gt;generate_series&lt;/code&gt; 这些熟悉的东西，而不用为了一次取数去写一段 DolphinDB 脚本。&lt;/p&gt;
&lt;p&gt;这篇记录的是把它从&amp;quot;能存&amp;quot;做到&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; daily_bar (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; symbol text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trade_date date &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt; double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; high double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; low double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume bigint,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; amount double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; turnover double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct_chg double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adj_factor double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&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;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; (symbol, trade_date)
&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;SELECT&lt;/span&gt; create_hypertable(&lt;span style="color:#e6db74"&gt;&amp;#39;daily_bar&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;trade_date&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunk_time_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 year&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if_not_exists &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TRUE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;日线量级下这样完全够用。但当我把分钟线也接进来（5 分钟 K 线，全市场一年约 &lt;strong&gt;1.6 亿行&lt;/strong&gt;）之后，问题出现了：&lt;strong&gt;单个 chunk 内部扫的是全市场&lt;/strong&gt;。查询&amp;quot;贵州茅台最近一年 5 分钟线&amp;quot;仍然要扫过所有股票的 5 分钟数据。&lt;/p&gt;
&lt;p&gt;解决办法是引入空间维度，按 &lt;code&gt;symbol&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; create_hypertable(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;ts&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; partitioning_column &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;symbol&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; number_partitions &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;8&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunk_time_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;7 days&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if_not_exists &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TRUE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;建完之后，一张超表实际变成 &lt;code&gt;8 × 时间片&lt;/code&gt; 个物理子表。查询条件里带 &lt;code&gt;symbol&lt;/code&gt; 时，规划器直接裁掉 7/8 的分区。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;经验值：&lt;code&gt;number_partitions&lt;/code&gt; 别超过 CPU 核心数的两倍太多。分区太碎会让插入时的元数据开销超过收益，8 分片在实验室的 8 核机器上正好。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="二压缩按-symbol-分段按时间排序"&gt;二、压缩：按 symbol 分段，按时间排序
&lt;/h2&gt;&lt;p&gt;时序数据最大的浪费是重复。同一只股票、连续的交易日，&lt;code&gt;symbol&lt;/code&gt; 完全重复，价格变化很小。TimescaleDB 的列式压缩把&amp;quot;同一 segment 内的行&amp;quot;转成数组存储，压缩比通常在 5~20 倍。&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ALTER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; minute_bar &lt;span style="color:#66d9ef"&gt;SET&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress_segmentby &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;symbol&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; timescaledb.compress_orderby &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;ts DESC&amp;#39;&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:#75715e"&gt;-- 7 天以前的数据自动压缩
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; add_compression_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;7 days&amp;#39;&lt;/span&gt;);
&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;code&gt;compress_segmentby&lt;/code&gt; 决定了&amp;quot;哪些行会被放在一起&amp;quot;。因为绝大多数查询都是按股票取一段连续时间，所以分段键选 &lt;code&gt;symbol&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;compress_orderby&lt;/code&gt; 决定段内排序。时间倒序排列，让&amp;quot;最新 N 条&amp;quot;的查询能快速定位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实验室实测（8 核 / 32GB / NVMe，A 股 5 分钟线 1 年）：&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;单股 1 年查询耗时&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;未压缩&lt;/td&gt;
 &lt;td&gt;6.3 GB&lt;/td&gt;
 &lt;td&gt;210 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;压缩后&lt;/td&gt;
 &lt;td&gt;780 MB&lt;/td&gt;
 &lt;td&gt;240 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;压缩后 + 带 symbol 条件&lt;/td&gt;
 &lt;td&gt;780 MB&lt;/td&gt;
 &lt;td&gt;35 ms&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;压缩后写入变慢（约 1.4 倍），但我们的写入集中在盘后批处理，完全无所谓。&lt;strong&gt;如果你的场景是盘中高频写入 + 实时查询，压缩窗口不要设得太短&lt;/strong&gt;——被压缩的 chunk 不能再直接 &lt;code&gt;INSERT&lt;/code&gt;，新数据会跑到新的 chunk 去，这是设计使然而不是缺陷。&lt;/p&gt;
&lt;h2 id="三连续聚合日线自动滚成月线"&gt;三、连续聚合：日线自动滚成月线
&lt;/h2&gt;&lt;p&gt;研究里最烦的是&amp;quot;每次都要手写一段 group by 把日线变月线&amp;quot;。连续聚合（Continuous Aggregate）把这一步变成库里的物化视图，增量刷新：&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; MATERIALIZED &lt;span style="color:#66d9ef"&gt;VIEW&lt;/span&gt; monthly_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WITH&lt;/span&gt; (timescaledb.continuous) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; symbol,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; time_bucket(&lt;span style="color:#e6db74"&gt;&amp;#39;1 month&amp;#39;&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; trade_month,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;first&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;max&lt;/span&gt;(high) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; high,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;min&lt;/span&gt;(low) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; low,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;last&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;, trade_date) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;sum&lt;/span&gt;(volume) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; volume,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;sum&lt;/span&gt;(amount) &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; amount
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; daily_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;GROUP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; symbol, trade_month
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WITH&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DATA&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;SELECT&lt;/span&gt; add_continuous_aggregate_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;monthly_bar&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; start_offset &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;3 years&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; end_offset &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 day&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; schedule_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;1 hour&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;注意 &lt;code&gt;end_offset =&amp;gt; INTERVAL '1 day'&lt;/code&gt;：&lt;strong&gt;只刷新到昨天为止&lt;/strong&gt;。当月数据还在变，如果聚合到当天，物化结果就是一个会不断被改写的&amp;quot;半成品&amp;quot;，容易在回测里用到未完成的月线。查询时当月数据仍然走实时聚合（TimescaleDB 会自动 union 未物化的部分）。&lt;/p&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-sql" data-lang="sql"&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;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; trade_month, &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;, volume
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; monthly_bar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; symbol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;600519&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;AND&lt;/span&gt; trade_month &lt;span style="color:#f92672"&gt;&amp;gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;2020-01-01&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; trade_month;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="四增量写入幂等-upsert-与复权因子"&gt;四、增量写入：幂等 upsert 与复权因子
&lt;/h2&gt;&lt;p&gt;盘后同步任务每天会重跑，必须保证幂等。&lt;code&gt;ON CONFLICT DO UPDATE&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;INSERT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;INTO&lt;/span&gt; daily_bar (symbol, trade_date, &lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, high, low, &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; volume, amount, turnover, pct_chg, adj_factor)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;VALUES&lt;/span&gt; (:symbol, :trade_date, :&lt;span style="color:#66d9ef"&gt;open&lt;/span&gt;, :high, :low, :&lt;span style="color:#66d9ef"&gt;close&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; :volume, :amount, :turnover, :pct_chg, :adj_factor)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; CONFLICT (symbol, trade_date) &lt;span style="color:#66d9ef"&gt;DO&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;UPDATE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SET&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.&lt;span style="color:#66d9ef"&gt;close&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; EXCLUDED.volume,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pct_chg &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.pct_chg,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; adj_factor &lt;span style="color:#f92672"&gt;=&lt;/span&gt; EXCLUDED.adj_factor;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里有个容易忽略的设计：&lt;strong&gt;存未复权价 + 复权因子&lt;/strong&gt;，而不是直接存前复权价。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;前复权价会在每次除权后整体重算，历史数据被&amp;quot;改写&amp;quot;，导致同一天的因子值在不同时间点算出来不一样；&lt;/li&gt;
&lt;li&gt;业界惯例（也是 akshare 的 &lt;code&gt;hfq&lt;/code&gt; 接口）是存后复权或存 &lt;code&gt;adj_factor&lt;/code&gt;，复权在查询层做，历史数值永远不变。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;查前复权价时在 SQL 里乘一下即可：&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; trade_date,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;close&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; adj_factor &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;MAX&lt;/span&gt;(adj_factor) OVER () &lt;span style="color:#66d9ef"&gt;AS&lt;/span&gt; close_qfq
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; daily_bar &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; symbol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;000001&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;ORDER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; trade_date;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
 &lt;blockquote&gt;
 &lt;p&gt;反面教材：早期我用&amp;quot;每天重写整段前复权序列&amp;quot;的方式同步数据，结果同一个因子在 3 月跑出来 IC 是 0.031，6 月重跑变成 0.027。差的那一点就是除权。&lt;strong&gt;数值漂移是量化研究里最隐蔽的 bug&lt;/strong&gt;。&lt;/p&gt;

 &lt;/blockquote&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-sql" data-lang="sql"&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;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; add_retention_policy(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;3 years&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;日线则不删，因为它小到可以忽略（15 年 A 股日线约 2 GB，压缩后 300 MB 左右）。&lt;/p&gt;
&lt;p&gt;如果要做真正的归档，可以把老 chunk 的压缩数据 &lt;code&gt;COPY&lt;/code&gt; 导出到对象存储（实验室用的是内网的 MinIO），再用 &lt;code&gt;drop_chunks&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-sql" data-lang="sql"&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;&lt;span style="color:#66d9ef"&gt;COPY&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; minute_bar &lt;span style="color:#66d9ef"&gt;WHERE&lt;/span&gt; ts &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;2021-01-01&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;TO&lt;/span&gt; PROGRAM &lt;span style="color:#e6db74"&gt;&amp;#39;gzip &amp;gt; /mnt/minio-archive/minute_bar_2020.csv.gz&amp;#39;&lt;/span&gt; CSV HEADER;
&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;SELECT&lt;/span&gt; drop_chunks(&lt;span style="color:#e6db74"&gt;&amp;#39;minute_bar&amp;#39;&lt;/span&gt;, older_than &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;5 years&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六一个取数函数统一到矩阵"&gt;六、一个取数函数，统一到矩阵
&lt;/h2&gt;&lt;p&gt;数据库再好，研究时用的还是 DataFrame。实验室封装了一个按需取数的函数，把&amp;quot;复权、停牌、字段选择&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;# src/data/loader.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; 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; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; create_engine
&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;DSN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;postgresql+psycopg2://quant:quant@127.0.0.1:5432/quant&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;
&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;load_bars&lt;/span&gt;(symbols: list[str], start: str, end: str,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; freq: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt;, adjust: str &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; engine&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; 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;取行情长表，自动按 adjust 做复权。
&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; adjust: &amp;#39;none&amp;#39; | &amp;#39;qfq&amp;#39;(前复权) | &amp;#39;hfq&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; 返回列: symbol, trade_date, open, high, low, close, volume, amount
&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; engine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; engine &lt;span style="color:#f92672"&gt;or&lt;/span&gt; create_engine(DSN, pool_pre_ping&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; table &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;daily_bar&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; freq &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;minute_bar&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; tcol &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; freq &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;1d&amp;#34;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;ts&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; sql &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;f&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;&lt;span style="color:#e6db74"&gt; SELECT symbol, &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; AS trade_date, open, high, low, close,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; volume, amount, adj_factor
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;table&lt;span style="color:#e6db74"&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; WHERE &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt; BETWEEN :start AND :end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; AND symbol = ANY(:symbols)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ORDER BY symbol, &lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;tcol&lt;span style="color:#e6db74"&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; &amp;#34;&amp;#34;&amp;#34;&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; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read_sql(sql, engine, params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: start, &lt;span style="color:#e6db74"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: end, &lt;span style="color:#e6db74"&gt;&amp;#34;symbols&amp;#34;&lt;/span&gt;: symbols})
&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;if&lt;/span&gt; adjust &lt;span style="color:#f92672"&gt;in&lt;/span&gt; (&lt;span style="color:#e6db74"&gt;&amp;#34;qfq&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;hfq&amp;#34;&lt;/span&gt;) &lt;span style="color:#f92672"&gt;and&lt;/span&gt; &lt;span style="color:#f92672"&gt;not&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;empty:
&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; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_values([&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;trade_date&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; grp &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(&lt;span style="color:#e6db74"&gt;&amp;#34;symbol&amp;#34;&lt;/span&gt;, sort&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;False&lt;/span&gt;)[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ratio &lt;span style="color:#f92672"&gt;=&lt;/span&gt; grp&lt;span style="color:#f92672"&gt;.&lt;/span&gt;transform(&lt;span style="color:#e6db74"&gt;&amp;#34;last&amp;#34;&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; 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 style="color:#66d9ef"&gt;else&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;for&lt;/span&gt; col &lt;span style="color:#f92672"&gt;in&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;open&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;high&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;low&amp;#34;&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; df[col] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df[col] &lt;span style="color:#f92672"&gt;*&lt;/span&gt; df[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;/&lt;/span&gt; ratio
&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; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;drop(columns&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;adj_factor&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;ratio&lt;/code&gt; 那一行就是前复权的全部秘密：把当前复权因子归一，最新的价格保持原始数值，历史价格被等比缩小。&lt;/p&gt;
&lt;h2 id="七踩过的坑清单"&gt;七、踩过的坑清单
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ALTER TABLE hypertable&lt;/code&gt; 的坑&lt;/strong&gt;：超表上加索引必须建在超表上（&lt;code&gt;CREATE INDEX ON daily_bar (...)）&lt;/code&gt;），不要手动往 chunk 上建，重建 chunk 之后索引就丢了。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;主键必须包含时间分区列&lt;/strong&gt;。TimescaleDB 的约束要求唯一索引包含分区列，&lt;code&gt;PRIMARY KEY (symbol)&lt;/code&gt; 会直接报错。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;time_bucket&lt;/code&gt; 与交易日历不是一回事&lt;/strong&gt;。&amp;ldquo;按月&amp;quot;聚合出来的是自然月，2 月只有 15 个交易日，用它算月收益没问题，但用它算&amp;quot;每月调仓&amp;quot;就会和交易日错位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;别在压缩 chunk 上做大量 UPDATE&lt;/strong&gt;。压缩块和解压块的写入路径不同，反复更新老数据会把压缩收益吃光。需要修正历史数据时，一次性 &lt;code&gt;decompress_chunk&lt;/code&gt; → 更新 → &lt;code&gt;compress_chunk&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;vacuum&lt;/code&gt; 不是万能的&lt;/strong&gt;。时序表几乎只增不改，但 &lt;code&gt;drop_chunks&lt;/code&gt; 之后还是建议看一眼 &lt;code&gt;autovacuum&lt;/code&gt; 的触发情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;数据层做到这里，已经不是&amp;quot;能存数据&amp;quot;的层次，而是&amp;rdquo;&lt;strong&gt;取数不成为研究瓶颈&lt;/strong&gt;&amp;quot;：单股分钟线 35 ms、全市场日线截面 1 秒内出结果。&lt;/p&gt;
&lt;p&gt;数据齐了、因子有了，接下来是最容易自欺欺人的一环：&lt;strong&gt;回测&lt;/strong&gt;。下一篇从零写一个向量化回测引擎，把手续费、滑点、涨跌停、T+1 全部显式建模。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。基础设施的耐心，最终都会体现在回测曲线是否可信上。&lt;/p&gt;</description></item><item><title>因子工程实战：从行情数据到可检验的 Alpha 因子</title><link>https://yuansima.com/p/quant-factor-engineering/</link><pubDate>Mon, 12 Oct 2026 20:10:00 +0800</pubDate><guid>https://yuansima.com/p/quant-factor-engineering/</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/akshare-tutorial/" &gt;上一篇数据抓取&lt;/a&gt;里，我们已经把 A 股日线行情写进了 TimescaleDB 的超表。但&lt;strong&gt;干净的数据不等于可用的信号&lt;/strong&gt;——从&amp;quot;能取到数据&amp;quot;到&amp;quot;能下单&amp;quot;，中间隔着整个因子工程。&lt;/p&gt;
&lt;p&gt;这篇文章把我们实验室的因子流水线拆开讲：怎么写、怎么校验、怎么避免自己骗自己。&lt;/p&gt;
&lt;h2 id="一先立四条纪律"&gt;一、先立四条纪律
&lt;/h2&gt;&lt;p&gt;因子工程里绝大多数翻车，不是因为数学不够花哨，而是因为违反了下面前四条之一：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;不使用未来信息&lt;/strong&gt;。T 日收盘后才能拿到的数据，只能在 T 日收盘后生成信号，T+1 开盘执行。任何 &lt;code&gt;center=True&lt;/code&gt; 的滚动窗口、任何全样本拟合的标准化，都是未来函数。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;截面可比&lt;/strong&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;。一个因子在入库前必须回答两个问题：它对未来收益有没有单调信息（IC / 分层），扣掉成本后还剩多少（换手）。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;第 4 条最容易被跳过，而它恰好是唯一真正重要的一条。&lt;/p&gt;
&lt;h2 id="二取数把宽表拉成矩阵"&gt;二、取数：把宽表拉成矩阵
&lt;/h2&gt;&lt;p&gt;实验室的行情表结构在上一篇文章里已经建好，核心字段是 &lt;code&gt;symbol / trade_date / close / volume / amount / turnover / pct_chg&lt;/code&gt;。因子计算更适合&amp;quot;矩阵&amp;quot;而不是&amp;quot;长表&amp;quot;，所以第一步先 pivot：&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;# src/data/market.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; 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; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; create_engine
&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;DSN &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;postgresql+psycopg2://quant:quant@127.0.0.1:5432/quant&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;
&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;load_price_matrix&lt;/span&gt;(start: str, end: str, engine&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; dict[str, 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;#39;close&amp;#39;,&amp;#39;volume&amp;#39;,&amp;#39;amount&amp;#39;,&amp;#39;turnover&amp;#39;,&amp;#39;pct_chg&amp;#39;} 五张 T×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; engine &lt;span style="color:#f92672"&gt;=&lt;/span&gt; engine &lt;span style="color:#f92672"&gt;or&lt;/span&gt; create_engine(DSN, pool_pre_ping&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, pool_size&lt;span style="color:#f92672"&gt;=&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; sql &lt;span style="color:#f92672"&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;&lt;span style="color:#e6db74"&gt; SELECT symbol, trade_date, close, volume, amount, turnover, pct_chg
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; FROM daily_bar
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; WHERE trade_date BETWEEN &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(start)s&lt;/span&gt;&lt;span style="color:#e6db74"&gt; AND &lt;/span&gt;&lt;span style="color:#e6db74"&gt;%(end)s&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; ORDER BY trade_date, symbol
&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; df &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;read_sql(sql, engine, params&lt;span style="color:#f92672"&gt;=&lt;/span&gt;{&lt;span style="color:#e6db74"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;: start, &lt;span style="color:#e6db74"&gt;&amp;#34;end&amp;#34;&lt;/span&gt;: end})
&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; out: dict[str, pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame] &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; col &lt;span style="color:#f92672"&gt;in&lt;/span&gt; [&lt;span style="color:#e6db74"&gt;&amp;#34;close&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;volume&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;amount&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;turnover&amp;#34;&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; out[col] &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;col)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_index()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;# 停牌日通常缺失，这里保留 NaN：后续因子计算里 NaN 会自然传播成&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; out
&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;fillna(method=&amp;quot;ffill&amp;quot;)&lt;/code&gt; 无脑填充&lt;/strong&gt;。停牌日的价格是&amp;quot;没有成交&amp;quot;，强行前向填充会让动量因子把停牌期算成 0 收益，回测里会凭空产生收益。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;矩阵的列（股票）会随时间变化&lt;/strong&gt;。新股上市、退市都会造成列不对齐，后续用 &lt;code&gt;DataFrame.align&lt;/code&gt; 或直接按日截面处理。&lt;/li&gt;
&lt;/ul&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;mom_20&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;过去 20 日收益&lt;/td&gt;
 &lt;td&gt;动量效应&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;rev_5&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;过去 5 日收益取负&lt;/td&gt;
 &lt;td&gt;短期反转&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;vol_20&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;20 日收益率标准差取负&lt;/td&gt;
 &lt;td&gt;低波动异象&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;turn_20&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;20 日平均换手取负&lt;/td&gt;
 &lt;td&gt;高换手 = 拥挤&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;&lt;code&gt;bias_20&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;close / MA20 - 1&lt;/code&gt; 取负&lt;/td&gt;
 &lt;td&gt;均值回复&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&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;# src/factor/basic.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&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&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: 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 日动量：T 日因子 = T 日收盘 / T-n 日收盘 - 1（截止当日，不含未来信息）。&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; close &lt;span style="color:#f92672"&gt;/&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(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;
&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;reversal&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;momentum(close, n)
&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;volatility&lt;/span&gt;(pct: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: 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:#75715e"&gt;# pct_chg 已是百分数，先转成小数&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:#f92672"&gt;-&lt;/span&gt;(pct &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(n)&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&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;turnover_avg&lt;/span&gt;(turnover: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: 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:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt;turnover&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(n)&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&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;bias&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: 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; ma &lt;span style="color:#f92672"&gt;=&lt;/span&gt; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rolling(n)&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 style="color:#f92672"&gt;-&lt;/span&gt;(close &lt;span style="color:#f92672"&gt;/&lt;/span&gt; ma &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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上面每个因子都只用「截止当日」的数据，这是故意保持的语义：&lt;strong&gt;因子定义里不包含任何执行滞后，滞后只在回测引擎里统一做一次&lt;/strong&gt;。原因很实际——如果因子内部先 &lt;code&gt;shift(1)&lt;/code&gt;，回测引擎再叠一层 &lt;code&gt;shift(1)&lt;/code&gt;，策略就凭空慢了半拍，收益会被系统性低估；反过来如果某处漏了 shift，又会偷到未来收益。&lt;strong&gt;滞后只在一处发生，才方便审计&lt;/strong&gt;。下一篇的引擎会严格实现这条约定。&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;# src/factor/process.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&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;mad_winsorize&lt;/span&gt;(df: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, n: float &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3.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;按日截面做 MAD 去极值（中位数 ± n * 1.4826 * MAD）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; med &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;median(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; mad &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(med, 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;abs()&lt;span style="color:#f92672"&gt;.&lt;/span&gt;median(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; &lt;span style="color:#ae81ff"&gt;1.4826&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; upper &lt;span style="color:#f92672"&gt;=&lt;/span&gt; med &lt;span style="color:#f92672"&gt;+&lt;/span&gt; n &lt;span style="color:#f92672"&gt;*&lt;/span&gt; mad
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; lower &lt;span style="color:#f92672"&gt;=&lt;/span&gt; med &lt;span style="color:#f92672"&gt;-&lt;/span&gt; n &lt;span style="color:#f92672"&gt;*&lt;/span&gt; mad
&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; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;clip(lower&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lower, upper&lt;span style="color:#f92672"&gt;=&lt;/span&gt;upper, axis&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;
&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;zscore&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;按日截面做 z-score。std=0（全市场同一个值）时返回 0 而不是 inf。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mu &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean(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; sd &lt;span style="color:#f92672"&gt;=&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;std(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; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; df&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sub(mu, 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(sd, axis&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;
&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;industry_neutralize&lt;/span&gt;(factor: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, industry: 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;行业中性化：对每个交易日，把因子对行业哑变量做 OLS，取残差。
&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; industry: 与 factor 同形状的行业标签矩阵（如 &amp;#34;银行&amp;#34;、&amp;#34;电子&amp;#34;），缺失记为 NaN。
&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; residual &lt;span style="color:#f92672"&gt;=&lt;/span&gt; factor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;copy() &lt;span style="color:#f92672"&gt;*&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:#66d9ef"&gt;for&lt;/span&gt; date &lt;span style="color:#f92672"&gt;in&lt;/span&gt; factor&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; y &lt;span style="color:#f92672"&gt;=&lt;/span&gt; factor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; g &lt;span style="color:#f92672"&gt;=&lt;/span&gt; industry&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mask &lt;span style="color:#f92672"&gt;=&lt;/span&gt; y&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna() &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; g&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; mask&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;30&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; dummies &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;get_dummies(g[mask], drop_first&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;, dtype&lt;span style="color:#f92672"&gt;=&lt;/span&gt;float)
&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; dummies&lt;span style="color:#f92672"&gt;.&lt;/span&gt;empty:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; residual&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; y[mask]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; X &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;column_stack([np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;ones(mask&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum()), dummies&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy()])
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; yv &lt;span style="color:#f92672"&gt;=&lt;/span&gt; y[mask]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;to_numpy()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; beta, &lt;span style="color:#f92672"&gt;*&lt;/span&gt;_ &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;linalg&lt;span style="color:#f92672"&gt;.&lt;/span&gt;lstsq(X, yv, rcond&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; residual&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date, mask] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; yv &lt;span style="color:#f92672"&gt;-&lt;/span&gt; X &lt;span style="color:#f92672"&gt;@&lt;/span&gt; beta
&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; residual
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;顺序很重要：&lt;strong&gt;先 MAD 去极值 → 再行业中性化 → 最后 z-score&lt;/strong&gt;。如果先标准化再中性化，残差的量纲会乱，后面做因子加权时权重就失去意义了。&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;提醒：行业中性化一定会&amp;quot;吃掉&amp;quot;一部分收益。如果你的因子本来就是行业轮动逻辑（比如&amp;quot;超配银行&amp;quot;），中性化之后就只剩行业内选股能力了——这不是坏事，只是要清楚自己在测什么。&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="五检验ic分层换手一个都不能少"&gt;五、检验：IC、分层、换手，一个都不能少
&lt;/h2&gt;&lt;p&gt;因子是否有效，实验室只用三个指标下结论。&lt;/p&gt;
&lt;h3 id="51-ic-与-icir"&gt;5.1 IC 与 ICIR
&lt;/h3&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;# src/factor/evaluate.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; scipy.stats &lt;span style="color:#f92672"&gt;import&lt;/span&gt; spearmanr
&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;forward_return&lt;/span&gt;(close: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, hold: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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;未来 hold 日收益（用于评价，绝不进入因子计算）。&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; close&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(&lt;span style="color:#f92672"&gt;-&lt;/span&gt;hold) &lt;span style="color:#f92672"&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&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;daily_ic&lt;/span&gt;(factor: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, fwd_ret: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, method: str &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;spearman&amp;#34;&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;每个交易日横截面上的因子与未来收益的相关性。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ics &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; date &lt;span style="color:#f92672"&gt;in&lt;/span&gt; factor&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; x, y &lt;span style="color:#f92672"&gt;=&lt;/span&gt; factor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date], fwd_ret&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mask &lt;span style="color:#f92672"&gt;=&lt;/span&gt; x&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna() &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; y&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; mask&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&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; method &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;spearman&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ics[date] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; spearmanr(x[mask], y[mask])&lt;span style="color:#f92672"&gt;.&lt;/span&gt;statistic
&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; ics[date] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;corrcoef(x[mask], y[mask])[&lt;span style="color:#ae81ff"&gt;0&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 style="color:#66d9ef"&gt;return&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(ics)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sort_index()
&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;ic_summary&lt;/span&gt;(ic: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series, periods_per_year: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;244&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; ir &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean() &lt;span style="color:#f92672"&gt;/&lt;/span&gt; ic&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:#66d9ef"&gt;if&lt;/span&gt; ic&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:#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:#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;IC均值&amp;#34;&lt;/span&gt;: round(float(ic&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean()), &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;IC标准差&amp;#34;&lt;/span&gt;: round(float(ic&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:#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;ICIR&amp;#34;&lt;/span&gt;: round(float(ir), &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;IC&amp;gt;0占比&amp;#34;&lt;/span&gt;: round(float((ic &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 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;年化ICIR&amp;#34;&lt;/span&gt;: round(float(ir &lt;span style="color:#f92672"&gt;*&lt;/span&gt; np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sqrt(periods_per_year &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)), &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; ir &lt;span style="color:#f92672"&gt;==&lt;/span&gt; ir &lt;span style="color:#66d9ef"&gt;else&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; &lt;span style="color:#e6db74"&gt;&amp;#34;样本天数&amp;#34;&lt;/span&gt;: int(len(ic)),
&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;经验阈值（日频、5 日持有）：&lt;code&gt;|IC均值| &amp;gt; 0.02&lt;/code&gt;、&lt;code&gt;ICIR &amp;gt; 0.3&lt;/code&gt;、&lt;code&gt;IC &amp;gt; 0 占比 &amp;gt; 0.55&lt;/code&gt; 才值得往下做分层测试。低于这个水平，后面所有回测都只是在拟合噪声。&lt;/p&gt;
&lt;h3 id="52-分层收益"&gt;5.2 分层收益
&lt;/h3&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;layered_return&lt;/span&gt;(factor: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, fwd_ret: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, q: int &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&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;按日截面分 q 组，等权持有 forward 收益，返回每组的年化收益。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; groups: dict[int, list[float]] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {i: [] &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; i &lt;span style="color:#f92672"&gt;in&lt;/span&gt; range(q)}
&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 &lt;span style="color:#f92672"&gt;in&lt;/span&gt; factor&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; x, y &lt;span style="color:#f92672"&gt;=&lt;/span&gt; factor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date], fwd_ret&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mask &lt;span style="color:#f92672"&gt;=&lt;/span&gt; x&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna() &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; y&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; mask&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; q &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; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; labels &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;qcut(x[mask]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rank(method&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;first&amp;#34;&lt;/span&gt;), q, labels&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; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; g, idx &lt;span style="color:#f92672"&gt;in&lt;/span&gt; labels&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groupby(labels)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;groups&lt;span style="color:#f92672"&gt;.&lt;/span&gt;items():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; groups[int(g)]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(float(y[mask]&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[idx]&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; annual &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; g, rets &lt;span style="color:#f92672"&gt;in&lt;/span&gt; groups&lt;span style="color:#f92672"&gt;.&lt;/span&gt;items():
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; s &lt;span style="color:#f92672"&gt;=&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;Series(rets)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; annual[&lt;span style="color:#e6db74"&gt;f&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Q&lt;/span&gt;&lt;span style="color:#e6db74"&gt;{&lt;/span&gt;g &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#e6db74"&gt;}&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&lt;/span&gt;] &lt;span style="color:#f92672"&gt;=&lt;/span&gt; round(float(s&lt;span style="color:#f92672"&gt;.&lt;/span&gt;mean() &lt;span style="color:#f92672"&gt;*&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;244&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)), &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;) &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; len(s) &lt;span style="color:#66d9ef"&gt;else&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; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame([annual], index&lt;span style="color:#f92672"&gt;=&lt;/span&gt;[&lt;span style="color:#e6db74"&gt;&amp;#34;年化收益&amp;#34;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一个真正有效的因子，&lt;code&gt;Q1 → Q5&lt;/code&gt; 应该单调，并且首尾组年化差异是稳定的，而不是靠某一年撑起来的。&lt;/p&gt;
&lt;h3 id="53-换手率"&gt;5.3 换手率
&lt;/h3&gt;&lt;p&gt;IC 再漂亮，如果因子每天把整个组合换个遍，扣完成本就归零。实验室的标准做法是算&lt;strong&gt;因子秩的日间自相关&lt;/strong&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;rank_autocorr&lt;/span&gt;(factor: pd&lt;span style="color:#f92672"&gt;.&lt;/span&gt;DataFrame, lag: int &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;gt;&lt;/span&gt; float:
&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;因子截面秩的自相关，越高越稳定（0.9 以上说明换手很低）。&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ranks &lt;span style="color:#f92672"&gt;=&lt;/span&gt; factor&lt;span style="color:#f92672"&gt;.&lt;/span&gt;rank(axis&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;, pct&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;True&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vals &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; date &lt;span style="color:#f92672"&gt;in&lt;/span&gt; ranks&lt;span style="color:#f92672"&gt;.&lt;/span&gt;index[lag:]:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; a &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ranks&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; b &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ranks&lt;span style="color:#f92672"&gt;.&lt;/span&gt;shift(lag)&lt;span style="color:#f92672"&gt;.&lt;/span&gt;loc[date]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mask &lt;span style="color:#f92672"&gt;=&lt;/span&gt; a&lt;span style="color:#f92672"&gt;.&lt;/span&gt;notna() &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; b&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; mask&lt;span style="color:#f92672"&gt;.&lt;/span&gt;sum() &lt;span style="color:#f92672"&gt;&amp;lt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;50&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vals&lt;span style="color:#f92672"&gt;.&lt;/span&gt;append(float(np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;corrcoef(a[mask], b[mask])[&lt;span style="color:#ae81ff"&gt;0&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 style="color:#66d9ef"&gt;return&lt;/span&gt; float(np&lt;span style="color:#f92672"&gt;.&lt;/span&gt;nanmean(vals))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="六落库因子表长什么样"&gt;六、落库：因子表长什么样
&lt;/h2&gt;&lt;p&gt;因子算完不要停在 notebook 里。实验室统一写入 &lt;code&gt;factor_value&lt;/code&gt; 表，主键 &lt;code&gt;(factor_name, trade_date, symbol)&lt;/code&gt;，字段 &lt;code&gt;raw / z&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-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;CREATE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TABLE&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;IF&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;EXISTS&lt;/span&gt; factor_value (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; factor_name text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; trade_date date &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; symbol text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; raw double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; z double &lt;span style="color:#66d9ef"&gt;precision&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;version&lt;/span&gt; text &lt;span style="color:#66d9ef"&gt;NOT&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;NULL&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;DEFAULT&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#39;v1&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;PRIMARY&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;KEY&lt;/span&gt; (factor_name, trade_date, symbol)
&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;SELECT&lt;/span&gt; create_hypertable(&lt;span style="color:#e6db74"&gt;&amp;#39;factor_value&amp;#39;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#39;trade_date&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; chunk_time_interval &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; INTERVAL &lt;span style="color:#e6db74"&gt;&amp;#39;90 days&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; if_not_exists &lt;span style="color:#f92672"&gt;=&amp;gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;TRUE&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;写入时用 &lt;code&gt;ON CONFLICT DO UPDATE&lt;/code&gt; 做幂等 upsert，这样重跑某一天的因子不会产生重复行：&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:#f92672"&gt;from&lt;/span&gt; sqlalchemy &lt;span style="color:#f92672"&gt;import&lt;/span&gt; text
&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;UPSERT &lt;span style="color:#f92672"&gt;=&lt;/span&gt; text(&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;&lt;span style="color:#e6db74"&gt; INSERT INTO factor_value (factor_name, trade_date, symbol, raw, z, version)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; VALUES (:factor_name, :trade_date, :symbol, :raw, :z, :version)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; ON CONFLICT (factor_name, trade_date, symbol)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; DO UPDATE SET raw = EXCLUDED.raw, z = EXCLUDED.z, version = EXCLUDED.version
&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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="七几个一定会踩的坑"&gt;七、几个一定会踩的坑
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;复权基准会变&lt;/strong&gt;。前复权价格在每次除权后整体重算，历史因子值会&amp;quot;漂移&amp;quot;。实验室的做法是因子表存&lt;strong&gt;未复权价 + 复权因子&lt;/strong&gt;，复权只在计算时做，并且记录 &lt;code&gt;version&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ST / 退市股没剔除&lt;/strong&gt;。&lt;code&gt;z&lt;/code&gt; 值算对了，但买不进去。因子入库前就该把&amp;quot;不可交易&amp;quot;标出来，别等到回测才过滤。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;成分股用的是今天的名单&lt;/strong&gt;。这是幸存者偏差的经典来源，做指数增强类策略时必须用当期成分股快照。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IC 算的是样本内&lt;/strong&gt;。因子是全样本算出来的，IC 也是全样本。真正的检验要在滚动窗口里做，下一篇会展开。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;到这里，我们有了一个可复现、可检验的因子表。接下来两件事必须按顺序做：先把&lt;a class="link" href="https://yuansima.com/p/vectorized-backtest-engine/" &gt;回测引擎&lt;/a&gt;写对，再用它去证伪这些因子——&lt;strong&gt;先有可信的尺子，再去量东西&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。数据给了原料，因子给了假设，剩下的交给回测去审判。&lt;/p&gt;</description></item><item><title>A 股量化交易实战路线图：从数据到实盘的七个环节</title><link>https://yuansima.com/p/quant-trading-roadmap/</link><pubDate>Sun, 11 Oct 2026 19:30:00 +0800</pubDate><guid>https://yuansima.com/p/quant-trading-roadmap/</guid><description>&lt;p&gt;&lt;em&gt;远思为径，码动价值。欢迎来到远思码技术实验室的最新技术实践记录。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;经常有人问我：&amp;ldquo;想做量化，从哪开始？&amp;rdquo; 我的回答一直是同一句：&lt;strong&gt;从回测开始，而不是从策略开始&lt;/strong&gt;。因为不能证伪的策略，连讨论的资格都没有。&lt;/p&gt;
&lt;p&gt;这篇文章是实验室&lt;strong&gt;量化系列的总入口&lt;/strong&gt;，把七个环节的顺序、选型、踩坑优先级一次讲清，每个环节都链接到对应的实战长文。&lt;/p&gt;
&lt;h2 id="一全景七个环节"&gt;一、全景：七个环节
&lt;/h2&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;①数据 → ②因子 → ③回测 → ④策略 → ⑤机器学习 → ⑥深度模型
 ↘ ↗
 ⑦实盘自动化（调度 + 推送 + 监控）────────────────┘
&lt;/code&gt;&lt;/pre&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;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;用免费数据能走多远？&lt;/td&gt;
 &lt;td&gt;&lt;a class="link" href="https://yuansima.com/p/akshare-tutorial/" &gt;Akshare 财经数据抓取实战&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/timescaledb-market-data-modeling/" &gt;TimescaleDB 量化行情库进阶&lt;/a&gt;、&lt;a class="link" href="https://yuansima.com/p/clickhouse-vs-dolphindb/" &gt;ClickHouse vs DolphinDB&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/quant-factor-engineering/" &gt;因子工程实战&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/vectorized-backtest-engine/" &gt;向量化回测引擎&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/ma-momentum-rotation-strategy/" &gt;双均线与动量轮动策略实战&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/lightgbm-cross-sectional-stock-prediction/" &gt;LightGBM 截面选股&lt;/a&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;TFT/ST-GNN 值不值得？&lt;/td&gt;
 &lt;td&gt;&lt;a class="link" href="https://yuansima.com/p/tft-stgnn-quant-rocm/" &gt;TFT 与 ST-GNN 的真实收益边界&lt;/a&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;td&gt;&lt;a class="link" href="https://yuansima.com/p/daily-signal-automation/" &gt;每日选股信号的自动化&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="二顺序很重要顺序错了就是浪费时间"&gt;二、顺序很重要，顺序错了就是浪费时间
&lt;/h2&gt;&lt;p&gt;实验室的推荐顺序，是从&amp;quot;&lt;strong&gt;能证伪的东西&lt;/strong&gt;&amp;ldquo;往&amp;rdquo;&lt;strong&gt;不确定的东西&lt;/strong&gt;&amp;ldquo;推进：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;先把回测引擎写对&lt;/strong&gt;（环节③）。很多人一上来就研究因子，结果跑出来的超额收益全部来自成本假设太乐观、停牌没剔除、T+1 没做。引擎不对，后面所有结论都要重做。&lt;/li&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;（环节⑤⑥）。它建立在因子池之上，跳过前两步直接用 GBDT 喂原始价量，几乎必然过拟合。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最后接自动化&lt;/strong&gt;（环节⑦）。它不难，但前提是前面每一步都能被脚本调用——notebook 里的代码是没法定时跑的。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果你只有两周时间，我建议的分配是：&lt;strong&gt;引擎 1 周、因子 4 天、策略 3 天&lt;/strong&gt;，其余都可以往后放。&lt;/p&gt;
&lt;h2 id="三硬件与环境清单"&gt;三、硬件与环境清单
&lt;/h2&gt;&lt;p&gt;实验室跑这套流程的真实配置（2026 年）：&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;Minisforum N5 Pro，AMD Radeon 890M iGPU，48GB UMA 统一内存&lt;/td&gt;
 &lt;td&gt;日频策略绰绰有余，深度模型也能跑&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;数据节点&lt;/td&gt;
 &lt;td&gt;同一台机器上的 TimescaleDB + MinIO&lt;/td&gt;
 &lt;td&gt;日线 15 年约 2GB，分钟线靠压缩压到 1/8&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;系统&lt;/td&gt;
 &lt;td&gt;Ubuntu 26.04 LTS&lt;/td&gt;
 &lt;td&gt;见 &lt;a class="link" href="https://yuansima.com/p/ubuntu-2604-install-guide/" &gt;安装与调优指南&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Python&lt;/td&gt;
 &lt;td&gt;Miniconda3 + systemd 守护&lt;/td&gt;
 &lt;td&gt;见 &lt;a class="link" href="https://yuansima.com/p/ubuntu-2604-miniconda3-systemd/" &gt;安装与 systemd 配置&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;GPU 加速&lt;/td&gt;
 &lt;td&gt;ROCm 7.2.3 + PyTorch ROCm 构建&lt;/td&gt;
 &lt;td&gt;见 &lt;a class="link" href="https://yuansima.com/p/install-llama-cpp-ubuntu-2604-rocm/" &gt;llama.cpp ROCm 部署笔记&lt;/a&gt;&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;一个反直觉的结论&lt;/strong&gt;：日频 A 股量化对硬件的要求低得惊人。5000 只股票 × 250 天 × 20 个因子，约 2500 万个浮点数，普通笔记本都能跑。&lt;strong&gt;真正的瓶颈是数据质量与流程严谨性，不是算力&lt;/strong&gt;。&lt;/p&gt;
&lt;h2 id="四技术栈选型表"&gt;四、技术栈选型表
&lt;/h2&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;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;pandas&lt;/td&gt;
 &lt;td&gt;polars / DuckDB&lt;/td&gt;
 &lt;td&gt;生态完整，向量化回测够用；性能瓶颈真出现再换&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;时序库&lt;/td&gt;
 &lt;td&gt;TimescaleDB&lt;/td&gt;
 &lt;td&gt;ClickHouse / DolphinDB&lt;/td&gt;
 &lt;td&gt;PostgreSQL 生态，SQL 直达，运维成本最低&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;回测&lt;/td&gt;
 &lt;td&gt;自研向量化引擎&lt;/td&gt;
 &lt;td&gt;backtrader / vn.py&lt;/td&gt;
 &lt;td&gt;每一行都看得懂，才能信任结论&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;因子检验&lt;/td&gt;
 &lt;td&gt;scipy + statsmodels&lt;/td&gt;
 &lt;td&gt;alphalens&lt;/td&gt;
 &lt;td&gt;自己算 IC/分层，避免工具黑箱&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;机器学习&lt;/td&gt;
 &lt;td&gt;LightGBM&lt;/td&gt;
 &lt;td&gt;XGBoost / CatBoost&lt;/td&gt;
 &lt;td&gt;表格数据的最优解，训练快、正则强&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;深度学习&lt;/td&gt;
 &lt;td&gt;PyTorch + pytorch-forecasting&lt;/td&gt;
 &lt;td&gt;TensorFlow&lt;/td&gt;
 &lt;td&gt;ROCm 支持好，TFT 有现成实现&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;调度&lt;/td&gt;
 &lt;td&gt;systemd timer&lt;/td&gt;
 &lt;td&gt;cron / Airflow&lt;/td&gt;
 &lt;td&gt;依赖管理、重试、日志开箱即用&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;部署&lt;/td&gt;
 &lt;td&gt;单机 + nginx&lt;/td&gt;
 &lt;td&gt;Docker Compose&lt;/td&gt;
 &lt;td&gt;策略服务的复杂度不值得上 K8s&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="五免费数据能走多远说实话"&gt;五、免费数据能走多远？说实话
&lt;/h2&gt;&lt;p&gt;这是被问得最多的问题。实验室的观点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;日频价量 + 财务数据，免费数据完全够用&lt;/strong&gt;。Akshare、Tushare 免费档、交易所官网，能覆盖 95% 的日频研究需求。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分钟线也能凑，但质量参差&lt;/strong&gt;。免费分钟线常见问题：停牌期缺失、集合竞价缺失、复权因子不全。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;付费数据的价值在&amp;quot;非价格信息&amp;rdquo;&lt;/strong&gt;：龙虎榜、资金流、公告文本、行业分类。这些才是 2026 年之后超额收益的主要来源。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;不要为了数据花钱，先花时间把流程做对&lt;/strong&gt;。我见过太多人买了 3 万的数据，却用着含未来函数的回测。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="六五个最常见的误区"&gt;六、五个最常见的误区
&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;我的策略年化 80%&amp;rdquo;&lt;/strong&gt; —— 先查三件事：有没有用未来数据、有没有扣成本、有没有剔除 ST 与停牌。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;参数调一调就好了&amp;rdquo;&lt;/strong&gt; —— 参数热力图上的深坑才是真实分布，单点最优是拟合结果。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;加个止损就能提高收益&amp;rdquo;&lt;/strong&gt; —— 止损降低波动，但通常也降低收益，它改善的是持有体验而非期望。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;机器学习不需要因子&amp;rdquo;&lt;/strong&gt; —— 模型只能组合信息，不能创造信息。喂原始价量的 GBDT 通常打不过十个手工因子。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;ldquo;跑通就能实盘&amp;rdquo;&lt;/strong&gt; —— 从回测到实盘，中间还隔着滑点、流动性、涨跌停、ETF 溢价与自己的心理。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="七一条完整的执行清单"&gt;七、一条完整的执行清单
&lt;/h2&gt;&lt;p&gt;如果你打算按这条路走下去，可以直接抄这份清单：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 环境就绪：Python 3.12+、pandas 2.x、TimescaleDB 跑通&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 行情落库：日线全市场，含复权因子，幂等增量更新&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 回测引擎：三个单元测试通过，成本/涨跌停/T+1 全部显式建模&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 因子库：至少 10 个因子，写清定义与 IC 记录&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 基准策略：双均线择时 + 动量轮动，作为后续对比基线&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 模型：LightGBM 跑通，purged 时序切分，分年度 IC 稳定&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 深度模型：TFT 作为风格因子叠加，验证与 GBDT 的低相关性&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 自动化：systemd timer + 幂等落库 + 推送 + 心跳告警&lt;/li&gt;
&lt;li&gt;&lt;input disabled="" type="checkbox"&gt; 小资金实盘：跑满 3 个月，对比回测与实盘的偏差&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最后一条最重要&lt;/strong&gt;：小资金实盘三个月，比对回测曲线与真实净值。那三个月的偏差，会告诉你前面所有环节哪里在骗你。&lt;/p&gt;
&lt;h2 id="结语"&gt;结语
&lt;/h2&gt;&lt;p&gt;量化不是一场关于&amp;quot;找到圣杯&amp;quot;的竞赛，而是一场&lt;strong&gt;关于减少自欺的工程&lt;/strong&gt;。数据要干净、回测要老实、成本要算足、上线要可监控——把这些做到位，年化个位数的稳定收益并不难；做不到，年化三位数的曲线也只是纸面上的。&lt;/p&gt;
&lt;p&gt;远思为径，码动价值。愿你跑出的每一条曲线，都经得起自己的审问。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本文系列中所有数据、参数与回测结果均为实验室内部示例，用于说明方法论，不构成任何投资建议。市场有风险，决策需谨慎。&lt;/em&gt;&lt;/p&gt;</description></item></channel></rss>