量化投资从入门到精通
【量化投资从入门到精通 #02】你的第一个量化策略双均线金叉历史上一共赚了多少
上篇 #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%"吓到——关键看对比:
- 策略跑赢了"死拿不动"约 9 个百分点。这一年 000001 是震荡阴跌,双均线靠"死叉空仓"躲过了大部分下跌,择时贡献是正的。
- 但绝对收益仍为负——双均线不是印钞机。它赚的是"趋势市里的顺势钱",震荡市里会反复挨耳光(21 笔交易里不少是小亏出局)。
- 夏普 -0.13 说明承担了波动却没换来正超额——这个参数、这只票、这一年,不值得重仓。
这正是量化思维的起点:先算账,再决定要不要上。肉眼看 K 线觉得"金叉应该能赚",回测一跑才知道真相。
五、常见陷阱
- 参数崇拜:5/20 是随便选的。换一组参数结果可能天差地别(下篇 #20 会讲参数敏感性)。
- 未来函数:回测里绝不能用"明天才知道"的信息。均线用的是当天及之前的收盘价,合规。
- 幸存者偏差:只挑涨得好的票回测会自我欺骗,要用全样本。
- 样本太短:249 天一个牛熊都没走完,结论只能当参考。
六、小结 + 下篇预告
你刚写出了人生第一个能算账的量化策略:取数 → 算信号 → 回测 → 读指标,全流程跑通。下篇 #03 我们把"收益/回撤/夏普"这几个数讲透——它们到底在回答什么投资问题。
免责声明
本文仅为量化方法教学演示,所用标的为示例,不构成任何投资建议,不承诺收益。投资有风险,入市需谨慎。



