尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python量化分析:手把手教你计算股票前复权与后复权价格

Python量化分析:手把手教你计算股票前复权与后复权价格 1. 项目概述为什么我们需要自己动手算复权价做量化分析或者股票数据回测的朋友肯定都遇到过“复权”这个概念。无论是从同花顺、东方财富这类软件导出数据还是用Tushare、AkShare等开源接口获取历史行情你都会面临一个选择前复权还是后复权很多新手会直接使用数据源提供的复权价格觉得这是“标准答案”。但如果你真的想构建一个严谨、可回溯、逻辑自洽的回测系统我强烈建议你掌握自己计算复权价的能力。为什么因为不同数据源对复权因子的计算逻辑、对分红送股事件的处理细节可能存在差异。直接使用“黑盒”数据一旦回测结果出现偏差你很难定位问题是出在策略本身还是数据源头。自己计算意味着你对数据的每一个调整都了如指掌回测的置信度会大大提高。这次我们就用Python数据分析的利器——pandas从最原始的除权除息数据出发手把手推导出前复权价和后复权价。这不是一个简单的函数调用而是一次对金融数据处理核心逻辑的彻底解构。2. 核心概念辨析前复权与后复权到底在做什么在动手写代码之前我们必须把概念掰扯清楚。很多人知道复权是为了消除分红、送股、配股等事件对股价造成的断裂使历史价格序列具有连续可比性。但“前”和“后”的区别才是关键。2.1 后复权站在过去看现在后复权的核心思想是保持上市第一天的价格不变将之后所有的分红、送股等事件都“权息”叠加到最新的价格上。计算方向从历史到未来从前往后进行复权。价格特征历史价格低最新价格高。它反映了如果从上市第一天持有至今且将所有分红进行再投资股价理论上会达到的水平。核心用途观察长期绝对涨幅。后复权价能最真实地反映一只股票自上市以来的累计收益率。你看那些“十年十倍股”的K线图通常用的就是后复权这样才能看到股价从几块钱涨到几百块的壮观景象。2.2 前复权站在现在看过去前复权的核心思想是保持最新交易日或某个基准日的价格不变将历史上所有的分红、送股等事件都“权息”折算到过去的价格上。计算方向从未来到历史从后往前进行复权。价格特征历史价格被调整最新价格与当前实际价格一致。它让历史上的每一个价格点都相当于以“今天的资本结构”来看待。核心用途技术分析。这是最常用的复权方式。因为K线形态、均线系统、技术指标如MACD、KDJ的计算都需要连续的价格序列。前复权保证了最近的价格是真实的同时历史价格也经过了平滑处理使得均线等指标具有实际分析意义。你在交易软件上默认看到的K线基本都是前复权。注意这里说的“最新价格一致”通常指与“不复权”的最新价一致。实际上前复权的最新价就是除权除息日经过调整后的开盘参考价。2.3 不复权价最原始的面貌不复权价就是交易所公布的原始成交价格。在发生除权除息时股价会有一个向下的跳空缺口。这个缺口使得价格序列不连续无法直接用于计算涨跌幅或技术指标。为了更直观地理解我们假设一只股票发生了一次送股事件日期事件不复权收盘价前复权收盘价后复权收盘价说明2023-05-18股权登记日20.00(待计算)(待计算)送股前最后一个交易日2023-05-19除权日 (10送5)13.3320.0020.00理论除权价20 / (10.5) ≈ 13.332023-05-18(回溯)20.0020.0020.00以这一天为基准点从上表可以直观看到不复权价在5月19日从20元“断裂”到13.33元。前复权价保持5月19日的13.33元不变将5月18日的价格“还原”为20元这样看历史K线是连续的。后复权价保持5月18日的20元不变将5月19日的价格“推导”为20元这样看现在股价仿佛没变但实际上股本已扩大。3. 数据准备获取与理解核心字段自己计算复权价起点不是复权价格而是除权除息事件表。这是整个计算的基石。通常你可以从数据商、开源接口或财经网站获取到类似下面的DataFrame我们命名为df_events。import pandas as pd import numpy as np # 示例除权除息事件数据 (虚构用于演示) data { trade_date: [2023-05-19, 2023-08-17, 2023-11-23, 2024-03-15], cash_div: [0.5, 0.0, 0.3, 0.0], # 每股现金分红元 share_split_ratio: [0.5, 0.0, 0.0, 0.2], # 送转股比例如10送5则为0.5 allotment_ratio: [0.0, 0.1, 0.0, 0.0], # 配股比例 allotment_price: [0.0, 10.0, 0.0, 0.0], # 配股价元 } df_events pd.DataFrame(data) df_events[trade_date] pd.to_datetime(df_events[trade_date]) df_events df_events.set_index(trade_date).sort_index() print(df_events)输出可能类似于cash_div share_split_ratio allotment_ratio allotment_price trade_date 2023-05-19 0.5 0.5 0.0 0.0 2023-08-17 0.0 0.0 0.1 10.0 2023-11-23 0.3 0.0 0.0 0.0 2024-03-15 0.0 0.2 0.0 0.0同时你需要一份不复权的日线行情数据我们命名为df_daily至少包含close收盘价字段。# 示例不复权日线行情数据 (虚构用于演示) dates pd.date_range(start2023-01-01, end2024-04-01, freqB) # 交易日历 np.random.seed(42) # 生成一个模拟的上涨趋势价格 base_price 20.0 trend np.linspace(0, 0.5, len(dates)) noise np.random.randn(len(dates)) * 0.5 prices base_price * (1 trend noise) df_daily pd.DataFrame({close: prices}, indexdates) print(df_daily.head())关键字段解读与注意事项现金分红 (cash_div)直接作用于股价。除息日股价会向下调整该数值。送转股比例 (share_split_ratio)比如“10送5”或“10转增5”比例就是0.5。它导致总股本增加股价按比例下调。配股比例与价格 (allotment_ratio,allotment_price)这是最易出错的部分。配股是向老股东按比例以低于市价的价格发行新股。它不仅改变股本还涉及股东追加资金因此复权因子的计算比送股更复杂。事件日期 (trade_date)必须是除权除息日即股权登记日的下一个交易日。所有调整都从这个交易日开始生效。实操心得数据质量决定一切。务必仔细核对你的df_events。常见的坑有把预案公告日当成除权日、漏掉了小额分红、送转股比例填写错误如10送5填成5。建议用多源数据交叉验证或选取几只知名股票用你的计算结果与主流软件如同花顺进行比对这是验证逻辑正确性的最直接方法。4. 核心计算复权因子的推导与计算理解了概念和数据现在进入最核心的部分——计算复权因子。复权因子是一个累积乘数价格乘以它就能得到复权价。它是连接不复权价与复权价的桥梁。4.1 计算每日复权因子复权因子的计算是逐事件进行的。对于每一个除权除息事件我们计算一个当日调整因子然后将其乘到历史的累积因子上。单次事件调整因子的计算公式如下调整因子 (前日收盘价 - 每股现金分红 配股比例 * 配股价) / (前日收盘价 * (1 送转股比例 配股比例))公式拆解与逻辑分子代表了除权除息后股东权益的理论总值。前日收盘价股权登记日的市值基准。- 每股现金分红除息现金被分走市值减少。 配股比例 * 配股价配股时股东需要额外出资这部分资金增加了公司的净资产因此要加回来。分母代表了除权除息后公司总股本扩张后的调整。前日收盘价 * (1 送转股比例 配股比例)送股和配股都增加了总股本。假设原股本为1送转股增加share_split_ratio配股增加allotment_ratio总股本变为1 share_split_ratio allotment_ratio。股价需要按比例摊薄。如果只有送股和分红无配股公式简化为调整因子 (前日收盘价 - 每股现金分红) / (前日收盘价 * (1 送转股比例))这更常见。在pandas中的实现步骤将事件表df_events与日线表df_daily通过日期对齐。为df_daily创建一个名为adj_factor的列初始值全部为1.0。按时间顺序遍历每一个除权除息事件。对于每个事件找到除权除息日ex_date在df_daily中的位置。获取除权日前一天的收盘价prev_close。根据上述公式计算本次事件的single_adj_factor。将df_daily中所有在ex_date之前不含ex_date的日期的adj_factor都乘以这个single_adj_factor。重要提示这里有一个关键选择调整因子是乘在除权日之前的价格上前复权逻辑还是乘在除权日及之后的价格上后复权逻辑我们目前计算的是后复权因子的累积过程。因为后复权是保持最早价格不变后续价格向上调整所以调整因子作用于历史数据除权日之前。理解了这一点前后复权的计算就清晰了。下面是计算后复权因子的代码示例def calculate_backward_adj_factor(df_daily, df_events): 计算后复权因子。 逻辑从前往后累积调整因子作用于除权除息日之前的所有历史价格。 # 复制一份日线数据避免修改原数据 df df_daily.copy() # 初始化后复权因子为1.0 df[backward_adj_factor] 1.0 # 按日期顺序处理每个事件 for ex_date, event in df_events.iterrows(): # 1. 找到除权除息日在日线数据中的位置 if ex_date not in df.index: print(f警告除权除息日 {ex_date} 不在日线数据中已跳过。) continue # 2. 获取除权日前一天的收盘价 # 需要找到ex_date之前最近的交易日 prev_dates df.index[df.index ex_date] if len(prev_dates) 0: print(f警告除权除息日 {ex_date} 之前无交易日数据无法计算调整因子已跳过。) continue prev_date prev_dates[-1] # 取最后一个即最近的前一个交易日 prev_close df.at[prev_date, close] # 3. 提取事件参数 cash_div event[cash_div] share_split event[share_split_ratio] allot_ratio event[allotment_ratio] allot_price event[allotment_price] # 4. 计算本次事件的单次调整因子 # 防止除零错误 if prev_close 0: single_adj_factor 1.0 print(f警告除权除息日 {ex_date} 的前收盘价为0调整因子设为1.0) else: # 使用完整公式 numerator prev_close - cash_div allot_ratio * allot_price denominator prev_close * (1 share_split allot_ratio) single_adj_factor numerator / denominator # 5. 将本次调整因子乘到所有历史日期ex_date之前的后复权因子上 df.loc[df.index ex_date, backward_adj_factor] * single_adj_factor return df # 调用函数计算 df_with_factors calculate_backward_adj_factor(df_daily, df_events) print(df_with_factors[[close, backward_adj_factor]].tail(10))4.2 从后复权因子到前复权因子有了后复权因子计算前复权因子就非常简单了。它们互为倒数关系但需要以某个基准点进行归一化。核心关系后复权价格 不复权价格 * 后复权因子 前复权价格 不复权价格 * 前复权因子并且对于任意一天其前复权价格与后复权价格之比等于最新交易日的不复权价格与后复权价格之比。推导后可以得到前复权因子 后复权因子 / 最新交易日的后复权因子这样做的目的是将最新交易日的前复权价格与不复权价格对齐。def calculate_forward_adj_factor(df_with_backward_factor): 计算前复权因子。 逻辑以后复权因子为基础除以最新日的后复权因子进行归一化。 df df_with_backward_factor.copy() # 获取最新交易日的后复权因子 latest_backward_factor df[backward_adj_factor].iloc[-1] # 计算前复权因子 df[forward_adj_factor] df[backward_adj_factor] / latest_backward_factor return df df_with_factors calculate_forward_adj_factor(df_with_factors) print(df_with_factors[[close, backward_adj_factor, forward_adj_factor]].tail(10))5. 生成复权价格序列与验证计算完因子生成价格序列就是简单的乘法。# 生成复权价格序列 df_with_factors[close_backward] df_with_factors[close] * df_with_factors[backward_adj_factor] df_with_factors[close_forward] df_with_factors[close] * df_with_factors[forward_adj_factor] # 选取关键日期查看效果 key_dates pd.to_datetime([2023-05-17, 2023-05-18, 2023-05-19, 2024-03-14, 2024-03-15]) mask df_with_factors.index.isin(key_dates) print(df_with_factors.loc[mask, [close, close_forward, close_backward]])验证逻辑正确性的几个检查点最新价格对齐检查最新交易日例如2024-03-15的close_forward是否非常接近或等于取决于计算精度close不复权价。因为前复权就是以最新价为准进行回溯的。历史价格连续性选取除权除息日如2023-05-19及其前一天2023-05-18观察close_forward。理论上前复权价在这两天之间应该是连续的没有跳空缺口。你可以计算一下这两天的收益率看看是否与用不复权价、并考虑分红送股后计算的理论收益率一致。后复权起点检查最早交易日或上市首日的close_backward是否等于close。因为后复权保持起点价格不变。长期一致性任意两日之间的收益率用前复权价计算、用后复权价计算结果应该是一致的。因为复权只是价格的线性变换不影响收益率序列。常见问题排查问题计算出的前复权价在除权日仍有微小跳空。排查大概率是prev_close取错了。确保你用的是股权登记日即除权除息日前最后一个交易日的收盘价而不是日历上的前一天。你的df_daily索引必须是交易日且没有缺失。问题后复权价格趋势异常比如最新价反而比历史价低。排查检查调整因子single_adj_factor的计算公式尤其是配股部分的处理。确认分子分母没有弄反。对于配股公式(prev_close allot_ratio * allot_price) / (prev_close * (1 allot_ratio))是一个常见变体其逻辑是将配股视为一次“加权融资”不同数据源可能有细微差异需要与你选定的基准数据源保持一致。问题与同花顺等软件数据对不上。排查首先确认事件数据是否完全一致日期、比例。其次软件可能对红利税、股本变动生效时点是收盘后还是开盘瞬间有不同处理。对于极高精度的研究这些细节需要考虑。但对于大多数回测上述逻辑已足够稳健。6. 性能优化与工程化实践上面的循环遍历方法在事件不多时没问题但面对全市场几千只股票、几十年历史数据时效率堪忧。我们需要向量化操作。思路我们可以为每个除权除息日计算一个“累积调整因子”然后通过pandas的merge_asof或fillna方法为每一个交易日找到其之后第一个除权除息日的累积因子从而一次性完成计算。def calculate_adj_factor_vectorized(df_daily, df_events): 向量化方法计算复权因子以后复权为例。 # 1. 计算每个事件的单次调整因子需要前收 # 将事件表与日线表合并获取每个事件日的前收 df_events_with_prev df_events.copy() # 使用 merge_asof 为每个事件找到前一个交易日的收盘价 # 需要先将索引重置以便操作 df_daily_reset df_daily[[close]].reset_index() df_events_reset df_events.reset_index() df_merged pd.merge_asof(df_events_reset.sort_values(trade_date), df_daily_reset.sort_values(trade_date), left_ontrade_date, right_ontrade_date, directionbackward) # 计算单次调整因子 prev_close df_merged[close] cash_div df_merged[cash_div] share_split df_merged[share_split_ratio] allot_ratio df_merged[allotment_ratio] allot_price df_merged[allotment_price] numerator prev_close - cash_div allot_ratio * allot_price denominator prev_close * (1 share_split allot_ratio) single_factor numerator / denominator df_merged[single_adj_factor] single_factor df_merged.set_index(trade_date, inplaceTrue) # 2. 计算累积调整因子从过去到现在 # 按时间倒序因为后复权因子是历史累积 df_merged df_merged.sort_index(ascendingFalse) df_merged[cum_adj_factor] df_merged[single_adj_factor].cumprod() df_merged df_merged.sort_index(ascendingTrue) # 3. 为每个交易日匹配其“未来”第一个事件的累积因子 # 创建一个包含所有交易日和事件日的完整日期序列 all_dates df_daily.index.union(df_merged.index).sort_values() # 构建一个Series在事件日存放其累积因子 event_factors pd.Series(indexdf_merged.index, datadf_merged[cum_adj_factor]) # 前向填充每个交易日都使用其之后最近一个事件日的累积因子 # 对于最早事件日之前的日期因子为1用bfill后的结果再处理 aligned_factors event_factors.reindex(all_dates).fillna(methodbfill) # 将没有更早事件的日期即bfill后仍为NaN的通常是最后一段日期因子设为1 aligned_factors.fillna(1.0, inplaceTrue) # 4. 将因子对齐到原始日线索引 backward_adj_factor aligned_factors.reindex(df_daily.index).fillna(1.0) df_daily[backward_adj_factor_vec] backward_adj_factor return df_daily # 测试向量化版本 df_daily_vec calculate_adj_factor_vectorized(df_daily, df_events) print(df_daily_vec[[close, backward_adj_factor_vec]].tail())向量化操作能提升数十倍甚至上百倍的性能。在实际工程中你还需要考虑数据存储计算好的复权因子可以持久化到数据库或文件中避免每次重复计算。批量处理对全市场股票进行循环计算利用多进程或分布式框架加速。异常处理增加对数据缺失、除零错误、日期错位等情况的鲁棒性处理。基准对比定期用你的计算结果与权威数据源进行抽样对比确保长期计算的准确性。7. 在量化回测中的应用与注意事项自己计算复权价最终目的是为了更可靠的回测。在应用时有以下几个关键点价格选择在回测中买卖信号基于前复权价因为这与交易软件看到的K线一致。但计算收益时特别是涉及分红再投资的收益需要理解其与后复权收益的关系。收益率计算使用前复权价计算的日收益率序列与使用后复权价计算的序列是完全一致的。这是复权因子线性性质决定的。所以回测中直接用前复权价计算收益率即可。初始资金与复权如果你的回测考虑分红再投资那么后复权价模拟的就是“分红立即以收盘价再投资”的净值曲线。这时用后复权价计算策略净值会更方便。事件日处理在除权除息日你的df_events表中的事件已经发生。在回测中如果你在股权登记日持有股票你将有权获得分红或配股。这部分现金流入或股份增加需要在你的回测引擎的资金和持仓模块中精确模拟而不仅仅依赖价格复权。价格复权解决了K线连续性问题但真实的现金流和股份变动需要单独处理。我的个人体会自己实现一遍复权计算是量化入门极好的一课。它强迫你去理解每一个市场规则细节。最初我直接使用数据接口的复权数据直到有一次回测结果与论文差异巨大排查一周才发现是数据源对某次特殊配股的处理方式不同。从那以后核心数据的生成我都尽量掌握在自己手中。这份代码可能不是最快的但逻辑的透明度给了我最大的安全感。当你看到自己计算出的复权价格曲线与主流软件完美贴合时那种对数据的掌控感是直接调用API无法比拟的。
返回列表