MT5 EA开发实战:从零开始构建一个趋势跟踪EA(附完整代码)

MT5 EA开发实战:从零开始构建一个趋势跟踪EA(附完整代码)
📌 前言
很多交易者都有一个"EA梦"——把自己的交易策略写成自动交易程序,让电脑24小时帮你赚钱。
但理想很丰满,现实很骨感。很多人刚开始学EA开发就被MQL5的语法和各种函数劝退了。要么就是写出来的EA回测看起来很美,实盘一用就亏。

▲ MT5策略测试器回测界面
作为一个做了多年EA定制的开发者,我可以很负责任地说:写一个能跑的EA不难,但写一个能稳定盈利的EA很难。
不过,难不等于不能学。今天这篇文章,我就带你从零开始构建一个完整的趋势跟踪EA。不求你看完就能写出盈利的EA,但求能帮你建立对EA开发的整体认知,少走一些弯路。
这篇文章的目标读者:
- 有一点MQL4/MQL5基础,但还没写过完整EA的新手
- 想了解EA开发流程的交易者
- 打算自己动手改EA的进阶用户
如果你完全零基础,可能需要先补一补MQL5的基础语法。
🎯 一、EA策略设计:先想清楚再动手

▲ 趋势跟踪EA策略逻辑流程图
写EA最忌讳的就是上来就敲代码。一个合格的EA,在写第一行代码之前,必须把策略逻辑想清楚。
我们今天要做的这个趋势跟踪EA,策略逻辑很简单:
核心策略逻辑
- **趋势判断:** 用200周期EMA判断大趋势
- 价格在EMA上方 → 只做多
- 价格在EMA下方 → 只做空
- **入场信号:** 用MACD交叉作为入场信号
- 上升趋势中,MACD线上穿信号线 → 做多
- 下降趋势中,MACD线下穿信号线 → 做空
- **止损设置:** 固定止损(比如30点)
- **止盈设置:** 固定止盈(比如60点),或者用追踪止损
- **仓位管理:** 固定手数,或者按资金百分比计算
为什么选这个策略?
- 逻辑清晰,容易理解和实现
- 趋势跟踪是最经典的策略类型之一
- 包含了EA开发中最常见的几个模块:
- 指标计算
- 趋势判断
- 信号生成
- 订单管理
- 风险管理
学会了这个,你就能在此基础上改出各种各样的趋势类EA。
📋 二、EA架构设计:搭好骨架再填肉

▲ EA三层模块化架构设计
一个结构清晰的EA,应该分成几个独立的模块。这样代码易读、易改、易维护。
我们的EA将分为以下几个模块:
// 1. 输入参数模块 // 所有可以调整的参数都放这里 // 2. 全局变量模块 // 需要在不同函数间共享的变量 // 3. 初始化函数 OnInit() // EA加载时执行一次 // 4. 主循环函数 OnTick() // 每个tick执行一次,核心逻辑都在这里 // 5. 指标计算函数 // 计算EMA、MACD等指标 // 6. 信号判断函数 // 判断是否满足开仓/平仓条件 // 7. 订单操作函数 // 开仓、平仓、修改止损止盈 // 8. 资金管理函数 // 计算开仓手数 // 9. 去初始化函数 OnDeinit() // EA卸载时执行一次
这是一个很标准的EA架构,大多数EA都可以套这个模板。
💻 三、代码实现:一步一步写

▲ MT5平台EA运行设置界面
好了,开始写代码。我会分段讲解,最后给出完整代码。
1. 输入参数
这是EA最"用户友好"的部分——用户可以在MT5里直接调整这些参数,不用改代码。
//+------------------------------------------------------------------+ //| 输入参数 | //+------------------------------------------------------------------+ input double LotSize = 0.1; // 交易手数 input int EMA_Period = 200; // EMA周期 input int MACD_FastEMA = 12; // MACD快线周期 input int MACD_SlowEMA = 26; // MACD慢线周期 input int MACD_Signal = 9; // MACD信号线周期 input int StopLoss = 30; // 止损(点数) input int TakeProfit = 60; // 止盈(点数) input bool UseTrailingStop = false; // 是否使用追踪止损 input int TrailingStop = 20; // 追踪止损距离(点数) input int MagicNumber = 12345; // EA魔术码(区分不同EA的订单) input int Slippage = 3; // 允许滑点(点数)
要点提示:
- 📝 MagicNumber非常重要,它是EA的"身份证"。每个EA用不同的魔术码,才不会互相干扰对方的订单。
- 📝 所有数值都要给一个合理的默认值,用户不修改也能直接用。
- 📝 参数注释要写清楚,方便用户理解每个参数是干嘛的。
2. 全局变量和初始化
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
int emaHandle; // EMA指标句柄
int macdHandle; // MACD指标句柄
double emaBuffer[]; // EMA数值缓冲区
double macdBuffer[]; // MACD主线缓冲区
double signalBuffer[];// MACD信号线缓冲区
//+------------------------------------------------------------------+
//| 初始化函数 |
//+------------------------------------------------------------------+
int OnInit()
{
// 创建EMA指标
emaHandle = iMA(_Symbol, _Period, EMA_Period, 0, MODE_EMA, PRICE_CLOSE);
if(emaHandle == INVALID_HANDLE)
{
Print("创建EMA指标失败,错误码:", GetLastError());
return(INIT_FAILED);
}
// 创建MACD指标
macdHandle = iMACD(_Symbol, _Period, MACD_FastEMA, MACD_SlowEMA,
MACD_Signal, PRICE_CLOSE);
if(macdHandle == INVALID_HANDLE)
{
Print("创建MACD指标失败,错误码:", GetLastError());
return(INIT_FAILED);
}
// 设置数组为时间序列(索引0是最新的bar)
ArraySetAsSeries(emaBuffer, true);
ArraySetAsSeries(macdBuffer, true);
ArraySetAsSeries(signalBuffer, true);
return(INIT_SUCCEEDED);
}
要点提示:
- 💡 在MQL5中,使用指标前需要先"创建"指标,获得一个句柄(handle)。
- 💡 `_Symbol`代表当前图表的品种,`_Period`代表当前周期。
- 💡 `ArraySetAsSeries`设置为true后,数组索引0就是最新的K线,索引1是前一根,以此类推。
- 💡 初始化失败一定要返回`INIT_FAILED`,这样MT5会显示初始化失败,不会乱开单。
3. 指标计算函数
每个tick我们都需要获取最新的指标数值。
//+------------------------------------------------------------------+
//| 更新指标数据 |
//+------------------------------------------------------------------+
bool UpdateIndicators()
{
// 复制EMA数据
int copied = CopyBuffer(emaHandle, 0, 0, 3, emaBuffer);
if(copied < 3)
{
Print("复制EMA数据失败");
return false;
}
// 复制MACD主线数据
copied = CopyBuffer(macdHandle, 0, 0, 3, macdBuffer);
if(copied < 3)
{
Print("复制MACD主线数据失败");
return false;
}
// 复制MACD信号线数据
copied = CopyBuffer(macdHandle, 1, 0, 3, signalBuffer);
if(copied < 3)
{
Print("复制MACD信号线数据失败");
return false;
}
return true;
}
要点提示:
- ⚠️ `CopyBuffer`可能返回小于你请求的数量,尤其是在新K线形成的时候。一定要检查返回值。
- ⚠️ 我们只需要最近3根K线的数据就够了(当前K线+前两根),不用复制太多,浪费性能。
4. 信号判断函数
这是EA的"大脑",决定什么时候开仓、什么时候平仓。
//+------------------------------------------------------------------+
//| 判断做多信号 |
//+------------------------------------------------------------------+
bool IsBuySignal()
{
// 趋势判断:价格在EMA上方
bool isUptrend = (Close[1] > emaBuffer[1]);
// MACD金叉:前前根MACD线在信号线下,前一根MACD线上穿信号线
bool macdCrossUp = (macdBuffer[2] < signalBuffer[2]) &&
(macdBuffer[1] > signalBuffer[1]);
return isUptrend && macdCrossUp;
}
//+------------------------------------------------------------------+
//| 判断做空信号 |
//+------------------------------------------------------------------+
bool IsSellSignal()
{
// 趋势判断:价格在EMA下方
bool isDowntrend = (Close[1] < emaBuffer[1]);
// MACD死叉:前前根MACD线在信号线上,前一根MACD线下穿信号线
bool macdCrossDown = (macdBuffer[2] > signalBuffer[2]) &&
(macdBuffer[1] < signalBuffer[1]);
return isDowntrend && macdCrossDown;
}
要点提示:
- 🎯 注意我们用的是索引1(前一根K线)的数据来判断信号,而不是索引0(当前K线)。
- 🎯 为什么?因为当前K线还没走完,信号可能会反复变化。用已收盘的K线来判断,信号才是确定的。
- 🎯 这是新手最容易踩的坑之一——用当前K线判断信号,回测看起来很准,实盘根本赚不到钱。
5. 订单操作函数
这部分是EA的"手",负责实际的买卖操作。
//+------------------------------------------------------------------+
//| 开多单 |
//+------------------------------------------------------------------+
bool OpenBuy()
{
// 检查是否已有同方向订单
if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
return false;
double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double sl = price - StopLoss * _Point;
double tp = price + TakeProfit * _Point;
MqlTradeRequest request = {0};
MqlTradeResult result = {0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = LotSize;
request.type = ORDER_TYPE_BUY;
request.price = price;
request.sl = sl;
request.tp = tp;
request.deviation= Slippage;
request.magic = MagicNumber;
request.comment = "Trend Following EA";
if(!OrderSend(request, result))
{
Print("开多单失败,错误码:", result.retcode);
return false;
}
Print("开多单成功,订单号:", result.order);
return true;
}
//+------------------------------------------------------------------+
//| 开空单 |
//+------------------------------------------------------------------+
bool OpenSell()
{
// 检查是否已有同方向订单
if(PositionSelect(_Symbol) && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
return false;
double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = price + StopLoss * _Point;
double tp = price - TakeProfit * _Point;
MqlTradeRequest request = {0};
MqlTradeResult result = {0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = LotSize;
request.type = ORDER_TYPE_SELL;
request.price = price;
request.sl = sl;
request.tp = tp;
request.deviation= Slippage;
request.magic = MagicNumber;
request.comment = "Trend Following EA";
if(!OrderSend(request, result))
{
Print("开空单失败,错误码:", result.retcode);
return false;
}
Print("开空单成功,订单号:", result.order);
return true;
}
要点提示:
- ⚠️ MT5的订单系统和MT4不一样。MT5用`PositionSelect`检查持仓,用`TRADE_ACTION_DEAL`成交。
- ⚠️ 开仓前一定要检查是否已经有同方向的持仓,避免重复开仓。
- ⚠️ `_Point`是品种的最小价格变动单位,用它来计算点数,才能在不同品种上都正确运行。
- ⚠️ 一定要设置合理的滑点(deviation),否则行情波动大的时候可能开不了仓。
6. 追踪止损函数
如果开启了追踪止损,我们需要在持仓盈利时不断上移止损。
//+------------------------------------------------------------------+
//| 更新追踪止损 |
//+------------------------------------------------------------------+
void UpdateTrailingStop()
{
if(!UseTrailingStop) return;
// 检查是否有持仓
if(!PositionSelect(_Symbol)) return;
// 只处理本EA的持仓
if((int)PositionGetInteger(POSITION_MAGIC) != MagicNumber) return;
double currentSL = PositionGetDouble(POSITION_SL);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
// 多单:价格上涨时上移止损
double newSL = SymbolInfoDouble(_Symbol, SYMBOL_BID) -
TrailingStop * _Point;
// 如果新止损比当前止损高,就更新
if(newSL > currentSL || currentSL == 0)
{
ModifySLTP(newSL, PositionGetDouble(POSITION_TP));
}
}
else // 空单
{
// 空单:价格下跌时下移止损
double newSL = SymbolInfoDouble(_Symbol, SYMBOL_ASK) +
TrailingStop * _Point;
// 如果新止损比当前止损低,就更新
if(newSL < currentSL || currentSL == 0)
{
ModifySLTP(newSL, PositionGetDouble(POSITION_TP));
}
}
}
//+------------------------------------------------------------------+
//| 修改止损止盈 |
//+------------------------------------------------------------------+
bool ModifySLTP(double sl, double tp)
{
MqlTradeRequest request = {0};
MqlTradeResult result = {0};
request.action = TRADE_ACTION_SLTP;
request.symbol = _Symbol;
request.sl = sl;
request.tp = tp;
request.magic = MagicNumber;
if(!OrderSend(request, result))
{
Print("修改止损止盈失败,错误码:", result.retcode);
return false;
}
return true;
}
要点提示:
- 📈 追踪止损的逻辑:多单盈利时,止损跟着价格往上移;空单盈利时,止损跟着价格往下移。
- 📈 注意判断条件:只有新的止损比当前止损"更好"的时候才更新。多单是"更高",空单是"更低"。
- 📈 追踪止损不能代替止损本身,它只是在盈利的时候保护利润。
7. 主循环 OnTick()
把所有模块串起来,每个tick执行一次。
//+------------------------------------------------------------------+
//| 主循环函数 |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. 更新指标数据
if(!UpdateIndicators()) return;
// 2. 检查是否有新K线(只在新K线形成时判断信号,避免反复交易)
static datetime lastBarTime = 0;
if(Time[0] == lastBarTime)
{
// 同一根K线内,只更新追踪止损,不判断新信号
UpdateTrailingStop();
return;
}
lastBarTime = Time[0];
// 3. 更新追踪止损
UpdateTrailingStop();
// 4. 判断交易信号
if(IsBuySignal())
{
// 有做多信号,先平掉空单(如果有的话),再开多单
if(PositionSelect(_Symbol) &&
PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
ClosePosition();
}
OpenBuy();
}
else if(IsSellSignal())
{
// 有做空信号,先平掉多单(如果有的话),再开空单
if(PositionSelect(_Symbol) &&
PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
ClosePosition();
}
OpenSell();
}
}
要点提示:
- ⏰ 这里的关键设计是"只在新K线时判断信号"。这样可以避免同一个信号反复触发,也避免了K线未收盘时信号不稳定的问题。
- ⏰ 同一根K线内,我们只更新追踪止损,不判断新信号。这是一个非常实用的设计。
- ⏰ `static`变量在函数调用之间会保留值,正好用来记录上一根K线的时间。
8. 平仓函数和去初始化
//+------------------------------------------------------------------+
//| 平仓当前持仓 |
//+------------------------------------------------------------------+
bool ClosePosition()
{
if(!PositionSelect(_Symbol)) return true;
double price;
ENUM_ORDER_TYPE orderType;
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
orderType = ORDER_TYPE_SELL;
}
else
{
price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
orderType = ORDER_TYPE_BUY;
}
MqlTradeRequest request = {0};
MqlTradeResult result = {0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = PositionGetDouble(POSITION_VOLUME);
request.type = orderType;
request.price = price;
request.position = PositionGetInteger(POSITION_TICKET);
request.deviation= Slippage;
request.magic = MagicNumber;
if(!OrderSend(request, result))
{
Print("平仓失败,错误码:", result.retcode);
return false;
}
Print("平仓成功,订单号:", result.order);
return true;
}
//+------------------------------------------------------------------+
//| 去初始化函数 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
// 释放指标句柄
if(emaHandle != INVALID_HANDLE)
IndicatorRelease(emaHandle);
if(macdHandle != INVALID_HANDLE)
IndicatorRelease(macdHandle);
}
要点提示:
- ♻️ 用完的指标句柄一定要释放,否则会造成资源泄漏。
- ♻️ `OnDeinit`在EA被移除、图表切换、周期切换时都会被调用。
🧪 四、回测与优化:EA写好了只是开始
EA写好了,不代表就能赚钱了。接下来才是重头戏:回测和优化。
回测注意事项
- **数据质量很重要**
- 尽量用高质量的历史数据
- 至少回测3-5年的数据
- 包含不同的市场环境(牛市、熊市、震荡市)
- **参数不要过度优化**
- 不要为了拟合历史数据而把参数调得太极端
- 保持参数的逻辑性和合理性
- 建议用样本内优化,样本外验证
- **注意滑点和手续费**
- 回测时一定要设置合理的滑点和手续费
- 实际交易中的成本会比回测高,保守一点没坏处
- **关注风险指标,不要只看收益率**
- 最大回撤:最好控制在20%以内
- 夏普比率:越高越好,至少要大于1
- 盈亏比:至少要1.5以上
- 胜率:趋势策略40%左右就不错了
常见的回测陷阱
⚠️ 过度拟合:参数调得太"完美",只适合历史数据,实盘就亏。
⚠️ 未来函数:用了未来的数据来判断过去的信号,回测超准,实盘没用。
⚠️ 偷价:在K线收盘价入场,但实际交易中你不可能刚好在收盘价成交。
⚠️ 忽略点差:有些品种点差很大,尤其是交叉盘,频繁交易成本很高。
💡 五、进阶优化方向
这个基础版EA只是一个起点。如果你想让它变得更好,可以从以下几个方向优化:
1. 资金管理优化
- 按账户资金百分比计算仓位(固定 fractional 模式)
- 凯利公式计算最优仓位
- 根据波动率动态调整仓位(ATR仓位管理)
2. 入场过滤优化
- 增加波动率过滤(震荡市减少交易)
- 增加多时间框架确认(大周期看涨+小周期入场)
- 增加成交量过滤(突破要有量能配合)
- 避开重大新闻事件(非农、利率决议等)
3. 出场策略优化
- 移动止盈(盈利到一定程度后收紧止盈)
- 时间止损(持仓多久没盈利就平仓)
- 反向信号平仓(出现反向信号就平仓)
- 多策略组合出场
4. 风险控制优化
- 每日最大亏损限制
- 最大持仓数量限制
- 连续亏损后降低仓位
- 账户最大回撤限制
⚠️ 六、重要提醒
最后,作为一个做了多年EA开发的"老司机",给你几个忠告:
1. 没有完美的EA
任何策略都有失效的时候,不要相信什么"永久盈利""年化100%"的鬼话。
2. 回测好不代表实盘好
回测只是起点,实盘表现通常会比回测差30%-50%,这是正常的。
3. 不要过度优化
参数调来调去,最后调出来的只是最适合历史数据的参数,不是最适合未来的。
4. 做好资金管理
这是唯一你能完全控制的事情。再好用的EA,仓位太重也会爆仓。
5. 持续监控和维护
市场在变,策略也会失效。定期检查EA的表现,必要时调整参数甚至暂停使用。
📝 完整代码
由于篇幅原因,完整代码我整理成了独立文件。如果你需要,可以关注我的视频号,私信"趋势EA"获取。
当然,更建议你照着这篇文章自己写一遍——只有自己敲过的代码,才是真正理解了的。
💡 **最后想说的话**
EA不是圣杯,但它是交易者的好帮手。它能帮你克服人性的弱点,严格执行交易纪律,24小时监控市场。
但记住:**EA只是工具,真正决定交易成败的,还是工具背后的人。**
希望这篇文章能帮你在EA开发的路上少走一点弯路。如果你觉得有帮助,欢迎分享给更多的交易者朋友。
🎬 关注晓辉编程视频号
MT4/MT5 EA开发实战 | 交易策略分享 | 编程技巧干货
微信搜索「晓辉编程」
💬 添加晓辉为好友
免费获取交易工具包 | 一对一EA定制咨询 | 加入交易者交流群
微信号:XiaoHuiProgramming
分享海报
分享:
🎬 关注晓辉编程视频号
MT4/MT5 EA开发实战 | 交易策略分享 | 编程技巧干货

微信搜索:晓辉编程
💬 添加晓辉为好友
一对一交流EA开发 | 定制需求咨询 | 进技术交流群

微信号:XiaoHuiProgramming











