バックテストを行うにはヒストリカルデータが必須。でも公開されているものは少ない。そこでMT4からCSVで吐き出してみることに

よっしゃー!FXを始めるためにまずは過去のデータで検証だ!

ちたまん

データって無料で手に入るの?

ツン子

それが日足ならともかく5分足とか、リアルタイムのティックデータは中々手に入らないね。

ちたまん

そーなんだ。大変だね

ツン子

クッソどうでもよさそうだね・・・

ちたまん

今回やりたいこと

  • 終値、高値といったヒストリカルデータをCSVとして取得したい
  • 再取得時には過去に取得したファイルに追記したい
  • 様々な通貨ペア、時間足のデータを一度に取得したい

と、いった感じ。1分足のデータを元に各時間足のデータを作成する方法は採用しない。1分足のサンプル数に縛られてしまうため。5分足なら5分の1。日足なら10080分の1といった感じで膨大な1分足データが無いと無理。証券会社によって違うだろうけど1分足には1か月分程度のデータしかない。

ということでそれぞれのチャートからデータを取得する必要がある。

と、いうことでまずは1つのチャートのデータをCSVに出力してみよう。

CSV出力の基本的なコード

int Handle = FileOpen(_Symbol + "_" + _Period + "m.csv", FILE_CSV | FILE_WRITE | FILE_COMMON, ",");
if(Handle != INVALID_HANDLE){

    FileWrite(Handle, "DateTime", "Open", "Close", "High", "Low");
    for(int i=Bars - 1;i>=0;i--){
      FileWrite(Handle, FormatDT(Time[i]), Open[i], Close[i], High[i], Low[i]);
    }
    FileClose(Handle);
}

このコードは現在のチャートからヒストリカルデータを取得してCSVに出力するコード

すでにファイルが存在する場合は上書きされる。

FileOpen(ファイル名,操作モード,区切り文字)

第2引数の操作モードにはフラグを列挙する。今回は「FILE_CSV | FILE_WRITE | FILE_COMMON」と3つ

  • FILE_CSV ファイルの種類はCSV
  • FILE_WRITE 書込みモード
  • FILE_COMMON commonフォルダのパス

最後のはようわからんけど保存先。これ以外のやり方はわからない。

3つ目の引数は区切り文字。読み込みはこの単位で行われる。「\n」を指定すれば行で読み込まれる。

書込みでCSVを指定した場合はこれがデータの区切りとして挿入される。

FileWrite(ファイルハンドル,書き込むデータ・・・)

第1引数にはFileOpenで取得したハンドルを指定。第2引数に書き込むデータを入れる。それ以降の引数にデータを入れると区切り文字を挿入しつつデータが挿入される。CSVモードでは最後に改行が入る。他は知らない。

FormatDT()は自作関数

string FormatDT(datetime dt){
   return StringFormat("%4d-%02d-%02d %02d:%02d",
      TimeYear(dt),
      TimeMonth(dt),
      TimeDay(dt),
      TimeHour(dt),
      TimeMinute(dt));
}

MQLでは年月日がドットで区切られる。このままだとエクセル等で日付書式が自動適用されないので「YYYY-MM-DD hh:mm」の形式に変換するための関数

まず見出し行を書き込んでその後に各データをFor分で書き込んでいく。

時間なら「Time」。始値なら「Open」という感じでそのまんまの名前の配列にセットされている。添え字は「0」が最新の値で増えるごとに過去のものになる。過去からデータを並べたい場合ので、For文は最大値から0に向かって減算させる必要がある。

for(int i=Bars – 1;i>=0;i–)

チャートによってデータの数はまちまち。データ数は「Bars」に入っている。

追記対応のコード

続いて上書きでなく追記できるように変更したものがこれ

string sdate = NULL;
bool fg = false;
int Handle = FileOpen(_Symbol + "_" + _Period + "m.csv", FILE_CSV | FILE_READ | FILE_WRITE | FILE_COMMON, ",");
if(Handle != INVALID_HANDLE){
    while(!FileIsEnding(Handle)){
       sdata = FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
    }
    
    if(sdate == NULL){
      FileWrite(Handle, "DateTime", "Open", "Close", "High", "Low");
    }
    
    FileSeek(Handle,0,SEEK_END);
    
    int i=Bars - 1;
    
    //sdataの日時を検索
    while(!fg && i>=0){
      if(FormatDT(Time[i]) == sdate){
         fg=true;
      }
      i--;
    }
    
    int j = 0;

    if(fg){
      j=i;
    }else{
      j=Bars-1;
    }
    
    for(j;j>=0;j--){
      FileWrite(Handle, FormatDT(Time[j]), Open[j], Close[j], High[j], Low[j]);
    }

    FileClose(Handle);
}

追記のアルゴリズムを簡単に説明すると

  • CSVの最終データの日時を取得する
  • 最新データから取得した日時を検索する
  • 該当があればその次のデータからCSVに追加で書き込む
  • 該当がなければすべて書き込む

こんな感じ

FileOpenに「FILE_READ」を追加する

ということでここからは書込みだけでなく読み込みも必要なのでFileOpen()に「FILE_READ」のフラグを追加する必要がある。

FileOpen(Symbol + “” + _Period + “m.csv”, FILE_CSV | FILE_READ | FILE_WRITE | FILE_COMMON, “,”)

FileReadString(ファイルハンドル)

さて実際にファイルの内容を読み込むには「FileReadString()」を使う。読み込まれる単位はFileOpenの第3引数で指定した区切り文字になる。

また特定の行のデータを取得するということは出来ない。そのため例えば2行目の最初のデータを取得したい場合は

赤字の部分のデータを取得したい
FileReadString(Handle);
FileReadString(Handle);
FileReadString(Handle);
FileReadString(Handle);
FileReadString(Handle);
FileReadString(Handle);

このように6回実行すれば良いことになる。

5回取得するごとに1行データが下がるというわけ。今回取得したいのは一番最後の日時のデータなので

    while(!FileIsEnding(Handle)){
       sdata = FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
       FileReadString(Handle);
    }

最初のFileReadStringだけ変数に保存して4回実行する。これが1行分。これをデータの最後まで繰り返せば変数「sdata」には最後の日時が入っている。

この時ファイルの最後を判定するためのフラグが「FileIsEnding()」。

最終行から書込み

さて追記するデータが確定してもそのまま書込みしたらまた上書きされてしまう。

そのため書込み位置を最終行にするための処理が必要。

FileSeek(Handle,0,SEEK_END);

それがこの部分。ただFileReadStringで最後までシークしてるのかもしれないけどね。確認してない。

複数の通貨ペア、時間足のデータを一括処理する

さてここまでで1つのチャートからCSVを吐き出す処理は出来た。EAに書き込んで時間足を変更すればどんどん取得できるけど、面倒なので1発で終わらしたい。

そこで思いついたのが以下の3つの方法。

  1. 指定のチャートを自動作成してCSV出力→閉じるを繰り返す処理
  2. 現在のチャートの通貨ペア、時間足を自動で切り替える処理
  3. 現在のチャート以外のデータを取得する処理

結論としては3番になりました。1番は多分無理。2番はやれるけど複雑。

とりあえず3番で作ったコード

string sdate;

string Symbols[] = {"USDJPY","GBPUSD"};
int Periods[] = {PERIOD_M5,PERIOD_D1};

for(int s =0;s<ArraySize(Symbols);s++){

   for(int p=0;p<ArraySize(Periods);p++){
      
      sdate = NULL;
      bool fg = false;
      int Handle = FileOpen(Symbols[s] + "_" + Periods[p] + "m.csv", FILE_CSV | FILE_READ | FILE_WRITE | FILE_COMMON, ",");
      
      if(Handle != INVALID_HANDLE){
          while(!FileIsEnding(Handle)){
             sdate = FileReadString(Handle);
             FileReadString(Handle);
             FileReadString(Handle);
             FileReadString(Handle);
             FileReadString(Handle);
   
          }
          
          if(sdate == NULL){
            FileWrite(Handle, "DateTime", "Open", "Close", "High", "Low");
          }
          
          FileSeek(Handle,0,SEEK_END);
          
          int i=iBars(Symbols[s],Periods[p]) - 1;
          
          while(!fg && i>=0){
   
            if(FormatDT(iTime(Symbols[s],Periods[p],i)) == sdate){
               
               fg=true;
            }
            
            i--;
          }
          
          int j = 0;
          if(fg){
            j=i;
   
          }else{
          
            j=iBars(Symbols[s],Periods[p])-1;
          }
          
          for(j;j>=0;j--){
            FileWrite(
               Handle,
               FormatDT(iTime(Symbols[s],Periods[p],j)),
               iOpen(Symbols[s],Periods[p],j),
               iClose(Symbols[s],Periods[p],j),
               iHigh(Symbols[s],Periods[p],j),
               iLow(Symbols[s],Periods[p],j)

            );
          }
          FileClose(Handle);
      }
   }
}

現在チャートと異なるデータを得る

現在チャートと異なる通貨ペアや時間足のデータを得るには頭に「i」を付けた関数を使えばよい。

例えば日時を得るには「Time[添え字]」ではなく「iTime()」を使う。

iTime(通貨ペア,時間足,オフセット)

通貨ペアは文字列。時間足は定数で指定する。最後のオフセットは配列の時の添え字と同じで現在値が「0」。過去に行くほど大きな数字になる。

例えば「USDJPYの1時間足で3時間前のデータ」を取得したいとしたら

iTime(“USDJPY”,PERIOD_H1,3)

こういう感じ。ここまでくれば簡単。あとは配列にあらかじめ取得したい通貨ぺアと時間足のリストを作っておく

string Symbols[] = {"USDJPY","GBPUSD"};
int Periods[] = {PERIOD_M5,PERIOD_D1};

これをFor分に当て込んで回せばOK

現在のチャートの通貨ペア、時間足を自動で切り替える処理の場合

さてお蔵入りしたけど一応この方法もある。チャートを切り替えるにはChartSetSymbolPeriod()という関数を使えば良い。ただしこの関数はいくつか注意点がある。

まずChartSetSymbolPeriodはチャートを変更する関数というより「変更を予約する関数」といったほうが良い。

関数を実行しても即時チャートが変更するわけではなく、手が空いたら変更してねというニュアンスだ。そのため実行しても変更されずに処理が走り、処理が終わってから変更される。

そして次にEAはチャートを変更すると初期化されてしまうという問題がある。EAにこの関数を実装してもチャートが変更されるとまた同じ処理が走ってしまう。つまりループでどんどんチャートを切り替えようと思っても1番目と2番目のチャートをグルグル回る永久ループに陥ってしまう。

これを回避するにはループ処理をやめて、グローバル変数に現在何番目のチャートなのかという情報を保持するといった工夫がいる。

一応作ってみたが、大したことしてないのにコードが複雑になって嫌気がさしたので没になったというわけ。

これでバックテストができる!複利で儲けて大金持ちや!
ぐへへへ

ちたまん

(うわー。駄目なやつや)

ツン子