
简介MT4自动交易系统资料包汇集了多个EA策略面向外汇交易者、策略开发者及量化爱好者帮助理解EA原理、策略编写与实战运用。包内含151个文件以mq4源码与ex4编译版为主搭配doc格式交易说明、rar/zip策略包、tpl模板及少量辅助文档共约16.99MB结构清晰便于对照学习。已有997人学习下载。内容涵盖海豚交易系统、多均线交叉EA、资金管理指标等多个经典策略不仅提供可直接运行的EA文件还附有交易规则、亏损认知、纪律执行等文字资料与配套录音方便读者从理论到实战完整掌握。无论是新手入门还是老手优化都能从中获取策略逻辑、参数调整与风险管理的实用参考适合在模拟账户中先行回测验证后再投入实盘。1. MT4自动交易系统到底在替你做什么先把EA的边界说清MT4自动交易系统最直接的形态是一段挂进 MetaTrader 4 图表里的 EAExpert Advisor用 MQL4 编写、编译之后随图表运行的执行程序。它解决的问题从来不是“帮你想策略”而是“替你把已经想清楚的规则稳定执行出去”。某开发者A同学在 Excel 里把一套双均线突破验证得很漂亮真下了实盘却发现自己凌晨盯不住盘突破点来了不敢追止损到了不忍心砍一犹豫就是一根大阴线。后来他把同一套规则写成 EA挂在图表上行情触发就进场止损自己带后半夜终于不用再守着屏幕数 K 线。这个场景基本就是 MT4 自动交易系统的价值坐标适合手里已经有明确规则、想摆脱情绪干扰、又不想天天盯盘的从业者。也要先说清楚边界EA 能替你执行不能替你判断参数不合理EA 越勤快亏得越快。下面从最小骨架讲到完整策略再落到仓位风控与实盘前最容易翻车的几个细节。2. MT4自动交易系统的骨架与消息驱动先跑通最小EA模板2.1 为什么EA是“被Tick唤醒”的不是边while边跑的很多从 Python 转过来的开发者第一反应是写一个 while(true) 循环挂在图表上等行情。这个思路在 MT4 里是错的。MT4 的 EA 是事件驱动模型你只需要实现三个回调初始化 OnInit、报价到达 OnTick、卸载 OnDeinit。每次平台收到一个最新报价就调用一次 OnTick函数返回后这一轮工作就结束了。EA 不是常驻内存的循环进程而是“被行情叫醒、处理完就睡”的短任务。常见做法是把主逻辑都塞进 OnTick让 EA 每个 Tick 只处理一次状态判断而不是自己循环拉数据。我一般会先用一个打印报价的最小模板验证环境确认自动交易开关、编译流程和日志输出都没问题再接真正的策略。下面这个模板可以直接新建一个 Expert Advisor 粘贴进去。// 最小EA模板每个新Tick打印一次当前报价验证环境是否就绪 #property strict int OnInit() { Print(EA已加载开始监听行情); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { Print(EA已卸载原因代码: , reason); } void OnTick() { double ask SymbolInfoDouble(_Symbol, SYMBOL_ASK); double bid SymbolInfoDouble(_Symbol, SYMBOL_BID); Comment(StringFormat(ask%s bid%s, DoubleToString(ask, _Digits), DoubleToString(bid, _Digits))); }逻辑说明OnTick 是核心入口每次报价更新都会被平台调用Comment 把当前买卖价直接画在图表左上角方便肉眼确认 EA 活着Print 则把信息写入“专家”标签页适合排查历史行为。代码里的 _Symbol 和 _Digits 是当前图表上下文变量你别写死品种名换图表挂载时就不会出现报价取错的问题。参数说明SymbolInfoDouble 返回指定品种的行情属性SYMBOL_ASK 和 SYMBOL_BID 分别对应卖价、买价。注意 MT4 的买价是站在做市商角度说的你要市价买入时成交在 ASK市价卖出时成交在 BID。这个方向感如果一开始没建立起来后面写订单函数时会连续翻车。还有一条新手最容易忽略的硬条件MT4 窗口右上角的“自动交易”按钮必须点亮同时工具栏里的“算法交易”开关也要保持打开否则 EA 编译通过、日志也打印就是不下单。这不是代码 bug是运行权限问题。2.2 把“策略状态”放在哪里内部变量、静态变量与订单记忆EA 处理的是离散的事件流每个 Tick 之间需要记住一些状态上次是否已经开仓、当天已经亏了多少、上一次均线是多还是空。这些状态可以放在普通全局变量里但你要清楚它们的生命周期——EA 被移除、重新编译或切换周期时普通变量会被清零。如果需要“重载后依然记得”的状态常见做法是两种一是用 static 修饰的局部变量二是用 MT4 自带的全局变量库GlobalVariableSet/GlobalVariableGet。前者适合只活在单次加载生命周期里的临时记忆后者适合需要跨图表、跨 EA 保存的标记。我一般只在必要的时候用全局变量库因为它会留在终端里哪天忘了清理会被其他 EA 误读到。// 用静态变量记录上一次信号方向避免每个Tick重复判断 void RememberSignal() { static int lastDirection 0; // 0无信号 1多头 -1空头 if (lastDirection ! 1) { // 这里写“由空转多”的逻辑分支 } }参数说明lastDirection 是 static 变量函数退出后值仍然保留下一次 OnTick 再进来时还能读到。这个做法在跨 Tick 的状态记忆里非常实用而且比全局变量干净——它只在当前 EA 实例内有效不会污染其他图表。需要警惕的是“重载后失忆”这件事第 5 章会展开讲它带来的实盘隐患。2.3 下单前的运行环境检查自动交易开关、账户状态与最小手数真正写策略前我建议先做一个 CanTrade 环境检查函数。原因是在实盘里EA 会碰到“编译正常但就是不动手”的尴尬情况有时是自动交易按钮被手动关掉了有时是账户处于只读模式有时是当前品种的最小手数和你写死的手数对不上。// 统一检查交易环境返回true才允许继续执行订单逻辑 bool CanTrade() { if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Print(自动交易开关未打开); return false; } if (!MQLInfoInteger(MQL_TRADE_ALLOWED)) { Print(当前图表不允许交易请检查算法交易权限); return false; } // 有些账户在凌晨维护时段处于不能下单的状态 if (AccountInfoInteger(ACCOUNT_TRADE_MODE) ACCOUNT_TRADE_MODE_DEMO) { // 模拟盘和实盘逻辑一致这里只是占位说明 } return true; }逻辑说明TerminalInfoInteger 读的是终端层面的开关MQLInfoInteger 读的是当前 EA 运行上下文是否被允许交易。两个都通过才说明“你有权限下单”。把这段检查放在 OnTick 最前面能避免大量无意义的失败后重试。参数说明ACCOUNT_TRADE_MODE 的取值用来区分模拟盘和实盘账户很多经纪商的模拟账户设置了不同的最小手数和杠杆程序里不要假设两者规则一致。最小手数的问题则要单独查品种属性在 4.1 节的手数计算里会专门处理。3. 把一套双均线突破写成完整EA信号、下单与回测路径3.1 策略定义与参数表先定规则再写代码双均线突破是 MT4 自动交易系统里最容易被讲成“玩具”的策略但它确实适合做落地样板规则清晰、参数少、下单路径短能完整覆盖“识别信号—检查持仓—执行下单”三段流程。用它跑通后把信号部分换成布林带、唐奇安通道或者时段过滤结构都不用大改。策略规则定义如下快线从下往上穿越慢线开多单快线从上往下穿越慢线开空单。同一时间只允许存在一个仓位方向不对时先平旧仓再开新仓。初始版本不做马丁、不加网格每单固定手数。参数表如下参数名默认值说明FastPeriod10快线均线周期SlowPeriod40慢线均线周期必须大于快线LotsPerOrder0.01每单单量MagicNumber2025001订单魔术号用于区分其他策略单MaxSpreadPoint30最大允许点差点差太大不进场这里要特别解释 MagicNumber。MT4 账户里可能同时有多个 EA 在跑也可能有手工单混在里面。程序判断“这个订单是不是我的”时不能只看品种和方向必须通过魔术号识别。否则会在平仓阶段把别人的单子一起平掉这是自动交易系统里最危险的低级错误。3.2 完整EA信号判断、防重复开仓与OrderSend执行下面是一份可以直接放进编辑器的完整双均线 EA。为了保持可读性我没有用复杂的类封装全部用经典 MQL4 函数实现原因是 OrderSend/OrderSelect 的调用链在新手排查时最直观。#property strict input double LotsPerOrder 0.01; input int FastPeriod 10; input int SlowPeriod 40; input int MagicNumber 2025001; input int MaxSpreadPoint 30; int OnInit() { if (FastPeriod SlowPeriod) { Print(慢线周期必须大于快线周期); return INIT_FAILED; } return INIT_SUCCEEDED; } void OnDeinit(const int reason) {} // 检查当前是否存在本EA的持仓 bool HasMyPosition() { for (int i OrdersTotal() - 1; i 0; i--) { if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if (OrderMagicNumber() MagicNumber OrderSymbol() _Symbol) return true; } return false; } void OnTick() { if (!CanTrade()) return; if (HasMyPosition()) return; // 已有持仓不再新开 // 点差过大时不追单 double spread (SymbolInfoDouble(_Symbol, SYMBOL_ASK) - SymbolInfoDouble(_Symbol, SYMBOL_BID)) / _Point; if (spread MaxSpreadPoint) return; double fastNow iMA(_Symbol, _Period, FastPeriod, 0, MODE_SMA, PRICE_CLOSE, 0); double slowNow iMA(_Symbol, _Period, SlowPeriod, 0, MODE_SMA, PRICE_CLOSE, 0); double fastPrev iMA(_Symbol, _Period, FastPeriod, 0, MODE_SMA, PRICE_CLOSE, 1); double slowPrev iMA(_Symbol, _Period, SlowPeriod, 0, MODE_SMA, PRICE_CLOSE, 1); // 金叉前一根快线在上方当前快线穿越到慢线上方 if (fastPrev slowPrev fastNow slowNow) { int ticket OrderSend(_Symbol, OP_BUY, LotsPerOrder, SymbolInfoDouble(_Symbol, SYMBOL_ASK), 30, 0, 0, double-ma, MagicNumber, 0, clrNONE); if (ticket 0) Print(开多失败错误码: , GetLastError()); } // 死叉开空单 if (fastPrev slowPrev fastNow slowNow) { int ticket OrderSend(_Symbol, OP_SELL, LotsPerOrder, SymbolInfoDouble(_Symbol, SYMBOL_BID), 30, 0, 0, double-ma, MagicNumber, 0, clrNONE); if (ticket 0) Print(开空失败错误码: , GetLastError()); } }逻辑说明这里用“前一根值对当前值”的比较来判断交叉而不是直接用两根最新值做大小判断是因为当前 K 线还没收盘时价格会反复变化用未收盘 K 线触发信号很容易出现“信号消失”。前一 bar 是已收盘数据相对可靠。HasMyPosition 的作用是让 EA 在持仓期间不再重复开仓这是防止 Tick 高频重复下单的第一道闸。参数说明OrderSend 里第 4 个参数买入用 SYMBOL_ASK、卖出用 SYMBOL_BID这是 MT4 的硬规则。第 5 个参数 30 是最大滑点点数超过这个值订单会被拒绝实际执行中如果你遇到大量滑点拒绝可以适当调大但回测里的盈利也会相应缩水。止损止盈这里先传 0由第 4 章的风控模块统一管理保持关注点单一。3.3 在策略测试器里验证品种、周期与三个时段缺一不可代码编译通过后不要直接挂实盘。我一般会在策略测试器里按固定顺序过三遍第一遍用 EURUSD H1第二遍换成品种所属的原始周期再跑第三遍用最近三到五年的完整数据跑一次长周期。测试器里有几个关键选项会直接决定结果质量。第一个是“建模”方式。默认的“每个报价”模式最接近真实行情回测速度慢但可靠“开盘价”模式速度快适合筛参数但会把很多盘中波动过滤掉。你如果在开盘价模式下验证双均线突破会发现信号数量比每个报价模式少一大截这不是策略问题是数据采样密度问题。第二个是“点差”设置。回测时把点差设成当前真实环境的平均值加 1 到 2 个点结果才不会被实盘打脸。第三个是日期范围至少分成三段一段做参数摸索一段做验证一段做外推观察。单一时间段的漂亮曲线没有任何说服力。4. 仓位管理与风控模块把人工纪律写进代码4.1 每单风险百分比开仓手数由“亏多少”倒推固定手数的问题在于账户资金减少时同样 0.01 手占用的风险比例在悄悄变大账户资金增长时固定手数又让利润跑不赢波动。行业里更稳妥的常见做法是按“每单风险比例”倒推手数先定好这一单最多亏账户权益的百分之几再根据止损距离换算手数。下面这个函数把风险计算和品种最小交易量约束一起处理了。它假设一个简化的点值模型把“标准手每点美元价值”作为输入参数不同品种再单独校准。input double RiskPercent 1.0; // 每单风险占权益的百分比 input double USDPerLotPerPip 10.0; // 一个标准手每点的美元价值主流货币对约等于10 double CalcRiskLot(double stopLossPips) { double equity AccountInfoDouble(ACCOUNT_EQUITY); double riskMoney equity * RiskPercent / 100.0; double lots riskMoney / (stopLossPips * USDPerLotPerPip); double minLot SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); double step SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); lots MathFloor(lots / step) * step; // 向下取整到经纪商允许的步进 if (lots minLot) return 0.0; // 低于最小手数就放弃这单 return lots; }逻辑说明先用账户权益乘以风险百分比得出“允许亏损的金额”再除以止损距离折算出的金额得到理论手数。关键一步是向下的步进取整因为很多经纪商的最小手数增量是 0.01你算出 0.015 手也不一定能下得去。取整之后再和最小手数比较低于下限返回 0调用方看到 0 就放弃开仓避免用 0.001 手去撞最小手数限制。参数说明USDPerLotPerPip 是我有意暴露出来的简化参数它假设美元计价账户下主流货币对的一标准手一点价值接近 10 美元。黄金、指数这类品种完全不适用换品种前必须重新校准。这个函数是“保守型”做法取整只往下不往上宁可少开也不让风险超标。4.2 移动止损与保本出场只往前推绝不手动后退很多 EA 的亏损不是开仓方向错而是拿不住盈利单赚了 20 个点就手动平仓后面那波 100 点行情全错过或者浮盈回吐眼看着利润变成亏损。把移动止损写进代码是为了让出场规则脱离人的情绪。input int TrailPips 40; // 盈利超过40点后启动移动止损 input int BreakEvenPips 10; // 保本阈值 void TrailStop() { for (int i OrdersTotal() - 1; i 0; i--) { if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if (OrderMagicNumber() ! MagicNumber || OrderSymbol() ! _Symbol) continue; double open OrderOpenPrice(); double oldSL OrderStopLoss(); double newSL 0; if (OrderType() OP_BUY) { if (Bid - open TrailPips * _Point) continue; newSL open BreakEvenPips * _Point; } else if (OrderType() OP_SELL) { if (open - Ask TrailPips * _Point) continue; newSL open - BreakEvenPips * _Point; } // 只推进不后退新止损比旧止损更有优势时才修改 if ((OrderType() OP_BUY newSL oldSL) || (OrderType() OP_SELL newSL oldSL)) { if (!OrderModify(OrderTicket(), open, newSL, OrderTakeProfit(), 0, clrNONE)) Print(移动止损失败错误码: , GetLastError()); } } }逻辑说明这个函数的特点是“只朝有利方向移动”。买入单的止损只会往上抬卖出单的止损只会往下压通过 newSL 与 oldSL 的比较实现。它还有一个隐性保护如果 EA 重启后旧止损已经比 newSL 更优就不会执行修改避免把止损改坏。参数说明TrailPips 设得越小移动越积极但容易被正常波动扫掉40 这个值在 H1 周期双均线策略里是一个相对中庸的起点。BreakEvenPips 是保本平移阈值设 0 就是刚回本就保住。需要理解的一点是这套移动止损并没有跟踪均线或结构位它是纯价格距离驱动优点是逻辑透明缺点是震荡行情里会频繁改单。4.3 单日亏损熔断与连亏停开把人工止损写进代码连续亏损时最容易失控的是人不是策略。EA 的优势在于可以在你情绪爆发前强制熔断。我习惯在 EA 里加两层限制单日亏损达到阈值后当天不再开新仓连续亏损达到固定次数后自动停到第二天再恢复。input double DayMaxLossPercent 5.0; // 单日最大亏损比例 input int MaxConsecutiveLoss 3; // 连续亏损达到3次暂停 double dayStartEquity 0; int consecutiveLoss 0; void ResetDayStateIfNeeded() { static datetime lastDay 0; datetime today iTime(_Symbol, PERIOD_D1, 0); if (today ! lastDay) { lastDay today; dayStartEquity AccountInfoDouble(ACCOUNT_EQUITY); } } bool DailyLossBreached() { double curEquity AccountInfoDouble(ACCOUNT_EQUITY); double ddPercent (dayStartEquity - curEquity) / dayStartEquity * 100.0; return ddPercent DayMaxLossPercent; } bool ConsecutiveLossBreached() { return consecutiveLoss MaxConsecutiveLoss; }逻辑说明ResetDayStateIfNeeded 在每个 Tick 开头调用通过比较 iTime 取到的交易日日期是否变化来判断是否进入新的一天。如果跨日就把当日初始权益重置一次。DailyLossBreached 用初始权益和当前权益的差值计算亏损比例触发后 OnTick 主流程直接 return不再执行任何开仓逻辑。参数说明DayMaxLossPercent 的 5 是相对激进的默认值保守交易者可以压到 2。consecutiveLoss 的更新应该放在每笔订单平仓后的回调里平仓亏损就自增盈利就清零。这个模块的价值在于把“今天不能再做了”从你的自觉里剥离出来变成程序内置的强制规则。5. 从回测到实盘的六条排查与避坑记录5.1 数据与执行层面的四类翻车现象一回测时策略看起来有信号但报告显示 0 笔交易。原因通常是测试器的“建模”方式选择了“仅用开盘价”这类模式下数据密度太低K 线内部的交叉没有暴露给 EA也可能是历史数据本身缺失尤其某些品种的凌晨时段根本没有报价。解决把测试器建模方式切到“每个报价”再到“品种列表”里右键选择“更新数据”把对应周期数据补全后重新回测。现象二同一个 EA回测净利润不错实盘跑一个月明显缩水。原因基本是点差、手续费和滑点没在回测里体现。解决测试器设置里把点差改成当前真实环境平均值加 1 到 2 个点同时把 EA 里的滑点上限从 30 提升到 50 再观察是否出现大量拒单。回测时越接近“苛刻环境”实盘落差越小。现象三实盘运行中日志突然出现大量“trade context busy”或下单失败。原因很可能是快速行情下 OnTick 密集触发上一次 OrderSend 还没完全返回下一次调用又挤进来了。解决在 OnTick 开头加一个简单的防重入判断用静态变量记录上一次订单操作的时间间隔小于 500 毫秒就跳过本次下单给平台留出处理间隙。现象四开仓后订单里没有止损价格或者止损被下到了奇怪的位置。原因有时是 EA 里把止损写成了硬编码数值换到黄金或指数品种后点数含义完全不同。解决止损一律用 _Point 做单位换算并根据品种的 SYMBOL_TRADE_STOPS_LEVEL 检查最小止损距离太近就拒绝下单而不是硬着头皮下。5.2 状态与参数层面的两个陷阱现象五MT4 重启之后EA 像失忆了一样重新开仓账面出现双倍仓位。原因很简单EA 内部静态变量在重启后被清零持仓判断 HasMyPosition 以为“我没有仓位”于是又开了一单。解决在 OnInit 里增加一个恢复流程启动时扫描当前账户所有订单把魔术号匹配的持仓重新“记入”状态再允许 OnTick 执行开仓逻辑。void RestoreStateFromOrders() { for (int i OrdersTotal() - 1; i 0; i--) { if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if (OrderMagicNumber() ! MagicNumber) continue; if (OrderSymbol() ! _Symbol) continue; // 把持仓状态恢复到内部标记 Print(检测到已有持仓票号: , OrderTicket()); } }现象六同一个策略、同一组参数在一个品种上回测很好换到另一个品种就明显变差。原因不是玄学而是不同品种的点值、点差结构、停损水平和波动周期差异太大。解决参数优化结果不要跨品种套用每换一个品种就重新做一次三段式回测那种号称“全品种通用”的参数组合多数是在一两个品种上人工拟合出来的。6. 参数寻优的验证技巧把“有效参数”从过拟合里捞出来6.1 三段式样本划分选参、验证、外推MT4 自带的优化功能很好用但它只告诉你“历史哪组参数赚得多”不告诉你“未来哪组参数还能活”。我见过太多人把优化结果里的第一名直接挂实盘结果前两个月表现不错第三个月遇到和回测段完全不同的波动结构回撤直接击穿底线。更可靠的方式是手工做三段式样本验证。样本段主要用途常见操作第一段早期行情参数寻优关闭遗传算法做网格扫描选出前 20 组表现较好的参数第二段中期行情参数验证用第一段选出的参数做单次回测观察净利润和回撤是否稳定第三段近期行情外推观察不重新优化直接把选定的参数跑到最新数据看是否还成立我一般把总历史数据按 6:2:2 切分。第一段跑优化时注意MT4 的优化结果默认按净利润排序你要手动再筛一遍交易次数和最大回撤。净利润极高但总共只成交 5 笔的参数基本是运气净利润中等但成交 300 笔以上、回撤控制在 20% 以内的参数才更接近“有效参数区间”。6.2 只看净利润是最大的参数陷阱“利润高不高”只是结果“参数附近稳不稳”才是真相。把优化结果导出你会看到一组很直观的规律有些参数周围全是相近盈利改几个周期值曲线变化不大有些参数是孤峰周围一改就变成亏损。孤峰就是典型的过拟合特征实盘中行情稍有偏移参数就不工作了。我习惯把最终选出参数的 FastPeriod、SlowPeriod 分别加减 2 到 3 个周期再各跑一遍如果三次结果的方向和盈亏形态基本一致才把它定为候选参数。另一个实用技巧是让参数保持“整数化”避免出现像 FastPeriod 12.7 这种过于精细的取值——真实规律很少需要精确到小数过于精细的取值往往是在拟合噪音。我的习惯是每次实盘前先问自己三个问题——参数附近波动的结果稳不稳、最差三个月回撤自己能不能睡着、回测点差和滑点设置是不是比真实环境更苛刻。如果有一个答案是否定的就继续回去调整而不是急着把 EA 挂上实盘。这套验证流程救过我很多次希望帮到你。本文还有配套的精品资源点击获取