pitfalls · 回测两大坑 2026-09-28

内容:
坑一:未来函数(用了当时根本不可能知道的信息)
坑二:交易成本(手续费 / 印花税 / 滑点)
用法:在终端运行 python pitfalls.py
本脚本为教学演示,不构成投资建议

导入

from datetime import datetime # 从 datetime 模块导入 datetime 类(上期学过:处理时间) from quant_data import get_kline # 从数据工具箱导入 get_kline(上期写的:负责拉行情) from metrics import make_signals # 从指标工具箱导入 make_signals(本期的:负责算信号)

参数区 · 三个"假设值"

三个假设值都可以改,改完重新运行看差别。
滑点率 0.001 你想买,实际成交价比预期贵一点;假设千分之一(0.1%)
手续费率 0.0003 佣金:券商收的,买卖各收一次;假设万分之三(0.03%)
印花税率 0.0005 印花税:卖出时单边收;假设万分之五(0.05%)
# ============================================================ # 参数区:三个"假设值",都可以改,改完重新运行看差别 # ============================================================ 滑点率 = 0.001 # 滑点:你想买,实际成交价比预期贵一点;这里假设千分之一(0.1%) 手续费率 = 0.0003 # 佣金:券商收的,买卖各收一次;这里假设万分之三(0.03%) 印花税率 = 0.0005 # 印花税:卖出时单边收;这里假设万分之五(0.05%)

① 拉数据

# ============================================================ # ① 拉数据:结束日期用 datetime.now() # ============================================================ df = get_kline( # 调用数据工具箱,拿 A 股日线 symbol="600519", # 标的代码:贵州茅台(你可以换成任意一只) start_date="20230101", # 开始日期 end_date=datetime.now().strftime("%Y%m%d"), # 结束日期 = 今天(strftime:把时间变成文字) ) df["收益率"] = df["close"].pct_change() # 新增列"收益率":今天比昨天涨跌百分之几 df = make_signals(df) # 调用指标工具箱,把信号列算出来

② 坑一:未来函数

两种"成交假设"对比:
作弊版:今天的信号 × 今天的收益
正确版:昨天的信号 × 今天的收益(shift(1))
# ============================================================ # ② 坑一:未来函数 —— 两种"成交假设"对比 # ============================================================ # 【作弊版】今天的信号 × 今天的收益 df["持仓_作弊"] = df["signal"] # 直接把今天的信号当成今天的持仓 # 【正确版】昨天的信号 × 今天的收益 df["持仓_正确"] = df["signal"].shift(1).fillna(0) # .shift(1):整列往下挪一行 df["收益_作弊"] = df["持仓_作弊"] * df["收益率"] # 作弊版每天的收益 = 持仓 × 当日涨跌 df["收益_正确"] = df["持仓_正确"] * df["收益率"] # 正确版每天的收益 = 持仓 × 当日涨跌 # .cumprod():把每天的 (1 + 当日收益) 依次累乘 —— 就是"1 块钱一天天滚下去变成多少" 净值_作弊 = (1 + df["收益_作弊"].fillna(0)).cumprod() # fillna(0):第一天没有收益,空值填 0 净值_正确 = (1 + df["收益_正确"].fillna(0)).cumprod() print("\n===== 坑一:未来函数(同一份数据,两种成交假设)=====") print(f"作弊版(今天的信号赚今天的收益)累计收益:{(净值_作弊.iloc[-1] - 1) * 100:.2f}%") print(f"正确版(昨天的信号赚今天的收益)累计收益:{(净值_正确.iloc[-1] - 1) * 100:.2f}%") # .iloc[-1]:按位置取最后一行(期末净值),-1 表示倒数第一个 # 冒号后的 .2f:把数字保留两位小数显示

③ 坑二:交易成本

买一次:滑点(买贵了)+ 手续费(佣金)
卖一次:滑点(卖便宜了)+ 手续费 + 印花税
# ============================================================ # ③ 坑二:交易成本 —— 有成本 vs 没成本 # ============================================================ # 买一次要摊上:滑点(买贵了)+ 手续费(佣金) 买入成本比例 = (1 + 滑点率) * (1 + 手续费率) - 1 # 买入这一下大概扣掉百分之几 # 卖一次要摊上:滑点(卖便宜了)+ 手续费 + 印花税 卖出成本比例 = 1 - (1 - 滑点率) * (1 - 手续费率 - 印花税率) # 卖出这一下大概扣掉百分之几 df["成本"] = 0.0 # 新增列"成本",初始全为 0 df.loc[df["signal_change"] == 1, "成本"] = 买入成本比例 # 金叉那天(要买入)记上买入成本 df.loc[df["signal_change"] == -1, "成本"] = 卖出成本比例 # 死叉那天(要卖出)记上卖出成本 if df["signal_change"].iloc[-1] == 1: # 最后一天是金叉 df.loc[df.index[-1], "成本"] = 0.0 # 把最后一天的成本改回 0 df["收益_含成本"] = df["收益_正确"] - df["成本"] # 含成本的每日收益 净值_无成本 = 净值_正确 # 不算成本的净值(上面已算好,直接用) 净值_有成本 = (1 + df["收益_含成本"].fillna(0)).cumprod() # 算了成本的净值 print("\n===== 坑二:交易成本(滑点 0.1% / 佣金 0.03% / 印花税 0.05%)=====") print(f"不算成本:累计收益 {(净值_无成本.iloc[-1] - 1) * 100:.2f}%") print(f"算了成本:累计收益 {(净值_有成本.iloc[-1] - 1) * 100:.2f}%") 差价 = (净值_无成本.iloc[-1] - 净值_有成本.iloc[-1]) * 100 # 两条曲线差了几个百分点 print(f"被成本吃掉:{差价:.2f} 个百分点") # 打印差距 交易次数 = len(df[df["signal_change"] != 0]) # len:数行数 = 动了几次手 print(f"期间一共动了 {交易次数} 次(买一次卖一次算两次)") print("\n⚠️ 以上费率都是教学用的假设值,实盘以你的券商费率为准。") print("⚠️ 教学演示,不构成投资建议。")

核心要点速览

未来函数:用了当时不知道的信息
交易成本:滑点 + 佣金 + 印花税
防未来函数:signal.shift(1)
买入成本:(1+滑点)×(1+佣金)-1
卖出成本:1-(1-滑点)×(1-佣金-印花税)
净值曲线:(1+收益).cumprod()
对比:作弊版 vs 正确版
结论:成本会吃掉真金白银
金句: shift(1) 防未来函数, cumprod() 算净值曲线 —— 回测不排坑,实盘两行泪。

运行输出 · python .\1.py

PS> python .\1.py [数据源] akshare 不可用(ProxyError),尝试 baostock... login success! logout success! [数据源] baostock 获取成功,共 179 条 ===== 坑一:未来函数(同一份数据,两种成交假设)===== 作弊版(今天的信号赚今天的收益)累计收益:-2.59% 正确版(昨天的信号赚今天的收益)累计收益:-5.82% ===== 坑二:交易成本(滑点 0.1% / 佣金 0.03% / 印花税 0.05%)===== 不算成本:累计收益 -5.82% 算了成本:累计收益 -7.27% 被成本吃掉:1.45 个百分点 期间一共动了 10 次(买一次卖一次算两次) ⚠️ 以上费率都是教学用的假设值,实盘以你的券商费率为准。 ⚠️ 教学演示,不构成投资建议。
关键结论:作弊版 -2.59% vs 正确版 -5.82% —— 未来函数虚增了 3.23 个百分点;
加成本后从 -5.82% 变成 -7.27%,被成本吃掉 1.45 个百分点;
期间共交易 10 次(买 5 次 + 卖 5 次)。