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 项准备
- 一个 MT4 客户端和 MetaEditor。在 MT4 里按 F4 就能打开 MetaEditor;新建「Expert Advisor」,文件名用英文,路径保持默认的
MQL4/Experts。 - 一条能用一句话说清的规则。比如「10 周期 EMA 上穿 30 周期 EMA 时做多,下穿时做空,每单固定 0.1 手,止损 30 点、止盈 60 点」。说不清规则,代码一定写不顺。
- 一个模拟账户。编译通过不等于能赚钱,先在模拟盘跑,别拿实盘试错。回测用「每个 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);
}
几个容易忽略的点:
- 价格一定要
NormalizeDouble。不处理小数位,5 位平台会直接返回 130(invalid stops)。 - 多空方向决定止损止盈加减。多单止损在价格下方,空单在上方,写反了就是给市场送钱。
- 滑点参数不能为 0。设为 0 时很多平台要求精确成交,行情一快就 138(requote)。
- ECN 账户先开仓再改单。部分 ECN 不接受带止损的市价单,需要先
OrderSend开仓,再用OrderModify补止损止盈。 - 魔术数要固定且唯一。它是 EA 认领自己订单的身份证,后面遍历订单池、平仓、统计都靠它。
下单函数的完整参数表和返回值说明见 下单函数;订单池的遍历、改单、平仓在 订单池遍历与 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 点滑点,收益通常会掉一大截。
- 参数稳不稳。把 FastMA、SlowMA、止损各上下调 20% 再跑,收益曲线不应从漂亮变成巨亏。只在某一个参数点好看,就是过拟合。
- 样本外测试。用 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。
上线前检查清单
- 参数用
input暴露,不在代码里写死;改参数不用重新编译。 OnInit()校验了 FastMA < SlowMA、手数大于 0、止损止盈不超过平台限制。- 每根 K 线只执行一次策略,同一方向已有持仓时不重复开仓。
- 所有价格都经过
NormalizeDouble,止损止盈方向正确。 - 下单失败会打印
GetLastError(),不会静默失败。 - 回测用「每个 tick」,点差/滑点按实盘设置,且做过样本外测试。
- 模拟盘至少跑 1 个月,确认没有重复开仓、漏平仓、资金曲线异常。
- 实盘先用最小手数,确认订单备注、魔术数、止损止盈都符合预期。
常见问题
写一个 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 章进阶实战里逐项拆开讲。