2. ETF池构建与数据获取

相对强弱排名轮动的核心逻辑,是“在同一可比范围内,选出近期动量最强的标的”。因此,ETF池的构建质量直接决定了策略上限:池子太窄,轮动没有腾挪空间;池子太杂,不同资产类别混在一起比较,强弱信号会失真。本节要解决三件事:选什么标的、从哪里取数、取到的数据如何清洗成可回测的干净面板。

一、ETF池的构建原则

构建轮动池时,建议遵循“同质可比、流动性优先、类别分层”三条原则。所谓同质可比,是指参与排名的ETF应属于可相互替代的资产类别,例如宽基指数之间、行业主题之间、或股债商品大类之间,而不是把货币ETF与科技ETF放在同一张排名表里直接比涨幅。流动性优先,是指日均成交额过低的品种要剔除,否则回测收益会被滑点和冲击成本吞掉。类别分层,是指先按资产大类分组,再在组内做相对强弱排序,避免单一风格主导整个排名。

一个可参考的示例池结构如下(参数为教学示例,非实测结果):

层级 示例标的 作用 筛选门槛(示例)
宽基 沪深300ETF、中证500ETF、创业板ETF 捕捉大小盘风格切换 近60日日均成交额 > 5000万元
行业主题 半导体ETF、医药ETF、新能源ETF、券商ETF 捕捉行业轮动 上市满1年,日均成交额 > 3000万元
跨境 纳指ETF、恒生科技ETF 提供跨市场分散 注意溢价率与QDII额度
防御 国债ETF、黄金ETF 风险偏好下降时的避风港 规模 > 10亿元

需要特别提醒的是“存续期”和“更名/转型”问题。部分ETF上市不足一年,历史数据不足以计算长周期动量;还有部分ETF发生过跟踪指数变更或基金转型,早期行情与当前标的不再可比。构建池子时应记录每个标的的上市日期与跟踪指数,必要时对转型前的数据做截断处理。

二、历史行情数据的获取

回测所需的最小字段集为:交易日期、开盘、最高、最低、收盘、成交量、成交额,以及复权因子或分红送转信息。数据来源通常有三类:一是行情终端导出(如通达信、同花顺的日线导出),适合小规模手工验证;二是开源数据接口(如AkShare、Tushare等),适合批量拉取与自动化更新;三是券商或数据商的本地数据库。无论哪种来源,都应保留原始文件并记录拉取日期,便于复现。

以开源接口为例,批量下载的典型流程是:先维护一份标的代码清单(含交易所前缀),再循环请求日线数据,最后按标的分别落盘为CSV。下面是一段示意性代码,展示批量获取与合并的基本骨架:

import akshare as ak
import pandas as pd

codes = {"510300": "沪深300ETF", "510500": "中证500ETF",
         "159915": "创业板ETF", "512480": "半导体ETF"}

frames = []
for code, name in codes.items():
    df = ak.fund_etf_hist_em(symbol=code, period="daily",
                             start_date="20200101", end_date="20241231",
                             adjust="qfq")   # 前复权
    df["code"] = code
    df["name"] = name
    frames.append(df)

panel = pd.concat(frames, ignore_index=True)
panel.to_csv("etf_panel_raw.csv", index=False)

注意 adjust 参数的选择:qfq 为前复权,hfq 为后复权,"" 为不复权。回测中若使用前复权价,历史价格会随最新分红不断变化,导致“今天跑的回测”和“昨天跑的回测”结果不一致;后复权价则以上市首日为基准,历史值稳定,更适合可复现的回测。建议在数据层统一采用后复权,并在展示层再换算为前复权或原始价。

三、复权、停牌与缺失值的处理

复权处理的关键是“价格序列与成交量序列要匹配”。分红除权日,价格会出现向下跳空,若直接用不复权收盘价计算动量,会把分红误判为下跌。正确做法是:用复权因子把收盘价调整为连续序列,同时成交量不做复权(成交量是实际成交股数),但若用成交额计算流动性,则成交额本身不受复权影响,可直接使用。

停牌处理要区分两种情况。第一种是标的自身停牌,行情缺失,此时该日不应参与排名,也不应产生交易信号;第二种是标的正常交易但数据源漏行,属于数据缺失,需要补全。判断方法是对每个标的生成完整的交易日历索引,再与原始数据做左连接,缺失的日期即为“疑似停牌或漏数”。对于停牌日,建议用前一日收盘价填充价格(保持净值连续),但将成交量与成交额置为0,并在信号计算时把该标的标记为“不可交易”。

缺失值的处理原则是“先诊断、再填补、后标记”。诊断阶段统计每个标的的缺失天数与连续缺失长度;填补阶段对短缺口(如1~3个交易日)可用前值填充,对长缺口(如超过10个交易日)应直接剔除该标的在该区间的排名资格;标记阶段在面板中增加 tradable 布尔列,供后续信号模块过滤。下面这张图展示了从原始数据到可回测面板的清洗链路与关键判断点:

ETF数据清洗链路:从原始行情到可回测面板 原始行情数据 日期/OHLC/量额/复权因子 标的元信息 上市日/跟踪指数/转型记录 交易日历 全市场统一交易日序列 复权处理:统一采用后复权价 收盘价 × 复权因子 → 连续价格序列;成交量不复权;成交额直接使用 交易日历对齐与停牌识别 完整日历左连接原始数据 → 缺失日 = 疑似停牌或漏数 停牌日:价格前值填充,量额置0,标记 tradable = False 缺失值分级处理 短缺口(≤3日):前值填充;长缺口(>10日):剔除该区间排名资格 输出字段:date / code / close_adj / volume / amount / tradable 可回测面板 宽表:日期 × 标的 价格连续、无空洞 含可交易标记 含流动性字段 可直接计算动量 与排名信号对接

清洗完成后,建议做三项快速校验:一是检查每个标的的首个有效日期是否等于其上市日;二是检查复权后价格序列是否存在异常跳变(如单日涨跌幅超过20%且无对应公告);三是检查 tradable 为 False 的天数占比,若某标的长期不可交易,应将其从池中剔除。这三项校验能拦截大部分数据质量问题。

四、面板组织与更新机制

最终交付给回测模块的数据,建议组织成“长表 + 宽表”双形态:长表保留 date, code, close_adj, volume, amount, tradable 等字段,便于逐标的核查;宽表以日期为索引、标的为列,存放复权收盘价,便于向量化计算动量排名。更新机制上,采用“增量追加 + 全量校验”的方式:每日收盘后追加当日数据,每周对最近一段区间做一次全量重算,防止接口返回的历史数据被修订而未被发现。

复权方式一旦确定,应在整个回测周期内保持一致。若中途从后复权切换为前复权,历史动量值会整体漂移,导致回测结果不可比。建议在数据配置文件中显式记录复权方式与数据拉取日期。

至此,ETF池与数据面板已经具备进入信号计算的条件:池子有明确的准入与剔除规则,价格序列连续可比,停牌与缺失值有标记而非被静默填补。下一步的动量计算与排名,都将建立在这份干净面板之上。