用Python和pandas实现市场反弹右侧的周度策略回测
市场反弹右侧这个说法,在周度策略分析里经常出现。很多人看盘时能一眼认出“这里好像反弹了”,但落到判断依据上,往往只能说“感觉跌不动了”“放量了”这类主观描述。真正的问题是:同样一段行情,不同人用不同口径判断,结论很难复现,更谈不上事后检验。本文结合 Python 和 pandas,把“市场反弹右侧”拆成趋势、突破、量能三个可计算维度,做成一个每周五收盘后自动确认、按周持有的回测流程。学完后,你可以在自己的行情数据上跑通这套周度策略研究链路,并知道信号不出现、回测太漂亮、指标对不上时该从哪里排查。本文只讨论策略研究方法与程序实现,不构成投资建议,也不针对任何具体标的作推荐。
1. 右侧反弹和周策略的底层逻辑
1.1 左侧与右侧:被动等待和主动确认
通俗地说,左侧交易是在下跌过程中“抄底”,价格还没企稳就提前买入;右侧交易是等价格从下跌转向上涨、出现明确信号后再参与。两者没有绝对优劣,但“右侧”天然更适合周度策略,因为一周才检查一次信号,等趋势确认后再行动,可以过滤掉大量盘中噪音。
技术定义上,右侧反弹不预测最低点,而是把买入条件交给一组可观测规则,比如“收盘价站上 20 日均线”“突破近期高点”“成交量同步放大”。这些规则共同构成一个确认机制,只有当价格用事实表明反弹开始后,策略才给出持有信号。
需要特别澄清一个误区:右侧交易不等于一定会涨。它只是把“判断最低点”这个困难问题,换成“判断信号是否成立”这个相对可标准化的问题。信号确认之后行情仍然可能逆转,所以回测和风险控制同样重要。
1.2 周策略报告为什么需要可复现指标
很多周度策略报告的问题是口径不确定。比如上周说“市场有反弹迹象”,下周说“反弹力度不足”,但反弹到底看什么指标?多少日均线?放量放多少倍?完全没有可复现的标准。
可复现指标要求每个判断都能落到具体数值上:
- 使用哪条均线,收盘价必须站上它。
- 近期高点窗口是 20 个交易日还是 60 个交易日。
- 成交量放大到什么倍数才算有效。
- 多个条件之间是“全部满足”还是“满足其中若干条”。
一旦规则确定,任何人都能在同一份数据上得到相同结果。这也是程序化方法对主观看盘最大的价值。
1.3 本文的流程框架
整套流程可以分解成四步:
- 准备日线行情数据,包含日期、开盘、最高、最低、收盘、成交量。
- 计算技术指标,把价格形态转换成数值特征。
- 合成“右侧确认信号”,每周五收盘后判断当周是否满足。
- 做周度回测,统计持有区间收益、最大回撤和胜率。
文章中的代码全部使用 Python 编写,依赖只涉及 pandas、numpy 和 matplotlib。全程通过模拟数据演示,这样没有行情数据接口也能先跑通流程,之后替换成自己准备好的 CSV 文件即可。
2. 环境准备与数据基础
2.1 Python 环境与依赖
建议使用 Python 3.9 及以上版本。核心依赖如下:
| 库 | 用途 | 安装命令 |
|---|---|---|
| pandas | 数据处理、滚动计算、周度聚合 | pip install pandas |
| numpy | 随机游走模拟、数值运算 | pip install numpy |
| matplotlib | 绘制净值曲线和回撤图 | pip install matplotlib |
| pyyaml | 后续扩展配置文件时使用 | pip install pyyaml |
学习环境中不需要安装数据库和消息队列。如果只想看信号和回测结果,pandas 加 numpy 就够用。
2.2 行情数据格式建议
无论数据来自本地 CSV、量化接口还是行情软件导出,建议统一整理成以下结构:
| 字段名 | 类型 | 说明 |
|---|---|---|
| date | datetime | 交易日 |
| open | float | 开盘价 |
| high | float | 最高价 |
| low | float | 最低价 |
| close | float | 收盘价 |
| volume | int | 成交量 |
这里有两个容易出错的地方。
第一,复权方式要统一。长期回测必须使用前复权或后复权数据,否则分红除权会造成收盘价跳空,移动平均线也会失真。建议在数据下载阶段就固定复权口径,不要在计算指标中途切换。
第二,缺失日要处理。停牌日通常没有行情记录,不要简单地用 0 填充。常见做法是保留原始交易日历,只在参与计算时过滤缺失值。
2.3 用模拟数据让流程先跑通
真实行情接口在不同网络环境下可用性差异很大,而本文重点是跑通流程,所以先用随机游走生成一段模拟K线。这样做的优点是:结果可复现、不受数据源限制、便于验证信号逻辑是否正确。
import numpy as np import pandas as pd rng = np.random.default_rng(42) n = 300 # 从 100 开始生成对数随机游走 price = 100 * np.exp(np.cumsum(rng.normal(0.0005, 0.02, n))) df = pd.DataFrame({ "date": pd.date_range("2025-01-02", periods=n, freq="B"), "close": price, }) # 用前一日收盘价作为开盘价,并构造最高最低 df["open"] = df["close"].shift(1).fillna(price[0]) df["high"] = df[["open", "close"]].max(axis=1) * 1.01 df["low"] = df[["open", "close"]].min(axis=1) * 0.99 df["volume"] = rng.integers(100_000, 500_000, size=n) print(df.head()) print(df.tail())这段代码只是生成一份结构完整的模拟行情,用来演示指标计算和回测流程。把这段数据替换成自己的 CSV 时,读取方式也很简单:
df = pd.read_csv("daily_data.csv", parse_dates=["date"])检查点:数据应包含 300 行左右,日期按交易日递增,close 大致在 100 附近波动。如果发现 close 出现极端异常值,先检查生成逻辑或原始数据源,不要继续计算指标。
3. 右侧确认信号的设计与实现
3.1 把反弹右侧拆成三个可计算维度
“反弹右侧”是一个复合概念,不能只凭单一条件判断。本文把它拆成三个维度:
| 维度 | 含义 | 常用规则 |
|---|---|---|
| 趋势恢复 | 价格重新站上中期均线 | 收盘价 > MA20 且 > MA60 |
| 突破确认 | 价格接近或创出近期新高 | 收盘价 >= 近 20 日最高价 x 0.98 |
| 量能配合 | 反弹有成交量支撑 | 成交量 MA5 > 成交量 MA20 x 1.2 |
这样拆分有三个好处:
- 趋势恢复过滤掉“跌不动但也没涨”的横盘行情。
- 突破确认过滤掉均线下的弱反弹。
- 量能配合过滤掉无量上涨的空转反弹。
三个条件放在一起,比单看一根 K 线或者一条均线更稳健。
3.2 用 pandas 计算技术指标
把上述规则翻译成 pandas 代码。这里使用滚动窗口计算均线和量能指标。
# 均线 df["ma20"] = df["close"].rolling(20).mean() df["ma60"] = df["close"].rolling(60).mean() # 量能均线 df["vol_ma5"] = df["volume"].rolling(5).mean() df["vol_ma20"] = df["volume"].rolling(20).mean() # 近 20 日最高价 df["high_20"] = df["high"].rolling(20).max() # 条件判断 df["above_ma20"] = df["close"] > df["ma20"] df["above_ma60"] = df["close"] > df["ma60"] df["volume_expand"] = df["vol_ma5"] > df["vol_ma20"] * 1.2 df["near_high"] = df["close"] >= df["high_20"] * 0.98 print(df[["date", "close", "ma20", "ma60", "high_20"]].tail())关键点说明:
rolling(20).mean()计算的是过去 20 个交易日收盘价均值,前 19 行结果是 NaN。high_20是过去 20 个交易日的最高价,用它来判断本次反弹是否突破了前期压力位。- 接近新高的阈值设为 0.98,意味着不需要严格创新高,允许小幅回踩。
3.3 生成右侧确认状态
四个布尔条件可以合并成一个最终信号。为保证方向一致,建议全部使用“与”关系,避免某一项条件过弱导致信号泛滥。
df["signal"] = ( df["above_ma20"] & df["above_ma60"] & df["volume_expand"] & df["near_high"] ).astype(int) # 只保留指标计算完成后的数据 model_df = df.dropna(subset=["ma60", "vol_ma20"]).reset_index(drop=True) print(model_df["signal"].value_counts())信号为 1 表示当日满足右侧反弹全部条件,为 0 表示不满足。这里参数的物理含义是:
- 20 日均线代表短期趋势。
- 60 日均线代表中期趋势。
- 量能放大 1.2 倍表示市场参与度提升。
- 距离 20 日高点 2% 以内表示突破在即。
如果发现信号数量太少,可以先看每个条件单独的值分布,再决定是放宽量能倍数还是放宽接近新高阈值。不要一次性把四个条件全部放宽,否则信号会失去过滤作用。
4. 把周策略做成回测
4.1 回测规则如何设计
本文策略的持有频率是“周度”,所以不能每天换仓。回测规则如下:
- 每周最后一个交易日收盘后,检查当日 signal 是否为 1。
- 如果 signal 为 1,则下一周持有;否则下一周空仓。
- 按周收益计算策略净值,对比同期全仓持有的基准净值。
核心是避免未来函数:本周五收盘后才能知道本周末的信号,所以本周的仓位只能由上一周末信号决定。用收益数据表述就是:
- 第 t 周末得到信号。
- 第 t+1 周使用该信号作为仓位。
- 第 t+1 周的策略收益等于仓位乘第 t+1 周的实际收益。
这样的时序关系必须体现在代码中。
4.2 回测代码实现
先按自然周聚合,取每周期最后一个交易日的收盘价和信号。
model_df["week"] = model_df["date"].dt.to_period("W-FRI") weekly = model_df.groupby("week").agg( week_last_close=("close", "last"), signal_last=("signal", "last"), ).reset_index() weekly["week_return"] = weekly["week_last_close"].pct_change().fillna(0) # 上一周末信号决定本周仓位 weekly["position"] = weekly["signal_last"].shift(1).fillna(0) # 策略收益 weekly["strategy_return"] = weekly["position"] * weekly["week_return"] # 策略净值和基准净值 weekly["strategy_nav"] = (1 + weekly["strategy_return"]).cumprod() weekly["benchmark_nav"] = (1 + weekly["week_return"]).cumprod() print(weekly.tail(10))这段代码中,position使用shift(1)是回测正确性的关键。如果不加,就会用本周信息推导本周收益,产生天然的前视偏差,回测结果会虚高。
不同交易市场对一周的定义不同。中国 A 股交易日历到周五,美国市场也可以按周五周历。使用W-FRI表示以周五为一周结束日。如果数据源包含节假日,周一至周五中可能只有四天有行情,last会取到当周实际最后一个交易日,这是期望行为。
4.3 回测指标怎么看
算完净值后,还需要统计几个核心指标:
total_return = weekly["strategy_nav"].iloc[-1] - 1 benchmark_return = weekly["benchmark_nav"].iloc[-1] - 1 dd = weekly["strategy_nav"] / weekly["strategy_nav"].cummax() - 1 max_drawdown = dd.min() trade_weeks = weekly[weekly["position"] == 1] win_rate = (trade_weeks["strategy_return"] > 0).mean() trade_count = int((weekly["position"].diff().fillna(0) == 1).sum()) print({ "total_return": round(total_return, 4), "benchmark_return": round(benchmark_return, 4), "max_drawdown": round(max_drawdown, 4), "win_rate": round(win_rate, 4), "trade_count": trade_count, })预期输出示例:
| 指标 | 数值 | 解读 |
|---|---|---|
| 策略累计收益 | 0.2356 | 模拟数据下策略跑赢基准 |
| 基准累计收益 | -0.0452 | 同期全仓持有收益 |
| 最大回撤 | -0.1187 | 策略净值从高点到低点最大跌幅 |
| 胜率 | 0.5625 | 有仓位周中盈利周占比 |
| 交易次数 | 6 | 信号从 0 变为 1 的次数 |
需要强调,这些数值来自随机模拟数据,只用于验证流程是否跑通,不代表真实行情收益。换一份数据后,指标可能完全不同。
从回测结果中还能观察到两类问题。如果 trade_count 过高,说明信号频繁翻转,持仓体验差;如果 trade_count 过低,说明信号太严格,大部分时间空仓。调整参数时,应同时观察交易次数和回撤,而不只是盯着累计收益。
5. 常见问题与排查路径
5.1 信号总是不出现
现象:运行完代码后,signal 列全部为 0,或者 300 行数据中只有一两个 1。
可能原因:
- 数据行数太少,MA60 计算后只剩不到五十行有效数据。
- 量能放大条件太严格,成交量 MA5 一直小于 MA20 的 1.2 倍。
- 接近新高条件太严格,价格始终达不到近 20 日最高价的 98%。
- 数据包含大量横盘或下跌段,反弹信号确实较少。
检查方式:打印每个布尔条件单独的值分布。
check = model_df[["above_ma20", "above_ma60", "volume_expand", "near_high"]].mean() print(check)每个条件的平均值可以理解为“该条件在历史上有多少比例的时间成立”。如果above_ma60只有 0.1,说明价格大部分时间在 60 日均线下方,这更接近长期下跌结构。此时不应为了凑信号强行放宽阈值,而是重新审视标的区间。
5.2 回测结果异常漂亮或异常难看
现象:回测净值曲线持续上涨,最大回撤几乎为 0;或者信号出现后一周稳定亏损。
原因分析顺序:
- 检查是否存在未来函数。重点看
position是否用了当周 signal,而不是上一周 signal。 - 检查是否使用未复权数据。分红除权会造成价格跳空,均线和收益率计算都会失真。
- 检查是否忽略了滑点和手续费。周度策略交易频率低,手续费影响相对小,但不能完全忽略。
- 检查样本期是否过短或过集中。牛市中几乎所有买入信号都有效,熊市中大部分信号都会失效。
回测必须在不同时间段上验证。最好把数据分成训练段和验证段,避免用同一段数据反复调参。
5.3 指标对不上行情软件
现象:自己用 pandas 算出来的 MA20 和行情软件显示的不一致。
可能原因:
- 复权口径不同。行情软件默认前复权,原始数据可能是不复权。
- 窗口计算起止日期不同。有的软件把当天包含在 MA20 里,有的用前 20 日收盘价再包含当天。
- 停牌日处理不同。有的数据源删除停牌日,有的保留为空。
检查方式:取最近 20 个交易日的收盘价,手工计算平均并对比。
排查思路:先确认数据源头,再确认复权方式,最后确认滚动窗口是否包含当日。
5.4 修改参数后结果剧烈变化
现象:把量能倍数从 1.2 改成 1.1,或者把近高阈值从 0.98 改成 0.95,回测收益和交易次数大幅变化。
这通常说明策略对参数过度敏感,也叫参数脆弱性。这样的策略进入真实数据后很容易失效。
建议做法:
- 记录每次参数组合下的交易次数、胜率、最大回撤和累计收益。
- 优先选择参数在一定区间内结果稳定的区域,而不是收益最高的尖峰。
- 不要反复在同一份数据上搜索最优参数。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 信号全为 0 | 阈值过严或数据量不足 | 打印单条件均值 | 调整量能倍数或近高阈值 |
| 回测收益过高 | 未来函数或未复权 | 检查 shift 与时序 | 修复仓位对齐,统一复权 |
| 指标与软件不一致 | 复权口径或窗口不同 | 手工验证近 20 日 MA | 统一数据口径 |
| 参数一改就失效 | 过拟合或参数敏感 | 扫描参数组合 | 寻找稳定参数区间 |
6. 从学习环境到生产环境的升级思路
6.1 本地跑通后还缺什么
本地代码跑通,只代表策略逻辑在历史上可以复现。真正按周出一份策略报告,还需要补齐很多工程环节。
首先是定时调度。周度策略要求每周五收盘后自动运行一次,可以通过 cron、Windows 计划任务或 Airflow 等调度工具触发。调度任务不能只执行一个脚本,还要写日志、记录运行结果、处理失败重试。
其次是数据可靠性。本地 CSV 可以手工更新,生产环境建议将数据落库,并通过数据源接口每日增量同步。每次计算前要校验数据完整性,比如检查日期是否连续、最后一周期是否确实包含周五。
最后是结果输出。信号结果和回测统计需要自动生成 Markdown、Excel 或 PDF 报告,方便后续归档和复盘。只打印在控制台里的策略,不满足周度报告的要求。
6.2 生产级周度策略检查清单
以下清单可以直接用于上线前检查:
- 数据校验:日期是否连续,最后一行是否为最新交易日,复权口径是否一致。
- 参数配置:均线窗口、量能倍数、近高阈值是否外置到配置文件。
- 时序校验:仓位是否严格使用上周信号,是否存在未来函数。
- 交易成本:回测是否计入手续费和滑点。
- 异常处理:数据为空、接口失败、指标出现 NaN 时脚本能否正常报警。
- 日志记录:每次运行是否记录信号、仓位、收益和错误信息。
- 回滚机制:参数改坏后,能否快速回滚到上一版配置。
- 权限安全:数据文件和配置文件的读写权限是否受控。
这些条目里,数据校验和时序校验最容易被忽略,却是影响结果可信度最关键的部分。
6.3 后续扩展方向
当前实现只是单标的、单信号、固定参数的周度框架,实际项目可以沿几个方向扩展。
第一,把单信号改成信号分数。三个维度的布尔条件可以换成 0 到 3 的得分,根据得分高低决定仓位比例,而不是简单持有或空仓。
第二,增加多标的选择。对多个指数或 ETF 同时计算右侧信号,每周输出“哪些标的满足条件、哪些标的接近条件”的对比表,形成真正的周度观察清单。
第三,引入更多风险指标。除最大回撤外,可以计算夏普比率、卡玛比率、波动率,以及每次交易的持有周数分布,更全面地评估策略。
第四,把研究报告自动化。指标计算、回测、图表、文字结论都做成模板,每周定时生成一篇周度策略文档。
对新手而言,最有价值的练习不是继续优化参数,而是把当前代码重写成类或函数,把数据加载、指标计算、信号生成、回测统计四个环节解耦。这样后续换数据、换参数、换频率都会容易很多。周度右侧策略研究,本质上不是找到一个永远有效的信号,而是建立一套可以持续验证、持续改进、能说得清规则的研究流程。
