データアクセスの整理

このセクションでは、価格データ(時系列)の取得、格納、及びリクエストに関する質問が考慮されます。

取引サーバからのデータ受信

価格データは、MetaTrader 5 端末で使用可能になる前に受信されて処理されなければなりません。データを受信するには MetaTrader 5 取引サーバへの接続が必要です。データは、端末のリクエストに応じてサーバから分足のパックブロックの形式で受信されます。

サーバのデータ参照メカニズムは、リクエストがチャートをナビゲートしているユーザによって送られたか MQL5 言語のプログラムで送られたかなどの方法には依存しません。

中間データの格納

サーバーから受信されたデータは自動的に解凍されてHCC 中間形式で格納されます。各シンボルのデータは terminal_directory\bases\server_name\history\symbol_name の別々のフォルダに書き入れられます。例えば MetaQuotes-Demo サーバから受信された EURUSD データは terminal_directory\bases\MetaQuotes-Demo\history\EURUSD\ に格納されます。

データは .hcc 拡張子を持つファイルに書き込まれます。各ファイルは一年分の分足を格納します。例えば EURUSD フォルダの 2009.hcc ファイルは 2009 年の EURUSD 分足を含みます。これらのファイルは全ての時間軸の価格データを準備するために使用され、直接アクセスは意図されていません。

中間データからの必要な時間軸のデータの取得

中間 HCC ファイルは、リクエストされた時間軸の価格データをHC 形式で構築するためのデータ元として使用されます。HC 形式のデータは最大限に迅速なアクセスのために用意された時系列です。これらは MQL5 プログラムチャートのリクエストに応じて作成されます。データ量は「チャートでのバーの最大数」パラメータの値を超えてはいけません。データは更に使用されるために hc 拡張子を持つファイルに格納されます。

システムリソース節約のために、時間軸のデータは必要時のみに RAM に格納されて保存されます。データは長時間呼び出されない場合には RAM から解放されてファイルに保存されます。データは、他の時間軸かの準備ができたデータがあるかどうかにかかわらず各時間軸で用意されます。データ形成及びアクセスのルールは全ての時間軸で同じです。HCC に分足 (M1)データが格納されていても、同じボリュームの M1 軸のデータの HC としての可用性があるわけではありません。

サーバからの新規データの受信によって、全ての時間軸で使用された価格データの HC 形式での自動更新が呼び出されます。また、それらのデータを計算の入力データとして暗黙的に使用する指標全ての再計算につながります。

「チャートのバーの最大数」パラメータ

「チャートのバーの最大数」パラメータはチャート、指標、及び MQL5 プログラムで使用可能な HC 形式のバーの数を制限します。これは全ての利用可能な時間軸のために有効であり、コンピュータリソースを節約するために提供されています。

このパラメータに大きな値を設定する場合は、小さな時間軸で多くの履歴の価格データが利用可能であると、時系列と指標バッファ格納に使用されるメモリは数百メガバイトになってクライアント端末プログラムのRAMの制限(MS Windows の32 ビットアプリケーション).に達することがあるので注意が必要です。

「チャートのバーの最大数」の変更はクライアント端末の再起動の際に有効になります。このパラメータの変更は、サーバでの追加データの自動や時系列の追加のバーの形成にはつながりません。チャートがデータのない領域にスクロールするかデータが MQL5 プログラムによってリクエストされた場合は、追加の価格データがサーバからリクエストされ、時系列は新しい制限をもって更新されます。

サーバから要求されたデータの量は「チャートのバーの最大数」パラメータを考慮した上のこの時間軸のバーの必要な数に相当します。このパラメータで設定された制限は厳密ではなく、時間軸で利用可能なバーの数が現在のパラメータ値を少し上回る場合があります。

データの可用性

HCC 形式でのデータの可用性または HC 形式での使用が準備中のデータの可用性は、これらのデータの絶対的な可用性を意味するものではありません。

MQL5 プログラムから価格データや指標値にアクセスする場合、それらの特定の瞬間、また特定の開始時間での可用性が保証されていないことを忘れてはなりません。これは、リソース節約の目的で、MQL5 プログラムに必要なデータの完全なコピーは MetaTrader 5 に格納されず、端末データベースへの直接アクセスのみが与えられているという事実と関係しています。

全ての時間軸の価格履歴が HCC 形式の共通のデータから構築され、サーバからのデータの更新は、全ての時間軸データの更新及び指標の再計算ににつながります。そのために、データは少し前に利用可能であったとしても利用不可能になることがあります。

端末データとサーバデータの同期化 #

MQL5 プログラムは任意のシンボルと時間軸のデータを呼び出すことが出来るので、必要な時系列のデータが端末にまだ形成されていないまたは必要な価格データが取引サーバと同期されていない可能性があります。この場合、待ち時間を予測するのは難しいです。

「無演算」ループを使用したアルゴリズムは最善の解決策ではありません。スクリプトは、イベント処理を持たないため代替アルゴリズムの選択肢がないので、唯一の例外です。カスタム指標では、このようなアルゴリズムや他の「無演算」ループは、全ての指標の計算と他のシンボルの価格データの処理の終了につながるため、推奨出来ません。

エキスパートアドバイザーと指標ではハンドルのイベントモデルが使用されるべきです。OnTick() または OnCalculate() イベントの取り扱い中に必要な時系列のデータの受信に失敗した場合、ハンドラの次の呼び出しの間にアクセスの可用性に依存してイベントハンドラを終了する必要があります。

履歴追加スクリプトの例

選択されたシンボルの履歴を取引サーバから受信するためのリクエストを実行するスクリプトの例を考えてみましょう。このスクリプトは、選択したシンボルのチャートで実行するためのものです。前述したように、価格データは取引サーバからパック化された 1分間のデータとして受信され、事前定義された時系列はそれから構築されるので、時間軸は重要ではありません。

データ受信に関する全てのアクションは別の関数 CheckLoadHistory(symbol, timeframe, start_date) として書きます。

int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
 {
 }

CheckLoadHistory() 関数は、任意のプログラム(エキスパートアドバイザー、スクリプトまたは指標)から呼び出すことが出来る普遍的な機能として設計されており、銘柄名、期間、必要な価格履歴の始まりを示す開始日の3つの入力パラメータを必要とします。

不足している履歴をリクエストする前に、関数コードに必要なチェックを挿入します。まず第一に、銘柄名と期間値が正しいことの確認が必要です。

  if(symbol==NULL || symbol=="") symbol=Symbol();
  if(period==PERIOD_CURRENT)     period=Period();

取引サーバにリクエストを送信する時にシンボルの履歴が利用出来る用、シンボルが 「気配値表示」 ウィンドウで利用可能であることを確認します。シンボルが 「気配値表示」 にない場合 SymbolSelect() 関数で追加します。

  if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
    {f
    if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOL) return(-1);
    SymbolSelect(symbol,true);
    }

示された銘柄/期間のペアで利用可能な履歴の開始日を受信する必要があります。CheckLoadHistory() に渡された startdate 入力パラメータの値が利用可能な履歴の範囲内にあるなら、その後取引サーバリクエストは必要ありません。現時点での銘柄/期間のペアの最初の日を取得するには、SeriesInfoInteger() 関数と SERIES_FIRSTDATE 修飾子が使用されます。

  SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
  if(first_date>0 && first_date<=start_date) return(1);

次の重要なチェックは、関数が呼び出されるプログラムの種類のチェックです。指標と同じ期間を持つ時系列の更新リクエストを送信することは望ましくありません。指標と同じシンボル期間のデータのリクエストが望ましくないのは、履歴データの更新が指標が作動するのと同じスレッドで実行されるという事実があるからです。従ってデッドロックが発生する可能性が高いです。これをチェックするには MQL5InfoInteger() 関数を MQL5_PROGRAM_TYPE 修飾子と使用します。

  if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
    return(-4);

全てのチェックが成功した場合、もう1 回取引サーバにリクエストを送らないですむか見てみます。初めに HCC 形式でのデータが使用可能な初めの日を見つけます。この値を SeriesInfoInteger() 関数と SERIES_TERMINAL_FIRSTDATE 修飾子でリクエストし start_date パラメータ値と比べます。

  if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
    {
    //--- 時系列を構築するために読み込まれたデータ
    if(first_date>0)
       {
        //--- 時系列の構築を強制する
        CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
        //--- 日にちをチェックする
        if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
          if(first_date>0 && first_date<=start_date) return(2);
       }
    }

全てのチェック後に実行スレッドが CheckLoadHistory() 関数の本体に残っている場合、取引サーバから欠落している価格データをリクエストする必要があります。初めに TerminalInfoInteger() 関数を使用して「チャートでのバーの最大数」値を返します。

int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);

これは、余分なデータのリクエストを防止するのに必要です。次に、取引サーバでの(期間に関係ない)シンボル履歴の初日を SeriesInfoInteger() 関数とSERIES_SERVER_FIRSTDATE 修飾子を使用して見つけます。

  datetime first_server_date=0;
  while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
    Sleep(5);

リクエストは非同期動作なので、この関数は first_server_date 変数が値を受け取るまでの遅延の5ミリ秒の小さい枠で呼び出されます。サイクルの実行がユーザによって終了された場合 IsStopped()true を返します。取引サーバからの価格データリクエストを開始する日付けの正しい値を示してみましょう。

  if(first_server_date>start_date) start_date=first_server_date;
  if(first_date>0 && first_date<first_server_date)
    Print("Warning: first server date ",first_server_date," for ",
symbol," does not match to first series date ",first_date);

サーバの開始日である first_server_date が HCC 形式のシンボルの開始日である first_date より早い場合、対応するエントリーが操作ログに出力されます。

欠落した価格データを取引サーバからリクエストする準備ができました。ループ形式でリクエストを作成し本体に記入を開始します。

  while(!IsStopped())
    {
    //1. HHC として再構築された時系列と中間の履歴との間の同期を待ちます。
    //2. この時間軸での現在のバーの数を受け取ります。
    //    バーの数が Max_bars_in_chart より多い場合、終了します。
    //3. 再構築された時系列で first_date 開始日を取得して start_date と比較します。
    //    first_date が start_date, より早い場合、作業が終わったので終了します。
    //4. サーバから新しい期間の履歴をリクエストします。(「 bars」で番号図けられた最後の使用可能のバーから100 足
    }

初めの 3 点は、すでに知られている手段によって実施されます。

  while(!IsStopped())
    {
    //--- 1.時系列の最構築が完成するまで待つ
    while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
        Sleep(5);
    //--- 2.存在するバーの数をリクエストする
    int bars=Bars(symbol,period);
    if(bars>0)
       {
        //--- チャートに描画出来る以上のバーがあるので終了する
        if(bars>=max_bars) return(-2);
        //--- 3. 時間軸での現在の開始日を返す
        if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
          // 開始日がリクエストされたものより早いので、作業完了
          if(first_date>0 && first_date<=start_date) return(0);
       }
    //4. サーバから新しい期間の履歴をリクエストします。(「 bars」で番号図けられた最後の使用可能のバーから100 足)。
    }

最後の4 番目のポイントが残っています。履歴のリクエストです。サーバに直接参照することは出来ません。が、HCC 形式の履歴が充分でない場合 コピー関数 は自動的にサーバリクエストの送信を開始します。first_date 変数で表される初日の時刻がリクエストの実行度を評価するためのシンプルで自然な基準なので CopyTime() 関数の使用が一番容易です。

時系列からデータを複製する関数を呼び出す際には、start パラメータ(価格データコピーの開始点からのバーの数)は端末で利用可能な履歴の範囲からとられなければなりません。例えば、100 足しかない時に、インデックス 500 から始まって 300 足を複製する試みは無意味です。このような要求は誤ったものとして理解されて処理されず、追加の履歴は取引サーバから読み込まれません。

これが bars インデックスから始めて 100 足を複製する理由です。これは、欠落している履歴の取引サーバからのスムーズな読み込みを提供します。サーバは余分の履歴を送るので、実際にはリクエストされた 100 より少し大きい数の足が読み込まれます。

  int copied=CopyTime(symbol,period,bars,100,times);

コピー操作の後では、複製された要素の数が分析されるべきです。試みが失敗した場合 copied の値は null に等しくfail_cnt カウンタの値は 1 で増えます。100 の試行失敗の後、関数の動作が停止されます。

int fail_cnt=0;
...
  int copied=CopyTime(symbol,period,bars,100,times);
  if(copied>0)
    {
    //--- データをチェックする
    if(times[0]<=start_date) return(0); // 複製された値のほうが小さい。準備完了
    if(bars+copied>=max_bars) return(-2); // バーの数がチャートに描画出来るより多い。準備完了
     fail_cnt=0;
    }
  else
    {
    //--- 100以上の失敗の試みが連続していない
     fail_cnt++;
    if(fail_cnt>=100) return(-5);
    Sleep(10);
    }
 

従って、各瞬間における現在の状況の正しい処理が関数内で実装されているだけではなく、CheckLoadHistory() 関数の呼び出し後に処理出来る追加情報を取得するための終了コードも戻されます。例えばこのようにです。

  int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
  switch(res)
    {
    case -1 : Print("Unknown symbol ",InpLoadedSymbol);                     break;
    case -2 : Print("More requested bars than can be drawn in the chart"); break;
    case -3 : Print("Execution stopped by user");                         break;
    case -4 : Print("Indicator mustn't load its own data");               break;
    case -5 : Print("Loading failed");                                     break;
    case  0 : Print("All data loaded");                                   break;
    case  1 : Print("Already available data in timeseries are enough");   break;
    case  2 : Print("Timeseries is built from available terminal data");   break;
    default : Print("Execution result undefined");
    }

この関数の完全なコードは、リクエスト結果の取り扱いを持つ任意のデータへのアクセスの正しい構成を示すスクリプトの例に記載されています。

コード:

//+------------------------------------------------------------------+
//|                                              TestLoadHistory.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                             https://www.MQL5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link     "https://www.mql5.com"
#property version   "1.02"
#property script_show_inputs
//--- 入力パラメータ
input string          InpLoadedSymbol="NZDUSD";   // 読み込まれるシンボル
input ENUM_TIMEFRAMES InpLoadedPeriod=PERIOD_H1; // Period to be loaded
input datetime        InpStartDate=D'2006.01.01'; // 開始日
//+------------------------------------------------------------------+
//| スクリプトプログラムを開始する関数                                          |
//+------------------------------------------------------------------+
void OnStart()
 {
  Print("Start load",InpLoadedSymbol+","+GetPeriodName(InpLoadedPeriod),"from",InpStartDate);
//---
  int res=CheckLoadHistory(InpLoadedSymbol,InpLoadedPeriod,InpStartDate);
  switch(res)
    {
    case -1 : Print("Unknown symbol ",InpLoadedSymbol);             break;
    case -2 : Print("Requested bars more than max bars in chart"); break;
    case -3 : Print("Program was stopped");                       break;
    case -4 : Print("Indicator shouldn't load its own data");     break;
    case -5 : Print("Load failed");                               break;
    case  0 : Print("Loaded OK");                                 break;
    case  1 : Print("Loaded previously");                         break;
    case  2 : Print("Loaded previously and built");               break;
    default : Print("Unknown result");
    }
//---
  datetime first_date;
  SeriesInfoInteger(InpLoadedSymbol,InpLoadedPeriod,SERIES_FIRSTDATE,first_date);
  int bars=Bars(InpLoadedSymbol,InpLoadedPeriod);
  Print("First date ",first_date," - ",bars," bars");
//---
 }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
int CheckLoadHistory(string symbol,ENUM_TIMEFRAMES period,datetime start_date)
 {
  datetime first_date=0;
  datetime times[100];
//--- 銘柄と期間をチェックする
  if(symbol==NULL || symbol=="") symbol=Symbol();
  if(period==PERIOD_CURRENT)     period=Period();
//--- シンボルが「気配値表示」で選ばれているかをチェックする
  if(!SymbolInfoInteger(symbol,SYMBOL_SELECT))
    {
    if(GetLastError()==ERR_MARKET_UNKNOWN_SYMBOL) return(-1);
    SymbolSelect(symbol,true);
    }
//--- データがあるかをチェックする
  SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date);
  if(first_date>0 && first_date<=start_date) return(1);
//--- 指標の場合自身のデータの読み込みはリクエストしない
  if(MQL5InfoInteger(MQL5_PROGRAM_TYPE)==PROGRAM_INDICATOR && Period()==period && Symbol()==symbol)
    return(-4);
//--- 2 回目の試み
  if(SeriesInfoInteger(symbol,PERIOD_M1,SERIES_TERMINAL_FIRSTDATE,first_date))
    {
    //--- 時系列を構築するために読み込まれたデータ
    if(first_date>0)
       {
        //--- 時系列の構築を強制する
        CopyTime(symbol,period,first_date+PeriodSeconds(period),1,times);
        //--- 日にちをチェックする
        if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
          if(first_date>0 && first_date<=start_date) return(2);
       }
    }
//--- 端末オプションでのチャートのバーの最大数
  int max_bars=TerminalInfoInteger(TERMINAL_MAXBARS);
//--- シンボル履歴情報を読み込む
  datetime first_server_date=0;
  while(!SeriesInfoInteger(symbol,PERIOD_M1,SERIES_SERVER_FIRSTDATE,first_server_date) && !IsStopped())
    Sleep(5);
//--- 読み込み開始日を治す
  if(first_server_date>start_date) start_date=first_server_date;
  if(first_date>0 && first_date<first_server_date)
    Print("Warning: first server date ",first_server_date," for ",symbol,
           " does not match to first series date ",first_date);
//--- ステップごとにデータを読み込む
  int fail_cnt=0;
  while(!IsStopped())
    {
    //--- 時系列の構築を待つ
    while(!SeriesInfoInteger(symbol,period,SERIES_SYNCHRONIZED) && !IsStopped())
        Sleep(5);
    //--- 構築されたバーをリクエストする
    int bars=Bars(symbol,period);
    if(bars>0)
       {
        if(bars>=max_bars) return(-2);
        //--- 初日をリクエストする
        if(SeriesInfoInteger(symbol,period,SERIES_FIRSTDATE,first_date))
          if(first_date>0 && first_date<=start_date) return(0);
       }
    //--- 次の部分のコピーはデータ読み込みを強制する
    int copied=CopyTime(symbol,period,bars,100,times);
    if(copied>0)
       {
        //--- データをチェックする
        if(times[0]<=start_date) return(0);
        if(bars+copied>=max_bars) return(-2);
        fail_cnt=0;
       }
    else
       {
        //--- 100 以下の失敗の試み
        fail_cnt++;
        if(fail_cnt>=100) return(-5);
        Sleep(10);
       }
    }
//--- 停止した
  return(-3);
 }
//+------------------------------------------------------------------+
//| 期間の文字列値を返す                                                 |
//+------------------------------------------------------------------+
string GetPeriodName(ENUM_TIMEFRAMES period)
 {
  if(period==PERIOD_CURRENT) period=Period();
//---
  switch(period)
    {
    case PERIOD_M1: return("M1");
    case PERIOD_M2: return("M2");
    case PERIOD_M3: return("M3");
    case PERIOD_M4: return("M4");
    case PERIOD_M5: return("M5");
    case PERIOD_M6: return("M6");
    case PERIOD_M10: return("M10");
    case PERIOD_M12: return("M12");
    case PERIOD_M15: return("M15");
    case PERIOD_M20: return("M20");
    case PERIOD_M30: return("M30");
    case PERIOD_H1: return("H1");
    case PERIOD_H2: return("H2");
    case PERIOD_H3: return("H3");
    case PERIOD_H4: return("H4");
    case PERIOD_H6: return("H6");
    case PERIOD_H8: return("H8");
    case PERIOD_H12: return("H12");
    case PERIOD_D1: return("Daily");
    case PERIOD_W1: return("Weekly");
    case PERIOD_MN1: return("Monthly");
    }
//---
  return("unknown period");
 }