EA 遥控方法

Dmitriy Gizlyk | 28 十一月, 2018

内容

概述

我们处于数字时代,各种用于金融市场交易的自动化系统已经变得非常普遍。 电子智能交易系统(EA)的主要优点一般认定为能够完美无瑕的执行算法,并可一天 24 小时不间断工作。 虚拟主机允许全天候自动运行 EA。

不幸的是,并非所有的 EA 在任何市场情况下都能同样有效。 在这些情况下,交易者通常需要手动启用和禁用它们。 当用户可以直接访问终端时,这很容易做到。 但是如果您无法快速访问终端来控制 EA 工作怎么办? 在这种情况下,能够远程控制 EA 操作会很好。 我们来研究一下在终端中遥控 EA 的可能方法之一。

1. 设置任务

乍一看,任务似乎很清晰:您需要创建一个程序,在收到外部命令后向 EA 发送指令。 但在更深入地研究这个问题之后,我们可以看到 MetaTrader 5 无法通过编程手段直接影响第三方 EA 的工作。 每个 EA 在单独的线程中工作,并且无法检测正运行在打开图表上的 EA。 用户 fxsaber 提供了一套解决方案 "智能系统 - MetaTrader 5 专用库"。

在此代码中,作者建议使用保存模板的功能。 乍一看,保存模板不会影响图表上所运行程序的操作。 确实如此,但在保存图表模板时,应用于图表的所有图形对象以及所有已启动的应用程序及其参数都会保存到文件中。 随后将保存的模板应用到图表,则将所有图形对象和程序与已保存的参数一起恢复。

这种影响的另一方面,加载模板之前存在的所有对象和程序都会从图表中删除。 换句话说,如果在图表上启动 EA,则在模板不包含此 EA 的情况下会将其删除,反之亦然。 因此,编辑为模板文件可减少从图表加载和删除 EA。

当然,可以提前准备必要的模板,并在必要时加载模板而无需编辑它们。 然而,在这种情况下,所需模板的数量增长速度是所用 EA 数量的两倍。 此外,如果对各品种使用不同的 EA 设置,每个添加的操作品种均会导致模板数量的增加。 准备模板变为例行工作,这样在应用模板时才不会出现混淆。

编辑模板也有其自身的细微差别。 默认情况下,模板将保存到 “数据_文件夹\Profiles\Templates\”。 但是 MQL5 只允许使用“沙箱”文件。 fxsaber 再次提出了一个解决方案:他建议在指定模板文件名时,添加至沙箱的路径。 因此,可以在不应用第三方函数库的情况下访问模板文件。

我们鸣谢 fxsaber,感谢他敏锐的头脑和非凡的思想。

在定义了 EA 管理方法之后,我们研究一下程序与用户间通信模型。 如今的世界迫使人们保持移动,并随时随身携带智能手机。 MetaTrader 5 平台提供了适用于 iPhone 和 Android 的版本。 将终端连接到他们的帐户后,用户可以分析图表上的价格变动并手动交易。 目前,移动终端不允许使用 EA 和第三方指标。 如果您想使用 EA,桌面终端和虚拟主机是唯有的选择。

连接到帐户的移动终端允许查看整个帐户,但是移动终端和桌面终端之间没有直接连接通道。 我们唯一可以产生影响的是放置和删除订单。 已下订单会立即显示在帐户上,并可由桌面终端中运行的 EA 跟踪。 因此,与下订单一起,我们可以将控制命令传输到我们的主控 EA。 我们只需要定义命令列表以及用于传递它们的代码。 以下篇幅将讨论这些问题。

2. 分析模板文件

首先,我们查看模板文件结构。 以下示例提供了 EURUSD M30 图表模板,以及将要用到的 ExpertMACD EA 和 MACD 指标。

<chart>
id=130874249669663027
symbol=EURUSD
period_type=0
period_size=30
digits=5
.....
.....
windows_total=2

<expert>
name=ExpertMACD
path=Experts\Advisors\ExpertMACD.ex5
expertmode=0
<inputs>
Inp_Expert_Title=ExpertMACD
Inp_Signal_MACD_PeriodFast=12
Inp_Signal_MACD_PeriodSlow=24
Inp_Signal_MACD_PeriodSignal=9
Inp_Signal_MACD_TakeProfit=50
Inp_Signal_MACD_StopLoss=20
</inputs>
</expert>

<window>
height=162.766545
objects=103

<indicator>
name=Main
........
</indicator>
<object>
.......
</object>

<object>
........
</object>
........
........
<object>
........
</object>

</window>

<window>
height=50.000000
objects=0

<indicator>
name=MACD
path=
apply=1
show_data=1
scale_inherit=0
scale_line=0
scale_line_percent=50
scale_line_value=0.000000
scale_fix_min=0
scale_fix_min_val=-0.001895
scale_fix_max=0
scale_fix_max_val=0.001374
expertmode=0
fixed_height=-1

<graph>
name=
draw=2
style=0
width=1
color=12632256
</graph>

<graph>
name=
draw=1
style=2
width=1
color=255
</graph>
fast_ema=12
slow_ema=24
macd_sma=9
</indicator>
</window>
</chart>

从代码中可以看出,模板文件中的信息是由标签构成和划分的。 该文件以包括主图表数据的 <chart> 标签开头,还包含有图表 ID,品种和时间帧。 我们感兴趣的信息位于 <expert> 和 </expert> 标签之间。

模板块的开头提供 EA 的数据:图表上显示的简称,和文件路径。 接下来是 expertmode 标志。 其状态指示 EA 是否允许交易。 该标志后跟由 <inputs> </inputs> 标签突出显示的 EA 参数。 然后我们可以在图表子窗口中找到数据。 每个子窗口都由 <window> 和 </window> 标签突出显示,其中包含已启动指标(<indicator> ... </indicator>)和应用图形对象(<object> ... </object>)的描述。

此外,请记住,如果在图表上启动的 EA 和/或指标创建图形对象,最好从模板中删除它们。 否则,对象将从模板里应用到图表上,并且在启动此类 EA 和/或指标时,它们会重新创建相同的对象。

这可能导致不受控制地创建大量相同的对象,令图表混乱,以及计算资源的过度消耗。 在最坏的情况下,必要的程序会显示对象创建错误,并从图表中卸载。

编程创建的对象一般通过为其分配 hidden 属性将图形对象隐藏在列表中,以防止用户修改。 在模板中提供了“hidden”标志来设置此属性。 它等于 1 时即隐藏对象。

因此,要启用/禁用 EA,我们只需要重写模板文件,更改 expertmode 标志值,并顺次删除隐藏的对象。 应用新模板会重新启动具有必要属性的 EA。

3. 定义命令

在定义了开发 EA 的主要原则之后,是时候创建通信系统了。 之前,我们已经决定使用订单发送命令。 但是我们如何在不破坏结算结果的情况下使用订单发送命令? 出于这些目的,我们将使用挂单,且在 EA 收到命令后将其删除。

在放置挂单时,我们应该确保它在其工作时间段内不会被激活。 此处的解决方案非常明显 - 我们需要在距离当前价格足够远的位置下单。

令我惊讶的是,在使用移动终端时,我发现无法对已开订单添加注释。 同时,用户可以在桌面终端中查看所创建订单的注释。 因此,我们可以安排从主控 EA 到用户的单向通信。 不过,用户将无法以这种方式发出命令。

因此,我们能够通过 EA 放置预设挂单,但我们不能在它们当中留有注释。 在这种情况下,我们可以使用价格和订单品种来传递命令。 在此,我们应该为编码命令定义价格。 它应与当前价格留有足够的距离,以便在任何情况下都不会激活放置的订单。

我相信,这些价格接近于零。 理论上,任何品种的价格接近零的概率都可以忽略不计。 所以,如果我们将订单价格设置为 1-5 个最小逐笔差价,它很可能永远不会被激活。 当然,在这种情况下,我们仅限于向下突破卖单 (sell stop),因为向下突破卖单的使用不会影响可用保证金

请记住,我们能够在移动应用程序中读取订单注释,即我们的主控 EA 能够以这种方式向我们发送数据。

综上所述,提出了以下命令代码:

# 价格 交易量 命令
1 1 最小逐笔报价 任意 请求 EA 的状态。
主控 EA 设置挂单且带有 EA 的品种,而 EA 的名称和交易许可在订单注释中指定。
2 2 最小逐笔报价 任意 EA 停止交易。
如果订单没有注释,则停止所有 EA。 如果在第一条命令之后由于修改订单而发出了命令,则停止某个品种上的某个 EA。
3 3 最小逐笔报价 任意 启动 EA 交易。
操作原理与命令 2 中的相同。
4 4 最小逐笔报价 最小 1 手针对买入
最小 2 手针对卖出
最小 3 手针对全部
删除挂单。
5 5 最小逐笔报价 最小 1 手针对买入
最小 2 手针对卖出
最小 3 手针对全部
平仓。


4. 创建主控 EA

现在我们已经充分了解了操作方法和通信通道,我们来开始开发 EA。 我们的 EA 代码将应用“智能系统 - MetaTrader 5 专用函数库”中的方法并进行微小修改 - 所有函数库方法都为公有以便于使用。

首先,让我们为应用的标签分配助记名称以方便使用。 其中一些已在应用程序库中声明。

#define FILENAME (__FILE__ + ".tpl")
#define PATH "\\Files\\"
#define STRING_END "\r\n"
#define EXPERT_BEGIN ("<expert>" + STRING_END)
#define EXPERT_END ("</expert>" + STRING_END)
#define EXPERT_INPUT_BEGIN ("<inputs>" + STRING_END)
#define EXPERT_INPUT_END ("</inputs>" + STRING_END)
#define EXPERT_CHART_BEGIN ("<chart>" + STRING_END)
#define EXPERT_NAME "name="
#define EXPERT_PATH "path="
#define EXPERT_STOPLEVEL "stops_color="

其它的则在我们的 EA 代码中声明。

#define OBJECT_BEGIN             ("<object>" + STRING_END)
#define OBJECT_END               ("</object>" + STRING_END)
#define OBJECTS_NUMBER           ("objects=")
#define OBJECT_HIDDEN            ("hidden=1")
#define EXPERT_EXPERTMODE        ("expertmode=")

还应注意,fxsaber 很注重其函数库与其它代码的兼容性,并取消了函数库末尾的助记名称。 这种方法消除了将同一名称重新分配给另一个宏的错误。 尽管不允许在函数库之外使用此类声明。 为了不在 EA 代码中重复类似的宏声明,我们在函数库代码中注释掉 #undef 指令。

//#undef EXPERT_STOPLEVEL
//#undef EXPERT_PATH
//#undef EXPERT_NAME
//#undef EXPERT_CHART_BEGIN
//#undef EXPERT_INPUT_END
//#undef EXPERT_INPUT_BEGIN
//#undef EXPERT_END
//#undef EXPERT_BEGIN
//#undef STRING_END
//#undef PATH
//#undef FILENAME

然后为我们的主控 EA 声明两个外部变量:以分钟为单位的信令订单的生命周期,以及识别它们的魔幻数字。

sinput int      Expirations =  5;
sinput ulong    Magic       =  88888;

将上面提到的函数库和交易操作哈桑农户库添加到 EA 代码中。

#include <fxsaber\Expert.mqh>
#include <Trade\Trade.mqh>

在全局变量模块中,声明交易操作类的实例以及用于存储工作图表 ID 和最后一笔命令订单单号的变量。

CTrade   *Trade;
ulong     chart;
ulong     last_command;

OnInit 函数中,初始化全局变量。

int OnInit()
  {
//---
   Trade =  new CTrade();
   if(CheckPointer(Trade)==POINTER_INVALID)
      return INIT_FAILED;
   Trade.SetDeviationInPoints(0);
   Trade.SetExpertMagicNumber(Magic);
   Trade.SetMarginMode();
   Trade.SetTypeFillingBySymbol(_Symbol);
//---
   chart=ChartID();
   last_command=0;
//---
   return(INIT_SUCCEEDED);
  }

不要忘记删除 OnDeinit 函数中的交易操作类实例。

void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(Trade)!=POINTER_INVALID)
      delete Trade;
  }


4.1. 命令管理器

启动主控 EA 的功能是从 OnTradeTransaction 事务执行的。 请记住,对于一笔订单的处理,可以多次调用该函数。 因此,在运行程序代码之前,我们将进行一系列检查。

我们将检查正在处理事件的订单单号的可用性。 我们还将检查订单状态,以便不处理删除订单的事件。 此外,我们应该检查订单魔幻数字以避免处理非 EA 订单,并验证订单类型,因为我们的命令仅通过 向下突破卖单 (sell stop) 达成。 此外,我们应该检查交易操作的类型,因为要添加或修改订单。

成功完成所有检查后,启动 CheckCommand 函数中安排的命令管理器。

void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
//---
   if(trans.order>0 && trans.order_state!=ORDER_STATE_REQUEST_CANCEL && request.magic==0 && 
      trans.order_type==ORDER_TYPE_SELL_STOP && 
      (trans.type==TRADE_TRANSACTION_ORDER_ADD || trans.type==TRADE_TRANSACTION_ORDER_UPDATE))
      CheckCommand(trans,request);
  }

启动 CheckCommand 函数时,它会在其参数中接收交易操作和请求结构。 首先,我们检查是否之前已处理过此请求。 如果该命令已被处理,请退出该函数。

void CheckCommand(const MqlTradeTransaction &trans,
                  const MqlTradeRequest &request)
  {
   if(last_command==trans.order)
      return;

如果没有,则将订单价格解码为命令。

   double tick=SymbolInfoDouble(trans.symbol,SYMBOL_TRADE_TICK_SIZE);
   uint command=(uint)NormalizeDouble(trans.price/tick,0);

然后,使用 switch 操作符调用与传入命令相对应的函数。

   switch(command)
     {
      case 1:
        if(StringLen(request.comment)>0 || trans.type==TRADE_TRANSACTION_ORDER_UPDATE)
           return;
        if(trans.order<=0 || (OrderSelect(trans.order) && StringLen(OrderGetString(ORDER_COMMENT))>0))
           return;
        GetExpertsInfo();
        break;
      case 2:
        ChangeExpertsMode(trans,request,false);
        break;
      case 3:
        ChangeExpertsMode(trans,request,true);
        break;
      case 4:
        DeleteOrders(trans);
        break;
      case 5:
        ClosePosition(trans);
        break;
      default:
        return;
     }

总之,保存最后一条命令的单号并从帐户中删除订单。

   last_command=trans.order;
   Trade.OrderDelete(last_command);
  }

附件中提供了所有函数的完整代码。


4.2. 已启动 EA 的数据

GetExpertsInfo 函数负责获取终端中所有已启动 EA 的数据。 如前决定,此函数会依据启动 EA 的图表上所显示的品种放置信令挂单,而 EA 名称及其状态将显示在订单注释中。

在函数的开头,通过调用 DeleteOrdersByMagic 函数删除以前设置的订单。

void GetExpertsInfo(void)
  {
   DeleteOrdersByMagic(Magic);

进而,安排循环来迭代终端中的所有活动图表。 循环伊始检查所分析图表是否为我们的主控 EA 的工作图表。 如果是,则转到下一个图表。

   long i_chart=ChartFirst();
   while(i_chart>=0 && !IsStopped())
     {
      if(i_chart==0 || i_chart==chart)
        {
         i_chart=ChartNext(i_chart);
         continue;
        }

在下一阶段,加载图表模板,初步检查图表上的 EA 状态。 如果 EA 不存在或模板未加载,则转到下一个图表。

      string temp=EXPERT::TemplateToString(i_chart,true);
      if(temp==NULL)
        {
         i_chart=ChartNext(i_chart);
         continue;
        }

接下来,在模板中找到 EA 区块。 如果找不到该区块,则转到下一个图表。

      temp=EXPERT::StringBetween(temp,EXPERT_BEGIN,EXPERT_END);
      if(temp==NULL)
        {
         i_chart=ChartNext(i_chart);
         continue;
        }

之后,恢复 EA 名称及其状态。 它们用于形成未来信令订单的注释。 如果 EA 允许交易,则在其名称前面加上“T”字母,否则使用“S”字母。

      string name =  EXPERT::StringBetween(temp,EXPERT_NAME,STRING_END);
      bool state  =  (bool)StringToInteger(EXPERT::StringBetween(temp,EXPERT_EXPERTMODE,STRING_END));
      string comment =  (state ? "T " : "S ")+name;

在循环结束时,确定图表品种和信令订单有效期。 发送订单并转到下一个图表。

      string symbol=ChartSymbol(i_chart);
      ENUM_ORDER_TYPE_TIME type=ORDER_TIME_GTC;
      datetime expir=0;
      if(Expirations>0)
        {
         expir=TimeCurrent()+Expirations*60;
         type=ORDER_TIME_SPECIFIED;
        }
      Trade.SellStop(SymbolInfoDouble(symbol,SYMBOL_VOLUME_MIN),SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_SIZE),symbol,0,0,type,expir,comment);
      i_chart=ChartNext(i_chart);
     }
  }

在上面提到的函数中,我们使用了 fxsaber的 函数库中的方法 — 将图表模板转换为字符串变量并获取指定标签之间的子字符串。

如有必要,检查图表上是否存在 EA,以获取指定图表的字符串变量的模板。 然后保存指定图表的模板,并将获取的模板作为二进制数组读取。 将数字数组转换为字符串并返回调用函数。 如果在任何验证阶段出现错误,则该函数返回 NULL。

  static string TemplateToString( const long Chart_ID = 0, const bool CheckExpert = false )
  {
    short Data[];
    return(((!CheckExpert || EXPERT::Is(Chart_ID)) && ::ChartSaveTemplate((ulong)Chart_ID, PATH + FILENAME) && (::FileLoad(FILENAME, Data) > 0)) ?
           ::ShortArrayToString(Data) : NULL);
  }

若要获取指定标签之间的子字符串,首先要定义子字符串开头和结尾的位置。

  static string StringBetween( string &Str, const string StrBegin, const string StrEnd = NULL )
  {
    int PosBegin = ::StringFind(Str, StrBegin);
    PosBegin = (PosBegin >= 0) ? PosBegin + ::StringLen(StrBegin) : 0;

    const int PosEnd = ::StringFind(Str, StrEnd, PosBegin);

然后剪切子串以便返回,并将原始字符串减少到原始余数。

    const string Res = ::StringSubstr(Str, PosBegin, (PosEnd >= 0) ? PosEnd - PosBegin : -1);
    Str = (PosEnd >= 0) ? ::StringSubstr(Str, PosEnd + ::StringLen(StrEnd)) : NULL;

    if (Str == "")
      Str = NULL;

    return(Res);
  }

在附件中可找到所有函数和类的完整代码。

4.3. EA 状态更改功能

在 ChangeExpertsMode 函数中更改 EA 状态。 在其参数中,指定的函数获取交易操作和请求结构,以及放置 EA 的新状态。

void ChangeExpertsMode(const MqlTradeTransaction &trans,
                       const MqlTradeRequest &request,
                       bool  ExpertMode)
  {
   string comment=request.comment;
   if(StringLen(comment)<=0 && OrderSelect(trans.order))
      comment=OrderGetString(ORDER_COMMENT);      
   string exp_name=(StringLen(comment)>2 ? StringSubstr(comment,2) : NULL);

然后如上所述,安排循环来迭代所有图表并加载模板。

   long i_chart=ChartFirst();
   while(i_chart>=0 && !IsStopped())
     {
      if(i_chart==0 || i_chart==chart || (StringLen(exp_name)>0 && ChartSymbol()!=trans.symbol))
        {
         i_chart=ChartNext(i_chart);
         continue;
        }
      string temp=EXPERT::TemplateToString(i_chart,true);
      if(temp==NULL)
        {
         i_chart=ChartNext(i_chart);
         continue;
        }

在下一阶段,如有必要,检查图表上必要 EA 的可用性。 如果在图表上启动的 EA 与请求不对应,则转到下一个图表。 另外,检查当前的 EA 状态。 如果它对应于放置的那一组,则转到下一个图表。

      string NewTemplate   =  NULL;
      if(exp_name!=NULL)
        {
         NewTemplate=EXPERT::StringBetween2(temp,NULL,EXPERT_NAME);
         string name=EXPERT::StringBetween(temp,NULL,STRING_END);
         if(name!=exp_name)
           {
            i_chart=ChartNext(i_chart);
            continue;
           }
         NewTemplate+=name+STRING_END;
        }
//---
      NewTemplate+=EXPERT::StringBetween2(temp,NULL,EXPERT_EXPERTMODE);
      bool state  =  (bool)StringToInteger(EXPERT::StringBetween(temp,NULL,STRING_END));
      if(state==ExpertMode)
        {
         i_chart=ChartNext(i_chart);
         continue;
        }

在通过所有必要的检查后,创建一个指定了 EA 状态的新模板。 从模板中删除隐藏的对象,并将新模板应用于图表。 执行所有操作后,转到下一个图表。

      NewTemplate+=IntegerToString(ExpertMode)+STRING_END+temp;
      NewTemplate=DeleteHiddenObjects(NewTemplate);
      EXPERT::TemplateApply(i_chart,NewTemplate,true);
//---
      i_chart=ChartNext(i_chart);
     }

完成图表迭代循环后,启动活动的 EA 数据收集函数,以向用户示意 EA 的新状态。

   GetExpertsInfo();
  }

附件中提供了所有函数的完整代码。

4.4. 删除订单和平仓的功能

平单和持仓的函数基于相同的算法,仅在目标对象上有所不同。 因此,我们在本文中只研究其中之一。 当调用 DeleteOrders 函数时,在参数中传递交易操作结构给它。 该结构用于在平单方向上解码订单交易量(根据第 3 节中的命令表)。

void DeleteOrders(const MqlTradeTransaction &trans)
  {
   int direct=(int)NormalizeDouble(trans.volume/SymbolInfoDouble(trans.symbol,SYMBOL_VOLUME_MIN),0);

之后安排用于迭代帐户上所有订单的循环。 在此循环中,将检查订单类型是否符合传入命令。 如果匹配,则发送订单删除请求。

   for(int i=total-1;i>=0;i--)
     {
      ulong ticket=OrderGetTicket((uint)i);
      if(ticket<=0)
         continue;
//---
      switch((ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE))
        {
         case ORDER_TYPE_BUY_LIMIT:
         case ORDER_TYPE_BUY_STOP:
         case ORDER_TYPE_BUY_STOP_LIMIT:
           if(direct==2)
              continue;
           Trade.OrderDelete(ticket);
           break;
         case ORDER_TYPE_SELL_LIMIT:
         case ORDER_TYPE_SELL_STOP:
         case ORDER_TYPE_SELL_STOP_LIMIT:
           if(direct==1)
              continue;
           Trade.OrderDelete(ticket);
           break;
        }
     }
  }

附件中提供了所有函数的完整代码。


结束语

本文提出了 MetaTrader 5 平台中遥控 EA 的方法。 该解决方案提升了交易者在其活动中使用交易机器人的移动性。 应用非标准方法来使用标准函数可以在不使用各种 dll 的情况下解决更广泛的问题。

参考

智能系统 - MetaTrader 5 专用函数库

本文中使用的程序

#
名称
类型
说明
1 Expert.mqh 类库 智能系统 - MetaTrader 5 专用函数库
2 Master.mq5 智能交易系统 用于管理终端中已启动的其它 EA 的主控 EA
3 Master.mqproj 项目文件 主控 EA 项目