MQL5多币种挂单EA实战开发
用Buy/Sell Stop限价单构建日内多品种交易系统
一、多币种挂单EA的设计目标与核心思路
传统动量突破策略常直接在现价进场,而本文讨论的EA采用挂单方式:在现价上方布置Buy Stop(高买),在下方布置Sell Stop(低卖),等待价格触发。该模式特别适合日内交易,因为挂单可在指定时段自动布置、触发或撤销,避免人工盯盘。EA被设计为同时监控多个品种(原文范例为EURUSD与AUDUSD),每个品种独立计算挂单参数、止损止盈与交易时间范围。
核心思路是利用新柱线事件驱动:当某品种无持仓且处于允许挂单时段,立即以注释区分的top_order(Buy Stop)与bottom_order(Sell Stop)各挂一张单;若持仓被平,则检查平仓原因,若为止盈或止损触发,则删除同品种所有挂单,等待下一根K线重新布局。反向模式开启时,持仓触发后会在对侧布置新的挂单实现顺逆转。
二、外部参数与交易时段枚举设计
为了让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;
三、时间范围判断与品种属性获取
函数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()则是自定义统计某品种挂单数的辅助。
五、挂单价格与止损止盈计算逻辑
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。
九、常见开发坑与对比其他方案
坑1:MT5挂单手数不可改,必须删后重挂,否则Modify失败。坑2:stops_level导致挂单价非法,必须像文中做边界修正。坑3:HistorySelect每次调耗资源,不宜在OnTick高频调用,原文置于事件级已缓解。对比市价突破EA,挂单EA能规避滑点不确定性,但可能漏单(价格瞬穿)。对比网格马丁,本文限价单无加仓,风险更线性。
- 使用CTrade前必须#include <Trade\Trade.mqh>且声明CTrade trade;
- 注释查重比订单ticket更直观,但需保证注释唯一性
- 服务器时间非本地时间,跨时区策略要换算
- 反向模式开启时追踪止损逻辑复杂,建议先关Reverse调试
十、可运行的简化挂单示例
评论区用户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);
}