ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MT4实战:用MQL4脚本批量给持仓订单设置止损

MT4实战:用MQL4脚本批量给持仓订单设置止损 1. 需求拆解这笔“小需求”背后藏着多少门道我先说说为什么会想写这个脚本。外汇、黄金、指数这类交易品种无论是做日内还是波段止损是无论如何都绕不开的东西。手动挂单的时候心里很清楚止损设在哪但真到了持仓阶段问题就来了行情走得快的时候你根本没时间去一个单子一个单子地翻、点、改行情走得慢的时候人又容易犯懒想着“再等等”结果等来的就是一根大阴线直接击穿成本。我身边不少人吃过这种亏所以“批量给持仓订单加固定点数止损”这个需求几乎是每个做实盘的人都想要的小工具。这个脚本本质上干的事情很简单把当前账户里所有持仓订单扫一遍按你设定的点数逐个计算出对应的止损价格然后调价。听起来像是个“循环加改单”的体力活但真正写起来里面涉及订单选择、品种过滤、点值换算、服务端错误处理、参数可配置化这些环节每一步都有讲究。对刚接触脚本编程、想用真实场景练手的朋友来说这是一个特别合适的入门项目代码量不大不依赖第三方库逻辑链条清晰而且做完就能在实盘或模拟盘上看到效果正反馈非常强。我写这篇文章就是想把整个思路、代码、踩坑过程完整地摊开讲一遍。不管你是第一次写交易脚本还是只会写Python想试试MQL4都能按这篇文章一步步把工具做出来。我会把代码贴出来逐行拆解也会把那些“教程里不会告诉你”的坑点单独拿出来说。另外提一句这篇文章以MT4平台为基础因为MT4的脚本机制最简单直接适合入门。MT5的写法会有些差异我会在对应位置单独说明免得你拿着MT4的代码去MT5那边编译一堆报错还不知道为什么。2. 动手前的关键认知点值、Bid/Ask、订单结构2.1 止损点数是怎么变成真实价格的很多人第一次写这个脚本时脑子里想的公式是“当前价减去N个点”然后顺手就写了Close - N * Point。这个思路方向对但实现上有个细节必须注意不能用 Close 作为计算基准要用 Bid 和 Ask 分开算。为什么因为一笔买单开仓后你看到的“当前价”其实是卖价Ask但持仓浮盈浮亏是按买价Bid来计算的。止损单触发的价格也是基于对方方向的报价——买单止损要看 Bid卖单止损要看 Ask。如果你统一用 Close 去算在某些流动性一般、点差偏大的品种上算出来的止损价可能离真实触发价差了那么零点几甚至一两个点。别小看这点误差行情正卡在止损位附近的时候一个点就能决定你这单是被扫掉还是继续扛。还有一个新手常犯的错把“点数”和“Point”搞混。MT4里Point是品种的最小价格变动单位EURUSD在多数平台是0.0001但黄金(XAUUSD)在部分平台是0.01在另一些平台可能是0.1。如果你拍脑袋写一个0.001进去当点值黄金的50点止损就变成0.05美元等于没设。正确做法是直接用Point变量由平台按品种自动换算代码里不要出现任何硬编码的数值。至于最后一步NormalizeDouble(price, Digits)是为了把计算出来的浮点数精度对齐到品种报价位数。EURUSD保留5位小数黄金保留2位或3位直接用Digits这个内置变量就不会错。不归一化直接提交的话服务端大概率会拒绝返回无效价格错误。2.2 订单筛选逻辑别把挂单也一起改了写脚本的人最容易忽略的就是OrdersTotal()返回的是“当前账户所有订单”这里面既包括已成交的持仓单也包括还没成交的挂单Buy Limit、Sell Stop之类。如果你不区分订单类型直接一股脑给所有订单设置止损挂单会被改得乱七八糟甚至报错。这里需要用OrderType()来判断。买家订单类型OP_BUY和卖家订单类型OP_SELL是真正的持仓单其余四种OP_BUYLIMIT、OP_SELLLIMIT、OP_BUYSTOP、OP_SELLSTOP全是挂单。我们批量设置止损的脚本只处理前两种。除了持仓/挂单这个维度实际使用时还会遇到“只想处理当前图表品种”“只想处理某个EA开的单子”这类需求。我最终在脚本里加了两个开关OnlyCurrentSymbol为真时只处理当前图表品种的订单适合你盯着EURUSD图表却不想动其他品种持仓的场景。MagicFilter魔法号是MT4里用来标记订单归属的整数编号EA开单时会写入。设成0表示不过滤设成比如20240316就只处理这个EA开的单子。这两个开关是我从实际使用中总结出来的。最开始脚本是“无差别全改”后来发现账户里挂着几个不同策略的订单一个脚本扫过去全给改了止损把另一个策略的风控计划彻底打乱。加过滤参数之后想改哪个就改哪个安全得多。2.3 脚本参数的设计逻辑MT4脚本Script和EAExpert Advisor最大的区别就是脚本放到图表上后执行一次就结束而EA会随着每个价格变动不断触发。所以这个批量止损功能用脚本实现是天然合适的你运行一次它把所有订单处理完打印日志退出。既然是“一次性的工具”参数设计就要往“好调、安全”方向走。我最终留了三个外部参数extern double StopDistancePips 50.0; // 止损距离(点数) extern bool ModifyIfExists true; // 已有止损时是否也修改 extern bool OnlyCurrentSymbol false; // 仅处理当前图表品种 extern int MagicFilter 0; // 0表示不过滤StopDistancePips用“点数”作为单位而不是“价格距离”是为了符合交易者的直觉。你跟我说“每单止损50点”我脑子里立刻有画面你要是跟我说“止损距离0.005美元”我得反应半天。用户输入的是点数脚本内部再乘以Point换算成价格距离这样对使用者最友好。ModifyIfExists这个开关是我后来加的。最初版本是“不管有没有止损都改成新值”结果有一次我本来只想给“没设止损的单子”兜底加上止损结果把所有单子的止损全重置了气得不行。加了这个开关之后可以自由选择策略。3. 完整代码与逐段拆解3.1 可以直接抄的MQL4脚本下面是我整理后一直在用的版本注释写得比较详细方便对照理解#property strict #property copyright Personal Utility #property version 1.00 // 外部输入参数运行时可在属性窗口里改 extern double StopDistancePips 50.0; // 止损距离单位点(pips) extern bool ModifyIfExists true; // false只给没有止损的订单添加 extern bool OnlyCurrentSymbol false; // true只处理当前图表品种 extern int MagicFilter 0; // 0不按魔法号过滤 // 统计变量 int g_modified 0; int g_skipped 0; int g_errors 0; //------------------------------------------------------------------ int OnStart() { // 参数合法性检查止损距离必须大于0 if(StopDistancePips 0) { Print(错误止损距离必须大于0脚本退出。); return(INIT_FAILED); } double distPrice StopDistancePips * Point; // 点数转价格距离 int total OrdersTotal(); Print(开始批量设置止损持仓订单总数, total, 距离, StopDistancePips, 点); // 倒序遍历订单避免中途修改导致下标错位 for(int i total - 1; i 0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) { g_errors; Print(选择订单失败下标, i, 错误码, GetLastError()); continue; } int type OrderType(); // 仅处理持仓单买单/卖单跳过挂单 if(type ! OP_BUY type ! OP_SELL) { g_skipped; Print(订单 #, OrderTicket(), 是挂单跳过); continue; } // 品种过滤 if(OnlyCurrentSymbol OrderSymbol() ! Symbol()) { g_skipped; Print(订单 #, OrderTicket(), 品种, OrderSymbol(), 不属于当前图表跳过); continue; } // 魔法号过滤 if(MagicFilter 0 OrderMagicNumber() ! MagicFilter) { g_skipped; Print(订单 #, OrderTicket(), 魔法号, OrderMagicNumber(), 不匹配跳过); continue; } // 根据买卖方向计算新的止损价 double newSL 0.0; if(type OP_BUY) newSL NormalizeDouble(Bid - distPrice, Digits); else newSL NormalizeDouble(Ask distPrice, Digits); // 如果已经持有止损且设置不允许覆盖则跳过 if(!ModifyIfExists OrderStopLoss() ! 0) { g_skipped; Print(订单 #, OrderTicket(), 已有止损, OrderStopLoss(), 按设置跳过); continue; } // 如果新止损和旧止损几乎一样没必要再提交修改 if(MathAbs(OrderStopLoss() - newSL) Point / 2.0) { g_skipped; Print(订单 #, OrderTicket(), 止损未变化跳过); continue; } // 执行修改 bool ok OrderModify(OrderTicket(), OrderOpenPrice(), newSL, OrderTakeProfit(), 0, clrNONE); if(ok) { g_modified; Print(订单 #, OrderTicket(), 止损已设置为 , DoubleToString(newSL, Digits)); } else { g_errors; Print(订单 #, OrderTicket(), 修改失败错误码, GetLastError()); } } Print(批量止损完成修改, g_modified, 跳过, g_skipped, 错误, g_errors); return(INIT_SUCCEEDED); }3.2 关键代码段为什么要这么写先说循环方向。这里用for(int i total - 1; i 0; i--)倒序遍历而不是很多人习惯的正序原因在于脚本执行过程中订单集合不会变MT4的脚本单次执行期间订单列表是稳定的所以正序倒序其实都能跑通。但如果未来你把这个逻辑改写成EA里的定时循环正序遍历时一旦中间有其他逻辑平仓了某个订单下标就可能错位导致漏单。养成倒序遍历的习惯是在为以后写更复杂的代码打基础。再说OrderSelect。在MT4里你没法直接通过订单号操作订单必须先调用OrderSelect把目标订单“选中”到当前上下文中然后再用OrderTicket()、OrderOpenPrice()这些函数读取信息。请注意OrderSelect的第二个参数传的是SELECT_BY_POS意思是“按订单列表中的位置来选择”如果你想指定单号应该用SELECT_BY_TICKET。很多新手在这里会搞混直接写了个OrderSelect(123456, SELECT_BY_TICKET, MODE_TRADES)结果就用错了。是因为第三个参数MODE_TRADES是“当前持仓挂单”的集合而MODE_HISTORY是“已平仓历史”。我们只处理正在持仓的单子所以用MODE_TRADES。然后是OrderModify的参数顺序。MT4的函数签名是这样的OrderModify(ticket, price, stoploss, takeprofit, expiration, arrow_color)注意第二参数price这里传入的是OrderOpenPrice()也就是原始开仓价。不少初学者误以为这个参数是“当前最新价”传入Bid或Ask结果修改订单时服务端直接拒绝。原因很简单修改订单时服务端需要核对“当前这张单子的开仓价”是否与你提交的数据一致不一致就返回错误。所以你读出来什么就原样传回去不要自作聪明。OrderStopLoss()和OrderTakeProfit()这两个函数分别读取订单当前止损和止盈。我们这里做了一个“止损没变化就不提交”的优化如果新旧止损价相差不到半个点就跳过。这是一个很实用的细节因为每次OrderModify都是一次服务端请求如果账户里有几十个持仓单全部重复提交同一价格白白浪费资源还可能因为短时间请求过多被拒。3.3 打印日志的层次脚本里的Print输出会在MT4的“专家”标签页里显示这对调试和事后复盘极其重要。我设计了三种层次的日志单条订单详情每个订单被处理时输出是哪一单、什么品种、修改成什么价、如果失败错误码是多少。跳过原因被跳过的订单会写明为什么跳过是挂单、品种不匹配、魔法号不匹配还是已有止损且不允许覆盖。最终汇总脚本跑完输出“修改了多少、跳过了多少、错了多少”。这个日志设计看着啰嗦实际用起来是真香。尤其当服务端因为某种原因拒绝某个订单时错误码直接打出来你一眼就能定位问题。如果你用Alert弹窗一个单子弹一下几十个持仓能把你烦死。脚本场景下Print是更合适的选择。3.4 进阶扩展思路不止于“固定点数”这个脚本的底层逻辑不止能用于“固定点数止损”。稍微改动一下就能玩出更多花活按“固定价格距离”设置止损把StopDistancePips改成StopDistancePrice代码里去掉乘Point那步就行。按“账户风险比例”反推止损距离先读账户净值乘以你愿意承担的风险百分比再除以持仓手数得到点数。不过这个逻辑要做到完全准确还得考虑货币对和账户结算货币的换算入门阶段先不展开。按“关键位附近”的绝对价格设置止损比如把脚本改成输入一个价格所有买单止损设成这个价所有卖单止损也设成这个价。用于你手工分析出支撑阻力位后一键统一设置。批量设置止盈把代码里的OrderModify参数里OrderTakeProfit()和newSL的位置换一下就是一个批量止盈脚本。MQL4脚本的框架是通用的你掌握了一个循环遍历订单、筛选、修改的套路就等于掌握了在MT4平台上操作“账户里所有订单”的万能钥匙。这一步跨过去后面写任何批量订单工具都是水到渠成。4. 实操过程编译、测试、真实环境验证4.1 在MetaEditor里编译脚本MT4自带的代码编辑器叫MetaEditor从MT4的顶部菜单“工具 - MetaQuotes Language Editor”打开。新建脚本的方式有两种点菜单“文件 - 新建”选择“脚本(Script)”或者直接在“导航器”面板里右键“脚本”文件夹选择“新建”。模板会自动生成一个带OnStart()空函数的文件。把前面贴的代码整个覆盖进去然后按F7编译。新手最容易在这里卡住的两件事一是编译报错二是编译成功却找不到文件。编译报错往往是语法层面的问题少写了一个分号、函数名拼错、中英文标点混用。MQL4对分号和大小写都比较敏感OnStart写成onstart也不行。报错信息会给出行号双击错误行光标会跳到对应位置。我第一次写这个脚本时就是在一个NormalizeDouble前面漏了个左括号报错行指到下一行找了半天才明白是上一行括号没闭合。编译成功的产物是一个.ex4文件它会被自动放到MT4数据目录的MQL4/Experts或MQL4/Scripts文件夹下。有新手疑惑“为什么我的脚本在EA列表里看不到”原因就是你新建时选错了类型。脚本必须放在Scripts文件夹EA放在Experts这是MT4的目录约定。4.2 在图表上运行脚本的两种方式运行脚本这件事第一次操作的人很容易一脸懵因为MT4里“运行脚本”和“挂EA”的操作位置不太一样。EA是通过拖拽或双击图表“附加”上去的而脚本是“执行一次就结束”所以它的触发方式不同。操作方法是在MT4的“导航器”面板里找到脚本目录看到你编译好的脚本名称把它直接拖到某个图表上松手就会弹出一个参数设置对话框。在这里你可以修改StopDistancePips、ModifyIfExists这些外部变量确认后脚本立即执行。或者双击脚本名称它也会直接在当前图表上运行不弹参数框用默认值。脚本运行过程中MT4底部会有一个小旋转图标转几秒钟就停了说明脚本执行完毕。你不要以为“怎么没反应”它本来就是一锤子买卖。参数设置对话框里的“属性”页签里还有个比较隐蔽的设置“允许实时信号交易”。有的平台默认不勾选导致脚本里的OrderModify被直接禁止执行结果全是错误。我第一次在模拟盘上测试就碰上这个排查了半天一查“工具 - 选项 - 智能交易系统”确认了才发现是这里的问题。如果你测试时所有订单都返回“市场已关闭”或交易被禁用之类的错误第一反应就去检查这个开关。4.3 实操中如何确认脚本真的生效脚本执行完你要想知道它到底改了什么有两条路可走第一条路是看MT4底部“专家”标签页的日志。脚本里每个Print输出的内容都会显示在这里。你会看到类似开始批量设置止损持仓订单总数5距离50.0点 订单 #123456 止损已设置为 1.08452 订单 #123457 止损已设置为 1.08503 批量止损完成修改4跳过1错误0这个日志是最直接的结果。我在自己写的脚本里把日志级别做得比较完整就是为了这一步能看清楚每一个订单的命运。第二条路是打开交易软件的“终端 - 交易”窗口看持仓列表里每个订单的“止损”列。改动成功的订单止损价应该已经变成新值。注意这里显示的价格是浮动的因为MT4的持仓单止损价会随行情实时变不会的止损价是你设置的固定价格除非你手动修改否则它不会自动变化。看到新值就说明改成功了。测试时强烈建议先开一张迷你手(0.01)的模拟单甚至是先只在模拟账号里测试不要拿实盘试。我的习惯是先在模拟盘里开5张不同品种、不同方向的单子有些设好止损有些空着止损然后跑一遍脚本观察哪些被改了、哪些被跳过了验证逻辑是否符合预期。这在实盘上是不敢这么操作的。4.4 关于脚本“闪退”“无法运行”的环境问题网上经常有人搜“脚本闪退”“无法加载脚本”“禁止运行脚本”这其实分两种情况。如果是Windows环境里运行.bat、.ps1、npm命令时报错比如“无法将‘pip’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”“在此系统上禁止运行脚本”那属于操作系统环境变量或PowerShell执行策略的问题跟你MT4脚本是两码事。解决PowerShell脚本被禁止需要在管理员模式下执行Set-ExecutionPolicy RemoteSigned解决pip或pnpm不识别十有八九是没配环境变量PATH或安装时没勾选“Add to PATH”。这一类问题我在第5章会统一给出一个排查速查表。另一种情况是MT4脚本本身没有“闪退”的概念——它就是执行一次然后停止看起来像“没反应”。如果你把脚本拖到图表上发现啥日志都没有先检查“专家”标签页是不是被日志刷屏挡住了或者过滤器有没有把所有消息都隐藏了。点“专家”标签页最下方的“消息”列或在空白处右键选择“全部显示”一般都能看到输出。5. 常见问题与排查技巧实录5.1 读懂OrderModify的错误码比瞎猜快十倍脚本写出来不可能第一次跑就一次全过。我把自己在实盘和模拟盘上踩过的错误码整理成了下面这张表遇到报错直接对号入座错误码含义常见原因解决方案129不正确的价格新止损价精度没归一化或价格不是有效报价确认用了NormalizeDouble检查报价位数130无效的交易参数止损离市价太近被经纪商的最小止损距离挡住查看品种规格里的“止损水平(StopLevel)”138需要重新报价报价刚变化你提交的价格已过时重新读取Bid/Ask后再算一次或加延时重试4014市场已关闭非交易时段或某些品种休息确认市场状态休市时段停止操作4108未知订单/订单已平仓订单在你操作前已经被平掉下单前重新OrderSelect验证4202交易被禁用账户类型或EA交易开关限制检查“工具 - 选项 - 智能交易系统”其中130是我遇到最多的。不同的经纪商对“最小止损距离”有不同要求专业术语叫STOPLEVEL单位也是点。比如某个经纪商规定欧美最低止损距离为30点你脚本里设成了20点那OrderModify必然返回130。处理办法不复杂读一下当前品种的MarketInfo(Symbol(), MODE_STOPLEVEL)如果发现设定的点数小于这个值就用STOPLEVEL兜底。代码加一行就够了int minStopLevel (int)MarketInfo(Symbol(), MODE_STOPLEVEL); if(StopDistancePips minStopLevel) { Print(警告设定止损距离低于经纪商要求自动调整为最小水平); StopDistancePips minStopLevel; }这个“自动兜底”是我后来加进去的。不加的话账户里只要有一两个单子距离低于经纪商要求脚本就会报错一大片影响体验。加了之后至少不会因为距离不够而整体失败。5.2 换了平台就失效MT5和MT4的差异我前面说过这份代码是针对MT4的。如果你用的是MT5别直接复制因为底层API完全不同。MT5里没有OrderModify这个函数而是用CTrade类的PositionModify方法订单集合也不是OrdersTotal()一个函数而是把“持仓(Position)”和“挂单(Order)”分开管理持仓用PositionsTotal()挂单用OrdersTotal()。MT5的写法大致是#include Trade\Trade.mqh CTrade trade; void OnStart() { double distPrice StopDistancePips * _Point; for(int i PositionsTotal() - 1; i 0; i--) { ulong ticket PositionGetTicket(i); if(ticket 0) continue; double newSL 0.0; if(PositionGetInteger(POSITION_TYPE) POSITION_TYPE_BUY) newSL SymbolInfoDouble(_Symbol, SYMBOL_BID) - distPrice; else newSL SymbolInfoDouble(_Symbol, SYMBOL_ASK) distPrice; trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP)); } }MT5里我建议用PositionGetTicket遍历持仓因为它的速度比PositionGetSymbol快而且不容易挑错对象。另外MT5的PositionModify内部自动处理了正常报价你用PositionGetDouble(POSITION_PRICE_OPEN)读原始开仓价传入时不用手动归一化。写法和MT4风格差异很大刚转过去的人都会有一阵不适应。5.3 环境类问题的排查速查表——给被“脚本”二字劝退的人因为这篇文章涉及“脚本”我知道不少读者可能同时在折腾Linux的shell脚本、Windows的批处理或者是Python里的pip装包甚至有人是因为“三角洲跑刀脚本”“京东签到脚本”这类游戏或薅羊毛脚本进来的。这里我统一给一份通用排查思路至少能帮你应付八成以上的“脚本跑不起来”问题问题现象排查方向常见处理提示“无法将xxx识别为...可运行程序”环境变量PATH重新安装时勾选“Add to PATH”或用完整路径执行提示“禁止运行脚本”PowerShell执行策略管理员运行Set-ExecutionPolicy RemoteSigned脚本双击后一闪而过脚本报错或缺少暂停在命令后加pause或先打开终端手动执行看报错脚本运行中报错但没日志日志输出不完善脚本关键步骤加echo/print输出编译成功但运行时无效果对象选择/作用域问题核对脚本的目标文件和目录确认权限设置这套排查思路放在交易脚本里也是一样的先确认环境允许、路径正确、日志输出可见再谈逻辑问题。5.4 真实踩坑记录我的一次“帮倒忙”经历写这个脚本的时候我自己就闹过一次笑话。当时账户里有3个黄金空单原本止损距离是80点我觉得行情走稳了想把止损全部收紧到40点。跑了一遍脚本输出显示“修改3跳过0”可我一看持仓3个单子的止损全都变成了40点——没毛病但问题是我原本想的是“只把还没有止损的单子补上”结果因为ModifyIfExists默认是true把原本有止损的也全覆盖了。那次之后我养成了一个习惯运行任何批量修改订单的脚本前先在纸上写下“我希望它改什么、跳过什么、绝不能改什么”然后对照参数逐项确认。这不是什么高深技术但能避免很多因为“想当然”造成的惨案。还有一次是在模拟盘上测试时发现所有订单全都返回错误130我以为是代码问题反复改了半天。后来发现是模拟盘平台把某品种的STOPLEVEL设置得非常高而我设定的止损点数低于它的门槛。这也促使我把“自动兜底”逻辑加了进去。所以我想强调模拟盘上踩坑不可怕可怕的是踩了坑却不记录。把每一次GetLastError()的错误码记下来随手写进自己的笔记里下次遇到直接翻笔记效率翻倍。现在这个脚本已经成为我的必备工具。行情剧烈波动时我可能同时持有十几个不同品种的订单手动设止损根本忙不过来脚本一拖一放几秒钟全部搞定晚上睡觉前想统一收紧全账户的止损也是拖一下的事。对新手来说这个练习项目还能帮你真正理解MT4的订单体系、点值概念和脚本生命周期这些知识在以后写EA时都是地基。最后分享一个小技巧脚本跑完后不要急着关掉图表先在“专家”标签页里把日志从头到尾过一遍尤其关注有没有错误字样。如果你是踩着默认的Help、News面板记得右键选择“专家”标签页并清空过滤否则日志会被其他行情信息淹没。这个习惯我一直保留到现在因为很多交易软件的问题都是靠日志里的蛛丝马迹揪出来的而不是靠肉眼去一个个比对持仓列表。
返回列表