A股申万二级行业轮动 · 纯行业数据版
零、入口路由(使用者只丢一句模糊的话,先读这里)
> 完整版(六段可直接复制的 prompt、消歧细则、各链出图清单)见 references/入口路由与模块化交付.md。
默认交付形态:Markdown 正文 + PNG 图 + 表格(图文并茂)。默认不出 PPT——只有使用者明确说「做成 PPT / 出一份周报 / 要宣讲」才升级形态(行业轮动结论按周滚动,PPT 生命周期只有一周)。语言:禁比喻、禁口号、不出现「你」;缩写首现给一句话定义。
| 链 | 触发词(模糊即可命中) | 回答什么问题 | 主要入口脚本 |
|---|---|---|---|
| R0 说明 | 介绍这个 skill / 能做什么 / 里面装了什么 | 装了什么、能干什么、明确不干什么 | —(读本文件 §零–§二) |
| R1 复现 | 复现行业轮动策略 / 从 0 到 1 / 怎么跑出来的 / 回测怎么做 | 我怎么自己跑出一模一样的策略(含三道验收关) | build_l2_panel_from_stocks.py + reproduce.py |
| R2 后市 | 后市怎么看 / 大盘怎么样 / 现在市场什么位置 / 大势 | 自上而下:①大势研判 → ②行业结构 → ③最新一周名单 | market_vision.py + weekly_recommend.py |
| R3 轮动 | 行业轮动怎么看 / 哪些行业好 / 轮动信号 / 行业推荐 | ①一级行业轮动观点 → ②二级 Top-10 → ③择时位置 | weekly_recommend.py + timing_drilldown.py |
| R4 择时 | 择时 / 什么时候买 / 买点 / 现在能不能加仓 | ①大势研判(该拿多少仓)→ ②一级行业择时(谁到买点) | market_vision.py + timing_drilldown.py |
| R5 单行业 | 出现具体行业名(医药生物 / 电子 / 中药Ⅱ …) | ①该行业择时(本包) → ②该行业内部 Top-10 名单(配套包) | timing_drilldown.py --l1/--l2;行业内 Top-10 需配套包 ashare-industry-top10-enhance |
判定优先级:具体行业名(R5)→ 动作词(复现 R1 / 择时 R4)→ 「后市/大盘」兜底 R2 → 无法判定就直接问一句,不要猜。
R2 / R3 / R4 读同一批数据、只是叙事顺序不同(R2 自上而下 / R3 自行业而外 / R4 只切时机),因此三者若同时出现「最新一周名单」,排名、区制、数据截止日必须完全一致——这是防口径漂移的检查点。
每周前置刷新(R2–R5 共用,跑一次):python refresh_l2_daily.py --dry-run 预检 → 正式刷新 → 再出结论。数据闸门:三个关键源(个股 K 线 / 申万一级指数 / 市场流动性)末点差异 ≤ 3 个交易日,否则停(末点不一致 = 最大静默故障,区制会缺尾周且不报错)。
两个引擎的边界:本 skill = 申万二级行业轮动(买哪个行业);ashare-industry-top10-enhance = 申万一级行业内部 Top-10(这个行业里买哪十只)。两者独立运行、互不替代。
一句话说明
这是一套 long-only 周频行业轮动策略,不做空、不对冲、不做个股选股,因子从行业指数数据构建。本包只提供代码、不内置数据——需先用 MCP 拉齐数据,再跑完整回测。
> 复现路径(主推 B 档):完整复现 基线(S1.32 / WFO 1.41) 走 B 档(完整个股聚合)——用通达信/腾讯自选股 MCP 拉量价+财报+估值+利率(全公开数据,无需自备 API),跑 scripts/build_l2_panel_from_stocks.py。拉数手册见 references/数据获取指南.md(逐源命令 + 常见问题与解决),构建流程见 references/个股聚合构建流程.md。A 档(纯行业版,参考读数 S1.27 ~ 1.37)数据需申万官方二级指数快照、MCP 拉不到同口径,仅作参考。
策略内核:量价反转选行业 + 毛利增速锚景气(仅 Bull 腿) + 估值/波动定仓位 + 区制门控切腿。
什么时候用
- 搭建/复现一套完整的行业轮动策略,但只有行业指数数据(第三方数据源只给行业指数);
- 想理解一套"因子 → 信号 → regime → 风控 → 回测"的完整轮动框架;
- 想在框架上引入自己的新因子做增量验证(见「如何引入新因子」);
- 想把周频策略扩展到月频/季频(见「月线/季频实现」);
- 需要每周行业推荐 + 上线后持续跟踪样本外净值;
- 需要对任意申万一级/二级行业做择时定位与下钻看图(六态/▲买点/族共振),作为每周推荐的主观补充(§5.2)。
一、策略与因子构建(把问题讲清楚)
1.1 数据流全景
l2_daily_factors(二级行业日频面板: ret_1d + turnover;由个股日频**等权聚合**而成)
│ 按 ISO 周聚合
▼
周频面板(131 行业 × ~650 周: ret_w, turnover_w, nav)
│ 从 nav 算动量/反转
▼
原始因子(mom_4w/8w/12w/52w, rev_12w/8w, turnover_rev, mom_skip)
│ 截面 rank → z
▼
合成信号(sig = 区制门控[动量 vs 反转]; sig_gp = sig + 1.75*gp[仅Bull])
│ 失效调仓选 Top-10 行业(`weight_mode='risk_parity'`:52 周协方差 + λ=0.3 对角收缩)
▼
组合收益(扣成本)→ vol targeting → ERP 择时 → 最终净值
1.2 因子怎么构建(核心)
第一步:日频 → 周频
行业指数日收益 ret_1d 累乘成周收益 ret_w = (1+ret_1d).prod()-1;换手率取周末快照 turnover_w。再从周收益反推行业净值 nav = (1+ret_w).cumprod()。
第二步:从净值算动量/反转
任意 horizon N 周的动量 = nav / nav.shift(N) - 1,反转 = 负动量。本策略用 4/8/12/52 周四档:
| 因子 | 定义 | 作用 |
|---|---|---|
mom_52w |
52 周价格动量 | 趋势强度 |
mom_4w |
4 周价格动量 | 近期涨幅 |
mom_skip |
mom_52w − mom_4w |
跳过近 4 周暴涨的"稳趋势"(动量腿核心) |
rev_12w |
−mom_12w(12 周反转) |
超跌反转(反转腿) |
rev_8w |
−mom_8w(8 周反转) |
超跌反转(反转腿,8 周是反转甜点) |
turnover_rev |
−turnover_w |
缩量(无人问津 = 错杀) |
第三步:截面 rank → z
每个因子在每周末按"131 个行业"横截面做 rank 转 z(cs_rank_z,NaN 保持 NaN)。截面 rank 消除行业间量纲差异,把因子统一到可比尺度。
第四步:区制门控合成信号
- 市场 regime 分
Bull/nonBull(构造见 §1.3),用regime_lag1(上周状态)决定本周买哪条腿,防前视。 pure_rev(反转腿)=0.125*rev_12w_z + 0.125*rev_8w_z + 0.75*turnover_rev_z—— 反转腿里缩量占主导(3/4 权重)。- ★ 为什么价格反转是 12w + 8w 两档(对研报信号的分解检验):研报给的「反弹确认」定义为当日收益 − 前 40 日累计,周频化为
reb = mom_1w − mom_8w,直接叠加 Sharpe 1.19→1.24,看似真增量。由rev_8w = −mom_8w可得reb = mom_1w + rev_8w,拆成三个形态分别检验:单窗口 rev_12w(0.25) S1.19 / WFO 1.07 → 双窗口版(rev_12w + rev_8w 各半)S1.22 / WFO 1.11 → 再叠加 mom_1w S1.24,但 WFO 回落 1.09。⇒ 增量来自 8 周反转窗口本身,不是研报的「反弹确认」(mom_1w 拖累样本外),故取双窗口版。权重 1:1 不是挑峰值——w8 ∈ {0.10, 0.125, 0.15} 的 WFO 同为 1.11(一片平原),取 1:1 只为少一个自由度。稳健性检验:三窗口 12+8+26 反而退化(WFO 1.04);分年非单年主导(2018 唯一负年 −1.20);滞后 1 周衰减 6.0%、前视检验 1.22→0.43(PASS);0~30bp 成本下 Δ 恒 +0.03。行业层增量rev_8w= +0.069(常规熊市段 +0.204)。 - ★ 差分型信号的前视审计盲区:
reb这类和差型信号在人为引入未来收益时,fwd_1w会在差分中自我抵消、退化成 −cum7w(纯反转)。实测前视 Sharpe:rev_12w单独 0.49、rev_12w + reb0.63(反而更高)、rev_8w单独 −0.02、mom_1w单独 4.07。⇒ 和差 / 差分因子必须拆开单独验证,不能只看审计是否 PASS。 sig=Bull ? mom_skip_z : pure_rev—— 牛市追稳趋势,非牛市买超跌缩量。sig_gp=sig + 1.75 * gp_z(仅 Bull 腿加 gp)—— 景气度只在趋势向上时有效。
第五步:失效调仓选 Top-10(risk_parity 加权)
不是每周全换,而是带"最小持有期 T=2 周 + 置换阈值 dZ=0.5 + 失效卖出 z_fail=-0.5"的路径依赖调仓:持仓行业在最小持有期内强制持有,之后若信号没跌破失效线就保留(加置换加成),只在空出的槽位用最高信号补齐。这样降换手、留趋势。
1.3 市场 regime(区制)
全行业等权驱动(零个股依赖):
trend = 0.5 * 全行业等权净值52周动量 + 0.5 * 12周动量
vol = 全行业等权周收益 20 周波动
liq = 全行业换手率均值
raw = 0.55*trend_z − 0.25*vol_z + 0.20*liq_z (expanding z, minp=2年)
Bull = raw ≥ expanding 70% 分位
逻辑:趋势强 + 波动低 + 流动性好 → Bull。expanding z 只用 t 及之前(防弱前视),Bull 占比约 32%。
> ★ 区制口径(两套,别混用对账):
> - A 档(common.py 的 build_regime):上面的简化版——全行业等权三因子(trend/vol/liq)+ expanding z / expanding 70% 分位。这是「当期信号」默认口径:严格无前视(只用 t 及之前)、自适应当前市场。代价是与 基线历史数字对不齐。
> - B 档(build_l2_panel_from_stocks.py 的 build_regime):完整四态区制(Bull/Trans/Bear/BearSev,基于 HS300 八特征 + 市场流动性)+ train 固定口径(2014-2019 定 μ/σ 与 70%/30% 分位)。这是「复现基线历史数字(S1.32/WFO1.41)」用的口径,能逐年对账;但参数是 2019 年定格的事后值,属弱前视,到 2026 已陈旧 7 年。
> - 对账铁律:拿「A 档当期信号(expanding)」去对「基线的 1.32」是三重错位(区制复杂度 + 区制参数 + 收益口径),必然对不上。要逐年对账走 B 档 train 固定;要出干净当期信号走 A 档 expanding。
> - B 档 区制逐周对账坑(实跑逐周 99.19% 吻合后固化):★ADX 是 SMA(rolling(70).mean())不是 Wilder、liq_amt_pct 用 rank(pct=True)、vol_80/atr_80 用 train 期 80% 分位、广度数据源差异(5 周类别边界倒挂、仅 1 周切腿)。完整清单见 references/个股聚合构建流程.md §7.1。
1.4 风控两层(择时层,非选股层)
- ERP 估值分位:ERP = 全A盈利收益率(1/PE) − 10Y 国债,expanding 分位 → 周频序列,shift(1) 后乘仓位。便宜(高分位)多配,贵(低分位)少配,躲阴跌熊。
- vol targeting:26 周波动 → 目标 22% 年化波动,
w = (22%/σ)^2,clip[0.4, 1.0],shift(1)。波动放大时降仓。
1.5 实测数字
2014-2026,扣 5bp,Top-10:
| 档位 | 数据源 | 全样本 Sharpe | 年化 | MaxDD | WFO(2022+) | 前视审计 |
|---|---|---|---|---|---|---|
| B 完整个股聚合版(主推) | 通达信/腾讯全A量价(MCP 拉) | 1.32 | 22.3% | −23.8% | 1.41 | PASS(衰减 13.7%) |
| A 纯行业版(参考) | 申万官方二级指数(基线数据源) | 1.27(补 ERP 1.37) | 25.2% | −25.3% | 0.97~0.98 | PASS(衰减 10.2%) |
> ★ 1.32 这组数字是「基线数据源(通达信/腾讯全A量价)」的样本内回测结果。用不同数据源(第三方平台指数、个股后复权价)复现,绝对绩效会不同——这是数据环境差(成分股快照、复权口径、缺失日处理),不是实现差。对账只认同源区间,不用其他数据源的数字对 1.32 判「复现失败」。
>
> 随包流程实测(同代码、数据快照不同):全样本 Sharpe 1.27 / 年化 22.6% / MaxDD −21.1% / 净值 12.27x / WFO 1.13 / 单边换手 15.3% / 前视衰减 6.7% PASS。与基线读数差 0.05 个 Sharpe、0.28 个 WFO,属数据环境差而不是实现差(换手率在同源区间逐位一致、区制逐周吻合 99.84%)。因此验收看结构不看某一位数字——验收线见 复现手册.md §15。
>
> 两档差异(WFO 1.41 vs 0.98)来自收益口径:B 档用个股等权行业收益,A 档用申万官方指数(市值加权),两者相关仅 0.74(已实测)。
>
> 注:refresh_l2_daily.py 提供的是个股等权聚合版 A 档面板(官方指数 MCP 拉不到时的替代构造,收益口径同 B 档),用它跑 A 档脚本实测 S≈1.26 / WFO≈0.87,与上表 A 档行(官方指数派生)的读数不同。两版是同一契约文件的两种构造方式,不可互相判「复现失败」。
>
> ★ 收益源跳变污染(实测):区制层 四态区制 逐周对齐 99.19%(HS300+流动性,不碰行业收益,不受污染影响)、组合层同构可交付;但某些平台的申万二级指数有 42 处「单日 |ret|>20% 且不回撤」跳变污染(虚增年化 6~7pp),用平台行业指数前必须验跳变,有污染就换个股层合成等权净值。详见 上手说明.md §二/§六。
职责划分(信号展示层与绩效复现层不混用):
| 对象 | 脚本 | 回答什么 |
|---|---|---|
| 信号展示层 | weekly_recommend.py / oos_track.py |
当期该买哪 10 个、为什么、仓位多少 |
| 绩效复现层 | reproduce.py / build_l2_panel_from_stocks.py |
完整回测绩效(含失效调仓 + vol + ERP) |
> 两者是不同对象:信号展示层出「当期信号」,绩效复现层出「回测绩效」,两者不混用。样本外跟踪(oos_track)对的是同口径的样本内;回测(reproduce/build)对的是回测基线。
★ 换手率是反转腿的唯一发动机(实测敏感性):去掉换手率,Sharpe 1.27→0.98(−22.6%)、WFO 0.97→0.73(−24.6%);仅保留换手率反而 S1.42——价格反转整体相对纯换手率是稀释,故组合中缩量占主导(0.75)、价格反转合计只占 0.25。注意区分两句话:价格反转整体稀释换手率 alpha ≠ 两个窗口无分工——rev_8w 相对 rev_12w 单窗口是真增量(对研报信号的分解检验:S 1.19→1.22、WFO 1.07→1.11,见上文「为什么价格反转是 12w + 8w 两档」)。
分年 13 年中 2018、2023 为弱势年。
1.6 已知边界(避免不切实际的预期)
- 拐点不可预测:ERP + vol 已是"降仓"最优解,"牛市踏空"是"降仓换回撤"的必然代价。
- 52-12 动量 horizon 不是最优:反转甜点是 8 周,52 周价格动量几乎无 alpha(动量靠 gp 景气)。
- 时序标准化无增量:行业内"相对自身历史位置"的信息已被截面 rank 捕获。
- 动量崩溃不可预测:强趋势行业的下跌不能用滞后量价可靠预警。
- 低频行业轮动天花板:行业轮动月频 S0.66、周频 S0.79(一级行业),二级行业周频才破 1。
- 收益源跳变污染:某些平台的行业指数有「单日 |ret|>20% 且不回撤」的跳变(实测 42 处,虚增年化 6~7pp),不是行情、是数据 bug。用平台行业指数前必须验跳变,有污染就换个股层合成等权净值。
二、数据契约(不内置,自己导数据)
本 skill 不内置数据。数据落盘在 data/ 目录(或设环境变量 SECTOR_ROTATION_DATA_DIR 指向别处)。
> ★ 文件名是代码的读取契约,不是基线的私有资产。build_l2_panel_from_stocks.py / common.py 按固定文件名与目录结构读取(差一个字符即报「缺数据」,不会猜测),因此下表契约名必须原样使用。但同名 ≠ 同源——契约只约束「文件名 + 必需列」,数据从哪个接口拉、放在哪个盘、文件叫什么,全部由使用者自定(zone3 / zone0 是历史编号,无语义,仅因脚本写死而保留)。做法是:用自有的数据源,把这 8 类数据落盘成同名文件。
B 档(主推)8 个数据源 —— 逐源拉取命令 + 常见问题与解决见 references/数据获取指南.md:
| 文件 | 必需? | 用途 |
|---|---|---|
stock_kline/stock_kline_YYYY.parquet |
✅ | 全 A 个股日频(symbol/date/close/turnover;择时下钻另需 amount 成交额) |
industry_map_full.parquet |
✅ | 个股→申万二级(131)映射 |
zone3_hs300_daily.csv |
✅ | HS300 日线(四态区制) |
zone0_factor_data.csv |
✅ | 市场流动性(成交额/涨跌停/涨跌家数) |
sector_fundamental_quarterly.csv |
✅ | 一级行业季度毛利增速(gp) |
official_sw_l1_daily_2014_2026.csv |
✅ | 一级行业指数日线(gp 财报对齐) |
bond_yield_daily.csv |
✅ | 10Y 国债(ERP) |
stock_valuation.parquet |
可选 | 全市场估值(ERP 盈利收益率,缺则自动降级 use_erp=False) |
数据缺失时,build_l2_panel_from_stocks.py 会逐个列出缺哪些文件并指向导数据手册,不报晦涩的 traceback。
大势研判层(R2/R4 用)额外要的 5 个派生缓存 —— 无需另行准备,脚本自己从契约数据现算:六维面板(market_vision.py)除上面 8 类契约数据外,还要 5 个日频缓存(涨跌家数 / 成交集中度 / 换手率中位数 / 一级行业成交额 / 一级行业等权日收益)。这 5 个不列进契约——build_vision_caches.py 会从 stock_kline + industry_map_full 现算(一次全历史约 3 分钟),market_vision.py 在缓存缺失时自动调用它,使用者无感。唯一不在契约数据内的是宏观月频(PMI / 社融脉冲 / M1−M2),需另拉,缺失时 M 维自动降级、不报错。
A 档(l2_daily_factors.parquet,由个股 K 线聚合生成):common.py 读它,它是由个股日频 K 线等权聚合成的 131 个二级行业日频面板(date/industry/ret_1d/rev_60d/mom_252d/mom_20d/mom_skip/turnover/n_stock),生成与刷新脚本为 scripts/refresh_l2_daily.py(口径:个股层算因子 → 每列 1%/99% winsorize → 按行业等权 mean)。
> ⚠️ 它不是「申万官方二级行业指数」(旧文档措辞有误):官方指数是市值加权、只有 OHLCV,补不出行业换手率;本面板是个股等权、含行业换手率。两者实测日频相关仅约 0.2(周频会更高),不可互换。
刷新这个文件不需要任何 MCP 调用——输入全是本地文件(个股 K 线 + 换手率 + 映射)。见 §5.1 的 refresh_l2_daily.py。
2.1 打包分发(refresh_data.py)
把代码与文档打成 zip(自动排除 data/ 下的数据文件,只含代码与文档):
cd scripts
python refresh_data.py --zip <版本号>
三、复现步骤
3.1 环境
- Python ≥ 3.8,依赖仅
pandas、numpy、scipy(matplotlib可选,用于画图)。
3.2 复现步骤(B 档,完整复现)
# 1. 按 references/数据获取指南.md 拉齐 7 个必需数据源(+估值可选),放进 data/
# (或设环境变量 SECTOR_ROTATION_DATA_DIR 指向自备数据目录)
# 2. 对账:先只产面板,核对行数/覆盖/换手率
cd scripts
python build_l2_panel_from_stocks.py --build-only
# 3. 跑完整回测
python build_l2_panel_from_stocks.py
预期输出:S1.32 / 年化 22.3% / MaxDD −23.8% / WFO 1.41 / 前视审计 PASS(衰减 13.7%)(通达信/腾讯全A量价口径),并落盘 l2_factor_panel_rebuilt.parquet。数据缺失时脚本会逐个列出缺哪些文件。
3.3 复现核对清单(拿到面板后逐项确认)
| 步骤 | 预期 | 若不符 |
|---|---|---|
| 面板行数 | 131 行业 × 约 650 周 | 检查映射是否覆盖全部个股、复权口径 |
| regime Bull 占比 | ≈30~35% | 检查 四态区制的 train 固定 μ/σ(非 expanding) |
| ret_w 口径 | 个股等权(与官方指数相关仅 0.74) | 是否误用了市值加权官方指数 |
| 前视审计衰减 | <15% | 排查是否信号用了当周收益 |
| WFO(2022+) | ≈1.41(通达信/腾讯全A量价) | 收益口径(等权 vs 市值加权)是第一主因 |
| 逐周 regime 吻合 | >95%(实测 99.19%) | 逐周 diff 基线区制序列,坑见 个股聚合构建流程.md §7.1(★ADX 是 SMA 不是 Wilder) |
四、持续跟踪样本外净值(上线后的关键,A 档配套)
回测数字是在全样本上选参数的结果,只有上线后逐周积累的"真·样本外"才能检验策略是否有效。交付口径的硬门槛 = 样本外 Sharpe 破 1。
每周五收盘后三步
cd scripts
# 1. 拿本周 Top-10 持仓
python weekly_recommend.py --top 10
# 2. 查到这 10 个行业本周的指数收益,算等权均值 = 组合周收益
# 在 oos_ret.csv 追加一行(格式 yw,ret_w,ISO 周如 2026-38):
# echo "2026-38,0.0050" >> oos_ret.csv
# 也支持明细格式(每行 yw,industry,ret_w),脚本自动按周等权聚合
# 3. 更新净值图
python oos_track.py
oos_track.py 输出 nav_oos_track.png:样本内(红色,2014→上线)+ 样本外(蓝色,上线后逐周),并在分隔线处标出最新样本外净值。
判断标准
- 8~12 周后做第一次正式对照:样本外 Sharpe 是否落在回测 WFO 的置信区间内(A 档口径约 0.97,B 档约 1.41,均为通达信/腾讯全A量价)。
- 样本外 Sharpe 持续 < 0.5 → 警惕策略失效或数据口径错位(样本外收益口径必须与回测一致:行业指数周收益、等权)。
> ★ 职责划分(信号净值与回测绩效不混用):oos_track.py / weekly_recommend.py 走「当期信号」口径(出 Top-10 + 仓位),reproduce.py / build_l2_panel_from_stocks.py 走「完整回测」口径(含失效调仓 + vol + ERP)。两者是不同对象,对账时只拿同口径比:样本外 vs 样本内(同 oos_track),回测 vs 回测(同 reproduce/build)。见 §1.5 职责划分表。
五、每周行业推荐(weekly_recommend.py,A 档配套)
python weekly_recommend.py --top 10 --out weekly_rec.csv
输出最新一周的:regime 状态(Bull/nonBull)、ERP 仓位分位、Top-10 行业表(排名/sig_gp/所属腿/关键因子),以及每个行业"为何可买"的分腿说明:
- 反转腿(非 Bull):买"超跌 + 缩量无人问津"的行业——
rev_12w/rev_8w(过去 12/8 周跌得多)+turnover_rev(换手率低)。逻辑:跌过头 + 没人关注 = 错杀修复。 - 动量腿(Bull):买"52 周强势且近 4 周没暴涨 + 景气好"的行业——
mom_skip = mom_52w − mom_4w+gp(毛利增速)。逻辑:趋势自我强化 + 基本面景气。
> 注意:推荐是"展示层"信号,不代表择时建议。它回答"按策略此刻该买哪 10 个行业、为什么",不回答"大盘该不该加仓"——后者由 ERP 分位 + vol targeting 决定。
5.1 自动化调度(每周五收盘后自动出推荐)
推荐流程本质是"刷新数据 → 出推荐 → 追加样本外净值"三件事,前两件可以全自动,第三件视数据源而定。核心是顺序:weekly_recommend.py / oos_track.py 读的是 l2_daily_factors,所以调度里第一步必须先把它刷新到最新周五,否则出的是旧持仓。
5.1.1 数据刷新:refresh_l2_daily.py(零 MCP 调用)
l2_daily_factors.parquet 是个股日频等权聚合的产物,所以它的刷新是纯本地重算,不消耗任何 MCP / Choice / EmQuant 配额:
① 个股 K 线刷新(唯一需要联网的环节;已有断点续传机制,注意限流)
↓ 本地文件落盘后
② python refresh_l2_daily.py ← 纯本地,18 秒,零配额
↓
③ python weekly_recommend.py --top 10 --out weekly_rec.csv
# 先干跑(预检各源新鲜度 + 比对历史,不写文件)
python refresh_l2_daily.py --dry-run
# 正式刷新(预检 → 重算 → 原子落盘 → 历史比对)
python refresh_l2_daily.py
脚本会打印每个输入源的「最新日期」与待补区间;输入全部是本地文件:
stock_kline/stock_kline_YYYY.parquet(close + turnover) + industry_map_full.parquet(l2)。
> ★ 已知性质(须知):winsorize 用的是全样本 1%/99% 分位,每新增一段数据
> 分位会微调 ⇒ 被 clip 的极端值微调 ⇒ 历史行业均值出现 ~1e-4 量级漂移(集中在股灾等极端日)。
> 实测刷新 09-04→09-30 后,重叠期 638 个可比周里 632 周(99.06%)的 Top-10 名单完全一致,
> 不一致的 6 周各差 1 个行业(全在 2015/2018 极端行情周),sig_gp 截面相关 0.99999857。
> ⇒ 刷新不会让基线回测结论失效,但历史数字并非逐位可复现,对外引用时以最新刷新为准。
> ⚠️ gp 依赖的 official_sw_l1_daily_2014_2026.csv 要放在数据目录根:
> 该文件(一级行业指数日线,31 列)用于把财报可用日对齐到交易周。缺了不会报错,只会静默降级为无 gp
> (sig_gp 在 Bull 腿少掉 1.75×gp_z 那一项)。补数走 EmQuant csd(31 个指数一次调用)
> 或 westock kline pt018xxxxx(批量,分批重试),量极小;补完记得同步到数据目录。
调度的三种挂法(把 refresh_l2_daily.py 放在 weekly_recommend.py 之前即可):
方式 A:Linux/macOS cron(每交易日收盘后,周五为主)
# crontab -e 追加(周五 17:00,A股已收盘 + 数据源已落盘)
0 17 * * 5 cd /path/to/skill/scripts && \
python refresh_l2_daily.py && \
python weekly_recommend.py --top 10 --out ../output/weekly_rec.csv && \
python oos_track.py >> ../output/cron.log 2>&1
要点:refresh_l2_daily.py 随 skill 提供(scripts/,零 MCP 调用),必须排在 weekly_recommend.py 之前;--out 用固定文件名每周覆盖,想留历史就改成 weekly_rec_$(date +\%Y\%W).csv(crontab 里 % 必须转义成 \%)。
方式 B:Windows 任务计划程序
schtasks /Create /TN "SectorRotationWeekly" /TR "python C:\path\to\skill\scripts\weekly_recommend.py --top 10 --out C:\path\to\output\weekly_rec.csv" /SC WEEKLY /D FRI /ST 17:00
方式 C:WorkBuddy 自动化(本环境内最省事)
在对话里一句话创建一条每周五 17:00 触发的循环自动化,把任务内容写全(刷新行业指数到最新周 → 跑 weekly_recommend.py 出 Top-10 并落盘 → 跑 oos_track.py 更新净值图),结果每周自动推送。无需自己维护 cron/计划任务。
半自动的一步——样本外净值追加:oos_track.py 依赖 oos_ret.csv,需要"这 10 个行业本周的指数收益等权均值"。如果数据源能直接取行业指数收益,写个小脚本自动聚合后 append 即可全自动;否则就半自动(调度只出推荐,周末查一次收益补一行 yw,ret_w,再跑 oos_track.py)。判断标准见 §四。
5.2 择时下钻(timing_drilldown.py,主观补充)
> 定位边界(先读这个):择时下钻是 基线的「人工风控眼镜」,不进 信号公式——它回答"这个行业现在处于什么位置、有没有超跌买点",不改变"该买哪 10 个"。参数(8 日迟滞、VR>1 分档)是在 30 个一级行业上定标的,搬到 131 个二级行业未重定标、不承诺统计显著性。把它当结构化看盘工具,不要当统计信号。
三种模式:
python timing_drilldown.py # 默认:本期 Top-10 时序图 + 31 一级行业状态总表
python timing_drilldown.py --l1 医药生物 # 一级下钻:该一级大图 + 下属全部二级网格
python timing_drilldown.py --l2 中药Ⅱ # 二级下钻:单一二级完整时序
指标怎么读:
| 指标 | 定义 | 用法 |
|---|---|---|
| 六态 | 位置分位(250日区间,0/2/4)× 方向(距MA60 上下,0/1)+ 8日迟滞确认 | 底部左侧→底部右侧 = 寻底→企稳;顶部右侧 = 趋势末段 |
| ▲买点 | 超跌共振(MOM 族 4 成员或 LIQ 族 3 成员同时 z≤−1)+ 距MA60 偏离 5 日回升 | 实心▲ = VR>1(波动放大)高置信;空心▲ = 常规 |
| 深跌关注区(灰底纹) | 位置分位 ≤ 0.15 连续 ≥15 日 | ★非买点信号,仅风险提示:位置极低不构成买入依据(见下方裁决) |
| 吸引力分 | 0.5×(1−位置分位) + 0.5×Φ(方向z) | 越高越"跌得深且在止跌",总表按它降序 |
| 三族共振 | MOM(ret20/60/120/250) / REV(ret5/10) / LIQ(amt5/20/60) 族内 z≤−1 成员数 | 弱/中/强三档,衡量超跌广度 |
| 破位金纹 | MA250 的 20 日斜率≤0 连续 ≥20 日 | 中期下行趋势风险底纹 |
> ★ 为什么没有"磨底型买点"(预注册验证否决,不再重复验证):位置型抄底(pos_s 极低 + 新低衰减 + 波动收敛)在 131 个二级行业上负期望——未来 20 日正收益占比仅 41%~54%(接近抛硬币),而▲的跌速极端共振(K_MOM==4)达 64.7%。▲的精度 100% 来自"跌速极端 z≤−1 共振",不是位置、不是止跌。另有行业早期成分股极少(如医疗美容 2014-2018 仅 1 只),z 序列失真,属数据病非判据病。
数据依赖(比因子回测少一个字段):
- 择时部分(
--l1/--l2/一级总表)只需stock_kline(close + amount)+industry_map_full; - 本期 Top-10(默认模式)需完整 B 档数据,缺则自动跳过 Top10、只出择时图(明确提示);
- 口径:
ret_1d= 个股日收益等权均值、amt= 个股成交额求和——与 周频情绪面板 一级行业择时面板同源同口径;个股日收益剔除 close≤0 坏点 + |ret|>0.2(停牌复牌错位假跳变,如 2015 停牌股复牌 +898%)。
每周固定跑(接在 5.1 的调度里):数据刷新后 python timing_drilldown.py 一次即可,产物为 timing_l1_overview.png(一级总表)+ timing_top10.png(Top10 时序)。看图顺序建议:先一级总表找"吸引力分高 + ▲ 触发"的一级 → 下钻该一级的二级网格 → 交叉核对 Top10 名单的六态位置(Top10 若落在顶部右侧,属趋势持仓而非抄底,风控含义不同)。
六、因子实验台:在存量数据上测试 / 挖掘 / 替换因子
> 这一章回答一个高频问题:我把数据拉齐、面板建好了,怎么用它试自己的因子,或者替换掉本包这 5 个因子(rev_12w / rev_8w / turnover_rev / mom_skip / gp)? 闭环三步:算 → 评 → 替。
6.1 三步闭环
| 步骤 | 做什么 | 用哪个文件 |
|---|---|---|
| ① 算 | 在存量数据上算出一个新因子(行业 × 周截面) | build_l2_factors() 加一列 |
| ② 评 | 单因子初筛:IC / ICIR / NDCG@k / 分年 | scripts/factor_lab.py |
| ③ 替 | 初筛通过 → 替换进合成 → 完整回测看增量 | 改 main() 合成公式 |
核心原则:单因子 IC 只是初筛,最终价值 = 替换后组合 Sharpe / WFO / 前视衰减的增量。IC 高 ≠ 组合增量——两个因子截面相关低,不代表它们选出的行业不同,先查重叠度。
6.2 因子在哪算(两个入口)
B 档(主推,自备数据):build_l2_panel_from_stocks.py 的 build_l2_factors() 已经算好基础因子,周频面板 wk 自带这些列可直接用:
| 列 | 含义 |
|---|---|
mom_4w / 8w / 12w / 52w |
4/8/12/52 周价格动量(个股 mean 聚合) |
turnover |
行业换手率 |
ret_1d / ret_w |
日收益 / 等权周收益(收益口径,不作因子使用) |
vol_20w |
20 周波动率 |
新因子 = 在这个 wk 上加一列(industry × yw 粒度),只用 t 及之前数据。示例——"换手率 8 周变化"(量能边际):
def build_l2_factors():
...
wk = wk.sort_values(['industry', 'yw'])
# 新因子:换手率的 8 周变化(放量/缩量的边际,而非水平值)
wk['dturnover_8w'] = wk.groupby('industry')['turnover'].transform(lambda s: s.diff(8))
return wk
A 档(有官方行业指数快照时):factor_registry.py 里写 func(wk) -> Series 并 register()(自动接截面 rank-z,见 6.4)。
6.3 先评估:factor_lab.py(单因子初筛)
# 1. 产出带新因子的面板(在 build_l2_factors() 里加了新列后)
python build_l2_panel_from_stocks.py --build-only
# 2. 评估单因子(默认读 l2_factor_panel_rebuilt.parquet)
python factor_lab.py --factor dturnover_8w
python factor_lab.py --factor rev_12w --factor turnover_rev --factor my_factor
输出解读(判断基准):
| 指标 | 通过线 | 说明 |
|---|---|---|
| IC 均值 | |IC| > 0.02 | 行业周频截面 IC 天花板 ≈0.032(换手率反转本身),不要预期远超 |
| ICIR(年化) | > 0.3 | 方向对且稳定 |
| IC>0 比例 | > 55% | 不靠少数几周拉均值 |
| NDCG@10 | 明显 >0.5 | 选出的 top 行业确实命中未来强者 |
| 分年 IC | 多数年同号 | 防单年主导(去最强年检验) |
6.4 再替换:改合成(两个入口的具体位置)
B 档(build_l2_panel_from_stocks.py)——三处:
# ① build_l2_factors() 末尾:新因子列已经加上(见 6.2)
# ② main() 的截面 z 列表(约 417 行):把新因子名加进去
for f in ['rev_12w', 'rev_8w', 'turnover_rev', 'mom_skip', 'gp', 'dturnover_8w']:
p[f'{f}_z'] = p.groupby('yw')[f].transform(cs_rank_z)
# ③ 合成公式(约 420 行):反转腿把 0.25 的份额重新分配,塞进新因子
p['pure_rev'] = (CONFIG['rev_w'] * 0.4 * p['rev_12w_z']
+ CONFIG['rev_w'] * 0.3 * p['rev_8w_z'].fillna(0.0)
+ CONFIG['rev_w'] * 0.3 * p['dturnover_8w_z']
+ (1 - CONFIG['rev_w']) * p['turnover_rev_z'])
然后 python build_l2_panel_from_stocks.py 跑完整回测,对比替换前后的 Sharpe / WFO / 前视衰减。
A 档(factor_registry.py)——注册 + 改权重(自动接 rank-z,不碰 common.py):
def _f_dturnover_8w(wk):
wk = wk.sort_values(['industry', 'yw'])
return wk.groupby('industry')['turnover_w'].transform(lambda s: s.diff(8))
register('dturnover_8w', _f_dturnover_8w, leg='reversal', desc='换手率8周变化')
# 反转腿加权重(份额重新分配)
REVERSAL_WEIGHTS = {'rev_12w': 0.10, 'rev_8w': 0.10, 'dturnover_8w': 0.05, 'turnover_rev': 0.75}
6.5 挖掘方向清单(存量数据能派生出什么)
从面板已有的 mom_*w / turnover / ret_w / vol_20w 出发,可低成本派生:
| 方向 | 构造思路 | 备注 |
|---|---|---|
| 量能边际 | turnover 的 diff / 同比 | "变化" vs 水平值,常比水平值更干净 |
| 波动率 | vol_20w 本身、或其变化 | 低波/高波选行业 |
| 收益偏度/尾部 | 行业周收益 rolling 偏度 | 右尾彩票 vs 左尾 |
| 中段动量 | mom_52w − mom_12w | 52 周纯动量已证弱,中段未必 |
| 截面地位跃迁 | 因子当期截面 rank 的 diff | "从冷门变热门"类 |
已证伪、别重复(长记忆里的物理结论):筹码 / 机构资金流 / 一致预期 dFY(行业级归零)、时序标准化位置因子(0 增量)、强趋势崩溃预警(AUC 0.61 太弱)、52-12 动量 horizon 寻优(反转甜点 = 8 周)。
6.6 三条铁律(加因子前必查,防前视 + 防假增量)
- 前视审计:新因子只用
t-1及之前数据;收益用fwd_1w(shift(-1))。加完因子跑完整回测,看内嵌审计"滞后 1 周衰减"是否 <15%。 - 截面 rank 而非原始值:因子进合成前自动
cs_rank_z。绝对值型因子(成交额/大单净流入)是规模混淆,先归一化再测。 - 看增量而非绝对值:新因子价值 = 组合 Sharpe / WFO 的变化,不是单因子 IC。IC 高但选行业重叠时叠加常是负增量——先查 Jaccard 重叠度。
七、月线 / 季频行业轮动如何实现
当前框架是周频。改成月频/季频,本质是"重采样 + 三个参数联动调整",不是简单 resample('M') 了事——信号 horizon、失效调仓周期、vol 窗口必须一起改(信号 horizon 和持仓周期不匹配会锁死 alpha)。
7.1 月频改法(三步)
- 重采样:把
load_industry_weekly()的周频聚合改成月频。yw键换成year-month(如'2014-01'),ret_w改成ret_m = (1+ret_1d).resample('M').prod()-1,turnover_w取月末快照。 - 信号 horizon 换月:
mom_52w→mom_12m(12 个月动量)、mom_4w→mom_1m、rev_12w/rev_8w→rev_3m/rev_2m。即所有"周数"除以 4.33。 - 失效调仓 + vol 参数:
T=2(周)→T=1(月);vol_win=26(周)→vol_win=12(月);wfo_split换成月份格式。
7.2 季频改法
同上,但 horizon 换成季度:mom_52w→mom_4q、rev_12w→rev_1q,vol_win→vol_win=8(8 个季度),T 保持 1(季)。
7.3 ⚠️ 必须知道的天花板(不要抱过高期望)
已有实证:行业轮动的 alpha 集中在周频。一级行业月频天花板 S0.66、周频 S0.79;二级行业周频才破 1。月频/季频换来的只是"更低的换手、更省心的调仓",代价是 Sharpe 显著下降(低频错过短周期反转)。所以:
- 月频/季频适合低换手、低跟踪成本的变体,不适合追求高 Sharpe;
- 改完必须重跑
reproduce.py的前视审计 + WFO,确认衰减 <15% 且样本外没崩。
八、文件清单
| 文件 | 作用 |
|---|---|
README.md |
★ 一页总览:包内有什么、怎么开始、口径边界(最先读) |
上手说明.md |
★ 上手说明:能复现到什么绩效 + 快速开始 + 四个口径裁决 |
复现手册.md |
★ 十一步 SOP:契约 → 提数 → 体检 → 聚合 → 区制 → gp → ERP → 信号 → 风控 → 回测 → 对账归因(逐步 prompt + 验收线 + 实测附录) |
复现实录.md / 复现实录.html |
★ 图文实录版:每步「指令 → 看什么 → 通过标准 → 实测答案」(.md 纯文本 / .html 排版版) |
data/ |
空目录(仅 data/数据说明.md),放自备数据 |
scripts/common.py |
核心库(A 档):数据自动发现 + 周频面板 + regime + gp/ERP + 失效调仓 + backtest |
scripts/factor_registry.py |
★ 因子注册表(换因子 / 换权重只改这里) |
scripts/reproduce.py |
A 档一键回测(读 l2_daily_factors.parquet,由 refresh_l2_daily.py 从个股 K 线聚合生成) |
scripts/refresh_data.py |
发布打包:只打代码+文档(不打包 data/),发新版本用 |
scripts/refresh_l2_daily.py |
★ 零 MCP 调用的 l2_daily_factors 刷新(纯本地重算,约 30–60 秒):新鲜度预检 + 原子落盘 + 历史漂移比对。每周出推荐前先跑它(§5.1.1) |
scripts/oos_track.py |
样本外净值持续跟踪(A 档配套,每周五追加 oos_ret.csv 后跑) |
scripts/weekly_recommend.py |
每周 Top-N 行业推荐 + "为何可买"分腿说明(A 档配套) |
scripts/timing_drilldown.py |
★ 择时下钻:六态/▲买点/族共振,默认出 本期 Top-10 时序 + 31 一级状态总表,--l1/--l2 任意行业下钻(主观补充,§5.2) |
scripts/market_vision.py |
★ 大势研判层(R2/R4):六维市场级面板 D/V/L/S/C/M(250 日滚动分位)+ 周频情绪口径情绪复合分 10 子项;--build-only 只产面板,纯描述层、不进 信号公式 |
scripts/market_vision_plot.py |
大势研判仪表盘出图(六维雷达 / 读数条形 / 时序 / 明细表);由 market_vision.py 自动调用 |
scripts/build_vision_caches.py |
★ 大势研判层的 5 个派生缓存从契约数据现算(涨跌家数 / 成交集中度 / 换手率中位数 / 一级行业成交额 / 一级行业等权日收益);缓存缺失时 market_vision.py 自动调用 |
scripts/build_l2_panel_from_stocks.py |
★ B 档:个股→行业聚合 + 四态区制 + gp/ERP + 回测(主推,喂 MCP 拉到的个股日频) |
scripts/factor_lab.py |
★ 因子实验台:单因子 IC/ICIR/NDCG@k/分年评估(测试/挖掘新因子的第一步,§六) |
references/入口路由与模块化交付.md |
★ 入口路由总表 + 六段可复制 prompt + 交付格式规约 + 口径铁律(§零的完整版) |
references/数据获取指南.md |
★ 导数据手册:8 个数据源逐源拉取 + 常见问题与解决 + 验收清单 |
references/个股聚合构建流程.md |
★ B 档完整构建流程(逐步公式 + 口径要点 + 对账清单) |
references/PaiWork结论版指令.md |
★ 贴到 Alpha派 PaiWork Skills 广场的结论产出版指令(AI 用平台数据现算当期 Top-10,与代码复现版同内核) |
references/已证伪方向速查.md |
★ 8 条已堵死的路(加因子 / 改框架前必查) |
数据口径铁律(已内嵌):信号 t-1、regime lag1、收益 fwd_1w = shift(-1)、ERP shift(1)、截面 rank nan_policy='omit'、年化用几何 CAGR nav^(52/N)。
本 skill 由 WorkBuddy 撰写,所有数字可由 reproduce.py 复现。