今回も誰の役に立つのかわからない、故に他では見かけない隙間に潜り込みます。
しかも全力他力本願です。
MT4のヒストリカルデータをRで加工します。
今回、MQLコードは出て来ません。
そしてRはただの前処理機として使用しています。
FX自動売買のバックテストに必要不可欠なヒストリカルデータですが、長期かつ高頻度で信頼性の高いものは(少なくとも無償では)なかなか入手しづらいのが現状です。
FXDDだと入手は楽なのですが、以前確認した時にパッと見で明らかに変な形の足を目撃してしまったのでちょっとなぁ・・・という感じです。
最近はアルパリジャパンも1分足を提供していますが、期間や銘柄数の点でちょっとさびしいです。
で、他に候補はいくつかあるのですが、今回はその中から比較的長期間(2001年11月末よりほぼ現在まで)のデータが提供されているFXCMのものを使用します。
FXCM提供の1分足データは専用(なのかな?)APIを用いて入手するわけですが、残念ながら私にそんな技術はありません。
そこでSoftgateさんのForex Studioに含まれるDatacenterを利用させて頂きます。
Datacenter(以下DC)はFXCMやFXDDのヒストリカルデータのダウンロード機能の他、時間帯調整済みMT4用hstデータの書き出し機能等を備えた強力便利ツールです。
DCを利用したFXCMヒストリカルデータのダウンロードについては、Softgateさんのブログをご参照下さい。
Softgateさんのブログは他の記事も技術的機知に富んだ素晴らしいコンテンツですので、MT4に興味のある方はぜひご一読を。
さてここからが厄介でした。
MT4に限らず、市場時系列データの時間帯は提供元によりまちまちです。
今回の場合ですとFXCMの元データはDCの仕様によりダウンロードした段階で "UTC/夏時間切り替えなし" で保存されている(はずでした)ので、それを私のメイン口座アルパリジャパンが採用している "UTC+2/UK夏時間" に合わせたいわけです。
ちなみに "UTC+2" というのはNY市場のクローズ(現地17:00)を0:00とすることにより日足が平日分のみの週5本に収まるようになる、多くのFX業者が採用している時間帯設定です。
前述のようにDCには時間帯調整機能が実装されているので、とにかくそんな面倒は一発解決!と思いきや、世の中そう甘くはなかったのです・・・。
とにかくまずは想定通り "UTC+2/UK夏時間" でDatacenterから書き出します。
注) ビルド6xx以降のMT4では保存先は画像中のパスではなく、ユーザーおよびターミナルごとに異なるデータフォルダ内のhistoryフォルダでなければなりません。
ですが、DCから直接そこへ書き出すと私の環境ではエラーメッセージが出るんですよね・・・。
書き出せてはいるようなので問題ないのかもしれませんが、気持悪いので私は一旦ここへ書き出してから移動しています。
ここで書き出したファイルは1分足のみなので、MT4でperiod_converter All スクリプトを使用して他の時間足も作ります。
とりあえず作成した日足を見てみましょう。
まず気になったのが、白矢印(小さくてすみません)で示した日曜日の足の出現です。
とはいえこれだけでは必ずしもデータ不良とは限りません。
シドニー市場や中東(土日も開いているんだとか)のデータが含まれているのかもしれません。
問題はアルパリの価格データと合致するかどうかです。
※ ここでターゲットとするデータですが、アルパリで4時間足以下のデータは先述の1分足データを利用して最長2011年6月までしか遡れません。
日足はドル円の場合で1993年頃までのデータが現在でもMT4からサーバーより取得可能です。
従いまして、本記事において検証および加工の手がかりとする公式データは日足の4本値のみということになりますので、それ以上の精度は期待なさらぬよう。
あと、Volume値に関してはデータおよびMT4の仕様上、気にする意味がないので完全無視です。
とりあえずMT4から1分足をcsvにエクスポート、Rで週末のデータを除去して日足の各4本値をプロットしてみます。
Rでの加工コードは後でまとめて掲載しますが、週末データの除去は簡単にできそうで意外と面倒です。(簡単な方法があるのかもしれないけど)
同じ悩みにぶつかったらしきリケジョママさんのコチラを参考にしました。
育休中の猛烈ブログ更新は産休に入っておられるようですがお元気でしょうか?
赤がアルパリ、その上に青でFXCMです。
2009年にとくに意味があるわけではなく、大体どの期間でもこんな感じになります。
高値と安値はほぼ揃っていますが、始値と終値のズレが気になります。
実際、この段階で両者ともに日中足が揃っている2011年以降の期間でそれぞれバックテストを行うと、戦略にもよりますがちょっと看過できない差異が生じます。
ではそのズレは補正できるものなのか?
ここで気になったのが、先程の日曜日の存在です。
もしかして時間帯設定が正しく行われていないのではないかと。
そもそもDCでダウンロード~保存される前のFXCMの元々のデータがどのような仕様なのか、探してみましたが資料が見つかりません。
それはきちんと定義、管理されたものなのでしょうか?
高値安値はほぼ近似しているので、価格方向ではそれほどズレていない、なおかつ時間帯の誤差もそれほど大きくはないと考えてよさそうです。
そこで下のような表を作って眺めてみました。
金曜日の最後の足から月曜日の最初の足まで、週末をはさんだ各日がいつ始まりいつ終わるかを並べています。
これを全期間に渡り目視で確認、何か法則がないか探しました。
全部で700行足らずですので、下手にプログラム化しようとして悩むより人力の方が速いかなと。
その結果、
・週末のデータがある期間と無い期間に分けられる
・オープンやクローズの時刻が何度か変わる
・変化後、その規則性は数ヶ月から数年、維持される
・複数の年で夏時間の切り替え時期に規則性が変化する
・規則性の変化はほぼすべて1時間単位
といったことがわかりました。
おそらく元データは複数のソースを統合したり、途中で管理方針が変更されるなどの理由により、一貫した時間軸に従っていないのではないかと。
DCはそれを一貫した規則性を持つものと仮定してカレンダーどおり、時計どおりの処理を行っているのでしょう。
あくまで元データのデタラメさが問題なのであって、このことによりDCの便利さや魅力が損なわれるものではありません。
期間ごとに何かしらの法則がありそうなことはわかったので、あとはオフセットを変えたり夏時間設定を変えて書き出しては検証するの繰り返しです。
ひたすらプロット、チャート、csvの目視という地道な作業。
この辺、自動でオフセットを繰り返して近似する時間帯を探すようなコードがあればかっこいいんでしょうけど、無いので詳しい過程は割愛します。
試行錯誤の末、もっとも無理のない形で説明できると私が考えている、DCに保存されたデータの時間帯の概要は、
・2001.11.28 - 2007.11.03 UTC +0 米国式にて夏時間切り替え
・2007.11.04 - 2008.02.16 UTC +2 夏時間切り替えなし
・2008.02.17 - 2008.02.23 UTC +3 夏時間切り替えなし
・2008.02.24 - 2012.03.31 UTC +1 夏時間切り替えなし
・2012.04.01 - current... UTC +0 夏時間切り替えなし
※もし後記コードと矛盾がありましたらコードの方が正解です。
つまり、元データがDCが想定しているとおりになっているのは直近2年半ちょっとらしいのです・・・。
で、これをDCでちまちま設定を変えながら分割して書き出し読み込むのは面倒なので、Rを使って一括変換しました。
その補正後の日足のプロットです。
かなり良くなりました。
終値にやや差異が目立ちますが、これはFXCMの金曜日のデータが直近数年以前は22時や23時にクローズしてしまっているためで、残念ながらどうにもなりません。
よし、これにて終了~!のつもりでcsvに書き出して両者のデータを並べ「大部分の日足4本値を数pips差程度に寄せられたな」と悦に入っていたら、どうもまだ時々始値に気になるズレが。
調べてみると・・・。
窓が・・・窓が閉じています・・・。
元データがこうなっているのでしょうか。
DCの仕様ということはないと思いますが。
これはどうしたものやら。
ここまでは「価格データは正しいけれども時刻が合っていない」という前提で作業を進めてきました。
ところがこれは価格データが間違っています。
窓以外の部分は概ね許容範囲内というかやむをえないところまで一致していそうなので、毎週月曜日の1本目の1分足の始値をアルパリの日足の始値に置き換えるのもアリでしょう。
ですがそれでは他に置き換えるべきデータを持っていない場合にはどうにもなりません。
なので今回はもうちょっと強引な方法を提案します。
それは「毎週月曜日1本目の1分足を除去してしまう」です。
我ながら乱暴だとは思いますが、バックテスト用としては意外と自然かもしれません。
月曜日のオープン時にチャート上の始値で1分以内にエントリーは難しいでしょうから。
これも簡単にできるかと思いきやちょっと面倒。
to.weekly()使って、とか思ったんですがどうも思っているのと挙動が違います。
週頭は日曜日なのかな。
最後にもう一点だけ補正を加えます。
これは日足チャートに1日あたりの1分足の数を数えるインジケーターを表示したものです。
古いデータでは数百本単位でデータの頻度が落ちますが、これはまぁ仕方ないですし1日の中でまばらに抜けているのであれば、ある程度は実用にも耐えます。
また周期的にポコポコと120本前後不足しているのは金曜日です。
しかし画像中の深く落ち込んでいる日には20本程度しか1分足がなく、しかもそれがオープン直後に集中しています。
主にクリスマス前後や8月15日前後にこういった日がいくつかありました。
これはデータの抜けというより、むしろブローカーの休業日になぜかデータがあると考えた方が自然でしょう。
そこで、1日の1分足が100本に満たない日はまるごと除去することにしました。
100本という数に深い意味は無いので、そこは適宜調整すればよいかと。
さて、ようやく長い検証と補正の旅も終わりです。
ここまで長文、駄文にお付き合い頂き誠にありがとうございます。
最終的な整理をしておきます。
※ 使用ソフトウェア MetaTrader4, Datacenter(ForexStudio), R(RStudioがあればなおよし)
※ MetaTreder4 / period_converter All スクリプト使用
※ R / xts パッケージ必須
※ FXCMのデータダウンロードにはアカウントが必要(デモで可、詳細は先述リンク確認)
※ 全期間のデータ処理には最低4GB以上のメモリーが無いとつらそう
======== MT4 ========
・ 下記作業およびその補正データを用いたバックテスト用にMT4ターミナルを新たにインストールする
・ 既存アカウントでも新規デモアカウトでもいいので何かしらのアカウントにログインする (以降、このアカウントのhistoryフォルダが作業対象となる。なお小数点以下の桁数はこのアカウントの仕様に依存するので注意)
・ MT4のツール > オプション > チャートで "ヒストリー内の最大バー数" と "チャートの最大バー数" を最低でも500万本以上に設定
・ ツール > オプション > サーバーで "プロキシサーバーを有効にする" にチェック、サーバーアドレスには接続不可能な値(1111とか)を入力 (MT4がサーバーと通信してデータが上書き更新されるのを防ぐため)
・ MT4を再起動、右下の表示が "回線不通!" になっているのを確認したら一旦終了
・ 所定のhistoryフォルダに補正作業を行う通貨ペアのhstファイルが存在する場合、各時間足すべて削除しておく
======== Datacenter ========
・ DCでFXCM 1分足データをダウンロード (時間かかります)
・ DCから "時間オフセット +2, イギリスの規則で夏時間を切り替える" 設定でエクスポート
======== MT4 ========
・ エクスポートしたhstファイルを所定のhistoryフォルダに入れ、MT4を起動
・ ツール > ヒストリーセンターから補正作業を行う通貨ペアの1 Minuteデータをcsv形式でRのワーキングディレクトリにエクスポート
・ MT4は一旦終了、エクスポートした通貨ペアのhstファイルを削除または移動しておく
======== R ========
・ Rで下記コードを実行、ワーキングディレクトリ内に "trim_(通貨ペア名)1.csv" が出力されるはず
======== MT4 ========
・ MT4を起動、ヒストリーセンターで先程の通貨ペアのデータが空になっていることを確認
・ インポートでRのワーキングディレクトリ内の "trim_(通貨ペア名)1.csv" をブラウズ
・ スキップを "1行" にして、その他はデフォルト設定でインポート
・ インポートした通貨ペアの1分足チャートを開いて period_converter All スクリプトを実行、他の時間足のhstファイルを作成
・ 処理が完了(ターミナルウインドウのエキスパートタブで確認)したらMT4を一旦終了、再起動して結果を確認
======== 完了 ========
※ MT4の設定や再起動、hstファイルの削除が適切に行われていないとうまくいかないかもしれません。
※ 他時間足生成スクリプトは類似したものならなんでも構いませんが、period_converter Allに比べてperiod_converter_ALLは遅かったりします。
※ MT4でもRでも10数年分の1分足データの準備や処理には数十秒から数分、待つことがあります。
※ 下記Rコードではxtsオブジェクトを操作するたびに gc() を行っていますが、大きなxtsオブジェクトの操作を繰り返すと使用メモリーが急速に肥大化するためです。 しかもgc()では処理しきれず終了するまで消えないゴミもたまります。(これ、何か良い対処法ないでしょうか・・・?)
※ エラー処理を実装していないので作業の可逆性を考えて重要な操作時には新規オブジェクトに代入していますが、とくに不安がなければセルフ代入でメモリーを節約してもよいでしょう。
それでは長い前フリの割にたいした中身の無い前処理コードをご覧ください。
# xts パッケージの読み込み
library(xts)
#-----------------------------------------------------------------------------------------------------------
# Functions
#-----------------------------------------------------------------------------------------------------------
# csv ヒストリカルファイル読み込み関数
fun.xts.single <- function(dat){
x.df <- read.csv(dat, header=FALSE,
col.names=c("Date","Time","Open","High","Low","Close","Volume"),
colClasses=c("character","character","numeric","numeric","numeric","numeric","integer"))
x.df$Datetime <- paste(chartr(".","-",x.df$Date),x.df$Time)
x.xts <- as.xts(zoo(x.df[,3:6],as.POSIXct(x.df$Datetime)))
rm(x.df)
return(x.xts)
}
# 検証用プロット関数
fun.xts.dualplot <- function(datA, datB, period, vgrd="months", mlabel="", ylabel=""){
plot(as.vector(datA[period]), type="l", col=2, lwd=2, main=mlabel, xlab=period, ylab=ylabel, xaxt="n")
lines(as.vector(datB[period]), type="l", col=4, lwd=2)
label.x <- axTicksByTime(datA[period], vgrd, format.labels="%m-%d")
grid.x <- axTicksByTime(datA[period], vgrd, format.labels="%m-%d")
axis(side=1, at=label.x, labels=names(label.x))
abline(v=grid.x, lty=2, col=8)
}
#-----------------------------------------------------------------------------------------------------------
# 週末データ削除用インデックス作成
#-----------------------------------------------------------------------------------------------------------
# 補正するデータの期間に合わせて日付を入力
index.day <- timeBasedSeq('20011128/20141116/d');
index.dow <- weekdays(index.day)
calender <- data.frame(index.day, index.dow)
index.weekday <- subset(calender, !(index.dow %in% c("土曜日","日曜日")))
index.weekday <- index.weekday[,-2]
#-----------------------------------------------------------------------------------------------------------
# csv ファイル読み込み
#-----------------------------------------------------------------------------------------------------------
original.hst <- fun.xts.single("USDJPY1.csv"); gc(); gc() # 読み込むファイル名を入力
#-----------------------------------------------------------------------------------------------------------
# 時間帯オフセット
# コメントアウトした日付はカレンダーから推定される変更日
# その日にデータが存在しない場合エラーになるのでコード中の日付に変更している
#-----------------------------------------------------------------------------------------------------------
adjust.hst <- original.hst
st.start <- NULL
st.end <- NULL
st.start[1] <- which(index(adjust.hst) == index(head(adjust.hst["2002-04-07"], 1)))
st.end[1] <- which(index(adjust.hst) == index(tail(adjust.hst["2002-10-25"], 1))) # 10-26
st.start[2] <- which(index(adjust.hst) == index(head(adjust.hst["2003-04-07"], 1))) # 04-06
st.end[2] <- which(index(adjust.hst) == index(tail(adjust.hst["2003-10-24"], 1))) # 10-25
st.start[3] <- which(index(adjust.hst) == index(head(adjust.hst["2004-04-04"], 1)))
st.end[3] <- which(index(adjust.hst) == index(tail(adjust.hst["2004-10-29"], 1))) # 10-30
st.start[4] <- which(index(adjust.hst) == index(head(adjust.hst["2005-04-03"], 1)))
st.end[4] <- which(index(adjust.hst) == index(tail(adjust.hst["2005-10-28"], 1))) # 10-29
st.start[5] <- which(index(adjust.hst) == index(head(adjust.hst["2006-04-02"], 1)))
st.end[5] <- which(index(adjust.hst) == index(tail(adjust.hst["2006-10-27"], 1))) # 10-28
st.start[6] <- which(index(adjust.hst) == index(head(adjust.hst["2007-03-11"], 1)))
st.end[6] <- which(index(adjust.hst) == index(tail(adjust.hst["2007-11-02"], 1))) # 11-03
for(i in 1:6){index(adjust.hst)[st.start[i]:st.end[i]] <- index(adjust.hst)[st.start[i]:st.end[i]] - 3600; gc(); gc()}
UTCm2.start <- which(index(adjust.hst) == index(head(adjust.hst["2007-11-04"], 1)))
UTCm2.end <- which(index(adjust.hst) == index(tail(adjust.hst["2008-02-15"], 1))) # 02-16
index(adjust.hst)[UTCm2.start:UTCm2.end] <- index(adjust.hst)[UTCm2.start:UTCm2.end] - 7200; gc(); gc()
UTCm3.start <- which(index(adjust.hst) == index(head(adjust.hst["2008-02-18"], 1))) # 02-17
UTCm3.end <- which(index(adjust.hst) == index(tail(adjust.hst["2008-02-23"], 1)))
index(adjust.hst)[UTCm3.start:UTCm3.end] <- index(adjust.hst)[UTCm3.start:UTCm3.end] - 10800; gc(); gc()
UTCm1.start <- which(index(adjust.hst) == index(head(adjust.hst["2008-02-25"], 1))) # 02-24
UTCm1.end <- which(index(adjust.hst) == index(tail(adjust.hst["2012-03-30"], 1))) # 03-31
index(adjust.hst)[UTCm1.start:UTCm1.end] <- index(adjust.hst)[UTCm1.start:UTCm1.end] - 3600; gc(); gc()
#-----------------------------------------------------------------------------------------------------------
# データのトリミング
#-----------------------------------------------------------------------------------------------------------
# 週末の削除
trim.weekend.hst <- adjust.hst[as.character(index.weekday)]; gc(); gc()
# この時点で書き出したければ・・・
# write.csv(cbind(strftime(index(trim.weekend.hst), format="%Y/%m/%d %H:%M"), coredata(trim.weekend.hst)),
# "trim_USDJPY1.csv", quote = FALSE, row.names = FALSE); gc(); gc()
# 月曜日1本目削除用インデックスの作成
# 上で作成した trim.weekend.hst からインデックスを抜き出すことに注意
index.day.H <- index(to.daily(trim.weekend.hst))
index.dow.H <- weekdays(index.day.H)
calender.H <- data.frame(index.day.H, index.dow.H)
index.monday <- subset(calender.H, index.dow.H %in% "月曜日")
index.monday <- index.monday[,-2]
index.mondayfirst <- NULL
for(i in seq_along(index.monday)){
index.mondayfirst[i] <- which(index(trim.weekend.hst) == index(head(trim.weekend.hst[as.character(index.monday[i])], 1)))
}
# 月曜日1本目の削除
trim.mondayfirst.hst <- trim.weekend.hst[-index.mondayfirst]; gc(); gc()
# write.csv(cbind(strftime(index(trim.mondayfirst.hst), format="%Y/%m/%d %H:%M"), coredata(trim.mondayfirst.hst)),
# "trim_USDJPY1.csv", quote = FALSE, row.names = FALSE); gc(); gc()
# データが不足している日の削除用インデックス作成
smallbars <- apply.daily(trim.mondayfirst.hst[,1], length) < 100 # 閾値とする本数をお好みで
index.notsmall <- as.Date(index(smallbars[!smallbars]))
# 過少データ日の削除
trim.smallbars.hst <- trim.mondayfirst.hst[as.character(index.notsmall)]; gc(); gc()
# csv ファイルの書き出し
write.csv(cbind(strftime(index(trim.smallbars.hst), format="%Y/%m/%d %H:%M"), coredata(trim.smallbars.hst)),
"trim_USDJPY1.csv", quote = FALSE, row.names = FALSE); gc(); gc()
#-----------------------------------------------------------------------------------------------------------
# compare with alpari
# アルパリまたはその他のデータと比較検証したい場合
# それぞれの日足データをMT4から書き出すなりデータ整形してファイル名を適宜指定
#-----------------------------------------------------------------------------------------------------------
# csv ファイル読み込み
alpari.1440 <- fun.xts.single("USDJPY1440_alpari.csv"); gc(); gc()
names(alpari.1440) <- c("A_open", "A_high", "A_low", "A_close")
fxcm.1440 <- fun.xts.single("USDJPY1440_fxcm.csv"); gc(); gc()
# (下記でもいい気がするけどなぜか結果が想定通りにならないことがある)
# fxcm.1440 <- to.daily(trim.smallbars.hst); gc(); gc();
names(fxcm.1440) <- c("F_open", "F_high", "F_low", "F_close")
# 抜けがあってもタイミングを揃えるために合体
merge.1440 <- merge(alpari.1440, fxcm.1440)
merge.1440 <- merge.1440[,c(1,5,2,6,3,7,4,8)]
merge.1440 <- merge.1440["2001-11::2014-11"]
# 1年ごとに重ねてプロットしてみたり
label.price <- c("Open", "High", "Low", "Close")
for(p in 1:4){
for(i in 2001:2014){
y <- as.character(i)
fun.xts.dualplot(merge.1440[,(p * 2 - 1)], merge.1440[,(p * 2)], y, mlabel=label.price[p])
}
}
# csv で書き出して眺めてみたり
write.csv(cbind(strftime(index(merge.1440), format="%Y/%m/%d"), coredata(merge.1440)),
"USDJPY1440_compare.csv", quote = FALSE, row.names = FALSE); gc(); gc()
#-----------------------------------------------------------------------------------------------------------
# 不要になったオブジェクトの削除
# 200MB弱のオブジェクトがいくつもできているのでエンバイロメントを保存するなら
# 不要なものを削除した方がよいかも
#-----------------------------------------------------------------------------------------------------------
rm(original.hst); gc(); gc() # 元データ
rm(adjust.hst); rm(trim.weekend.hst); rm(trim.mondayfirst.hst); gc(); gc() # 途中過程
rm(trim.smallbars.hst); gc(); gc() # 完成データ
rm(alpari.1440); rm(fxcm.1440); rm(merge.1440); gc(); gc() # 検証用日足データ
・最後に
以上、ムキになって日足基準でアルパリに揃えようとがんばってみましたが、本来基準とするべき信頼できる1分足データが手に入らない限りは本当にこれで正しいのかどうかは知りようがありません。
また、いくつかの通貨ペアで同じ内容の処理でよさそうなことを確認していますが、他のすべてに通用するかどうかは不明です。
さらに言えばアルパリのデータが正しいという保証はどこにもありませんし、FXCMのデータにここまでする価値があるのかも・・・。
MetaQuotesやFXDDよりはマシかもしれないとはいえ、こんな日もあります。
いずれにしろ、どんなに信頼性が高かろうが過去データは過去データです。
たとえ正確な過去データが手に入ったとしても、それが自分にとっての現実とは限らないのがリアルトレードではないでしょうか。
それではみなさん、
Merry Christmas & A Happy New Year !!!
(Softgateさん、本記事公開にご理解頂きありがとうございます)