通过指定的幻数计算总持仓量的最佳方法

Dmitry Fedoseev | 9 十二月, 2013

简介

MetaTrader 5 客户端允许用一个交易品种平行处理几个 EA 交易。这很简单 - 只需要打开几个图表,然后将 EA 交易附加到它们。如果每个 EA 交易与其他采用相同交易品种的 EA 交易独立工作,就会非常好(采用不同交易品种的 EA 交易没有此类问题)。

第一,它允许 EA 交易与其在策略测试程序中的测试性能和优化完全一致的交易。建仓条件可能取决于已经建立的仓位大小或没有仓位。如果几个 EA 交易采用相同的交易品种,则它们会相互影响。

第二个或许也是更重要的事是允许 EA 交易依据在 EA 交易中实施的交易策略使用不同的资金管理系统。最后 - 能够监视每个 EA 交易的结果并在必要时关闭 EA 交易。

1. 持仓量计算的一般原则

在建仓时,您可以通过在传递给 OrderSend() 函数的 MqlTradeRequest 结构中指定幻数变量值,用幻数对其进行标记。在订单得到执行时,交易也被标记有订单的幻数。此外,通过分析历史记录中的交易,我们可以查看不同 EA 交易建立的交易。

总仓位的计算方法相当简单:举例而言,如果您执行买入 0.1 手的交易,之后其他人买入 0.1 手,再卖出 0.1 手,则总持仓量将等于 0.1+0.1-0.1=+0.1 手。我们加上买入量,减去卖出量,就得到总持仓量。

在总持仓量等于 0 时开始计算非常重要。第一个并且最显然的此类时间点是开立帐户时。换言之,您可以使用 HistorySelect() 函数请求帐户的所有交易历史记录,该函数的第一个参数等于 0(最不可能的时间),第二个参数的值为 TimeCurrent()(服务器的最新已知时间):

HistorySelect(0,TimeCurrent()); // 加载历史数据

接着,从头到尾遍历整个历史记录,针对具有指定幻数的每笔交易,加上买入量,减去卖出量。这也是一个解决方案,但在实践中,交易的历史记录可能非常大。这可能显著影响 EA 交易的速度,尤其是在测试和优化期间,以至于不能实际使用此类 EA 交易。我们需要找出交易历史记录中总的净持仓量等于零的最后一刻。

为此,我们必须首先遍历整个历史记录并找出总 p的净持仓量等于零的最后一刻。找到此时间点之后,我们将其保存在某个变量中(固定仓位时间)。之后,EA 交易将从保存的时间点开始遍历交易历史记录。更好的解决方法是将此时间点保存在客户端的一个全局变量中,而不是 EA 交易的变量,因为在后面一种情形中,分离 EA 交易时就会摧毁该变量。

在此类情况下,即使 EA 交易已启动,您也需要载入最少的必需历史记录,而不是交易的整个历史记录。很多 EA 交易可以在相同的交易品种上交易,因此我们将与所有 EA 交易共享此全局变量(含有已存储的最近零持仓量时间点)。

让我们偏离主题,考虑一下客户端全局变量的使用,这样允许几个 EA 交易采用相同的交易品种(可能具有不同的参数),并且避免 EA 交易的不同实例创建的名称的重复。

2. 使用客户端的全局变量

MQL5 语言具有 MQL5InfoString() 函数,允许获取有关 mql5 程序的不同信息。

为了获取有关文件名的信息,使用 MQL5_PROGRAM_NAME 标识符调用此函数:

MQL5InfoString(MQL5_PROGRAM_NAME); // EA 名

因此,我们以一个 EA 交易的名称作为全局变量名称的开头。一个 EA 交易可能处理几个交易品种;这意味着我们需要添加交易品种的名称(交易品种)。几个 EA 交易可能处理相同的交易品种,但时间框架不同(不同的设置),对于这些情况,我们需要使用幻数。因此,我们也添加幻数。

举例而言,如果 EA 交易有一个存储在变量 Magic_N 中的幻数,则我们将其添加到全局变量的名称中。

所有全局变量的名称看起来如下所示:

gvp=MQL5InfoString(MQL5_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // EA名及交易品种名 
                                                                            // 和它的幻数

其中 gvp(全局变量前缀)是一个在公共变量部分中声明的字符串变量。

我希望澄清此术语以避免全局变量的混淆,因为它们在编程中使用(全局变量在所有函数内都可见,而函数的局部变量仅在函数内部可见)。

但是在这里,我们有不同的情况 - “全局变量”这一术语指客户端的全局变量(存储在一个文件中的特殊变量,通过 GlobalVariable...() 函数使用)。当讨论在编程中使用的全局变量时,我们将使用“公共变量”这一术语。局部变量这一术语一样指局部变量。

全局变量非常有用,因为它们在 EA 交易去初始化(EA 交易、客户端和计算机重启)之后仍然保存它们的值,但是在测试模式中,必须清除所有变量(或在优化时清除以前的传递)。应在实际操作中使用的全局变量与在测试期间创建的全局变量分隔开来;必须在测试之后删除测试期间创建的全局变量。但是您不应修改或删除 EA 交易创建的全局变量。

使用 AccountInfoInteger() 函数并采用 ACCOUNT_TRADE_MODE 标识符调用该函数,您能够区分当前模式:测试程序、演示或实际帐户。

让我们向全局变量添加一个前缀:"d" - 使用演示帐户,"r" - 使用实际帐户,"t" - 在策略测试程序中工作:

gvp=MQL5InfoString(MQL5_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // EA 名及交易品种名
                                                                            // 和 EA 的幻数
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO))
  {
   gvp=gvp+"d_"; // 演示帐户
  }
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL)
  {
   gvp=gvp+"r_"; // 实际
  }
if(MQL5InfoInteger(MQL5_TESTING))
  {
   gvp=gvp+"t_"; // 测试
  } 

函数应从 EA 交易的 OnInit() 函数调用。

如前文所述,在测试时应删除全局变量,换言之,我们需要在 EA 交易的 OnDeinit() 函数中添加删除全局变量的函数:

void fDeleteGV()
  {
   if(MQL5InfoInteger(MQL5_TESTING)) // 测试模式
     {
      for(int i=GlobalVariablesTotal()-1;i>=0;i--) // 检查所有的全局变量 (从后向前)
        {
         if(StringFind(GlobalVariableName(i),gvp,0)==0) // 搜索指定前缀
           {
            GlobalVariableDel(GlobalVariableName(i)); // 删除变量
           }
        }
     }
  }

目前不可能在 MetaTrader 5 内中断测试,即不能保证 OnDeinit() 函数的执行,然而将来可能会发生。我们不知道在中断策略测试程序之后是否将执行 OnDeinit() 函数,因此我们在 EA 交易开始运行时就删除全局变量 - 在 OnInit() 函数内。

我们获得 OnInit()OnDeinit() 函数的以下代码:

int OnInit()
  {
   fCreateGVP(); // 创建客户终端全局变量名的前缀
   fDeleteGV();  // 当工作于测试时,删除全局变量
   return(0);
  }

void OnDeinit(const int reason)
  {
   fDeleteGV();  // 当工作于测试时,删除全局变量
  }

我们还可以通过创建采用全局变量短名称的函数来简化全局变量的使用(代替 GlobalVariableSet(gvp+...)。

设置全局变量的值的函数:

void fGVS(string aName,double aValue)
  {
   GlobalVariableSet(gvp+aName,aValue);
  }

获取全局变量的值的函数:

double fGVG(string aName)
  {
   return(GlobalVariableGet(gvp+aName));
  }

删除全局变量的函数:

void fGVD(string aName)
  {
   GlobalVariableDel(gvp+aName);
  }

我们已经讨论了全局变量,但这并不是全部。

我们需要提供为交易品种创建全局变量的可能性,并且在帐户和策略测试程序中提供不同的操作。这些全局变量的名称不应取决于 EA 交易的名称和幻数。

让我们为一个全局变量前缀声明另一个变量,命名为 "Commom_gvp"。接着,在使用帐户时,其值为 "COMMON",在使用策略测试程序时,其值与变量 gvp 的相同(以在策略回测进程开始之前或结束之后删除变量)。

最后,准备全局变量前缀的函数具有以下形式:

void fCreateGVP()
  {
   gvp=MQL5InfoString(MQL5_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_";
   Commom_gvp="COMMOM_"; // 所有 EA 的常用变量前缀
   if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO)
     {
      gvp=gvp+"d_";
     }
   if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL)
     {
      gvp=gvp+"r_";
     }
   if(MQL5InfoInteger(MQL5_TESTING))
     {
      gvp=gvp+"t_";
      Commom_gvp=gvp; // 用于测试, 变量有如此前缀 
                      // 测试之后将被删除
     }
  }

有人可能认为全局变量的前缀不包括额外的信息 - 演示和实际帐户的分离,以及测试时的 "t" 前缀,尽管可以通过添加表示我们的 EA 交易在策略测试程序中工作的字符 "t" 来实现。但是我通过这种方式来进行。我们不知道将来以及分析 EA 交易的工作需要什么。

存储并不是如他们说的一样让人悲伤。

上述函数意味着客户端处理一个帐户,在其工作期间没有任何帐户改变。禁止在 EA 交易的工作期间改变帐户。当然,如果需要,可通过向全局变量的名称添加一个帐号来解决此问题。

另一个非常重要的事项!全局变量名称的长度不得超过 63 个字符。因此,不要向您的 EA 交易指定太长的名称。

我们完成了全局变量的讨论,现在是时候考虑本文的主题:按指定幻数计算持仓量。

3. 计算持仓量

首先,让我们使用 GlobalVariableCheck() 函数检查是否有一个全局变量含有最后一个零持仓量时间的相关信息(为了简单起见,如果没有任何已建立的仓位,我们称之为“零仓位”情形)。

如果有这样的变量 - 让我们从存储在该变量中的时间开始加载交易的历史记录,否则我们将加载整个历史记录:

if(GlobalVariableCheck(Commom_gvp+sSymbol+"_HistStTm")) // 保存持仓为 "零" 时的时间
  {
   pLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm"); // 初始化时间设置 
                                                                             // 仅选择需要的历史数据
  }
else
 {
   GlobalVariableSet(Commom_gvp+sSymbol+"_HistStTm",0);
 }
if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // 加载必要的合约历史部分
  { 
   return(false);
  } 

接下来,我们定义某个交易品种的总的净持仓量:

double CurrentVolume=fSymbolLots(pSymbol);

使用 fSymbolLots() 函数确定持仓量。

有几种方式获取持仓量:例如可以使用 PositionSelect() 函数来完成。如果函数返回 false,则表示没有任何仓位(持仓量等于零)。如果函数返回 true,则可以使用 PositionGetDouble() 函数及 POSITION_VOLUME 标识符获取持仓量。使用 PositionGetInteger() 函数及 POSITION_TYPE 标识符确定仓位类型(买入或卖出)。对于长仓,函数返回一个正值,对于短仓,函数返回一个负值。

完整的函数看起来如下所示:

double fSymbolLots(string aSymbol)
  {
   if(PositionSelect(aSymbol,1000)) // 仓位选择成功, 所以它存在
     {
      switch(PositionGetInteger(POSITION_TYPE)) // 依据方向返回正或负
        {
         case POSITION_TYPE_BUY:
            return(NormalizeDouble(PositionGetDouble(POSITION_VOLUME),2));
            break;
         case POSITION_TYPE_SELL:
            return(NormalizeDouble(-PositionGetDouble(POSITION_VOLUME),2));
            break;
        }
     }
   else
     {
      return(0);
     }
  }

此外,您可以通过遍历所有仓位来确定交易品种的总持仓量,仓位数量由 PositionsTotal() 函数确定。之后,使用 PositionGetSymbol() 函数查找必要的交易品种,分别使用 PositionGetDouble() 函数及 POSITION_VOLUME 标识符和 PositionGetInteger() 函数及 POSITION_TYPE 标识符确定持仓量和方向。

在这个例子中,准备就绪的函数具有以下形式: 

double fSymbolLots(string aSymbol)
  {
   double TmpLots=0;
   for(int i=0;i<PositionsTotal();i++) // 遍历所有持仓
     {
      if(PositionGetSymbol(i)==aSymbol) // 我们发现指定交易品种的持仓
        {
         TmpLots=PositionGetDouble(POSITION_VOLUME);
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            TmpLots*=-1; // 标记依据持仓类型           }
         break;
        }
     }
   TmpLots=NormalizeDouble(TmpLots,2);
   return(TmpLots);
  }

确定当前持仓量之后,我们将从尾到头遍历交易历史记录,直到总持仓量等于当前持仓量。

使用 HistoryDealsTotal() 函数确定交易的选定历史记录的长度,使用 HistoryDealGetTicket() 函数确定每笔交易的单证,使用 HistoryDealGetInteger() 函数(DEAL_TYPE 标识符表示交易类型)和 HistoryDealGetDouble()DEAL_VOLUME 标识符表示交易量)提取交易数据:

double Sum=0; 
int FromI=0;
int FromTicket=0;
for(int i=HistoryDealsTotal()-1;i>=0;i--) // 从后至前遍历所有合约 
  {
   ulong ticket=HistoryDealGetTicket(i); // 得到合约的单号
   if(ticket!=0)
     {
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // 依据合约方向增减持仓量
        {
         case DEAL_TYPE_BUY:
            Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            Sum=NormalizeDouble(Sum,2);
            break;
         case DEAL_TYPE_SELL:
            Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            Sum=NormalizeDouble(Sum,2);
            break;
        }
      if(CurrentVolume==Sum) // 所有合约已扫描
        {
         sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME); // 保存持仓为 "零" 时的时间
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
         FromI=i; // 保存索引
         break;
        }
     }
  }

当我们找到此点时,我们将时间存储到将在以后加载交易历史记录时使用的全局变量(历史记录中的交易索引存储在 FromI 变量中)。

在索引为 FromI 的交易之前,交易品种的总持仓量等于零。

现在,我们从 FromI 前进到历史记录的末尾,并且统计含有指定幻数的交易的量:

static double sVolume=0;
static ulong sLastTicket=0;
for(int i=FromI;i<HistoryDealsTotal();i++) // 从第一个合约至结尾
  {
   ulong ticket=HistoryDealGetTicket(i);   // 取合约的单号
   if(ticket!=0)
     {
      if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol) // 指定交易品种
        {
         long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
         if(PosMagic==aMagic || aMagic==-1) // 指定标识号
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // 增减持仓量 
                                                       // 依据合约类型
              {
               case DEAL_TYPE_BUY:
                  sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  sLastTicket=ticket;
                  break;
               case DEAL_TYPE_SELL:
                  sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  sLastTicket=ticket;
                  break;
              }
           }
        }
     }
  } 

在循环结束之后,我们将获得按指定幻数统计的当前持仓量,含有指定幻数的最后一笔交易的单证将存储在 sLastTicket 变量中,在执行交易之后,含有指定幻数的总持仓量将等于 sVolume。函数的初步工作结束。

sLoadHistoryFrom、sLastTicket 和 sVolume 变量声明为静态变量(它们在函数执行完毕之后存储它们的值),在以后调用函数时将使用这些值。

我们拥有时间(交易历史记录的起点)、交易单证、执行交易后的总持仓量将具有当前值。

因为零持仓量的时间,足以从当前时间遍历到已保存的时间,并执行交易量汇总,保存持仓量和最后一笔交易的单证。

因此,EA 交易的总持仓量的计算是对最后几笔交易的处理:

if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // 请求至当前时间的合约历史
  {
   return(false);
  }
for(int i=HistoryDealsTotal()-1;i>=0;i--) // 从末至前循环
  {
   ulong ticket=HistoryDealGetTicket(i); // 取单号
   if(ticket!=0)
     {
      if(ticket==sLastTicket) // 我们发现已计算的合约, 保存单号并终止循环
        {
         sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
         break;
        }
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // 依据合约类型增减持仓量      
        {
         case DEAL_TYPE_BUY:
            sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
         case DEAL_TYPE_SELL:
            sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
            break;
        }
     }
  }

函数的算法可以表示如下:

完整的函数:

bool fGetPositionVolume(string aSymbol,int aMagic,double aVolume)
  {
   static bool FirstStart=false;
   static double sVolume=0;
   static ulong sLastTicket=0;
   static datetime sLoadHistoryFrom=0;
   // 当 EA 开始时,函数首次执行
   if(!FirstStart)
     {
      if(GlobalVariableCheck(Commom_gvp+aSymbol+"_HistStTm"))
        {
         sLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+aSymbol+"_HistStTm");
        }
      else
        {
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",0);
        }
      if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // 如果不成功返回, 
                                                      // 我们将在下一个即时价时重复
        {
         return(false);
        }
      double CurrentVolume=fSymbolLots(aSymbol); // 总计持仓量
      double Sum=0;
      int FromI=0;
      int FromTicket=0;
      // 搜索最后的持仓为零的时间
      for(int i=HistoryDealsTotal()-1;i>=0;i--)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
              {
               case DEAL_TYPE_BUY:
                  Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
               case DEAL_TYPE_SELL:
                  Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
              }
            if(CurrentVolume==Sum)
              {
               sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME);
               GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
               FromI=i;
               break;
              }
           }
        }
      // 计算指定的识别号及交易品种的持仓量
      for(int i=FromI;i<HistoryDealsTotal();i++)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol)
              {
               long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
               if(PosMagic==aMagic || aMagic==-1)
                 {
                  switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                    {
                     case DEAL_TYPE_BUY:
                        sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        sLastTicket=ticket;
                        break;
                     case DEAL_TYPE_SELL:
                        sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        sLastTicket=ticket;
                        break;
                    }
                 }
              }
           }
        }
      FirstStart=true;
     }

   // 重计算合约的持仓量 (指定的交易品种及识别号)
   // , 零持仓时间之后
   if(!HistorySelect(sLoadHistoryFrom,TimeCurrent()))
     {
      return(false);
     }
   for(int i=HistoryDealsTotal()-1;i>=0;i--)
     {
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket!=0)
        {
         if(ticket==sLastTicket)
           {
            sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
            break;
           }
         switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
           {
            case DEAL_TYPE_BUY:
               sVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
               break;
            case DEAL_TYPE_SELL:
               sVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
               break;
           }
        }
     }
   aVolume=NormalizeDouble(sVolume,2);;
   return(true);
  }

交易品种和幻数被传递给返回持仓量的函数。如果成功,返回 true,否则返回 false。

成功时,它将请求的量返回到通过引用传递给函数的变量 aVolume。在函数中声明的静态变量不允许使用这个具有不同参数(交易品种和幻数)的函数。

在 MQL4 中,可通过用不同的名称创建此函数的副本,然后针对另一对“交易品种-幻数”组合调用该副本,或者将变量 FirstStart、sVolume、sLastTicket、sLoadHistoryFrom 声明为公共变量 - 针对每一对“交易品种-幻数”,并将它们传递给函数来解决这个问题。

在 MQL5 中,也可以用相同的方式实施,但是 MQL5 有一个更方便的特点 - 类,在这种地方,使用类非常合理。使用类时,必须为每一对交易品种-幻数创建一个类的实例,数据将存储在每个类实例中。

让我们声明 PositionVolume 类。在函数内声明为静态变量的所有变量,都将被声明为 private(私有),除了 Volume 变量以外,我们不会从 EA 交易直接使用它们。但是,我们将仅在执行持仓量计算函数之后才需要它。我们还声明了 Symbol 和 Magic 变量 - 不可能将它们传递给函数,仅在初始化类实例时才进行一次。

类具有两个公共函数:初始化函数和用于计算持仓量的函数,以及一个用于确定总持仓量的私有函数:

class PositionVolume
  {
private:
   string            pSymbol;
   int               pMagic;
   bool              pFirstStart;
   ulong             pLastTicket;
   double            pVolume;
   datetime         pLoadHistoryFrom;
   double            SymbolLots();
public:
   void Init(string aSymbol,int aMagic)
     {
      pSymbol=aSymbol;
      pMagic=aMagic;
      pFirstStart=false;
      pLastTicket=0;
      pVolume=0;
     }
   bool              GetVolume(double  &aVolume);
  }; 
bool PositionVolume::GetVolume(double  &aVolume)
  {
   if(!pFirstStart)
     {
      if(GlobalVariableCheck(Commom_gvp+pSymbol+"_HistStTm"))
        {
         pLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm");
        }
      else
        {
         GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",0);
        }
      if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
        {
         return(false);
        }
      double CurrentVolume=fSymbolLots(pSymbol);
      double Sum=0;
      int FromI=0;
      int FromTicket=0;
      for(int i=HistoryDealsTotal()-1;i>=0;i--)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
              {
               case DEAL_TYPE_BUY:
                  Sum+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
               case DEAL_TYPE_SELL:
                  Sum-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                  Sum=NormalizeDouble(Sum,2);
                  break;
              }
            if(CurrentVolume==Sum)
              {
               pLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME);
               GlobalVariableSet(Commom_gvp+pSymbol+"_HistStTm",pLoadHistoryFrom);
               FromI=i;
               break;
              }
           }
        }
      for(int i=FromI;i<HistoryDealsTotal();i++)
        {
         ulong ticket=HistoryDealGetTicket(i);
         if(ticket!=0)
           {
            if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol)
              {
               long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
               if(PosMagic==pMagic || pMagic==-1)
                 {
                  switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                    {
                     case DEAL_TYPE_BUY:
                        pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        pLastTicket=ticket;
                        break;
                     case DEAL_TYPE_SELL:
                        pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                        pLastTicket=ticket;
                        break;
                    }
                 }
              }
           }
        }
      pFirstStart=true;
     }
   if(!HistorySelect(pLoadHistoryFrom,TimeCurrent()))
     {
      return(false);
     }
   for(int i=HistoryDealsTotal()-1;i>=0;i--)
     {
      ulong ticket=HistoryDealGetTicket(i);
      if(ticket!=0)
        {
         if(ticket==pLastTicket)
           {
            break;
           }
         if(HistoryDealGetString(ticket,DEAL_SYMBOL)==pSymbol)
           {
            long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
            if(PosMagic==pMagic || pMagic==-1)
              {
               switch(HistoryDealGetInteger(ticket,DEAL_TYPE))
                 {
                  case DEAL_TYPE_BUY:
                     pVolume+=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                     break;
                  case DEAL_TYPE_SELL:
                     pVolume-=HistoryDealGetDouble(ticket,DEAL_VOLUME);
                     break;
                 }
              }
           }
        }
     }
   if(HistoryDealsTotal()>0)
     {
      pLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
     }
   pVolume=NormalizeDouble(pVolume,2);
   aVolume=pVolume;
   return(true);
  }
double PositionVolume::SymbolLots()
  {
   double TmpLots=0;
   for(int i=0;i<PositionsTotal();i++)
     {
      if(PositionGetSymbol(i)==pSymbol)
        {
         TmpLots=PositionGetDouble(POSITION_VOLUME);
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            TmpLots*=-1;
           }
         break;
        }
     }
   TmpLots=NormalizeDouble(TmpLots,2);
   return(TmpLots);
  }

为每一对交易品种-幻数使用此类时,必须创建一个类实例:

PositionVolume PosVol11;
PositionVolume PosVol12;
PositionVolume PosVol21;
PositionVolume PosVol22;

它应该在 EA 交易的 OnInit() 函数进行初始化,例如:

PosVol11.Init(Symbol_1,Magic_1); 
PosVol12.Init(Symbol_1,Magic_2);
PosVol21.Init(Symbol_2,Magic_1); 
PosVol22.Init(Symbol_2,Magic_2);   

之后,可以按指定的交易品种和幻数获取持仓量。让我们调用对应类实例的 GetVolume 函数。

如果成功,它返回 true,并将值赋予作为函数的参数,通过引用传递的变量:

double Vol11;
double Vol12;
double Vol21;
double Vol22;
PosVol11.GetVolume(Vol11);
PosVol12.GetVolume(Vol12);
PosVol21.GetVolume(Vol21);
PosVol22.GetVolume(Vol22);

到这里,可以说已经完成了,但还剩下控制测试。

4. 控制测试

要测试函数的工作,我们使用一个同时处理四个仓位的 EA 交易:

  1. 使用周期 14 、EURUSD、幻数 1 的 RSI 指标;
  2. 使用周期 21 、EURUSD、幻数 2 的 RSI 指标;
  3. 使用周期 14 、GBPUSD、幻数 1 的 RSI 指标;
  4. 使用周期 21 、GBPUSD、幻数 2 的 RSI 指标;

幻数为 1 的 EA 交易进行 0.1 手的交易,幻数为 2 的 EA 交易进行 0.2 手的交易。

在执行交易时,交易量被添加到 EA 交易的变量,在交易之前和交易之后,使用上述函数确定每个仓位的量。 

如果在计算持仓量时出错,则函数生成一条消息。

可以在本文的附件中找到 EA 交易的代码(文件名:ePosVolTest.mq5)。

总结

EA 交易需要很多函数,应该以在所有阶段都方便使用的方式实施这些函数。应该以最能使用计算资源的方式编写这些函数。

本文所提议的持仓量计算方法满足以下条件:在启动时,它仅加载需要的最少交易历史记录。在工作时,它使用最后几笔交易重新计算当前持仓量。