← 返回博客列表

量化投资从入门到精通

【量化投资从入门到精通 #02】你的第一个量化策略双均线金叉历史上一共赚了多少

2026年08月22日 17:12 · 麦蕊智数 MyData

上篇 #01 我们把"我觉得要涨"变成了一条规则。这篇把它升级成一笔能算账的策略——双均线金叉。

一、这个方法解决什么投资问题

很多人炒股的痛点是:买在高点、卖在低点,情绪被盘面牵着走。双均线金叉是量化里最经典的"择时"入门套路——它用两条均线把"趋势转强"这个模糊的感觉,变成一个机械的、可回测的买卖信号

它解决的核心问题是:怎么让"什么时候该在场内、什么时候该空仓"这件事,不靠感觉,靠规则。

二、核心思想(用大白话讲清)

均线 = 一段时间内收盘价的平均,是一张"市场平均成本"的体温图。

  • 短期均线(如 5 日):灵敏,跟着价格快速走,代表最近的情绪。
  • 长期均线(如 20 日):迟钝,代表一个月的整体方向。

金叉 = 短期线从下面上穿长期线 → 短期比长期强了,趋势可能转多,买入。 死叉 = 短期线从上面下穿长期线 → 短期走弱,卖出。

就这么简单:金叉进场、死叉离场。不预测顶底,只跟随趋势

三、实操演示:用麦蕊数据跑一遍回测

我们用麦蕊的日 K 线接口拉取一只代表股票(示例 000001,非推荐)近一年的数据,按 5 日 / 20 日双均线回测:

import os, datetime, statistics
from mairui import Client

CODE = "000001"
FAST, SLOW = 5, 20

mc = Client("<你的证书>")   # 证书从环境变量读取,不写死在代码里


def load_closes(code, n_days=250):
    """取最近 n_days 根日 K 线(前复权),按日期升序返回收盘价列表。"""
    end = datetime.date.today().strftime("%Y%m%d")
    st = (datetime.date.today() - datetime.timedelta(days=int(n_days * 1.6) + 30)).strftime("%Y%m%d")
    rows = mc.stock_history(code, "d", "n", st=st, et=end)      # "n" = 前复权
    rows = [r for r in rows if r.get("c") is not None and r.get("t")]
    rows.sort(key=lambda r: r["t"])
    return [float(r["c"]) for r in rows[-n_days:]]


def sma(vals, n):
    """简单移动平均,前 n-1 位为 None。"""
    out = [None] * len(vals)
    for i in range(n - 1, len(vals)):
        out[i] = sum(vals[i - n + 1:i + 1]) / n
    return out


def backtest_ma_cross(closes, fast, slow):
    """金叉持有 / 死叉空仓。信号只用当日及之前的均线——没有未来函数。"""
    ma_f, ma_s = sma(closes, fast), sma(closes, slow)
    pos = [0] * len(closes)
    for i in range(slow, len(closes)):
        prev, cur = ma_f[i - 1] - ma_s[i - 1], ma_f[i] - ma_s[i]
        if prev <= 0 < cur:
            pos[i] = 1                      # 金叉
        elif prev >= 0 > cur:
            pos[i] = 0                      # 死叉
        else:
            pos[i] = pos[i - 1]             # 维持原仓位
    equity, trades = [1.0], 0
    for i in range(1, len(closes)):
        day_ret = closes[i] / closes[i - 1] - 1
        equity.append(equity[-1] * (1 + day_ret * pos[i]))
        if pos[i] != pos[i - 1]:
            trades += 1
    return equity, trades


def calc_metrics(eq, periods=252):
    rets = [eq[i] / eq[i - 1] - 1 for i in range(1, len(eq))]
    peak, max_dd = eq[0], 0.0
    for v in eq:
        peak = max(peak, v)
        max_dd = min(max_dd, v / peak - 1)
    ann_vol = statistics.pstdev(rets) * (periods ** 0.5)
    ann_ret = eq[-1] ** (periods / (len(eq) - 1)) - 1
    return {"cum_ret": eq[-1] / eq[0] - 1, "max_dd": max_dd,
            "ann_vol": ann_vol, "sharpe": (ann_ret / ann_vol) if ann_vol else 0.0}


closes = load_closes(CODE, 250)
equity, n_trades = backtest_ma_cross(closes, FAST, SLOW)
m = calc_metrics(equity)

print(f"策略累计收益:{m['cum_ret'] * 100:.2f}%")
print(f"最大回撤:{m['max_dd'] * 100:.2f}%")
print(f"年化波动:{m['ann_vol'] * 100:.2f}%")
print(f"夏普比率:{m['sharpe']:.2f}")
print(f"买卖次数:{n_trades}")

真实回测结果(2026-08-06 实跑,区间 2025-07-28 ~ 2026-08-06,249 个交易日):

指标 双均线(5/20) 买入持有
累计收益 -1.18% -10.35%
最大回撤 -9.88% (同期更差)
年化波动 9.20%
夏普 -0.13
买卖次数 21

四、结果解读:这笔账在说什么

别被"-1.18%"吓到——关键看对比

  1. 策略跑赢了"死拿不动"约 9 个百分点。这一年 000001 是震荡阴跌,双均线靠"死叉空仓"躲过了大部分下跌,择时贡献是正的。
  2. 但绝对收益仍为负——双均线不是印钞机。它赚的是"趋势市里的顺势钱",震荡市里会反复挨耳光(21 笔交易里不少是小亏出局)。
  3. 夏普 -0.13 说明承担了波动却没换来正超额——这个参数、这只票、这一年,不值得重仓。

这正是量化思维的起点:先算账,再决定要不要上。肉眼看 K 线觉得"金叉应该能赚",回测一跑才知道真相。

五、常见陷阱

  • 参数崇拜:5/20 是随便选的。换一组参数结果可能天差地别(下篇 #20 会讲参数敏感性)。
  • 未来函数:回测里绝不能用"明天才知道"的信息。均线用的是当天及之前的收盘价,合规。
  • 幸存者偏差:只挑涨得好的票回测会自我欺骗,要用全样本。
  • 样本太短:249 天一个牛熊都没走完,结论只能当参考。

六、小结 + 下篇预告

你刚写出了人生第一个能算账的量化策略:取数 → 算信号 → 回测 → 读指标,全流程跑通。下篇 #03 我们把"收益/回撤/夏普"这几个数讲透——它们到底在回答什么投资问题。

免责声明

本文仅为量化方法教学演示,所用标的为示例,不构成任何投资建议,不承诺收益。投资有风险,入市需谨慎。

标签: 历史数据
QQ 客服 3826425416 咨询时请提供证书号或订单号,便于快速处理
咨询