查看全课大纲(第 22 / 38 节)

实现低PE股票池(2)

约 12 分钟

📺 正在播放小象学院官方高清录播(支持倍速与清晰度调节)

实现低PE股票池(二):调仓周期设计与集合变动追踪

小象实战讲义 · 初阶量化交易:策略编写及系统搭建 本讲义仅用于编程与量化方法教学,不构成任何投资建议。

在量化投资中,选股模型不仅需要在静态截面上挑选优质标的,更必须在动态时间序列上制定科学的定期调仓机制(Rebalancing Frequency)。股票的市盈率估值随着每日收盘价波动而实时微调,若每日都根据全市场最新 PE 重新洗牌股票池,不仅会引发高额的印花税、佣金与冲击成本摩擦,更会破坏持仓的稳定性。

本讲深入剖析调仓周期设为 7 个交易日的工程权衡哲学;详细讲解新调入(Incoming)、被调出(Outgoing)与留存(Retained)三大股票集合的动态运算与持久化方案;并建立股票池等权持有基准收益率的追踪流水线。


💡 核心导读

  • 为什么选择 7 个交易日作为调仓周期?
    • 高频调仓的摩擦弊端:每日调仓会导致股票池频繁微调,持股周期极短,产生的印花税(单边万分之五)与交易佣金会迅速蚕食掉策略全部 Alpha 超额收益;
    • 低频调仓的滞后弊端:若按季度(60 交易日)调仓,标的股价大幅上涨导致估值严重偏离时无法及时止盈退出;
    • 7 日平衡点:约 1.5 周的调仓周期,既给予低估值因子充分的价值回归时间窗口,又将换手率与交易成本控制在极低水平。
  • 股票池定期调仓的三大集合变动运算:
    • 设上一期股票池标的集合为 St−1S_{t-1},当期最新筛选出的股票池标的集合为 StS_t:
    • 新调入集合(Incoming=St−St−1\text{Incoming} = S_t - S_{t-1}):新进入估值优势区间的标的,进入择时买入监控队列;
    • 被调出集合(Outgoing=St−1−St\text{Outgoing} = S_{t-1} - S_t):估值过高或基本面劣化的标的,若当前持有则触发无条件清仓卖出(强制离场);
    • 留存集合(Retained=St∩St−1\text{Retained} = S_t \cap S_{t-1}):估值依然具备优势的标的,维持现有持仓状态,继续受均线择时规则保护。
  • 股票池持久化与基准收益率基准:
    • 将每期调仓结果按 {'date': date, 'codes': [code1, code2, ...]} 格式落库至 MongoDB stock_pool 集合;
    • 计算每期股票池等权平均收益率,作为衡量后续择时策略能否击败纯选股基准的对比基准。
┌─────────────────────────────────────────────────────────────┐
│                    股票池定期调仓集合变动模型               │
└──────────────────────────────┬──────────────────────────────┘
                               │
       上期股票池 (S_t-1)              当期股票池 (S_t)
       ┌──────────────────┐          ┌──────────────────┐
       │   被调出 Outgoing│  交集    │   新调入 Incoming│
       │ (S_t-1 - S_t)    │ Retained │ (S_t - S_t-1)    │
       │  ► 触发清仓卖出  │ (S_t-1∩S)│  ► 触发择时监控  │
       └──────────────────┴──────────┴──────────────────┘

一、动态股票池状态转移与交易路由

在量化交易引擎中,股票池不仅是一个静态名单,更是驱动交易路由状态机的核心动力:

┌────────────────────────────────────────────────────────────────┐
│                    调仓日状态转移与执行规则                    │
├────────────────────────────────────────────────────────────────┤
│ 1. 扫描 Outgoing 集合: 若账户持有该标的,立即发出 Sell 订单清仓│
│ 2. 扫描 Incoming 集合: 将标的加入候选监控列表,等待均线金叉触发│
│ 3. 扫描 Retained 集合: 标的保留在持仓中,若均线死叉则执行止损  │
└────────────────────────────────────────────────────────────────┘

二、MongoDB stock_pool 数据集合结构设计

在数据库中,每期调仓记录作为独立文档存储:

{
  "date": "2023-01-16",
  "codes": ["600000", "600002", "600005", "..."],
  "count": 100
}

通过在 date 字段建立唯一索引,回测引擎可秒级查询任意回测交易日所属的当期有效股票池。

⚠️ 调仓回测的偏差与成本:本讲名单为内置合成数据。真实回测中每次调仓都会产生佣金、印花税与冲击成本,名单越大、换手越快,摩擦对收益的侵蚀越重;名单只能用当时实际在市的股票计算(幸存者偏差),调入调出依据的财务数据必须已经披露(前视偏差),7 日调仓周期同样是可调参数、存在过拟合风险。本演示不构成任何收益承诺。


📝 动手练一练

下面我们用 Python 编写一段 股票池定期调仓与集合变动动态追踪脚本,模拟连续多个调仓周期中新调入与被调出标的的自动识别:

import pandas as pd
# 保证宽表在任意终端宽度下都完整打印(不折叠中间列),使输出确定、可复现
pd.set_option("display.width", 250)
pd.set_option("display.max_columns", None)
pd.set_option("display.max_rows", None)
import numpy as np

def track_stock_pool_rebalance(pool_history_dict, rebalance_dates):
    """
    股票池定期调仓与集合动态追踪引擎
    计算各个调仓周期: 新进集合 (Incoming)、调出集合 (Outgoing)、留存集合 (Retained)
    """
    records = []
    prev_set = set()
    
    for r_date in rebalance_dates:
        curr_list = pool_history_dict[r_date]
        curr_set = set(curr_list)
        
        # 集合运算
        incoming = curr_set - prev_set
        outgoing = prev_set - curr_set
        retained = curr_set & prev_set

        # 注意:不能直接 list(set)——Python 字符串的集合迭代顺序受 PYTHONHASHSEED
        # 影响,跨进程不稳定,会让同一份代码每次打印的名单顺序都不同。
        # 真实交易系统同样必须显式排序,保证候选名单可复现、可对账。
        records.append({
            'rebalance_date': r_date,
            'pool_size': len(curr_list),
            'incoming_count': len(incoming),
            'outgoing_count': len(outgoing),
            'retained_count': len(retained),
            'incoming_codes': sorted(incoming),
            'outgoing_codes': sorted(outgoing)
        })
        prev_set = curr_set
        
    return pd.DataFrame(records)

def test_rebalance_run():
    dates = ["2023-01-05", "2023-01-16", "2023-01-27"]
    
    # 模拟 3 期各 4 只股票的低 PE 股票池变动
    pool_data = {
        "2023-01-05": ['600000', '600001', '600002', '600003'],
        "2023-01-16": ['600000', '600002', '600004', '600005'],  # 600001/600003 调出, 600004/600005 调入
        "2023-01-27": ['600000', '600004', '600005', '600006']   # 600002 调出, 600006 调入
    }
    
    res = track_stock_pool_rebalance(pool_data, dates)
    print("================ 股票池定期调仓集合追踪报告 ================")
    print(res[['rebalance_date', 'pool_size', 'incoming_count', 'outgoing_count', 'retained_count']])
    print("------------------------------------------------------------")
    for _, row in res.iterrows():
        print(f"[{row['rebalance_date']}] 新调入买入候选: {row['incoming_codes']} | 调出强平清仓: {row['outgoing_codes']}")
    print("============================================================")
test_rebalance_run()

运行结果输出:

================ 股票池定期调仓集合追踪报告 ================
  rebalance_date  pool_size  incoming_count  outgoing_count  retained_count
0     2023-01-05          4               4               0               0
1     2023-01-16          4               2               2               2
2     2023-01-27          4               1               1               3
------------------------------------------------------------
[2023-01-05] 新调入买入候选: ['600000', '600001', '600002', '600003'] | 调出强平清仓: []
[2023-01-16] 新调入买入候选: ['600004', '600005'] | 调出强平清仓: ['600001', '600003']
[2023-01-27] 新调入买入候选: ['600006'] | 调出强平清仓: ['600002']
============================================================

👉 点击查看解析

本练习的核心是设计固定的调仓周期(如月度或季度),并在每次调仓时对比前后低PE股票池的集合差异,利用集合运算识别新增与剔除的股票,从而实现成分股变动的持续追踪。

本章小结

  1. 确立 7 日调仓纪律:兼顾价值因子的动态有效性与交易摩擦成本的最优控制。
  2. 掌握三元集合状态机:新调入标的纳入监控、被调出标的强制离场、留存标的继续持有并由均线择时护航。
  3. 完成选股层与执行层的解耦:股票池提供候选资产集,择时模块负责买卖触发,回测引擎负责订单撮合。

📋 行动清单 (Action Checklist)

  • 运行本讲调仓追踪代码,观察多期交集与差集的运算结果;
  • 验证调出标的清仓逻辑:确保持仓股一旦跌出前 100 名能在调仓日被准确卖出;
  • 预习下一讲,将 10 日均线择时信号接入股票池,开始搭建完整的回测撮合引擎。

—— 小象教研组

配套资料 · 直接下载

课件、代码、数据集等配套资料全部免费,点击即可直接下载,无需分享或解锁。

  • 第3章课件:上手搭建最简单的交易系统
    PDF · 3.8MB
    下载
  • 开发环境准备:软件安装指导
    PDF · 0.9MB · 建议第3章动手实践前先通读
    下载
  • 量化入门推荐书单
    PDF · 318KB
    下载