4. 数据获取与清洗实战
跨市场ETF轮动模型的第一道工程门槛,不是策略逻辑,而是数据。一个轮动信号往往同时依赖A股ETF的收盘价、港股或美股ETF的净值、基金份额变化、折溢价率以及人民币汇率。这些数据来自不同渠道、不同时区、不同频率,字段口径也不一致。如果直接把它们拼进一张表就计算动量或估值分位,模型会在无声处失真。本节围绕“取什么、从哪取、怎么对齐、缺了怎么办”四个环节展开,并给出可复用的处理流程。
一、五类数据的来源与口径
ETF行情数据指二级市场的开高低收、成交额与成交量,通常来自交易所行情或券商接口,A股为日频15:00收盘,港股为16:00收盘,美股为次日凌晨收盘(北京时间)。净值数据指基金单位净值(NAV),来自基金公司公告或交易所披露,一般T日盘后公布,QDII类ETF因海外市场时差常延迟1~2个交易日。份额数据指基金总份额与流通份额,来自基金定期报告或交易所每日披露,是判断资金申赎方向的关键。折溢价率由二级市场价格与净值推导:折溢价率 =(收盘价 − 单位净值)/ 单位净值,溢价为正、折价为负,需特别注意QDII ETF的净值日期滞后问题。汇率数据用于把港币、美元计价资产折算为人民币,常用中国外汇交易中心公布的中间价或离岸即期汇率。
二、多源数据的对齐策略
对齐的核心是统一“时间轴”和“计价货币”两个维度。时间轴上,先建立以A股交易日为主日历的基准索引,再把港股、美股数据映射进来。对于时区差异,常用做法是:港股ETF当日行情与A股同日对齐;美股ETF因收盘晚于A股,其T日行情实际对应北京时间T+1日凌晨,建模时应把美股T日数据对齐到A股T+1日,避免使用未来信息。对于净值滞后,QDII ETF的T日净值往往在T+2才可得,若用T日价格除以T日净值计算折溢价,会得到虚假的极端值,正确做法是用“可得的最新净值”并记录其对应日期,或直接使用披露的折溢价率字段。
货币维度上,港币、美元资产须乘以对应汇率折算为人民币,再与A股ETF比较。汇率本身也有日期,应与资产价格使用同一估值日,或明确采用“资产用T日、汇率用T日”的一致规则。下面这张图展示了五类数据从来源到对齐、再到进入轮动因子表的完整链路,以及各环节的关键处理动作。
三、缺失值的成因与处理规则
缺失在ETF数据中非常普遍,成因主要有四类:一是节假日错位,A股休市而港股开市,或反之;二是停牌,ETF因标的指数调整或流动性问题暂停交易;三是净值延迟,QDII产品净值晚于价格公布;四是汇率休市,境外假期导致汇率无报价。不同成因应对方式不同,不能一律用前向填充。
实践中建议按“先判断、再填充、后标记”的顺序处理。对于节假日错位,应以基准日历为准,非交易日不生成记录,而不是填0;对于停牌,价格用停牌前最后有效价前向填充,但成交额应置为0或缺失,避免把停牌日误判为低波动;对于净值延迟,使用最近可得净值并保留nav_date字段,计算折溢价时若nav_date与trade_date相差超过2个交易日,则将该折溢价标记为不可用;对于汇率缺失,优先用同一来源的前一有效值填充,并限制最大填充天数(如不超过3个交易日),超过则标记为缺失并让该资产当日退出轮动候选池。
import pandas as pd
# 假设 price_df 含 trade_date, etf_code, close, volume
# nav_df 含 nav_date, etf_code, nav
# 1) 以A股交易日历为基准
cal = pd.to_datetime(price_df['trade_date'].unique())
cal = pd.DatetimeIndex(sorted(cal))
# 2) 净值按可得日期合并,保留净值日期
df = price_df.merge(nav_df, left_on=['trade_date','etf_code'],
right_on=['nav_date','etf_code'], how='left')
df['nav_age'] = (df['trade_date'] - df['nav_date']).dt.days
# 3) 折溢价计算,净值过旧则置空
df['premium'] = (df['close'] - df['nav']) / df['nav']
df.loc[df['nav_age'] > 2, 'premium'] = None
# 4) 停牌日:价格前向填充,成交额置0并标记
df['data_flag'] = 'normal'
mask_suspend = df['volume'].fillna(0) == 0
df.loc[mask_suspend, 'close'] = df['close'].ffill()
df.loc[mask_suspend, 'data_flag'] = 'suspend_filled'
# 5) 汇率缺失:限3日前向填充
fx = fx_df.set_index('date')['rate'].reindex(cal)
fx = fx.ffill(limit=3)
df['fx_rate'] = df['trade_date'].map(fx)
df.loc[df['fx_rate'].isna(), 'data_flag'] = 'fx_missing'
这段代码体现了三个关键约束:净值年龄超过2日不计算折溢价、停牌价格填充但打标记、汇率填充设上限。它们共同保证进入因子计算的数据“可用且可追溯”。
四、对齐后的校验与结果判读
清洗完成后不能直接建模,应先做三项校验。第一,检查每个ETF在基准日历上的记录覆盖率,若某只ETF覆盖率低于90%,说明其停牌或数据源缺失严重,应剔除或降权。第二,检查折溢价率的分布,正常ETF日频折溢价多在±2%以内,若出现±10%以上的极端值,多半是净值日期错配或汇率未折算,需回查nav_date与fx_rate。第三,检查跨市场收益的一致性,例如同一指数在A股与港股的两只ETF,折算人民币后的20日收益差应在合理范围内,若差异过大,往往是汇率日期或复权处理不一致。
完成上述步骤后,得到的宽表应包含交易日、ETF代码、人民币收盘价、净值及其日期、折溢价率、份额、汇率和数据标记字段。这张表是后续动量、折溢价、资金流因子的唯一输入,任何字段的口径变化都应在data_flag中留痕,确保模型回测与实盘使用同一套数据规则。