资讯动态

零基础学MQL4:从EA构建到回测优化的完整实战指南

发布时间:2026/10/17 11:42:18 来源:尧图企业网站定制
简介这份《零基础学MQL基于EA的自动化交易编程》docx文档面向希望踏入程序化交易领域的交易者与初学者系统讲解MQL语言在MetaTrader 4/5平台上的应用涵盖自动化交易概念、EA智能交易程序编写、语法特性与常用函数等核心内容帮助读者从零掌握策略自动化与风险管理的编程思路。资料包含1个docx文档整体大小仅29KB文档结构完整开篇概述自动化交易与MQL价值随后深入EA开发基础、数据类型、变量常量与交易函数适合零基础读者按章节循序渐进学习。该资源已有1350人学习下载轻量却覆盖了从概念、语法到实践落地的主要知识点既可作入门自学材料也可在编写EA、调试自动化策略时快速查阅参考。1. 零基础学MQL先搞懂EA才是你入门的捷径打开行情软件按下“自动交易”按钮却不知道那一行行MQL代码到底怎么让机器人替你下单——这是很多交易新手卡住的地方。MQL不是玄学它是一套专门为交易平台设计的编程语言而EAExpert 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.01MODE_MINLOT和MODE_MAXLOT返回该品种允许的手数上下限。用MathFloor向下取整确保计算出的手数不会超过风险预算。参数说明比例仓位看起来科学但有一个隐患——EA回测时如果账户余额增长手数会不断变大导致后期波动剧烈余额缩水时手数变小又很难回本。所以很多实战EA干脆用固定手数跑一段时间人工调整后再换。我不建议新手一上来就用比例仓位先用固定0.01把手数逻辑跑通再研究资金曲线平滑的问题。4. 回测与参数优化在策略测试器里验证EA的3个关键步骤写过EA之后你必须学会在策略测试器里验证它。很多人写好了直接挂实盘那是拿自己的真金白银做实验。正确的流程是先做历史回测再根据不同参数组合做优化最后在模拟盘上跑几周。回测不是看盈利金额有多大而是看资金曲线是否平滑、回撤是否在你能容忍的范围内。4.1 回测前的检查清单品种、周期、时间范围、初始资金打开MT4的策略测试器CtrlR第一件事不是点“开始”而是核对测试设置。我每次回测前都会检查这四项设置项常见错误正确做法测试品种忘了切换测了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点交易就设置HourStart8、HourEnd12。这个过滤能让你避开多数非农、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顺利跑起来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑