港股复权行情数据全指南:因子逻辑与回测陷阱解析 两年前我第一次跑港股组合回测结果让我懵了很久选的几只明星股当年明明都在涨组合收益却是负的。逐笔对账后才发现问题根本不是选股策略而是行情数据没做复权处理。港股蓝筹分红又勤又快股价每到除净日就跳空一截如果你直接拿原始收盘价去算收益率这个下跌其实是分红造成的幻觉而不是市场真的在跌。这篇文章就是从那一次踩坑开始结合我自己长期使用CnOpenData港股上市公司复权行情数据的经验整理出来的。内容覆盖复权的底层原理、复权表的字段设计与使用逻辑、回测中前复权和后复权的选择、数据质量的验证方法以及港股行情数据处理里那些不容易察觉的坑。适合经常接触港股日线数据的量化研究者、金融学术团队、策略开发者和数据工程同学参考。1. 为什么港股比A股更需要复权数据1.1 除权除息制造了一种假跳空先聊一个最基础的问题复权到底在消除什么一支股票的价格是含权的。公司宣布每股派现0.5港元在除净日港股里除息日就叫除净日之前买入的投资者有权获得这笔现金分红。除净日当天交易所会在前收盘价基础上扣掉这0.5港元形成新的开盘参考价。假设前收盘是10港元除息后当天开盘参考价就是9.5港元左右。如果行情表里保存的是原始价格K线图上就会凭空出现一个向下的跳空缺口。更麻烦的是收益率计算你今天以10港元买入第二天价格变成9.5港元直接算日收益率是-5%。但实际上你获得了0.5港元分红总资产并没有减少。也就是说不复权数据会在除息日产生一个假的负收益干扰所有基于价格变化的统计计算。这个道理听起来简单但在实际数据处理里非常容易被忽略。因为单个股票单次分红造成的跳空可能只有1%到2%藏在日常波动里看不出来。可一旦你做的是组合层面、跨多年、上百只股票的收益统计这些假跳空会系统性压低收益尤其是高股息组合误差相当可观。1.2 港股的分红文化让假跳空成为常态A股公司分红的频率和比例这几年虽然在提高但整体上仍不算高频。港股不一样这是一个成熟的、以机构投资者为主的高分红市场。汇丰控股常年按季度派息香港本地地产股、公用事业股、中资蓝筹每年派息比例都不低。很多港股上市公司的股息率长期维持在4%到6%甚至更高。高分红带来的直接结果就是除净事件非常频繁。一只蓝筹股一年除息两到四次是常态有些公司还会在中期报告和年度报告各有一次派息。每一轮除净都会让原始价格出现一次跳空一年下来不复权行情数据里的累计跳空幅度可能达到10%以上。这意味着如果你拿到一份港股原始行情数据什么处理都不做直接算长期收益率高股息股票的收益会被显著低估。这一点和A股有很明显的差异。A股虽然也有除权除息但分红频率低、单次比例小不复权数据在短周期研究里造成的偏差相对可控。港股的场景决定了复权不是可选项而是必选项。1.3 拆股和合股另一种会撕裂数据连续性的操作除息之外股本结构变化是另一个必须复权的理由。港股市场拆股和合股都相当常见。腾讯控股在2014年做过一次1拆5即每股拆成5股股价从500多港元瞬间变成100多港元。如果不复权K线图上会出现一个巨大的向下缺口看起来像股价崩了40%实际上股东权益完全没变。合股则相反价格会突然变大。港股里一些长期下跌的公司会把每10股合并成1股股价从0.5港元变成5港元不了解情况的投资者甚至会把合股误读成暴涨。对于量化研究来说拆股合股的处理难度比分红更高因为价格调整比例大可能跨越多个交易日的数据连续性。如果行情表只提供未经调整的价格任何技术指标、趋势判断、历史收益统计都会出错。所以我们可以这样理解复权数据它解决的核心问题是让价格序列在除息、拆股、合股这些公司行为发生时仍然连续可比使得今天的价格和五年前的价格处于同一个度量口径。2. CnOpenData的复权行情表字段设计与使用逻辑2.1 一张典型的港股复权日线表长什么样以CnOpenData这类学术数据平台提供的港股上市公司复权行情数据为例典型的日线复权表通常包含以下字段字段说明stock_code港交所证券代码通常为5位数字编码如00700.HKstock_name证券简称trade_date交易日期open当日开盘价原始未复权行情high当日最高价low当日最低价close当日收盘价volume成交量单位通常为股amount成交金额单位通常为港元adj_factor复权因子exchange交易所标识不同厂商字段命名可能略有差异但核心逻辑一致价格字段保存的是交易所实际成交的原始价格再单独给出一个复权因子。这个设计非常关键后面会单独讲。交易日部分港股和A股并不完全一致。港股没有A股的春节、国庆长期休市但会有圣诞节、复活节、佛诞等假期还有台风导致的临时休市。所以字段里的trade_date应该以港交所实际交易日历为准不能直接用A股交易日拼接。2.2 复权因子的计算公式与使用逻辑复权因子的核心作用是把原始价格还原成调整后价格。不同平台对因子的初始化和方向定义可能不同但基本原理是一致的。假设某只股票在除净日D派发每股现金股息d前一交易日D-1的收盘价为P₁。则除净日参考价P_ref P₁ - d。为了让价格连续除净日及之后的原始价格需要乘以一个因子f P₁ / (P₁ - d)。如果是拆股或送股每股变成原来r倍数量的股份那么参考价P_ref (P₁ - d) / r因子更新为f P₁ × r / (P₁ - d)。在一条完整的复权表里adj_factor通常是一个从上市日或某个基准日开始累计的数值。拿到因子后有两种常用变换后复权价格 close × adj_factor前复权价格 close × 最新adj_factor / 各日adj_factor这里要注意不同数据源对adj_factor的语义可能有差异。有的定义是后复权价格与原价之比有的定义是累计分红送股调整倍数。拿到一张表之后建议先用一个已知除息日的样本验证因子的变化逻辑确认无误后再批量使用。2.3 给因子而不是直接给复权价的设计逻辑我最初接触复权表的时候有一个困惑为什么厂商不直接给好前复权或后复权价格非要给个因子让用户自己算用过一段时间就明白了。复权价格本身依赖基准日。前复权是以最新价格为基准调整历史价格后复权是以早期某个价格为基准调整后续价格。你下载一份后复权收盘价如果研究需求变了想要前复权口径就需要重新下载数据。但如果你手里有原始价格和复权因子两套口径都可以随时切换还不受基准日限制。对做学术研究的人来说这个设计尤其重要。事件研究法要计算超额收益可能需要对事件日之前和之后分别采用不同的收益口径因子回测中更新数据时只需要把新增交易日的因子追加进来而不需要重建整条价格序列。因素因子独立存放价格与因子解耦灵活性高得多。2.4 与其他常见数据源的横向对比我自己也用过不少行情数据渠道简单给个横向比较数据来源复权能力适用场景成本CnOpenData提供原始价格复权因子口径透明学术研究、批量建模相对友好Tushare Pro提供前复权、后复权接口个人策略开发积分制Wind/Choice复权字段全面有前后复权选项券商、机构投研终端费用较高Yahoo Finance提供调整后价格但口径不透明快速验证、海外研究免费选择数据源时我的建议是优先确认三件事是否提供原始未复权价格、是否提供可解释的复权因子、历史上是否包含已退市股票。第三点对学术研究特别重要如果数据表只保留当前存续公司回测就会引入幸存者偏差。3. 回测陷阱前复权、后复权选错结论完全不同3.1 前复权和后复权到底差在哪前复权和后复权的数学关系并不复杂但使用场景差异非常大。前复权以最新交易日为基准调整历史价格。它的特点是当前价格保持真实市场价历史价格会被压扁或拉长。一张前复权K线图看起来干净顺滑技术指标可以正常计算但有一个隐蔽问题每次发生新的除权除息整条历史价格序列都会被重新计算。你今天下载的前复权数据和三个月后下载的同一区间前复权数据历史价格可能不一样。后复权则以早期某个日期为基准后续价格被不断放大。高分红股票经过多年复权后后复权价格可能会远高于真实股价看起来几十倍甚至上百倍但它保留了真实的长期收益轨迹。在任意两个日期之间计算收益率后复权口径下是准确的。一句话总结前复权适合看图和展示后复权适合算收益和做量化分析。3.2 我的回测口径后复权算收益前复权做展示我在自己的回测流程里有一条铁律所有收益率计算一律使用后复权价格只有最终画图、展示持仓市值时才切换前复权口径。用CnOpenData的复权因子做转换核心代码很简单import pandas as pd df pd.read_csv(hk_adj_daily.csv, parse_dates[trade_date]) df df.sort_values([stock_code, trade_date]) # 后复权价格 df[post_close] df[close] * df[adj_factor] # 前复权价格以最新交易日因子为基准 last_factor df.groupby(stock_code)[adj_factor].transform(last) df[pre_close] df[close] * last_factor / df[adj_factor] # 日收益率必须用后复权价格计算 df[daily_ret] df.groupby(stock_code)[post_close].pct_change()对比一下错误用法。直接用原始close算pct_change一只除息股票在除净日会出现一个近似等于股息率的负收益率。假设股息率5%在熊市行情里你可能会把这个负收益归因于市场下跌在牛市里则会把它误认为个股利空无论哪一种都是错误归因。把这种错误收益率累积成月度、年度收益偏差会被不断放大。还有一个细节如果你需要周线或月线数据建议先从复权日线聚合而不是直接下载不复权周线。原因很简单周线、月线的复权是非线性的尤其遇到周内除息时直接用周线价格算收益会丢失事件日信息。3.3 事件研究为什么必须避开前复权前复权价格有一个很少有人提到的性质它隐含了未来信息。前复权是以最新价格为基准把历史价格统一调整到现在这个时点。这意味着今天发生的一笔分红会改变五年前的价格。如果你做事件研究研究某一年年报发布前后的股价反应前复权历史数据里已经混入了之后多年的除权信息。这些未来信息在学术上属于典型的前视偏差会直接污染超额收益的估计。事件研究和因子回测的正确做法是完全一致的使用后复权价格或者使用原始价格配合事件日当天的收益调整。这也是为什么我特别强调数据表里原始价格和复权因子分开存放的设计对研究型用户更友好——你可以根据研究需要自由构造不引入未来信息的口径。我自己做多空组合测试时净值曲线计算全部基于后复权价格因子排序也基于后复权收益率。只有最后给团队汇报时会把后复权净值曲线重新映射回前复权展示层面方便同事直观理解。4. 拿到表别急着跑四个数据验证方法4.1 用港交所披露易核对除净日期数据表拿到手第一件事不是写策略代码而是验证数据本身。港交所有一个公开渠道叫披露易可以查到上市公司发布的公告。我会选三四只近期有分红记录的港股比如汇丰控股、中国移动这类稳定派息的公司在披露易里找到他们的除净日公告记录每股派息金额和除净日期然后与复权表里的价格变化、因子变化逐一核对。核对逻辑很简单除净日当天原始价格会出现向下跳空跳空幅度大约等于每股派息同时复权因子应该在这一天有一个明确的变化。如果因子没有任何变动或者价格跳空幅度与派息金额明显不匹配数据就有问题。这个步骤大概花20到30分钟但能避免后面几天的无效工作。4.2 手工复算复权因子光看因子的变化还不够我会做一次手工复算。假设某股票在6月5日除净6月4日收盘价为20.50港元公告显示每股派现0.50港元没有送股和拆股。那么除净日参考价应该是20.00港元。如果此前复权因子为1除净日当天的因子应为20.50 / 20.00 1.025。我用数据表验证6月5日调整后的后复权价格为20.00 × 1.025 20.50正好与前一日收盘价连续。如果除净日同时发生拆股比如1股拆成2股参考价就是20.50 - 0.50/ 2 10.00港元因子更新为20.50 × 2 / 20.00 2.05。验证后复权价格为10.00 × 2.05 20.50同样连续。这种手工复算不需要复杂工具一张Excel就能完成。挑两到三个样本做一遍因子逻辑是否正确就非常清楚了。4.3 双源交叉验证后复权收益率单源验证可能不够我还会用另一个独立数据源做交叉验证。比如拿Tushare或Wind同一只股票同一天的收盘价数据用各自复权后的价格计算区间收益率两者相关系数应该非常接近1绝对值差异应该在极小范围内。注意交叉验证时不要直接比较复权因子因为不同数据源的因子基准定义可能不同。要比较的是后复权收益率这个指标不受基准日影响是统一的物理量。如果一个源算出来某只股票过去一年的收益是20%另一个源算出15%差异里往往藏着除净事件或数据拼接的问题。4.4 边界样本检查数据验证里另一类容易被忽视的问题是边界样本。第一类是停牌样本。长期停牌的股票在行情表里可能出现价格长时间不变的情况要确认表里有没有停牌标记或者有没有成交量为0的交易日保留。第二类是仙股港股里股价低于1港元的股票很多成交稀疏很多交易日成交量为零要确认这些行的价格字段是真实成交价还是空值填充。第三类是退市股票。学术回测最忌讳幸存者偏差如果数据表不包含已经私有化、退市的公司任何策略回测结果都会偏乐观。CnOpenData这类学术平台的表通常会尽量覆盖历史存续样本但拿到数据后自己还是要确认一下样本里有没有已退市股票最后交易日期是否完整。5. 港股行情数据使用的五个隐藏坑5.1 别拿A股交易日历硬套港股第一个坑就特别常见。A股和港股的交易日并不完全重合。港股没有A股的国庆黄金周但有圣诞节、复活节、佛诞日等假期每年还可能有台风导致的临时停市。如果你在本地生成一个交易日序列直接沿用A股日历会出现两种情况一是港股某天有交易而你的序列里没有导致数据丢失二是港股休市而序列里仍然有交易导致收益率为0或空值。正确的做法是直接使用复权表里出现的trade_date集合或者从港交所官网获取交易日历。AH两地上市公司的研究尤其要小心同一家公司A股和港股的交易日可能不同直接用同一套日期来merge会对齐出错。5.2 收盘价与收市竞价机制港股在2016年重新推出了收市竞价交易时段。在正常交易日16:00至16:10之间系统会按竞价方式确定收盘价。一些流动性不足的股票收盘竞价价格可能与最后一笔连续交易价格有明显差异。做日线研究时要确认数据表里的close字段是官方收盘价也就是收盘竞价时段确定的最终价格而不是15:59的最后一笔成交价。两种口径大部分时候差异不大但在市场剧烈波动时可能产生不小的偏差。如果表里有特殊标识说明收盘价类型务必看清楚。5.3 代码、成交量和成交额的格式问题港股代码在不同渠道有不同写法常见的有00700.HK、0700.HK、700.HK三种。跨数据源做合并时代码格式不一致是常态必须先统一成同一种格式再join否则会出现大量匹配失败或错误匹配。成交量和成交额同样要留心。有的数据源成交量单位是股有的是手港股每手股数因股票而异腾讯一手是100股汇丰一手是400股不能直接用一个固定倍数来换算。成交额单位可能是港元也可能是千港元。CnOpenData表的字段说明一般会标注清楚但使用时不要默认所有厂商都一致。5.4 停牌与数据缺口是常态A股停牌相对少见港股长期停牌的公司并不少有的甚至停牌超过一年。停牌期间复权表里可能出现价格连续不变、成交量为零的记录也可能直接缺失这些日期。对策略回测来说停牌股票的处理直接影响结果。通常做法是停牌期间按0收益处理持仓市值用最后收盘价继续计算或者保守一点把停牌超过一定天数的股票强制移出样本。无论选择哪种都要在方法论里写清楚因为不同处理方式对回测净值的影响可能很大。复权因子在停牌期间通常不会变化因为除息除权和拆合股这类事件极少发生在停牌期间。如果发现某个长期停牌股票的因子在停牌期间出现变动就需要特别关注。5.5 港元计价与汇率换算最后一个坑也是最容易在A股研究者的流程里翻车的港股行情数据默认以港元计价。如果你的策略同时覆盖A股和港股而组合净值统一用人民币计算就需要把港币价格按交易日汇率换算成人民币。这里要特别注意有些数据平台会直接提供一个人民币计价的行情字段看起来方便但里面用的汇率可能是中间价、离岸价或月末汇率不同口径会带来0.5%到1%的收益偏差。更稳妥的做法是保留港元原始价格单独维护一条每日汇率序列在计算组合净值时统一换算。如果数据表里同时有港币计价的原始字段这个工作就会非常简单。说回文章开头那次回测翻车后来我把整个流程固定成了现在的样子原始价格和复权因子分开管理收益率全部统一在后复权口径下计算展示层面才切换到前复权每新换一个数据源先做一遍披露易核对加手工复算验证。数据这关过一遍后面写策略逻辑才敢放心往下走。如果你正准备用CnOpenData的港股复权行情数据这20分钟的验证工作真的很值。