环森编程 首页 Java Python MQL4 教程 MT4 EA 编写 站点地图
首页 › MQL4 教程 › MT4 EA 编写教程

MT4 EA 编写教程:从零写一个自动交易 EA

这篇教程不讲空概念,直接带你写一个能编译、能回测的 MT4 EA:先搭 OnInit / OnTick / OnDeinit 骨架,再写均线交叉入场、OrderSend 下单、止损止盈和订单池保护,最后用策略测试器做回测与参数优化。文中所有代码都是完整的 MQL4 源码,可以复制进 MetaEditor 按 F7 编译。适合正在搜「MT4 EA 编写教程」「EA 自动交易系统怎么写」、想自己动手而不是买现成 EA 的读者。

最后更新 2026-11-03 · 约 6,000 字 · 附完整源码 · 浏览器里也能练 MQL4

先看均线交叉 EA 实战 → MQL4 教程总目录
不想从零敲?先用 MQL4 EA 参数生成器 选均线周期、手数、止损止盈和交易时段,生成一份可编译的 .mq4 骨架,再对照本文把逻辑补完。工具纯浏览器运行,不上传代码。

EA、指标、脚本:先分清再动手

用 MQL4 写出来的程序都存成 .mq4,但按用途分三类,放错文件夹在 MT4 里就看不到:

类型能不能下单运行方式放在哪
EA(Expert Advisor)能,自动下单/改单/平仓挂到图表,每个报价触发 OnTick()MQL4/Experts
指标(Indicator)不能,只计算和显示随图表刷新,画线或画柱MQL4/Indicators
脚本(Script)能,但只执行一次拖到图表立即运行,跑完退出MQL4/Scripts

本文写的是 EA。判断一段代码是不是 EA,看它有没有 OnTick() 并且调用了交易函数就够了。指标就算画得再像买卖点,也不会自己下单;想把指标变成自动交易,得把它的计算逻辑搬进 EA 的 OnTick()。三类程序的区别在第 1 章 第一个 EA 里有更细的演示,这里不再展开。

写 EA 前的 3 项准备

  1. 一个 MT4 客户端和 MetaEditor。在 MT4 里按 F4 就能打开 MetaEditor;新建「Expert Advisor」,文件名用英文,路径保持默认的 MQL4/Experts。
  2. 一条能用一句话说清的规则。比如「10 周期 EMA 上穿 30 周期 EMA 时做多,下穿时做空,每单固定 0.1 手,止损 30 点、止盈 60 点」。说不清规则,代码一定写不顺。
  3. 一个模拟账户。编译通过不等于能赚钱,先在模拟盘跑,别拿实盘试错。回测用「每个 tick」模式,样本至少覆盖一段趋势和一段震荡。
术语先对齐:本文说的「点」是平台的最小报价单位 Point。5 位报价的 EURUSD 上,1 个标准点(pip)= 10 个 Point,所以「止损 300 点」= 30 个标准点;黄金、指数同理,先看 Digits 再换算,别把 30 点写成 300 点。

第 1 步:搭一个能编译的 EA 骨架

所有 EA 都从这三个函数开始。OnInit() 在挂载时执行一次,用来校验参数;OnTick() 每个报价执行一次,是策略主体;OnDeinit() 在卸载时执行,用来收尾。先让这个骨架编译通过,再往里填逻辑。

#property strict

input int    FastMA     = 10;        // 快线周期
input int    SlowMA     = 30;        // 慢线周期
input double Lots       = 0.10;      // 固定手数
input int    StopLoss   = 300;       // 止损(Point)
input int    TakeProfit = 600;       // 止盈(Point)
input int    Magic      = 20261103;  // 魔术数,区分不同 EA 的订单

int OnInit()
  {
   if(FastMA >= SlowMA)
     {
      Print("参数错误:FastMA 必须小于 SlowMA");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(Lots <= 0)
     {
      Print("参数错误:Lots 必须大于 0");
      return(INIT_PARAMETERS_INCORRECT);
     }
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   Print("EA 已停止,reason=", reason);
  }

void OnTick()
  {
   // 策略逻辑下一步再写
  }

编译快捷键是 F7。常见的第一类报错不是语法错,而是文件没放进 MQL4/Experts,或者 MetaEditor 打开的是别的目录——先确认左上角的路径,再谈代码。

第 2 步:把入场条件写成代码

均线交叉是最适合练手的入场逻辑:快线从下往上穿过慢线叫金叉,反之叫死叉。MQL4 用 iMA() 取均线值,最后一个参数 shift 表示取第几根 K 线:0 是当前未收盘的 K 线,1 是上一根已收盘的 K 线。判断交叉要用 前两根都已收盘 的值,否则同一根 K 线内价格来回抖,信号会反复触发。

double fast1 = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double fast2 = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 2);
double slow1 = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);
double slow2 = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 2);

bool crossUp   = (fast2 <= slow2 && fast1 > slow1);
bool crossDown = (fast2 >= slow2 && fast1 < slow1);

if(crossUp)
   Print("金叉,准备做多");
if(crossDown)
   Print("死叉,准备做空");

iMA 的第二个参数是时间周期,0 表示当前图表周期;MODE_EMA 是指数均线,换成 MODE_SMA 就是简单均线。指标函数的完整用法见 技术指标函数,不要凭记忆猜参数顺序。

第 3 步:下单、止损止盈与订单管理

下单只用 OrderSend() 一个函数,但参数很多,顺序错一个就会返回 -1。最关键的六个参数是:品种、方向、手数、价格、滑点、止损/止盈。下面这段可以直接用:

void OpenOrder(int type)
  {
   RefreshRates();
   double price = (type == OP_BUY) ? Ask : Bid;
   double sl = 0, tp = 0;

   if(StopLoss > 0)
      sl = (type == OP_BUY) ? price - StopLoss * Point : price + StopLoss * Point;
   if(TakeProfit > 0)
      tp = (type == OP_BUY) ? price + TakeProfit * Point : price - TakeProfit * Point;

   sl = NormalizeDouble(sl, Digits);
   tp = NormalizeDouble(tp, Digits);

   int ticket = OrderSend(Symbol(), type, Lots, NormalizeDouble(price, Digits),
                          Slippage, sl, tp, "MA Cross EA", Magic, 0,
                          (type == OP_BUY) ? clrBlue : clrRed);
   if(ticket < 0)
      Print("下单失败,GetLastError()=", GetLastError());
   else
      Print("下单成功,ticket=", ticket);
  }

几个容易忽略的点:

下单函数的完整参数表和返回值说明见 下单函数;订单池的遍历、改单、平仓在 订单池遍历与 OrderSelect 里有完整示例。

第 4 步:只在新 K 线开仓

OnTick() 每个报价都会执行,EURUSD 活跃时段一秒钟可能来好几次。如果金叉条件持续成立,EA 就会在同一根 K 线上开出一串订单——这是新手 EA 亏得最快的原因之一。两个保护必须同时加:

bool IsNewBar()
  {
   static datetime lastBar = 0;
   datetime cur = iTime(NULL, 0, 0);
   if(cur == lastBar)
      return false;
   lastBar = cur;
   return true;
  }

bool HasPosition(int type)
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      if(OrderSymbol() != Symbol() || OrderMagicNumber() != Magic)
         continue;
      if(OrderType() == type)
         return true;
     }
   return false;
  }

IsNewBar() 用 static 变量记住上一根 K 线的时间,保证每根 K 线只执行一次策略;HasPosition() 遍历订单池,按品种和魔术数过滤,确认这个方向是否已有持仓。两个都加上,才能避免「信号没变、仓位越开越多」。

完整源码:均线交叉 EA

把上面的片段拼起来,就是一个可以编译、可以回测的完整 EA。新建 MaCrossEa.mq4,整段粘进去,F7 编译,再把 .ex4 拖到图表上。

//+------------------------------------------------------------------+
//| MaCrossEa.mq4                                                    |
//| 均线交叉 EA:10/30 EMA 金叉做多、死叉做空                        |
//+------------------------------------------------------------------+
#property strict

input int    FastMA     = 10;
input int    SlowMA     = 30;
input double Lots       = 0.10;
input int    StopLoss   = 300;
input int    TakeProfit = 600;
input int    Magic      = 20261103;
input int    Slippage   = 10;

int OnInit()
  {
   if(FastMA >= SlowMA)
     {
      Print("参数错误:FastMA 必须小于 SlowMA");
      return(INIT_PARAMETERS_INCORRECT);
     }
   if(Lots <= 0)
     {
      Print("参数错误:Lots 必须大于 0");
      return(INIT_PARAMETERS_INCORRECT);
     }
   return(INIT_SUCCEEDED);
  }

void OnDeinit(const int reason)
  {
   Print("EA 已停止,reason=", reason);
  }

void OnTick()
  {
   if(Bars < SlowMA + 2)
      return;
   if(!IsNewBar())
      return;

   double fast1 = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 1);
   double fast2 = iMA(NULL, 0, FastMA, 0, MODE_EMA, PRICE_CLOSE, 2);
   double slow1 = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 1);
   double slow2 = iMA(NULL, 0, SlowMA, 0, MODE_EMA, PRICE_CLOSE, 2);

   bool crossUp   = (fast2 <= slow2 && fast1 > slow1);
   bool crossDown = (fast2 >= slow2 && fast1 < slow1);

   if(crossUp && !HasPosition(OP_BUY))
      OpenOrder(OP_BUY);
   if(crossDown && !HasPosition(OP_SELL))
      OpenOrder(OP_SELL);
  }

bool IsNewBar()
  {
   static datetime lastBar = 0;
   datetime cur = iTime(NULL, 0, 0);
   if(cur == lastBar)
      return false;
   lastBar = cur;
   return true;
  }

bool HasPosition(int type)
  {
   for(int i = OrdersTotal() - 1; i >= 0; i--)
     {
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
         continue;
      if(OrderSymbol() != Symbol() || OrderMagicNumber() != Magic)
         continue;
      if(OrderType() == type)
         return true;
     }
   return false;
  }

void OpenOrder(int type)
  {
   RefreshRates();
   double price = (type == OP_BUY) ? Ask : Bid;
   double sl = 0, tp = 0;

   if(StopLoss > 0)
      sl = (type == OP_BUY) ? price - StopLoss * Point : price + StopLoss * Point;
   if(TakeProfit > 0)
      tp = (type == OP_BUY) ? price + TakeProfit * Point : price - TakeProfit * Point;

   sl = NormalizeDouble(sl, Digits);
   tp = NormalizeDouble(tp, Digits);

   int ticket = OrderSend(Symbol(), type, Lots, NormalizeDouble(price, Digits),
                          Slippage, sl, tp, "MA Cross EA", Magic, 0,
                          (type == OP_BUY) ? clrBlue : clrRed);
   if(ticket < 0)
      Print("下单失败,GetLastError()=", GetLastError());
   else
      Print("下单成功,ticket=", ticket);
  }
这份代码的定位是教学骨架。它没有做反向平仓、移动止损、按净值算手数和新闻过滤,直接上实盘会遇到回撤和重复持仓问题。先把「能编译、能回测、能看懂每一行」跑通,再逐项加风控,顺序不要反。

第 5 步:回测与参数优化

MT4 自带策略测试器(Ctrl+R)。选好 EA 和品种,把「模型」设成每个 tick,点「开始」;图形里的资金曲线只是结果,先看下面四件事:

  1. 交易次数够不够。只有十几笔的交易,统计上说明不了任何问题,至少几百笔再谈稳定性。
  2. 点差和滑点是不是实盘水平。回测默认点差往往偏小,把点差调到券商真实水平、加 1–2 点滑点,收益通常会掉一大截。
  3. 参数稳不稳。把 FastMA、SlowMA、止损各上下调 20% 再跑,收益曲线不应从漂亮变成巨亏。只在某一个参数点好看,就是过拟合。
  4. 样本外测试。用 2020–2023 的数据调参,再用 2024 至今的数据验证;后一段没跑过,才是相对真实的成绩。

回测通过后还有两步:先在模拟盘跑 1–3 个月,观察滑点、隔夜利息、点差扩大对结果的影响;再决定要不要上实盘、用多小的仓位。回测优化与参数稳健性的完整方法见 回测与参数优化。

常见错误码与排查表

EA 下单失败时,OrderSend 返回 -1,具体原因在 GetLastError() 里。下面是最常遇到的六个:

错误码含义怎么排查
130止损/止盈无效(invalid stops)打印 price/sl/tp 与 Digits;确认已 NormalizeDouble;查 MODE_STOPLEVEL 最小距离;5 位平台 1 pip = 10 Point
131手数无效(invalid volume)对照 MODE_MINLOT / MODE_MAXLOT / MODE_LOTSTEP,手数要落在区间内且是步长的整数倍
134资金不足(not enough money)按可用保证金反算最大手数,别用固定 1 手去跑小账户
138重新报价(requote)把 Slippage 从 0 调到 10–30;仍频繁就改市价成交或换流动性更好的时段
146交易上下文忙(context busy)同一时间只允许一个下单线程;失败后 Sleep(200) 再重试,别在循环里死磕
4109不允许交易检查 EA 属性里是否勾选「允许实时交易」、账户是不是只读/投资者密码

排查顺序建议固定成:先打印所有入参 → 再查账户和品种限制 → 最后才怀疑策略逻辑。错误码处理的系统写法见 错误处理与 GetLastError。

上线前检查清单

常见问题

写一个 MT4 EA 最短要多久?

有编程基础的人,照着本文的骨架和均线示例,一到两天能写出第一个可编译、可回测的 EA;零基础建议按《MQL4 教程》的顺序学两到三周。真正花时间的不是语法,而是把入场、出场、仓位和风控规则说清楚,规则含糊,代码一定写不顺。

EA 的 .mq4 和 .ex4 文件有什么区别?

.mq4 是源码,能用 MetaEditor 打开和修改;.ex4 是 F7 编译后的可执行文件,MT4 客户端只认 .ex4。改完源码必须重新编译,把新的 .ex4 放进 MQL4/Experts 并刷新导航面板。只拿到 .ex4 是看不到逻辑的,也没法审计它会不会乱下单。

为什么 EA 回测赚钱、模拟盘却亏钱?

常见原因有四个:回测没算真实点差、滑点和隔夜利息;历史数据质量差或用了未来函数;参数被优化到只适合那一段行情,属于过拟合;以及行情结构变了。把点差调成实盘水平、做样本外测试、参数上下浮动 20% 再跑一遍,能排掉大部分假象。

一个 EA 能同时跑多个货币对吗?

可以,每个图表挂一个实例即可。要注意三点:用 MagicNumber 区分不同实例的订单;代码里用 Symbol() 取当前品种,不要硬编码 EURUSD;多品种同时持仓会叠加保证金占用,手数要按账户净值重新算。

EA 下单报错 130(invalid stops)怎么排查?

130 表示止损或止盈价格无效。先打印下单时的 price、sl、tp 和 Digits,确认 sl/tp 已用 NormalizeDouble 处理;再查 MarketInfo(Symbol(), MODE_STOPLEVEL) 的最小止损距离;5 位报价平台上 1 个标准点等于 10 个 Point,别把 30 点写成 300 点。ECN 账户还要求先不带止损开仓,再用 OrderModify 补上。

网上找的 EA 源码能直接拿来实盘吗?

不建议。先看懂它用什么策略、有没有马丁格尔或无限加仓、亏损时怎么退出,再放到模拟盘跑足够长时间。来源不明的 .ex4 无法审计,风险更高;开源代码也要看清授权协议。拿来学习、改参数可以,直接上实盘等于把资金交给一个你不理解的逻辑。

EA 必须 24 小时开着 MT4 吗?

是。EA 运行在 MT4 客户端里,客户端关掉或断网就不会执行 OnTick。想让 EA 长期挂机,需要一台常开的 Windows 机器或 VPS,并处理断电、断网、平台维护导致的漏单;重新上线后要检查持仓与订单池,而不是假设它一直在跑。

系统学 MQL4

本文是「写出来」的路线,语法和函数细节建议配一套系统课程补。环森的 MQL4 教程共 43 节,从变量、循环、函数讲到交易函数、指标、均线交叉实战,再到 OnTimer、订单池、错误处理、指标缓冲区、交易面板和回测优化;每节都有可运行代码,编程练习提交后由 AI 按参考答案批改。

把本文的均线 EA 跑通之后,下一步是给它加反向平仓、移动止损和按净值算手数——这三件事决定它能不能从「回测能看」走到「实盘能活」。这些内容在第 6 章进阶实战里逐项拆开讲。