Python量化复盘:事件驱动低吸策略与AI再平衡实战 📅 发布时间:2026/8/28 16:09:09 👁 浏览次数: 在过去一年的投资复盘工作中我遇到一个比较典型的问题市场出现系统性下跌时按照“事件驱动型低吸”思路买入的资产到一年后到底赚没赚钱如果同期把仓位重新分配到 AI 相关方向收益结构又会有怎样的变化因为涉及的标的比较多、时间跨度长单靠手工整理 Excel 得出来的结论很难让人放心。于是我整理了一套基于 Python 的量化复盘流程把“何时买入、持有多久、何时再平衡、收益如何归因”这几件事用代码固化下来。本文就用这套思路完整拆解一次“疫情底买入 AI 再配置”的年度跟踪复盘所有代码都是可运行的适合做投资数据分析、策略研究或者想规范化自己复盘流程的开发者参考。1. 为什么要把一年买入与调仓决策“回测化”1.1 事件驱动型低吸策略是什么先解释一下标题里提到的“pandemic stock dip”。在疫情初期全球资本市场出现了罕见的系统性下跌。这类由外部突发事件引发的下跌往往速度快、跌幅大但同时也给长期投资者提供了比较低的买入价格。所谓“事件驱动型低吸策略”核心思路是在明确的极端下跌事件发生后不预测精确底部而是按纪律分批买入宽基指数或者核心资产组合再持有一段固定周期比如一年观察结果。这种策略和普通“抄底”的区别在于有明确触发条件不是凭感觉买入。有明确的买入规则比如等权重、分批建仓。有明确的持有周期比如 12 个月。有明确的复盘指标比如累计收益、最大回撤、超额收益。1.2 AI 再平衡是什么意思再平衡Rebalancing是组合管理里非常核心的动作。当我们持有一段时间之后最初设定的资产权重会因为不同资产涨幅不同而发生偏移。比如最初 AI 方向占 30%涨了一年可能变成 45%这时候如果希望维持风险暴露在目标范围就需要做一次“卖出涨多的、买入跌多的”或“主动向高景气方向增加权重”的调仓。标题里的 “AI reallocation” 是指在复盘时把原有组合中的一部分权重重新配置到 AI 相关资产上。这里的 AI 相关资产可以是 AI 主题 ETF、算力产业链、软件应用类公司也可以是自建的 AI 核心股票池。为了方便演示本文不会绑定具体股票而是使用结构化的示例数据来模拟。1.3 为什么要用代码复盘而不是拍脑袋手工复盘最大的问题是口径不统一。不同的人对“收益”的理解可能完全不同有人看的是账面浮盈有人算的是年化收益还有人把分红再投资也算进去。代码复盘的优势在于规则一致买入规则、持仓周期、再平衡规则一旦写进代码任何人都能复现。过程透明每一步都有日志和中间结果。便于迭代换一组参数或者换一个时间窗口重新跑一遍就有结论。便于和基准比较可以快速计算超额收益和最大回撤。2. 环境准备与数据口径2.1 实验环境本文示例使用 Python 3.9 以上版本核心依赖如下pandas1.5.0 numpy1.24.0 matplotlib3.6.0 seaborn0.12.0 openpyxl3.0.0建议使用虚拟环境安装python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install pandas numpy matplotlib seaborn openpyxl如果使用的是 Anaconda也可以直接用 conda 创建环境。2.2 数据来源与样例结构本文不依赖某个固定数据接口因为你实际使用的数据源可能完全不同。你可以从行情终端导出 CSV也可以使用 Tushare、AkShare 等接口获取日线行情或者直接手工整理一份样例数据。为了便于演示我把数据格式约定如下字段含义date交易日期格式为 YYYY-MM-DDsymbol资产代码本文使用示例代码close收盘价后复权或前复权均可pct_chg当日涨跌幅小数表示如果你使用 CSV 文件结构大致如下date,symbol,close,pct_chg 2023-01-03,CORE-AI,100.0,0.001 2023-01-03,TECH-NEXT,50.0,-0.002 2023-01-03,HEALTH-X,80.0,0.0032.3 数据清洗与口径约定在正式分析前必须统一数据口径是否复权。计算收益率时建议使用后复权价格因为后复权价格能真实反映持有资产的长期收益不会因为除权除息产生虚假回撤。日期对齐。各只资产的交易日期必须对齐到同一日历如果某天某只资产停牌对应的收益率可以按 0 处理或者用前值填充。空值处理。pct_chg出现空值需要区分是停牌还是数据缺失。停牌对收益率为 0数据缺失建议补全后再计算。下面给出一个简洁的清洗函数# 文件路径data_clean.py import pandas as pd def load_market_data(csv_path: str) - pd.DataFrame: df pd.read_csv(csv_path, parse_dates[date]) # 按日期和资产代码排序 df df.sort_values([date, symbol]).reset_index(dropTrue) # 删除重复的日期代码 df df.drop_duplicates(subset[date, symbol], keeplast) return df3. 核心概念从价格序列到收益指标3.1 复权价格与收益序列计算收益率时最常用的是简单收益率df[ret] df.groupby(symbol)[close].pct_change()pct_change()的本质是ret_t (close_t - close_{t-1}) / close_{t-1}这里要注意如果使用日线数据第一行的收益率为 NaN因为前一天的价格不存在。后面用fillna(0)处理即可。3.2 净值曲线与累计收益组合净值曲线的构造方法是把每日组合收益率累乘。如果组合在初始日净值为 1那么第 T 日的净值等于nav_t (1 r_1) * (1 r_2) * ... * (1 r_t)在 pandas 中对应nav (1 df[ret]).cumprod()为什么用累乘而不是累加因为收益率是复合的。第一年赚 10%、第二年亏 10%累乘得到的是 0.99不是 1.0。这才是真实的总收益。3.3 最大回撤与夏普比率最大回撤描述的是“从历史最高点跌到之后某个低点的最大亏损幅度”是衡量策略风险的重要指标。计算逻辑分三步计算净值序列的累计最大值。计算当前净值相对累计最大值的回撤。取回撤序列的最小值。cummax_nav nav.cummax() drawdown nav / cummax_nav - 1 max_drawdown drawdown.min()夏普比率衡量的是“每承担一单位波动能拿到多少超额收益”。简化版本如下sharpe (策略年化收益率 - 无风险利率) / 策略年化波动率年化波动率通常用日收益率标准差乘以根号 252 估算252 代表一年大约 252 个交易日。3.4 为什么再平衡比择时更可控很多人在复盘时只关心“买没买对”却忽略了“调没调对”。再平衡的价值在于强制纪律到了设定好的时间点不管市场情绪如何都执行调仓。风险控制避免单一方向权重过高导致组合波动失控。收益归因通过对比“再平衡前”和“再平衡后”的净值曲线能判断调仓动作究竟是加分还是减分。4. 实战疫情底买入策略的年度回测4.1 定义事件日与持有期假设我们把市场出现极端下跌的那一天记为事件日event_date。买入规则是事件日收盘时买入并按等权重分配给组合内的四类资产持有期为 12 个月约 252 个交易日。先构建模拟数据。为了让代码可运行我们生成两个示例资产的价格序列时间跨度为 1 年多# 文件路径backtest_demo.py import pandas as pd import numpy as np np.random.seed(42) dates pd.bdate_range(start2023-01-02, end2024-06-28) def gen_price_series(dates, start_price, drift, vol): rets np.random.normal(drift / 252, vol / np.sqrt(252), len(dates)) price start_price * np.cumprod(1 rets) return pd.Series(price, indexdates) df_list [] for symbol, start, drift, vol in [ (CORE-AI, 1.0, 0.15, 0.25), (HEALTH-X, 1.0, 0.08, 0.18), (TECH-NEXT, 1.0, 0.12, 0.22), (CONSUMER-A, 1.0, 0.05, 0.15), ]: s gen_price_series(dates, start, drift, vol) df_list.append(pd.DataFrame({date: s.index, symbol: symbol, close: s.values})) df pd.concat(df_list, ignore_indexTrue) df[ret] df.groupby(symbol)[close].pct_change() df[ret] df[ret].fillna(0)上述代码生成了四类资产一年的模拟行情。实际的疫情底买入复盘里只需把df替换成真实行情数据即可。4.2 构建组合净值曲线买入日event_date我们取 2023-01-03。等权买入后每天的组合收益率为四类资产收益率的平均值# 文件路径backtest_demo.py event_date pd.Timestamp(2023-01-03) start_date event_date end_date event_date pd.offsets.BDay(252) sub df[(df[date] start_date) (df[date] end_date)].copy() pivot sub.pivot(indexdate, columnssymbol, valuesret).fillna(0) # 等权组合 portfolio_ret pivot.mean(axis1) portfolio_nav (1 portfolio_ret).cumprod() print(portfolio_nav.head())这里有一个比较关键的细节pivot之后如果某天有一列数据缺失我们用fillna(0)填充。它的隐含假设是当日该资产停牌收益率为 0。如果某只资产长期停牌这种处理方式可能会低估波动实际项目中需要单独处理。4.3 对比基准与计算指标为了判断策略是否跑赢了大盘我们需要一个基准。基准可以使用宽基指数也可以用统一生成的示例指数# 文件路径backtest_demo.py benchmark_ret np.random.normal(0.06 / 252, 0.16 / np.sqrt(252), len(portfolio_ret)) benchmark_nav (1 benchmark_ret).cumprod() def calc_metrics(nav_series, rf0.02): nav nav_series.copy() total_return nav.iloc[-1] / nav.iloc[0] - 1 years len(nav) / 252 annual_return (1 total_return) ** (1 / years) - 1 daily_ret nav.pct_change().dropna() annual_vol daily_ret.std() * np.sqrt(252) sharpe (annual_return - rf) / annual_vol max_dd (nav / nav.cummax() - 1).min() return { 累计收益: total_return, 年化收益: annual_return, 年化波动: annual_vol, 夏普比率: sharpe, 最大回撤: max_dd, } metrics_df pd.DataFrame({ 策略: calc_metrics(portfolio_nav), 基准: calc_metrics(pd.Series(benchmark_nav, indexportfolio_nav.index)), }) print(metrics_df.round(4))输出结果大致长这样指标策略基准累计收益0.0876-0.0121年化收益0.0865-0.0122年化波动0.07650.1356夏普比率0.8692-0.2376最大回撤-0.0842-0.1541这里的数字是模拟数据结果只用于演示计算逻辑不代表任何真实投资建议。4.4 一年的结果说明了什么从回测指标来看事件驱动型低吸策略的优点不在于“单月暴涨”而在于持有期内回撤相对可控。组合层面的夏普比率高于单押某类资产的基准。通过分散持仓降低了因某个方向判断失误导致的大幅亏损。但同时也要看到一旦市场没有出现预期中的修复行情这类策略同样会面临长期横盘甚至阴跌。所以“事件驱动买入 长期持有”并不等于稳赚而是把胜率建立在“极端下跌之后均值回归概率较高”这个统计规律上。5. 实战向 AI 方向再平衡的归因分析5.1 板块分组与权重设计现在进入标题中的“AI reallocation”部分。假设在买入初期组合内有四类资产CORE-AIAI 核心方向TECH-NEXT泛科技方向HEALTH-X医疗健康方向CONSUMER-A消费方向初始等权配置即每个方向权重为 25%。持有半年后四类资产涨幅差异变大我们希望把组合权重调整为CORE-AI35%TECH-NEXT25%HEALTH-X20%CONSUMER-A20%这个调整对应的就是“超配 AI 核心、低配防御方向”。为什么要这样调原因可以是基本面逻辑也可以是量化信号例如过去一段时间 AI 方向的趋势强度优于其他方向。5.2 收益贡献与归因计算调仓前的收益贡献可以用如下公式近似贡献_i 权重_i * 区间收益率_i它衡量的是“如果什么都不动每个方向为组合整体收益贡献了多少百分点的净值增长”。代码如下# 文件路径rebalance_analysis.py weight_before pd.Series({ CORE-AI: 0.25, TECH-NEXT: 0.25, HEALTH-X: 0.25, CONSUMER-A: 0.25, }) # 计算从买入日到调仓日的区间收益 rebalance_date start_date pd.offsets.BDay(125) period_df df[(df[date] start_date) (df[date] rebalance_date)].copy() period_pivot period_df.pivot(indexdate, columnssymbol, valuesclose) return_before period_pivot.iloc[-1] / period_pivot.iloc[0] - 1 contribution_before weight_before * return_before print(调仓前区间收益) print(return_before.round(4)) print(调仓前贡献) print(contribution_before.round(4))这里需要注意的是区间收益用的是“期初收盘价到期末收盘价”没有考虑期间现金流。如果中间有分红、拆分就要用复权价计算。5.3 目标权重调整的实现再平衡的本质是一次“虚拟交易”。我们需要站在调仓日重新计算每类资产应该持有的市值然后求出调整方向。假设调仓前总市值为 100 万元调仓前各资产市值等于总市值乘以权重。调仓后的目标市值等于总市值乘以新权重。差额为正代表买入差额为负代表卖出。# 文件路径rebalance_analysis.py weight_after pd.Series({ CORE-AI: 0.35, TECH-NEXT: 0.25, HEALTH-X: 0.20, CONSUMER-A: 0.20, }) total_value 1_000_000 value_before total_value * weight_before target_value total_value * weight_after trade_amount target_value - value_before print(再平衡交易指令) print(trade_amount.round(2))如果某个资产的trade_amount为正意味着需要买入该资产如果为负意味着需要卖出该资产把资金腾挪到超配方向。5.4 调仓后的净值对比进行再平衡之后需要重新计算净值曲线。这一步可以简化成调仓前按旧权重计算组合收益调仓后按新权重计算组合收益。# 文件路径rebalance_analysis.py pivot_ret sub.pivot(indexdate, columnssymbol, valuesret).fillna(0) pivot_ret_before pivot_ret[pivot_ret.index rebalance_date] pivot_ret_after pivot_ret[pivot_ret.index rebalance_date] # 调仓前等权 nav_before (1 pivot_ret_before.mean(axis1)).cumprod() # 调仓后按新权重 def weighted_nav(ret_df, weights): weighted_ret ret_df[weights.index] weights.values return (1 weighted_ret).cumprod() nav_after weighted_nav(pivot_ret_after, weight_after) # 拼接净值 nav_full pd.concat([nav_before, nav_after])整个流程的核心思想是把一次再平衡拆成“段内净值 段间拼接”这样就能得到一条连续的调仓后净值曲线。把它和“完全不调仓”的组合净值画在一起就能直观看到 AI 再平衡是否提升了最终收益。6. 常见问题与数据陷阱6.1 常见报错与环境问题问题现象常见原因解决思路KeyError: CORE-AIsymbol 名称不一致比如大小写不同或包含空格使用df[symbol].unique()检查全量代码ValueError: cannot reindex on an axis with duplicate labelspivot 后存在重复日期检查原始数据里是否有重复的 datesymbolpct_change 产生大量 NaN复权价缺失或排序错误先按 date 和 symbol 排序再计算cumprod 结果异常偏大收益率小数与百分比混用统一以小数存储收益率例如 1% 写作 0.016.2 数据与分析陷阱第一复权口径不一致。前复权和后复权的最近价格不同但长期收益率趋势相同。如果从多个数据源混着拉数据容易让同一只资产在不同时间段的价格对不齐。建议统一使用后复权。第二幸存者偏差。如果我们用“当前还存活的 AI 概念股清单”去做历史回测那些已经退市或跌幅巨大的标的不会出现在清单里最终收益会被高估。更稳妥的做法是使用指数数据或者包含已退市样本的完整数据集。第三极值行情影响。模型生成的数据里可能没有极端行情但真实市场里会出现连续跌停、停牌、熔断等情况。此时pct_change和cumprod的假设会被打破需要单独处理异常交易状态。第四参数过拟合。如果你反复尝试不同的买入日“直到结果满意”那么回测结果就已经失真了。正确做法是把买入规则、持有周期、再平衡规则提前固定再用历史数据验证。6.3 策略层面的误区不要把“事件驱动低吸”理解成越跌越买。绝大多数下跌都会有基本面恶化的因素单纯因为价格便宜而买入可能陷入估值陷阱。更合理的方式是在确定性较强的系统性危机事件发生后分批买入宽基指数或一篮子核心资产并严格控制单次买入仓位。7. 最佳实践与工程化建议7.1 把回测做成可复用的函数库在实际项目中不建议每次复盘都复制粘贴代码。更好的做法是把核心逻辑封装成模块比如metrics.py、backtest.py、rebalance.py对外提供统一接口。# 文件路径metrics.py from typing import Dict import pandas as pd def compute_nav(rets: pd.Series) - pd.Series: return (1 rets).cumprod() def evaluate_nav(nav: pd.Series, rf: float 0.02) - Dict[str, float]: total_return nav.iloc[-1] / nav.iloc[0] - 1 years len(nav) / 252 annual_return (1 total_return) ** (1 / years) - 1 daily_ret nav.pct_change().dropna() annual_vol daily_ret.std() * np.sqrt(252) sharpe (annual_return - rf) / annual_vol max_dd (nav / nav.cummax() - 1).min() return { 累计收益: total_return, 年化收益: annual_return, 年化波动: annual_vol, 夏普比率: sharpe, 最大回撤: max_dd, }封装完成后每次复盘只需要读取行情数据调用对应函数即可。7.2 数据快照与结果归档做年度复盘时原始数据、中间计算脚本、最终指标结果要分开保存。行情数据会定期更新如果不做快照第二年再跑可能因为数据源变化导致结论无法复现。推荐结构project/ ├── data/ │ └── snapshot_2024_06.csv ├── scripts/ │ ├── backtest.py │ └── rebalance.py └── reports/ └── 2024_annual_review.md7.3 风险控制与压力测试回测结果好看并不代表未来能赚钱。工程层面至少要做两类压力测试情景压力测试假设持有期内再次下跌 20% 或 30%组合最大回撤是否在承受范围内。参数敏感性测试把持有周期改成 12 个月、18 个月、24 个月观察结论是否依然成立。如果参数稍微变化策略就从盈利变成亏损那么说明策略的稳定性不足应该降低仓位或者放弃。7.4 让 AI 辅助复盘报告AI 技术发展很快日常投资复盘里完全可以用大模型辅助生成结构化报告。比如把回测指标、调仓记录、行情摘要整理成文本再让大模型生成一份月度复盘草稿最后由人工确认和补充。这样做的好处是节省整理时间坏处是大模型可能“一本正经地胡说八道”所以任何结论都必须以实际数据为准。在代码层面可以使用简单的 Prompt 模板拼接指标数据# 文件路径report_helper.py summary_text f 策略累计收益{metrics[策略][累计收益]:.2%} 基准累计收益{metrics[基准][累计收益]:.2%} 最大回撤{metrics[策略][最大回撤]:.2%} 调仓后 CORE-AI 权重{weight_after[CORE-AI]:.0%} 然后再交给 LLM 处理成更完整的复盘文案。8. 小结这一年回看下来的结论其实并不复杂事件驱动型低吸策略能不能赚钱不取决于“买入当天是不是最低点”而取决于买入之后是否严格执行了持有纪律和再平衡计划。本文从数据处理、净值计算、指标输出、调仓归因四个环节完整演示了一套用 Python 做年度投资复盘的方法。里面用到的累计收益、最大回撤、夏普比率、收益贡献、再平衡交易量等指标都是投资分析里的常用工具也同样适用于其他长期策略的复盘。如果你刚好也在做类似的年度复盘建议先别追求复杂模型把手里的行情数据清洗干净用等权组合跑一遍净值曲线再逐步加入 AI 再平衡逻辑这样更容易定位问题出在数据、策略还是执行层面。代码和数据都可以做成快照保存下来来年复盘时直接复用可比手工整理表格省心不少。