English Русский 中文 Español Deutsch Português 한국어 Français Italiano Türkçe
MQL5 プログラミング基礎:時刻

MQL5 プログラミング基礎:時刻

MetaTrader 5 | 26 11月 2015, 16:39
12 392 0
Dmitry Fedoseev
Dmitry Fedoseev

目次


はじめに

MQL5 は時間と連動するシンプルな関数を数多く提供しています。そういった関数に慣れるのに難しいと思う必要はありません。日時の利用を要求するタスクは多くありません。主なタスクは以下です。

  • 与えられた時点における特定のアクションを行うため(図 1) これらは毎日、毎日同時刻に、一日の既定時刻に、毎週既定日に、あるいは単に既定の日時に行うアクションです。

    図1 時点 
    図1 時点 

  • 既定の時間範囲内で特定アクション(タイムセッション)を有効化または無効化するため これは1日における時間セッション(毎日ある時点から別の時点へ)を含み、週の特定日における特定アクション、週のある日の既定時刻から週の別の日の既定時刻までのタイムセッション、単に指定の日時範囲内のアクションを有効/無効化します。

    図2  時間範囲 
    図2  時間範囲 

現実には時間使用はひじょうに複雑なものです。問題は時間測定および Expert Advisors とインディケータが使用される環境の特殊性に関連しています。

  • 価格変動の不在によりチャート上で失われるバーそれは M1、M5、M15 と言った短い時間枠で特に顕著です。バーの欠損は長い時間枠でも観測されることがあります。

  • ディールセンターからのクオートは実際には月曜にあたる日曜のバーを含むことがあります。

  • 週末。月曜の前日は土曜ではなく金曜となります。金曜の次に来るのは土曜ではなく月曜です。

  • 日曜のバーに加え週末すべてを含むクオートを継続的に提供するディーリングセンターもあります。週日に比べるとかなり低いものではあっても、週末中も価格変動は起こっています。

  • トレードサーバーおよびローカルコンピュータ(トレーダーのコンピュータとトレーディング端末)間のタイムゾーンの相違。異なるディールセンターのサーバー時刻は同じではありません。

  • 夏時間 

本稿は時間に関する一般論から入ります。そして時間に連動する標準的 MQL5 関数を概観します。また同時にプログラムテクニックについて考察し、最後に実用的問題を取り上げます。

本稿は長い記事となっているため、最近MQL5 を始めたばかりの初心者プログラマーは一度読んだだけではすべてを理解しにくいことでしょう。最低でも3日はかけて読み進めるのがよいでしょう。


時間測定の特殊性

少しばかり本題からそれて、天文学の話をします。地球が太陽の周りを公転するかたわら、地軸を中心に自転しているのは周知の事実です。地軸は公転軌道に対してわずかに傾いています。地軸を中心に地球が1周するのに必要な時間を天文(天球、グローバル)座標では天文日または恒星日と呼ばれます。

地上で暮らす一般の人(宇宙飛行士とは対照的に)は恒星日に何の関心も払いません。重要なことは日中と夜間の交代です。日中-夜間の周期に必要な時間は太陽日と呼ばれます。地球の北極延長線上から太陽系を見ると(図3)、地球が地軸を中心に自転し反時計回りに太陽の周りを公転しているのが判ります。よって太陽について公転軸を一巡するためには地球は360日強の日数を費やして回ることになります。結果、1太陽日は恒星日よりもわずかに長いものとなります。

図3  地軸を中心とした地球の時点と太陽を回る公転(地球の北極側から見たもの)
図3  地軸を中心とした地球の時点と太陽を回る公転(地球の北極側から見たもの)

便宜上また精度のために、1太陽日を時間計測の基本とみなします。1太陽日は60分を1時間として24分割します、など。1恒星日は23時間56分4秒です。軌道面に対する地軸のわずかな傾きは地球上の生物に知られる季節の変化を産み出します。

太陽日の1年は完全な1年ではありません。実際は少し超過し、365日6時間です。そのためカレンダーが定期的に調整されます。4年ごとに1日追加(4の倍数年)され、2月に29日目を設ける(うるう年)ことでこれが行われます。ただし、この調整は完全に正確なものではなく、4の倍数であってもうるう年にならない年もあります。『00』(100の倍数)で終わる年にはカレンダーの調整は行われません。しかもそれだけではありません。

ある年が100の倍数で同時に400の倍数であれば、その年はうるう年とみなされ、カレンダーは調整される必要があります。1900年は4と100の倍数ですが、400の倍数ではないのでうるう年ではないのです。2000年は4、100、400の倍数なのでうるう年です。次に4と100の倍数になるのは2100年です。が、400の倍数ではないのでうるう年とはなりません。本稿の読者の方は4年ごとにうるう年だという認識でかまいません。次に100の倍数で同時にうるう年となるのは2400年です。


タイムゾーン

地球は地軸の周りを自転していることから日中と夜間の交代が生じます。日中と夜間、むしろ地上の異なる場所で同時の一日の任意の時刻です。1日は24時間なので地球の円周は15° ずつに24分割され、それぞれはタイムゾーンと呼ばれます。便宜上、タイムゾーンの境界はかならずしも経線に沿っていませんが、その代り行政上の領土境界:国境や地域の境など、に沿っています。

基準点はグリニッジ子午線と呼ばれる最初の経線です。それはグリニッジのロンドン地区を通っており、名前の由来はそこにあります。「グリニッジ標準時」はグリニッジ子午線から両側それぞれ7.5度に広がっています。グリニッジ標準時から東に区画される12のタイムゾーン(+1~+12)、西に区画される12のタイムゾーン(-1~-12)があります。実際には、-12~+12のタイムゾーンは 15度ではなく7.5度です。-12~+12のタイムゾーンは子午線180の左右に位置し、これがいわゆる「国際日付変更線」です。

グリニッジで正午(12:00)のとき、-12~-24:00のタイムゾーンでは00:00、一日の始まりです。一方+12のタイムゾーンでは翌日の00:00です。同じことが別の任意の時間についても言えます。時計の時刻が同じでもカレンダーの日付は異なるのです。実際には-12のタイムゾーンは使われず、それは+12と置き換わります。同じことが-11のタイムゾーンについても起こります。-11のタイムゾーンは+13と置き換わるのです。このことはおそらく経済関係の特殊性に関連します。たとえば、+13のタイムゾーンに位置するサモア独立国は日本と強い経済関係にあり、そのため日本のタイムゾーンに近いと都合がよいのです。

また、-04:30 や+05:45 などのめずらしいタイムゾーンもあります。ご興味のある方は Windows の時間設定機能のタイムゾーンリストを利用して確認することができます。


夏時間

日光の有効利用とエネルギー節約の実践のため夏時間制として1時間時計を早める国は世界に多くあります。約80か国が夏時間制を取り入れています。その他の国は実施していません。広範囲で夏時間を実施している国の中には、実用的な理由で複数のタイムゾーンにまたがるものもあります(米国を含み)。夏時間は主要な経済的に発展した国で実施されています。ほぼヨーロッパの各国(ドイツ、英国、スイスを含む)、米国(ニューヨーク、シカゴ)、オーストラリア(シドニー)、ニュージーランド(ウェイントン)などです。日本では夏時間は実施されていません。2011年3月27日、ロシアは最後に1時間早めましたが、10月に元の時間に戻すことはしませんでした。それ以来、ロシアは公式に夏時間制実施を行っていません。

夏時間への変更手続きは国によって異なります。米国では、ローカル時刻で3月の第二日曜日の午前2時に時間を早め、11月第一日曜日午前2時に時計を戻します。ヨーロッパでは、3月最終日曜日の午前2時に夏時間に移行し、10月最終日曜日の午前3時に戻します。ローカル時刻に設定する代わりにヨーロッパでは一斉に時間以降が行われます。ロンドンで午前2時、ベルリンで午前3時、というようにタイムゾーンに応じて時刻以降が行われるのです。夏時間から戻されるのは、ロンドンで午前3時、ベルリンで午前4時、といったぐあいです。

オーストラリアとニュージーランドは南半球に位置しており、夏が始まるのは北半球で冬がやってくるころです。その結果、オーストラリアでは10月の第一日曜日に夏時間に移行し、標準時間に戻るのは4月の第一日曜日です。オーストラリアの夏時間開始に関しては、開始日および終了日がその他の国と一致しないので、正確にはむつかしいものがあります。ニュージーランドでは、夏時間への移行は9月の最終日曜日午前2時に行われ、4月の第一日曜日午前3時に標準時間に戻ります。


時間標準

前述のように、太陽日を時間測定の基本とし、他のあらゆるタイムゾーンで時間を判断する基本となるグリニッジ標準時が時間標準とされています。グリニッジ標準時は GMT と短縮されることがよくあります。

ただし、地球の自転がわずかに不均等であると立証されたため、時間を計測するために電波時計が使用され、UTC(協定世界時)が新たな時間標準となりました。最近 UTC はすべてのタイムゾーンでの時間計測において全世界の第一時間標準の役割を果たしています。夏時間制に対する必要な調整も行われています。UTC は夏時間制からの影響は受けません。

太陽を利用した GMT は、電波時計に基づく UTC とは完全に一致しません。UTC と GMT の間には500日間で約1秒までの相違があります。このため、6月30日または12月31日に1秒の調整が行われることがあります。


日時フォーマット

日付フォーマットは国により異なります。たとえばロシアでは、習慣的に最初に日付を書き、次に月と年を書きます。日付の数字は点で区切られます。たとえば、01.12.2012 = 2012年12月1日、のようにです。米国では日付フォーマットは月/日/年です。ここでは日付の数字はスラッシュ "/" で区切られます。また点やスラッシ "/" 以外に、日付の数字を区切るのにハイフン "-" が使われることもあります。時刻表記の際は、時、分、秒はコロン ":" で区切られます。たとえば、12:15:30 = 12 時 15 分 30 秒です。

日時フォーマットを指定するシンプルな方法があります。たとえば、"dd.mm.yyyy" は最初に日付(2桁の日付。日付が 1~9 なら頭に 0 を付けます)、それから月(2 桁)、そして 4 桁の年を意味します。"d-m-yy" は日(1 桁の数字可)が最初に来て、つぎに月(1 桁の数字可)、そして 2 桁の年を意味します。たとえば、1/12/12 = 2012年12月1日、のようにです。日、月、年の値はハイフン "-" で区切られます。

時刻はスペースで区切られます。時刻フォーマットは時に "h" 、分に "m" 、秒に "s" が使われます。また同時に必要な桁数も指定されます。たとえば、"hh:mi:ss" は最初に時(0 は 1~9 の値の前に追加します)、それから分(2 桁必要です)、そして秒(2 桁)を意味します。ここで時、分、秒の値はコロンで区切られます。

プログラマー的視点からもっとも正確であるとみなされる日時表記形式は "yyyy.mm.dd hh:mi:ss" です。この表記を使用して書かれた日付でストリングをソートするとき、年代順で簡単に並べ替えることができるのです。毎日テキストファイルで情報を格納し、それを同じフォルダに保管するとします。この形式を使用してファイル名をつけたら、フォルダ内のファイルは都合よくソートされ順番に整列されることでしょう。

これで理論部分は終了です。ここから実装部分に進みます。


MQL5 における時間

MQL5では時刻は1970年1月1日に開始されたいわゆる Unix エポックの始まりから経過した秒数で測定されます。時刻を格納するには datetime タイプの変数を使用します。タイプ変数の最小値は 0 (エポック開始日に対応して)です。一方最大値は 32 535 244 799 (3000年12月31日23:59:59 に対応して)です。


現在サーバー時刻の判断

現在時刻を判断するには TimeCurrent() 関数を使用します。この関数は最後にわかっているサーバー時刻を返します。

datetime tm=TimeCurrent();
//--- output result
Alert(tm);

同じサーバー時刻がチャートのバー時刻を指定するのに使用されます。最後にわかっているサーバー時刻は銘柄ウィンドウで開かれたあらゆるシンボルの最後の価格変更時刻です。銘柄ウィンドウに EURUSD だけがある場合 TimeCurrent() 関数は EURUSD の直近価格変更を返します。銘柄ウィンドウは通例数多くのシンボルを表示するので、関数は基本的に現在のサーバー時刻を返します。ただし、価格は週末には変動しないため、関数が返す値は実際のサーバー時刻とは大きく異なることとなります。

特定のシンボル(直近の価格変動時刻)によってサーバー時刻を見つける必要があれば、 SYMBOL_TIME 識別子を伴うSymbolInfoInteger() 関数を使用することができます。

datetime tm=(datetime)SymbolInfoInteger(_Symbol,SYMBOL_TIME);
//--- output result
Alert(tm);


現在ローカル時刻の判断

ローカル時刻は(ユーザーのPC時計によって表示されている)PC TimeLocal() 関数によって判断されます。

datetime tm=TimeLocal();
//--- output result
Alert(tm);

実際には、EAおよびインディケータをプログラムする際は通常サーバー時刻を使うようになります。ローカル時刻は警告やジャーナル入力の際便利です。それはユーザーがPC時計表示時刻とメッセージや入力タイムスタンプを比較し、そのようなメッセージや入力がどのくらい前に登録されたか確認するのにより利便性があります。ただし、MetaTrader 5 ターミナルは自動的に関数 Alert() および Print() を使用したアウトプットであるメッセージやジャーナル入力にタイムスタンプを追加します。よって TimeLocal() 関数を使用する必要が生じるのはひじょうに稀でしょう。


時間のアウトプット

上記コードの tm 変数値は Alert() 関数を用いたアウトプットであることに注意が必要です。その値は読みやすい形式で表示されます。たとえば、"2012.12.05 22:31:57"などです。これはAlert() 関数が渡される引数を 文字列 タイプ(関数Print() および Comment()を使用する場合、またtext ファイルおよび csv に出力する場合同じことが起こります)に変換することによります。datetimeタイプ変数の値を持つテキストメッセージを作成する際にタイプは責任を持って変換する必要があります。フォーマットされた時刻を取得する必要がある場合文字列タイプへ、または必要なものが数値の場合に文字列タイプが続く long タイプへ変換します。

datetime tm=TimeCurrent();
//--- output result
Alert("Formatted: "+(string)tm+", in seconds: "+(string)(long)tm);

long タイプおよび ulong タイプの変数の値範囲は datetimeタイプ変数の値範囲に重なるため、それらも時刻を格納するのに使用可能ですが、その場合はフォーマットされた時刻を出力するために long タイプを datetime タイプに変換し、その後さらに文字列タイプに変換するか、数値を出力したければ文字列タイプにのみ変換します。

long tm=TimeCurrent();
//--- output result
Alert("Formatted: "+(string)(datetime)tm+", in seconds: "+(string)tm);


時間のフォーマット

時刻フォーマットについては記事 『MQL5 プログラミング基礎:文字列』 のさまざまな変数を文字列に変換することを述べたセクションで考察されています。ここではキーポイントを簡潔に述べます。データタイプを変換するだけでなく、MQL5 は日時データを文字列に変換する際フォーマットを指定する関数を提供しています。それは TimeToString() 関数です。

datetime tm=TimeCurrent();
string str1="Date and time with minutes: "+TimeToString(tm);
string str2="Date only: "+TimeToString(tm,TIME_DATE);
string str3="Time with minutes only: "+TimeToString(tm,TIME_MINUTES);
string str4="Time with seconds only: "+TimeToString(tm,TIME_SECONDS);
string str5="Date and time with seconds: "+TimeToString(tm,TIME_DATE|TIME_SECONDS);
//--- output results
Alert(str1);
Alert(str2);
Alert(str3);
Alert(str4);
Alert(str5);

TimeToString() 関数は datetime タイプ変数に適用可能です。また long タイプおよび ulong タイプ、その他の整数変数にも適用できますが、時刻を格納するには使用するべきではありません。

StringFormat() 関数を用いてテキストメッセージをフォーマットするときにはタイプ変換をする必要があります。

  • 時刻を datetime タイプ変数で格納するとき

    datetime tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Formatted: %s, in seconds: %I64i",(string)tm,tm);
    //--- output result
    Alert(str);
  • 時刻を long タイプ変数で格納するとき

    long tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Formatted: %s, in seconds: %I64i",(string)(datetime)tm,tm);
    //--- output result
    Alert(str);
  • または TimeToString() 関数を使用して

    datetime tm=TimeCurrent();
    //--- generating a string
    string str=StringFormat("Date: %s",TimeToString(tm,TIME_DATE));
    //--- output result
    Alert(str);


時間を数字に変換。時間の加減算

フォーマットされた日付(文字列)を数字(エポック開始から経過した秒数)に変換するには StringToTime() 関数が使用されます。

datetime tm=StringToTime("2012.12.05 22:31:57");
//--- output result
Alert((string)(long)tm);

上記コードの結果、秒にての出力時刻は以下のような日付"2012.12.05 22:31:57"に対応して "1354746717" となります。

時刻が数値で表わされていると、さまざまな処理が簡単で便利になります。たとえば、過去あるいは未来の日付と時刻を見つけることができます。時刻は秒で計測されるため、期間を秒で追加する必要があります。1分は60秒、1時間は60分または3,600秒であることがわかっているので、期間の長さを計算するのにさほど労力はかかりません。

現在時刻から1時間(3,600秒)を引く、または現在時刻に1時間を足すと、1時間前または1時間後の時刻を得ることになります。

datetime tm=TimeCurrent();
datetime ltm=tm-3600;
datetime ftm=tm+3600;
//--- output result
Alert("Current: "+(string)tm+", an hour ago: "+(string)ltm+", in an hour: "+(string)ftm);

StringToTime() 関数に渡される日付は完全なものである必要はありません。その上、時刻なしの日付、あるいは日付なしの時刻を渡すことも可能です。時刻なしの日付を渡す場合、関数は指定日付の 00:00:00 時の値を返すことになります。

datetime tm=StringToTime("2012.12.05");
//--- output result
Alert(tm);

時刻だけを渡す場合、関数は現在日付の指定時刻に応じた値を返します。

datetime tm=StringToTime("22:31:57");
//--- output result
Alert((string)tm);

また時刻を秒部分なしで返すことも可能です。日付を渡す場合、渡すことのできる時刻コンポーネントは時間部だけです。これは実践ではほとんど要求されることはありません。ただし、興味があれば遠慮なくご自身で実験してください。


日時のコンポーネント

個別の日時コンポーネント(年、月、日など)の値を決定するには TimeToStruct() 関数および MqlDateTime ストラクチャを使用します。ストラクチャは参照により関数に渡されます。関数実行後、ストラクチャには渡されたデータのコンポーネント値が書き込まれます。

datetime    tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
//--- output date components
Alert("Year: "        +(string)stm.year);
Alert("Month: "      +(string)stm.mon);
Alert("Day: "      +(string)stm.day);
Alert("Hour: "        +(string)stm.hour);
Alert("Minute: "     +(string)stm.min);
Alert("Second: "    +(string)stm.sec);
Alert("Day of the week: "+(string)stm.day_of_week);
Alert("Day of the year: "  +(string)stm.day_of_year);

日付コンポーネント以外にストラクチャには追加のフィールドが含まれることに注意します。曜日(day_of_weekフィールド)および通年の日数(day_of_year)です。曜日は 0(0 -日曜、1-月曜など)から計算されます。通年の日数もゼロから計算されます。その他の値は一般に認められている数え方に従います(月は1から。日数も1から)。

TimeCurrent() 関数を呼ぶには別の方法もあります。MqlDateTime タイプのストラクチャは参照によって関数に渡されます。関数実行のあと、ストラクチャには現在日付コンポーネントが書き込まれます。

MqlDateTime stm;
datetime tm=TimeCurrent(stm);
//--- output date components
Alert("Year: "        +(string)stm.year);
Alert("Month: "      +(string)stm.mon);
Alert("Day: "      +(string)stm.day);
Alert("Hour: "        +(string)stm.hour);
Alert("Minute: "     +(string)stm.min);
Alert("Second: "    +(string)stm.sec);
Alert("Day of the week: "+(string)stm.day_of_week);
Alert("Day of the year: "  +(string)stm.day_of_year);

TimeLocal() 関数もまたこの方法で呼ばれます。


コンポーネントからの日付作成

MqlDateTime ストラクチャを datetime タイプに逆変換することも可能です。これには StructToTime() 関数を使用します。

ちょうど1ヶ月前の時刻を決めます。一月の日数は一定ではありません。30 日または 31 日の月もあれば、2月は 28 日または29 日です。よって前に考察した時刻の加減算はあまり適しているとは言えません。そこで日付をコンポーネントに分解し、月の値を1減らします。月の値が1の場合はそれを12と設定し、年の値を1減らします。

datetime tm=TimeCurrent();
MqlDateTime stm;
TimeToStruct(tm,stm);
if(stm.mon==1)
  {
   stm.mon=12;
   stm.year--;
  }
else
  {
   stm.mon--;
  }
datetime ltm=StructToTime(stm);
//--- output result
Alert("Current: "+(string)tm+", a month ago: "+(string)ltm);


バー時刻の判断

インディケータ作成の際には、MetaEditor はOnCalculate() 関数の2つのバージョンのうちの1つを自動的に作成します。

バージョン1:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   return(rates_total);
  }

バージョン2:

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
  {
   return(rates_total);
  }

関数の1番目のバージョンのパラメータにはエレメントがすべてのバーの時間を持つ time[] 配列をインクルードします。

2番目のバージョンの場合は、EAのプログラミングをしたり別の時間枠のバー時刻へのインディケータからのアクセスを作成する際常に行うのと同じように、CopyTime() 関数を使用します。この関数は3通りのバージョンで存在します。すべてのバージョンにおいて、最初の2つの関数パラメータはシンボルとバー時刻が決定されるチャートの時間枠を決めます。最後のパラメータは返された値を格納する配列を決めます。また真ん中の2つのパラメータは使われている関数のバージョンに依存します。

CopyTime -バージョン1 コピーをするバーインデックスとエレメント数を指定する必要があります。

//--- variables for function parameters
int start = 0; // bar index
int count = 1; // number of bars
datetime tm[]; // array storing the returned bar time
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert(tm[0]);

この例は D1 の時間枠における直近バー時刻コピーの実装を示しています。使用する関数バージョンはそれに渡されるパラメータタイプによります。上記例は int タイプ変数の使用を示しています。それは指定されたバー数に対するバー番号による時刻を取得する必要があることを意味しています。

CopyTime() 関数使用時、バーはゼロを起点に右から左へ数えられます。取得された値(最終パラメータ)に対して使用される動的配列は CopyTime() 関数自体により必要なサイズに拡大縮小されます。静的配列も使用可能ですが、その場合は配列サイズが必要なエレメント数(4番目のパラメータ値)に厳密に対応している必要があります。

一度に複数バーから時刻が取得されるときは、返される配列のエレメントの順序を理解することが重要です。チャート上のバーが指定バーから始めて右から左へ数えられると同時に、配列エレメントは左から右へ整列されます。

//--- variables for function parameters
int start = 0; // bar index
int count = 2; // number of bars
datetime tm[]; // array storing the returned bar time
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert("Today: "+(string)tm[1]+", yesterday: "+(string)tm[0]);

このコードの結果、昨日のバーは tm 配列の 0 エレメントに格納され、1番目のエレメントには本日バー時刻が持たれます。

チャートでバーを数える順序と同じ順序に整列された配列内に時刻を持つ方が便利に思えることもあります。その場合ArraySetAsSeries() 関数が効果的です。

//--- variables for function parameters
int start = 0; // bar index
int count = 2; // number of bars
datetime tm[]; // array storing the returned bar time
ArraySetAsSeries(tm,true); // specify that the array will be arranged in reverse order
//--- copy time 
CopyTime(_Symbol,PERIOD_D1,start,count,tm);
//--- output result
Alert("Today: "+(string)tm[0]+", yesterday: "+(string)tm[1]);

そうすると本日バー時刻はインデックス 0 のエレメント内にあり、昨日バー時刻はインデックス 1のエレメントにあります。

CopyTime -バージョン2 ここでは CopyTime() 関数を呼ぶ際、コピーを開始するバー時刻とコピーするバー数を指定する必要があります。このバージョンは低い時間枠のバーを持つ高い時間枠の時刻を決めるのに適しています。

//--- get the time of the last bar on M5
int m5_start=0; 
int m5_count=1;
datetime m5_tm[];
CopyTime(_Symbol,PERIOD_M5,m5_start,m5_count,m5_tm);
//--- determine the bar time on H1 that contains the bar on M5
int h1_count=1;
datetime h1_tm[];
CopyTime(_Symbol,PERIOD_H1,m5_tm[0],h1_count,h1_tm);
//--- output result
Alert("The bar on М5 with the time "+(string)m5_tm[0]+" is contained in the bar on H1 with the time "+(string)h1_tm[0]);

高い時間枠にあるバーの開始である低い時間枠にあるバー時刻を決めたい場合はもっと複雑です。高い時間枠にあるバーと同じ時刻のバーは低い時間枠で失われる可能性があります。そのような場合には、前回の高い時間枠に含まれる、最終の低い時間枠の時刻を取得します。よって低い方の時間枠のバー数を決め、次のバー時刻を取得する必要があります。

以下は前述の実装で既製の関数形式になっています。

bool LowerTFFirstBarTime(string aSymbol,
                         ENUM_TIMEFRAMES aLowerTF,
                         datetime aUpperTFBarTime,
                         datetime& aLowerTFFirstBarTime)
  {
   datetime tm[];
//--- determine the bar time on a lower time frame corresponding to the bar time on a higher time frame 
   if(CopyTime(aSymbol,aLowerTF,aUpperTFBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]<aUpperTFBarTime)
     {
      //--- we got the time of the preceding bar
      datetime tm2[];
      //--- determine the time of the last bar on a lower time frame
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      if(tm[0]<tm2[0])
        {
         //--- there is a bar following the bar of a lower time frame  that precedes the occurrence of the bar on a higher time frame
         int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0])-2;
         //--- the Bars() function returns the number of bars from the bar with time tm[0] to
         //--- the bar with time tm2[0]; since we need to determine the index of the bar following 
         //--- the bar with time tm2[2], subtract 2
         if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
           {
            return(false);
           }
        }
      else
        {
         //--- there is no bar of a lower time frame contained in the bar on a higher time frame
         aLowerTFFirstBarTime=0;
         return(true);
        }
     }
//--- assign the obtained value to the variable 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

以下は関数パラメータです。

  • aSymbol -シンボル
  • aLowerTF -低い時間枠
  • aUpperTFBarTime 高い時間枠のバー時刻
  • aLowerTFFirstBarTime -低い時間枠の戻り値

すべてのコードにおいて、この関数は CopyTime() 関数の呼び出しがうまくいったかチェックします。エラーの場合はこの関数が偽を返します。バー時刻は aLowerTFFirstBarTime パラメータによって参照で返されます。

以下は関数使用例です。

//--- time of the bar on the higher time frame H1
   datetime utftm=StringToTime("2012.12.10 15:00");
//--- variable for the returned value
   datetime val;
//--- function call
   if(LowerTFFirstBarTime(_Symbol,PERIOD_M5,utftm,val))
     {
      //--- output result in case of successful function operation
      Alert("val = "+(string)val);
     }
   else
     {
      //--- in case of an error, terminate the operation of the function from which the LowerTFFirstBarTime() function is called
      Alert("Error copying the time");
      return;
     }

関数が高い時間枠に存在しないバーの時刻を受け取ると、高い時間枠のバーに含まれる低い時間枠のバーがない状況が生じます。その場合、関数は真を返し、時刻値 0 が aLowerTFFirstBarTime 変数に書き込まれます。高い時間枠のバーが存在すれば、低い時間枠のそれぞれに対応するバーが最低ひとつはあります。

高い時間枠のバーに含まれる低い時間枠の最終バー時刻を見つけることの方がやや簡単です。高い時間枠の次のバー時刻を計算し、低い時間枠の対応するバー時刻を決めるために取得した値を利用します。結果の時刻が計算された高い時間枠の時刻に等しければ、低い時間枠における先行するバー時刻を決定する必要があります。結果時刻が計算された高い時間枠の時刻より小さければ、正しい時刻を取得したことになります。

以下は前述の実装で既製の関数形式になっています。

bool LowerTFLastBarTime(string aSymbol,
                        ENUM_TIMEFRAMES aUpperTF,
                        ENUM_TIMEFRAMES aLowerTF,
                        datetime aUpperTFBarTime,
                        datetime& aLowerTFFirstBarTime)
  {
//--- time of the next bar on a higher time frame
   datetime NextBarTime=aUpperTFBarTime+PeriodSeconds(aUpperTF);
   datetime tm[];
   if(CopyTime(aSymbol,aLowerTF,NextBarTime,1,tm)==-1)
     {
      return(false);
     }
   if(tm[0]==NextBarTime)
     {
      //--- There is a bar on a lower time frame corresponding to the time of the next bar on a higher time frame.
      //--- Determine the time of the last bar on a lower time frame
      datetime tm2[];
      if(CopyTime(aSymbol,aLowerTF,0,1,tm2)==-1)
        {
         return(false);
        }
      //--- determine the preceding bar index on a lower time frame
      int start=Bars(aSymbol,aLowerTF,tm[0],tm2[0]);
      //--- determine the time of this bar
      if(CopyTime(aSymbol,aLowerTF,start,1,tm)==-1)
        {
         return(false);
        }
     }
//--- assign the obtain value to the variable 
   aLowerTFFirstBarTime=tm[0];
   return(true);
  }

以下は関数パラメータです。

  • aSymbol -シンボル
  • aUpperTF -高い時間枠
  • aLowerTF -低い時間枠
  • aUpperTFBarTime 高い時間枠のバー時刻
  • aLowerTFFirstBarTime -低い時間枠の戻り値

すべてのコードにおいて、この関数は CopyTime() 関数の呼び出しがうまくいったかチェックします。エラーの場合はこの関数が偽を返します。バー時刻は aLowerTFFirstBarTime パラメータによって参照で返されます。

以下は関数使用例です。

//--- time of the bar on the higher time frame H1
datetime utftm=StringToTime("2012.12.10 15:00");
//--- variable for the returned value
datetime val;
//--- function call
if(LowerTFLastBarTime(_Symbol,PERIOD_H1,PERIOD_M5,utftm,val))
  {
//--- output result in case of successful function operation
   Alert("val = "+(string)val);
  }
else
  {
//--- in case of an error, terminate the operation of the function from which the LowerTFFirstBarTime() function is called
   Alert("Error copying the time");
   return;
  }

注意! 関数に渡される時刻は高い時間枠の性格な時刻であると考えます。バーの正確な時刻がわからず、そのバーのあるティックだけまたは高い時間枠に含まれる低い時間枠のバーだけがわかっている場合は、時刻の正常化が必要となります。MetaTrader 5 ターミナルで使用される時間枠は日をバーの整数値に分けます。エポックの開始からバー数を判断し、それを秒で表されるバーの長さで積算します。

datetime BarTimeNormalize(datetime aTime,ENUM_TIMEFRAMES aTimeFrame)
  {
   int BarLength=PeriodSeconds(aTimeFrame);
   return(BarLength*(aTime/BarLength));
  }

以下は関数パラメータです。

  • aTime -時刻
  • aTimeFrame -時間枠

以下は関数使用例です。

//--- the time to be normalized
datetime tm=StringToTime("2012.12.10 15:25");
//--- function call
datetime tm1=BarTimeNormalize(tm,PERIOD_H1);
//--- output result
Alert(tm1);

ただ、これは時刻の正常化方法というだけでなく、低い時間枠の時刻によって高い時間枠の時刻を決めるための別の方法でもあります。

CopyTime -バージョン3 この場合、CopyTime() 関数を呼ぶとき、時刻がコピーされる元のバーに関して時間範囲を指定します。この方法により高い時間枠のバーに含まれる低い時間枠のバーをすべて簡単に取得することができます。

//--- time of the bar start on H1
datetime TimeStart=StringToTime("2012.12.10 15:00");
//--- the estimated time of the last bar on
//--- M5
datetime TimeStop=TimeStart+PeriodSeconds(PERIOD_H1)-PeriodSeconds(PERIOD_M5);
//--- copy time
datetime tm[];
CopyTime(_Symbol,PERIOD_M5,TimeStart,TimeStop,tm);
//--- output result 
Alert("Bars copied: "+(string)ArraySize(tm)+", first bar: "+(string)tm[0]+", last bar: "+(string)tm[ArraySize(tm)-1]);


一日の開始時刻および開始時刻からの経過時間判断

既定時刻から一日の開始時刻を決めるのにもっとも明白な方法は時刻をコンポーネントに分けることです。時、分、秒ををゼロから合計していくのです。ですが、もっと簡単な方法がひとつあります。1日は 86,400 秒です。ある日の時間を秒数で割った結果を整数で取得し、それを1日の秒数で掛ける必要があります。

datetime tm=TimeCurrent();
tm=(tm/86400)*86400;
//--- output result
Alert("Day start time: "+(string)tm);

注意! この方法は整数変数にのみ効果があります。計算にダブルや浮動小数点タイプの値があれば、端数部分はMathFloor() 関数を用いて切り捨てる必要があります。

MathFloor(tm/86400)*86400

かけ算の次に値は NormalizeDouble() 関数を用いて正常化する必要があります。結果の値は整数であるべきなので、四捨五入関数 MathRound()を使用します。

MathRound(MathFloor(tm/86400)*86400)

整数変数を使用する際、余りは自動的に切り捨てられます。時刻に関しては、ダブルタイプや浮動小数点タイプの変数が必要なことはまずありません。そういった変数を使用することはたいがい根本的に誤った方法であると言えます。

一日のスタートから経過した秒数を判断するには、時間を 86400で割った余りを取り除くので十分です。

datetime tm=TimeCurrent();
long seconds=tm%86400;
//--- output result
Alert("Time elapsed since the day start: "+(string)seconds+" sec.");

秒で取得した時間を時、分、秒に変換するために同様の方法が使えます。関数としての実装

int TimeFromDayStart(datetime aTime,int &aH,int &aM,int &aS)
  {
//--- Number of seconds elapsed since the day start (aTime%86400),
//--- divided by the number of seconds in an hour is the number of hours
   aH=(int)((aTime%86400)/3600);
//--- Number of seconds elapsed since the last hour (aTime%3600),
//--- divided by the number of seconds in a minute is the number of minutes 
   aM=(int)((aTime%3600)/60);
//--- Number of seconds elapsed since the last minute 
   aS=(int)(aTime%60);
//--- Number of seconds since the day start
   return(int(aTime%86400));
  }

最初に渡されるパラメータは時刻です。その他のパラメータは戻り値に使用されます。aH-時、aM-分、aS-秒です。関数それ自体はその日のスタートから秒数トータルを返します。関数をチェックします。

datetime tm=TimeCurrent();
int t,h,m,s;
t=TimeFromDayStart(tm,h,m,s);
//--- output result
Alert("Time elapsed since the day start ",t," s, which makes ",h," h, ",m," m, ",s," s ");

その日のスタートバーを決めるためのインディケータで使用される日数を計算することもできます。

bool NewDay=(time[i]/86400)!=(time[i-1]/86400);

バーは左から右にインデックスが付けられ、その場合 time[i] が現在バー時刻、time[i-1] が前回バー時刻、と考えられます。


週の開始時期およびそこからの経過時間判断

週のスタート時期を決めることは日のスタートを決めるのよりもやや複雑です。曜日は一定しており週の継続時間を秒で計算することはできる(604800 秒)ものの、エポック開始からの整数としての週数をただ計算し、週の経過で積算するだけでは十分とは言えません。

問題は、多くの国で一週間は月曜から始まりますが、一週間が日曜から始まる国(米国、カナダ、イスラエルその他)もあるということです。ただ、記憶にあるように時間計測のエポックは木曜日にスタートします。木曜日が週の始まりだとしたら、前述のシンプルな計算で十分です。

便宜上、 0 に相当するエポック日初日例により週のスタートを決定するという特殊性について考察します。必要なのは、時刻に足すとエポックの初日(1970.01.01 00:00)を変更し、ゼロから4日目まで数える値です。すなわち4日間の継続時間を足す必要があるのです。一週間が月曜に始まるなら、木曜は4日目となります。その場合は3日の経過時間を足す必要があります。ただ、一週間が日曜日に始まるなら、木曜日は5日目となります。よって4日の経過時間を足す必要があります。

曜日数の計算をする関数を書きます。

long WeekNum(datetime aTime,bool aStartsOnMonday=false)
  {
//--- if the week starts on Sunday, add the duration of 4 days (Wednesday+Tuesday+Monday+Sunday),
//    if it starts on Monday, add 3 days (Wednesday, Tuesday, Monday)
   if(aStartsOnMonday)
     {
      aTime+=259200; // duration of three days (86400*3)
     }
   else
     {
      aTime+=345600; // duration of four days (86400*4)  
     }
   return(aTime/604800);
  }

この関数は新しい週の最初のバーを決めるインディケータ内で使うと便利です。

bool NewWeek=WeekNum(time[i])!=WeekNum(time[i-1]);

バーは左から右にインデックスが付けられ、その場合 time[i] が現在バー時刻、time[i-1] が前回バー時刻、と考えられます。

これで週の始まり時期を計算することができます。曜日数を計算するためにはエポックが3日(または4日)早く始まると仮定されるため、ここでは時間をさかのぼる修正をする必要があります。

long WeekStartTime(datetime aTime,bool aStartsOnMonday=false)
  {
   long tmp=aTime;
   long Corrector;
   if(aStartsOnMonday)
     {
      Corrector=259200; // duration of three days (86400*3)
     }
   else
     {
      Corrector=345600; // duration of four days (86400*4)
     }
   tmp+=Corrector;
   tmp=(tmp/604800)*604800;
   tmp-=Corrector;
   return(tmp);
  }  

関数は long タイプの値を返します。というのも一番最初の週の値はマイナス(エポック開始の3日~4日前)だからです。関数の二番目のパラメータは週が日曜日に始まるか月曜日に始まるかで決まります。

これで週の開始時期を取得しました。よって週の始まりからの経過秒数を計算することができます。

long SecondsFromWeekStart(datetime aTime,bool aStartsOnMonday=false)
  {
   return(aTime-WeekStartTime(aTime,aStartsOnMonday));
  }

秒は日、時、分、秒に変換することができます。日の開始から時、分、秒数を計算するのは難しくないのですが、今回の場合は TimeToStruct() 関数を使う方が簡単でしょう。

long sfws=SecondsFromWeekStart(TimeCurrent());
MqlDateTime stm;
TimeToStruct(sfws,stm);
stm.day--;
Alert("Time elapsed since the week start "+(string)stm.day+" d, "+(string)stm.hour+" h, "+(string)stm.min+" m, "+(string)stm.sec+" s");

値 stm.day は1少ないことに留意ください。月の日数は、全日数を決める必要があるため1から数えられます。このセクションの関数について実践的でないと考える方がいらっしゃるかもしれませんが、時間との連携を経験するという点でこれら関数を理解することは大きな価値のあるものです。


既定日から経過した週、開始年、開始月の判断

MqlDateTime ストラクチャのフィールド、特に day_of_year を見ると、一年の始まり、また月の始まりからの週数を決める関数を作成したいと思うものです。既定日付からの週数を決めるには一般的関数を書く方が適しています。関数処理の原則はエポック開始からの週数を決める関数を用いたものと類似しています。

long WeekNumFromDate(datetime aTime,datetime aStartTime,bool aStartsOnMonday=false)
  {
   long Time,StartTime,Corrector;
   MqlDateTime stm;
   Time=aTime;
   StartTime=aStartTime;
//--- determine the beginning of the reference epoch
   StartTime=(StartTime/86400)*86400;
//--- determine the time that elapsed since the beginning of the reference epoch
   Time-=StartTime;
//--- determine the day of the week of the beginning of the reference epoch
   TimeToStruct(StartTime,stm);
//--- if the week starts on Monday, numbers of days of the week are decreased by 1,
//    and the day with number 0  becomes a day with number 6
   if(aStartsOnMonday)
     {
      if(stm.day_of_week==0)
        {
         stm.day_of_week=6;
        }
      else
        {
         stm.day_of_week--;
        }
     }
//--- calculate the value of the time corrector 
   Corrector=86400*stm.day_of_week;
//--- time correction
   Time+=Corrector;
//--- calculate and return the number of the week
   return(Time/604800);
  }

この関数を基に、一年の開始およびひと月の開始からの週数を決める関数を2つ書きます。これにはまず一年の開始時期およびひと月の開始時期を決める必要があります。時刻をコンポーネントに分け、いくつかのフィールド値を調整し、コンポーネントを表記に戻します。

  • 一年の開始時期を決める関数:

    datetime YearStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.mon=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }
  • 月の開始時期を決める関数:

    datetime MonthStartTime(datetime aTime)
          {
           MqlDateTime stm;
           TimeToStruct(aTime,stm);
           stm.day=1;
           stm.hour=0;
           stm.min=0;
           stm.sec=0;
           return(StructToTime(stm));
          }

以下は一年およびひと月開始からの週数を決める関数です。

  • 一年の開始から:

    long WeekNumYear(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,YearStartTime(aTime),aStartsOnMonday));
          }
  • ひと月の開始から:

    long WeekNumMonth(datetime aTime,bool aStartsOnMonday=false)
          {
           return(WeekNumFromDate(aTime,MonthStartTime(aTime),aStartsOnMonday));
          }

最後に完全に実践的なタスクに取り組みます。


実験的ツールセットの作成

すでにお話したように、クオートが日曜日のバーを含み、を継続して週末全体のバーを提供するディーリングセンターがあります。そこでいかなる場合においても適切に処理を行うのに必要な関数を確認するべきです。インターネットで適切なディーリングセンターを見つけ、デモアカウントでクオートを使用して関数処理を検証することももちろんできます。ただ、正しいディーリングセンターを検索することはさておき、必要な検証を実行するチャートの適切な箇所を検索することも必要です。

関数を検証するために独自の検証領域を作成します。金曜日、週末、月曜日が特に関心の的です。必要に応じ金曜日、週末、月曜日のバーを持つ配列を作成します。選択肢は合計4つあります。

  1. 週末バーなし
  2. 日曜日の終わりのバー、例えば4
  3. 継続した週末クオート
  4. 土曜日バー、日曜日バーはなし

配列が大きくなり過ぎないように時間枠は H1 を使用します。最大配列サイズは 96 エレメント(24 バー/日 × 4 日)で、配列自体はグラフィカルオブジェクトを使用して描かれる場合チャートにフィットします。時刻を伴うインディケータバッファのようなものを取得し、インディケータを開始するときの OnCalculate() 関数の初回実行に似た方法でループ内で配列を繰り返します。このように関数処理を可視化することができるのです。

このツールは本稿に添付したスクリプト(sTestArea.mq5 ファイル)の形で実装されます。準備はスクリプトの OnStart() 関数で行われます。関数コードの冒頭にある Variant 変数により上記リストの4つのオプションから一つを選択することができます。OnStart() 関数の下にインディケータの OnCalculate() 関数に似た LikeOnCalculate() を確認します。この関数にはパラメータが2つあります。:rates_total -バー数、および time[] -バーの時刻を持つ配列、です。

そして、インディケータを書くようにこの関数で作業を続けます。SetMarker() 関数を呼ぶことで関数からマーカーを設定することが可能です。SetMarker() 関数に渡されるパラメータは次のようなものです。:バーインデックス、バッファインデックス(マーカーが表示される行)、マーカー色。

図4 はスクリプトの処理結果を示しています。値が 2 の Variant 変数および各バー(バーは適切なタイムスタンプでマークされています)の下に設定された 2 行のマーカーです。全チャートエレメントに設定された色は非表示です。

図4  sTestArea.mq5 スクリプトのパフォーマンス
図4  sTestArea.mq5 スクリプトのパフォーマンス

バーのタイムスタンプは曜日に応じて色を変えます。:金曜日-赤、土曜日-紫、日曜日-グリーン、月曜日-ブルー。これで週末バーに対して特別な処理が必要な関数を書き進めることができます。しかもそれら関数の動作をビジュアルで監視することが可能なのです。


Pivot インディケータ -オプション1

まずシンプルな Pivot インディケータを作成してみます。Pivot ラインを計算するためには昨日の終値が必要です。また、昨日の高値および安値も必要です。インディケータ値はこれら 3 つの値から計算されます。その日の新たな高値および安値を特定し、新しい一日の始まりにおける Pivot 値を計算し、その後その日一日のレベルを描き表示します。

インディケータ処理方法を 2 とおり提供します。

  1. Pivot は新たな日(週末バーはないと仮定して)ごとに計算されます。週末バーがある場合、土曜日バーと日曜日バーは処理が異なります。
  2. 土曜日バーは金曜日に属します。一方、日曜日バーは月曜日に属します(これはクオートが週末をとおして継続的に提供される場合、また日曜日バーのみ存在する場合に適用します)。ここでは多分週末にはバーはないだろうという考えておく必要があります。

一番目のバージョンでは、新しい日の始まりを決定するだけで十分です。関数に対し現在時刻(aTimeCur)および前回時刻(aTimePre)を渡し、エポック開始からの日数を計算し、それらが一致しなければ新しい日が始まったと推測します。

bool NewDay1(datetime aTimeCur,datetime aTimePre)
  {
   return((aTimeCur/86400)!=(aTimePre/86400));
  }

第二バージョンです。土曜日が始まっていれば、デイスタートは無視します。日曜日が始まっていれば、デイスタートを決めます(これは当然ただ新たな日です)。月曜日が日曜日の次に始まっていれば、デイスタートはとばします。月曜日の前が土曜日や金曜日といった別の曜日であれば、デイスタートを決定します。得られるのは以下のような関数です。

bool NewDay2(datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
//--- new day
   if(NewDay1(aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      switch(stm.day_of_week)
        {
         case 6: // Saturday
            return(false);
            break;
         case 0: // Sunday
            return(true);
            break;
         case 1: // Monday
            TimeToStruct(aTimePre,stm);
            if(stm.day_of_week!=0)
              { // preceded by any day of the week other than Sunday
               return(true);
              }
            else
              {
               return(false);
              }
            break;
         default: // any other day of the week
            return(true);
        }
     }
   return(false);
  }

そしてここにあるのはバージョンに応じた一般的関数です。

bool NewDay(datetime aTimeCur,datetime aTimePre,int aVariant=1)
  {
   switch(aVariant)
     {
      case 1:
         return(NewDay1(aTimeCur,aTimePre));
         break;
      case 2:
         return(NewDay2(aTimeCur,aTimePre));
         break;
     }
   return(false);
  }

"sTestArea" ツール(添付の sTestArea_Pivot1.mq5 ファイル。デイスタートは茶色でマークしています)を用いて関数処理を検証します。検証は 8 とおり実行する必要があります。:4 つのバー作成オプションに対し 2 つの関数バージョンです。関数処理が適切に行われていると確認したところで、インディケータの作成を始めることができます。ただし、インディケータ作成は本稿の対象ではないので、ここでは既製のインディケータ(Pivot1.mq5 ファイル)をアタッチし、もっとも難しい部分のみ詳細を考察します。


時間セッションの判断

Expert Advisor が一日の指定の時間範囲内で毎日同じ間隔でトレードを行えるようにする必要があります。トレードセッション開始時刻および終了時刻を指定します。 "14:00" と指定される時刻の文字列変数よりも、個別に指定された時と分は、その関数が Expert Advisor で使用されていれば「ストラテジーテスタ」での最適化を可能にします。

時間セッションを決めるには以下に従います。

  1. 開始時点に対してデイスタートからの時間を秒で計算します。終了時点についても同様に計算します。
  2. デイスタートからの現在時間を秒で計算します。
  3. 開始および終了時刻と現在時刻を比較します。

トレードセッションがある日に始まり、別の日に終了するということもありえます。すなわち、トレードセッションが夜中を過ぎ、結果そこでデイスタートから計算される終了時刻は開始時刻よりも小さい値となってしまうのです。このため 2 つのチェックが必要です。そこで得られる関数は以下のようなものです。

bool TimeSession(int aStartHour,int aStartMinute,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- session start time
   int StartTime=3600*aStartHour+60*aStartMinute;
//--- session end time
   int StopTime=3600*aStopHour+60*aStopMinute;
//--- current time in seconds since the day start
   aTimeCur=aTimeCur%86400;
   if(StopTime<StartTime)
     {
      //--- going past midnight
      if(aTimeCur>=StartTime || aTimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- within one day
      if(aTimeCur>=StartTime && aTimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

トレードセッションが夜中を過ぎる場合、現在時刻はセッション開始時刻よりも大きくなるか等しく、もしくは終了時刻よりも小さくなる必要があります。セッションが一日以内に起これば、現在時刻は開始時刻よりも大きいか等しく、かつ終了時刻よりも小さくなる必要があります。

関数処理検証のために作成されるインディケータは本稿末尾に添付(Session.mq5 ファイル)があります。アプリケーションの他のインディケータ同様、このインディケータも検証だけに使用されるのではなく実践的目的のためにも使用可能です。


1日における時点の判断

指定時刻に等しいかシンプルにチェックすることは適切に行えません。というのも、ティックは定期的に発生せず、数秒から数分の遅延の可能性があるからです。マーケットにおいて指定時刻に単にティックが存在しないということも大いにありえます。既定のタイムスタンプの交点を確認する必要があります。

現在時刻は指定時刻よりも大きいか等しい必要があります。一方、前回時刻は指定時刻よりも小さくなる必要があります。一日のある時点を決定する必要があるため、現在時刻(前回時刻も同様に)をデイスタートからの秒に換算します。同じく、既定の時刻パラメータ(時、分)も秒に換算します。たぶん前回時刻は前日となっています。すなわち、デイスタートからの秒に換算すると現在時刻よりも大きくなります。この場合、時間セッションを判断するときと同じ方法で進めます。2とおりのチェックを行うのです。

そこで得られる関数は以下のようなものです。

bool TimeCross(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
//--- specified time since the day start
   datetime PointTime=aHour*3600+aMinute*60;
//--- current time since the day start
   aTimeCur=aTimeCur%86400;
//--- previous time since the day start
   aTimePre=aTimePre%86400;
   if(aTimeCur<aTimePre)
     {
      //--- going past midnight
      if(aTimeCur>=PointTime || aTimePre<PointTime)
        {
         return(true);
        }
     }
   else
     {
      if(aTimeCur>=PointTime && aTimePre<PointTime)
        {
         return(true);
        }
     }
   return(false);
  }

この関数を基に作成されるインディケータがあります(本稿に添付の TimePoint.mq5 ファイルです)。


Pivot インディケータ -オプション2

これである時点を決定する方法を知りました。これから Pivot インディケータを改良します。通常の 00:00の代わりに、一日の始まりを既定の任意時刻とします。それをユーザー定義日と呼ぶことにします。ユーザー定義日開始を判断するため前述の TimeCross() 関数を使用します。異なる週末のバー作成オプションにより、数日を除外する必要があります。いますぐすべての確認ルールを見つけるのは難しいので、一歩ずつこれを行います。重要なことはまず何かを得、進めていく上でオプションを持つことです。検証スクリプト sTestArea.mq5 があります。よって正しいソリューションは実験的に見つけることも可能です。

『週末バーなし』ケースはもっともシンプルです。新しいデイスタートは時刻による既定のタイムスタンプの交点にあります。

日曜日の終わりに数本のバーのみある場合、TimeCross() 関数は関数パラメータが何であれ、デイスタートとして第一の日曜日バーを決めます。週末にはクオートがないと仮定し(日曜日バーは月曜日に属する)、日曜日は無視します。既定時刻が連続する日曜日バーの中ごろのどこかにあてはまれば、すでに新しいデイスタートは金曜日に設定されているため、それも無視します。

継続する週末クオート:ユーザー定義日が歴日の中ごろに当てはまれば(図5)、

図5  歴日の中ごろに始まるユーザー定義日金曜日-赤、土曜日-紫、日曜日-グリーン、月曜日-ブルー
図5  歴日の真ん中で始まるユーザー定義日
金曜日-赤、土曜日-紫、日曜日-グリーン、月曜日-ブルー

土曜日の半分は金曜日として扱われ、日曜日の半分は月曜日として扱われる可能性があります。ただ、土曜日の中ごろから日曜日の中ごろまでどこにも属さないバーがあります。土曜日から日曜日までの間隔を等分し、その半分を金曜日、また半分を月曜日として扱うことはもちろん可能でしょう。週末のクオートがたいして重要でなくても、これはひじょうにシンプルなインディケータをかなり複雑化するかもしれません。

もっとも妥当な解決法は土曜日バーと日曜日バーをすべて金曜日から月曜日まで続くユーザー定義日とみなすことです。これは土曜日と日曜日にスタートするユーザー定義日をスキップするということです。

そこで得られる関数は以下のようなものです。

bool NewCustomDay(int aHour,int aMinute,datetime aTimeCur,datetime aTimePre)
  {
   MqlDateTime stm;
   if(TimeCross(aHour,aMinute,aTimeCur,aTimePre))
     {
      TimeToStruct(aTimeCur,stm);
      if(stm.day_of_week==0 || stm.day_of_week==6)
        {
         return(false);
        }
      else
        {
         return(true);
        }
     }
   return(false);
  }

この関数を基に作成されるインディケータがあります(本稿に添付の Pivot2.mq5 ファイルです)。


週間トレード日の決定

Expert Advisor が特定日にのみトレードを行うようにするのはきわめて簡単です。TimeToStruct() 関数を用いて時刻をコンポーネントに分けます。そして Expert Advisorのパラメータにある各曜日に対してブールタイプの変数を宣言します。曜日に応じて関数は対応する変数値を返します。

これはよち最適な方法で行うことができます。Expert Advisor またはインディケータを初期化するとき、特定の日にトレードを行うようにする、または特定の日にトレードを行わないようにする変数値を配列に書き込みます。それから、曜日に対応する配列エレメントの値をチェックします。ここで関数を2とおり取得します。一つは初期化中に呼ばれ、もう一方は必要に応じて呼ばれます。

変数:

input bool Sunday   =true; // Sunday
input bool Monday   =true; // Monday
input bool Tuesday  =true; // Tuesday 
input bool Wednesday=true; // Wednesday
input bool Thursday =true; // Thursday
input bool Friday   =true; // Friday
input bool Saturday =true; // Saturday

bool WeekDays[7];

初期化関数:

void WeekDays_Init()
  {
   WeekDays[0]=Sunday;
   WeekDays[1]=Monday;
   WeekDays[2]=Tuesday;
   WeekDays[3]=Wednesday;
   WeekDays[4]=Thursday;
   WeekDays[5]=Friday;
   WeekDays[6]=Saturday;
  }

主要関数:

bool WeekDays_Check(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   return(WeekDays[stm.day_of_week]);
  }

この関数を基に作成されるインディケータは本稿末尾に添付(TradeWeekDays.mq5 ファイル)があります。


週間トレード時間の決定

既定のある曜日から別の既定の曜日までのトレードセッションを決める必要があります。この関数は TimeSession() 関数に似ています。唯一異なる点は、週の始まりからの経過時間を基に計算されることです。そこで得られる関数は以下のようなものです。

bool WeekSession(int aStartDay,int aStartHour,int aStartMinute,int aStopDay,int aStopHour,int aStopMinute,datetime aTimeCur)
  {
//--- session start time since the week start
   int StartTime=aStartDay*86400+3600*aStartHour+60*aStartMinute;
//--- session end time since the week start
   int StopTime=aStopDay*86400+3600*aStopHour+60*aStopMinute;
//--- current time in seconds since the week start
   long TimeCur=SecondsFromWeekStart(aTimeCur,false);
   if(StopTime<StartTime)
     {
      //--- passing the turn of the week
      if(TimeCur>=StartTime || TimeCur<StopTime)
        {
         return(true);
        }
     }
   else
     {
      //--- within one week
      if(TimeCur>=StartTime && TimeCur<StopTime)
        {
         return(true);
        }
     }
   return(false);
  }

この関数を基に作成されるインディケータは本稿末尾に添付(TradeWeekDays.mq5 ファイル)があります。

実質的にすべてのもっとも一般的な時間経過タスクについて考察してきました。また、適切なプログラムテクニックとそれを解決する標準的な MQL5 関数を再考しました。


その他の MQL5 関数

時間と連携する MQL5関数はこのほかにもまだあります。:TimeTradeServer()TimeGMT()TimeDaylightSavings()TimeGMTOffset()です。これらの主な特殊性はクロックとユーザーPCの時間設定で使用される点です。

The TimeTradeServer() 関数 TimeCurrent() 関数は週末に誤った時刻(金曜日の最終価格変動時刻)を表示すると本稿で先に述べました。TimeTradeServer() 関数は正しいサーバー時刻を計算します。

datetime tm=TimeTradeServer();
//--- output result
Alert(tm);

TimeGMT() 関数 この関数はクロック値とユーザーPCの時間設定:タイムゾーンおよび夏時間、を基に GMT 時間を計算します。

datetime tm=TimeGMT();
//--- output result
Alert(tm);

より正確には、この関数は UTC 時間を返します。

TimeDaylightSavings() 関数 この関数はユーザーPC設定から夏時間に対し修正値を返します。

int val=TimeDaylightSavings();
//--- output result
Alert(val);

夏時間に対する修正値なしに時刻を取得するには、ローカル時間に修正値を追加する必要があります。

TimeGMTOffset() 関数 この関数によりユーザーPCのタイムゾーンを取得することができます。その値は GMT 時間を取得するためローカル時間に追加するため秒で返されます。

int val=TimeGMTOffset();
//--- output result
Alert(val);

ユーザーPC上の時間は TimeGMT()-TimeGMTOffset()-TimeDaylightSavings()です。

datetime tm1=TimeLocal();
datetime tm2=TimeGMT()-TimeGMTOffset()-TimeDaylightSavings();
//--- output result
Alert(tm1==tm2);


時間に関わるその他便利な関数

うるう年を判断する関数

bool LeapYear(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
//--- a multiple of 4 
   if(stm.year%4==0)
     {
      //--- a multiple of 100
      if(stm.year%100==0)
        {
         //--- a multiple of 400
         if(stm.year%400==0)
           {
            return(true);
           }
        }
      //--- not a multiple of 100 
      else
        {
         return(true);
        }
     }
   return(false);
  }

うるう年を判断する原理は前出「時間測定の特殊性」の項目で述べています。

一ヶ月の日数を判断する関数

int DaysInMonth(datetime aTime)
  {
   MqlDateTime stm;
   TimeToStruct(aTime,stm);
   if(stm.mon==2)
     {
      //--- February
      if(LeapYear(aTime))
        {
         //--- February in a leap year 
         return(29);
        }
      else
        {
         //--- February in a non-leap year 
         return(28);
        }
     }
   else
     {
      //--- other months
      return(31-((stm.mon-1)%7)%2);
     }
  }

この関数はその年がうるう年かどうか確認し、2月に28または29のどちらか適切な値を返し、その他の月についての日数を計算します。最初の7か月の日数は次のように順に入れ替えます。:31、30、31、30など。また、残りの5か月の日数も同様です。よってこの関数は7で割った余りを計算します。奇数パリティチェックを行い、取得される修正値は31から減算されます。


ストラテジーテスタで時間関数を処理する場合の特殊性

ストラテジーテスタは独自のクオートのストリームを作成し、TimeCurrent() 関数の値はストラテジーテスタ内のストリームに対応します。TimeTradeServer() の関数値は TimeCurrent()の値に対応しています。同様に、TimeLocal() の関数値はTimeCurrent()の値に対応しています。ストラテジーテスタの TimeCurrent() 関数はタイムゾーンおよび夏時間修正に配慮しません。Expert Advisors の処理は価格変動を基にしているため、みなさんの Expert Advisor は時間を扱う必要があり、TimeCurrent() 関数を使用します。これによりストラテジーテスタでみなさんの Expert Advisor を安全に検証することが可能となるのです。

関数 TimeGMT()、TimeDaylightSavings()、TimeGMTOffset() のみユーザーPCの現在設定を基に処理を行います(夏時間への移行、夏時間から戻りはストラテジーテスタでシミュレーションされません)。Expert Advisorを検証する際、夏時間への移行および標準時間への戻りをシミュレーションする必要があるなら(ほんとうに必要ならば)、みなさんご自身でこれを行うこととなります。これには完全な分析と、時計を移行する正確な時刻に関する情報が必要となります。

この問題についての解決法は単一の記事の範囲をはるかに越えるものですから、ここでは考察しません。If an Expert Advisor が欧米のセッション時間に動作するなら、ディーリングセンターが夏時間を採用する間、サーバー時刻とイベント時刻間に不一致は生じません。がアジアセッション(日本は夏時間を採用していません。オーストラリアは11月に時計を夏時間に移行します)では状況は異なります。


おわりに

本稿は時間に関わる標準 MQL5 関数をすべて取り上げました。時間関連タスクを処理する際に使用されるプログラムテクニックについても説明しました。また複数のインディケータ、数個の関数作成について、提供される処理原理の詳述を交えて示しました。

時間に関連して動作する標準関数はすべて複数カテゴリーに分類が可能です。

  1. TimeCurrent() および TimeLocal() は現在時刻を判断するのに使用される主要関数です。
  2. TimeToString()StringToTime()TimeToStruct()StructToTime() は時間処理関数です。
  3. CopyTime() はバー時刻に連携する関数です。
  4. TimeTradeServer()TimeGMT()TimeDaylightSavings()TimeGMTOffset() はユーザーのPC設定に依存する関数です。


添付ファイル

  • sTestArea.mq5 -複雑な時間関数を検証するためのスクリプト
  • sTestArea_Pivot1.mq5 Pivot1.mq5インディケータの時間関数を検証するために使用されるsTestArea.mq5 スクリプト
  • Pivot1.mq5 -標準日数(NewDay関数)を使用するピボットインディケータ
  • Session.mq5 -1日のトレードセッションインディケータ(TimeSession関数)
  • TimePoint.mq5 -既定時点のインディケータ(TimeCross 関数)
  • Pivot2.mq5 -ユーザー定義日(NewCustomDayy関数)を使用するピボットインディケータ
  • TradeWeekDays.mq5 -トレード曜日のインディケータ(WeekDays_Check関数)
  • SessionWeek.mq5 1週間のトレードセッションインディケータ(WeekSession 関数)
  • TimeFunctions.mqh -本稿で提供されたすべての関数。単一ファイル内。

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

添付されたファイル |
stestarea.mq5 (4.96 KB)
pivot1.mq5 (4.5 KB)
session.mq5 (3.51 KB)
timepoint.mq5 (3.32 KB)
pivot2.mq5 (4.14 KB)
tradeweekdays.mq5 (3.59 KB)
sessionweek.mq5 (5.08 KB)
timefuncions.mqh (20.52 KB)
インディケータエミッションの積分特性計算 インディケータエミッションの積分特性計算
インディケータエミッションはマーケットリサーチでほとんど研究されていない分野です。時間依存性データのひじょうに大きな配列を処理することで起こる分析の難しさがその主な理由です。既存のグラフ分析は資源集約的に過ぎ、そのためエミッションの時系列を利用する簡素なアルゴリズムの開発をもたらしました。本稿では視覚的(直観的イメージ)分析がどのようにエミッションの積分特性の研究に置き換えることができるのかを示します。トレーダー、自動売買システムの開発者双方に興味深いものとなることでしょう。
MetaTrader 4 および MetaTrader 5 用トレードシグナルについての一般情報 MetaTrader 4 および MetaTrader 5 用トレードシグナルについての一般情報
MetaTrader 4 / MetaTrader 5 トレードシグナルによりトレーダーにシグナル提供者のトレード処理のコピーが可能となります。われわれの目標は定期購買者を保護し不必要なコストから解放する新しい大規模に利用されるサービスの開発でした。
MetaTrader 4とMetaTrader 5のシグナルプロバイダーになる方法 MetaTrader 4とMetaTrader 5のシグナルプロバイダーになる方法
取引シグナルを提供して収入を得たいですか?MQL5.comのWebサイトに販売者として登録し、取引口座を指定し、トレーダーがあなたの取引に購読してコピーできるようにできます。
MetaTrader4とMetaTrader5のトレーディングシグナル用ウィジェット MetaTrader4とMetaTrader5のトレーディングシグナル用ウィジェット
MetaTrader4とMetaTrader5ユーザーがシグナル提供者になり、さらなる利益を生む機会を得ることができるようになりました。新しいウィジェットを用いて、あなたのサイトやブログ、SNSページにトレーディング実績を掲載できます。ウィジェットを用いる利点は明確です;シグナルプロバイダーの人気を向上し、成功したトレーダーとしての評判を築くのみでなく、新しい購読者を惹きつけます。その他のサイトにウィジェットを載せているトレーダーはこれらの利益を享受できます。