请 [注册] 或 [登录]  | 返回主站

量化交易吧 /  量化策略 帖子:3364712 新帖:0

如何将MetaTrader 5中的交易复制到MetaTrader 4

技术性调整发表于:4 月 17 日 17:59回复(1)

引言

曾几何时,许多交易者都认为MetaTrader 5对于实盘交易来说是一个不成熟的、不可靠的平台。但是没多久,公众看法就发生了改变,现在大家都在问MetaTrader 5何时能具备实盘交易的能力。许多交易者已经对MetaTrader 5的优点大加赞赏了。另外,由MetaQuotes软件公司举办的交易锦标赛引起了广大MQL5语言开发者的兴趣。现在,这种兴趣转化为从交易中获利的愿望。“何时MetaTrader 5才能具备实盘交易能力?”这个问题公众问错了对象。它应该由经纪商来回答。何时转到新平台上来的最终决定权在他们手上。

然而面对这种情况,一个普通交易者能做什么呢?答案显而易见,你可以利用MetaTrader 4提供的实盘交易机会,作为MetaTrader 5交易的载体。例如,写一个拷贝工具。将两个MetaTrader 4终端在Web上进行捆绑并不是一件新鲜事。现在我们要实现MetaTrader 4和MetaTrader 5之间的捆绑。

序言

为了将前面提到的想法付诸实施,有必要阐明如下问题:“利润来自哪里?”以及“交易者如何把控利润的增长?”咋一看,答案显而易见。低买高卖。但是,让我们考虑一下利润的组成部分。利润来源于买卖的价差和下单数量的乘积。也就是说,利润有两个组成部分:报价和交易头寸的交易量。

交易者能够控制什么呢?这两个组成部分哪一个才是能够被交易者把控的呢?当然是交易头寸的交易量了。报价来自于经纪商,交易者无法改变。那么得到的第一个结论是:为了实现交易复制,必须保持交易头寸交易量的同步。


1. 两个平台的比较

1.1订单计算系统的区别

由于两个平台之间的订单计算系统不一样,这样就使得复制操作复杂化了。不过我们不要忘记,这个捆绑操作是由MetaTrader 5主导的。这就意味着,在MetaTrader 4中我们实际上要重复相同的订单计算系统。

MetaTrader 5中的交易头寸可由个别交易订单得来,这一点和MetaTrader 4中的订单计算方式并不冲突。MetaTrader 5中为一个头寸设置的止赢止损,可以通过向每一个新建订单设置相同的止赢止损来实现。但当遇到问题:MetaTrader 4中什么订单需要平仓时,两个平台之间的显著差异就体现出来了。由于在MetaTrader 5中,一个交易头寸并没有独立的订单计算系统,这个问题可能成为绊脚石。

1.2交易头寸的交易量

让我仔细考虑下,平仓不同的订单会有什么不一样的结果?它将会影响我们的利润吗?例如,我们在不同时间开启了两个订单,并在不同的时间平仓了,但是某段时间内他们是同时存在的。也就是说,我们试图在订单计算系统中模拟一个交易头寸。

让我们来计算一下,如果我们改变订单的平仓位置,将对利润产生什么影响?

类型 
交易量 
开仓位置
 平仓位置
0.11.393881.38438
0.11.38868
1.38149

让我们编写一段计算代码:

void OnStart()
  {
   double open1=1.39388,close1=1.38438,
         open2=1.38868,close2=1.38149;
   Print("total  ",n1(profit((open1-close1),0.1)+profit((open2-close2),0.1)));
   Print("order2  pp=",ns(open2-close2),"  profit=",n1(profit((open2-close2),0.1)));
   Print("order1  pp=",ns(open1-close1),"  profit=",n1(profit((open1-close1),0.1)));
  }
string ns(double v){return(DoubleToString(v,_Digits));}
string n1(double v){return(DoubleToString(v,1));}
double profit(double v,double lot){return(v/_Point*lot);}
下面是计算结果:
订单1  点数=0.00950  利润=95.0
订单2  点数=0.00719  利润=71.9
合计 166.9

现在我们交换close1close2的值。

订单1  点数=0.01239  利润=123.9
订单2  点数=0.00430  利润=43.0
合计  166.9

图 1. 订单平仓的不同情形

图1显示了两种情况下AB和CD段的交易量都是0.1,而BC段的交易量都是0.2,这并不取决于哪些订单交易量被平仓的事实。

对于单一订单来说利润是不同的,但是对于整个头寸来说,总的利润是一样的。希望你注意的是,上面的例子是用两个交易量一样的订单来计算的。也就是说,在同一个位置,我们不是平仓一个订单,而是关闭了相同交易量。如果我们严格遵循平仓量的原则,那么一个订单的交易量就无关紧要了。如果要平仓的量比订单仓量大,那么会有部分平仓的情况发生。

综上所述:对于总体的持仓头寸来说,哪一个订单被平仓不重要,重要的是在给定位置的平仓量必须一致。

1.3复制交易

从上面的例子你可以看出,为了获取一样的利润,没必要传输MQL5语言写成的EA的交易信号。你只需要复制交易头寸交易量。虽然如此,但这还不是一种完全理想的交易方式,原因我们在后面会提到。然而,这些理由并不能成为,用MQL5语言编写的EA为我们赚取真实利润的阻碍。

报价的差异是利润下滑的首要因素。在某些经纪商中甚至超过了点差。原因是报价是实时复制的。当一个EA在MetaTrader 5中决定在某价位进行开仓时,MetaTrader 4终端连接的经纪商这时很可能有着不同的报价。价格对交易者来说可能有利也可能不利。

时间因素是影响利润的第二个因素。MetaTrader 5中一个头寸出现后,交易被复制,这之间的延时不可避免。

这两个因素使得头皮型策略黯然神伤。因此,在MetaTrader 5实盘交易成为可能之前,这类策略是不适用的。 

如果一个系统能够获利(从一笔交易中),且利润远大于点差并对经纪商的报价不敏感,那么使用交易复制器来赚钱将是可行的。


2. 设定问题

  1. 在MetaTrader 5和MetaTrader 4之间传输信号
  2. 从MetaTrader 5中翻译头寸信息
  3. 在MetaTrader 4中接收信号
  4. 在MetaTrader 4中复制交易头寸

2.1. 在MetaTrader 5和MetaTrader 4之间传输信号

第一步是通过共享文件传输信号。可能会问:频繁的读写不会损伤硬盘吗?如果你仅仅在头寸发生变化时写入新数据,那么这样的操作不会很频繁。不会比Windowa开发者更改系统页面文件更频繁了。这也相应的证明了不会对硬盘有损伤。如果不是非常频繁的请求共享文件以写入数据,那么这种实现方式是可以接受的。这其实也是对头皮型策略的另一个限制,虽然不像前面提到的限制条件那样明显。

你可以使用MetaTrader 5的特性,对任何深度的子目录中的文件进行读写。我不敢保证“任何”深度都行,但对10级深度子目录下的文件进行读写,肯定是没问题的。我们也不需要更深层级的了。当然你也可以使用DLL来实现。除非别无选择,否则我是不会用DLL的,这是我的基本原则。想要不通过DLL来解决这个问题,只需将MetaTrader 4安装至MetaTrader 5终端的 \Files\ 文件夹下(查看文件操作)。

那么共享文件的目录如下:

C:\Program Files\MetaTrader 5\MQL5\Files\MetaTrader 4\experts\files\xxx      // xxx是共享文件的名称。

这样,MetaTrader 4和 MetaTrader 5就都可以访问这个文件了,MQL5函数提供了文件共享的功能。

2.2. 从MetaTrader 5中翻译头寸信息

为了节约资源开销来翻译头寸,我们需要设计一个函数,来监控所有交易品种交易头寸的建立/修改/关闭。如前所述,转换一笔交易时我们只需要知道头寸的交易量。增加到交易品种的交易量以及止损止赢水平。

为了追踪变化,我们需要知道一个头寸先前的状态。如果当前状态和前一个状态不一致(即头寸发生了变化),需要在文件中记录下来。我们需要设计一个函数来将此信息写入文件。为了让多个程序同时访问,文件必须是打开状态的。

不要遗漏头寸被修改的情况,追踪系统一定要在OnTimer()函数中实现,因为我们需要同时追踪所有的交易品种,而不同交易品种的报价可能在不同的时间到来。我们还要将此种变化在文件中用符号标记一下。

2.3. 在MetaTrader 4中接收信号

有必要对文件的更新进行追踪。这可以通过设置一个变量来实现,它的状态用来监控头寸的变化。我们需要一个函数来从文件中读取头寸状态。这是一个标准函数。

将文件内容传入数组中用于计算。这里我们需要一个解析器。当从MetaTrader 5中传输内容时,把所有内容以字符串形式存储比较方便,因为不单是数字,字符也会被传输。另外,将一个交易品种的所有数据写入一个文本字符串,有助于我们避免产生混淆。

2.4. 在MetaTrader 4中复制交易头寸

这部分的函数是最复杂的。他将被分为多个子函数。

  1. 虚拟头寸比较;
  2. 订单选择函数;
  3. 订单开仓函数;
  4. 订单平仓函数;
  5. 订单修改函数。

2.4.1. 虚拟头寸比较

为了确保复制的头寸符合要求,要对虚拟头寸进行比较。函数需要单独计算每个交易品种的头寸,并且要能够过滤掉被禁止的交易(如果有的话)。

实际上,可能存在这种情况,即从MetaTrader 5传过来的交易品种,某经纪商报价系统中并不存在。这种情况下一般不应该禁止交易,但应该提供告警。用户有权知道这种情况的发生。

2.4.2. 订单选择函数

此函数用来选择基于某交易品种的订单,以便后续对他们进行进一步的操作。此种情况下,因为我们仅仅广播未平仓头寸,所以挂单必须要被过滤掉。

2.4.3. 开仓函数

它应该包含所有计算用到的参数。因此,传递头寸的交易量和类型给它就够了。

2.4.4. 订单平仓函数

就像开仓函数一样,在平仓前它需要计算所有值。

2.4.5. 订单修改函数

本函数需要包含实时市场情况的检查功能。因为许多经纪商都不允许在开仓的时候设置止损止赢,所以在开仓后再设置才是可行的。此外,同时开仓和设置止损止赢增加了下单失败的可能性。

因此,头寸很快会被重复。我们知道,设置止损止赢是必须要做的事情,它非常重要。

3. 实现

代码几乎逐行详细注释了。因此,我只解释代码中最难懂的部分。

在MetaTrader 5和MetaTrader 4之间传输信号

MetaTrader 5中的如下函数实现关联功能:

void WriteFile(string folder="Translator positions") // 默认情况下的共享文件名

打开方式参数的意义:

FILE_WRITE|FILE_SHARE_READ|FILE_ANSI

文件打开待写 | 允许不同的程序共享读取文件| ansi格式

MetaTrader 4中实现关联的函数:

int READS(string files,string &s[],bool resize)

resize参数禁止对接收到的数组数据,进行内存的重新分配。在代码中,该数组的内存根据循环计算的结果动态分配,因为开发者无法预估数组的行数。它取决于MetaTrader 5中所选择的交易品种数目。因此无法在MetaTrader 4中事先计算好。

所以,该数组在每一循环后增加一行。但是,此操作须在第二次函数调用时锁定,因为此时数组的大小已经确定并不会改变了。bool resize正是用来实现这一目的的。

从MetaTrader 5中翻译头寸信息

使用OnTimer 函数来管理头寸翻译,所有的头寸数据会以每秒一次的频率被如下函数接收

void get_positions()

对比头寸前值和现值的函数:

bool compare_positions()

当至少有一个地方不一致时,返回(true)返回(true)意味着头寸发生变化,共享文件要重写。当重写文件时,计数器cnt_command加一。

在MetaTrader 4中接收信号

当使用READS()函数读文件后,我们得到一个字符串数组s[]

为了将这些信息利用起来,我们需要一个转换器。

函数如下:

int parser(int Size)

它仅仅是一个用于调用线性识别函数的壳:

void parser_string(int x)

此函数对除交易品种名称外的所有单元进行识别。

交易品种名称在算法开始处的循环中进行一次识别,函数如下:

void parser_string_Symbols(int x)

接下来,除非特别指出,我们的讨论都是基于MQL4的。

虚拟头寸比较

头寸比较分为两个部分。在如下函数中比较头寸交易量和类型:

bool compare_positions()

在此函数中,获取头寸实际状态的函数如下:

void real_pos_volum()

根据前面提到的原则,比较函数的机制是“全有或全无”。也就是说,只要有一个地方不一致,那么所有的头寸都被认为是有变化的。在real_pos_volum()函数中使用了几个过滤器,已经在代码注释中详细描述了,并且将被其他函数反复使用。

特别的,它用于将一个交易品种的所有订单交易量求和进入一个虚拟头寸。为了正确操作锁定的头寸(如果有的话),买单交易量用减号,卖单交易量用加号。

第二部分是比较止损水平(止损水平就是止赢和止损),在类似上面的函数中实现:

bool compare_sl_tp_levels()
和交易量的情况一样,在壳内可以获取函数中止赢止损的水平信息:
void real_pos_sl_tp_levels()

订单选择函数

选择订单的唯一目的就是平仓,这也就是为什么这个复杂的函数仅仅实现平仓功能:

void close_market_order(string symbol,double lot)

整个交易品种和交易量的参数,应该平仓哪一个?为了尽可能少的拆分订单,在函数的第一个循环中,查找订单,其交易量等同于参数传递寻求保证的平仓订单,它等同于参数传递的平仓交易量的交易量。

如果没有这样的订单(平仓信号FlagLot等于状态true),那么在循环中订单指定交易量最先被平仓 (对于订单交易量是否超出的检测,则是通过Closes()函数来实现)。

选择订单修改其止赢止损的功能由如下函数实现:

void modification_sl_tp_levels()

订单仅根据交易品种名称过滤,因为同一个交易品种订单的止赢止损都是一样的。

开仓函数

实现函数如下:

int open_market_order(string symbol,int cmd,double volume,
                     int stoploss=0,int takeprofit=0,int magic=0)

它包含了对订单开仓所需的所有特定数据的检查。

订单平仓函数

实现函数如下:

bool Closes(string symbol,int ticket,double lot)

代码包含一个检查以防止lot参数超出先前选中订单的真实交易量。

订单修改函数

实现函数如下:

bool OrderTradeModif(int ticket,string symbol,int cmd,double price,
                    double stoploss=0,double takeprofit=0,int magic=0)

代码进行检查,如果止损水平和订单类型不符,则值进行对调。代码也检查水平是否已经到达请求的值。

4. 逻辑函数

至此,前面的问题解决了,但是代码中仍有一些还未解释的函数。它们是逻辑函数,我们称之为基础函数,是程序运作的核心。

void processing_signals()
void processing_sl_tp_levels()

这两个函数的特征是做无限循环,直到有条件的break退出。我们必须注意的是,脚本本身也是作为无限循环实现的。为了让用户能够方便的移除此程序,循环的主要条件就是拥有内置IsStopped()函数。

代码是以如下的方式从EA交易转移至循环脚本的:

 // Init()
 while(!IsStopped())
    {
     // Start()
     Sleep(1000);
    }
 // Deinit()

整个脚本的逻辑判断在标准函数start()中以同样的无限循环形式描述。

start() 函数中的循环代码类似如下:

     如果交易数据流空闲
          读文件,并将数据保存在数组中(不改变数组的大小);
          如果文件有变化
               写入新的注释;
               记住循环的开始时间;
               如果头寸对比不相等
                    处理此头寸的大小;
               如果头寸的止赢止损不相等;
                    处理此头寸的止赢止损;
               计算检查的结束时间;
          如果时间没有超时
               剩下的时间暂停;

最复杂的逻辑构造是位于函数processing_signals()processing_sl_tp_levels()

我们采用“从简单到复杂”的原则介绍这些个函数,虽然代码中的调用正好相反。

//+------------------------------------------------------------------+
//| 处理止赢和止损                                                     |
//+------------------------------------------------------------------+
void processing_sl_tp_levels()
  {
//--- 记住进入循环体的时间
   int start=GetTickCount();
   while(!IsStopped())
     {
      //--- 如果交易流空闲
      if(Busy_and_Connected())
        {
         //--- 选则订单并修改止赢止损           
         modification_sl_tp_levels();
        }
      //--- 如果延迟时间到,在文件中更新数据
      if(GetTickCount()-start>delay_time)READS("Translator positions",s,false);
      //--- 如果文件中标识是否有过更新的计数器发生变化,那么退出循环
      if(cnt_command!=StrToInteger(s[0]))break;
      //--- 短时休眠      
      Sleep(50);
      //---如果订单的实际止赢止损和文件中的一致,则退出循环
      if(!compare_sl_tp_levels())break;
     }
   return;
  }

正如之前提到的,函数仅在两种情况下退出无限循环:

第一种情况是cnt_command 的值和文件中保存的值不相等时。在退出之前,如果循环操作的时间超过了由全局变量delay_time设置的延迟时间时,我们需从文件中读取最新的信息。

因为所有的修改都被Busy_and_Connected()函数保护,所以可能会发生超时。也就是说,只在交易数据流空闲的时候才操作。

需要特别说明的是,在MetaTrader 4 (和MetaTrader 5相比较而言) 中,发送一系列指令到服务器,而没有一次重新报价是不可能的。服务器只会接受第一个请求,其余的将会被丢弃。因此,在向服务器发送指令前,我们必须检查一下交易数据流是否空闲。

第二种退出循环的情况是由止赢止损位比较函数compare_sl_tp_levels()控制的:如果头寸的止赢止损相等,那么退出循环。

现在我们来看最复杂的部分:processing_signals ()函数,它的构成类似,但是逻辑判断部分实现的功能迥异。

让我们来详细分析如下代码:

//--- 把文件中存储的头寸持仓方向,转换成-1,+1的形式
int TF=SymPosType[i]*2-1;
//--- 把实际头寸持仓方向转换成-1,+1的形式
int TR=realSymPosType[i]*2-1;
//--- 保存文件中的头寸大小
double VF=SymPosVol[i];
//--- 保存实际的头寸大小
double VR=realSymPosVol[i];
double lot;
//--- 如果当前货币对的头寸不相等
if(NormalizeDouble(VF*TF,8)!=NormalizeDouble(VR*TR,8))
  {
//--- 如果实际头寸大小不为零并且持仓方向不一致,或者
//--- 如果持仓方向一致,但是实际头寸大小比文件中的大
   if((VR!=0 && TF!=TR) || (TF==TR && VF<VR))
     {
      //--- 如果持仓方向一致,但是实际头寸大小比文件中的大
      if(TF==TR && VF<VR)lot=realSymPosVol[i]-SymPosVol[i];
      //--- 如果实际头寸大小不为零并且持仓方向不一致
      else lot=realSymPosVol[i];
      //--- 平仓,并退出循环
      close_market_order(Symbols[i],lot);
      break;
     }
   else
     {
      //--- 如果持仓方向一致,但是实际头寸大小比文件中的小 
      if(TF==TR && VF>VR)lot=SymPosVol[i]-realSymPosVol[i];
      //--- 如果持仓方向不一致且仓位等于零
      else lot=SymPosVol[i];
      //---开仓,并退出循环 
      open_market_order(Symbols[i],SymPosType[i],lot);
      break;
     }
  }

TFTR变量以buy=-1,sell=1的形式存储头寸类型。TF是保存在文件中的值,TR是虚拟头寸的实际的值。VFVR也一样。

因此,不相等:
if(VF*TF!=VR*TR)

将返回true如果交易量或类型不一致。

然后是逻辑连接:

if((VR!=0 && TF!=TR) || (TF==TR && VF<VR))

它表示,如果实际交易量不等于零,类型也不相同,那么你必须平仓整个头寸了。

这包括当文件中的交易量为零以及头寸方向转变的情况。当头寸方向转变时,你必须首先为开仓做准备,例如,平掉之前的持仓。在下一个迭代中,进入逻辑判断的另一个分支,开仓。

第二部分逻辑连接的复杂条件是,如果类型正确,但是实际交易量比文件中存储的大,那么你必须减少实际交易量。因此,我们首先计算减少交易量的必要手数大小。

如果没有正好满足条件的订单,并且头寸不相等,那么要新开仓。这里也有两个变量:新开整个头寸大小的订单或者在现有订单上加仓。要提醒一下,开仓函数中对是否超出开仓大小限制进行了检查,因此超出部分的仓位将会在下一个迭代中进行开仓。因为首先考虑平仓,然后才开仓,基于以上事实,锁单是几乎不可能的。

代码中有一个细节我想指出。当MetaTrader 4中的头寸刚刚根据止赢止损位平仓时,又重新开仓的情况。我之前提到过,5位数报价的差异通常是在2-3点之间。对于15点的点差来说是微不足道的。正是由于这个差异,如果止赢或者止损在MetaTrader 4中早于MetaTrader 5中触发,程序将会视图重新开仓,在MetaTrader 5的止赢或者止损触发后再次平仓。

虽然不会造成大的损失,但是会损失点差。因此,对算法进行重新设计,当头寸平仓后,MetaTrader 4不会再次恢复头寸,一直等待直到文件状态改变。只有等到那时程序才会重新开始行动。在这种情况下,如果交易者发现不对的话,可以手动平仓。在MetaTrader 5改变文件前,头寸不会被再次建立。

这样做的唯一缺点是,有一种罕见的情况,即头寸在MetaTrader 4中的止赢止损位平仓了,但是MetaTrader 5中的头寸却没有平仓。这种情况下,我建议重启Copyist positions脚本。最后要说明的是:本程序不对周末时间进行检查。这没什么影响,只是将会有产生很多无价值的重新报价日志。

5. 检查程序运行的实际情况

将MetaTrader 4安装到C:\Program Files\MetaTrader 5\MQL5\Files\ 文件夹下

在MetaTrader 5终端的任意图表(本EA的运行效果不取决于它所在的图表)上运行编译过的EATranslator positions

 

图 2. 转换MetaTrader 5上的头寸

我们看到多行注释,第一行是计数器状态,下面逐行是所有头寸的日志。

在MetaTrader 4终端的任意图表(运行效果不取决于它所在的图表)上运行Copyist positions脚本。

图 3. 在MetaTrader 4中复制头寸

现在我们可以在MetaTrader 5中运行任何EA交易。EA的运行结果很快就会复制到MetaTrader 4中。

图 4. MetaTrader 4 (顶部) 和 MetaTrader 5 (底部)中的头寸和订单

顺便说一句,MetaTrader 5的帐户管理可以手动进行,或者通过观察密码登陆。

因此,比方说,你可以在任何一个锦标赛帐户上启动复制器。


总结

本文的目的是促进交易者向新平台过渡,并鼓励其对MQL5语言进行研究。

总之我想说的是,本程序无法完全替代直接使用MetaTrader 5实盘帐户进行交易。如果不考虑逻辑判断部分,本程序是对任何交易系统适用的通用代码,因此正如一切通用的东西一样,总是会有不尽如人意的地方。但是有此作为基础,你可以为某特定的策略写一个信号转换工具。对于很多不会编程的交易者来说,可以将它作为正式版本发布前的过渡。

对于那些精通编程的人来说,我建议修改代码来使得它能够通过magic数字识别订单,并实现挂单的转换和下单。只要服务器网络连接稳定,使用挂单不会影响利润。如果连接经常断开,所有的订单包括挂单,都必须被再次复制。

让我们学习新的语言,并使用它开发出稳健的系统。祝你在交易中好运。

全部回复

0/140

量化课程

    移动端课程