自动选择经纪公司,以便 Expert Advisor 高效运行

Shashev Sergei | 26 二月, 2016

简介

我们经常会面临这样的情况:Expert Advisor 与一家经纪公司合作能够成功盈利,而与另一家经纪公司合作则无法盈利,甚至于会出现亏损。这其中的原因可能各有不同。不同的经纪公司有不同的设置:

  • 报价。它们由于以下两个因素而稍有不同:数据传送不同,用于平滑报价的过滤不同。对于某些 Expert Advisor,这可能具有关联性。当经常与一家经纪公司进行交易而很少与另一家经纪公司进行交易时,可能会出现这些情况。
  • 滑点。不同的经纪公司可能有很大的不同。这也可能会由于预期利润降低而导致 EA 的特性更糟。
  • 重新报价。在一些经纪公司,会比其他公司更频繁地重新报价。在这种情况下,EA 会由于大量重新报价而失去成功进入点位的机会。
因此,一些 EA 的运行很大程度上取决于它所合作的经纪公司。如果没有选择,我们可以忍受报价、滑点和重新报价。但现在我们可以选择,而且选择的范围会越来越广。我们可以比较不同的经纪公司,查看他们的技术特性,利用这些信息,我们可以为我们的 EA 选择最好的经纪公司。

统计

当然,我们可以从不同的经纪公司启动多个终端,使用一两个月左右,然后再选择一个利润最大的经纪公司。但这种测试提供的信息量很小。最好是获取更多信息:每笔交易的平均滑点、每个特定交易开放时所发生的重新报价次数、开放时间等。为了不用分析日志,开发了一个统计项目。该项目基于以下一系列规则:

  1. EA 分析市场,进行交易,获取有关交易的所有必要信息,并将这些信息传递给一个通用模块。
  2. 此模块包含有关当前交易和已完成交易的所有信息。它还计算有关经纪公司所有技术特性的统计信息。
  3. 它应最大限度适应处理海量数据,这样,我们可以只查阅必要信息,而不是能够收集和计算的所有信息。

分析统计信息(重新报价次数、交易执行时间和滑点)并查看我们能够完成的所有交易,然后找出最适合合作的经纪公司。如果有关所有公司的统计数据都是负数,我们应更改EA 的一些参数,例如在市时间、交易频率等。更改参数,直到 EA 开始盈利。如果在某个阶段,EA 与一家经纪公司的合作停止盈利,而与其他经纪公司的合作有盈利,我们将停止测试这家公司。



理论

我们可以安排通过文件或 dll 将 EA 中的数据提交给一个应用程序。在技术实现方面,包含文件的变量更加简单,因为它不需要严格的系统编程。但是,处理文件并不是很方便,因为我们事先并不知道各个经纪公司的 MetaTrader 4 终端的位置、他们用哪种货币进行测试、如果文件丢失该怎么办等问题。如果我们需要在最高的安全级别下动态地完成所有事情,最好安排通过 dll 传递数据。


要使不同的终端使用同一个 dll,它应位于系统目录 windows\system32 中。重要的是,一个终端中的所有 EA 下载同一份 dll,因为它们在同一进程内运行,即终端 (terminal.exe),这意味着它们拥有同一个地址空间,即它们使用相同的变量。一个具有多个 EA 的终端拥有一份其自己的 dll,适用于所有 EA,并且 dll 中的变量相同,在 dll 内声明,另一个终端拥有另一份带其他变量的 dll 。因此,一个终端无法访问另一个终端的变量。


我们需要创建单个 dll,从中收集不同终端的数据。有不同的方式来安排同步操作包含一个数据字段的不同进程。可通过文件实现这一点,但是我们又会面临路径指示问题,如果我们有很多终端和 EA,还会面临处理速度的问题。最好的解决方案是可分的核心内存。使用此方案需要更加关注和了解使用的操作系统(在我们的案例中为 Windows)的功能,然而,这有无限的可能性。为了安排连续访问共享内存的某个块,将使用程序信号量的特殊机制。


理论结论:EA 通过 dll 将数据写入共享内存中, 然后是应用程序(我们称之为“Monitor”),它从内存读取数据,显示数据,并执行必要统计计算。当 MetaTrader 4 第一次调用 DLL 时,操作系统将生成一份适用于每个终端的 DLL,因为每个终端都是一个独立的进程。操作方案如下图所示。


实践

Expert Advisor

当然,有关当前交易的数据应由 Expert Advisor 生成。要传递数据,我们需要构建函数 dll 的接口。对于已实施的任务,我们需要三个函数:

bool NewExpert(string  isBrokerName, string isInstrument, int Digit);

创建一个新的 Expert Advisor,通过经纪人的名称和证券对其进行标识。为了计算一些统计特征,我们将传递证券价格某点之后的数字位数。

bool NewDeal(string isInstrument, int Action, int magik, 
double PriceOpen, int Slippage, int TimeForOpen, int Requotes);

按如下方式对新交易进行登记:当终端进程已使用经纪人的名称进行标识时,那么对于新的交易,执行交易的证券名称即可。其他参数为交易特征

表 1.交易建立

参数

操作

0 – 买入,1 - 卖出

magik

幻数

PriceOpen

开盘价

滑点

滑点

TimeForOpen

建立周期

重新报价

收到的重新报价的次数

bool CloseDeal(string isInstrument, int magik, double PriceClose, 
               int Slippage, int TimeForClose, int Requotes);

交易关闭将通过证券和幻数进行标识。已传递的参数:

表 2.交易关闭

参数

PriceClose

收盘价

滑点

滑点

TimeForClose

关闭周期

重新报价

收到的重新报价的次数


由于此接口,初始化、建立和关闭函数将如下所示:


初始化:

int init()
  {
   int Digit;
   if(IsDllsAllowed() == false)
     {
       Print("Calling from libraries (DLL) is impossible." + 
             " EA cannot be executed.");
       return(0);
     }
   if(!IsTradeAllowed())
     {
       Print("Trade is not permitted!");
       return(0);     
     }
   Digit = MarketInfo(Symbol(), MODE_DIGITS);
   if((Digit > 0) && (Bid > 0))
     {  
       if(!NewExpert(AccountServer(), Symbol(), Digit))
         {
           Print("Creation of a new broker failed");
           return (0);
         }                      
       Print("A broker is successfully created ");                    
       return(0);      
     }   
   Print("No symbol in MarketInfo!");       
   return(0);  
  }

在初始化期间,在检查终端参数(交易权限和 DLL 调用确认)之后,我们收到了有关证券的位数及其当前价格的信息。如果这两个参数都大于零,该证券将足够显示在终端中,我们可以使用它。每个经纪人的名称各不相同,可使用函数 AccountServer() 收到其名称,所有终端在共享内存中根据名称区分彼此。EA 进行交易的证券的名称各不相同。这就是会出现以下情况的原因:如果不同的 EA 连接到同一个货币对,它们将下载同一份 DLL,而这可能会导致冲突。

开立新订单的函数:

int Deal(int act, double Lot)
  {
   int N = 0;
   int ticket;
   int err;
   double Price_open;
   double Real_price;
   datetime begin_deal;
   double Lots;
   int cmd;
   int magik;
   magik = GenericMagik() + 1;   
   Lots = NormalizeDouble(Lot, 1);
// checking margin for a position opening
   AccountFreeMarginCheck(Symbol(), cmd, Lots);
   err = GetLastError();
   if(err > 0)
     {
       Print("No money for new position");
       return(0);
     }      
   begin_deal=TimeCurrent(); 
   while(N < count)
     {
       if(act == 1)
         {
           Price_open = NormalizeDouble(Ask, Digits);
           cmd = OP_BUY;
         }
       if(act == 2)
         {
           Price_open = NormalizeDouble(Bid, Digits);
           cmd = OP_SELL;
         }
       ticket = OrderSend(Symbol(), cmd, Lots, Price_open,
                          slippage, 0, 0, 0, magik);
       if(ticket > 0)
         {
           if(OrderSelect(ticket, SELECT_BY_TICKET) == true)
             {
               Real_price = OrderOpenPrice();
               NewDeal(Symbol(), cmd,magik, Real_price ,
                       MathAbs(Real_price - Price_open),
                       (TimeCurrent() - begin_deal), N);
             }                   
           return(ticket);
         }
       N++;
       Sleep(5000);
       RefreshRates();
     } 
   return(0);
  }

使用具有两个参数的函数 Deal 开立订单:操作(1 - 买入,2 - 卖出)和手。每个订单的幻数与上一个订单的幻数不同 - 幻数逐渐递增。头寸试图在计数尝试建仓。有关尝试次数的信息以及建立时长、价格和滑点将被传递给共享内存,Monitor 从此处读取这些信息。


订单关闭函数:

bool CloseOrder(int magik)
  {
   int ticket, i;
   double Price_close;
   int count = 0;
   datetime begin_time;
   double Real_close; 
   begin_time = TimeCurrent();    
   for(i = OrdersTotal() - 1; i >= 0; i--) 
     {
       if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
           if(OrderSymbol() == Symbol()) 
               if(OrderMagicNumber() == magik)
                 {                  
                   while(count < 10)
                     {                       
                       if(OrderType() == OP_BUY)        
                           Price_close = NormalizeDouble(Bid, Digits);
                       if(OrderType() == OP_SELL)        
                           Price_close = NormalizeDouble(Ask, Digits);
                       if(OrderClose(OrderTicket(), OrderLots(),
                                     Price_close, slippage))
                         { 
                           Real_close = OrderClosePrice();
                           CloseDeal(Symbol(), magik, Real_close,
                                     MathAbs(Real_close - Price_close),
                                     (TimeCurrent() - begin_time), count); 
                           return(true);
                         }
                       count++;
                       Sleep(5000);
                       RefreshRates();
                     }
                 }
     }
   return(false); 
  }

关闭 CloseOrder() 函数只有一个输入参数 - 幻数。订单多次尝试关闭,此尝试次数以及交易执行时间、收盘价格和滑点将被传送到内存,然后由 Monitor 读取。

其他代码是已测试的 EA。因此,要使用您自己的 EA 中的“统计”,您需要导入必要的 dll 函数;对于初始化和建仓/平仓,使用函数 Deal CloseOrder。。如果需要,您可以重新编写这些函数;但应根据 dll 中包含的接口,传递有关交易的数据。


下面是 EA 使用 DLL 的实现示例(不包括上述枚举函数的代码)。

// Enable dll for operation with monitor
#import "statistik.dll"
  bool NewExpert(string  isBrokerName, string isInstrument, 
                 int Digit);   // Create a broker
  bool NewDeal(string isInstrument, int Action, int magik, 
               double PriceOpen, int Slippage, int TimeForOpen,
               int Requotes);
  bool CloseDeal(string isInstrument, int magik, double PriceClose, 
                 int Slippage, int TimeForClose,
                 int Requotes);
#import
//---- 
extern int Num_Deals = 3;
extern int TimeInMarket = 4;
// maximally acceptable slippage
int  slippage = 10;
// time for rest after a trade
int TimeForSleep = 10;
// period of request
int time_for_action = 1;
// number of attempts for opening a position
int count = 5;
// Function of a new bar
bool isNewBar()
  {
    static datetime BarTime;
    bool res = false; 
    if(BarTime != Time[0]) 
      {
        BarTime = Time[0];  
        res = true;
      } 
   return(res);
  }
//+------------------------------------------------------------------+
//| Generation of magic                                                  |
//+------------------------------------------------------------------+ 
int GenericMagic()
  {
   int deals;
//----  
   for(int i = OrdersTotal() - 1; i >= 0; i--) 
     {
       if(OrderSelect(i, SELECT_BY_POS, MODE_TRADES))
           if(OrderSymbol() == Symbol())
               if(OrderMagicNumber() != 0)
                   deals++;
     }       
   return (deals);
  }
//+------------------------------------------------------------------+
//| forming signals to open/close a position                         |
//+------------------------------------------------------------------+
int GetAction(int &action, double &lot, int &magic)
   {
    int cnt, total;  
    if(OrdersTotal() <= Num_Deals)
      {
        if(Close[1] > Close[2])
          {
            action = 1;
            lot = 1;
            return(0);
          }
        if(Close[2] < Close[1])
          {
            action = 2;
            lot = 1;         
            return(0);               
          }
      }
    total = OrdersTotal();
    for(cnt = total - 1; cnt >= 0; cnt--)
      {
        if(OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES))
            if(OrderSymbol() == Symbol())  
                if((TimeCurrent() - OrderOpenTime()) > TimeInMarket*60)
                  {
                    action = 3;
                    magic = OrderMagicNumber();
                    return(0); 
                  }
      }
   }
//+------------------------------------------------------------------+
//| expert start function                                            |
//+------------------------------------------------------------------+
int start()
  {
   int action = 0;
   double lot = 1;
   int magic = 0;     
   while(!IsStopped())
     {
       Sleep(time_for_action*1000);      
       RefreshRates();
       if(isNewBar())
         {
           GetAction(action, lot, magic);
           if(((action == 1) || (action == 2)))
             {                                        
               if(IsTradeAllowed())
                   Deal(action, lot);
               Sleep(TimeForSleep*1000);
             }
           if(action == 3)
             {
               if(IsTradeAllowed())
                   if(!CloseOrder(magik))
                     {
                       Print("MANUAL CLOSING OF A POSITION IS NEEDED");
                       Sleep(TimeForSleep*1000);   
                     } 
             }
           action = 0;
           lot = 0;
           magik = 0;
         }
     }
   Print("A serious error occurred, the EA stopped operating");  
   return(0);
  }
//+------------------------------------------------------------------+

EA 的执行块是基于启动函数的无限循环。智能交易系统以预定频率 time_for_action 调用分析函数 GetAction() ,该函数将通过引用返回 EA 应执行的操作、应建仓的手数以及幻数(如果您需要平仓)。


在这里,分析块很简单 - 如果上一根柱大于上上根柱,则买入;反之则卖出。由时间平仓。如果要测试您自己的 EA,只需根据它们的算法重新编写此块。您可以不在执行部分中进行任何更改。

DLL

可在不同的环境中用不同的语言实现 DLL。我们工作所需的 dll 是使用 Visual C++ 创建的。交易将采用以下结构:

struct DealRec
  {
    int Index;
    int Magic;
    int Cur;
    int Broker;
    double PriceOpen;
    double PriceClose;
    int SlipOpen;
    int SlipClose;
    int Action;  // 0 = BUY 1 = SELL
    int TimeForOpen;
    int TimeForClose;
    int ReqOpen;
    int ReqClose;
    int Profit;
    bool Checked; // indication that the position is closed
  };

它们将分两个阶段实现 - 在开盘和收盘时。即,在开盘时传递一部分数据(开盘价、开盘时的滑点等),在收盘时传递另一部分数据(收盘价、收盘时间等)。在 dll 中调用函数的原型

__declspec(dllexport) bool __stdcall NewExpert (char *isBrokerName, 
                                                char *isInstrument, 
                                                int Digit);
__declspec(dllexport) bool __stdcall NewDeal (char *isInstrument, 
                                              int Action, 
                                              int magic, 
                                              double PriceOpen, 
                                              int Slippage, 
                                              int TimeForOpen, 
                                              int Requotes);
__declspec(dllexport) bool __stdcall CloseDeal (char *isInstrument, 
                                                int magic, 
                                                double PriceClose, 
                                                int Slippage, 
                                                int TimeForClose, 
                                                int Requotes);

仅在传递行中与 MQL4 的原型不同。您可以全面查看源 dll,这可能有助于创建其他项目。要重新编译项目,使用程序 Visual C++ 打开文件 statistic.dsw。全代码 dll 位于文件 statistic.cpp 和 statistic.h 中,其他 dll 为辅助内容。所有枚举的文件均位于 Statistic.zip 中。

Monitor

用于快速编写包含表格和图形接口的应用程序的最佳工具 - 来自 Borland 的解决方案。即 Delphi 和 C++Builder。

Monitor 功能:创建共享内存,从共享内存读取数据并在表格中显示数据,保留滑点的统计信息。还有更多选项可使工作更加便捷。下面是 Monitor 的功能说明:

  1. 保留开立头寸的日志;
  2. 保留平仓的日志;
  3. 滑点和重新报价的统计信息;
  4. 可调节的表格;
  5. 以 html 文件格式保存交易。


实现位于附带的压缩文件 Statistic Monitor.zip 中。要重新编译项目,使用程序 C++Builder。项目文件的扩展名为 *.bpr。主要代码位于 в main.cpp 中。

测试

为了进行测试,创建了一个特殊的 EA - 它具有根据时间建仓和平仓的最简单条件(前面显示了实现情况)。EA 及 dll 和 monitor.exe 位于压缩文件 monitor+dll+expert.zip 中。启动时,单击 START,进而创建一个共享内存。DLL 应位于文件夹 system32 中。然后,启动多个终端,并将 EA 连接到货币的图表,将进行这些货币的交易。此后,将累积大量交易统计信息。将在 Monitor/日志中收集数据。应时常将这些数据转换为文件,以 html 页面形式存储。



应用程序的实际操作相同。这样,交易者可以比较不同的经纪公司在技术特征方面的操作,并为自动交易选择最好的经纪公司。

总结

在 MQL4 中使用 Dll 能够开发不同的应用程序,这些应用程序不仅有助于制定有关交易的决策,还有助于收集统计数据。后者在交易和选择经纪公司方面可能非常有用。创建的应用程序应帮助开发人员完成这一困难的搜寻。要分析经纪人,根据本文分析的示例中的说明,将 statistic.dll 连接到 Expert Advisor。此工作所需的文件位于 monitor+dll+expert.zip 中。要执行此操作,将 statistic.dll 复制到文件夹 system32 中,从任何位置启动 Statistic. exe,启动装有 Expert Advisor 的终端(下载有 dll),开始交易并将数据传递到共享内存中。Statistic.exe 会创建辅助文件,这就是为什么最好从空文件夹启动应用程序的原因。如果交易机器人的开发人员对此程序感兴趣,可进行修改和补充。

应注意,并非所有经纪公司都为自动交易提供类似的条件:

  1. 经纪人可能会禁止自动交易。
  2. 经纪人可能会禁止在下单时指明止损 (SL) 或获利(TP) https://www.mql5.com/ru/forum/103341
  3. SL 和 TP 的非对称位。
  4. 可能不提供互相建单的选项。
  5. 同一个帐户中的同时建仓数的限制。如果订单数(未平仓位数 + 挂单数)突破限制,函数 OrderSend 将返回错误代码 ERR_TRADE_TOO_MANY_ORDERS。
  6. 其他限制。

这就是为什么强烈建议认真阅读将与您开展合作的经纪公司的规章制度的原因。


统计项目显示,如果您要向 MQL4 选项添加其他语言和编程环境,情况将变得极其复杂。创建的程序对与不同的经纪公司合作的 Expert Advisor 非常有用,因为它有助于用便捷的方式分析这些公司的技术特征。如果经纪公司有滑点,将根据时间执行交易并频繁重新报价,那么面对此类经纪公司,我们需要做什么?有很多替代方案!