
最近围绕“8月13日”和“可交易反弹”这两个关键词市场上有不少讨论。不少朋友会问为什么有人三周前就能预判到反弹而我只能等反弹走出来之后才后知后觉其实这类问题不该停留在“猜涨跌”的层面更好的方式是把“可交易反弹”变成一个可以被量化、被回测、被评估的信号系统。本文会抛开对行情预测对错的争论从量化开发的视角出发手把手带你用 Python 完成“可交易反弹”的定义、识别和回测。无论你是刚接触量化的小白还是已经在做策略研究的开发者都能在这套闭环流程里找到可以直接复用的思路和代码。1. “可交易反弹”到底是什么意思很多行情观点里会出现“可交易反弹”这个词。简单来说它指的是市场经历一段下跌后出现了具备参与价值的短期上涨窗口。和普通的技术性反弹不同“可交易反弹”通常要满足两个条件反弹幅度足够大扣掉手续费和滑点后依然有利润空间反弹持续性足够强不会今天涨完明天就跌回去。但从工程角度来看这个概念有一层更大的问题它太模糊了。每个人对“下跌”“上涨”“可交易”的理解都不一样如果没有一个统一的量化口径就无法回测、无法比较、无法优化。所以我们做量化分析的第一步不是急着找最优参数而是把“可交易反弹”拆解成能够被计算机识别的规则。常见的拆解思路有几种用均线判断趋势状态比如价格从均线下方重新站上均线用 RSI 判断超卖比如 RSI 低于 30 后重新上穿 30用 MACD 判断动能比如出现金叉或底背离用布林带判断波动比如价格触及下轨后收回。这些指标本身不是圣杯但它们共同构成了一个可以落地的信号体系。下面我们就用 Python 把这套体系实现出来。2. 环境准备与项目结构本文示例代码使用 Python 编写运行环境以常见配置为例操作系统Windows / macOS / Linux 均可Python 版本3.9 及以上数据接口akshare示例用也可以换成 tushare 或本地 CSV 数据依赖库pandas、numpy、matplotlib。如果你的环境还没有安装这些库可以用下面的命令安装pip install akshare pandas numpy matplotlib这里需要提醒一下akshare 的接口会随数据源网站改版而变化不同版本的接口参数可能略有差异。本文代码以“获取 A 股日线行情”为目标核心是讲解量化分析的思路实际使用时请根据你机器上的 akshare 版本进行微调。建议按下面的目录结构来组织项目trade_rebound/ ├── data/ │ └── stock_data.csv ├── signals.py ├── backtest.py └── main.pydata/存放下载的行情数据signals.py负责信号计算backtest.py负责回测逻辑main.py主程序入口。3. 行情数据获取与预处理3.1 下载行情数据量化分析的第一步是拿到干净、准确的数据。这里我们以 akshare 为例获取某只指数的日线行情。代码只做示例说明实际使用时请把symbol换成自己关注的品种代码。# 文件路径data_download.py import akshare as ak import pandas as pd # 这里以沪深300指数为例不同版本的方法名可能不同 df ak.stock_zh_index_daily(symbolsh000300) # 只保留需要的列 df df.rename(columns{ date: date, open: open, high: high, low: low, close: close, volume: volume }) df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 保存到本地 df.to_csv(data/stock_data.csv, indexFalse) print(df.tail())如果 akshare 的接口不可用也可以直接加载本地 CSV 文件格式保持date,open,high,low,close,volume即可df pd.read_csv(data/stock_data.csv, parse_dates[date])3.2 数据预处理要点拿到原始数据后不能直接开始算指标至少要做下面几件事检查日期是否连续有没有缺失交易日检查是否有重复行检查是否有空值按日期升序排序。对应的处理代码def prepare_data(df: pd.DataFrame) - pd.DataFrame: 行情数据清洗与预处理 df df.copy() df[date] pd.to_datetime(df[date]) df df.drop_duplicates(subsetdate) df df.sort_values(date).reset_index(dropTrue) # 简单检查数据区间 print(f数据范围{df[date].iloc[0]} 至 {df[date].iloc[-1]}) print(f总交易日{len(df)}) return df前复权和后复权的问题在计算收益率时也需要考虑。如果只是做短期反弹信号测试使用后复权数据更接近真实收益表现如果只拿了原始价格数据则要在结果分析时说明这个限制。4. 反弹信号定义比预测更重要4.1 信号一RSI 超卖回升RSI相对强弱指数是最常用的超买超卖指标之一。它的核心逻辑是当 RSI 进入超卖区通常低于 30说明短期下跌情绪过度释放后续可能出现反弹。我们在原始 RSI 基础上再加一个“回升”条件RSI 先前进入过低值随后重新上穿某个阈值。这样定义出来的信号更接近交易中的“确认买入”动作。import pandas as pd def add_rsi_signal(df: pd.DataFrame, period: int 14, oversold: float 30, exit_level: float 50) - pd.DataFrame: 计算 RSI 超卖回升信号 df df.copy() delta df[close].diff() gain delta.clip(lower0).rolling(windowperiod).mean() loss (-delta.clip(upper0)).rolling(windowperiod).mean() # 防止除零 rs gain / loss.replace(0, pd.NA) df[rsi] 100 - (100 / (1 rs)) # RSI 曾进入超卖区 df[was_oversold] df[rsi].shift(1) oversold # RSI 上穿 exit_level且前一天处于超卖区 df[rsi_cross] (df[rsi] exit_level) (df[was_oversold]) return df这里有一个容易踩的细节was_oversold使用了shift(1)目的是保证信号发生在 RSI 上穿当天而不是同一天既超卖又上穿。4.2 信号二均线重新站上均线的逻辑很好理解当价格从下方重新站上短期均线说明短期买盘在恢复。但为了过滤掉震荡市里的假突破我们还可以附加一个条件中期均线不能处于明显下行状态。def add_ma_signal(df: pd.DataFrame, short: int 5, long: int 20) - pd.DataFrame: 计算价格站上短期均线信号 df df.copy() df[ma_short] df[close].rolling(windowshort).mean() df[ma_long] df[close].rolling(windowlong).mean() # 前一天在短期均线下方当天站上 df[cross_above] ( (df[close].shift(1) df[ma_short].shift(1)) (df[close] df[ma_short]) ) # 中期趋势不深度恶化长期均线斜率大于0或跌幅可控 df[ma_long_slope] df[ma_long].diff(3) return df注意这里我把“长期均线斜率”设计成了可选条件。因为不同品种的波动特征差异很大固定参数的过滤效果需要靠回测来验证而不是拍脑袋决定。4.3 信号三MACD 底背离简化版MACD 的经典用法是金叉死叉。底背离则是更高级的用法价格创出新低但 MACD 的绿柱面积或 DIF 值没有创新低说明下跌动能衰竭。完整底背离逻辑比较繁琐这里给出简化版思路def add_macd_signal(df: pd.DataFrame, fast: int 12, slow: int 26, signal: int 9) - pd.DataFrame: 计算 MACD 金叉信号 df df.copy() ema_fast df[close].ewm(spanfast, adjustFalse).mean() ema_slow df[close].ewm(spanslow, adjustFalse).mean() df[dif] ema_fast - ema_slow df[dea] df[dif].ewm(spansignal, adjustFalse).mean() df[macd_hist] (df[dif] - df[dea]) * 2 # 金叉DIF 上穿 DEA df[macd_gold] ( (df[dif].shift(1) df[dea].shift(1)) (df[dif] df[dea]) ) return df4.4 信号合并与过滤有了单一信号之后下一步是合并。合并方式可以是“或”逻辑也可以是“与”逻辑“或”逻辑信号更灵敏但噪音多“与”逻辑信号更严格但可能会漏掉行情。我的建议是先用“或”逻辑做初筛再用额外的风险过滤条件比如最大跌幅、成交量放大比例做二次筛选。def add_combined_signal(df: pd.DataFrame) - pd.DataFrame: 合并反弹信号 df df.copy() # 任一信号触发都记为反弹信号 df[signal] ( df.get(rsi_cross, False) | df.get(cross_above, False) | df.get(macd_gold, False) ).astype(int) return df5. 回测框架验证信号是否真的“可交易”5.1 交易规则设定在回测之前要先把交易规则说清楚。本文示例采用如下规则信号出现后的第二个交易日开盘买入持有固定 5 个交易日后卖出不考虑资金管理每次满仓手续费按万 2.5 计算滑点按 0.01% 估算。“信号出现后第二个交易日买入”是为了避免未来函数。很多人回测结果特别漂亮就是因为直接用了信号当天的收盘价买入这在现实中根本做不到。5.2 完整回测代码# 文件路径backtest.py import pandas as pd import numpy as np def run_backtest(df: pd.DataFrame, hold_days: int 5, fee_rate: float 0.00025, slippage: float 0.0001) - pd.DataFrame: 简单反弹策略回测 规则信号出现后次日开盘买入持有 hold_days 天后开盘卖出 df df.copy() df[buy_price] np.nan df[sell_price] np.nan df[trade_return] 0.0 for i in range(len(df)): if df.loc[i, signal] ! 1: continue buy_idx i 1 sell_idx i 1 hold_days if sell_idx len(df): continue buy_price df.loc[buy_idx, open] sell_price df.loc[sell_idx, open] # 扣除手续费和滑点 buy_cost buy_price * (1 fee_rate slippage) sell_income sell_price * (1 - fee_rate - slippage) ret sell_income / buy_cost - 1 df.loc[buy_idx, buy_price] buy_price df.loc[sell_idx, sell_price] sell_price df.loc[buy_idx, trade_return] ret return df def evaluate(df: pd.DataFrame): 输出回测指标 trades df.dropna(subset[buy_price]) if trades.empty: print(没有产生交易信号) return {} returns trades[trade_return] win_rate (returns 0).mean() avg_return returns.mean() total_return (1 returns).prod() - 1 max_drawdown drawdown(returns) stats { 交易次数: len(trades), 胜率: f{win_rate:.2%}, 平均单次收益: f{avg_return:.2%}, 累计收益: f{total_return:.2%}, 最大回撤: f{max_drawdown:.2%}, } for k, v in stats.items(): print(f{k}: {v}) return stats def drawdown(returns: pd.Series) - float: 根据每笔收益序列计算回撤 equity (1 returns).cumprod() peak equity.cummax() dd equity / peak - 1 return dd.min()5.3 运行主程序# 文件路径main.py import pandas as pd from signals import add_rsi_signal, add_ma_signal, add_macd_signal, add_combined_signal from backtest import run_backtest, evaluate if __name__ __main__: # 1. 加载数据 df pd.read_csv(data/stock_data.csv, parse_dates[date]) # 2. 计算信号 df add_rsi_signal(df) df add_ma_signal(df) df add_macd_signal(df) df add_combined_signal(df) # 3. 回测 df run_backtest(df) # 4. 评估 evaluate(df)运行后你会看到类似这样的输出交易次数: 37 胜率: 54.05% 平均单次收益: 0.86% 累计收益: 32.51% 最大回撤: -8.23%注意这个结果只是示例不构成任何投资建议。不同数据区间、不同品种、不同参数下结果可能差异巨大。我们真正要观察的不是“赚了多少钱”而是这套信号系统是否稳定、回撤是否可控、交易次数是否足够。6. 常见问题与排查思路在实际运行和调整策略时下面几个问题出现的频率非常高。问题现象常见原因解决思路回测收益特别高但实盘完全不一样引入了未来函数比如信号当天就买入检查信号是否用了当日收盘数据买入是否顺延到下一交易日交易次数过少多个信号用“与”逻辑合并条件太严格放宽过滤条件或改用“或”逻辑结果不稳定换数据区间差异大参数过拟合专为某段行情调参做样本外测试减少参数数量买入后连续下跌没有考虑趋势过滤逆势抄底加入长期均线方向过滤数据缺失导致信号中断停牌、退市、数据源接口调整检查数据完整性并用前值填充或删除异常段这里重点说一下“未来函数”问题。很多初学回测的朋友会在信号计算当天直接使用收盘价买入然后第二天卖出结果回测曲线非常漂亮。实际上信号在收盘后才能确定你最早只能在下一个交易日开盘时买入。你回测中使用的任何当日信息都会造成虚高的收益。这是一个很隐蔽的坑需要格外注意。再看一个数据问题。akshare 返回的部分指数数据是没有成交量的如果策略里用到放量条件会导致整列 NaN。建议在数据预处理阶段就检查列完整性并做好异常值处理。另一个常见问题是涨跌停限制。股票在涨停时无法买入在跌停时无法卖出回测如果不加这个限制结果会偏向乐观。简单做法是在回测里增加一个判断如果买入当日开盘价接近涨停价比如涨幅超过 9.5%则放弃这笔交易。7. 最佳实践与工程建议7.1 把信号和研究过程分离实际做策略研究时不要把信号逻辑、回测逻辑、数据加载全写在一个文件里。建议分成三个模块数据层、信号层、回测层。这样每次调整信号参数时不需要改动回测代码也能更清楚地定位问题。7.2 参数不能“调”出来要“验”出来当你不断调整 RSI 周期、均线周期、持有天数直到回测结果满意为止时其实已经陷入了过拟合。正确的做法是先确定一组逻辑合理的参数在样本内数据上测试再用样本外数据验证如果两组结果差异过大说明策略不具备稳定性。对于“可交易反弹”这类中短期策略建议关注两个核心指标一是单次平均收益能不能覆盖手续费和滑点二是最大回撤是否在可接受范围内。单看胜率意义不大因为即使胜率只有 40%只要盈亏比足够策略依然可以盈利。7.3 交易成本必须计足A 股的手续费看起来只有万几但加上印花税、滑点和冲击成本对短线策略的影响可能非常显著。很多回测中看起来“能赚钱”的策略在计入真实成本后可能变成负收益。建议在回测中至少按双边万五到万八来估算如果你做的是高频方向成本还要更高。7.4 加入必要的风控条件一个完整的“可交易反弹”策略不应该只有买入信号还应该有仓位管理和止损条件。比如单笔亏损超过 3% 强制止损连续亏损 3 次后暂停交易策略出现单月回撤超过 5% 时降低仓位。这些规则虽然看起来简单但在工程实现上能显著提升策略的稳定性。真正的量化交易系统往往不是因为预测准而赚钱而是因为风控到位而活下来。7.5 用数据库管理数据而不是每次下载如果策略需要长期跟踪建议把日线数据存入本地数据库SQLite、MySQL 均可每天增量更新。这样既能避免接口调用次数限制也能保证回测数据的一致性。8. 总结与后续学习方向回到开头的问题三周前预测的“可交易反弹”到底在哪里从量化开发的角度看这个问题的答案并不重要。重要的是你能不能用清晰、可复现的规则去定义反弹并用历史数据去验证这套规则是否有效。本文完成了这样一套闭环用 Python 获取和清洗行情数据用 RSI、均线、MACD 三类信号识别反弹用回测框架验证信号的交易价值用胜率、累计收益、最大回撤等指标评估策略表现针对未来函数、涨跌停、交易成本等问题做了排查和修正。下一步你可以从这几个方向继续深入尝试把单品种扩展成多品种观察信号在不同品种上的普适性引入更多风险过滤条件比如波动率过滤、成交量确认学习更专业的回测框架如 backtrader、vectorbt研究组合管理把单一反弹策略升级成多策略、多周期体系。当你能用自己的代码识别出“可交易反弹”并且说清楚它的胜率、回报和风险时你就不再是凭感觉做交易而是真正意义上的量化开发者。建议你打开编辑器把本文代码跑一遍然后替换成自己关注的数据体验一下从定义信号到回测验证的完整过程。