连载系列「Python量化实战」第 11 篇(模块六 · 量化策略实战开篇)。上一篇《Python可转债数据分析》我们完成了进阶数据模块;从本篇起,进入"从数据到策略"的技术跃迁。

双均线金叉是几乎所有量化教程的第一个策略:短期均线上穿长期均线买入、下穿卖出。但网上大量教程只贴公式不给完整代码,或者用的数据源要么被封 IP、要么字段对不上,读者复制下来根本跑不通。本文用 Python + mairui SDK,从拉取前复权日K线 → 计算 MA5/MA20/MA60 → 生成金叉/死叉信号 → 按信号回测,给出一套端到端可运行的完整实现,并用真实回测结果讨论这个经典策略的局限性。

本文你将得到什么

  • 一套端到端可运行代码:前复权日K线 → MA 均线 → 金叉/死叉信号 → 无未来函数回测
  • 真实回测结果(21 笔交易、胜率 28.6%、策略 -35.27%)及"回测首要价值是证伪"的讨论
  • 5 个实测踩坑点(日期格式、复权参数、未来函数、异常脱敏、交易成本)

一、在线体验

想先看看K线接口返回什么数据?打开 API Playground 即可在线调用测试,无需安装任何环境:
https://mairuiapi.com/playground

在 Playground 中选择"历史分时交易"接口,输入股票代码即可看到返回的 JSON 结构(开高低收、成交量等字段),确认数据满足需求后再动手写代码,效率更高。

二、环境准备

本文代码使用 mairui SDK 获取股票数据,安装方法如下:

pip install mairui

SDK 的完整接口文档与使用说明请查阅 GitHub 仓库:https://github.com/MaiRuiApi/mairui

K线接口的详细参数说明请查阅官网 API 文档:https://mairuiapi.com/hsdata

运行环境:

  • Python 3.9+
  • mairui SDK 1.0.0
  • pandas 2.2+

licence(证书)通过环境变量读取,不要写死在代码里:

import os
import mairui

# 证书从环境变量读取,注册后可在官网获取
licence = "LICENCE-66D8-9F96-0C7F0FBCD073"  # 演示证书,演示证书(读者复制即用,无需替换)
api = mairui.Client(licence)

本文数据截至 2026-07-24(前复权日K线),回测区间:2024-01-02 至 2026-07-24。读者复现时数据可能因行情更新而略有差异。

三、获取前复权日K线数据

回测必须用复权价格。分红送股会让不复权价格出现向下跳空,均线在除权日附近产生虚假交叉信号。stock_history 接口支持复权参数:

import pandas as pd

def fetch_daily_kline(api, code, start, end):
    """拉取前复权日K线并整理为 DataFrame。"""
    # d=日线, f=前复权;st/et 日期格式为 YYYYMMDD
    rows = api.stock_history(code, "d", "f", st=start, et=end)
    kline_df = pd.DataFrame(rows)
    kline_df = kline_df.rename(columns={"t": "date", "o": "open", "h": "high",
                                        "l": "low", "c": "close", "v": "volume"})
    kline_df["date"] = pd.to_datetime(kline_df["date"])
    return kline_df.sort_values("date").reset_index(drop=True)

kline_df = fetch_daily_kline(api, "600519", "20240101", "20260724")

关键解释:

  • 接口返回字段为缩写:t=日期、o/h/l/c=开高低收、v=成交量(手)、a=成交额,重命名成语义化列名方便后续处理;
  • 日期参数必须是 YYYYMMDD 格式(如 20240101),带连字符会返回 HTTP 400,这是实测踩过的坑;
  • 复权参数 f 为前复权、n 为不复权,回测统一用前复权。

真实运行输出:

K线条数: 619, 区间: 2024-01-02 ~ 2026-07-24

完整字段说明可在官网 API 文档(https://mairuiapi.com/hsdata)中查阅。

四、计算均线与金叉死叉信号

均线用 rolling().mean() 一行搞定,关键是交叉判定:金叉 = 今天 MA5 在 MA20 上方、且昨天还在下方(或相等)。用 shift(1) 取前一日值做向量化判断,不需要写循环:

def add_ma_and_signals(kline_df):
    """计算 MA5/MA20/MA60,并生成金叉/死叉信号列。"""
    for win in (5, 20, 60):
        kline_df[f"MA{win}"] = kline_df["close"].rolling(win).mean()
    ma_fast, ma_slow = kline_df["MA5"], kline_df["MA20"]
    # 金叉:MA5 上穿 MA20
    kline_df["gold_cross"] = (ma_fast > ma_slow) & (ma_fast.shift(1) <= ma_slow.shift(1))
    # 死叉:MA5 下穿 MA20
    kline_df["death_cross"] = (ma_fast < ma_slow) & (ma_fast.shift(1) >= ma_slow.shift(1))
    return kline_df

关键解释:

  • rolling(5).mean() 前 4 行是 NaN,属正常现象,交叉判定中 NaN 参与比较结果为 False,不会产生假信号;
  • 两个布尔条件必须都用括号包住再做 & 运算,这是 pandas 新手最常见的语法报错点。

在 2024-01 至 2026-07 的 619 个交易日里,真实统计结果:

金叉次数: 21, 死叉次数: 20

平均约一个月出现一次交叉——这个信号频率意味着交易成本不可忽略,后文回测会验证这一点。

五、按信号回测:金叉买入、死叉卖出

回测规则要避免未来函数:信号在收盘后才能确认(均线用收盘价算),所以成交只能发生在次日开盘价。这是很多教程用"当日收盘价成交"高估收益的常见错误:

def backtest_ma_cross(kline_df):
    """信号次日开盘价成交的简单回测(不含手续费/滑点,教学用)。"""
    trades, position, entry_price, entry_date = [], 0, None, None
    for i in range(len(kline_df) - 1):
        row, next_row = kline_df.iloc[i], kline_df.iloc[i + 1]
        if position == 0 and row["gold_cross"]:
            # 金叉次日开盘买入
            position, entry_price, entry_date = 1, next_row["open"], next_row["date"]
        elif position == 1 and row["death_cross"]:
            # 死叉次日开盘卖出
            exit_price = next_row["open"]
            trades.append({
                "buy_date": entry_date.date(), "buy_price": round(entry_price, 2),
                "sell_date": next_row["date"].date(), "sell_price": round(exit_price, 2),
                "return_pct": round((exit_price / entry_price - 1) * 100, 2),
            })
            position = 0
    return pd.DataFrame(trades)

六、完整实战示例与真实回测结果

把上面三步串起来(完整可运行代码即文中各函数 + 下面的 main 流程):

def main():
    licence = os.environ["MAIRUI_LICENCE"]   # 证书从环境变量读取
    api = mairui.Client(licence)

    kline_df = fetch_daily_kline(api, "600519", "20240101", "20260724")
    kline_df = add_ma_and_signals(kline_df)
    trades_df = backtest_ma_cross(kline_df)

    win_rate = (trades_df["return_pct"] > 0).mean() * 100
    print(trades_df.to_string(index=False))
    print(f"交易次数: {len(trades_df)}, 胜率: {win_rate:.1f}%")

main()

真实运行输出(截取首尾交易):

  buy_date  buy_price  sell_date  sell_price  return_pct
2024-02-08    1585.60 2024-03-11     1540.59       -2.84
2024-03-14    1584.59 2024-03-29     1567.23       -1.10
...
2026-04-02    1431.42 2026-04-22     1386.98       -3.10
2026-07-15    1203.66 2026-07-24     1297.41        7.79

交易次数: 21, 胜率: 28.6%
策略累计收益: -35.27% | 同期买入持有: -16.33%

这个结果值得认真讨论:两年半 21 笔交易、胜率仅 28.6%,策略累计 -35.27%,明显跑输同期买入持有的 -16.33%(还没算手续费)。原因在于:该标的这段时间处于宽幅震荡下行,双均线是趋势跟踪策略,在震荡市里会被反复"打脸"——金叉追高、死叉割肉。

这正是回测的价值:它的首要作用不是"证明策略赚钱",而是在投入真金白银之前证伪。一个诚实的回测框架,比一个只展示盈利曲线的教程有用得多。改进方向(留给后续篇目展开):加 MA60 趋势过滤(只在长期均线向上时接受金叉)、换参数组合批量测试、跨多标的验证。

七、避坑与进阶

  • 日期格式st/et 必须为 YYYYMMDD,带连字符会 HTTP 400;
  • 复权参数f(前复权)/n(不复权)可用,回测务必用前复权,否则除权日会产生虚假交叉;
  • 未来函数:信号确认在收盘后,成交价用次日开盘价,不要用当日收盘价;
  • 异常信息脱敏:SDK 抛出的 HTTP 异常信息中含完整请求 URL(内嵌你的证书),日志或截图对外展示前务必先把证书串替换掉;
  • 交易成本:本文为教学简化未计手续费与滑点,21 笔交易的双边成本会进一步拉低净收益,实盘评估必须计入;
  • 频率限制:批量回测多只股票时注意控制请求频率,工程化方案见本系列第 16 篇。

八、总结与延伸

本文完成了「Python量化实战」策略模块的第一课:用 stock_history 拉取前复权K线、用 rolling + shift 向量化生成金叉/死叉信号、用"次日开盘成交"的规则做无未来函数的回测,并用真实数据看到了趋势策略在震荡市的局限。下一篇我们把视角从"择时"切换到"选股"——《Python多因子选股模型搭建:5个核心因子与打分排序实现》。

延伸阅读:

  • 在线体验更多接口:https://mairuiapi.com/playground
  • 查看完整 API 文档:https://mairuiapi.com/hsdata
  • SDK 文档与源码:https://github.com/MaiRuiApi/mairui
  • 关注公众号获取本系列更新

本文为技术演示,文中回测结果仅说明方法论,不构成投资建议。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐