どうも潤奈です( ・Д・)
前回は波のゆくさきArmadaさんがnoteに書かれているゴトー日を研究してEAを作ろう!の記事の途中過程にある、価格変動を日本時間基準でCSV出力させるコードを参考にしたCSV出力用EAを書いて行きました。
今回はそこから応用して各曜日毎に集計してCSV出力するコードを書いて行きたいと思います( ・Д・)
CSV出力イメージ
※右のグラフは表計算ソフトで表示したグラフになります。
ソースコード
ではさっそくコードを書き込んで行きます。
パラメーター作成
1 2 3 4 5 6 7 8 9 10 |
#property copyright "Copyright 2021, Zyuna32246" #property link "https://zyunafx.com/" #property version "1.00" #property strict //---パラメーター設定 input int GMT_Adjust=7; //日本時間とMT4時間の差(冬季) input int sHour=10; //基準となる時間(時) input int sMinutes=0; //基準となる時間(分) input int eHour=10; //基準から遡る時間数(時) |
#property~の4行は作成した段階で挿入されている定型文なので、その下から入力して行きます。
1文づつの構成は、「修飾子 型 固有名=値; //コメント」となります。
まずMT4のサーバー時間は日本時間とタイムゾーン(GMT)が違うのでその差を修正する為に、GMT_Adjust変数でその時間差を宣言します。(証券会社によっては日本時間表示のMT4もあるのでチャートを開いて確認して下さい。)
次に基準となる時間からの価格変動を取得したいので、基準となる「sHour=時」と「sMinutes=分」をそれぞれ宣言し、その基準からどこまで(eHour=時)取得するのか?の変数を宣言します。
全体に関わる変数/配列を宣言
1 2 3 4 5 6 7 |
//---全体に関わる変数/配列 double Pips; //価格をpipsに置き換える為の値を入れる変数 double rate[][31][5]; //各分毎の価格×31日分×5曜日を入れる配列 string rateTime[]; //各分毎の「時:分」を入れる配列 int Count[][31][5]; //各分毎に価格を入れた回数×31日分×5曜日を入れる配列 int pNum; //配列の数を入れる変数 string sTime,eTime; //計測開始と終わりの時間を入れる変数 |
ここで宣言する理由としては、{ }で閉じた中で宣言すると、別の{ }内で値を引き継いで利用が出来ない為(方法はありますが)、{ }の外であるこの段階で宣言します。
それぞれの変数/配列の説明はコメントで加えていますので詳細は割愛しますが、今回は3次元配列を使っているので簡単にその説明だけします。
3次元配列とは
今回はrate配列とCount配列は3次元配列で宣言しています。
rate[601]の1次元配列だと使用出来る箱が601個なのに対し、rate[601][31][5]の3次元配列にすると601×31×5=93,155個の箱を用意出来ます。
※[601]という数はこの後ArrayResize関数で指定します。
※[31]の数字は31日分という意味で、[5]は月~金の5曜日分の数になります。
下に表で表していますが、rate[縦(行)][横(列)][奥(シート)]となります。例としてrate[2][4][0]は-26.26の値が入っています。
今回の項目に置き換えると、rate[分][日付][曜日]です。
※この縦・横・奥という概念は今回のデータを取る上での私の解釈です。
今回のこのイメージ図は出力したいCSVデータの形とは異なりますが、前回の「日付毎の集計」から考え方を引き継いでいますのでこの様な形で説明させて頂きました。
OnInit()内のコード
OnInit()はチャートに適用した時や、チャートの時間軸を変更した時に最初に呼び出される関数です。
なので、1回だけ計算すれば良い物などはここに書いてしまいます。
point→pipsに変換用の値を求める
1 2 3 4 5 6 7 |
//point→pipsに変換用の値を求める switch(_Digits){ case 2: Pips=0.01; break; case 3: Pips=0.01; break; case 4: Pips=0.0001; break; case 5: Pips=0.0001; break; } |
_Digits変数で適用したチャートの通貨ペアの価格の小数点以下の桁数を取得します。
その取得した桁数をswitch処理で一致するcase(条件)に進み処理を行い、break処理でswitch処理から抜けます。
小数点以下の桁数が2桁と3桁の業者があったり、決済通貨(右側)が円かドルなどでも桁数が違うのでこういった調整が必要になります。
配列数の準備
1 2 3 4 5 6 7 |
//配列に必要な数を計算 pNum=eHour*60; //遡る時間数を分数に置き換える //配列のサイズを指定 ArrayResize(rate,pNum+1); ArrayResize(rateTime,pNum+1); ArrayResize(Count,pNum+1); |
各配列には各分毎×時の値を入れる為の箱数が必要なので、eHour=遡る時間に60分を掛けた値をpNum変数に入れておきます。
次にArrayResize関数でそれぞれの配列サイズ(数)を指定します。
+1にしているのは、基準となる時間も1枠必要なので〇〇時間+1分の配列サイズが必要になります。
例)10:00を含む10分後までの必要な配列サイズは、10分+1分必要になります。
計測開始年月日を取得
1 2 3 4 5 |
//計測開始年月日として取得 例)20100217 sTime=(string)Year()+IntegerToString(Month(),2,'0')+IntegerToString(Day(),2,'0'); return(INIT_SUCCEEDED); }//OnInit()END |
最初に処理される所なので、計測開始年月日としてここで現在年月日を取得し、「+」で連結させてsTime変数に入れておきます。
Year()の頭に(string)が付いているのは、datetime型であるYear()をstring型に変換する。という意味になります。
IntegerToString関数は、指定した桁数に足らない場合、指定した文字で埋める関数です。
例えばMonth()=1の場合、1桁しかないので’0’で文字埋めして2桁にするようにしているので01となります。
OnTick()内のコード
OnTick()はティックを受信する度に処理される関数です。
サマータイム切り替え日を求める
1 2 3 4 5 6 7 |
//3月第2日曜日の日付を求める(86400=(60秒×60分×24時間)=1日の秒数) datetime SummerTime=StrToTime((string)Year()+".03.14"); SummerTime-=TimeDayOfWeek(SummerTime)*86400; //11月第1日曜日の日付を求める datetime WinterTime=StrToTime((string)Year()+".11.07"); WinterTime-=TimeDayOfWeek(WinterTime)*86400; |
アメリカ基準時刻の業者では夏時間:3月第2日曜日で切替わり、冬時間:11月第1日曜日で切替わります。
MT4には第〇〇曜日が〇日です。という事を求める関数がないので計算しないといけません。
夏時間と冬時間の求め方は同じで、StrToTime関数で現在年と”.03.14″(冬は”.11.07″)を連結させた文字をdatetime型の時間に変換して、SummerTime(WinterTime)変数に入れます。
そしてTimeDayOfWeek関数でその年月日の曜日を取得(日曜日:0~土曜日:6)して86400を掛けて秒数に変換します。
その値をSummerTime変数の年月日から減算(-=)します。
つまり、その年の3月14日が日曜日であれば0なので値に変更なしで、水曜日であれば3×86400=3日分の秒数を引いた3月11日が日曜日という事になります。
サマータイム判定
1 2 3 |
datetime CurrentTime=TimeCurrent(); //サーバー時間 int Summer=0; if(CurrentTime>=SummerTime && CurrentTime<WinterTime)Summer=1; //サマータイム期間の場合 |
TimeCurrent()関数でサーバー時間を取得してCurrentTime変数に入れます。
Summer変数には0を代入しておきます。
そしてif()関数で先ほど求めた夏時間と冬時間の切り替え日の間に、現在のサーバー時間が該当すれば、Summer変数に1を入れます。
日本時間を求める
1 2 3 4 5 |
datetime jTime=CurrentTime+(GMT_Adjust-Summer)*3600; //3600=(60秒×60分)=1時間の秒数 int jD=TimeDay(jTime); //日本時間の日を取得 int jH=TimeHour(jTime); //日本時間の時を取得 int jM=TimeMinute(jTime); //日本時間の分を取得 int dW=TimeDayOfWeek(jTime); //日本時間の曜日を取得(0:日~6:土) |
GMT+2(夏時間GMT+3)のブローカーの場合、日本時間はGMT+9なので9-2=7をGMT_Adjust変数にパラメーターで設定します。
よってサーバー時間にGMT差7×3600(7時間分の秒数)を足した日本時間をjTime変数に入れます。
サマータイムの場合、MT4のサーバー時間はGMT+3になる為、GMT差は6になります。
次にTimeDay()関数とTimeHour()関数とTimeMinute()関数とTimeDayOfWeek()関数を使って、日本時間の日と時と分と曜日を取得してjD変数に日、jH変数に時、jM変数に分、dW変数に曜日を入れます。
※曜日は0~6の数字で取得され、0=日、1=月、2=火、3=水、4=木、5=金、6=土となります。
計測開始時分を確認して処理{ }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if(jH==sHour && jM==sMinutes){ //計測開始時分の確認 double kOpen=Open[0]; //基準時間の始値を入れる datetime cTime=Time[0]; //計測開始の時間を入れる int Blank=0; //データがないローソク足計測用 static int First=0; //初回だけ動かしたい処理制御に使用 //--- //この間に計測する分数をループ処理を記述 //--- First=1; //1を入れてrateTimeにデータ入れる処理をさせない } |
if()文でパラメーターで設定した時と分が現在の日本時間と同じか確認します。
次に基準となる現在の始値Open[0]と現在の時間Time[0]をそれぞれの変数に入れます。
Blank変数には0を、First変数にはstatic変数をint型の前に書いて初回だけ0が入るようにしておきます。
最後に計測するループ処理後にFirst変数に1を入れます。
計測する分数のループ処理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//計測する分数をループ処理 for(int i=0;i<=pNum;i++){ if(TimeMinute(cTime)==TimeMinute(Time[i-Blank])){ //計測時間と取得時間が同じか確認 rate[i][jD-1][dW-1]+=(Open[i-Blank]-kOpen)/Pips; //取得時間の価格と基準価格の差を取得 Count[i][jD-1][dW-1]++; //取得時間の計測出来た回数をカウント }else{ //計測時間と取得時間が違う場合 Blank++; //データがない足の数をカウント } //--- //時間を取得してrateTime[]配列に入れる処理 //--- } |
for()文で現在値0から遡る分数の回数ループ処理させます。
ここでのi変数に入る数は基準時間からの〇分前の数になります。
まずif()文で計測開始時間cTimeの時間(分)とこれから取得する時間(分)が同じかを確認します。
同じ場合はrate[i][日付-1][曜日-1]配列に、取得した始値から基準価格の差(point)の値を、Pips変数で割ってpoint→Pipsに単位を変換して配列に加算(+=)します。
Count[i][日付-1][曜日-1]配列には、加算した回数として1を足して行きます。
}else{ は、もし時間が違う場合は、ヒストリカルデータの欠損が考えられるので、Blank変数にその回数を足して行きます。
ところで、Time[i-Blank]という書き方をしているのは何故かについて詳しく説明します。
まずi変数は〇分前という数で使用しますので以下の表のようになります。
しかし、例えば9:54のヒストリカルデータが欠損していたとしたら次の表のように、欠損した所から1分ズレてしまいます。(これが結構あるんです…)
理想の挙動としては欠損していた場合、もう一度同じi変数の値から取得して行かないといけません。
なので取得したい時間と違った場合、Blank変数に1を足してTime[i-Blank]とする事で欠損していた場合は値を取得せずに、次の処理でもう一度同じ値を取得するという処理になるようにしています。
1列目の時間情報を取得して格納
出力させるイメージはA列のようなデータです。
これは変動するデータではないので1回だけ処理出来れば良いです。
1 2 3 4 5 6 7 8 |
if(First==0){ //初回のループのみ処理 int tHour=TimeHour(jTime-60*i); //計測開始時間から1分前の時を取得 int tMinute=TimeMinute(cTime); //計測時間の分を取得 rateTime[i]=(string)tHour+"時"+(string)tMinute+"分"; //計測の時分を入れる } cTime-=60; //次の計測時間を1分過去に戻す }//for(int i=0;i<=pNum;i++)END First=1; //1を入れてrateTimeにデータ入れる処理をさせない |
if()文で初回のみ処理させます。
まず、日本時間jTimeからループで回ってくるi=〇分に60を掛けた秒数を引いて、その時間(時)をtHour変数に入れます。
次にcTime変数の時間(分)をtMinute変数に入れます。
そして、rateTime[]配列に取得した時間(時、分)を文字列として繋げて入れます。
if()文を抜けた所で、cTime変数の時間を手動で60秒減算(-=)しておきます。
更に、for()文を抜けた所でFirst変数に1を入れます。そうする事で最初のfor()文のループ処理だけrateTime[]配列にデータを入れる処理をする事が出来ます。
OnTester()内のコード
OnTester()関数はバックテストをした際、最後に処理される関数です。
ここにCSV出力のコードを書いて行きます。
計測終了年月日を取得
1 2 |
//計測終了年月日として取得 例)20220217 eTime=(string)Year()+IntegerToString(Month(),2,'0')+IntegerToString(Day(),2,'0'); |
コード構成は開始年月日を取得時と同じです。
この最後に処理されるタイミングの年月日を取得して繋げたデータをeTime変数に入れます。
31日分のファイル作成用のループ処理
1 2 3 4 5 6 7 8 9 10 |
//ループ処理で31日分のファイルを作成 for(int d=0;d<31;d++){ //ここに以降のCSV出力処理のコードが入ります }//for(int d)END |
各日付毎にCSVファイルを分けたいので、CSVファイル作成~終了までの式をfor()文のループ処理で31日分繰り返させます。
CSVファイル準備
1 2 3 4 |
//ファイル名 例)CSV_Analysis_AllDay_Weekdays_1_220100217_20220217_1000.csv string fName="CSV_Analysis_AllDay_Weekdays_"+(string)(d+1)+"_"+sTime+"_"+eTime+"_"+(string)sHour+IntegerToString(sMinutes,2,'0')+".csv"; int fDelete=FileDelete(fName,0); //同じ名前のファイルを消す int fOpen=FileOpen(fName,FILE_CSV | FILE_READ | FILE_WRITE,","); //ファイルを作る |
ファイルデータの名前として必要な情報を連結してfName変数に入れます。
CSVファイルなので最後に”.csv”を付け忘れないように注意して下さい。
今回は各日付毎にファイルを分けるので、ファイル名”CSV_Analysis_AllDay_Weekdays_”の後に日付の数字を追加しています。
d変数には配列の関係で0から始まるループ処理をさせていますので、日付に直す為に1を足しています。
※これを追加しないとファイル名が全て同じになってしまい、上書き(置き換える)する事になって1つしかファイルが残りません。
FileDelete()関数でfName変数の名前のデータがあれば削除させます。
FileOpen()変数でcsvファイルを作成します。
各引数はCSVファイルを作成するのに必要な引数として覚えて頂いたら問題ないです。
見出し作成して書き込む
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//1行目の件名データを作成 string subject; //件名入れる変数を宣言 string dWeek; //曜日名を入れる変数を宣言 for(int i=0;i<=5;i++){ //1列分+5曜日分ループ処理 //各曜日を判定する switch(i){ case 1: dWeek="月"; break; case 2: dWeek="火"; break; case 3: dWeek="水"; break; case 4: dWeek="木"; break; case 5: dWeek="金"; break; } if(i!=0){ subject+=","+dWeek; }else{ subject=(string)(d+1)+"日"; } } FileWrite(fOpen,subject); //1行目に件名データを書き込む FileSeek(fOpen,0,SEEK_END); //行末にシフト |
まず見出しとなる件名を入れるsubject変数と曜日名を入れるdWeek変数を文字列型で宣言します。
switch処理でdWeek変数に各曜日を入れて、break処理でswitch処理から抜けます。
次にfor文で集計日付と曜日を「,」で区切って連結させて行きます。
if(i=0){ から始めているのは、最初の1回だけでよい i==0 の処理より回数の多い処理を上に先に持って来ているからです。
FileOpen関数で開いているファイルfOpenにsubject変数のデータを書き込みます。それぞれの要素は「,」で区切ってあるのでセルが分かれます。
FileSeek関数で行末(ファイルの最後)に移動します。
各分毎のデータを書き込む
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
string data; //1行毎データを入れる変数を宣言 for(int i=pNum;i>=0;i--){ //時間(分)のループ処理 for(int k=0;k<5;k++){ //曜日毎のループ処理 double pRange=0; //価格変動幅を入れる変数 //計測の回数が1回も満たないか確認 if(Count[i][d][k]!=0){ pRange=rate[i][d][k]/Count[i][d][k]; //取得合計した価格差を取得回数で割って平均値を出す } if(k!=0){ //最初以外は値を連結する data+=","+(string)pRange; }else{ //最初は時間と値を入れる data=rateTime[i]+","+(string)pRange; } }//for(int k)END FileWrite(fOpen,data); //書き込む }//for(int i)END FileClose(fOpen); //ファイルを閉じる }//for(int d)END return(true); }//OnTester()END |
説明ポイントが多いので、コードを分割させながら説明して行きます。
1 2 |
string data; //1行毎データを入れる変数を宣言 for(int i=pNum;i>=0;i--){ //時間(分)のループ処理 |
まず1行毎のデータを入れるdata変数を宣言します。
for()文でi変数には取得した分数を入れて、減算させながらループ処理させて行きます。これが縦軸のループ処理になります。
1 2 |
for(int k=0;k<5;k++){ //曜日毎のループ処理 double pRange=0; //価格変動幅を入れる変数 |
続いてその中にfor()文でk変数に0を入れて、加算させながら5回ループ処理させて行きます。これが横軸のループ処理になります。
その中で価格変動幅を入れるpRange変数を宣言します。
1 2 3 4 |
//計測の回数が1回も満たないか確認 if(Count[i][d][k]!=0){ pRange=rate[i][d][k]/Count[i][d][k]; //取得合計した価格差を取得回数で割って平均値を出す } |
価格変動幅の平均値を出すのに取得合計した価格差を取得回数で割るのですが、一度も取得していない時は0となり、0で割るとエラーが出てしまいます。
なのでif()文で、その[時間][日付][曜日]の値を取得した回数で0以外の時に計算するように制限を加えます。
取得していれば、pRange変数に取得合計したrate[時間][日付][曜日]配列を取得回数Count[時間][日付][曜日]配列で割って平均値を入れます。
1 2 3 4 5 |
if(k!=0){ //最初以外は値を連結する data+=","+(string)pRange; }else{ //最初は時間と値を入れる data=rateTime[i]+","+(string)pRange; } |
見出し作成時と同じ様に、最初は時間と「,」と平均値を入れて、その後は「,」と平均値を連結させてdata変数に加えて行きます。
1 2 |
}//for(int k)END FileWrite(fOpen,data); //書き込む |
FileWrite関数で指定ファイルfOpenにdata変数の値を書き込みます。
※for(int k~)文の外に書いて下さい。
FileWrite関数はファイルの書き込み後に改行文字が追加されるので自動で改行されます。
1 2 |
}//for(int i)END FileClose(fOpen); //ファイルを閉じる |
すべて書き込み後、FileClose関数で指定ファイルfOpenを閉じます。
1 2 3 4 |
}//for(int d)END return(true); }//OnTester()END |
31日分のループ処理の}で閉じた後、return(true);でOnTester関数の処理は終了です。
コンパイル
コードをすべて書き込んだ後に「コンパイル」を実行します。
コンパイルを実行して、下にエラーが出なければこれでこのコードが使用する事が出来ます。
バックテスト時の設定
このコードをバックテストする際は以下の設定でお願いします。
期間(時間足):M1(1分足)
モデル:始値のみ(全ティックを選択しないでください)
このコードは1分毎に価格変動のデータを集計しますので、1分足の設定をして下さい。
そして1分毎の時間はコード内で確認していますが、1分の間でのティック処理までの制御は行っていません
従って全ティックでバックテストを行ってしまうと1分×ティック数のデータを集計してしまう事になりますので、値が変化してしまいます。
※より正確になるという事ではありません。
まとめ
前回は各日付毎に列を分けて集計しましたが、今回は日付をファイル毎に分けて曜日毎にデータを集計してみましたので、更に細かい分析が出来るようになります。
バックテストなどで勝てるロジック(結果を見て)探すのも良いですが、そのベースになるローソク足(価格の動き)を分析する事はロジックを考える上でもかなり重要だと思います。
そういった点で波のゆくさきArmadaさんのnoteにはその考え方が書かれておりとても勉強になります( ・Д・)
ちなみにこのように1回で31日分のデータファイルが出力されるので、いろんな期間を連続でバックテストすると後の整理が大変なので注意して下さいw
EAの全ソースコード
以下の記事をご購入頂きましたら今回のEAの全ソースコードをまとめて閲覧する事が出来ますので、コピーして作成すればすぐにバックテストで試す事が出来ます( ・Д・)
尚、この無料記事にすべて記述していますので、模写して行けば同じ物が出来ます。
時短を考える方、この記事を評価して頂ける方だけご購入ご検討下さい( ・Д・)
コメント