/~learn/s/944b3ca8
> 建立日期:2026-10-23 > 状态:现象已量化确认,整改方案全部「未实施」,待确认后再动代码。 > 本文是「截面选品(周频 Top10)↔ 择时策略池(每日 position 信号)」两个时钟打架这一整篇讨论的归档,同时把「策略持仓年龄」的度量口径一并定义清楚。
---
截面选品是「每周定稿、周一执行」的低频决策,择时信号是「每天变」的高频决策,二者的更新频率天然不一致;系统当前把它们按日强行合成,结果就是「品种在 Top10 边缘反复进出 + 权重每天大幅跳动」,产生了大量与因子观点无关的换手。 这不是 bug,是多时钟(multi-horizon)错配,属于策略设计问题,必须显式建模(迟滞 / 平滑 / 换手预算 / 确认制),不能靠"再调调因子权重"解决。
---
对照截面网页「策略持仓」表格,每一行有 前仓位 / 现仓位 / 预测仓位 三列,由择时策略池的买卖信号驱动。用户观察到:
由此引申出的问题是:一个"被选进来 3 天、择时信号又刚翻空"的持仓,到底该怎么算?它的"持仓年龄"是多少?该按哪一方的观点执行?
---
先把事实摆清楚(依据代码与 DB 实况,不是推测):
| # | 时钟 | 频率 | 谁在驱动 | 代码/数据位置 |
| :-: | ------ | ------ | --------- | -------------- |
| ① | 截面打分 | 每交易日都会算一遍 | generate_push_message() → save_weekly_snapshot() | engine.py |
| ② | 组合权重重算 | 每交易日重算(DB 中 cross_section_portfolio 共 95 天记录) | save_portfolio_timing() | cross_section.py |
| ③ | 择时 position 信号 | 每日更新(不同锚点节奏差异极大,见 §5.3) | 择时策略池 | cross_section_portfolio.timing_today / timing_predicted |
| ④ | 人工实际执行 | 周一 9:30–10:30 一次(设计铁律第 1 条) | 人 | 铁律:「周五出结果,周一执行」 |
关键矛盾:设计意图是「周频决策 + 周频执行」,但系统内部实际是「日频重算」。也就是说,周二到周五每天网页上的"现仓位"都在变,但用户并不会去调仓 —— 网页展示的是一个"如果今天执行会是什么样"的虚拟组合,它在 5 个交易日里可能已经换了两三轮。
这就解释了为什么用户会觉得"品种在乱跳":看的其实是日频虚拟组合,执行的却是周频真实组合,两个东西根本不是同一个组合。
---
| 中文 | 英文 | 含义 | 在本系统的体现 |
| ------ | ------ | ------ | -------------- |
| 多时钟错配 | multi-horizon / multi-frequency mismatch | 信号生成周期与执行周期不一致 | 截面周频 vs 择时每日 |
| 信号衰减 | signal decay / alpha decay | 信号预测力随持有时间下降 | 排名第 1 的品种 2 周后可能掉到 30 名 |
| 信号半衰期 | signal half-life | 信号强度衰减到一半所需时间 | 本系统核心品类的排名半衰期约 1–2 周 |
| 陈旧信号 | stale signal | 用已经过期的观点做当下决策 | 周一执行的是上周五的因子分数 |
| 双边打脸 | whipsaw | 买入后立即反向、卖出后立即反转,双向付出成本 | Top10 边缘品种反复进出 |
| 换手率 | turnover | 单位时间买卖金额占比 | 见 §5.2,均值约 19%/日 |
| 滞回 / 迟滞 | hysteresis | 进/出用不同阈值,避免在边界抖动 | 目前没有(进出同一阈值) |
| 持仓年龄 | holding age / position age | 一个仓位已持有多久 | 目前未统计,本文 §7 定义 |
| 换手预算 | turnover budget | 给定时间窗内允许的最大换手 | 仅周中再平衡器有 30%/日上限 |
| 实现缺口 | implementation shortfall | 决策价与成交价的差 | 周一开盘执行 vs 周五收盘价 |
| 调仓周期不匹配 | rebalance horizon mismatch | 同上 multi-horizon 的另一说法 | — |
> 与「追涨杀跌」的区别:追涨杀跌是因子方向问题;多时钟错配是频率结构问题。因子再好,频率错了照样被摩擦成本吃掉。
---
cross_section_weekly_snapshot 表实测:每个交易日 109 行(全池 109 只全部入库,is_top 全为 1,即"全量排名模式")。也就是说「Top10」只是前端取 rank<=10 的展示,底层每天都有完整 109 只的排名。
cross_section_portfolio 表实测:95 个交易日,每个交易日都有一版持仓与权重。近期样本:
| 日期 | 持仓只数 | 权重合计 |
| ------ | :---: | :---: |
| 2026-10-23 | 11 | 99.89% |
| 2026-10-22 | 11 | 98.64% |
| 2026-10-21 | 12 | 98.60% |
| 2026-10-20 | 11 | 98.37% |
| 2026-10-19 | 7 | 38.55% |
| 2026-10-16 | 7 | 37.27% |
| 2026-10-15 | 5 | 23.30% |
| 2026-10-14 | 8 | 52.45% |
| 2026-10-13 | 10 | 41.33% |
| 2026-10-12 | 11 | 55.78% |
注意 10-12 ~ 10-19 这一周:权重合计从 55.78% 掉到 23.30% 又回到 38.55%,只数从 11 → 10 → 8 → 5 → 7 → 7。如果用户真按这个每日调仓,一周内要把仓位砍到 1/4 再买回来 —— 这显然是择时信号在剧烈摆动,而不是因子观点在变。
按锚点统计 timing_today 同一天的均值,取 记为一次"信号翻转":
≥ 20 个百分点变化
| 锚点 | 有数据天数 | 大幅翻转次数 | 翻转频率 | 典型翻转 |
| ------ | :---: | :---: | :---: | ------ |
QQQ(Invesco QQQ Trust,纳斯达克100 ETF) | 94 | 16 | 约每 6 天 1 次 | 09-15 50→75、09-18 68.75→43.75、09-22 25→56.25、09-26 81.25→56.25 |
HSI(恒生指数) | 74 | 6 | 约每 12 天 1 次 | 08-11 21.84→79.59、08-12 79.59→52.38(次日又反过来)、09-14 61.22→26.53 |
399102.SZ(创业板综) | 71 | 4 | 约每 18 天 1 次 | 07-02 0.53→62.07、07-13 39.08→18.39、08-18 32.18→64.37、09-11 42.53→5.75 |
510880(红利ETF) | 47 | 1 | 极稳 | 08-09 8.82→43.74 |
511260(十年国债ETF) | 5 | 0 | — | — |
这张表是本文最重要的一张:同样是"择时信号",QQQ 锚点平均 6 天就大幅变一次,红利锚点 47 天只变 1 次。当一个新品种被选入 Top10、它挂的锚点是 QQQ 时,"品种年龄"和"信号年龄"根本不是一回事 —— 品种可能才进来 2 天,而它的信号已经在 2 天里翻了 1 次。
---
| 日期 | 新进 | 退出 |
| ------ | :---: | :---: |
| 2026-10-23 | 5 | 5 |
| 2026-10-22 | 2 | 3 |
| 2026-10-21 | 5 | 4 |
| 2026-10-20 | 6 | 2 |
| 2026-10-19 | 2 | 2 |
| 2026-10-16 | 4 | 2 |
| 2026-10-15 | 1 | 4(持仓仅 5 只) |
| 2026-10-14 | 2 | 4 |
| 2026-10-13 | 4 | 5 |
| 2026-10-12 | 1 | 1 |
| 2026-10-09 | 4 | 4 |
| 2026-10-08 | 1 | 1 |
| 2026-10-02 | 0 | 0 |
| 2026-10-01 | 2 | 2 |
平均每天换掉 3 只左右;极端日(10-20、10-23)一天换 5 只。一个 11 只的组合,日周转接近 30%。
> 口径注脚(2026-10-24 核对):上表的「成员」= cross_section_portfolio 里 weight > 0 的行。所以「退出」不等于被排名踢出 Top10,大量是①择时 position 归零(行被剔除)②QDII 溢价排除后被候补顶替③对冲位/国债位轮换。真正由「截面打分换名次」造成的换手只占一小部分,见下表。
排名 Top10 变动 vs 组合成员变动(同期对比)
| 日期 | 排名 Top10 换手 | 组合成员换手 | 单边换手 | 持仓数 |
| ------ | :---: | :---: | :---: | :---: |
| 2026-10-19 | 0 进 0 出 | 2 进 2 出 | 19.5% | 7 |
| 2026-10-20 | 2 进 2 出 | 6 进 2 出 | 50.8% | 11 |
| 2026-10-21 | 1 进 1 出 | 5 进 4 出 | 19.6% | 12 |
| 2026-10-22 | 1 进 1 出 | 2 进 3 出 | 12.4% | 11 |
| 2026-10-23 | 0 进 0 出 | 5 进 5 出 | 31.5% | 11 |
最近 4 个交易日排名 Top10 只动了 0~2 只,组合却每天换 5~6 只 —— 这就是「选品时钟」和「执行时钟」错配的直接量化:换手主因不在打分排名,而在择时/溢价/对冲位。
> 补充证据(10-14 → 10-15):排名 Top10 只差 1 进 1 出(518880.SH 黄金ETF 掉到 13 名、159612.SZ 标普500ETF 从 11 名补上),组合却少掉 4 只(513400.SH 道琼斯ETF、513800.SH 东证指数ETF、511260.SH 十年国债ETF、511090.SH 30年国债ETF),而两天 exposure_cap 都是 0.46 —— 不是仓位上限压的,是择时/对冲位。
| 日期 | 单边换手 | 日期 | 单边换手 |
| ------ | :---: | ------ | :---: |
| 2026-09-30 | 5.2% | 2026-10-16 | 22.3% |
| 2026-10-01 | 16.4% | 2026-10-19 | 19.5% |
| 2026-10-02 | 0.5% | 2026-10-20 | 50.8% |
| 2026-10-08 | 7.6% | 2026-10-21 | 19.6% |
| 2026-10-09 | 22.3% | 2026-10-22 | 12.4% |
| 2026-10-12 | 8.9% | 2026-10-23 | 31.5% |
| 2026-10-13 | 25.5% | ||
| 2026-10-14 | 19.2% | ||
| 2026-10-15 | 23.6% |
近 15 日平均单边换手约 19%/日。 按 A 股 ETF 双边摩擦(佣金 + 冲击成本,保守取单边 0.05%~0.10%)估算,年化摩擦成本约 19% × 2 × 250 × 0.05% ≈ 4.8%(乐观口径),悲观口径接近 10%。这是一个可以直接吞掉全部超额收益的量级。
| 品种 | 权重 | 前仓位 → 现仓位 → 预测 | 锚点 |
| ------ | :---: | :---: | ------ |
| 511090.SH 30年国债ETF | 39.62% | 89.21 平 | — |
| 512800.SH 银行ETF | 12.00% | 48.68 → 50.44 → 36.33 | 510880 |
| 512890.SH 红利低波ETF | 12.00% | 48.68 → 50.44 → 36.33 | 510880 |
| 511260.SH 十年国债ETF | 11.08% | 99.79 平 | — |
| 159502.SZ 标普生物科技ETF | 7.16% | 71.57 平 | — |
| 159612.SZ 标普500ETF | 5.00% | 80 → 100 → 40 | SPY |
| 159577.SZ 美国50ETF | 3.92% | 85.95 → 77.45 → 72.22 | QQQ |
| 513800.SH 东证指数ETF | 3.68% | 39 平 | EWJ |
| 513050.SH 中概互联ETF | 2.58% | 20.41 平 | HSI |
| 512200.SH 房地产ETF | 1.50% | 22.99 → 20.69 | 399102.SZ |
| 510060.SH 央企ETF | 1.35% | 48.68 → 50.44 → 36.33 | 510880 |
一句话读法:159612.SZ 标普500ETF 的择时信号从 80 直接跳到 100,预测又要跌到 40 —— 这个"预测仓位"一栏本身就在剧烈摆动,如果每天照着它调,标普500 一天之内要走 100→40。
---
| 层 | 代价 | 说明 |
| :--: | ------ | ------ |
| ① 直接成本 | 佣金 + 买卖价差 + 冲击成本 | 换手 19%/日 → 年化摩擦 5%~10% |
| ② 噪声成本 | 把"信号抖动"当成"新信息" | QQQ 锚点 6 天翻一次,其中大部分是噪声 |
| ③ 行为成本 | 用户在周中看到组合大变,忍不住手动干预 | 破坏「周一调仓」铁律,实盘与回测脱节 |
| ④ 归因污染 | 事后无法判断收益来自"选品"还是"择时" | 两个时钟都在动,归因变成一笔糊涂账 |
其中 ③ 最隐蔽:只要网页每天展示一个变化剧烈的组合,用户的纪律迟早会被击穿。 这是"UI 反过来塑造行为"的典型例子。
---
「持仓年龄」不是单一指标。必须拆成三个,否则永远对不齐。
| 名称 | 建议字段 | 定义 | 当前系统状态 | ||
| ------ | --------- | ------ | ------------ | ||
| 品种年龄 | days_in_portfolio | 该品种连续出现在 cross_section_portfolio 中的交易日数 | ❌ 未统计(可从历史表回算) | ||
| 信号年龄 | days_since_flip | 该品种所挂锚点的 timing_today 距离上次大幅翻转( | Δ | ≥20pt)的天数 | ❌ 未统计 |
| 排名年龄 | rank_stability | 该品种连续在 Top10 内的周数(≈streak_weeks) | ⚠️ 部分有(快照表已有 streak_weeks,但只覆盖排名,不含权重) |
为什么必须分开:
「前仓位 / 现仓位 / 预测仓位」三列的正确解读:它不是"一个仓位的演变",而是"同一天三个不同口径的计算结果"。真正该展示的是「上周一实际执行 → 今天虚拟 → 下周一建议」,而不是"昨天/今天/明天"。
days_in_portfolio INT -- 品种在组合中的连续交易日数
days_since_flip INT -- 锚点择时信号距上次大幅翻转的天数
weight_change_5d REAL -- 近 5 日权重累计变化(衡量平滑度)
turnover_20d REAL -- 该品种近 20 日累计单边换手
> 落地时注意:cross_section_portfolio 新增列必须同步 CREATE / MIGRATE / INSERT 列列表 / VALUES 占位符 / 参数 tuple 五处(该文件 L402 已写明这条教训)。
---
位置:engine.py 的 _apply_holding_inertia() / _commit_inertia_portfolio(),状态文件 .holding_inertia.json(adata/db/ 下)。
当前参数(v4,commit 1d4b039)
| 常量 | 值 | 含义 |
| ------ | :--: | ------ |
_INERTIA_TARGET_N | 10 | 真实组合目标持仓数(与排名表行数解耦) |
_MAX_NEW_PER_WINDOW | 3 | 滚动 5 个交易日内最多新进 3 只(不是自然周,也不是"一百多只") |
_NEW_WINDOW_DAYS | 5 | 滚动窗口长度(交易日) |
_INERTIA_BONUS | 0.030 | 在位成员评分加成(最长叠到 0.045) |
_ENTRY_MARGIN | 0.015 | 超配额新进者的压分幅度 |
_HARD_EXIT_SCORE | 0.20 | 评分低于此不再保护 |
_INERTIA_OFF_REGIMES | BEAR / SEVERE_BEAR | 熊市整层停用 |
⚠️ 关键事实:v4 之前(10-23 19:03 之前)这层从未执行过。 旧版守卫是 if len(ranked) < top_n * 2: return ranked,而推送路径传 top_n=99、全池 109 行 → 109 < 198 恒成立 → 每个交易日都直接早退。三重佐证:
git show 1d4b039^);tests/adata_test/selection/test_holding_inertia.py);.holding_inertia.json 长期恒为 {}(2 字节,mtime 仍是 10-23 09:37)。所以 §5.1 / §5.2 那些换手数据全部是在「裸跑」口径下产生的,不含任何惯性保护。新逻辑 10-23 收盘后才提交,最早 10-26(周一)首次运行只记录不阻尼,10-27 起才产生实际加分保护,滚动 5 日配额要到 10-29~10-30 才形成完整窗口。
它能解决什么、不能解决什么
结论:它解决的是"进不进 TopN",不解决本文说的"权重每天跳"和"信号频率错配"。
max_daily_turnover = 0.30(单日最多换 30%)trigger_tiers = [0.015, 0.03, 0.05] 价格触发分档减/建仓cash_buffer = 0.05,周一强制收敛这是现成的「换手预算」参考实现,可直接借鉴其阈值结构,不必另起一套。
---
按优先级排列,P0 只观测不改逻辑,确认问题量级后再逐层动。
| 优先级 | 方案 | 做什么 | 风险 | 依赖 | ||
| :---: | ------ | -------- | ------ | ------ | ||
| P0 | 观测层 | 新增 §7.1 四个字段 + 页面展示「品种年龄 / 信号年龄」两列(只读,不改任何权重) | 无 | 无 | ||
| P1 | 进出迟滞(hysteresis) | 进入 Top10 用 rank<=10,但退出用 rank>13(或 rank>15);留 3~5 名缓冲带 | 低;会略微降低"及时性" | P0 数据先看分布 | ||
| P2 | 权重 EMA 平滑 | w_t = α·w_target + (1-α)·w_{t-1},α 取 0.2~0.3;对应"半衰期 2~3 期" | 中;会滞后于真实信号 | P1 验证后 | ||
| P3 | 换手预算 | 每日 Σ | Δw | /2 ≤ 10%(对比当前均值 19%、峰值 50.8%),超出部分顺延次日 | 中;极端行情可能跟不上 | 复用 midweek_rebalancer 阈值结构 |
| P4 | 择时确认制 | 锚点信号翻转必须连续 2 日确认才生效(QQQ 6 天翻一次,噪声占大头) | 中高;可能错过快速反转 | 需回测确认 | ||
| P5 | 池子/仓位分层解耦 | 截面负责"买什么",择时负责"买多少",两者不在同一张表上相乘 | 高;属于架构级改动 | 需要单独设计文档 |
建议推进顺序:P0 → 数据观察 4~8 周 → P1 + P3(低风险组合)→ 回测验证 → 再议 P2/P4/P5。
> ⚠️ 禁止在 P0 观测数据出来之前直接上 P2/P4 —— 参数(α、确认天数)没有数据支撑就是拍脑袋。
---
对比 A/B/C/D 四组,同一段区间、同一套 ETF 池、同一执行假设(周一开盘价):
| 组 | 配置 |
| :--: | ------ |
| A | 现状(每日重算,无任何平滑) |
| B | A + 进出迟滞(P1) |
| C | B + 权重 EMA α=0.25(P2) |
| D | C + 每日换手上限 10%(P3) |
必须看的指标:
| 指标 | 为什么看 |
| ------ | --------- |
| 年化单边换手 | 直接对应成本 |
| 成本后年化收益 | 摩擦吃掉多少超额 |
| 夏普 / Calmar | 平滑是否改善了风险调整收益 |
| 最大回撤 | 平滑会不会在市场急跌时"迟钝" |
| 组合成员半衰期 | 一个品种平均待多久(周) |
| 20 日双边打脸率 | 买入后 20 日内反向卖出的比例,本文问题的直接度量 |
| 品种年龄分布 | 是否还大量出现"1~3 天就换掉" |
> 只看"成本后收益"会误判:平滑可能在降低换手的同时也降低了动量捕捉能力,必须同时看回撤和打脸率。
---
| 文档 | 关系 |
| ------ | ------ |
| 04-选基择时融合方案 | 04 讲的是「仓位总量怎么映射」(单宏观 / 大类映射 / 波动率目标 / BL);本文讲的是「映射之后权重怎么不抖」。04 是"定水位",本文是"防水位每天忽高忽低"。 |
| 06-仓位满仓化与择时联动方案 | 06 解决了"目标 95% 仓位 + 动态递补",本文发现递补过程本身就在制造高频换手;06 的五层过滤提供了"哪些品种可以跳过",可作为本文 P1 迟滞规则的输入。 |
| 09-市场状态平滑过渡 | 09 平滑的是"天气判断"(regime 标签),本文平滑的是"选品与信号"。同一类问题的两个层次,方法论可互相借鉴(滞回 / 连续化 / 标签滞后 ≈ 本文的迟滞与 EMA)。 |
| 03-约束自己 | 铁律第 1 条「周一调仓」是本文的执行基准。若网页继续展示日频虚拟组合,这条铁律迟早被击穿 —— 这也是本文要提出「展示口径」改造的原因。 |
---
- 方案 a:展示"上周五定稿 → 本周执行"的实盘口径,日频变化不展示(推荐); - 方案 b:继续展示日频虚拟组合,但加显眼提示"这不是你的真实持仓"; - 方案 c:两种都展示,加 tab 切换。