用Python重塑经典策略:移动平均线交叉(基础篇)
📘

用Python重塑经典策略:移动平均线交叉(基础篇)

第 1/3 篇

用 Python 重写均线交叉策略

均线交叉是 MT5 里最常被验证的入门系统:快线向上穿过慢线做多,向下穿过做空。原文作者在 2025 年 1 月 8 日发布的版本,在 MQL5 社区获得 1237 次浏览、3 条评论,说明这类基础逻辑仍有高频复盘需求。 用 Python 重塑而非直接在 MQL5 写 EA,好处是先脱离终端做历史回测,再决定要不要进 MT5 实盘验证。外汇与贵金属杠杆高,任何交叉信号都只是概率倾向,不能直接当作方向保证。 你可以先取一组 EURUSD 的 H1 收盘价,用 pandas 算 20 与 50 周期 SMA,打印出金叉死叉发生的 Bar 时间,对照 MT5 图表人工核对一遍。

「均线交叉策略还值得测吗」

不少经典策略诞生于手工盘口时代,放到如今算法主导的行情里,相关性要先打问号。移动平均线交叉是技术分析的旧核心:用一条短周期均线和一条长周期均线,短线上穿长线看作多头倾向,下穿看作空头倾向。 原文给出的样本图是 CADJPY 货币对上的均线交叉应用示例,说明该逻辑在外汇交叉盘上可直接肉眼核对。我们后续会用现代量化手段重测它的信号质量,而不是凭经验续命。 这一节先列清要回答的几个问题:有没有定量证据支撑继续用、相比裸价分析优势在哪、算法交易里是否仍有效、能否叠加其他指标提准、以及 AI 能否预测交叉发生。带着这几个点去开 MT5 拉历史,比盲信老教程更实在。

◍ 先拉四年 EURUSD 的 M15 再说

要把 MT5 和 Python 打通,第一步不是写策略,而是先把数据搬出来。这里直接取 2020-01-01 到 2024-06-25 的 EURUSD M15 周期,约 4.5 年、按 15 分钟一根算接近 1.6 万根 K 线,足够覆盖疫情波动、加息周期和常规震荡,样本不算小。 整个实验设了两个对标目标:一是直接预测下一根价格涨跌的准确率,拿它当基准线;二是预测两条均线是否发生交叉。如果后者没比前者强,那「均线交叉」这个套路大概率只是心理安慰。 模型跑完还有一层检查——让算法自己挑「哪些变量对预测交叉最关键」。若它根本不把那两条均线排进去,说明你原先假设的「均线互相牵引」可能不成立;排进去了,才值得继续往下搭模型。外汇和贵金属杠杆高,这类统计结论只代表历史样本倾向,实盘仍可能随时失效。

用 EURUSD 实测均线交叉的预测效力

拿 EURUSD 在 MT5 里跑一套数据实验,先看均线交叉对后市方向的指示是否还站得住。账户登录后读取 EURUSD 最小交易量,实测输出为 0.01,按 10 倍倍数下单即 0.1 手,这是后续回测仓位的基准。 数据取 2020-01-01 到当前的 M15 K 线,向前看 20 根(look_ahead=20)来标注信号:快线 SMA_5 在 20 根后位于慢线 SMA_50 之上记 1,之下记 0;同时用收盘价相对 20 根后的涨跌单独标一组 close_target 作对照。 模型不只用传统交叉,而是把 XGBoost、CatBoost、随机森林、LDA/QDA、逻辑回归、MLP 全拉进来,用 TimeSeriesSplit 按时间切训练集与测试集,避免未来函数污染。两组准确率分别存表:一组测「交叉信号方向」,一组测「直接价格变动」,对比才知道交叉到底有没有增量信息。 外汇与贵金属属高风险品种,历史标注准确率不代表未来概率,实盘前务必用自己经纪商数据重跑验证。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import pandas_ta as ta
class="kw">import numpy as np
class="kw">import MetaTrader5 as mt5
from class="type">class="kw">datetime class="kw">import class="type">class="kw">datetime
class="kw">import seaborn as sns
class="kw">import time
account = class="num">123436536
password = "Enter Your Password"
server = "Enter Your Broker"
if(mt5.initialize(login=account,password=password,server=server)):
    print("Logged in succesfully")
else:
    print("Failed to login")
timeframe = mt5.TIMEFRAME_M15
deviation = class="num">1000
volume = class="num">0
lot_multiple = class="num">10
symbol = "EURUSD"
class="macro">#Setup trading volume
symbols = mt5.symbols_get()
for index,symbol in enumerate(symbols):
    if symbol.name == "EURUSD":
        print(f"{symbol.name} has minimum volume: {symbol.volume_min}")
        volume = symbol.volume_min * lot_multiple
class="macro">#Specify date range of data to be modelled
date_start = class="type">class="kw">datetime(class="num">2020,class="num">1,class="num">1)
date_end = class="type">class="kw">datetime.now()
class="macro">#Define how far ahead we are looking
look_ahead = class="num">20
class="macro">#Fetch market data
market_data = pd.DataFrame(mt5.copy_rates_range("EURUSD",timeframe,date_start,date_end))
market_data["time"] = pd.to_datetime(market_data["time"],unit=&class="macro">#x27;s&class="macro">#x27;)
class="macro">#Add simple moving average technical indicator
market_data.ta.sma(length=class="num">5,append=True)
class="macro">#Add simple moving average technical indicator
market_data.ta.sma(length=class="num">50,append=True)
class="macro">#Delete missing rows
market_data.dropna(inplace=True)
class="macro">#Add a column for the target
market_data["target"] = class="num">0
market_data["close_target"] = class="num">0

class="macro">#Encoding the target
ma_cross_conditions = [
    (market_data["SMA_5"].shift(-look_ahead) > market_data["SMA_50"].shift(-look_ahead)),
    (market_data["SMA_5"].shift(-look_ahead) < market_data["SMA_50"].shift(-look_ahead))
]
class="macro">#Encoding pattern
ma_cross_choices = [
    class="macro">#Fast MA above Slow MA
    class="num">1,
    class="macro">#Fast MA below Slow MA
    class="num">0
]
price_conditions = [
    (market_data["close"] > market_data["close"].shift(-look_ahead)),
    (market_data["close"] < market_data["close"].shift(-look_ahead))
]
class="macro">#Encoding pattern
price_choices = [
    class="macro">#Price fell
    class="num">0,
    class="macro">#Price rose
    class="num">1
]
market_data["target"] = np.select(ma_cross_conditions,ma_cross_choices)
market_data["close_target"] = np.select(price_conditions,price_choices)
class="macro">#The last rows do not have answers
market_data = market_data[:-look_ahead]
market_data
class="macro">#XGBoost
from xgboost class="kw">import XGBClassifier
class="macro">#Catboost
from catboost class="kw">import CatBoostClassifier
class="macro">#Random forest
from sklearn.ensemble class="kw">import RandomForestClassifier
class="macro">#LDA and QDA
from sklearn.discriminant_analysis class="kw">import LinearDiscriminantAnalysis , QuadraticDiscriminantAnalysis
class="macro">#Logistic regression
from sklearn.linear_model class="kw">import LogisticRegression
class="macro">#Neural network
from sklearn.neural_network class="kw">import MLPClassifier
class="macro">#Time series split
from sklearn.model_selection class="kw">import TimeSeriesSplit
class="macro">#Accuracy metrics
from sklearn.metrics class="kw">import accuracy_score
class="macro">#Visualising performance
from sklearn.metrics class="kw">import confusion_matrix
class="kw">import seaborn as sns

「用时间序列切分验证均线交叉预测器」

这段 Python 片段演示了如何对移动平均线交叉目标做严格的时间外验证,而不是随机洗牌后报虚高准确率。核心用的是 TimeSeriesSplit,设了 10 折、gap 等于前瞻窗口 look_ahead,避免训练集泄漏未来信息。 预测列取了 open/high/low/close/tick_volume/spread 加上 SMA_5 与 SMA_50,目标字段是 target(即均线交叉后的方向变化)。模型只跑了小神经网络 MLPClassifier,隐藏层 (20,10)、lbfgs 求解、early_stopping 开启。 循环里逐折在 train 区间拟合,在 test 区间算 accuracy_score,结果写进 error_ma_crossover 表的第 8 列(索引 7,对应 Neural Network Small)。你在 MT5 导出的 tick 或分钟数据上重跑这套,能直接看到样本外准确率是否随折数衰减——若前几折 0.6 往后掉到 0.5 以下,说明过拟合严重,外汇与贵金属品种此类现象常见,杠杆交易高风险。

MQL5 / C++
class="kw">import matplotlib.pyplot as plt
from sklearn.model_selection class="kw">import learning_curve
class="macro">#Time series split
splits = class="num">10
gap = look_ahead
models = ["Logistic Regression","Linear Discriminant Analysis","Quadratic Discriminant Analysis","Random Forest Classifier","XGB Classifier","Cat Boost Classifier","Neural Network Small","Neural Network Large"]
error_ma_crossover = pd.DataFrame(index=np.arange(class="num">0,splits),columns=models)
error_price = pd.DataFrame(index=np.arange(class="num">0,splits),columns=models)
predictors = ["open","high","low","close","tick_volume","spread","SMA_5","SMA_50"]
tscv = TimeSeriesSplit(n_splits=splits,gap=gap)
class="macro">#Training each model to predict changes in the moving average cross over
for i,(train,test) in enumerate(tscv.split(market_data)):
    model = MLPClassifier(solver=&class="macro">#x27;lbfgs&class="macro">#x27;,alpha=class="num">1e-5,hidden_layer_sizes=(class="num">20, class="num">10), random_state=class="num">1,early_stopping=True)
    model.fit( market_data.loc[train[class="num">0]:train[-class="num">1],predictors] , market_data.loc[train[class="num">0]:train[-class="num">1],"target"] )
    error_ma_crossover.iloc[i,class="num">7] = accuracy_score(market_data.loc[test[class="num">0]:test[-class="num">1],"target"],model.predict(market_data.loc[test[class="num">0]:test[-class="num">1],predictors]))

◍ 交叉信号比裸价预测更值得信

直接对价格变动做分类预测时,模型整体表现疲软。逻辑回归到深度神经网络按复杂度从左排到右,准确率甚至有多款低于 50%,和随机抛硬币的差距并不大;复杂度上升也没有带来精度改善,说明端到端猜价格在小周期上概率倾向不可靠。 换成交叉信号做目标后,线性判别分析(LDA)在图4里大幅甩开其余模型,成为所有被测模型里最稳的一个。和它在直接预测任务里的糟糕成绩摆在一起,能看出预测均线交叉可能比直接押价格变动更靠谱,这种信号层面的抽象削弱了噪声干扰。 下面这段是训练各模型去拟合收盘价变动目标的循环骨架,用 MLP 分类器在时序切分上跑交叉验证,把每段测试集准确率写回 error_price 第8列。 [CODE] #Training each model to predict changes <span class="keyword">in</span> the close price <span class="keyword">for</span> i,(train,test) <span class="keyword">in</span> enumerate(tscv.split(market_data)): &nbsp;&nbsp;&nbsp;&nbsp;model = MLPClassifier(solver=<span class="string">'lbfgs'</span>,alpha=<span class="number">1</span>e-<span class="number">5</span>,hidden_layer_sizes=(<span class="number">20</span>, <span class="number">10</span>), random_state=<span class="number">1</span>,early_stopping=True) &nbsp;&nbsp;&nbsp;&nbsp;model.fit( market_data.loc[train[<span class="number">0</span>]:train[-<span class="number">1</span>],predictors] , market_data.loc[train[<span class="number">0</span>]:train[-<span class="number">1</span>],<span class="string">"close_target"</span>] ) &nbsp;&nbsp;&nbsp;&nbsp;error_price.iloc[i,<span class="number">7</span>] = accuracy_score(market_data.loc[test[<span class="number">0</span>]:test[-<span class="number">1</span>],<span class="string">"close_target"</span>],model.predict(market_data.loc[test[<span class="number">0</span>]:test[-<span class="number">1</span>],predictors])) error_price error_ma_crossover [/CODE] 逐行拆解:第1行注释标明本段在训练预测收盘价变动的模型;第2行用 enumerate 配合 tscv 时序切分拿到每段训练集与测试集索引。第3到4行实例化一个双隐层(20,10)的 MLP 分类器,lbfgs 求解、早停开启,用 predictors 列拟合 close_target。第5行在测试段算准确率写进 error_price 第8列(索引7)。末尾两行输出价格误差表与均线交叉误差表,方便对比两者精度落差。 开 MT5 把这段逻辑接上自己的 EURUSD 小时数据,先跑 LDA 对均线交叉的命中率,再跑 MLP 对裸价变动的命中率,两者差值大概率能复现上面的结论。外汇与贵金属杠杆高,信号再稳也只代表概率倾向,仓位别压满。

MQL5 / C++
class="macro">#Training each model to predict changes <span class="keyword">in</span> the close price
<span class="keyword">for</span> i,(train,test) <span class="keyword">in</span> enumerate(tscv.split(market_data)):
&nbsp;&nbsp;&nbsp;&nbsp;model = MLPClassifier(solver=<span class="class="type">class="kw">string">&class="macro">#x27;lbfgs&class="macro">#x27;</span>,alpha=<span class="number">class="num">1</span>e-<span class="number">class="num">5</span>,hidden_layer_sizes=(<span class="number">class="num">20</span>, <span class="number">class="num">10</span>), random_state=<span class="number">class="num">1</span>,early_stopping=True)
&nbsp;&nbsp;&nbsp;&nbsp;model.fit( market_data.loc[train[<span class="number">class="num">0</span>]:train[-<span class="number">class="num">1</span>],predictors] , market_data.loc[train[<span class="number">class="num">0</span>]:train[-<span class="number">class="num">1</span>],<span class="class="type">class="kw">string">"close_target"</span>] ) 
&nbsp;&nbsp;&nbsp;&nbsp;error_price.iloc[i,<span class="number">class="num">7</span>] = accuracy_score(market_data.loc[test[<span class="number">class="num">0</span>]:test[-<span class="number">class="num">1</span>],<span class="class="type">class="kw">string">"close_target"</span>],model.predict(market_data.loc[test[<span class="number">class="num">0</span>]:test[-<span class="number">class="num">1</span>],predictors]))
error_price
error_ma_crossover

常见问题

基础只需 pandas 和 numpy 处理数据,matplotlib 画图;回测可加 backtrader 或直接用 pandas 向量化算信号。
四年 M15 约 1.3 万根 K 线,样本够初筛;但外汇高风险,需多品种多周期交叉验证才敢用实盘。
小布可接入你的品种页,自动拉数据、切分时间序列并输出交叉信号胜率,你只需看结论调参数。
实测交叉信号在 EURUSD M15 上方向胜率倾向高于裸价预测,因滤掉了噪声,但仍随行情 regime 衰减。
大概率过拟合了参数;用时间序列切分(如按年滚动)验证,样本外胜率掉一半以上就该降复杂度。