简介一份面向外汇自动交易学习者与MetaTrader策略开发者的MQL4源码文档围绕锁仓、顺势加码与对冲单管理展开适合用来理解EA如何结合波动率自动处理手动订单止盈、平仓及对冲逻辑。压缩包共1个doc文件约45KB篇幅集中便于直接阅读并对照代码结构。文档保留了Hedge_Assistante、start等核心函数说明并列出USE_ATR、PercentATR40、HedgingLevel30、AutoTakeProfit等可调参数结合ATR、iHigh/iLow指标展示波动率计算与止盈水平设置思路对拆解手动单识别、GBPUSD与其他货币对止盈差异、OrderClose与OrderModify调用时机较有帮助。已有1381人学习可作为EA入门到进阶阶段研究锁仓顺势加码策略的参考材料。1. 一份 2012 年的 MQL4 对冲助手它到底在做什么压缩包里只有一个.mq4文件头部注释停在 2012-02-17版本记录写到 V2.2。在外汇 EA 源码里它算短的那一类两百行不到没有指标信号没有趋势判断只做一件事——盯住你手动下的单按 ATR 算一段距离在你成本价的另一侧挂一张反向 STOP 单锁住顺带可选地替你把手动单的止盈改成固定点数。标题里的“顺势加码”在代码里其实没有对应实现真正的逻辑是“锁仓 对冲单配对”。它适合谁已经能看懂OrderSend、OrderSelect这几套调用想把锁仓这类逻辑落到 MQL4 代码里的人或者手上有一份同年代的 EA想拿它当参照物对照写法的人。源码里那几个真实的 bug 比功能本身更值得拆。2. 拆开 Hedge_Assistante手动单识别与对冲单配对整个 EA 的入口极短start()里只调了一次Hedge_Assistante()就return(0)。所有逻辑都压在Hedge_Assistante()这一个函数里按顺序做四件事给手动单设止盈、给没对冲的手动单补一张对冲单、给失去对手盘的对冲单删掉、给已有对冲单的手动单把止盈清零。先把这四步的判据看清楚后面改起来才不会越改越乱。2.1 用 MagicNumber0 和 OrderProfit()!0 判断“这是人下的单”MQL4 里没有“订单来源”这个字段EA 和人工单混在一个持仓池里只能靠约定区分。这份源码的约定是EA 自己下的单一定带非零 magic人手下的单 magic 恒为 0。// 遍历当前持仓与挂单挑出本品种、无 magic、有浮动盈亏的单 for(i 0; i OrdersTotal(); i) { if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) false) continue; if(Symbol() OrderSymbol() OrderMagicNumber() 0 OrderProfit() ! 0) { // 到这里就认为是一张“手动单” Ticket OrderTicket(); Type OrderType(); Lots OrderLots(); OpenPrice OrderOpenPrice(); } }三个条件缺一不可。Symbol()OrderSymbol()把范围锁在本图表品种上否则一个账户跑多个品种时会互相干扰OrderMagicNumber()0是区分人工单和 EA 单的唯一依据OrderProfit()!0的真实用途是把未成交的挂单排掉——BUY LIMIT、SELL STOP 这类单在成交前浮动盈亏恒为 0用这个判据比用OrderType()1更省事但代价在第 5 章会讲。参数上SELECT_BY_POS表示按持仓列表索引取单MODE_TRADES限定只取当前持仓和挂单不含历史单。如果换成MODE_HISTORYOrderProfit()拿到的是已平仓盈亏判据就完全失效了这一点在改代码时最容易写错。2.2 对冲单怎么挂把 Ticket 写进 MagicNumber配对关系的设计是这份源码里最巧的一处手动单的 ticket 号直接当成对冲单的 magic 号写进去。这样一对单向两个方向都能查——从手动单找对冲单用 magic 反查从对冲单找手动单用 magic 当 ticket 去OrderSelect。// 手动单是 BUY就在成本价下方挂 SELL STOP 锁住 Tic OrderSend(Symbol(), OP_SELLSTOP, Lots, NormalizeDouble(OpenPrice - HedgingLevel, Digits), 0, 0, 0, Hedging DoubleToStr(Ticket, 0), Ticket, 0, Green); if(Tic 0) Alert(Ticket, 的对冲单:, Tic, 生成 SELL STOP:, NormalizeDouble(OpenPrice - HedgingLevel, Digits), , Lots, Lots);OrderSend的十一个参数按位置捋一遍品种、订单类型、手数、价格、滑点挂单无滑点填 0、止损0 表示不设、止盈0 表示不设、注释、magic、过期时间0 表示一直有效、箭头颜色。价格必须过一遍NormalizeDouble(..., Digits)否则五位报价平台上算出1.0802349999这种尾数服务器会直接返回 130 错误invalid stops。反向的清理逻辑在第二段循环里拿对冲单的 magic 去和所有手动单的 ticket 比对比对不上说明对手盘已经平掉了这张对冲单就成了裸单用OrderDelete删掉。源码里还加了一层OrderType()1的保护只删挂单不删市价单——因为对冲单一旦成交就变成了市价持仓删不掉只能平。这个限制在 V2.1 的版本注释里写得很明白“对于已经成交的对冲单系统不能自动平仓或删除需要手动平仓。”2.3 在 MT4 里把它跑起来目录、编译、参数落地步骤没什么玄学但顺序错了会白折腾半小时。终端里点“文件 → 打开数据文件夹”进MQL4/Experts把.mq4丢进去回到 MetaEditor 按 F7 编译。编译通过后回终端在“导航”面板的“EA 交易”上右键刷新文件才会出现。参数类型默认值实际作用USE_ATRbooltrue是否用 ATR 动态算对冲距离关掉则用固定点数PercentATRint40对冲距离占当日 ATR(14) 的百分比HedgingLeveldouble30USE_ATRfalse时生效单位是“点”AutoTakeProfitboolfalse是否替手动单做固定点数止盈挂到图表上之前先在终端设置里勾上“允许自动交易”EA 属性里也要勾“允许实时交易”两个开关任何一个没开OrderSend都会静默返回 -1日志里只有一行 4109 错误。第一次跑建议AutoTakeProfitfalse只观察挂单行为确认HedgingLevel算出来的距离合理再打开止盈。源码顶部#include stdlib.mqh是为了用ErrorDescription(GetLastError())把错误码翻成人话这个头文件在老版本里是标准件缺了会编译不过。3. ATR 动态对冲距离与固定点数止盈锁仓的核心不是“锁”这个动作是锁在哪个价位。锁得太近一个正常回抽就把两边都打成交白交两笔点差锁得太远等于没锁回撤照样吃满。源码给的解法是用 ATR 把距离和市场波动绑起来波动大就锁远一点波动小就锁近一点。3.1 USE_ATRtrue 时HedgingLevel 变成当日 ATR 的 40%// 用日线 ATR(14) 的 40% 作为对冲距离 if(USE_ATR) HedgingLevel iATR(NULL, PERIOD_D1, 14, 0) * PercentATR / 100.0; else HedgingLevel HedgingLevel * Point; // 这一行有问题见 5.1iATR(NULL, PERIOD_D1, 14, 0)四个参数分别是品种NULL 表示当前图表品种、周期日线、平均周期14、偏移0 表示当前未收盘的那根 K 线。偏移传 0 意味着 ATR 值在整个交易日里是实时变化的——早上算出 0.0090晚上可能变成 0.0130同一张手动单在不同时刻补的对冲单距离不一样。想让距离稳定下来把偏移改成 1取昨天已收盘的日线 ATRdouble atr iATR(NULL, PERIOD_D1, 14, 1); // shift1取上一根已收盘 K 线 double dist atr * PercentATR / 100.0;举个具体数EURUSD 某日 ATR(14)0.0120PercentATR40算出的距离是 0.0048。手动 BUY 开在 1.08500对冲的 SELL STOP 就挂在 1.08020。这个距离约等于 48 个标准点对日内波动来说是个偏保守的锁仓位好处是不容易被正常回抽扫到坏处是真正反转时已经亏掉的这部分锁不住。PercentATR调到 20 附近锁仓位会明显贴近成本价适合波动小的亚洲盘时段调到 60 以上基本等于只锁极端行情。这个参数没有普适值取决于你手动开仓的逻辑本身容忍多大回撤。3.2 手数、点位与 Digits 的换算对冲单的手数是Lots OrderLots()直接复制手动单的持仓量等于一比一完全对冲。账户锁死后净值不再变化但保证金占用翻倍——两个方向的持仓都要占用保证金这是锁仓策略最容易被忽略的成本。double Lots OrderLots(); // 一比一完全对冲 // 半对冲写法double Lots NormalizeLots(OrderLots() * 0.5);NormalizeLots需要自己写或者用MathFloor(lots / MarketInfo(Symbol(), MODE_LOTSTEP)) * MarketInfo(Symbol(), MODE_LOTSTEP)否则 0.15 手的一半算出来是 0.075下单时被服务器按 0.07 或 0.08 截断实际对冲比例和你以为的不一样。点位换算上有个老坑四位报价平台的Point是 0.0001五位平台是 0.00001。源码里NormalizeDouble(OpenPrice - HedgingLevel, Digits)用Digits做规范化避开了硬编码小数位这一点写得没问题。但如果USE_ATRfalse走固定点数分支HedgingLevel30在四位平台上是 30 个点0.0030在五位平台上乘完Point就变成 3 个点距离缩水十倍。跨平台用同一份源码时这个分支必须按Point与 pip 的比例重新标定。顺带说一句标题里的“顺势加码”在代码里找不到实现——没有加仓次数、没有加仓间距、没有盈利触发条件只有锁仓和对冲单维护。如果你要的是马丁或金字塔式加仓这份源码给不了得自己在Hedge_Assistante()之外再开一个函数。3.3 AutoTakeProfitGBPUSD 17 点其他 10 点if(Symbol() GBPUSD) TP 17; else TP 10; // 单位Point if(OrderType() OP_BUY) { // 路径一价格已经走到位直接市价平掉 if(Bid - OrderOpenPrice() TP * Point) { if(OrderClose(OrderTicket(), OrderLots(), Bid, 2, 0) true) { Alert(Good Job! 自动止盈, Bid); return(0); } } // 路径二还没到位把止盈挂到目标价上 if(OrderTakeProfit() - OrderOpenPrice() (TP - 1) * Point || OrderTakeProfit() - OrderOpenPrice() (TP 1) * Point) { OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), OrderOpenPrice() TP * Point, 0, 0); } }两条路径并存的原因很实际MT4 的止盈单在极端行情下可能因为跳空被滑过成交价所以 EA 每个 tick 自己检查一次当前价到位就主动平。OrderClose第五个参数 2 是允许的滑点注释里写的是 2 个点。OrderModify的六个参数订单号、价格对已成交单无实际作用、止损、止盈、过期时间、箭头颜色。这里止损传的是OrderStopLoss()即保留原止损不动只改止盈。容差是(TP-1)*Point到(TP1)*Point一个最小报价单位的宽度。四位平台上这是 0.1 个点五位平台上也是 0.1 个点浮点误差加上服务器端的价格规范化很容易在这个窗口里反复触发OrderModify日志刷屏还吃服务器限流具体怎么改在 5.5 展开。还有一处设计细节要注意OrderClose成功后直接return(0)跳出整个函数。也就是说一个 tick 只处理一张单账户里同时挂三张手动单的话止盈要三个 tick 才能转完一圈。对日内交易影响不大但如果手动单很多这个“一次一张”的节奏会让你觉得 EA 反应慢。4. 编译、回测与日志验证这份代码写于 2012 年那时候的 MQL4 编译器宽松得离谱变量可以隐式声明for循环里声明的变量出了循环还在。现在的终端用的是 build 600 之后的严格模式直接拿原始文件编译大概率报错得先做几处兼容处理。4.1 老代码在新终端上的编译坑第一处是for (int j i; ...)后面的第二个循环又写了for (j 0; ...)在旧编译器里j的作用域是函数级能编过新编译器里j只属于第一个循环第二处会报 “j - variable not defined”。改法是在函数开头统一声明void Hedge_Assistante() { int i, j, Tic, Ticket, Hedge, Type; // 统一在顶部声明兼容严格模式 double TP; ... }第二处是Comment(msg, Red, 10)。MQL4 的Comment是变参函数能接受任意个参数并全部拼成字符串显示编译不报错但图表左上角会把Red枚举值和10一起印出来变成一段带数字的乱码。想控制颜色和字号得用ObjectCreate建标签或者干脆改成Comment(msg)只输出文本。第三处是start()这个入口。build 600 之后标准入口是OnTick()但start()作为兼容别名仍然会被调用不用急着改名。真要改的话把函数体原样搬进OnTick()即可return语句不用动。第四处是Alert的行为差异。实盘里Alert会弹窗加提示音每改一次止盈弹一次账户开着没人管的时候能被弹到怀疑人生。策略测试器里Alert不弹窗只写进日志所以回测时看着干干净净上实盘才发现被弹窗淹没。测试阶段建议把Alert全换成Print实盘前再挑关键节点留一两个。4.2 用策略测试器验证逻辑不是验证收益这份 EA 没有入场信号只有持仓管理逻辑所以回测收益曲线没有参考价值——它不会自己开仓跑一天下来一笔交易都没有。测试器在这里的用途是验证配对逻辑对不对而不是看赚了多少。思路是先伪造一张“手动单”再观察 EA 有没有正确给它挂上对冲。MT4 测试器的可视化模式里不能手动下单所以常见做法是在start()里临时插一段测试代码// 仅在测试环境启用伪造一张 magic0 的“手动单”用于观察对冲配对 void Mock_Manual_Order(int type, double lots) { double price (type OP_BUY) ? Ask : Bid; // magic 传 0 才是关键这样 EA 才会把它当成人工单 int t OrderSend(Symbol(), type, lots, price, 3, 0, 0, MOCK, 0, 0, CLR_NONE); if(t 0) Print(mock manual order ticket, t); else Print(mock failed, err, GetLastError()); }调用时机放在start()最前面加一个“当前没有持仓才下单”的判断避免每 tick 都开一张。跑起来之后日志里应该按顺序出现三行mock manual order ticket12345、12345的对冲单:12346 生成 SELL STOP:...、以及若干次止盈相关的记录。三行都齐了说明 ticket→magic 的绑定关系生效了。回测周期建议选 M5 或 M15模型选“每个 tick”区间挑一段有明显单边行情的日子。单边行情下行时对冲单会被触发成交正好能验证 2.2 里提到的那条限制——成交后的对冲单不会被自动删除只会在对手盘平掉时变成裸单。4.3 日志怎么读Alert 与 Print 的分工Alert、Print、Comment三个输出通道用途完全不同混着用会看瞎。Comment画在图表左上角每 tick 刷新源码用它显示 ATR(14) 和当日的AtrHi、AtrLo这是观察动态距离最省事的方式Print只进日志适合放循环里的调试信息Alert弹窗加声音只留给“单子真的挂上了”和“下单失败”这两个节点。排查时按关键词 grep 日志能覆盖九成问题日志关键词含义出现后先看什么的对冲单:对冲单挂单成功挂单价与成本价的距离是否等于预期设置止盈止盈修改成功是否在同一秒内重复出现Modify TakeProfit error修改止盈失败后面的错误码130/131 是价格问题1 是连接问题Close Order err主动平仓失败是不是市场已关闭或报价过期错误码能翻成人话是因为头部#include stdlib.mqh里的ErrorDescription()。如果日志里出现的是纯数字没有描述说明这个头文件没引进来ErrorDescription返回了空串。5. 避坑五处会让你真金白银翻车的地方这份源码最值钱的地方不是功能是它踩过的坑都留在代码里了。下面五条都是按“现象 → 原因 → 解决”记录的其中 5.1 属于看一眼就该改的级别不改的话 EA 挂上去基本等于没锁。5.1 HedgingLevel 每个 tick 被乘一次 Point距离缩到趋近于零现象设USE_ATRfalse、HedgingLevel30挂出来的对冲单几乎贴着成本价肉眼看上去两条线重叠日志里的挂单价和开仓价只差零点几个点。原因HedgingLevel HedgingLevel * Point;这一行直接改了extern变量本身。第一个 tick 把 30 变成 0.0030第二个 tick 再乘 0.0001 变成 0.0000003指数级衰减几十个 tick 之后距离就小到NormalizeDouble都抹平了。解决extern参数只读不改用一个局部变量承接计算结果。double dist; if(USE_ATR) dist iATR(NULL, PERIOD_D1, 14, 1) * PercentATR / 100.0; else dist HedgingLevel * Point; // 只在本地算不回写 extern double hedgePrice NormalizeDouble(OpenPrice - dist, Digits);改完再看日志挂单价距离应该稳定在HedgingLevel * Point不再随时间变化。这条如果不改USE_ATRfalse这个分支基本是废的只能用 ATR 模式。5.2 正向循环里删单后面的单被静默跳过现象账户里同时有三张失去对手盘的对冲挂单理论上应该全删实际只删掉一张或两张剩下的要等下一轮才清。原因OrderDelete执行后持仓列表会立刻重排被删位置后面的单整体前移一位而循环变量i照常i正好跳过前移上来的那张。这是 MQL4 里最经典的一个索引陷阱。解决倒序遍历删完直接continue不要在同一轮里再对同一个索引做OrderSelect。for(i OrdersTotal() - 1; i 0; i--) // 从后往前删单不影响未遍历部分 { if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES) false) continue; if(OrderMagicNumber() 0) continue; // 只处理对冲单 if(OrderType() 1) continue; // 已成交的对冲单删不掉跳过 if(!HasCounterpartOrder(OrderMagicNumber())) // 自己封装的配对检查 OrderDelete(OrderTicket()); }同样的倒序原则也适用于OrderClose循环只要循环体里有任何会改变持仓总数的操作就必须倒着走。5.3 Sleep(10000) 把 EA 卡死十秒快行情下漏掉整段走势现象行情剧烈波动时日志里出现十秒以上的空白段止盈明显滞后手动单该平的没平。原因OrderModify成功后跟了一句Sleep(10000)。MT4 的 EA 跑在单线程里Sleep期间不处理任何新 tick价格在这十秒里怎么走EA 一概不知道。作者的意图大概是防止OrderModify被服务器限流但代价太大。解决去掉Sleep改用时间戳做冷却让 EA 每个 tick 都能醒着看一眼行情。static datetime lastMod 0; if(TimeCurrent() - lastMod 60) // 同一张单 60 秒内最多改一次 { if(OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), OrderOpenPrice() TP * Point, 0, 0)) lastMod TimeCurrent(); }static变量在两次 tick 之间保持值不变正好用来做节流。冷却时间按品种定主要货币对 30 到 60 秒足够交叉盘可以放宽到 120 秒。5.4 OrderProfit()!0 判活单恰好保本的手动单被漏掉现象偶尔出现两张一模一样的对冲单手数、价格都重复账户保证金莫名多占一份。原因手动单的浮动盈亏正好为 0 的那一瞬间价格刚好回到开仓价OrderProfit() ! 0判为假这张单被当成“不存在”循环走完发现没有对冲单于是又挂了一张。解决判活单用订单类型不用盈亏。// 市价单类型是 0(BUY) 和 1(SELL)挂单是 2 以上 bool isManualMarketOrder (OrderMagicNumber() 0 OrderType() 1);改完顺手加一道幂等保护挂对冲单之前先把整个持仓列表扫一遍确认没有任何一张单的 magic 等于当前手动单的 ticket再执行OrderSend。成本是几十次OrderSelect换来的是不会重复挂单。5.5 止盈容差只有 1 个 Point五位报价下会把服务器刷到限流现象日志里同一张单的“设置止盈”连续出现十几条中间夹着Modify TakeProfit error错误码 1 或 130。原因容差窗口是(TP-1)*Point到(TP1)*Point只有两个Point宽。五位报价平台上Point0.00001窗口宽度 0.00002服务器端对价格做四舍五入后经常落在窗口之外EA 于是再改一次来回震荡。另外OrderModify第二个参数传的是OrderOpenPrice()对已成交单来说这个参数会被忽略但传错值在某些桥接环境下会返回 130。解决容差按点算不按最小报价单位算。double targetTP (OrderType() OP_BUY) ? OrderOpenPrice() TP * Point : OrderOpenPrice() - TP * Point; // 偏离超过 3 个 Point 才动手避开浮点误差和服务器取整 if(MathAbs(OrderTakeProfit() - targetTP) 3 * Point) { OrderModify(OrderTicket(), OrderOpenPrice(), OrderStopLoss(), targetTP, 0, 0); }MathAbs比双向比较写起来干净也少了两个容易写反的不等号。冷却节流和这个容差配合用日志会安静很多。6. 把它改成能管控的样子三个最小改动和一张对照表原始代码可以跑但直接挂实盘等于把所有风险敞口交给一个 2012 年的作者。我一般不会大改结构只做三处最小侵入的补丁改完再上模拟盘观察一周。第一处是总敞口上限。现在的逻辑是“有多少手动单就挂多少对冲单”账户里五张手动单就锁五份保证金。加一个全局计数超过阈值就不再新挂input int MaxHedgeCount 3; // build 600 之后用 input 替代 extern int CountHedgeOrders() { int n 0; for(int k OrdersTotal() - 1; k 0; k--) { if(!OrderSelect(k, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderSymbol() Symbol() OrderMagicNumber() ! 0) n; } return(n); }在OrderSend之前插一句if(CountHedgeOrders() MaxHedgeCount) return;成本是每 tick 多一轮遍历换来的是保证金不会失控。第二处是把Alert分级。下单成功、下单失败这两类保留AlertOrderModify成功改成Print否则一个震荡夜能弹上百次。第三处是给Hedge_Assistante()加一个“仅在新 K 线执行”的开关锁仓逻辑本来就不需要每 tick 跑用static datetime lastBar比当前 K 线时间相同就returnCPU 占用能降一个数量级。改前改后对照着看更清楚位置原写法改成解决什么距离计算HedgingLevel HedgingLevel * Point局部变量dist距离逐 tick 衰减到 0清理循环for(i0; iOrdersTotal(); i)倒序i--删单后漏掉后续订单冷却Sleep(10000)static datetime时间戳EA 被阻塞十秒活单判据OrderProfit() ! 0OrderType() 1保本瞬间重复挂单止盈容差±1 * PointMathAbs(...) 3 * Point反复 Modify 被限流验证方法不用复杂改完编译先在策略测试器可视化模式下跑一周 M15 数据看日志里还有没有重复挂单和连续 Modify再换到模拟账户手动开一张最小手数的单盯着Comment显示的 ATR 值和实际挂单价对不对得上。两个环节都过了才算能碰真仓。血泪经验是这类对冲助手的风险从来不在算法在边界条件。我最早拿它上模拟盘的时候就是因为 5.1 那个extern回写锁仓距离被算成 0.0000003五张单锁得跟没锁一样回撤照吃不误日志里一切正常只有把挂单价和开仓价打印出来对比才发现问题。从那以后我每次拿到这种老源码都强制先把所有extern变量在函数体里打印一遍跑满一百个 tick 再打印一遍值变了就说明有回写。希望帮到你。本文还有配套的精品资源点击获取