Appearance
均线交叉策略
均线交叉(Moving Average Crossover)是量化交易中最经典、最基础的策略。虽然简单,但它包含了趋势跟踪策略的所有核心要素:趋势识别、信号生成、持仓管理。正因为简单,它的每一个环节都清晰可理解,是学习量化策略开发的最佳起点。
本文将带你完整实现一个均线交叉策略,从数据获取到绩效评估,并进行参数对比实验。建议你在阅读的同时,在自己的 Python 环境中运行每一行代码。
策略原理
什么是均线交叉
均线交叉策略的核心逻辑只有一句话:当短期均线上穿长期均线时买入(金叉),当短期均线下穿长期均线时卖出(死叉)。
这背后的经济学直觉是:
- 短期均线反映近期价格趋势,长期均线反映中长期趋势
- 短期均线上穿长期均线(金叉),说明短期趋势开始强于长期趋势,价格可能进入上升通道
- 短期均线下穿长期均线(死叉),说明短期趋势开始弱于长期趋势,价格可能进入下降通道
策略参数
均线交叉策略只有两个参数:
| 参数 | 含义 | 常见取值 |
|---|---|---|
fast_period | 短期均线周期 | 5、10、20 |
slow_period | 长期均线周期 | 20、30、60、120 |
参数选择的原则:fast_period < slow_period,且两者差距不能太小(否则信号太频繁)也不能太大(否则信号太滞后)。
第一步:数据获取
我们以沪深300成分股为例,获取足够长的历史数据来确保回测的统计意义:
python
import akshare as ak
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 获取沪深300ETF(510300)的日线数据
# 使用ETF而非个股,避免个股特异性和退市风险
df = ak.fund_etf_hist_em(
symbol="510300",
period="daily",
start_date="20180101",
end_date="20241231",
adjust="qfq"
)
df = df.rename(columns={
"日期": "Date", "开盘": "Open", "收盘": "Close",
"最高": "High", "最低": "Low", "成交量": "Volume"
})
df["Date"] = pd.to_datetime(df["Date"])
df = df.set_index("Date")
df = df[["Open", "High", "Low", "Close", "Volume"]]
print(f"数据范围: {df.index[0].date()} ~ {df.index[-1].date()}")
print(f"总交易日: {len(df)}")
print(f"起止价格: {df['Close'].iloc[0]:.3f} → {df['Close'].iloc[-1]:.3f}")
print(df.describe())为什么用 ETF 而不是个股? ETF 代表一篮子股票的表现,能有效分散个股风险。沪深300ETF 跟踪沪深300指数,涵盖A股市值最大的300家公司,是A股市场的核心宽基指数。
第二步:计算均线
python
def calc_moving_averages(df, fast_period, slow_period):
"""
计算短期和长期移动均线
参数:
df: DataFrame,包含 Close 列
fast_period: int,短期均线周期
slow_period: int,长期均线周期
返回:
DataFrame,新增 MA_fast 和 MA_slow 列
"""
df = df.copy()
df["MA_fast"] = df["Close"].rolling(window=fast_period).mean()
df["MA_slow"] = df["Close"].rolling(window=slow_period).mean()
return df
# 计算 10日 和 30日 均线
df = calc_moving_averages(df, fast_period=10, slow_period=30)
print(df[["Close", "MA_fast", "MA_slow"]].tail(10))可视化均线
python
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False
fig, ax = plt.subplots(figsize=(14, 6))
ax.plot(df.index, df["Close"], color="gray", alpha=0.5, label="收盘价")
ax.plot(df.index, df["MA_fast"], color="red", linewidth=1.2, label="MA10(短期)")
ax.plot(df.index, df["MA_slow"], color="blue", linewidth=1.2, label="MA30(长期)")
ax.set_title("沪深300ETF - 均线走势")
ax.set_ylabel("价格(元)")
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()第三步:生成买卖信号
信号生成的核心逻辑:检测均线交叉点。
python
def generate_signals(df):
"""
根据均线交叉生成买卖信号
信号定义:
1 = 金叉买入(短期均线从下方穿越长期均线)
-1 = 死叉卖出(短期均线从上方穿越长期均线)
0 = 无信号
返回:
DataFrame,新增 signal 列
"""
df = df.copy()
# 判断当日短期均线与长期均线的关系
df["fast_above_slow"] = df["MA_fast"] > df["MA_slow"]
# 检测交叉:当日关系与前一日关系不同
df["crossover"] = df["fast_above_slow"] != df["fast_above_slow"].shift(1)
# 金叉:从"短期<长期"变为"短期>长期"
df["signal"] = 0
df.loc[
(df["fast_above_slow"] == True) &
(df["fast_above_slow"].shift(1) == False),
"signal"
] = 1 # 金叉
# 死叉:从"短期>长期"变为"短期<长期"
df.loc[
(df["fast_above_slow"] == False) &
(df["fast_above_slow"].shift(1) == True),
"signal"
] = -1 # 死叉
# 移除均线未计算完成的初始行
df = df.dropna(subset=["MA_fast", "MA_slow"])
return df
df = generate_signals(df)
# 查看信号统计
buy_count = (df["signal"] == 1).sum()
sell_count = (df["signal"] == -1).sum()
print(f"金叉买入信号: {buy_count} 次")
print(f"死叉卖出信号: {sell_count} 次")
print(f"\n最近5次信号:")
print(df[df["signal"] != 0][["Close", "MA_fast", "MA_slow", "signal"]].tail(5))理解信号生成
需要注意一个重要细节:信号在收盘时确认,交易在次日开盘时执行。 这是因为均线是用收盘价计算的,只有收盘后才能确定当天是否发生了交叉。这种"当日确认、次日执行"的方式更接近实际交易。
第四步:回测
回测是用历史数据模拟策略执行,计算策略的实际收益和风险指标。
完整回测引擎
python
def backtest(df, initial_capital=100000, commission_rate=0.0003, slippage=0.001):
"""
均线交叉策略回测引擎
参数:
df: DataFrame,包含信号和价格数据
initial_capital: float,初始资金
commission_rate: float,佣金费率(双边),默认万三
slippage: float,滑点比例,默认千分之一
返回:
dict,包含交易记录、净值曲线和绩效指标
"""
capital = initial_capital
position = 0 # 持仓数量(股数)
entry_price = 0 # 买入价格
trades = [] # 交易记录
portfolio_values = [] # 每日净值
for i in range(len(df)):
date = df.index[i]
close = df["Close"].iloc[i]
signal = df["signal"].iloc[i]
# 获取次日开盘价作为成交价
# 这里简化处理:用当日收盘价近似
# 实际中应使用次日开盘价
if i + 1 < len(df):
next_open = df["Open"].iloc[i + 1]
else:
next_open = close
# 执行交易
if signal == 1 and position == 0:
# 金叉买入
buy_price = next_open * (1 + slippage)
shares = int(capital / (buy_price * (1 + commission_rate)))
shares = shares // 100 * 100 # 凑整百
if shares > 0:
cost = shares * buy_price * (1 + commission_rate)
capital -= cost
position = shares
entry_price = buy_price
trades.append({
"date": date,
"action": "BUY",
"price": round(buy_price, 3),
"shares": shares,
"cost": round(cost, 2)
})
elif signal == -1 and position > 0:
# 死叉卖出
sell_price = next_open * (1 - slippage)
revenue = position * sell_price * (1 - commission_rate)
profit = revenue - position * entry_price
profit_rate = profit / (position * entry_price)
capital += revenue
trades.append({
"date": date,
"action": "SELL",
"price": round(sell_price, 3),
"shares": position,
"revenue": round(revenue, 2),
"profit": round(profit, 2),
"profit_rate": round(profit_rate * 100, 2)
})
position = 0
# 记录每日净值
portfolio_value = capital + position * close
portfolio_values.append({
"date": date,
"portfolio_value": portfolio_value
})
# 计算绩效
values_df = pd.DataFrame(portfolio_values).set_index("date")
final_value = values_df["portfolio_value"].iloc[-1]
return {
"trades": trades,
"values": values_df,
"final_value": final_value,
"total_return": (final_value / initial_capital - 1) * 100
}
# 运行回测
result = backtest(df)
print(f"初始资金: 100,000 元")
print(f"最终资产: {result['final_value']:,.0f} 元")
print(f"总收益率: {result['total_return']:.2f}%")
print(f"交易次数: {len(result['trades'])} 笔")第五步:绩效评估
核心指标计算
python
def evaluate_performance(result, initial_capital=100000):
"""
计算策略的综合绩效指标
参数:
result: backtest 函数的返回值
initial_capital: 初始资金
返回:
dict,各项绩效指标
"""
values = result["values"]["portfolio_value"]
# 计算每日收益率
daily_returns = values.pct_change().dropna()
# 总收益率
total_return = (values.iloc[-1] / initial_capital - 1)
# 年化收益率
trading_days = len(values)
years = trading_days / 252 # 一年约252个交易日
annual_return = (1 + total_return) ** (1 / years) - 1
# 最大回撤
cummax = values.cummax()
drawdown = (values - cummax) / cummax
max_drawdown = drawdown.min()
# 夏普比率(假设无风险利率为3%)
risk_free_rate = 0.03 / 252 # 日化无风险利率
excess_returns = daily_returns - risk_free_rate
sharpe_ratio = np.sqrt(252) * excess_returns.mean() / excess_returns.std() if excess_returns.std() > 0 else 0
# 交易统计
trades = result["trades"]
sell_trades = [t for t in trades if t["action"] == "SELL"]
win_trades = [t for t in sell_trades if t.get("profit", 0) > 0]
win_rate = len(win_trades) / len(sell_trades) if sell_trades else 0
profits = [t["profit_rate"] for t in sell_trades if t.get("profit_rate") is not None]
avg_profit = np.mean(profits) if profits else 0
avg_win = np.mean([p for p in profits if p > 0]) if any(p > 0 for p in profits) else 0
avg_loss = abs(np.mean([p for p in profits if p < 0])) if any(p < 0 for p in profits) else 0
profit_loss_ratio = avg_win / avg_loss if avg_loss > 0 else float('inf')
metrics = {
"总收益率": f"{total_return * 100:.2f}%",
"年化收益率": f"{annual_return * 100:.2f}%",
"最大回撤": f"{max_drawdown * 100:.2f}%",
"夏普比率": f"{sharpe_ratio:.2f}",
"交易次数(买卖对)": f"{len(sell_trades)}",
"胜率": f"{win_rate * 100:.1f}%",
"平均每笔收益": f"{avg_profit:.2f}%",
"盈亏比": f"{profit_loss_ratio:.2f}",
"回测年数": f"{years:.1f}"
}
return metrics
# 评估策略表现
metrics = evaluate_performance(result)
print("=== 策略绩效 ===")
for k, v in metrics.items():
print(f" {k}: {v}")与基准对比
python
# 计算买入持有策略的收益(作为基准)
buy_hold_return = (df["Close"].iloc[-1] / df["Close"].iloc[0] - 1) * 100
print(f"\n买入持有收益率: {buy_hold_return:.2f}%")
print(f"策略收益率: {result['total_return']:.2f}%")
if result['total_return'] > buy_hold_return:
print("策略跑赢基准")
else:
print("策略跑输基准 — 这并不意味着策略没有价值")重要提醒:均线交叉策略在震荡市中表现不佳,会产生大量虚假信号(频繁买卖),每次交易都会被手续费侵蚀。它的优势在于捕捉大趋势。因此,在某些时期跑输基准是完全正常的。
第六步:参数对比实验
不同参数组合对策略表现的影响很大。下面我们对比三组常用参数:
| 组合 | 短期均线 | 长期均线 | 特点 |
|---|---|---|---|
| 5/20 | 5日 | 20日 | 信号灵敏,交易频繁 |
| 10/30 | 10日 | 30日 | 均衡型,本文默认参数 |
| 20/60 | 20日 | 60日 | 信号稀少,适合大趋势 |
python
def run_ma_crossover(df, fast_period, slow_period, initial_capital=100000):
"""
运行完整均线交叉策略(数据获取→计算均线→生成信号→回测)
"""
# 计算
df_copy = calc_moving_averages(df[["Open", "High", "Low", "Close", "Volume"]].copy(),
fast_period, slow_period)
df_copy = generate_signals(df_copy)
result = backtest(df_copy, initial_capital)
metrics = evaluate_performance(result, initial_capital)
return {
"params": f"MA{fast_period}/MA{slow_period}",
"result": result,
"metrics": metrics
}
# 获取原始数据
raw_df = ak.fund_etf_hist_em(
symbol="510300", period="daily",
start_date="20180101", end_date="20241231", adjust="qfq"
)
raw_df = raw_df.rename(columns={
"日期": "Date", "开盘": "Open", "收盘": "Close",
"最高": "High", "最低": "Low", "成交量": "Volume"
})
raw_df["Date"] = pd.to_datetime(raw_df["Date"])
raw_df = raw_df.set_index("Date")
raw_df = raw_df[["Open", "High", "Low", "Close", "Volume"]]
# 三组参数对比
param_sets = [
{"fast_period": 5, "slow_period": 20},
{"fast_period": 10, "slow_period": 30},
{"fast_period": 20, "slow_period": 60},
]
results = []
for params in param_sets:
r = run_ma_crossover(raw_df, **params)
results.append(r)
print(f"\n=== {r['params']} ===")
for k, v in r["metrics"].items():
print(f" {k}: {v}")参数对比可视化
python
fig, axes = plt.subplots(2, 1, figsize=(14, 10), height_ratios=[2, 1])
# 净值曲线对比
ax1 = axes[0]
colors = ["red", "blue", "green"]
for r, color in zip(results, colors):
ax1.plot(
r["result"]["values"].index,
r["result"]["values"]["portfolio_value"],
color=color,
linewidth=1.2,
label=r["params"]
)
# 添加买入持有基准线
benchmark = raw_df["Close"] / raw_df["Close"].iloc[0] * 100000
ax1.plot(benchmark.index, benchmark.values, color="gray",
linewidth=1, linestyle="--", label="买入持有")
ax1.set_title("均线交叉策略 - 参数对比(净值曲线)", fontsize=14)
ax1.set_ylabel("资产净值(元)")
ax1.legend()
ax1.grid(True, alpha=0.3)
# 回撤对比
ax2 = axes[1]
for r, color in zip(results, colors):
values = r["result"]["values"]["portfolio_value"]
cummax = values.cummax()
drawdown = (values - cummax) / cummax * 100
ax2.fill_between(drawdown.index, drawdown.values, 0,
color=color, alpha=0.3, label=r["params"])
ax2.set_title("回撤对比", fontsize=12)
ax2.set_ylabel("回撤(%)")
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()不同市场环境下的表现
均线交叉策略的表现高度依赖市场环境:
趋势行情(策略优势期)
- 单边上涨:策略能跟上大部分涨幅,金叉后持有直到死叉才卖出
- 单边下跌:策略能及时清仓回避,死叉信号帮助避开大部分跌幅
震荡行情(策略劣势期)
- 横盘震荡:均线反复交叉,产生大量虚假信号。每次金叉买入后价格回落,死叉卖出后价格又上涨,频繁亏损
- 这种现象被称为**"均线缠绕"**,是均线策略最大的弱点
用数据验证
python
# 将回测期划分为不同市场阶段
# 这里简化为按年划分
raw_df["year"] = raw_df.index.year
print("=== 各年度策略表现 ===")
print(f"{'年份':<8} {'5/20':<12} {'10/30':<12} {'20/60':<12}")
print("-" * 44)
for year in sorted(raw_df["year"].unique()):
year_data = raw_df[raw_df["year"] == year]
if len(year_data) < 30: # 数据太少跳过
continue
row = f"{year:<8}"
for params in param_sets:
r = run_ma_crossover(year_data, **params)
row += f" {r['metrics']['总收益率']:<12}"
print(row)通过分年度数据,可以清晰地看到策略在不同市场环境下的表现差异。这有助于判断策略是否适合当前市场环境。
改进思路
基础均线交叉策略有明显的局限性,以下是几个实用的改进方向:
1. 添加过滤器:减少虚假信号
python
def generate_signals_with_filter(df, trend_period=60):
"""
添加趋势过滤器的信号生成
仅在长期趋势向上时才执行金叉买入
避免在下降趋势中的反弹金叉买入
"""
df = df.copy()
# 计算趋势判断均线
df["MA_trend"] = df["Close"].rolling(window=trend_period).mean()
# 基础信号
df = generate_signals(df)
# 过滤:只在收盘价 > 趋势均线时才执行买入
df.loc[
(df["signal"] == 1) & (df["Close"] < df["MA_trend"]),
"signal"
] = 0 # 取消在下跌趋势中的金叉买入信号
return df2. 用 EMA 替代 SMA
指数移动平均(EMA)比简单移动平均(SMA)对近期价格更敏感,信号更及时:
python
# 将 SMA 替换为 EMA
df["MA_fast"] = df["Close"].ewm(span=fast_period, adjust=False).mean()
df["MA_slow"] = df["Close"].ewm(span=slow_period, adjust=False).mean()3. 加入止损机制
python
# 在回测引擎中加入止损逻辑
max_loss = -0.05 # 单笔最大亏损5%
if position > 0:
current_loss = (close - entry_price) / entry_price
if current_loss <= max_loss:
# 触发止损
sell_price = close * (1 - slippage)
revenue = position * sell_price * (1 - commission_rate)
capital += revenue
position = 04. 多时间周期确认
在日线出现金叉时,检查周线是否也处于上升趋势。只有多周期共振时才执行交易:
python
# 简化示例:要求 MA20 也在上升
df["MA_slow_rising"] = df["MA_slow"] > df["MA_slow"].shift(5)
df.loc[
(df["signal"] == 1) & (df["MA_slow_rising"] == False),
"signal"
] = 05. 加入成交量确认
python
# 金叉时要求成交量放大
df["vol_ma5"] = df["Volume"].rolling(5).mean()
df["vol_surge"] = df["Volume"] > df["vol_ma5"] * 1.2
df.loc[
(df["signal"] == 1) & (df["vol_surge"] == False),
"signal"
] = 0以上改进可以单独使用,也可以组合使用。但要注意:每增加一个过滤条件,都会减少交易次数。如果过滤得太严格,可能会错过真正的大趋势。改进的目标是提高信号质量,而不是完全消除亏损——那是不可能的。
策略的局限性
在结束之前,需要坦诚地指出均线交叉策略的几个固有局限:
滞后性:均线是基于历史价格计算的,任何基于均线的信号都必然滞后于实际价格转折点。参数越大的均线,滞后越严重。
震荡市磨损:在横盘震荡市场中,策略会产生大量虚假信号,每次交易的手续费和滑点会持续侵蚀资金。这是均线策略最大的成本。
不适用于所有股票:均线策略适合流动性好、趋势性强的标的(如宽基ETF),不适合波动剧烈的小盘股或庄股。
参数敏感性:不同参数在不同市场环境下表现差异巨大,不存在"最优参数"。今天回测最好的参数,明天可能就不好用了。
单独使用风险较高:均线交叉策略最好与其他指标(如成交量、RSI、MACD)结合使用,形成综合判断。
小结
本文完整实现了均线交叉策略的开发流程:
- 数据获取:使用 akshare 获取沪深300ETF 的历史数据
- 计算均线:用 pandas 的 rolling 方法计算简单移动平均
- 生成信号:检测均线交叉点,金叉买入、死叉卖出
- 回测:模拟策略执行,考虑了手续费和滑点
- 绩效评估:计算总收益、最大回撤、夏普比率等核心指标
- 参数对比:三组参数的表现对比和可视化
- 改进思路:趋势过滤、EMA替代、止损机制等方向
均线交叉策略虽然简单,但它教会了我们量化策略开发的核心方法论。掌握了这套流程,你就可以将同样的方法应用到更复杂的策略上。
免责声明:本文所有代码和策略仅用于技术教学目的,不构成任何投资建议。均线交叉策略存在固有局限性,在实盘交易中可能产生亏损。投资有风险,入市需谨慎。