4. 数据准备与清洗实战
轮动策略的调仓频率优化,本质上是在比较“不同换仓节奏下,同一套选股逻辑的收益与成本差异”。因此,数据集必须同时满足三个条件:价格序列足够干净、成交量能反映真实可交易性、财务数据在时间上不超前。很多回测结果失真,并非策略逻辑有误,而是数据在复权、停牌、财务披露时点上处理不当。本节以一个“行业动量轮动”示例为主线,完整走一遍从原始数据到可优化数据集的构建过程。
一、明确数据集的最小字段与时间口径
频率优化需要的数据粒度通常高于日频策略本身,因为要模拟“每周、每两周、每月”等不同调仓周期。示例设定如下:标的池为 8 个申万一级行业指数(用其成分股加权价格近似),回测区间 2018-01-01 至 2024-12-31,基准为沪深300。所需字段包括:
- 价格类:交易日、标的代码、开盘价、收盘价、复权因子;
- 成交类:成交量、成交额、当日是否停牌、是否涨跌停;
- 财务类:报告期、营业收入同比、ROE、公告日期(用于判断可用时点);
- 日历类:交易日历、节假日、月末/周末标记。
时间口径必须先统一:所有价格按后复权处理,保证收益率连续;财务数据按公告日而非报告期对齐,避免“未来函数”。这是后续频率比较公平的前提。
二、价格清洗:复权、停牌与异常值
原始行情常见三类问题。第一是除权除息造成的价格跳空,若直接用不复权收盘价计算动量,会把分红误判为下跌。处理方式是使用复权因子:adj_close = close × adj_factor,并以区间首日为基准归一化。第二是停牌,停牌日价格不变但不可交易,若在调仓日恰好停牌,应顺延至下一可交易日或以最近可成交价替代。第三是异常值,例如单日涨跌幅超过 ±20%(非涨跌停制度下的数据错误),应标记并剔除。
# 示例:价格清洗核心逻辑(伪代码)
df = load_daily_price(codes, start, end)
df["adj_close"] = df["close"] * df["adj_factor"]
df["ret"] = df.groupby("code")["adj_close"].pct_change()
# 标记停牌与异常
df["is_suspend"] = df["volume"] == 0
df["is_outlier"] = df["ret"].abs() > 0.20
df.loc[df["is_outlier"], "ret"] = np.nan
# 停牌日收益置零,避免污染动量
df.loc[df["is_suspend"], "ret"] = 0.0
清洗后应做一次完整性检查:每个标的在区间内的交易日数量是否接近交易日历总数,缺失率是否低于 2%。若某行业缺失率过高,说明其成分或代码映射有问题,需回查数据源。
三、成交量与可交易性过滤
频率优化会放大交易成本的影响,因此必须让“不可交易”的样本不进入调仓候选。示例规则:调仓日若某标的成交额低于过去 20 日均值的 30%,或当日停牌、涨停无法买入,则将其从候选池剔除,顺延选择下一名次标的。这一步直接决定了高频调仓的回测是否可信。
四、财务数据的时点对齐
财务指标最容易引入前视偏差。正确做法是:以公告日期为可用起点,将财务记录合并到“公告日之后最近一个交易日”。例如某行业 2023 年中报于 8 月 28 日公告,则 8 月 28 日及之后的交易日才可使用该数据,之前一律沿用上一期。合并时使用merge_asof按日期向后匹配,而非按报告期直接 join。
五、构建频率优化专用面板
完成上述清洗后,将数据整理为“日期 × 标的”的宽表面板,每个调仓周期只需按日历切片取数。下图展示了从原始多源数据到可优化数据集的完整处理链与关键判断点:
六、数据质量校验清单
在进入频率比较之前,建议逐项核对以下指标,任何一项不达标都应回到对应清洗环节修正:
| 校验项 | 示例阈值 | 不达标时的处理 |
|---|---|---|
| 价格缺失率 | < 2% | 回查代码映射与数据源 |
| 复权后单日收益异常 | |ret| > 20% 占比 < 0.5% | 标记剔除并记录 |
| 停牌日收益 | 全部为 0 | 重跑停牌置零逻辑 |
| 财务公告日覆盖 | 100% 有公告日 | 缺失则用报告期+90天保守替代 |
| 调仓日可交易标的数 | ≥ 5 个 | 放宽成交额阈值或扩大标的池 |
完成以上步骤后,得到的面板数据即可直接支撑后续“周频、双周频、月频”的对比实验:每个调仓周期只需按日历切片,取候选池、因子值与成本参数,即可进入频率优化的回测环节。数据口径的一致性,是频率比较结论可信的根本保障。