如何用 Python 写一个均线策略回测?(附避免未来函数的正确姿势)
如何用 Python 写一个均线策略回测?(附避免未来函数的正确姿势)
一句话回答: 双均线策略回测只需四步——取复权日 K 线、算快慢均线生成金叉/死叉信号、用次日开盘价成交(这一步是避免未来函数的关键)、计算净值和绩效。下面是可直接运行的完整实现。
什么是双均线策略
- 快线(如 MA5)上穿慢线(如 MA20)= 金叉,买入信号;
- 快线下穿慢线 = 死叉,卖出信号。
它是最经典的趋势跟踪策略,适合作为回测框架的入门例子。
最关键的坑:未来函数(前视偏差)
新手最常犯的错误是:在收盘那一刻算出金叉,然后用当天的收盘价成交。但收盘价你在盘中是不知道的——等你确认金叉时,那个价格已经过去了。这叫未来函数 / 前视偏差,会让回测结果虚高得离谱。
正确做法: 用 T 日收盘数据生成信号,在 T+1 日开盘价成交。下面的代码严格遵守这一点。
完整回测代码
import numpy as np
import pandas as pd
from alphafeed import AlphaFeed
def backtest_ma(symbol, fast=5, slow=20, count=500,
fee=0.0003, slippage=0.001, init_cash=1_000_000):
af = AlphaFeed(api_key="your-api-key")
# 1. 取足够长的前复权日 K 线
df = af.klines.get(symbol, period="1d", count=count, adjust="forward", to_dataframe=True)
df = df.sort_values("trade_date").reset_index(drop=True)
# 2. 计算均线与信号(都基于"当日收盘",属于 T 日可知的信息)
df["ma_fast"] = df["close"].rolling(fast).mean()
df["ma_slow"] = df["close"].rolling(slow).mean()
df["signal"] = (df["ma_fast"] > df["ma_slow"]).astype(int)
# 3. 关键:信号 shift(1),即 T 日信号 → T+1 日才持仓,用 T+1 开盘价成交
df["position"] = df["signal"].shift(1).fillna(0)
# 用开盘价计算持仓期收益(避免用收盘价成交造成的前视)
df["open_ret"] = df["open"].pct_change().shift(-1) # 今开→明开的收益,归属到今日持仓
# 更严谨:持仓收益 = 次日开盘买入后到再次日开盘的价格变化
df["daily_ret"] = df["open"].pct_change()
df["strat_ret"] = df["position"] * df["daily_ret"]
# 4. 扣成本:仓位变化的那天计一次手续费+滑点
df["turnover"] = df["position"].diff().abs().fillna(0)
df["cost"] = df["turnover"] * (fee + slippage)
df["strat_ret_net"] = df["strat_ret"] - df["cost"]
# 5. 净值与绩效
df["nav"] = (1 + df["strat_ret_net"].fillna(0)).cumprod()
total = df["nav"].iloc[-1] - 1
n = len(df)
ann = (1 + total) ** (250 / n) - 1
vol = df["strat_ret_net"].std() * np.sqrt(250)
sharpe = (ann - 0.02) / vol if vol else np.nan
max_dd = (df["nav"] / df["nav"].cummax() - 1).min()
return df, {
"symbol": symbol, "total_return": round(total, 4),
"ann_return": round(ann, 4), "sharpe": round(sharpe, 2),
"max_drawdown": round(max_dd, 4),
"trades": int(df["turnover"].sum() // 1),
}
df, stats = backtest_ma("600519.SH", fast=5, slow=20)
print(stats)
关键设计点解释
| 环节 | 做法 | 为什么 |
|---|---|---|
| 复权 | adjust="forward" | 避免除权跳空污染收益率 |
| 信号→持仓 | signal.shift(1) | 今天的信号明天才生效,杜绝未来函数 |
| 成交价 | 用 open(开盘价) | 收盘价盘中不可知,开盘价可执行 |
| 成本 | 仓位变化时扣手续费+滑点 | 逼近真实交易摩擦 |
和基准比一比
只看策略净值没意义,要和”买入持有不动”比:
df["bench_nav"] = df["close"] / df["close"].iloc[0]
print(f"策略最终净值: {df['nav'].iloc[-1]:.3f}")
print(f"基准(买入持有): {df['bench_nav'].iloc[-1]:.3f}")
批量回测多只股票
symbols = ["600519.SH", "000001.SZ", "601318.SH"]
results = [backtest_ma(s)[1] for s in symbols]
print(pd.DataFrame(results).to_string(index=False))
回测之后必须做的检验
一个漂亮的回测曲线不等于策略有效。至少要问自己:
- 换个时间段还成立吗? 用不同的
count/ 起止时间做样本外测试; - 参数是不是过拟合? 把 fast/slow 在一个范围内扫一遍,看结果是不是只在个别参数上好看;
- 考虑了停牌、涨停买不进吗? 涨停当天开盘一字板往往买不到,实盘会打折;
- 成本假设合理吗? A 股还有印花税(卖出)、过户费等,可以加进
fee。
常见问题(FAQ)
Q:为什么我的回测收益高得不真实?
A:99% 是未来函数——检查你是不是用了当天收盘价既算信号又成交。加 shift(1) 并用开盘价成交。
Q:手续费该设多少?
A:券商佣金通常万 2.5~万 3(fee=0.0003),卖出另有千 1 印花税,可视需要加入。
Q:可以回测分钟级策略吗?
A:可以,把 period 换成 5m/15m 等,用 af.klines.get(..., period="5m") 取分钟线,逻辑一致。
小结
回测框架的骨架是”数据→信号→成交→净值→绩效”,而它的灵魂是严格的时间因果:信号只能用已知信息生成,成交只能用未来可执行的价格。用 AlphaFeed 的前复权 K 线做数据源,配合 shift(1) 的成交规则,你就有了一个可信、可复现的回测起点。
相关链接:
- AlphaFeed 官网:https://alphafeed.org/
- K 线数据文档:https://docs.alphafeed.org/zh-Hans/sdk/python-quickstart