简介:这份《零基础学MQL:基于EA的自动化交易编程》docx文档,面向希望踏入程序化交易领域的交易者与初学者,系统讲解MQL语言在MetaTrader 4/5平台上的应用,涵盖自动化交易概念、EA智能交易程序编写、语法特性与常用函数等核心内容,帮助读者从零掌握策略自动化与风险管理的编程思路。资料包含1个docx文档,整体大小仅29KB,文档结构完整,开篇概述自动化交易与MQL价值,随后深入EA开发基础、数据类型、变量常量与交易函数,适合零基础读者按章节循序渐进学习。该资源已有1350人学习下载,轻量却覆盖了从概念、语法到实践落地的主要知识点,既可作入门自学材料,也可在编写EA、调试自动化策略时快速查阅参考。
1. 零基础学MQL:先搞懂EA才是你入门的捷径
打开行情软件按下“自动交易”按钮,却不知道那一行行MQL代码到底怎么让机器人替你下单——这是很多交易新手卡住的地方。MQL不是玄学,它是一套专门为交易平台设计的编程语言,而EA(Expert Advisor)就是用这门语言写出来的自动交易程序。零基础学MQL,本质上不是先背语法,而是先弄懂“程序如何看懂行情、如何决定下单、如何管理持仓”这条链路。这篇笔记会带你从最核心的EA骨架开始,一行行把移动平均线交叉策略写成可回测的机器人,再把回测、参数优化和运行时常见的坑全部摊开。适合完全没写过代码、但想验证自己交易思路的人,也适合写过其他语言、想快速上手MQL的朋友。没必要先啃完官方文档,先让一个小EA跑起来,后面的事都好说。
2. 从零认识MQL4与EA:程序结构、事件函数和订单执行逻辑
很多初学者第一次打开MetaEditor,看到满屏英文函数就懵了。其实MQL4的EA结构非常固定,你要做的不是记住所有函数,而是理解“事件驱动”这四个字。EA不是按顺序从头到尾跑一遍就结束的程序,它会在每个新报价(Tick)到来时,自动调用一次OnTick()函数。你写的所有策略逻辑,本质上都是“当新报价来了,我要检查什么条件、要不要下单”。这个模型一旦建立,后面学什么都快。
2.1 EA的三个固定事件函数:OnTick、OnInit、OnDeinit
任何一个EA都至少包含一个OnTick()函数,这是MT4/MT5平台自动调用的入口。它不像普通脚本那样需要你手动点击“运行”,而是在每个价格跳动时被平台唤醒。别忘了还有另外两个配套函数:
int OnInit() { // 程序被加载到图表时执行一次 // 返回 INIT_SUCCEEDED 表示初始化成功 Print("EA初始化完成,当前账户余额:", AccountBalance()); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { // 程序被移除或图表切换时执行 Print("EA已卸载,原因代码:", reason); } void OnTick() { // 每个新报价触发一次 // 这里写你的核心策略逻辑 }逻辑说明:OnInit()在EA挂到图表上时只跑一次,适合读配置文件、设置全局变量、检查账户状态。OnDeinit()在EA被删除或关闭图表时清理资源,比如删除定时器、释放句柄。而OnTick()才是真正的主战场,策略判断、下单、平仓全都写在这里。参数说明:INIT_SUCCEEDED是一个固定常量,值为0,如果你的初始化条件不满足,可以返回一个非零值让EA拒绝启动。新手最常见的错误是把账号密码等参数写在OnTick()里重复查询,浪费性能,应该放在OnInit()里只读一次。
2.2 MQL4的核心数据类型:价格、时间、订单属性
写EA之前,你必须分清几个核心类型。double用来存价格和利润,因为价格有小数点;int用来存订单编号、仓位数量;datetime用来存K线时间;string用来存注释和标签。下面这段代码演示了如何读取当前行情信息:
void OnTick() { double currentPrice = Bid; // 当前卖价(你买入时用的价格) double askPrice = Ask; // 当前买价(你卖出时用的价格) int pointValue = _Point; // 最小报价单位,例如EURUSD为0.00001 int digitCount = _Digits; // 价格小数位数,例如EURUSD为5 Print("当前Bid=", currentPrice, " Ask=", askPrice, " Point=", pointValue, " Digits=", digitCount); }逻辑说明:Bid是你卖出(做空开仓、做多平仓)时看到的价格,Ask是你买入(做多开仓、做空平仓)时看到的价格。初学者最容易搞反的点就在这:用Ask计算开多单的止损,用Bid计算开空单的止损。_Point和_Digits是平台预定义变量,它们会随图表品种自动变化,不需要你手动赋值。参数说明:如果做的是黄金XAUUSD,_Digits通常是2,_Point是0.01;如果是EURUSD,_Digits是5,_Point是0.00001。这些值直接影响你计算止损点数时的乘法基准。
2.3 下单函数 OrderSend 的每个参数都别乱填
下单是EA的核心动作,OrderSend()函数像一份“下单申请表”,少了任何一项平台都会拒绝。下面是标准调用方式:
int ticket = OrderSend( _Symbol, // 当前图表品种 OP_BUY, // 订单类型:OP_BUY买入 / OP_SELL卖出 0.01, // 手数,0.01是微型手 Ask, // 开仓价格,买单用Ask,卖单用Bid 10, // 最大滑点偏差,单位为点数 Ask - 200 * _Point, // 止损价:买入价往下200点 Ask + 200 * _Point, // 止盈价:买入价往上200点 "MyFirstEA", // 订单注释 0, // 魔法数字,用于识别本EA的单子 0, // 订单到期时间,0表示永不过期 clrNONE // 箭头颜色,颜色常量 ); if(ticket < 0) { Print("下单失败,错误码:", GetLastError()); } else { Print("下单成功,订单号:", ticket); }逻辑说明:OrderSend()执行成功会返回一个订单编号(ticket),失败则返回-1。所以很多老手习惯用if(ticket < 0)来统一处理失败情况。参数说明:_Symbol保证了EA只操作当前图表品种,避免在EURUSD图表上意外下单到GBPUSD。滑点参数是“最大容忍度”,不是实际滑点,设成10意味着如果实际价格偏离你要求的价格超过10个点,这个订单就会被拒绝,而不是被强制执行。止损止盈用Ask - 200 * _Point而不是直接写数字,是为了让EA在不同品种上通用——黄金的200点不等于欧美货币对的200点,用_Point换算才标准。魔法数字(Magic Number)特别重要,当一个图表挂了多个EA时,它就是区分“谁的单子”的唯一凭据。
2.4 挂单、修改和删除订单:管理持仓的三板斧
EA不只做即时开仓,还会用到挂单(限价单、止损单),以及修改持仓单的止损止盈。核心函数有OrderModify()和OrderDelete()。看下面的修改止损示例:
void MoveStopLossToBreakEven(int ticket, double entryPrice, bool isBuy) { double stopLoss = 0; if(isBuy) stopLoss = Bid - 100 * _Point; // 买入单移动到成本价上方100点 else stopLoss = Ask + 100 * _Point; // 卖出单移动到成本价上方100点 bool success = OrderModify( ticket, // 要修改的订单号 entryPrice, // 订单的原始开仓价格,不能改动,必须和原来一致 stopLoss, // 新的止损价 OrderTakeProfit(), // 保留原来的止盈价 0, // 有效期 clrNONE // 颜色 ); if(!success) Print("修改止损失败,错误码:", GetLastError()); }逻辑说明:OrderModify()要求你传入的“开仓价”必须与原始订单完全一致,否则会报错135。所以更可靠的写法是用OrderOpenPrice()去取原始价格,而不是自己记录一个变量——因为EA可能被重启,变量丢失,但订单价格是存在服务器上的。参数说明:这里用OrderTakeProfit()直接读取原止盈值,避免你临时填写错价格导致平仓逻辑变化。对于挂单,OrderDelete()只需要传入订单号,但删挂单之前要先检查订单是否存在,否则会报错4550。新手经常困惑的还有“持仓中还是挂单中”的区分,通过OrderType()返回值判断:OP_BUY、OP_SELL是持仓单,OP_BUYLIMIT、OP_SELLLIMIT、OP_BUYSTOP、OP_SELLSTOP是挂单。
2.5 遍历已有订单:别让EA重复开单
EA最尴尬的翻车现场,就是每个Tick触发一次开仓,结果一分钟内开了几十单。解决办法是在开仓前先遍历所有订单,检查是否已有本EA的持仓单。下面是标准过滤代码:
bool HasOpenOrderByMagic(int magic) { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) { if(OrderMagicNumber() == magic && OrderSymbol() == _Symbol) { return true; // 找到一张属于本EA的单子 } } } return false; }逻辑说明:OrdersTotal()返回当前账户所有持仓单加上挂单的总数。遍历时采用倒序i,是为了防止你在循环中平仓、删单导致索引错乱——每次删除后列表长度变化,从尾部往前处理最安全。OrderSelect()按位置选中订单,然后通过OrderMagicNumber()和OrderSymbol()双重过滤,确保不会误判其他EA或手工下的单。参数说明:SELECT_BY_POS是“按序号选单”,MODE_TRADES表示只选交易列表中的单(包含持仓和挂单),如果你只关心持仓,可以再判断OrderType()是否小于2。这个函数建议放在开仓判断的第一行,否则任何策略逻辑都是在裸奔。
3. 把移动平均线交叉写成EA:从策略到代码的完整拆解
零基础学MQL最爽的时刻,就是把自己在图表上人工判断的“金叉死叉”变成一行行自动执行的代码。移动平均线交叉策略非常适合入门:概念简单、参数直观、回测速度快。但别小看它,这里面藏着很多后续所有EA都通用的关键写法——均线指标句柄的获取、K线数据的读取、开仓条件的“唯一触发”控制。
3.1 方案设计:这块EA要在什么逻辑下开仓平仓
先写出人工交易规则,再翻译成代码。规则如下:当5周期EMA上穿20周期EMA时,开多单(买入);当5周期EMA下穿20周期EMA时,开空单(卖出)。持仓中不再重复开单,且单子止损设置在入场价附近。为了让新手不卡在“历史K线与实时K线的数据偏移”上,我们统一用“已经收盘的当前K线”来判断交叉信号,这样回测和实盘行为一致。
这个设计有四个关键决定:第一,用EMA而不是SMA,因为EMA对价格反应更灵敏,交叉信号出现更早;第二,用已收盘K线判断,避免实时K线刚形成交叉,下一根K线价格波动后又消失,导致信号反复闪烁;第三,持仓期间不开新单,避免震荡行情下EA连续追单;第四,止损用固定点数,方便回测时对比不同止损的影响。所有参数都用input定义,这样回测时可以在策略测试器里直接调节,不用改代码重编译。
3.2 用 iMA 创建均线句柄:别在 OnTick 里重复创建
MQL4里获取移动平均线数值的关键函数是iMA(),它返回一个“指标句柄”,之后你通过CopyBuffer()把句柄对应的数值复制到数组里。这里最常见的坑就是“在OnTick里反复创建句柄”,白白消耗性能。正确写法是把句柄放在OnInit()里创建一次:
int fastMaHandle; int slowMaHandle; int OnInit() { fastMaHandle = iMA(_Symbol, _Period, 5, 0, MODE_EMA, PRICE_CLOSE); slowMaHandle = iMA(_Symbol, _Period, 20, 0, MODE_EMA, PRICE_CLOSE); if(fastMaHandle == INVALID_HANDLE || slowMaHandle == INVALID_HANDLE) { Print("指标句柄创建失败:", GetLastError()); return(INIT_FAILED); } return(INIT_SUCCEEDED); }逻辑说明:iMA()第一参数是品种,第二参数是图表周期,后面依次是“周期数”“价格偏移量”“均线类型”“应用价格”。这里传入_Period表示EA工作在什么周期,就读取什么周期的均线——你把它挂在M15图表上,它就是15分钟均线。参数说明:0是均线偏移量,通常固定为0,意思是使用当前K线作为计算起点,如果你写1,表示把均线整体向右移动1根K线。MODE_EMA是指数移动平均线,PRICE_CLOSE表示用收盘价计算。句柄创建后,后续读取就靠这个整数句柄,不需要再调用iMA()。
3.3 读取K线数据和均线数值:用CopyBuffer填充数组
创建好句柄后,在OnTick()里读取最新的两根已收盘K线的均线值。注意,K线索引是从0开始的,0号是正在形成的K线,1号是刚收盘的上一根K线。我们要判断“上一根收盘K线”和“上上根收盘K线”的均线大小关系,这样才能捕捉到交叉:
void OnTick() { static datetime lastBarTime = 0; if(iTime(_Symbol, _Period, 1) != lastBarTime) { lastBarTime = iTime(_Symbol, _Period, 1); CheckForSignals(); // 每根新K线只检查一次 } }逻辑说明:这个“只在新K线出现时执行”的写法,比直接用OnTick()里判断交叉靠谱得多。它用iTime()读取上一根K线的开盘时间,如果时间变了,说明一根新K线已经形成,此时才执行信号检查。这能有效避免同一个交叉信号被触发多次。参数说明:static datetime lastBarTime是静态变量,值在函数调用之间保留,这是MQL里常用的“记忆上一次事件”的手段。如果你不想用静态变量,也可以把lastBarTime定义成全局变量,效果一样。
读取均线数值的代码如下:
void CheckForSignals() { double fastArray[]; double slowArray[]; ArraySetAsSeries(fastArray, true); // 让数组索引对齐K线索引 ArraySetAsSeries(slowArray, true); int fastCount = CopyBuffer(fastMaHandle, 0, 1, 2, fastArray); int slowCount = CopyBuffer(slowMaHandle, 0, 1, 2, slowArray); if(fastCount < 2 || slowCount < 2) { Print("均线数据不足,等待下一根K线"); return; } double fastPrev1 = fastArray[1]; // 上一根已收盘K线的快速均线值 double slowPrev1 = slowArray[1]; double fastPrev2 = fastArray[0]; // 注意:数组索引0对应的是K线索引1 double slowPrev2 = slowArray[0]; // 金叉:上一根快速均线大于慢速均线,上上根快速均线小于慢速均线 if(fastPrev1 > slowPrev1 && fastPrev2 <= slowPrev2) { OpenBuy(); } // 死叉:上一根快速均线小于慢速均线,上上根快速均线大于慢速均线 else if(fastPrev1 < slowPrev1 && fastPrev2 >= slowPrev2) { OpenSell(); } }逻辑说明:ArraySetAsSeries(..., true)的作用是让数组下标与K线时间顺序一致——array[0]对应当前正在形成的K线,array[1]对应刚收盘的上一根K线。这会让代码更直观。CopyBuffer从句柄复制数据,第三个参数1表示从K线索引1开始复制(跳过正在形成的0号K线),第四个参数2表示复制两根K线的数据。参数说明:很多人困惑为什么FastPrev2用的是fastArray[0],因为复制时起始位置是1,所以数组里的索引0实际上对应的是K线索引1,数组里的索引1对应K线索引2。如果你不设ArraySetAsSeries,索引方向正好相反,代码极容易出错。
3.4 开仓与平仓:把交易动作封装成独立函数
开仓和平仓逻辑最好封装成函数,这样信号判断、仓位管理、日志输出都能分离。下面是一个简单的开多单函数,完整考虑了资金占用和失败重试:
void OpenBuy() { if(HasOpenOrderByMagic(MagicNumber)) return; // 已有持仓,不再开新单 double lotSize = CalculateLotSize(); // 根据余额计算手数 double stopLossPrice = Ask - StopLossPoints * _Point; double takeProfitPrice = Ask + TakeProfitPoints * _Point; int ticket = OrderSend(_Symbol, OP_BUY, lotSize, Ask, SlippagePoints, stopLossPrice, takeProfitPrice, "EMA Cross EA", MagicNumber, 0, clrGreen); if(ticket < 0) { int error = GetLastError(); Print("开多失败,错误码:", error, " Ask=", Ask); // 常见错误100:价格被禁用,可能要等几个Tick再重试 } else { Print("开多成功,订单号:", ticket); } }逻辑说明:CalculateLotSize()是一个自定义函数,我们会在下一节详细给出实现。开仓前先调用HasOpenOrderByMagic检查持仓,这是防止重复开单的第一道防线。参数说明:StopLossPoints和TakeProfitPoints是input变量,以“点数”为单位,在代码内部乘以_Point换成价格。注意开多单时,止损价用Ask计算,因为你的开仓价实际上是Ask;有些新手误用Bid计算止损,导致止损价离现价太近或太远。
平仓函数更复杂一点,因为你不仅要遍历订单,还要决定“平掉哪一个方向的单子”。这里给出一个按魔法数字平掉全部多单的示例:
void CloseAllBuyOrders() { for(int i = OrdersTotal() - 1; i >= 0; i--) { if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) { if(OrderMagicNumber() == MagicNumber && OrderType() == OP_BUY) { if(OrderClose(OrderTicket(), OrderLots(), Bid, SlippagePoints, clrRed)) { Print("平多成功,订单号:", OrderTicket()); } else { Print("平多失败,错误码:", GetLastError()); } } } } }逻辑说明:OrderClose()的第三个参数是平仓价格,对多单来说你卖出,所以用Bid。最后一个参数是颜色,只影响图表上的箭头显示。参数说明:OrderLots()返回原始手数,不需要你自己存储。注意平仓后,OrderSelect选中的订单已经不存在了,所以循环继续倒序遍历不会有问题。
3.5 仓位管理:固定比例手数怎么写,以及为什么不建议直接上大仓
仓位管理是EA能稳定存活的前提。最简单的做法是“固定手数”,比如每次0.01手。但认真做的人会用“余额百分比”动态计算手数。下面是一个标准实现:
input double RiskPercent = 1.0; // 每笔交易占用余额的百分比,1表示1% input double FixedLotSize = 0.01; // 如果启用固定手数,就用这个值 input bool UseFixedLot = true; // true=固定手数,false=百分比 double CalculateLotSize() { if(UseFixedLot) return(FixedLotSize); double balance = AccountBalance(); double riskAmount = balance * RiskPercent / 100.0; double pipValue = PointValue(); // 每个点对应的美元价值 double riskPerTrade = riskAmount / StopLossPoints / pipValue; // 对风险取整到标准手数步进 double stepSize = MarketInfo(_Symbol, MODE_LOTSTEP); double normalized = MathFloor(riskPerTrade / stepSize) * stepSize; double minLot = MarketInfo(_Symbol, MODE_MINLOT); if(normalized < minLot) normalized = minLot; double maxLot = MarketInfo(_Symbol, MODE_MAXLOT); if(normalized > maxLot) normalized = maxLot; return(normalized); }逻辑说明:MarketInfo()是查询品种规格的函数,MODE_LOTSTEP返回最小手数步进,例如0.01;MODE_MINLOT和MODE_MAXLOT返回该品种允许的手数上下限。用MathFloor向下取整,确保计算出的手数不会超过风险预算。参数说明:比例仓位看起来科学,但有一个隐患——EA回测时,如果账户余额增长,手数会不断变大,导致后期波动剧烈;余额缩水时手数变小,又很难回本。所以很多实战EA干脆用固定手数跑一段时间,人工调整后再换。我不建议新手一上来就用比例仓位,先用固定0.01把手数逻辑跑通,再研究资金曲线平滑的问题。
4. 回测与参数优化:在策略测试器里验证EA的3个关键步骤
写过EA之后,你必须学会在策略测试器里验证它。很多人写好了直接挂实盘,那是拿自己的真金白银做实验。正确的流程是:先做历史回测,再根据不同参数组合做优化,最后在模拟盘上跑几周。回测不是看盈利金额有多大,而是看资金曲线是否平滑、回撤是否在你能容忍的范围内。
4.1 回测前的检查清单:品种、周期、时间范围、初始资金
打开MT4的策略测试器(Ctrl+R),第一件事不是点“开始”,而是核对测试设置。我每次回测前都会检查这四项:
| 设置项 | 常见错误 | 正确做法 |
|---|---|---|
| 测试品种 | 忘了切换,测了EURUSD但EA在XAUUSD上优化过 | 必须与你打算实盘的品种一致 |
| 测试周期 | 用了M15策略参数却挂在H1上测试 | 信号周期必须与EA挂载周期一致,除非你写了多周期逻辑 |
| 测试时间段 | 只测一年趋势行情,忽略震荡年份 | 至少覆盖3年以上,包含趋势和震荡切换 |
| 初始资金 | 用100万美元测试,实盘只有5000美元 | 初始资金设置成和你实际账户接近,否则手数计算失真 |
特别注意“测试时段”对结果的影响。如果你只用2020年整年测试一套参数,很可能那一年的行情刚好适合这套均线参数,换到2019年就拉胯。我一般会把最近5年数据都拉进来,先跑区间内全部,再看分年度报告,确认没有“靠某一年爆赚”的问题。
4.2 回测结果怎么看:净利润、回撤、盈利因子、交易频率
跑完回测,不要让“报表上的大数字”冲昏头脑。重点看四个指标:净利润、最大回撤、盈利因子、交易次数。下表是我判断EA是否值得继续优化的建议值:
| 指标 | 及格线 | 备注 |
|---|---|---|
| 盈利因子 | > 1.5 | 盈利总额除以亏损总额,小于1说明策略本身负期望 |
| 最大回撤 | < 30% | 资金从最高点到最低点的跌幅,超过30%实盘很难拿住 |
| 交易次数 | > 100次 | 样本太少不具备统计意义,均线交叉策略在M15上一年通常有几十次 |
| 净利润 | > 0且复利增长 | 不要只看最后一天余额,要看月度曲线是否持续向上 |
如果盈利因子在1.2附近,说明策略几乎没有优势,靠手续费和滑点就能磨掉它。回测报表里的“最大回撤”是按你设置的初始资金计算的,如果你实盘账户资金比回测小,回撤比例会被放大,因为手数规格无法等比例缩小(有最小手数限制)。另外,回测里的“预期获利”和“最大连亏”是重要的压力指标——如果你接受不了最大连亏,就应该考虑加过滤条件,而不是靠改手数硬扛。
4.3 用“输入参数”扫描做优化:别把所有参数丢进遗传算法
MT4的策略测试器支持“输入参数”页签自动扫参。以一个均线交叉EA为例,你需要优化的参数包括:快速均线周期、慢速均线周期、止损点数、止盈点数。下面是标准设置流程:
# 在测试器输入参数页签,每个参数设置“开始/步长/结束” # 例如快速均线: # 开启优化 = 勾选 # 起始值 = 3 # 步长 = 2 # 结束值 = 21 # 慢速均线: # 起始值 = 10 # 步长 = 5 # 结束值 = 60 # 止损点数: # 起始值 = 100 # 步长 = 50 # 结束值 = 600逻辑说明:扫参范围不能无限大,否则组合数量爆炸。比如快速均线10个候选、慢速均线11个、止损11个、止盈11个,总共一万多组,跑起来很慢。我一般先固定止损和止盈,只优化两条均线的周期组合,找到“稳定盈利区间”后,再固定均线参数去优化止损止盈。参数说明:不要在优化时把“初始资金”也设成变量,那只会在优化结果中引入资金增长对复利的影响,干扰判断。优化完成后,不要直接选净值最高的那组参数——那通常是最拟合历史行情的“过度优化”结果,正确做法是观察“参数平原”区域:周围参数表现也不错,而不是只有孤零零一个峰点。
4.4 前向测试:用预留给未见过数据的“望后”方式做验证
参数优化最大的陷阱是“过拟合”——你选出的完美参数只是完美适应了过去5年的行情,未来可能完全失效。一个简单的防过拟合方法是“分段测试”:
把5年数据分为三段: 2019-2020:第一段,不要看测试结果,先不做任何优化 2021-2022:第二段,在这段数据上优化参数 2023-2023:第三段,用优化出的参数跑这段“未见过”的数据如果第三段结果和优化段差不多,说明参数有泛化能力;如果第三段亏损明显,说明优化出的参数只是历史数据拟合出来的偶然。分段测试在MT4里做起来有点麻烦,因为策略测试器默认用全部数据。常见的做法是先限定测试日期在第二段,优化出参数,再把测试日期改到第三段,用相同参数重新跑一次。这个习惯值得养成,它比任何花哨的优化算法都更能帮你判断一套EA到底行不行。
5. EA运行避坑指南:5个让程序翻车的最常见问题与排查流程
EA写好了、回测也漂亮,但一上模拟盘或实盘就各种幺蛾子。这一章我按“现象 → 原因 → 解决”的格式,总结五条最常见的踩坑记录,每一条都是血泪换来的。
5.1 现象:EA挂上后完全不执行,左下角显示“笑脸”但没有任何操作
原因有三:第一,EA的“允许算法交易”按钮没打开,图表上方如果出现哭脸图标,说明EA被禁止交易;第二,EA在OnInit()里返回了INIT_FAILED,导致初始化失败;第三,开仓条件长期不满足,比如均线交叉在当前震荡行情里没出现。解决方法是先看“专家日志”页签,按顺序排查:确认图标是笑脸 → 确认OnInit里的日志没有错误 → 在OnTick()开头临时加Print("tick received")看是否被调用。如果收到了Tick却不交易,说明策略条件不满足,而不是程序坏了。
5.2 现象:EA下单手数总是出问题,提示“无效手数”或“交易被拒绝”
最常见的原因是手数与品种规格不符。比如你在黄金上设置了0.01手,但某些平台的黄金最小手数是0.1;或者手数步进是0.01,你却算出来0.05,被强制截断后变成无效值。另一个原因是没有检查“当前账户是否支持该品种交易”,比如美分账户的一手规格和标准账户不同。解决方法是永远不要直接使用计算结果下单,先通过MarketInfo获取MODE_MINLOT、MODE_MAXLOT、MODE_LOTSTEP,对手数做标准化处理。我上面CalculateLotSize()里已经包括了这一步,但很多人只复制了一半,没有做向下取整,导致手数变成0.011这样的非法值。
5.3 现象:EA在回测里正常,实盘第一单就出现“价格过期”错误码138
回测环境里价格是静态的,实盘里每个Tick都在变化。当你的EA处理信号需要几十毫秒,期间价格已经跳过了你设定的买入价,此时OrderSend中指定的Ask已经不是最新价,就会触发138错误。解决方式有两种:一是用“市价单”配合重试机制,下一次Tick重新获取Ask再下单;二是使用MarketInfo(_Symbol, MODE_ASK)实时获取最新卖价,而不用之前缓存的变量。注意市价单也会滑点,所以滑点参数设成合理值(比如10-20点)是必须的,设置过大意味着你接受更差的价格,设置过小则可能频繁下单失败。
5.4 现象:EA长时间运行后,状态栏提示“数组越界”或“无效句柄”
原因通常是K线数据历史不足,或者指标句柄在断网重连后失效。在EA运行期间,如果平台服务重启、品种历史数据重新加载,原本有效的指标句柄可能会变成INVALID_HANDLE。解决方法是:在每次读取CopyBuffer之前,先检查fastMaHandle是否等于INVALID_HANDLE,如果等于就重新在OnInit里初始化一遍。同时,CopyBuffer返回的数组数量要判断,如果复制结果少于你请求的2根,直接跳出逻辑等待下一根K线,不要硬用数组里的垃圾数据。
5.5 现象:EA自动平仓和手动平仓互相冲突,订单被平了又开
这通常是因为你的遍历函数没有排除“非本EA订单”。比如你用手动平掉了一单,但EA的循环里还认为存在本EA持仓,导致信号出现时不开单;或者你挂单列表里混入了手工挂单,EA却当成自己的单子去修改。解决方案不复杂:开仓前、平仓前、修改前,都必须用魔术号码加品种双重过滤。我在前面给的HasOpenOrderByMagic()就是标准写法,直接用即可。还需要注意:MT4的策略测试器里,同一EA的多个实例如果使用相同魔法数字,它们之间会抢单。所以每挂一个图表的EA,都应该设置不同的输入参数MagicNumber,这是很多新手忽略的细节。
6. 从单策略EA进阶:加入时间过滤与多策略模块,让EA更贴近实盘
当你把上面的EMACrossEA完整跑通之后,下一步不是急着写更复杂的指标,而是给EA加上“实盘生存”所必需的约束。两个最管用的进阶方向:一是时间过滤——只在特定时段交易,避开点差剧烈波动的数据公布时段;二是把信号判断、下单逻辑、风控模块拆开,变成可以复用的代码结构。
时间过滤的实现很简单,但效果很直观。比如你不想在每天20:00到22:00之间的高波动时段开新仓,只需在开仓函数最前面加一个小时检查:
bool IsTradingTimeAllowed() { if(!UseTimeFilter) return(true); int currentHour = Hour(); if(HourStart <= currentHour && currentHour < HourEnd) return(true); return(false); }逻辑说明:Hour()返回平台服务器时间的当前小时,注意不是你的本地时间,而是经纪商的服务器时区,你必须先在MT4行情窗口里确认服务器时间和北京时间差多少小时。参数说明:HourStart和HourEnd是input变量,例如想只在8点到12点交易,就设置HourStart=8、HourEnd=12。这个过滤能让你避开多数非农、CPI公布时段的剧烈滑点,虽然也会让你错过一些好行情,但长期看降低的意外亏损远大于错过的利润。
多策略模块化的核心思路,是把每个逻辑从“过程型代码”改成“对象型代码”,但MQL4没有真正的类对象,所以更实际的做法是用“文件拆分 + 全局结构体”。你可以把OnTick()里原先那一大段均线交叉判断,替换成调用独立的信号函数,像这样:
void OnTick() { if(!IsTradingTimeAllowed()) return; if(EMACrossSignal() == BUY_SIGNAL) { ExecuteStrategy("EMA_Cross", OP_BUY); } else if(EMACrossSignal() == SELL_SIGNAL) { ExecuteStrategy("EMA_Cross", OP_SELL); } // 以后再加入RSI信号,只需再写一个 RSIOverSoldSignal() 判断 }这样改之后,你会发现新增策略几乎不动原有代码。每个策略函数只负责返回信号类型,下单和风控集中在ExecuteStrategy()里统一处理。我这个习惯是从某位前辈那边学来的,最初代码全堆在一个OnTick()里,加一个过滤条件就要改半天,后来把逻辑拆开后,反而更容易发现重复下单的问题。实盘的另一个好习惯是:每次修改EA,一定要改版本号并输出到日志——否则你可能自己都忘了当前挂在图表上的代码是哪一版。我吃过一次亏:修改了止损逻辑,忘了重新编译挂载,结果图表上跑的还是旧版,白白亏损了一笔。从那以后,我每次编译后都会看一眼MetaEditor底部的“编译成功”提示,并强制在OnInit里打印"Ver 1.05"这样的版本号,确保自己用的就是刚改的代码。零基础学MQL,最后学到的不只是写函数,更是建立一套“先验证、再小仓、后实盘”的交易工程思维。希望这篇笔记能帮你少踩几个坑,也祝你第一个EA顺利跑起来。
本文还有配套的精品资源,点击获取