简介
经常访问本网站的用户都很清楚, MQL5 是开发自定义 EA 交易的最佳选择,不幸的是, 并不是所有的经纪商都允许创建 MetaTrader 5 的账户。. 就栓您现在使用的经纪商允许这一点,您也许也会在将来换到只提供 MetaTrader 4 的经纪商那里,您将如何处理在这种情况下开发的所有 MQL5 EA 交易呢?您是否要花费大量时间对它们进行返工以适应MQL4?也许,开发一个既能在MetaTrader 5中工作又能在MetaTrader 4中工作的EA更为合理?
在本文中,我们将尝试开发这样的EA,并检查基于订单网格的交易系统是否可用。
关于条件编译的几句话
条件编译将允许我们开发一个同时在 MetaTrader 4 和 MetaTrader 5 中工作的EA。应用的语法如下:
#ifdef __MQL5__ // MQL5 代码 #else // MQL4 代码 #endif
条件编译允许我们指定只有在MQL5 EA中完成编译时才应编译某个块。在MQL4和其他语言版本中编译时,只需丢弃此代码块,而是使用#else运算符后的代码块(如果已设置)。
这样,如果在 MQL4 和 MQL5 中实现了不同的功能,那么我们将以两种方式实现它,而条件编译允许选择特定语言所必需的选项。
在其他情况下,我们将使用在MQL4和MQL5中都有效的语法。
网格交易系统
在开始EA开发之前,让我们描述一下网格交易策略的基础知识。
网格是指将多个限价订单置于当前价格之上,同时将相同数量的限价订单置于当前价格之下的 EA 交易。
限价订单是通过一定的步骤而不是单一的价格来设定的。换言之,第一个限价订单被设定在当前价格之上的某个距离,第二个限价设置在第一个限价之上的相同距离处,以此类推。订单数量和应用的步骤各不相同,
一个方向的订单高于当前价格,而另一个方向的订单低于当前价格。应该考虑的是:
- 在一个趋势中,买入订单应该高于当前价格,而卖出订单应该低于当前价格;
- 在盘整期间,卖出订单应高于当前价格,而买入订单应低于当前价格。
您可以应用止损水平,也可以不使用它们。
如果你不使用止损和获利,所有未平仓合约,无论是盈利的还是亏损的,都会存在,直到整体利润达到一定水平。之后,所有未结头寸以及不受价格影响的限价订单都将关闭,并设置新的网格。
下面的屏幕截图显示了一个开启的网格:
因此,在理论上,网格交易系统可以让你在任何市场中获利,而不需要等待任何独特的进入点,也不需要使用任何指标。
如果使用止损和获利,那么利润是由于一个头寸的亏损被价格朝一个方向移动时的整体利润所覆盖而获得的。
没有止损水平,利润是由于在正确的方向上打开更多的订单而获得的。即使最初价格触及一个方向的仓位,然后转向,正确方向的新仓位将弥补先前开启的仓位的损失,因为最终会有更多的仓位。
我们的网格 EA 的工作原理
我们已经描述了上面最简单网格的工作原理,您可以为更改打开订单的方向、添加以相同价格打开多个订单的能力、添加指标等网格提供自己的选项。
在本文中,我们将尝试实现最简单的网格化版本,而不会造成停止损失,因为它基于的思想非常诱人。
事实上,认为价格迟早会在向一个方向移动时达到利润,即使最初的开仓方向是错误的,这似乎是合理的。假设在一开始,价格就经历了调整,触发了两个订单,此后,价格开始向相反(主要趋势)的方向移动。在这种情况下,早晚两个以上的订单将朝着正确的方向打开,我们的初始损失将在一段时间后变成利润。
似乎交易系统造成损失的唯一情况是,当价格首先触及一个订单,然后返回并触及另一个订单,然后再次改变方向并触及另一个订单,并反复改变其方向,触及越来越远的订单。但这种价格行为在实际情况下是可能的吗?
EA 模板
我们将从模板开始开发EA,这将使我们能够立即看到将涉及哪些标准MQL函数。
#property copyright "Klymenko Roman (needtome@icloud.com)" #property link "https://www.mql5.com/en/users/needtome" #property version "1.00" #property strict //+------------------------------------------------------------------+ //| EA 交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| EA 交易去初始化函数 | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| EA交易分时函数 | //+------------------------------------------------------------------+ void OnTick() { } void OnChartEvent(const int id, // 事件 ID const long& lparam, // 长整型的事件参数 const double& dparam, // 双精度浮点型的事件参数 const string& sparam) // 字符串类型的事件参数 { }
它与使用 MQL5 向导创建 EA 时生成的标准模板的唯一区别是 #property strict 这一行,我们添加它以便 EA 也在 MQL4 中工作。
我们需要 OnChartEvent() 函数来响应单击按钮,接下来,我们将实现Close All(全部关闭)按钮,以便在达到所需利润或只想停止 EA 时手动关闭所有交易品种仓位和订单。
开启仓位函数
可能任何EA最重要的功能都是下订单的能力,第一个问题就在这里等着我们。在 MQL5 和 MQL4 中,订单的设置方式非常不同。为了以某种方式统一这个功能,我们必须开发一个自定义函数来下订单。
每个订单都有自己的类型:买入订单、卖出订单、限价买入或卖出订单。在下订单时设置此类型的变量在MQL5和MQL4中也不同。
在 MQL4 中,订单类型由 int 类型变量指定,而在 MQL5 中,则使用ENUM_ORDER_TYPE枚举,在 MQL4 中没有这样的枚举。因此,为了组合这两种方法,我们应该创建一个自定义枚举来设置订单类型。因此,我们将来要创建的函数将不依赖于MQL版本:
enum TypeOfPos{
MY_BUY,
MY_SELL,
MY_BUYSTOP,
MY_BUYLIMIT,
MY_SELLSTOP,
MY_SELLLIMIT,
};
现在我们可以创建一个自定义函数来下订单了,让我们称它为 pdxSendOrder()。我们将传递下订单所需的所有信息:订单类型、未平仓价格、止损(未设定为0)、获利(未设定为0)、成交量、未平仓订单(如果在MQL5中应修改未平仓)、注释和交易品种(如果您需要为不是当前打开的交易品种开启订单):
// 发送订单的函数 bool pdxSendOrder(TypeOfPos mytype, double price, double sl, double tp, double volume, ulong position=0, string comment="", string sym=""){ // 检查传入的数值 if( !StringLen(sym) ){ sym=_Symbol; } int curDigits=(int) SymbolInfoInteger(sym, SYMBOL_DIGITS); if(sl>0){ sl=NormalizeDouble(sl,curDigits); } if(tp>0){ tp=NormalizeDouble(tp,curDigits); } if(price>0){ price=NormalizeDouble(price,curDigits); } #ifdef __MQL5__ ENUM_TRADE_REQUEST_ACTIONS action=TRADE_ACTION_DEAL; ENUM_ORDER_TYPE type=ORDER_TYPE_BUY; switch(mytype){ case MY_BUY: action=TRADE_ACTION_DEAL; type=ORDER_TYPE_BUY; break; case MY_BUYSTOP: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_BUY_STOP; break; case MY_BUYLIMIT: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_BUY_LIMIT; break; case MY_SELL: action=TRADE_ACTION_DEAL; type=ORDER_TYPE_SELL; break; case MY_SELLSTOP: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_SELL_STOP; break; case MY_SELLLIMIT: action=TRADE_ACTION_PENDING; type=ORDER_TYPE_SELL_LIMIT; break; } MqlTradeRequest mrequest; MqlTradeResult mresult; ZeroMemory(mrequest); mrequest.action = action; mrequest.sl = sl; mrequest.tp = tp; mrequest.symbol = sym; if(position>0){ mrequest.position = position; } if(StringLen(comment)){ mrequest.comment=comment; } if(action!=TRADE_ACTION_SLTP){ if(price>0){ mrequest.price = price; } if(volume>0){ mrequest.volume = volume; } mrequest.type = type; mrequest.magic = EA_Magic; switch(useORDER_FILLING_RETURN){ case FOK: mrequest.type_filling = ORDER_FILLING_FOK; break; case RETURN: mrequest.type_filling = ORDER_FILLING_RETURN; break; case IOC: mrequest.type_filling = ORDER_FILLING_IOC; break; } mrequest.deviation=100; } if(OrderSend(mrequest,mresult)){ if(mresult.retcode==10009 || mresult.retcode==10008){ return true; }else{ msgErr(GetLastError(), mresult.retcode); } } #else int type=OP_BUY; switch(mytype){ case MY_BUY: type=OP_BUY; break; case MY_BUYSTOP: type=OP_BUYSTOP; break; case MY_BUYLIMIT: type=OP_BUYLIMIT; break; case MY_SELL: type=OP_SELL; break; case MY_SELLSTOP: type=OP_SELLSTOP; break; case MY_SELLLIMIT: type=OP_SELLLIMIT; break; } if(OrderSend(sym, type, volume, price, 100, sl, tp, comment, EA_Magic, 0)<0){ msgErr(GetLastError()); }else{ return true; } #endif return false; }
首先,检查传递给函数的数值并规范化价格。
输入参数. 之后,使用条件编译来定义当前的MQL版本,并根据其规则设置顺序。多出的 useORDER_FILLING_RETURN 输入参数是为 MQL5 使用的,在它的帮助下,我们根据代理支持的模式配置订单执行模式。因为 useORDER_FILLING_RETURN 输入参数只对 MQL5 EA 才是必须的,再次使用条件编译来添加它:
#ifdef __MQL5__ enum TypeOfFilling //执行模式 { FOK,//ORDER_FILLING_FOK RETURN,// ORDER_FILLING_RETURN IOC,//ORDER_FILLING_IOC }; input TypeOfFilling useORDER_FILLING_RETURN=FOK; //执行模式 #endif
此外,下订单时,还使用包含EA幻数的EA_Magic输入参数。
如果在EA设置中未设置此参数,则 EA 已启动的交易品种上的任何仓位都被视为属于EA,这样,EA就完全控制了它们。
如果设置了幻数,则EA只考虑在其工作中具有此幻数的仓位。
显示错误. 如果订单成功设置, 就返回 true。否则,会将适当的错误代码传递给 msgErr() 的函数,以便进一步分析并显示可理解的错误消息。这个函数显示包含详细错误描述的本地化消息,在这里提供完整的代码是没有意义的,所以我只展示其中的一部分:
void msgErr(int err, int retcode=0){ string curErr=""; switch(err){ case 1: curErr=langs.err1; break; // case N: // curErr=langs.errN; // break; default: curErr=langs.err0+": "+(string) err; } if(retcode>0){ curErr+=" "; switch(retcode){ case 10004: curErr+=langs.retcode10004; break; // case N: // curErr+=langs.retcodeN; // break; } } Alert(curErr); }
我们将在下一节详细讨论本地化。
EA 本地化
在恢复EA开发之前,让我们先用双语。让我们添加选择EA消息语言的功能,我们将提供两种语言:英语和俄语。
使用可能的语言选项创建枚举,并添加用于选择语言的适当参数:
enum TypeOfLang{ MY_ENG, // 英语 MY_RUS, // 俄语 }; input TypeOfLang LANG=MY_RUS; // 语言
接下来,创建一个用于存储EA中使用的所有文本字符串的结构。在此之后,声明我们创建的类型的变量:
struct translate{ string err1; string err2; // ... 其它字符串 }; 翻译语言;
我们已经有了包含字符串的变量,不过,目前还没有任何字符串。创建一个函数,该函数用在Language输入参数中所选语言的字符串填充它。让我们称这个函数为 init_lang(). 部分代码显示如下:
void init_lang(){ switch(LANG){ case MY_ENG: langs.err1="No error, but unknown result. (1)"; langs.err2="General error (2)"; langs.err3="Incorrect parameters (3)"; // ... other strings break; case MY_RUS: langs.err0="Во время выполнения запроса произошла ошибка"; langs.err1="Нет ошибки, но результат неизвестен (1)"; langs.err2="Общая ошибка (2)"; langs.err3="Неправильные параметры (3)"; // ... other strings break; } }
唯一剩下要做的就是调用init_lang()函数,以便用必要的值填充字符串。调用它的最佳位置是标准的 OnInit() 函数,因为它在EA启动期间被调用,这正是我们需要的。
主要输入参数
现在是时候向我们的EA添加主要输入参数了,除了已经描述的EA_Magic和LANG之外,这些是:
input double Lot=0.01; //手数大小 input uint maxLimits=7; //在一个方向上的限价订单数量 input int Step=10; //网格步长点数 input double takeProfit=1; //当达到指定利润时关闭交易, $
换句话说,我们将在一个方向上打开 maxLimits 个订单,而在反方向上打开相同数量的订单。第一个订单位于距离当前价格 Step 个点的地方,而第二个订单又在第一个订单距离 Step 个点的位置,以此类推。
一旦利润达到 takeProfit 值 ($),就固定利润。在这种情况下,所有未结头寸都将关闭,所有已下订单也将取消。之后,EA将重置其网格.
我们根本不考虑亏损的可能性,因此获利是平仓的唯一条件。
填写 OnInit 函数
如前所述, OnInit() 函数在第一次EA启动期间调用一次,我们已经在其中加上了对 init_lang() 函数的调用。让我们把它填到最后,这样就不会再回来了。
在我们的 EA 框架中,OnInit() 函数的唯一目标就是如果价格是3位或者5位小数,就修正 Step 输入参数,换句话说,如果经纪商在交易品种中使用了一个额外的数字位:
ST=Step; if(_Digits==5 || _Digits==3){ ST*=10; }
这样,我们将使用校正后的 ST 参数,而不是EA本身中的Step输入参数。在通过指定double类型调用任何函数之前声明它。
因为我们需要交易品种价格中的距离而不是点数来形成一个网格,所以让我们立即执行转换:
ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT);
在这个函数中,我们还可以检查EA是否允许交易。如果交易被禁止,最好立即通知用户,以便他们能够改进。
可以使用以下小代码进行检查:
if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){ Alert(langs.noBuy+" ("+(string) EA_Magic+")"); ExpertRemove(); }
如果交易被禁止,我们会以用户选择的语言通知。之后,EA操作完成。
所以,OnInit()函数的最终外观如下:
//+------------------------------------------------------------------+ //| EA 交易初始化函数 | //+------------------------------------------------------------------+ int OnInit() { init_lang(); if(!MQLInfoInteger(MQL_TRADE_ALLOWED)){ Alert(langs.noBuy+" ("+(string) EA_Magic+")"); ExpertRemove(); } ST=Step; if(_Digits==5 || _Digits==3){ ST*=10; } ST*=SymbolInfoDouble(_Symbol, SYMBOL_POINT); return(INIT_SUCCEEDED); }
添加全部关闭按钮
与 EA 工作的便利性与其遵守选定的交易策略同样重要。
在我们的例子中,便利性表现在能够一目了然地看到有多少多头和空头头寸已经打开,并找出所有当前打开的头寸的总利润。
如果我们对利润满意或出现问题,我们也应该能够迅速关闭所有未结订单和头寸。
因此,让我们添加显示所有必要数据的按钮,并在单击时关闭所有仓位和订单。
图形对象的前缀. MetaTrader中的每个图形对象都应该有一个名称,一个EA创建的对象的名称不应与手动或其他EA创建的图表上的对象的名称一致。因此,首先,让我们定义要添加到所有图形对象名称中的前缀:
string prefix_graph="grider_";
计算仓位和利润. 现在我们可以创建一个函数来计算未结多头和空头头寸的数量以及它们的总利润,并用获得的数据显示按钮,或者如果已经存在这样的按钮,则更新按钮上的文本。让我们称这个函数为 getmeinfo_btn():
void getmeinfo_btn(string symname){ double posPlus=0; double posMinus=0; double profit=0; double positionExist=false; // 对开启的买入和卖出仓位计数, // 并计算它们的总利润 #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=symname) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; positionExist=true; profit+=PositionGetDouble(POSITION_PROFIT); profit+=PositionGetDouble(POSITION_SWAP); if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY){ posPlus+=PositionGetDouble(POSITION_VOLUME); }else{ posMinus+=PositionGetDouble(POSITION_VOLUME); } } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; } if(OrderSymbol()!=symname) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; positionExist=true; profit+=OrderCommission(); profit+=OrderProfit(); profit+=OrderSwap(); if(OrderType()==OP_BUY){ posPlus+=OrderLots(); }else{ posMinus+=OrderLots(); } } } #endif // 有开启的仓位 // 增加关闭按钮 if(positionExist){ createObject(prefix_graph+"delall", 233, langs.closeAll+" ("+DoubleToString(profit, 2)+") L: "+(string) posPlus+" S: "+(string) posMinus); }else{ // otherwise, delete the button for closing positions if(ObjectFind(0, prefix_graph+"delall")>0){ ObjectDelete(0, prefix_graph+"delall"); } } // 更新当前图表 // 显示所实现的变化 ChartRedraw(0); }
在这里,我们再次使用条件编译,因为在 MQL5 和 MQL4 中操作开启仓位的方法是不同的。为了相同的原因,在文章中我们还将多次使用条件编译。
显示按钮. 还请注意,为了在图表上显示按钮,我们使用了createObject()自定义函数。这个函数检查是否在图表上存在作为第一个函数参数传递名称的按钮。
如果已经创建了按钮,只需根据第三个函数参数中传递的文本更新按钮上的文本。
如果没有按钮,就在图表的右上角创建它。在这种情况下,第二个函数参数设置按钮宽度:
void createObject(string name, int weight, string title){ // 如果图表上没有 'name' 按钮,就创建它 if(ObjectFind(0, name)<0){ // 定义相对于要显示按钮的图表右上角的偏移 long offset= ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87; long offsetY=0; for(int ti=0; ti<ObjectsTotal((long) 0); ti++){ string objName= ObjectName(0, ti); if( StringFind(objName, prefix_graph)<0 ){ continue; } long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE); if( tmpOffset>offsetY){ offsetY=tmpOffset; } } for(int ti=0; ti<ObjectsTotal((long) 0); ti++){ string objName= ObjectName(0, ti); if( StringFind(objName, prefix_graph)<0 ){ continue; } long tmpOffset=ObjectGetInteger(0, objName, OBJPROP_YDISTANCE); if( tmpOffset!=offsetY ){ continue; } tmpOffset=ObjectGetInteger(0, objName, OBJPROP_XDISTANCE); if( tmpOffset>0 && tmpOffset<offset){ offset=tmpOffset; } } offset-=(weight+1); if(offset<0){ offset=ChartGetInteger(0, CHART_WIDTH_IN_PIXELS)-87; offsetY+=25; offset-=(weight+1); } ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0); ObjectSetInteger(0,name,OBJPROP_XDISTANCE,offset); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,offsetY); ObjectSetString(0,name,OBJPROP_TEXT, title); ObjectSetInteger(0,name,OBJPROP_XSIZE,weight); ObjectSetInteger(0,name,OBJPROP_FONTSIZE, 8); ObjectSetInteger(0,name,OBJPROP_COLOR, clrBlack); ObjectSetInteger(0,name,OBJPROP_YSIZE,25); ObjectSetInteger(0,name,OBJPROP_BGCOLOR, clrLightGray); ChartRedraw(0); }else{ ObjectSetString(0,name,OBJPROP_TEXT, title); } }
回应按钮的点击. 现在如果我们调用 getmeinfo_btn() 函数, Close All 按钮就会在图表上出现 (如果我们有开启的仓位). 但是,单击此按钮时还没有发生任何事情。
要添加对单击按钮的响应,我们需要在OnChartEvent()中截取单击。由于这是 OnChartEvent() 函数的唯一目标,因此我们可以提供其最终代码:
void OnChartEvent(const int id, // 事件 ID const long& lparam, // 长整型的事件参数 const double& dparam, // 双精度浮点型的事件参数 const string& sparam) // 字符串类型的事件参数 { string text=""; switch(id){ case CHARTEVENT_OBJECT_CLICK: // 如果按钮名称是 prefix_graph+"delall", 则 if (sparam==prefix_graph+"delall"){ closeAllPos(); } break; } }
现在,当单击“关闭仓位”按钮时,会调用CloseAllPos()函数。这个函数还没有实现,我们将在下一部分中完成。
额外的操作. 我们已经有了计算必要数据并显示关闭仓位按钮的函数getmeinfo_btn()。此外,我们还实现了单击按钮时发生的操作。然而,在 EA 中的任何地方都没有调用这个getmeinfo_btn() 函数,所以,现在它没有显示在图表上。
我们将会在处理标准的 OnTick() 函数时,使用 getmeinfo_btn() 函数。
同时,让我们把注意力转到 OnDeInit() 标准函数上。因为我们的EA创建了一个图形对象,所以在关闭EA时,请确保它创建的所有图形对象都从图表中删除。这就是为什么我们需要 OnDeInit() 函数,它在关闭 EA 时自动调用。
最终,OnDeInit() 函数体看起来如下:
void OnDeinit(const int reason) { ObjectsDeleteAll(0, prefix_graph); }
这段代码在关闭 EA 时删除所有包含指定前缀的图形对象,到目前为止,我们只有一个这样的对象。
实现关闭所有仓位的函数
既然我们已经开始使用closeAllpos()函数,那么让我们实现它的代码。
closeAllPos() 函数关闭所有当前开启的仓位,并删除所有设置的订单,
但是它也不是那么简单。该函数不只是删除所有当前打开的仓位,如果我们有一个开启的多头仓位和同一个空头仓位,我们将尝试用一个相反的仓位来关闭其中一个仓位。如果您的经纪商在当前交易工具上支持此操作,我们将收回为打开两个头寸而支付的利差。这会提高我们 EA 的获利能力。当根据获利关闭所有仓位时,我们实际上能够取得比在takeprofit 参数中指定的稍多的利润。
所以,closeAllPos() 函数的第一行包含了对另一个函数的调用: closeByPos().
closeByPos() 函数尝试根据反向仓位关闭仓位,在关闭了反向仓位之后,closeAllPos() 函数再以常规方式关闭剩余的仓位。之后,它再关闭设置的订单。
在 MQL5 中,我通常使用 CTrade 对象来关闭仓位,因此,在实现这两个自定义函数之前,让我们先包含类并立即创建其对象:
#ifdef __MQL5__ #include <Trade\Trade.mqh> CTrade Trade; #endif
现在我们可以开始开发通过反向仓位关闭所有仓位的函数:
void closeByPos(){ bool repeatOpen=false; #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; if( PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY ){ long closefirst=PositionGetInteger(POSITION_TICKET); double closeLots=PositionGetDouble(POSITION_VOLUME); for(int ti2=cntMyPos-1; ti2>=0; ti2--){ if(PositionGetSymbol(ti2)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; if( PositionGetInteger(POSITION_TYPE)!=POSITION_TYPE_SELL ) continue; if( PositionGetDouble(POSITION_VOLUME)!=closeLots ) continue; MqlTradeRequest request; MqlTradeResult result; ZeroMemory(request); ZeroMemory(result); request.action=TRADE_ACTION_CLOSE_BY; request.position=closefirst; request.position_by=PositionGetInteger(POSITION_TICKET); if(EA_Magic>0) request.magic=EA_Magic; if(OrderSend(request,result)){ repeatOpen=true; break; } } if(repeatOpen){ break; } } } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderSymbol()!=_Symbol ) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; if( OrderType()==OP_BUY ){ int closefirst=OrderTicket(); double closeLots=OrderLots(); for(int ti2=cntMyPos-1; ti2>=0; ti2--){ if(OrderSelect(ti2,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderSymbol()!=_Symbol ) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; if( OrderType()!=OP_SELL ) continue; if( OrderLots()<closeLots ) continue; if( OrderCloseBy(closefirst, OrderTicket()) ){ repeatOpen=true; break; } } if(repeatOpen){ break; } } } } #endif // 如果我们根据反向仓位关闭了仓位 // 再次运行 closeByPos 函数 if(repeatOpen){ closeByPos(); } }
如果 close by 操作成功,函数会调用自身,这是必要的,因为持仓量可能不同,这意味着关闭两个持仓可能不会总是产生必要的结果。如果交易量不同,则一个仓位的交易量只会减少,使其在下一个函数启动期间可由相反的仓位关闭。
在关闭了所有反向仓位之后, closeAllPos() 可以关闭剩余的:
void closeAllPos(){ closeByPos(); #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; Trade.PositionClose(PositionGetInteger(POSITION_TICKET)); } int cntMyPosO=OrdersTotal(); for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue; if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue; Trade.OrderDelete(orderTicket); } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderSymbol()!=_Symbol ) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; if( OrderType()==OP_BUY ){ MqlTick latest_price; if(!SymbolInfoTick(OrderSymbol(),latest_price)){ Alert(GetLastError()); return; } if(!OrderClose(OrderTicket(), OrderLots(),latest_price.bid,100)){ } }else if(OrderType()==OP_SELL){ MqlTick latest_price; if(!SymbolInfoTick(OrderSymbol(),latest_price)){ Alert(GetLastError()); return; } if(!OrderClose(OrderTicket(), OrderLots(),latest_price.ask,100)){ } }else{ if(!OrderDelete(OrderTicket())){ } } } } #endif // 删除关闭仓位的按钮 if(ObjectFind(0, prefix_graph+"delall")>0){ ObjectDelete(0, prefix_graph+"delall"); } }
实现 OnTick 函数
我们已经实现了几乎所有的EA功能,现在是时候开发最重要的部分了——下订单网格。
标准OnTick()在每个交易品种分时到达时调用函数。我们将使用该函数检查网格订单是否存在,如果不存在,则创建它。
柱形起点检查. 但是,在每个分时处执行检查是多余的。例如,每5分钟检查一次网格就足够了。要执行此操作,请将代码检查柱起点添加到OnTick()函数中。如果这不是柱开始的第一个分时,就在不执行任何操作的情况下完成函数:
if( !pdxIsNewBar() ){ return; }
pdxIsNewBar() 函数看起来如下:
bool pdxIsNewBar(){ static datetime Old_Time; datetime New_Time[1]; if(CopyTime(_Symbol,_Period,0,1,New_Time)>0){ if(Old_Time!=New_Time[0]){ Old_Time=New_Time[0]; return true; } } return false; }
为了让EA每五分钟检查一次我们的状况,它应该在M5时间段启动。
检查获利. 在检查网格可用性之前,我们应该检查当前所有未平仓的网格头寸是否实现获利。如果已达到获利,则调用上面描述的closeAllPos()函数。
if(checkTakeProfit()){
closeAllPos();
}
为了检查获利,要调用 checkTakeProfit() 函数,它计算所有当前未结头寸的利润,并将其与 takeProfit 输入参数的值进行比较。
bool checkTakeProfit(){ if( takeProfit<=0 ) return false; double curProfit=0; double profit=0; #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; profit+=PositionGetDouble(POSITION_PROFIT); profit+=PositionGetDouble(POSITION_SWAP); } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; } if(OrderSymbol()!=_Symbol) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; profit+=OrderCommission(); profit+=OrderProfit(); profit+=OrderSwap(); } } #endif if(profit>takeProfit){ return true; } return false; }
显示 Close All 按钮. 不要忘记我们已经实现但是还没有显示的 Close All 按钮,是时候增加它的函数调用了:
getmeinfo_btn(_Symbol);
它将看起来这样:
设置一个网格. 最后,我们到了EA中最重要的部分。这看起来相当简单,因为所有代码都再次隐藏在函数后面:
// 如果交易品种具有未结仓位或已下订单,则 if( existLimits() ){ }else{ // 否则设置网格 initLimits(); }
如果交易品种有开启的仓位,则 existLimits() 函数返回 'true':
bool existLimits(){ #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; return true; } int cntMyPosO=OrdersTotal(); for(int ti=cntMyPosO-1; ti>=0; ti--){ ulong orderTicket=OrderGetTicket(ti); if(OrderGetString(ORDER_SYMBOL)!=_Symbol) continue; if(EA_Magic>0 && OrderGetInteger(ORDER_MAGIC)!=EA_Magic) continue; return true; } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if(OrderSymbol()!=_Symbol) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; return true; } } #endif return false; }
如果函数返回 'true', 什么都不做,否则,使用 initLimits() 函数设置一个新的订单网格:
void initLimits(){ // 用于设置网格订单的价格 double curPrice; // 当前交易品种的价格 MqlTick lastme; SymbolInfoTick(_Symbol, lastme); // 如果没有取得当前价格,取消设置网格 if( lastme.bid==0 ){ return; } // 与可用于放置止损的价格的最小距离并且 // 最可能的挂单 double minStop=SymbolInfoDouble(_Symbol, SYMBOL_POINT)*SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); // 设置买入订单 curPrice=lastme.bid; for(uint i=0; i<maxLimits; i++){ curPrice+=ST; if( curPrice-lastme.ask < minStop ) continue; if(!pdxSendOrder(MY_BUYSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){ } } // 设置卖出订单 curPrice=lastme.ask; for(uint i=0; i<maxLimits; i++){ curPrice-=ST; if( lastme.bid-curPrice < minStop ) continue; if(!pdxSendOrder(MY_SELLSTOP, curPrice, 0, 0, Lot, 0, "", _Symbol)){ } } }
测试 EA
我们的 EA 开发好了,现在我们应该对其进行测试,并得出有关交易策略表现的结论。
由于我们的EA同时在MetaTrader 4和MetaTrader 5中工作,因此我们可以选择终端版本,在其中执行测试。虽然这里的选择很明显,MetaTrader 5被认为更易于理解和更好。
首先,让我们在没有任何优化的情况下执行测试。使用合理值时,我们的EA不应完全依赖输入值。让我们使用:
- EURUSD 交易品种;
- M5 事件框架;
- 时间段从2018年8月1日到2019年1月1日;
- 测试模式1 Minute OHLC.
输入参数默认值保持不变 ( 0.01 手, 步长 10 个点, 每个网格 7 个订单, 获利 $1).
结果显示如下:
从图表上可以看出,整个月和一周一切都很顺利。我们赚了差不多 $100 而回撤为 $30. 然后一个看似不可能的事件发生了。请看一下可视化工具,看看9月份的价格是如何变化的:
它是从9月13日16:15之后开始的,首先,价格触发了一个买入订单,然后它激活了两个卖出订单,再加上两个买入订单,而最终还有剩余的5个卖出订单,结果,我们有了3个买入订单和7个卖出订单,
这在图片上看不到,但价格没有进一步下降。到了9月20日,它回到了最高点,激活了剩下的4个买入订单。
因此,我们有7个卖出订单和7个买入订单,这意味着我们再也无法实现获利。
如果我们看一下价格的进一步变动,它将进一步上涨大约80点。如果我们的链上有13个订单,那么我们很有可能扭转局面,获得利润。
即使这还不够,后来价格会下降200点,所以在这个链上有30个订单,理论上我们可以获得利润。虽然这可能需要几个月的时间,而且回撤量将是巨大的。
测试网格中的新订单数. 让我们检查一下假设,网格中的13个订单什么也没有改变,而20个订单让我们毫发无损:
然而,回撤约为300美元,而总利润略高于100美元。也许,我们的交易策略并不是一个彻底的失败,但它确实需要巨大的改进。
因此,现在没有必要优化它,但我们还是试着这么做吧。
优化. 使用以下参数进行优化:
- 网格中的订单数量: 4-21;
- 网格步长: 10-20 points;
- 获利保持相同 ($1).
结果表明,13个点的步长是最好的,而网格中的订单数是16:
这是在 "基于真实分时的每一分时" 模式下的测试结果。尽管结果是正面的,但119美元5个月的回撤221美元并不是最好的结果。这意味着我们的交易策略确实需要改进。
改进交易策略的可能方法
显然,仅仅利用获利是不够的。有时,价格会在两个方向上触及全部或大部分订单,在这种情况下,如果不是无限期的话,我们可以等几个月的利润。
让我们考虑一下如何解决检测到的问题。
人工控制. 当然,最简单的方法是不时地手动控制EA。如果一个潜在的问题正在酝酿中,我们可能会下更多的订单,或者干脆关闭所有的头寸。
设置附加网格. 例如,如果一个方向70%的订单和另一个方向70%的订单受到影响,我们可以尝试设置另一个网格。来自额外网格的订单可能允许一个方向的未平仓数量快速增加,从而更快地实现获利。
除了未结头寸的数量外,我们还可以检查最后一个未结头寸的日期。例如,如果在打开最后一个仓位后超过一周,则会设置一个新的网格。
有了这两种选择,就有可能进一步加剧已经有大幅回撤的情况。
关闭整个网格并打开一个新网格. 除了设置一个额外的网格,我们可以关闭所有属于当前网格的仓位和订单,承认我们输了一场战斗,但不是整个战争。
我们可以做这一点的情况有很多:
- 如果在两个方向上打开的订单超过N%,
- 如果从打开最后一个仓位到现在已经过了N天,
- 如果所有持仓的亏损都达到了$N。
例如,让我们尝试实现列表中的最后一项。我们将添加一个传入参数,在该参数中,我们将以美元为单位设置损失的大小,在该值处,我们需要关闭当前网格上的仓位并打开一个新的仓位。小于0的数字用于设置损失:
input double takeLoss=0; //如有亏损则平仓, $
现在我们必须重新编写checkTakeProfit()函数,以便它返回所有未结头寸的利润,而不是'true'或'false':
double checkTakeProfit(){ double curProfit=0; double profit=0; #ifdef __MQL5__ int cntMyPos=PositionsTotal(); for(int ti=cntMyPos-1; ti>=0; ti--){ if(PositionGetSymbol(ti)!=_Symbol) continue; if(EA_Magic>0 && PositionGetInteger(POSITION_MAGIC)!=EA_Magic) continue; profit+=PositionGetDouble(POSITION_PROFIT); profit+=PositionGetDouble(POSITION_SWAP); } #else int cntMyPos=OrdersTotal(); if(cntMyPos>0){ for(int ti=cntMyPos-1; ti>=0; ti--){ if(OrderSelect(ti,SELECT_BY_POS,MODE_TRADES)==false) continue; if( OrderType()==OP_BUY || OrderType()==OP_SELL ){}else{ continue; } if(OrderSymbol()!=_Symbol) continue; if(EA_Magic>0 && OrderMagicNumber()!=EA_Magic) continue; profit+=OrderCommission(); profit+=OrderProfit(); profit+=OrderSwap(); } } #endif return profit; }
更改显示为黄色。
现在,我们能够修改ontick()函数,以便它检查除获利外所有头寸的止损:
if(takeProfit>0 && checkTakeProfit()>takeProfit){ closeAllPos(); }else if(takeLoss<0 && checkTakeProfit()<takeLoss){ closeAllPos(); }
另外的测试
让我们看看这些改进是否有用。
我们将只优化-5至-100美元范围内的止损美元。其余参数保持在上一次测试中选择的级别(13个点的步长,网格中16个订单)。
当止损为 -$56 时我们取得的利润最多。5个月内的利润为156美元,最高回撤为83美元:
通过分析图表,我们可以看到止损在五个月只激活了一次。当然,这样做的结果是,在利润与回撤方面会更好。
然而,在做出最终结论之前,让我们先看看我们的EA是否能够在所选参数的长期内产生至少一些利润。让我们在过去五年中尝试一下:
结果令人沮丧,也许,额外的优化可以改善它。在任何情况下,使用这种网格交易策略都需要彻底的改进。对于长期自动交易来说,额外的未平仓头寸迟早会克服亏损的想法是错误的。
为订单增加止损和获利
不幸的是,上面列出的其他EA改进选项也不会产生更好的结果。但是,为单独交易设置止损如何呢?或许,增加止损将改善我们长期自动交易的EA。
对五年历史的优化结果表明,与上述结果相比,效果更好。
止损140个点,获利50个点是最有效的。如果在30天内当前网格上没有打开单个仓位,则关闭该位置并打开新网格。
最终结果如下:
利润为351美元而回撤是226美元当然,这比不使用止损获得的交易结果要好。然而,我们不得不注意到,在执行最后一次交易后不到30天内关闭当前网格时获得的所有结果都是亏损。此外,超过30天的天数也大多以损失告终。所以这个结果更多的是巧合而不是规律。
结论
本文的主要目标是编写一个同时在 MetaTrader 4 和 MetaTrader 5 中工作的 EA 交易,在那个方面我们已经成功。
此外,我们再次看到,在几个月的历史上测试EA是不够的,除非您准备好每周调整其参数。
不幸的是,基于简单网格的想法是不可行的,但也许我们错过了什么。如果你知道如何开发一个基本的网格实际上是有利可图的,请写在评论你的建议。
无论如何,我们的发现并不意味着基于网格的交易策略不能盈利,例如,看看这些信号:
- EURUSD 网格;
- AUDUSD 网格;
- AUDCAD 网格.
这些信号基于一个网格,比这里描述的网格更复杂。这个网格实际上可以每月产生高达100%的利润。ENUM_ORDER_TYPE我们将在下一篇关于网格的文章中详细讨论它。