English Русский 中文 Español Deutsch Português 한국어 Français Italiano Türkçe
指定されたマジックナンバーによるトータルポジションボリューム計算のための最適化された手法

指定されたマジックナンバーによるトータルポジションボリューム計算のための最適化された手法

MetaTrader 5トレーディングシステム |
2 163 32
Dmitry Fedoseev
Dmitry Fedoseev

はじめに

MetaTrader 5のクライアント端末によりひとつのシンボルについて複数のExpert Advisorsで並行して作業を行うことが可能となりました。それはシンプルです。いくつかチャートを開き、そこにExpert Advisorsを添付するだけです。同じシンボルについて作業している別のExpert AdvisorsからそれぞれのExpert Advisorが独立して作業を行うことができればそれはすばらしいことです。(複数のExpert Advisorsが異なるシンボルについて作業する場合は何も問題はありません。)

まずそれにより、Expert Advisorは完全に検証とストラテジーテスタの最適化に従って取引を行うことができます。ポジションを開く条件はサイズまたはすでに開いているポジションがないことに依存します。複数のExpert Advisorsが同一シンボルについて作業を行えば、お互いに影響しあいます。

二番目に、またより重要なことは、Expert Advisorsが異なる資金管理システムを使えることです。Expert Advisorsに実装されているトーディング戦略に依存します。そして最後に、各Expert Advisorの結果の観測と必要に応じてスイッチを切ることのできる可能性です。


1. ポジションボリューム計算の一般的原理

注文をオープンにするとき、OrderSend()変数に渡されるMqlTradeRequestストラクチャのマジック変数の値を指定することでそれをマジックナンバーでマークすることができます。注文が実行されると、取引はまたオーダーマジックナンバーでマークされます。それ以上に履歴内取引を分析することで異なるExpert Advisorsで開かれた取引を見ることができます。

トータルポジションの計算方法はきわめて簡単です。たとえば、ボリューム0.1で買い取引を一件、もうひとつ別の買い0.1で、売り0.1実行する場合、トータルポジションのボリュームは0.1+0.1-0.1=+0.1に等しくなります。買い取引のボリュームを足し、売りボリュームを引くとトータルポジションのボリュームを得ることができます。

重要なのは、トータルポジションが0のときに計算を始めることです。そのようなことが最初にまたもっとも明白なのは、アカウントがオープンした瞬間です。言い換えれば、HistorySelect()関数を使い最初のパラメータ0(可能な最小の時間)として、TimeCurrent() (サーバーの最近知られている時間)を使った二番目のパラメータの値としてアカウントの取引履歴をすべてリクエストすることができます。

HistorySelect(0,TimeCurrent()); // load all history

それから、それぞれの取引について指定されたマジックナンバーによって買い取引のボリュームを足し、売り取引のボリュームを引最初から最後まで全履歴にあたります。それも手段のひとつでしょうが、実際には取引履歴は膨大なデータです。これはExpert Advisorの処理スピードに多大な影響を与えます。特に検証中と最適化中そのようなExpert Advisorの実用的使用が不可能になるまでです。トータルネットポジションボリュームが0になる取引履歴の最終地点を見つける必要があります。

そのために、まず全履歴をあたり、そしてネットポジション合計がゼロになる最終ポイントを見つけます。このポイント見つけ、変数(固定されたポジション時刻)として格納します。のちに、Expert Advisorが格納されたポイントから取引履歴ぜんぶにあたります。この時点を格納するよりよいソリューションは、Expert Advisorの変数の代わりにクライアント端末のグローバル変数に格納することです。理由はそのような場合、Expert Advisorを切り離すときデータが破壊されるからです。

そのような場合にはExpert Advisorを起動するとき、取引履歴全体の代わりに最小限必要な履歴をロードする必要があります。 同一シンボルについて取引を行うことのできるExpert Advisorsは数多くあるので、このグローバル変数(保存されているボリュームゼロの直近時刻)をすべてのExpert Advisorsと共有します。

メインの話題からそれて、クライアント端末でのグローバル変数の使用について考察します。それにより複数のExpert Advisorsが同一シンボル(たぶん異なるパラメータ)について作業し、別のExpert Advisorsのインスタンスにより作成された名前が偶然一致することを避けることができます。


2. クライアント端末のグローバル変数利用

MQL5言語はMQLInfoString()関数を持ち、そのことにより mql5プログラムについて異なる情報を取得することができます。

ファイル名に関する情報を得るには、MQL_PROGRAM_NAME拡張子と共にこの関数を呼びます。

MQL5InfoString(MQL_PROGRAM_NAME); // Expert Advisor name

Expert Advisor名を伴なうグローバル変数名を始めます。Expert Advisorは複数のシンボルで作業が可能です。それは、シンボル名 (Symbol)を追加する必要があるということです。Expert Advisorは同一シンボルの異なる時間枠(異なる設定)で作業が可能です。この場合、マジックナンバーを使う必要があります。よってマジックナンバーも追加します。

たとえば、Expert AdvisorがMagic_Nに保存されているマジックナンバーを持つと、 それをグローバル変数名に追加します。

すべてのグローバル変数名は以下のようなものです。

gvp=MQLInfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // name of an Expert Advisor and symbol name 
                                                                            // and its magic number

gvp (グローバル変数プレフィクス)の場所は、共通変数のセクションに宣言されいるストリング変数です。

プログラミングで使用されるので、グローバル変数の混乱を避けるため、用語についてはっきりさせたいと思います。(グローバル変数はすべての関数内でビジブルです。関数のローカル変数は関数内部においてのみビジブルです。)

ここで、異なるケースに出くわします。『グローバル変数』という用語はクライアント端末のグローバル変数(ファイルに保存された特別変数、それらはGlobalVariable...()変数によって使用可能です。)を指します。グローバル変数(プログラミングで使用されるので)について語るとき、『共通変数』という語を使います。ローカル変数と言う語はローカル変数を意味します。

グローバル変数は便利です。というのも、グローバル変数はExpert Advisorの最初期化(Expert Advisor、クライアント端末、コンピュータの再起動)、後その値を保存するからです。しかし検証モードではすべての変数(または最適化の最は前のパス)を消去する必要があります。実際の処理で使用されるグローバル変数は、検証の際作成されるグローバル変数から分離する必要があります。そしてそのグローバル変数は検証後削除する必要があります。ただし、Expert Advisorによって作成されるグローバル変数を変更したり削除することはできません。

AccountInfoInteger()関数を用い、それをACCOUNT_TRADE_MODE識別子で呼ぶことで、現在使用しているモードを区別することができます。 テスター、デモ、実アカウントなど。

グローバル変数にプレフィクスを追加します。"d" はデモアカウントで動作する場合。"r"は実アカウントで動作する場合 "t" はストラテジーテスタで動作する場合です。

gvp=MQLInfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_"; // name of an Expert Advisor, symbol name
                                                                                  // and the Expert Advisor magic number
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO))
  {
   gvp=gvp+"d_"; // demo account
  }
if(AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_REAL)
  {
   gvp=gvp+"r_"; // real
  }
if(MQL5InfoInteger(MQL_TESTER))
  {
   gvp=gvp+"t_"; // testing
  }

関数はExpert AdvisorのOnInit()から呼ばれる必要があります。

上述のように、グローバル変数は検証時削除する必要があります。または、Expert AdvisorのOnDeinit()関数内にグローバル変数を削除する関数を追加する必要がある、とも言えます。

void fDeleteGV()
  {
   if(MQL5InfoInteger(MQL_TESTER)) // Testing mode
     {
      for(int i=GlobalVariablesTotal()-1;i>=0;i--) // Check all global variables (from the end, not from the begin)
        {
         if(StringFind(GlobalVariableName(i),gvp,0)==0) // search for the specified prefix
           {
            GlobalVariableDel(GlobalVariableName(i)); // Delete variable
           }
        }
     }
  }

これでMetaTrader 5での検証を妨げることが可能です。言い換えれば、OnDeinit()関数の実行は確約されたものではないということです。が、本件についてはのちに触れることにします。Strategy Testerがインタラプトされた後OnDeinit()関数が実行されるか否かは定かではありません。よって、Expert Advisorが実行される最初の段階でOnInit()内部にてグローバル変数を削除します。

OnInit()関数とOnDeinit()関数の下記コードを取得します。

int OnInit()
  {
   fCreateGVP(); // Creating a prefix for the names of global variables of the client terminal
   fDeleteGV();  // Delete global variables when working in Tester
   return(0);
  }

void OnDeinit(const int reason)
  {
   fDeleteGV();  // Delete global variables when working in tester
  }

また、グローバル変数作成のため短縮名のついた関数を作成する(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);
  }

ここまでグローバル変数について述べてきましたが、これがすべてではありません。

シンボルについてグローバル変数を作成する可能性を提供したり、アカウントやストラテジーテスタで異なる処理を行ったりする必要があります。こういったグローバル変数名はExpert Advisorの名前やマジックナンバーには依存しません。

グローバル変数プレフィクスに別の変数を宣言し、それを "Commom_gvp"と名付けます。アカウントで動作しますが、それは値"COMMON"を有し、ストラテジーテスタ(ストラテジーバックテスト処理の前後で変数を削除するため)で動作するときは変数gvpと同じ値を有します。

最後に、グローバル変数プレフィクスを準備する関数は以下です。

void fCreateGVP()
  {
   gvp=MQL5InfoString(MQL_PROGRAM_NAME)+"_"+_Symbol+"_"+IntegerToString(Magic_N)+"_";
   Commom_gvp="COMMOM_"; // Prefix for common variables for all Expert Advisors
   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(MQLInfoInteger(MQL_TESTER))
     {
      gvp=gvp+"t_";
      Commom_gvp=gvp; // To be used in tester, the variables with such a prefix 
                      // will be deleted after the testing
     }
  }

グローバル変数プレフィクスは余分な情報を含んでいる、と思う方がいらっしゃるでしょう。それというのは、デモアカウントと実アカウントの分離情報です。プレフィク "t" は検証時"t" charを追加するだけで行うことが可能ですが、それはExpert Advisorがストラテジーテスタで動作していることを示します。私はそれをこんな風にやっちゃいました。これからの将来そしてExpert Advisorsの動作分析に要求される事柄については判りません。

Store is no sore they say.

上述の関数ではクライアント端末は一つのアカウントで動作し、動作中アカウントの変更はないということを意味しています。Expert Advisor動作中のアカウント変更は禁止されています。もちろん、必要とあればこの問題はグローバル変数名にアカウント番号を追加することで解決可能です。

もうひとつ重要な注意点!すべてのグローバル変数名の長さは63 シンボルに制限されています。よってみなさんのExpert Advisorsに長い名前をつけるのはやめましょう。

グローバル変数については以上です。いよいよ本稿の中心的話題を考察する時間です。指定されたマジックナンバーの使用によるポジションボリュームの計算です。


3. ポジションボリュームの計算

まず、GlobalVariableCheck()関数を使って、最後ががゼロのポジションボリューム情報のグローバル関数があるか確認します。(わかりやすくするために、オープンしているポジションがない場合、それを『ゼロポジション』ケースと呼ぶことにします。)

そのような変数がある場合には、時刻から始めて、変数に保存された取引履歴をロードします。なければ、履歴全体をロードします。

if(GlobalVariableCheck(Commom_gvp+sSymbol+"_HistStTm")) // Saved time of a "zero" total position
  {
   pLoadHistoryFrom=(datetime)GlobalVariableGet(Commom_gvp+pSymbol+"_HistStTm"); // initial date setting 
                                                                             // select only the history needed
  }
else
 {
   GlobalVariableSet(Commom_gvp+sSymbol+"_HistStTm",0);
 }
if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // Load the necessary part of the deal history
  { 
   return(false);
  }

次に、あるシンボルについてネットポジションの合計ボリュームを定義します。

double CurrentVolume=fSymbolLots(pSymbol);

ポジションボリュームはfSymbolLots()関数を使って決定されます。

ポジションボリュームを得る方法は複数あります。PositionSelect()関数を使っても可能です。関数がfalseを返したら、それはポジションがない(そのボリューム=0)を意味します。関数がtrueを返したら、ボリュームは POSITION_VOLUME識別子を伴うPositionGetDouble() 関数を使って取得可能です。ポジションタイプはPOSITION_TYPE識別子を伴うPositionGetInteger() 関数を使って取得可能です。関数はロングポジションに対して正の値を、ショートポジションに対しては負の値を返します。

関数の完成型は以下です。

double fSymbolLots(string aSymbol)
  {
   if(PositionSelect(aSymbol,1000)) // the position has been selected successfully, so it exists
     {
      switch(PositionGetInteger(POSITION_TYPE)) // It returns the positive or negative value dependent on the direction
        {
         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()関数を用いて必要なシンボルを見つけ、ポジションボリュームと方向を判断します。( POSITION_VOLUME識別子を伴うPositionGetDouble() 関数およびPOSITION_TYPE識別子を伴PositionGetInteger() 関数)

こんかいの場合は、すぐに使える関数は以下のようなものです。

double fSymbolLots(string aSymbol)
  {
   double TmpLots=0;
   for(int i=0;i<PositionsTotal();i++) // Go through all positions
     {
      if(PositionGetSymbol(i)==aSymbol) // we have found a position with specified symbol
        {
         TmpLots=PositionGetDouble(POSITION_VOLUME);
         if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_SELL)
           {
            TmpLots*=-1; // the sign is dependent on the position type           }
         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--) // go through all the deals from the end to the beginning 
  {
   ulong ticket=HistoryDealGetTicket(i); // Get ticket of the deal
   if(ticket!=0)
     {
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // We add or subtract the volume depending on deal direction
        {
         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) // all the deals has scanned
        {
         sLoadHistoryFrom=HistoryDealGetInteger(ticket,DEAL_TIME); // Save the time of a "zero" position
         GlobalVariableSet(Commom_gvp+aSymbol+"_HistStTm",sLoadHistoryFrom);
         FromI=i; // Save the index
         break;
        }
     }
  }

このポイントを見つけたとき、時刻をグローバル変数に保存します。それはのちに取引履歴をロードする際に使用することになります。(履歴の取引インデックスはFromI変数に保存されます。)

インデックスFromIを伴う取引の前にそのシンボルについてのトータルポジションがゼロになります。

これでFromIから履歴の最後に向かい、指定のマジック版バーによって取引ボリュームをカウントします。

static double sVolume=0;
static ulong sLastTicket=0;
for(int i=FromI;i<HistoryDealsTotal();i++) // from the first deal until the end
  {
   ulong ticket=HistoryDealGetTicket(i);   // Get deal ticket
   if(ticket!=0)
     {
      if(HistoryDealGetString(ticket,DEAL_SYMBOL)==aSymbol) // Specified symbol
        {
         long PosMagic=HistoryDealGetInteger(ticket,DEAL_MAGIC);
         if(PosMagic==aMagic || aMagic==-1) // Specified magic
           {
            switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // add or subtract the deal volumes 
                                                       // depending on the 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変数に保存されます。取引実行後、指定のマジックナンバーを伴うポジションのトータルボリュームは Volumeと等しくなります。関数の予備作業は終了です。

変数sLoadHistoryFrom、sLastTicket 、Volumeは静的変数(値は関数実行完了後保存します。)この値はのちに関数呼びだしのたびに使われます。として宣言されます。

時刻情報(取引履歴の開始点)、取引チケットは入手しています。実行後、トータルポジションボリューム(指定されたシンボルの)は現在値となります。

ボリュームポジションがゼロの時刻のため、現在時刻から保存された時刻へ履歴をたどるだけで十分です。そして取引ボリュームの合計計算をおこない、ボリュームと最終取引のチケットを保存します。

これでExpert Advisorのトータルポジションボリュームの計算は最後数件の処理をしています。

if(!HistorySelect(sLoadHistoryFrom,TimeCurrent())) // Request for the deals history up to the current time
  {
   return(false);
  }
for(int i=HistoryDealsTotal()-1;i>=0;i--) // Loop from the end
  {
   ulong ticket=HistoryDealGetTicket(i); // Get ticke
   if(ticket!=0)
     {
      if(ticket==sLastTicket) // We have found the already calculated deal, save the ticket and break
        {
         sLastTicket=HistoryDealGetTicket(HistoryDealsTotal()-1);
         break;
        }
      switch(HistoryDealGetInteger(ticket,DEAL_TYPE)) // Add or subtract deal volume depending on 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;
   // First execution of function when Expert Advisor has started
   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 if unsuccessful, 
                                                      // we will repeat on the next tick
        {
         return(false);
        }
      double CurrentVolume=fSymbolLots(aSymbol); // Total volume
      double Sum=0;
      int FromI=0;
      int FromTicket=0;
      // Search the last time when position volume was equal to zero
      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;
              }
           }
        }
      // Calculate the volume of position with specified magic number and symbol
      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;
     }

   // Recalculate the volume of a position (with specified symbol and magic)
   // for the deals, after the zero position time
   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では、この問題は別名でこの関数のコピーを作ることで解決できました。そして別のペアに"symbol-magic"を呼ぶか、まあは共通変数からそれぞれのペア"symbol-magic" に変数 FirstStart、sVolume、sLastTicket、 sLoadHistoryを宣言し、関数に渡します。

MQL5に同じ方法でそれを実装するのも可能です。しかし、MQL5にはもっと便利な機能があります。クラスです。それはクラスを使用するのに適したケースです。クラスを使用するときは、シンボルとマジックナンバーのペアにクラスインスタンスを作成する必要があります。データはそれぞれクラスインスタンスに保存されるのです。

ここでPositionVolumeクラスを宣言します。関数内で静的に宣言されたすべての変数は、プライベートとして宣言されます。よってVolume変数以外はExpert Advisorから直接それらを使うことはできません。それが必要なのは、ボリューム計算関数の実行後のみです。また、Symbol変数およびMagic変数も宣言します。それらを関数に渡すのは実用的ではありません。クラスインスタンスを初期化するとき一度だけおこないます。

クラスは2つのパブリック関数を持ちます。初期化関数とポジションボリュームを計算する関数、そしてポジションのトータルボリュームを決定するプライベート関数です。

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;

それはExpert AdvisorOnInit()関数で初期化する必要があります。以下がその例です。

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. コントロールテスト

関数の動作を検証するために4つのポジションに同時に動作するExpert Advisorを使います。

  1. RSIインディケータを期間14でシンボル EURUSDについてマジックナンバー1を使って
  2. RSIインディケータを期間21でシンボル EURUSDについてマジックナンバー2を使って
  3. RSIインディケータを期間14でシンボル GBPUSDについてマジックナンバー1を使って
  4. RSIインディケータを期間21でシンボル GBPUSDについてマジックナンバー2を使って

マジックナンバー1を使ったExpert Advisorはボリューム0.1ロットの取引をし、 マジックナンバー2を使ったExpert Advisorはボリューム0.2ロットの取引をしました。

取引が実行されると、取引ボリュームがTExpert Advisorの変数に追加されます。取引の前後は各ポジションボリュームは上述の関数を使って判断されます。

ボリューム計算にエラーがあると、関数はメッセージを生成します。

Expert Advisorのコードは本稿の添付で参照できます。(ファイル名: ePosVolTest.mq5)


おわりに

Expert Advisorには多くの関数が必要です。そしてそれらは全段階で使いやすい方法で実装される必要があります。これら関数は計算リソースを最大限に活用するように書かれる必要があります。

本稿で提案したポジションボリュームの計算手法は次の条件を満たすものです。導入されるとき、要求される取引履歴の最小情報のみロードします。作業時は、直近の取引情報を使用して現在のポジションボリュームを計算します。

MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/125

添付されたファイル |
eposvoltest.mq5 (17.98 KB)
最後のコメント | ディスカッションに移動 (32)
SergeyNU
SergeyNU | 31 7月 2019 において 14:58

こんにちは。

これらのクラスとOOPがどのように機能するのか理解するのを手伝ってください。このクラスをExpert Advisorに接続したとすると、Expert Advisorからアクセスしたときだけ実行されるのでしょうか?それとも並行して動作し、要求があった時だけ結果を出すのでしょうか?

Payman
Payman | 5 7月 2020 において 07:47
ファイルはコンパイルできない。
jelagins
jelagins | 10 9月 2025 において 13:13

mql5 のコンパイル中に、次の警告とエラーが発生しました: 'long' から 'int' への型変換によるデータ損失の可能性 eposvoltest.mq5 426 20 、 long' から 'datetime' への型変換によるデータ損失の 可能性 eposvoltest.mq5 439 32、long' から 'int' への型変換によるデータ損失の可能性 eposvoltest.mq5 456 26、long' から 'int' への型変換によるデータ損失の可能性 eposvoltest.mq5 491 23、OrderSend' の戻り値をチェックすべきである。mq5 236 4、OrderSend'の戻り値をチェックすべきである。mq5 268 4、'-' - 式が真偽値ではない eposvoltest.mq5 279 14、'MQL5_TESTING'は非推奨です。代わりに'MQL_TESTER'を使用してください。mq5 335 23、MQL5_TESTING' は非推奨です。eposvoltest の代わりに 'MQL_TESTER' を使用してください。mq5 346 23

10.09.2025

ceejay1962
ceejay1962 | 10 9月 2025 において 16:49
jelagins データ損失の可能性 eposvoltest.mq5 426 20、 'long' から 'datetime' への型変換によるデータ損失の 可能性 eposvoltest.mq5 439 32、'long' から 'int' への型変換によるデータ損失の可能性 eposvoltest.mq5 456 26、'long' から 'int' への型変換によるデータ損失の可能性 eposvoltest.mq5 491 23, 'OrderSend' の戻り値をチェックすべきである eposvoltest.mq5 236 4, 'OrderSend' の戻り値をチェックすべきである eposvoltest.mq5 268 4, '-' - 式が boolean ではない eposvoltest.eposvoltest.mq5 279 14, 'MQL5_TESTING' は非推奨です、代わりに 'MQL_TESTER' を使用してください eposvoltest.mq5 335 23, 'MQL5_TESTING' は非推奨です、代わりに 'MQL_TESTER' を使用してください eposvoltest.mq5 346 23

10.09.2025

このコードが2010年のものであることを考えれば、驚くにはあたらない!

fxsaber
fxsaber | 10 9月 2025 において 17:05
コードに誤りがあった。
bool fOpSell(string aSymbol,double aVolume=0.1,int aSlippage=0,int aMagic=0,string aComment="",string aMessage="",bool aSound=false)
  {
   request.symbol=aSymbol;
   request.action=TRADE_ACTION_DEAL;
   request.type=ORDER_TYPE_SELL;
   request.volume=aVolume;
   request.price=SymbolInfoDouble(aSymbol,SYMBOL_BID);
   request.sl=0;
   request.tp=0;
   request.deviation=aSlippage;
   request.type_filling=ORDER_FILLING_FOK;
   request.comment=aComment;
   request.magic=aMagic;
   if(aMessage!="")Print(aMessage);
   if(aSound)PlaySound("expert");
   OrderSend(request,result);
   if(result.retcode==TRADE_RETCODE_DONE)
     {
      Print("...ラッキー(#"+IntegerToString(result.order)+")");
      if(aSound)PlaySound("ok");
      return(1);
     }
   else
     {
      Print("...ミス"+IntegerToString(result.retcode)+" - "+fTradeRetCode(result.retcode));
      if(aSound)PlaySound("timeout");
      return(-1);
     }
  }

修正版は予告編にあります。

プロフィット引き出しモデル構築のためのTesterWithdrawal() 関数の使用 プロフィット引き出しモデル構築のためのTesterWithdrawal() 関数の使用
本稿は処理中に資産の特定部分の引き出しをするトレードシステムにおけるリスク見積をするためのTesterWithDrawal()関数使用について述べていきます。また、ストラテジーテスタにおける資産の引き出し計算のアルゴリズムへのこの関数の影響についても述べます。この関数はExpert Advisorsのパラメータ最適化に有用です。
MQL5オブジェクト指向のプログラミングアプローチを使ったExpert Advisorのプログラミング MQL5オブジェクト指向のプログラミングアプローチを使ったExpert Advisorのプログラミング
本稿では初心者のためのAdvisor in MQL5でプログラミングをする段階的ガイドで行ったこと、すなわちExpert Advisor作成に対してオブジェクト指向のアプローチに注目します。ほとんどの方はこれは難しいと思われるでしょう。しかし本稿を読み終わるまでにみなさんはブジェクト指向でExpert Advisorを書けるようになっている、という点をはっきり述べておきたいと思います。
Expert AdvisorプログラミングにおけるMQL5標準トレードクラスライブラリの使用 Expert AdvisorプログラミングにおけるMQL5標準トレードクラスライブラリの使用
本稿は、Expert Advisorプログラミングにおいてポジションのクローズ、変更、指値注文出し、取引セット前のマージン削除と検証を実装する MQL5標準トレードクラスライブラリの使用方法について述べていきます。注文と取引詳細情報の取得に使用できるトレードクラスの使い方もお見せします。
名前つきパイプを使用したMetaTrader 5端末間コミュニケーションにDLLを使用しないソリューション 名前つきパイプを使用したMetaTrader 5端末間コミュニケーションにDLLを使用しないソリューション
本稿は名前つきパイプを使用したMetaTrader 5端末同士のプロセス間コミュニケーションの実装方法について述べていきます。名前付きパイプを使用するにはCNamedPipesクラスが作成されます。 その使用検証と接続計測のために、ティックインディケータ、サーバー、クライアントスクリプトが提供されます。リアルタイムのクオートには名前付きパイプの使用で十分です。