首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >如何用 Python 写一个均线策略回测?(附避免未来函数的正确姿势)

如何用 Python 写一个均线策略回测?(附避免未来函数的正确姿势)

原创
作者头像
用户7083614
发布2026-09-05 18:41:59
发布2026-09-05 18:41:59
280
举报

一句话回答: 双均线策略回测只需四步——取复权日 K 线、算快慢均线生成金叉/死叉信号、用次日开盘价成交(这一步是避免未来函数的关键)、计算净值和绩效。下面是可直接运行的完整实现。

什么是双均线策略

  • 快线(如 MA5)上穿慢线(如 MA20)= 金叉,买入信号;
  • 快线下穿慢线 = 死叉,卖出信号。

它是最经典的趋势跟踪策略,适合作为回测框架的入门例子。

最关键的坑:未来函数(前视偏差)

新手最常犯的错误是:在收盘那一刻算出金叉,然后用当天的收盘价成交。但收盘价你在盘中是不知道的——等你确认金叉时,那个价格已经过去了。这叫未来函数 / 前视偏差,会让回测结果虚高得离谱。

正确做法: 用 T 日收盘数据生成信号,在 T+1 日开盘价成交。下面的代码严格遵守这一点。

完整回测代码

代码语言:python
复制
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(开盘价)

收盘价盘中不可知,开盘价可执行

成本

仓位变化时扣手续费+滑点

逼近真实交易摩擦

和基准比一比

只看策略净值没意义,要和"买入持有不动"比:

代码语言:python
复制
df["bench_nav"] = df["close"] / df["close"].iloc[0]
print(f"策略最终净值: {df['nav'].iloc[-1]:.3f}")
print(f"基准(买入持有): {df['bench_nav'].iloc[-1]:.3f}")

批量回测多只股票

代码语言:python
复制
symbols = ["600519.SH", "000001.SZ", "601318.SH"]
results = [backtest_ma(s)[1] for s in symbols]
print(pd.DataFrame(results).to_string(index=False))

回测之后必须做的检验

一个漂亮的回测曲线不等于策略有效。至少要问自己:

  1. 换个时间段还成立吗? 用不同的 count / 起止时间做样本外测试;
  2. 参数是不是过拟合? 把 fast/slow 在一个范围内扫一遍,看结果是不是只在个别参数上好看;
  3. 考虑了停牌、涨停买不进吗? 涨停当天开盘一字板往往买不到,实盘会打折;
  4. 成本假设合理吗? 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) 的成交规则,你就有了一个可信、可复现的回测起点。

参考

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 什么是双均线策略
  • 最关键的坑:未来函数(前视偏差)
  • 完整回测代码
  • 关键设计点解释
  • 和基准比一比
  • 批量回测多只股票
  • 回测之后必须做的检验
  • 常见问题(FAQ)
  • 小结
  • 参考
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档