MQL5多币种挂单EA实战开发
🤖

MQL5多币种挂单EA实战开发

用Buy/Sell Stop限价单构建日内多品种交易系统

MQL5 编程 难度 · 进阶 2026-07-07 9 分钟阅读
#MQL5#多币种EA#限价订单#日内交易#挂单管理

一、多币种挂单EA的设计目标与核心思路

传统动量突破策略常直接在现价进场,而本文讨论的EA采用挂单方式:在现价上方布置Buy Stop(高买),在下方布置Sell Stop(低卖),等待价格触发。该模式特别适合日内交易,因为挂单可在指定时段自动布置、触发或撤销,避免人工盯盘。EA被设计为同时监控多个品种(原文范例为EURUSD与AUDUSD),每个品种独立计算挂单参数、止损止盈与交易时间范围。

核心思路是利用新柱线事件驱动:当某品种无持仓且处于允许挂单时段,立即以注释区分的top_order(Buy Stop)与bottom_order(Sell Stop)各挂一张单;若持仓被平,则检查平仓原因,若为止盈或止损触发,则删除同品种所有挂单,等待下一根K线重新布局。反向模式开启时,持仓触发后会在对侧布置新的挂单实现顺逆转。

✦ 模板来源
本文EA以较早的《MQL5 Cookbook: 多币种EA交易 - 简洁而快速的途径》为代码模板,保留了多品种数组循环结构,但将市价交易逻辑替换为挂单管理函数。

二、外部参数与交易时段枚举设计

为了让EA既能限定时段又能连续运行,作者在Enums.mqh中建立ENUM_HOURS枚举,将0至23小时映射为h00~h23。外部参数对每个品种暴露四个时间控制项:TradeInTimeRange(是否启定时段)、StartTrade(起算小时)、StopOpenOrders(停止布置挂单小时)、EndTrade(强制平仓小时)。此外还有PendingOrder(挂单距现价点数)、TakeProfit、StopLoss、TrailingStop、Reverse(反转)、Lot等。

以下为两个品种的简化参数声明,使用sinput与input区分不可优化与可优化项。实际工程中常用数组Symbols[]、StartTrade[]等批量存储,便于循环处理。

sinput string     Symbol_01            ="EURUSD";
input  bool       TradeInTimeRange_01  =true;
input  ENUM_HOURS StartTrade_01        = h10;
input  ENUM_HOURS StopOpenOrders_01    = h17;
input  ENUM_HOURS EndTrade_01          = h22;
input  double     PendingOrder_01      = 50;
input  double     TakeProfit_01        = 100;
input  double     StopLoss_01          = 50;
input  bool       Reverse_01           = true;
input  double     Lot_01               = 0.1;
⚠ 参数校验坑
OnInit中必须调用CheckInputParameters(),确保StartTrade<StopOpenOrders<EndTrade。若顺序错乱,EA会直接返回初始化失败,避免在非法时段乱下单。

三、时间范围判断与品种属性获取

函数IsInTradeTimeRange()与IsInOpenOrdersTimeRange()均通过TimeTradeServer()获取服务器时间的结构体MqlDateTime,比较小时字段。前者用于决定是否允许持仓与挂单存续,后者仅控制是否还能新建挂单。这样可实现:例如10点起允许交易,17点后不再新建单但已有单可跑,22点全部强平。

bool IsInOpenOrdersTimeRange(int symbol_number)
  {
   if(TradeInTimeRange[symbol_number])
     {
      MqlDateTime last_date;
      TimeTradeServer(last_date);
      if(last_date.hour<StartTrade[symbol_number] ||
         last_date.hour>=StopOpenOrders[symbol_number])
         return(false);
     }
   return(true);
  }

为读取挂单属性,文章定义了ENUM_ORDER_PROPERTIES枚举与pending_order_properties结构体,并用GetPendingOrderProperties()封装OrderGetInteger/OrderGetDouble/OrderGetString。注意MQL5中挂单与持仓分开管理,必须用OrderSelect类函数(此处用OrderGetTicket循环)而非PositionSelect。

四、挂单的布置、修改与删除机制

SetPendingOrder()封装了CTrade::OrderOpen,失败时在日志打印错误码。ModifyPendingOrder()设计巧妙:若传入volume>0,因MT5不允许直接改挂单手数,则先DeletePendingOrder()再SetPendingOrder()重挂;若volume=0仅改价。CorrectStopLossByOrder()会根据挂单价格重算关联持仓的SL。

void ModifyPendingOrder(int symbol_number, ulong ticket, ENUM_ORDER_TYPE type, double price, double sl, double tp, ENUM_ORDER_TYPE_TIME type_time, datetime time_expiration, double stoplimit_price, string comment, double volume)
  {
   if(volume>0)
     {
      if(!DeletePendingOrder(ticket)) return;
      SetPendingOrder(symbol_number,type,volume,0,price,sl,tp,type_time,comment);
      CorrectStopLossByOrder(symbol_number,price,type);
     }
   else
     {
      if(!trade.OrderModify(ticket,price,sl,tp,type_time,time_expiration,stoplimit_price))
         Print("Error when modifying: ",GetLastError());
      else
         CorrectStopLossByOrder(symbol_number,price,type);
     }
  }

布置前需用CheckPendingOrderByComment()遍历OrdersTotal(),比对ORDER_SYMBOL与ORDER_COMMENT,防止重复挂单。OrdersTotalBySymbol()则是自定义统计某品种挂单数的辅助。

✦ 注释即ID
用"top_order"和"bottom_order"作为挂单注释,既方便查重,也便于在ManagePendingOrders中识别对立单,是轻量级的订单追踪方案。

五、挂单价格与止损止盈计算逻辑

CalculatePendingOrder()依据ORDER_TYPE_BUY_STOP或SELL_STOP,在ask/bid加减PendingOrder点数的点值(symb.point)。关键是必须检查经纪商stops_level限制:若计算价触及禁挂区,则拉回至symb.up_level+offset或symb.down_level-offset。CalculatePendingOrderStopLoss/TakeProfit同理,确保挂单的SL/TP合法。

double CalculatePendingOrder(int symbol_number,ENUM_ORDER_TYPE order_type)
  {
   double price=0.0;
   if(order_type==ORDER_TYPE_SELL_STOP)
     {
      price=NormalizeDouble(symb.bid-CorrectValueBySymbolDigits(PendingOrder[symbol_number]*symb.point),symb.digits);
      return(price<symb.down_level ? price : symb.down_level-symb.offset);
     }
   if(order_type==ORDER_TYPE_BUY_STOP)
     {
      price=NormalizeDouble(symb.ask+CorrectValueBySymbolDigits(PendingOrder[symbol_number]*symb.point),symb.digits);
      return(price>symb.up_level ? price : symb.up_level+symb.offset);
     }
   return(0.0);
  }

反向模式中,CalculateReverseOrderTrailingStop()利用前一根K线的低/高价加减点数,计算追随挂单的触发价;ModifyPendingOrderTrailingStop()在每次事件中对侧挂单随价格拉移,实现移动止损式挂单。

六、成交历史识别与平仓原因判断

EA需知道上一笔仓位是否因TP或SL离场,从而决定是否清掉挂单。GetLastDealComment()用HistorySelect后倒序找同品种最近成交注释;IsClosedByTakeProfit/StopLoss简单用StringFind查"tp"/"sl"子串。IsLastDealTicket()借助last_deal_ticket[]数组记忆上次成交号,避免重复处理。

bool IsClosedByTakeProfit(int symbol_number)
  {
   string last_comment=GetLastDealComment(symbol_number);
   if(StringFind(last_comment,"tp",0)>-1) return(true);
   return(false);
  }

此方案依赖成交注释包含特定标记,因此平仓函数或交易类必须写入对应comment。若使用净持仓账户或注释被覆盖,该判断会失效,这是实战中需注意的局限。

七、主循环与事件处理架构

CheckSignalsAndTrade()在新柱线时遍历品种:若超EndTrade则ClosePosition+DeleteAllPendingOrders;否则TradingBlock()无持仓时布置双侧挂单。OnTrade()与OnChartEvent()均调用ManagePendingOrders(),后者在持仓存在时依Reverse配置维护对立挂单或追踪止损。

void OnTrade()
  {
   ManagePendingOrders();
  }
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id>=CHARTEVENT_CUSTOM && lparam==CHARTEVENT_TICK)
     {
      ManagePendingOrders();
      CheckSignalsAndTrade();
     }
  }

与旧版模板相比,本文用ManagePendingOrders替代了原本的市价交易块,使逻辑更专注挂单生命周期。开发者只需修改TradingBlock与ManagePendingOrders即可换策略思路。

八、回测优化与多品种参数分离

原文在策略测试器中先单独优化EURUSD参数,再优化AUDUSD,最后双品种同测,手数固定1.0选最大恢复因子。因每个品种参数独立,可避免跨品种过拟合。优化时仅放开input项(如PendingOrder_XX、TakeProfit_XX),固定Magic与Deviation。

⚠ 优化误区
多币种EA若一次性联合优化所有品种参数,极易曲线拟合。应像范例那样逐品种优化再组合验证,且样本外数据复查。

九、常见开发坑与对比其他方案

坑1:MT5挂单手数不可改,必须删后重挂,否则Modify失败。坑2:stops_level导致挂单价非法,必须像文中做边界修正。坑3:HistorySelect每次调耗资源,不宜在OnTick高频调用,原文置于事件级已缓解。对比市价突破EA,挂单EA能规避滑点不确定性,但可能漏单(价格瞬穿)。对比网格马丁,本文限价单无加仓,风险更线性。

十、可运行的简化挂单示例

评论区用户Otto提供了极简版MarketOrders改挂单代码,虽非完整EA,但演示了BuyLimit/SellLimit基本用法。以下片段展示以点数距现价挂限价单并带SL/TP,可作为新手练手参照。

input int    inp_Dist     =  120;
input int    inp_Stop     =  125;
input int    inp_Take     =  150;
input double inp_Volume   = 0.01;

double distPend = inp_Dist*_Point;
double distStop = inp_Stop*_Point;
double distTake = inp_Take*_Point;

void OnTick()
{
   double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
   double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID);
   double levelBuy =NormalizeDouble(bid-distPend,_Digits);
   double levelSell=NormalizeDouble(ask+distPend,_Digits);
   Trade.BuyLimit (inp_Volume,levelBuy ,_Symbol,levelBuy -distStop,levelBuy +distTake);
   Trade.SellLimit(inp_Volume,levelSell,_Symbol,levelSell+distStop,levelSell-distTake);
}

常见问题

MT5不允许直接修改挂单手数。需先调用OrderDelete删除原单,再用OrderOpen以新手数重挂,原文ModifyPendingOrder在volume>0时即采用此方式。
用ENUM_HOURS定义小时,设TradeInTimeRange=true,StartTrade=h08,StopOpenOrders=h16,EndTrade=h20,并在IsInOpenOrdersTimeRange中比对服务器小时即可。
用HistorySelect后取同品种最后成交注释,若含"tp"则止盈,含"sl"则止损,参考IsClosedByTakeProfit/IsClosedByStopLoss函数。
多半触及经纪商stops_level禁挂区。应像CalculatePendingOrder那样,若计算价小于down_level或大于up_level,拉回至边界加减offset。
可用OrderGetString(ORDER_COMMENT)查重与识别对立单,如原文用top_order/bottom_order区分Buy/Sell Stop,简化订单管理。