这次我们复盘的对象不是某个开源项目,而是一笔投资决策的完整生命周期:在市场恐慌导致的暴跌阶段买入资产,持有并跟踪一年,再借助大模型做组合调仓。标题里的 "Year follow-up on buying pandemic stock dip and AI reallocation" 翻译过来就是“疫情暴跌抄底一周年复盘与 AI 调仓”。整个主题可以拆成三层来看:策略怎么定义、回测怎么验证、AI 怎么辅助调仓。
如果你自己会做投资组合管理,这篇文章能给你一套可执行的复盘框架;如果你是做量化回测的技术同学,文中的 Python 代码可以直接改造成自己的回测脚本;如果你想把大模型接进投研流程,第 6 节的 Prompt 设计和输出解析可以当模板用。需要提前说明的是,这篇文章不是投资建议,也不会给出“买入某资产一年后赚了多少”这种结论,因为买入方式、标的选择、仓位管理不同,结果差异会非常大。文章提供的是验证方法和分析工具,让你用自己的数据得出自己的结论。
先说我对这个主题的核心判断:暴跌买入这件事,难点从来不是“敢不敢买”,而是“怎么买、买多少、什么时候再平衡”。一年回头看,真正决定组合结果的往往是仓位管理,而不是最初的买入点。AI 在这里能做的事情同样有限:它不能预测市场,但可以把估值分位、宏观文本、组合风险这些信息压成结构化判断,减少人工分析时的遗漏。
1. 核心能力速览
先把整套复盘流程的关键信息列出来,方便你快速判断要不要继续往下看。
| 能力项 | 说明 |
|---|---|
| 策略主题 | 疫情冲击后的暴跌买入,一年周期持有与调仓复盘 |
| 分析工具 | Python + pandas + 行情数据接口 + 大模型 API |
| 主要能力 | 回撤计算、分批买入回测、绩效指标、AI 调仓建议生成 |
| 数据需求 | 指数或个股日线行情、估值分位数据、宏观新闻与舆情文本 |
| 硬件要求 | 普通笔记本即可,CPU 足够,不需要 GPU |
| 启动方式 | Jupyter Notebook 或 Python 脚本,无复杂部署 |
| API 能力 | 大模型分析环节使用 API;行情数据接口按具体数据源调整 |
| 批量任务 | 支持批量回测多只资产、批量生成调仓分析文本 |
| 高风险环节 | 数据口径、复权处理、Prompt 输出解析、AI 幻觉 |
| 适合读者 | 个人投资者、量化初学者、想接入大模型辅助投研的开发者 |
这套流程不需要 GPU,不需要 Docker,一个 Python 环境加一个合规的行情数据源就能跑起来。硬件基本没有门槛,真正的门槛在数据质量和策略规则是否足够清晰。
2. 为什么把观察周期定在一周年
一周年复盘的核心目的,是把一次买入决策放到足够长的时间窗口里去检验。疫情冲击带来的市场下跌不是一天完成的,修复过程也不是。如果把观察窗口定在三个月,你看到的可能只是恐慌情绪的延续或者一次技术性反弹,很容易得出错误结论。一年刚好覆盖了从恐慌、修复到情绪重新升温的过程,能比较完整地评价“买入决策”本身是否正确。
这个周期适合回答四个问题:
- 从买入点到一年后,资产价格是否修复到买入成本之上。
- 修复过程中的最大浮亏是多少,是否超出了心理承受范围。
- 分批买入和一次性买入的结果差异有多大。
- 持有到中间,如果做一次调仓,哪些调整被证明是有效的。
需要特别说明的是,本文不预设“当时买入就一定赚钱”的结论。市场存在长期不修复甚至继续下行的可能,也存在修复速度远超预期的路径。一周年复盘的价值,恰恰是把这些不同路径摊开来看,而不是用一个结果倒推一个原因。
3. 数据准备与观察口径
3.1 行情数据
回测至少需要日线级别的数据字段:日期、开盘价、最高价、最低价、收盘价、成交量。最关键的是复权处理。标的存在分红、送转的情况下,不复权数据会让历史价格不连续,直接导致收益计算失真。更稳妥的做法是使用前复权或后复权数据,并在分析日志里记录采用的是哪种口径。如果后续要切换到其他数据源,口径不一致会造成结论无法对比。
3.2 估值分位数据
暴跌不等于便宜。判断一次下跌是否值得介入,需要看估值处于历史什么位置。常见做法是计算当前市盈率或市净率在过去 5 到 10 年的百分位。比如,估值分位低于 30% 才进入候选区间。这个阈值必须可以配置,因为不同行业、不同市场适用的区间差异很大,用一套固定参数跑所有资产是回测里最常见的过度拟合来源之一。
3.3 文本数据
AI 调仓环节会用到公开市场信息,例如宏观数据发布、行业新闻、公司公告摘要。文本来源要注意版权和数据合规,只能使用你有权限访问的公开数据。任何抓取脚本都要遵守目标网站的访问条款,不能绕过访问限制。更合理的做法是直接使用有授权的新闻数据服务,把文本获取和解析的工作外包给数据服务商。
3.4 数据目录结构
建议把原始数据、中间结果和最终输出分开管理:
project/ ├── data/ │ ├── raw/ # 原始行情数据 │ ├── processed/ # 清洗后的数据 │ └── meta/ # 估值分位等辅助数据 ├── notebooks/ # Jupyter 分析脚本 ├── outputs/ │ ├── equity/ # 净值曲线 │ └── reports/ # 调仓报告 └── config.yaml # 策略参数数据版本管理是很多人忽略的点。一年以后回看时,你需要的不仅是当时的净值结果,还包括当时用的数据源、复权方式、参数阈值。没有这些留痕,复盘就只能停留在“赚了还是亏了”的层面。
4. 策略框架:先定义“抄底”,再谈收益
很多人把“抄底”理解成一次性的买入动作,但可回测的策略必须把动作拆成可执行的规则。规则越明确,一年后的复盘越容易归因。
4.1 触发条件
触发条件至少需要两个维度之一:
- 价格回撤:资产价格从阶段高点回撤超过阈值,例如 20%。
- 估值分位:当前估值进入历史低位区间,例如低于过去 5 年 30% 分位。
两个条件同时满足时,信号的可靠性通常更强,但同时满足的机会也会变少。这个取舍需要在回测里用不同参数对比,而不是拍脑袋决定。建议先跑一组网格参数,观察不同阈值下的交易次数、最大回撤和期末收益,再根据你自己的持仓偏好选择。
4.2 建仓方式
建议优先测试分批买入,而不是一次性买入。一次性买入对择时要求极高,在恐慌期很容易买在半山腰;分批买入等于用时间换确定性,但问题在于如果市场直接反弹,后面的批次会买在高位,拉低整体收益。这两种路径没有绝对优劣,只有适不适合当前的波动环境。回测的任务不是证明哪一种更好,而是告诉你在某一段历史行情里,两种方式各自付出了多少波动代价。
4.3 调仓规则
调仓要提前定义,不能凭感觉。常见规则有两种:
- 固定周期再平衡:例如每季度把组合拉回目标比例。
- 阈值再平衡:例如某类资产占比偏离目标超过 5 个百分点时触发调整。
对一年维度的复盘来说,固定周期规则更容易执行,也更便于归因。每次调仓都记录触发原因、操作比例和事后的结果,积累几次之后就能形成属于你自己的“调仓行为准则”。
4.4 两种抄底风格对比
| 维度 | 一次性重仓 | 分批定投式下跌加仓 |
|---|---|---|
| 择时要求 | 高 | 低 |
| 最大浮亏 | 可能很深 | 相对平滑 |
| 对资金的要求 | 前期完成建仓 | 需要保留后续弹药 |
| 心理压力 | 大 | 较小 |
| 主要风险 | 买在半山腰 | 市场快速反弹导致成本偏高 |
5. 回测与复盘流程
5.1 计算回撤序列
回撤是“抄底”策略最基础的输入。它表示当前价格距离历史高点的百分比。
import pandas as pd import numpy as np def calc_drawdown(close: pd.Series) -> pd.Series: """计算价格距历史高点的回撤序列,返回负值,例如 -0.20 表示回撤 20%。""" rolling_max = close.cummax() drawdown = close / rolling_max - 1.0 return drawdown这个函数可以直接打印出来观察:某段时间内是否出现过回撤超过阈值的情况,持续时间有多长,最大回撤发生在哪一天。这些信息是判断策略参数是否合理的起点。
5.2 分批买入回测示例
下面是一个教学用的简化回测函数。它把资金分成若干批,在回撤触发后、回撤继续扩大的日子里分批买入,并统计最终的持仓市值。需要注意,这是一个简化版本,没有考虑手续费、滑点、现金利息,也没有处理同一轮下跌中“是否应该只买一次”的问题。
def backtest_staged_buy( prices: pd.Series, cash: float, trigger: float = -0.20, stages: int = 4, ): """ 简化版分批下跌加仓回测。 参数说明: - prices: 日线收盘价序列,索引为日期 - cash: 初始资金 - trigger: 触发加仓的回撤阈值,默认 -20% - stages: 计划买入批次数 """ dd = calc_drawdown(prices) budget_per_stage = cash / stages position = 0.0 buy_dates = [] for i in range(len(prices)): # 回撤触发,且回撤仍在扩大,则买入一批 if dd.iloc[i] <= trigger and len(buy_dates) < stages: if i > 0 and dd.iloc[i] < dd.iloc[i - 1]: position += budget_per_stage / prices.iloc[i] buy_dates.append(prices.index[i]) # 期末市值:剩余现金 + 持仓市值 remaining_cash = cash - budget_per_stage * len(buy_dates) final_value = remaining_cash + position * prices.iloc[-1] return buy_dates, final_value真实回测需要在买入环节加入手续费、印花税和滑点假设,否则结果会偏乐观。回测的价值不是让你相信历史收益,而是理解策略在不同行情阶段的体验。
5.3 绩效指标
一年复盘至少要记录这几项指标:总收益率、最大回撤、年化波动率、修复时间、相对买入持有基准的超额收益。
def summary_metrics(close: pd.Series): dd = calc_drawdown(close) total_return = close.iloc[-1] / close.iloc[0] - 1.0 max_drawdown = dd.min() annual_vol = close.pct_change().std() * np.sqrt(252) return { "total_return": round(total_return, 4), "max_drawdown": round(max_drawdown, 4), "annual_vol": round(annual_vol, 4), }判断策略是否值得继续跟踪,不能只看总收益率,还要看最大回撤和修复时间。一个收益率高但回撤极深的策略,在真实持有过程中很可能拿不住,最终执行出来的结果和回测相距甚远。
5.4 与基准对比
每次复盘都要和“买入持有”基准做对比。基准可以是不加择时、从同一时间点一次性买入并持有的净值曲线。对比的目的是回答一个问题:额外的择时和分批操作,是否真的带来了超额收益,还是只是把波动变得好看了。如果两者的期末净值接近,但分批操作投入了大量精力,那这个策略的实际价值就要重新评估。
6. AI 调仓辅助
6.1 AI 在调仓流程中的边界
AI 辅助调仓的目标不是取代决策,而是把散落的信息聚合成结构化分析。它可以完成的事情包括:
- 归纳组合当前的持仓集中度和风险暴露。
- 解读宏观新闻和行业信息中与持仓相关的部分。
- 生成风险提示清单和调仓方向建议。
- 把历史复盘结果整理成一段可保存的说明。
它不应该直接决定买卖,也不能给出具体时点承诺。原因很直接:大模型会产生幻觉,特别是在生成具体数字的时候。AI 输出的每个关键数字都需要人工核对,这是把 AI 接入投研流程不可省略的步骤。
6.2 调仓分析 Prompt 模板
以下代码使用 OpenAI 兼容接口格式,实际使用时需要替换成自己的 API 地址、模型名和密钥。
import json import openai client = openai.OpenAI( api_key="your-api-key", # 替换为实际密钥 base_url="your-endpoint-url" # 按实际服务商地址调整 ) def generate_reallocation_suggestion( portfolio_summary: str, market_context: str, risk_constraints: str, ) -> dict: prompt = f""" 你是一个组合管理研究助手。请基于输入信息生成调仓建议草稿。 组合现状: {portfolio_summary} 近期市场与宏观信息: {market_context} 组合约束: {risk_constraints} 输出要求: 1. 先列出组合当前的结构性风险,不超过 5 条。 2. 给出调仓方向,只描述方向,不承诺具体收益。 3. 列出最重要的数据缺口和不确定因素。 4. 最后输出 JSON 结构: {{ "risks": ["风险1", "风险2"], "direction": "调仓方向描述", "unknowns": ["数据缺口1", "数据缺口2"] }} 注意:分析仅供研究参考,不构成投资建议。 """ response = client.chat.completions.create( model="your-model-name", # 按实际可用模型调整 messages=[{"role": "user", "content": prompt}], temperature=0.2, ) content = response.choices[0].message.content # 尝试解析 JSON 输出,失败时返回原始文本便于排查 try: return json.loads(content) except json.JSONDecodeError: return {"raw_output": content}温度参数设为 0.2 是为了让输出更稳定。如果模型频繁跑偏,可以进一步要求它“不要输出任何多余文字,只输出 JSON”,并在解析时做容错处理。
6.3 解析输出与保存记录
解析后的结果建议直接写入复盘记录文件,格式如下:
{ "date": "2021-03-15", "risks": ["科技板块占比超过 40%,行业集中度偏高"], "direction": "适当降低高波动资产占比,保留现金等待确定性机会", "unknowns": ["最新季报数据尚未覆盖", "宏观政策变化未纳入上下文"] }每次调仓分析都留存一份这样的记录,加上当时的行情数据快照和策略参数,一年后就可以回看 AI 的分析在哪些场景下是对的、哪些场景下是缺失的。这种“分析留痕”比单次调仓结果更有价值。
6.4 人工复核清单
每次生成调仓建议后,至少做三件事:
- 核对模型输出的数字与原始数据是否一致。
- 检查建议是否违反组合约束,例如最低仓位、单一行业上限。
- 把建议内容回填到组合里做一次“假设调仓”的净值模拟,再判断是否值得执行。
AI 输出在形式上可能非常专业,但它不掌握你的资金体量、现金流计划和真实风险承受能力。调仓决策必须保留人的最终判断权。
7. 一周年复盘中值得观察的现象
这一节从方法论角度总结复盘时值得关注的规律。不同标的、不同入场时间会得到不同结论,但有一些共性问题值得注意。
第一,暴跌买入的分化来源主要是仓位管理,而不是买入点。同样的触发条件,分批买入和一次性买入的净值路径差别很大。一年后回看,真正拉开差距的往往是后续批次是否留出了现金流,以及市场继续下跌时是否还有能力加仓。
第二,一次性买入的浮亏深度会直接影响持有纪律。回撤越深,中间放弃的概率越大。策略本身可能有效,但如果持有者因为承受不住波动而中途卖出,实际收益就会和回测结果差很多。复盘时要重点记录净值曲线上的“心理压力区间”,判断这个区间是否超出自己的能力范围。
第三,市场修复速度无法预测。有的资产走的是快速修复路径,有的需要很长时间磨底。一周年复盘时会出现“同一策略在不同资产上结果差异很大”的情况,这并不代表策略失效,而是标的基本面和修复路径不同。对这一点要有预期,不要因为某一个标的的结果不好就否定整个框架。
第四,AI 分析能降低信息遗漏,但不能消除判断误差。当多个模型给出的调仓方向不一致时,通常说明当前数据不足以支撑确定性结论,此时更稳妥的做法是降低调仓频率,而不是频繁操作。如果 AI 输出的内容与公开数据明显矛盾,优先检查输入文本是否完整、模型知识是否过时。
8. 常见问题与排查方法
排错时建议按“数据 -> 策略 -> AI 分析”的顺序来。先确认数据口径没问题,再检查策略规则是否符合预期,最后才怀疑模型输出。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 回测收益异常偏高 | 未做复权处理、忽略手续费和滑点 | 检查价格字段和成交假设 | 使用复权数据,加入交易成本 |
| 一年内没有产生买入信号 | 回撤阈值设置过深,或数据历史不够长 | 打印回撤序列分布并观察极值 | 调整触发阈值,延长数据窗口 |
| AI 输出无法解析成 JSON | 模型返回了额外文本或格式错误 | 打印原始返回内容 | 在 Prompt 中限制输出格式并做容错解析 |
| 调仓建议与组合约束冲突 | Prompt 中缺少约束条件 | 检查输入是否包含风险约束 | 在 Prompt 中强制加入约束清单 |
| 净值曲线和行情对不上 | 数据源字段口径不一致 | 抽样核对收盘价和成交量 | 统一数据源,记录数据版本 |
| AI 生成的具体数字有误 | 模型幻觉或上下文缺失 | 与源数据交叉验证 | 关键数字由代码直接计算,不让模型猜测 |
| 行情接口调用失败 | 网络限制、权限或接口变更 | 检查返回状态码和错误信息 | 切换备用数据源,配置重试和降级 |
9. 风险边界与合规提醒
这篇文章的性质