
先说结论如果你正在做ETF相关的量化回测、市场结构分析或者资产配置研究CnOpenData的ETF日线行情数据是一个值得放进备选池的底层数据源。我前后用了一周时间把这套数据完整过了一遍也拿它跑了几组策略和统计检验下面把字段细节、清洗思路、实操代码和踩过的坑一次性说清楚。1. 内容整体设计与思路拆解拿到任何一套行情数据第一步不是急着写代码而是先把数据的设计思路摸透。ETF日线数据和个股日线数据表面看都是OHLCV结构但ETF有几个特有的属性会直接影响数据处理方式这一点在拿到CnOpenData这套数据时体现得非常明显。1.1 核心需求解析这套数据解决什么问题ETF日线数据最典型的使用场景有三个一是做指数增强和ETF轮动策略的回测二是做ETF市场微观结构研究比如折溢价、流动性分层三是做资产配置里的品种筛选和替换。我自己主要用它做两件事跟踪ETF与标的指数之间的偏离度以及把ETF的历史波动率结构拆出来做风格因子。这两个任务对数据质量要求不太一样——前者要求复权处理准确、时间长且连续后者要求成交量、成交额字段没有异常断档。CnOpenData这套数据覆盖了沪深两市的股票型ETF、跨境ETF、商品ETF、债券ETF基本能支撑上述全部需求。很多人在拿到数据后习惯性直接算收益率这其实是个误区。ETF的日线价格里有相当一部分交易日受到分红、份额折算的影响如果不做复权处理就计算净值变动结果会系统性偏低。尤其像某些跨境ETF和商品ETF年内多次分红复权差异可以累积到相当可观的幅度。这个细节我会在第2部分展开。1.2 方案选型考量为什么值得把这套数据纳入工作流市面上能拿到ETF日线数据的渠道并不少从免费的数据接口到付费终端都有但真正适合系统化研究的并不多。免费的接口往往存在三个问题字段不全、历史深度不够、调取频率受限。付费终端则存在导出效率和批量处理的问题尤其是当你需要处理全市场两百多只ETF、跨度数年数据的时候逐只导出的操作成本非常高。CnOpenData这套数据是整表结构一次就能拉取多只ETF在同一时间范围内的全部行情记录这在批量处理场景下有天然优势。它的字段设计覆盖了从价格、成交到复权因子的完整链条既可以直接做截面分析也可以加工成面板数据跑时间序列模型。另外值得一提的是这套数据的字段命名很规范基本不需要做大量的重命名工作可以直接接入pandas或DuckDB流程。对需要把时间花在策略逻辑而非数据清洗上的研究者来说这是实打实的效率提升。2. 数据字段、预处理逻辑与核心实操要点2.1 字段结构说明与关键字段解读CnOpenData的ETF日线数据主体字段大致分为四类标识信息、交易信息、复权信息和市场状态信息。具体来说字段类别字段示例使用说明标识信息证券代码、证券简称、交易所用于合并其他数据源和市场分组交易信息开盘价、最高价、最低价、收盘价、成交量、成交金额基础行情用于技术分析和流动性刻画复权信息复权因子、累计复权因子处理分红和份额折算的核心依据市场状态交易状态、上市日期、退市日期辅助筛选有效交易区间这里我特别提醒两个字段。第一个是复权因子。CnOpenData提供的复权因子是后复权口径即基准日为最新交易日的复权方式。使用方式是后复权价格不复权收盘价×复权因子。千万不要直接拿复权因子当价格倍数去算收益率正确做法是把复权因子先做对数差分再叠加到价格序列的日收益率上。第二个是交易状态字段。ETF的交易日历与股票基本同步但存在两类例外基金建仓期内的停牌以及部分跨境ETF因为境外市场节假日导致的临时停牌。这两种情况在数据里表现为当日无成交但并非休市如果不加筛选地直接填充或跳过会把停牌当作零收益处理给后续的波动率计算带来明显的下行偏差。2.2 数据清洗与预处理一套可以照抄的流程我在处理这套数据时整理出了一套稳定可复用的流程核心分四步。第一步是去重与主键校验。ETF的代码在后复权基准日调整或份额折算时偶尔会出现同一交易日两笔记录的情况。我的做法是用“证券代码交易日期”做一次分组计数把出现次数大于1的记录单独导出检查再按成交量优先保留有效记录。第二步是交易状态筛选。把交易状态字段里所有非正常交易的记录剔除或者单独打标。我的建议是不要直接删除而是保留一个状态列这样后续如果想专门研究停牌前后行为还能把样本捡回来。第三步是复权处理。这一步的关键是计算日收益率的方式。正确公式是日收益率 (当日收盘价×当日复权因子) / (前日收盘价×前日复权因子) - 1这个公式无论中间隔了多少天都能正确处理分红和折算带来的价格跳变。我建议在生成收益率序列后顺手做一次极值检查直接标出单日涨跌幅超过20%的记录ETF由于申赎机制单日价格偏离通常远小于个股这些记录大概率是数据错误、分红除权未标记或极端市场情况。第四步是停牌与缺失值策略。ETF的停牌一般不会太长但如果做因子计算需要连续收益率我的做法是停牌天数在5天以内用NaN占位不填充超过5天则看具体原因。原因是分红的正常做复权即可原因是流动性枯竭的建议整段剔除。清洗完的数据我会额外生成两个派生字段一是对数收益率用于统计建模二是过去20日的日均成交额用于刻画流动性分组。这两个字段后面做策略分组时会反复用到。3. 实操过程与核心环节实现3.1 数据加载与多ETF批量处理示例我以Python为例给出一个可以直接改用的数据加载流程。假设你已经从CnOpenData拿到了CSV格式的ETF日线文件文件路径为 etf_daily.csv。import pandas as pd df pd.read_csv(etf_daily.csv, parse_dates[trade_date]) df df.sort_values([sec_code, trade_date]).reset_index(dropTrue) # 基础校验主键重复检查 dup_check df.groupby([sec_code, trade_date]).size() print(重复记录数:, (dup_check 1).sum()) # 交易状态过滤保留正常交易 df_valid df[df[trade_status] 正常交易].copy() # 复权因子处理生成后复权收盘价 df_valid[adj_close] df_valid[close] * df_valid[adj_factor] # 日收益率计算按每只ETF分组 df_valid[ret] df_valid.groupby(sec_code)[adj_close].pct_change() # 生成对数收益率 df_valid[log_ret] np.log(df_valid[adj_close] / df_valid.groupby(sec_code)[adj_close].shift(1))这段代码跑完之后你就得到了一张干净的标准面板表后面做任何策略回测或统计检验都能直接复用。这里有一个小技巧在合并多只ETF数据时不要用pd.concat逐文件拼接而是把所有文件读进一个列表后一次性pd.concat然后统一排序。多次拼接的开销虽然不大但在数据量大了之后会明显拖慢流程一次合并是更高效的做法。3.2 用清洗后的数据跑一个ETF动量策略示例下面分享一个完整的ETF动量轮动策略示例这个策略逻辑简单但能真实检验数据质量。策略规则是每月末选取过去20个交易日涨幅最高的5只ETF等权持有到下月末。# 生成月末截面 df_valid[ym] df_valid[trade_date].dt.to_period(M) # 计算每只ETF的月内累计收益用我们前面生成的对数收益率 monthly_ret df_valid.groupby([ym, sec_code])[log_ret].sum().reset_index() # 每月选收益前5的ETF def select_top5(group): return group.nlargest(5, log_ret)[sec_code].tolist() top5_per_month monthly_ret.groupby(ym).apply(select_top5).reset_index(nameselected) # 回测持有下月收益 merged monthly_ret.merge(top5_per_month, onym, howinner) port_ret merged[merged[sec_code].apply( lambda x: x in merged[selected].iloc[0] )][log_ret].mean()这个策略本身不是重点重点是运行过程中可以检查数据质量如果清洗时漏掉了复权这个策略会在分红月份出现异常信号因为除权造成的价格跳降会被误判为动量转弱如果停牌记录处理不当策略会在停牌期间输出缺失收益导致组合收益计算不连续。换句话说策略回测本身就是一套严格的数据校验工具。3.3 数据落库与增量更新建议如果你打算把这套数据长期用于跟踪研究我建议不要每次跑CSV而是把清洗后的数据落进数据库。SQLite或DuckDB都够用DuckDB在列存分析上更快一点。CREATE TABLE etf_daily AS SELECT * FROM read_csv(etf_daily.csv);落库后按日期建立索引后续增量更新只需要处理新增交易日数据不用每轮全量重读。这里有两点需要注意一是复权因子更新时历史全量记录都需要重新计算所以推荐在库里保存原始的close和adj_factor不要只保存adj_close二是数据库表结构里主键务必是“证券代码交易日”否则后续频繁更新时容易出现重复记录。4. 常见问题与排查技巧实录4.1 典型数据异常与排查方法我在处理这套数据时实际遇到了几类问题虽说不上高频但都值得记录。第一类是相同交易日重复记录。出现原因主要是数据拼接时没有按主键去重或者源数据中个别ETF在份额折算日生成了额外交割记录。排查方法就是用我前面写的分组计数语句定位重复记录后人工核对当天是否有折算公告。第二类是复权因子为1的时间段。如果你看到某只ETF早期的复权因子恒等于1先不要改数据因为这大概率是基金成立初期未发生分红或折算的正常状态。只有当复权因子序列出现不连续的跳变但价格未同步变化时才需要怀疑数据错误。第三类比较隐蔽是除权日价格缺失。ETF在实施大额分红时基金管理人会提前发布公告但实际除权日的行情记录偶尔会因撮合延迟而缺失。这种情况下如果用前一日收盘价直接填充会把除权缺口当作真实亏损影响后续所有指标计算。我的处理方式是如果发现缺失日前后有分红公告先把缺失位置标记出来再用复权因子倒推一个合理的除权参考价而不是简单前值填充。4.2 数据质量校验的五个量化指标除了上面这些靠肉眼和经验判断的问题我建议数据到手后先用几个客观指标做质量体检这五个指标能快速暴露大多数异常校验项计算方式合理区间异常嫌疑空值占比核心字段空值数/总记录数0.5%超10%则检查整段数据负价格数量价格字段小于等于0的记录数0出现即报错极端涨跌幅比例单日涨跌幅绝对值超过20%的记录占比盘内ETF通常0.1%明显高于则查复权或拆分交易日连续性每只ETF的相邻交易间隔分布间隔1-3天为主长间隔频繁需查停牌标识成交额与成交量比值日成交额/日成交量与价格区间匹配比值长期异常可能是数量级错误这五个指标的做法是任何一个亮红灯都先暂停后续分析回到清洗环节找原因。我见过不少人在数据上栽跟头最后追踪到根因就是某一年的某只ETF成交金额漏了一个小数位这种错误在后续统计里很难被发现但会让所有与该ETF相关的结论失真。4.3 关于策略研究中的过拟合与数据窥探最后说一个和数据质量关系不大、但和数据分析结果高度相关的问题。ETF日线数据本身没有主动或被动地制造过拟合但当我们反复在历史数据上测试策略、不断调整参数时难免陷入数据窥探的陷阱。我个人的经验是用CnOpenData这套数据做研究时至少要把样本内和样本外做一个明确切割。比如用前70%的数据做参数选择和因子筛选后30%的数据只做一次最终验证不在这个区间上迭代调参。这个习惯可以避免很多“历史回测很漂亮实盘一塌糊涂”的尴尬。另外ETF市场在过去几年经历了明显的品种扩容和流动性分层。早期上市的部分ETF成交非常寡淡用它们的历史数据回测得到的结论在今天大概率不适用。做流动性筛选时建议把“近20日日均成交额高于某个阈值的ETF”作为基本门槛这个阈值可以根据研究目标和市场阶段灵活设定。5. 一些个人体会这周用这套数据做下来我的整体感受是一套干净、字段完整、口径清晰的ETF日线数据其实是量化研究中容易被低估的底层资产。很多高端策略的差异并不在模型复杂度上而在于底层数据的处理精度。复权、除权、停牌这些细小的环节每多处理干净一个最终策略结果的置信度就多一分。最后再说一个小技巧如果在研究过程中发现某只ETF的收益序列与其他同类ETF出现无法解释的长期背离先别急着去找策略问题回头检查一遍该ETF的份额变化记录和基金公告。ETF的场内价格和净值之间偶尔会有阶段性偏离但长期背离一定事出有因。数据的背后永远是产品本身的规则在起作用把规则吃透了数据自然就顺了。