MQL5クックブック - サービス
はじめに
最近、MetaTrader 5には「サービス」と呼ばれる新しいプログラムタイプができました。その開発者によると、サービスを使用するとユーザーはターミナル用のカスタム価格フィードを作成できます。つまり、証券会社の取引サーバーに実装されているのと同じように、外部システムからリアルタイムの価格配信を実装できます。サービスの特徴はこれだけではありません。
この記事では、サービスで作業する際の微妙な違いについて考察します。この記事は主に初心者向けです。そのため、コードを完全に再現できるようにして、例ごとに複雑さを増すようにしてみました。
1.動作中のデーモン
MQL 5のサービスは、Windowsサービスと類似しています。ウィキペディアでは、サービスの次の定義を示しています。
私たちの場合、サービスの外部環境はオペレーティングシステム自体ではなく、MetaTrader 5ターミナルシェルです。
デーモンについて一言。
デーモンはインタラクティブなユーザーが直接制御するのではなく、バックグラウンドプロセスとして実行されるコンピュータプログラムです。
この用語は、MITのProject MACのプログラマーによって造られました。1963年にProject MACに参加していたFernando J. Corbató氏によると、物理学と熱力学の架空のエージェントで分子を分類するのに役立つという「マクスウェルの悪魔」にインスパイアされて、最初にデーモンという用語を使用したのは彼のチームです。UNIXシステムはこの用語を継承しています。
マクスウェルの悪魔は、バックグラウンドで働く超自然的な存在としてのデーモンのギリシア神話の解釈と一致しています。UNIX System Administration Handbookで述べられているように、古代ギリシャ人の「個人的なデーモン」の概念は、現代の「守護天使」の概念に似ていました。
古代ギリシャ人はコンピューターを持っていませんでしたが、エンティティの関係は彼らにとって明らかでした。
2.サービス – ドキュメント情報
このトピックについて詳しく説明する前に、ドキュメンテーション資料に目を通して、その開発者がサービスの機能をどのように説明しているかを確認することをお勧めします。
2.1 アプリケーションの種類
ドキュメントの最初のページ、つまり「MQL5アプリケーションの種類」セクションでは、サービスはMQL5プログラムの一種として定義されています。
- サービスは、指標、エキスパートアドバイザー(EA)、スクリプトとは異なり、機能するためにチャートに結合する必要がないプログラムです。スクリプトと同様、サービスはトリガー以外のイベントを処理しません。サービスを起動するには、そのコードにOnStartハンドラ関数を含める必要があります。サービスはStart以外のイベントを受け入れませんが、EventChartCustomを使用してカスタムイベントをチャートに送信することができます。サービスは <ターミナルディレクトリ>\MQL5\Servicesに保存されます。
ここで、サービスはスクリプトに非常に似ていることに注意してください。基本的な違いは、サービスはどのチャートにも関連付けられていないことです。
2.2 プログラムの実行
プログラムの実行セクションでは、MQL5のプログラムの概要を提供します。
プログラム | 実行 | メモ |
---|---|---|
ツール | 自身のスレッド内、サービスと同じ数の実行スレッドが存在 | ループされたサービスは他のプログラムの実行を中断できない |
スクリプト | 自身のスレッド内、スクリプトと同じ数の実行スレッドが存在 | ループされたスクリプトは他のプログラムの実行を中断できない |
EA | 自身のスレッド内、EAと同じ数の実行スレッドが存在 | ループされたEAは他のプログラムの実行を中断できない |
指標 | 銘柄上のすべての指標に対して1つのスレッドスレッドの数は、指標付きの銘柄の数と同じ | 1つの指標の無限ループは同銘柄の他のすべての指標を停止 |
つまり、実行フローをアクティブ化する方法に関しては、サービスとスクリプトやEAとの違いはありません。また、サービスは、ループされたコードブロックの存在が他のMQL5プログラムの動作に影響を与えないという点で、スクリプトやEAに似ています。
2.3 サービスで使用できない関数
使用が許可されていない関数の完全なリストは開発者によって提供されています。
- ExpertRemove()
- EventSetMillisecondTimer()
- EventSetTimer()
- EventKillTimer()
- SetIndexBuffer()
- IndicatorSetDouble()
- IndicatorSetInteger()
- IndicatorSetString()
- PlotIndexSetDouble()
- PlotIndexSetInteger()
- PlotIndexSetString()
- PlotIndexGetInteger
サービスは単一のStartイベントを処理するため、EAを停止してタイマーを操作できないのでこれは合理的です。また、カスタム指標の関数を使用することもできません。
2.4 サービスのロードとアンロード
適切なドキュメンテーションセクションには、いくつかの重要なポイントが含まれています。それぞれについて考えてみましょう。
ターミナルのシャットダウン時にサービスが起動された場合、サービスはターミナルの起動直後にロードされます。サービスは、作業が完了するとすぐにアンロードされます。
これは、サービスの顕著な特性の1つです。サービスは監視されるべきではありません。一度起動すると、自動的にタスクを実行します。
サービスには単一のOnStart()ハンドラがあり、ここではネットワーク関数を使用してカスタム銘柄を作成および更新するなど、無限のデータ受信および処理ループを実装できます。
ここで、簡単な結論を導き出すことができます。サービスが一連の1回限りのアクションを実行する必要がある場合、コードのブロックをループする必要はありません。タスクにサービスの継続的または定期的な操作が含まれる場合は、コードブロックをループでラップする必要があります。このようなタスクの例については後で検討します。
EA、指標、スクリプトとは異なり、サービスは特定のチャートに結合されていないため、それらを起動するための別のメカニズムが提供されています。
おそらく、これはサービスの2つ目の注目すべき特徴です。動作するのにスケジュールは必要ありません。
[サービスの追加]コマンドを使用して、ナビゲータから新しいサービスインスタンスを作成します。適切なインスタンスメニューを使用して、サービスインスタンスを起動、停止、および削除できます。すべてのインスタンスを管理するには、サービスメニューを使用します。
これは、サービスの3つ目の注目すべき特性です。プログラムファイルが1つしかないため、同時に複数のインスタンスを実行できます。これは通常、異なるパラメータ(入力変数)を使用する必要がある場合におこなわれます。
3. サービスプロトタイプ
サービスの開始と管理のメカニズムは、F1を押して開くことができるターミナルのヘルプで説明されているので、ここでは省きます。
MetaEditorで、サービステンプレートを作成し、dEmpty.mq5という名前を付けます。
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { //--- } //+------------------------------------------------------------------+
コンパイル後、ナビゲータにサービス名が表示されます(図1)。
図1:ナビゲータサブウィンドウのdEmptyサービス
ナビゲータサブウィンドウでdEmptyサービスを追加して起動すると、操作ログに次のエントリが表示されます。
CS 0 19:54:18.590 Services service 'dEmpty' started CS 0 19:54:18.592 Services service 'dEmpty' stopped
ログは、サービスが開始および停止されたことを示しています。 そのコードにはコマンドが含まれていないため、ターミナルに変更はありません。サービス開始後は特に気付くことはありません。
サービステンプレートにいくつかのコマンドを入力してみましょう。dStart.mq5サービスを作成し、次の行を記述します。
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
サービスを開始すると、[エキスパート]タブに次のエントリが表示されます。
CS 0 20:04:28.347 dStart Service "dStart" starts at: 2022.11.30 20:04:28.
dStartサービスはその開始を通知してから停止しています。
以前のサービスの機能を拡張して、新しいサービスにdStartStop.mq5という名前を付けましょう。
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); ::Sleep(1000); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
現在のサービスは、その開始だけでなく、活動の停止もすでに通知しています。
操作ログでサービスを開始すると、次のエントリが表示されます。
2022.12.01 22:49:10.324 dStartStop Service "dStartStop" starts at: 2022.12.01 22:49:10 2022.12.01 22:49:11.336 dStartStop Service "dStartStop" stops at: 2022.12.01 22:49:11
1回目と2回目の時間が1秒違うことは容易にわかります。最初と最後のコマンドの間でSleep()ネイティブ関数がトリガーされています。
次に、現在のサービスの機能を拡張して、強制的に停止されるまで実行できるようにしましょう。 新しいサービスにdStandBy.mq5という名前を付けましょう。
//+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); do { ::Sleep(1); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name,::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- final goodbye for(ushort cnt=0; cnt<5; cnt++) { ::PrintFormat("Count: %hu", cnt+1); ::Sleep(10000); } } //+------------------------------------------------------------------+
プログラムの停止によってdo whileループを終章した後も、サービスはいくつかのカウンタ値をログに書き込みます。このような各エントリの後、Sleep()が10秒の遅延間隔で呼び出されます。
操作ログには以下のレコードが入っています。
CS 0 23:20:44.478 dStandBy Service "dStandBy" starts at: 2022.12.01 23:20:44 CS 0 23:20:51.144 dStandBy Service "dStandBy" stops at: 2022.12.01 23:20:51 CS 0 23:20:51.144 dStandBy Count: 1 CS 0 23:20:51.159 dStandBy Count: 2 CS 0 23:20:51.175 dStandBy Count: 3 CS 0 23:20:51.191 dStandBy Count: 4 CS 0 23:20:51.207 dStandBy Count: 5
サービスは23:20:44に開始され、23:20:51に強制停止されました。また、カウンタ値の間隔が0.02秒を超えていないことも簡単にわかります。以前は、このような間隔に対して10秒の遅延が設定されていました。
Sleep()関数に関するドキュメントには次が書かれています。
注意
カスタム指標からSleep()関数を呼び出すことはできません。これは、指標がインターフェイススレッドで実行され、速度が低下してはならないためです。この関数には、0.1秒ごとにEA停止フラグの状態を確認する機能が組み込まれています。
したがって、私たちの場合、Sleep()関数は、サービスが強制的に停止されたことをすぐに検出し、mql5プログラムの実行を遅らせました。
完全を期すために、IsStopped()ステータス確認関数の戻り値についてドキュメントに記載されている内容を見てみましょう。
戻り値
_StopFlagシステム変数に0以外の値が含まれている場合、trueを返します。MQL5プログラムがその操作を完了するように命令された場合、ゼロ以外の値が_StopFlagに書き込まれます。この場合、すぐにプログラムを終了する必要があります。そうしないと、プログラムは3秒後に外部から強制的に終了されます。
したがって、強制停止後、サービスが完全に非アクティブ化される前に、サービスが別の処理をおこなうために3秒が与えられます。この瞬間を実際に確認してみましょう。先ほどのサービスのコードのループ後に行列演算を追加してみましょう。計算には約1分かかります。サービスが強制的に停止された後、サービスがすべてを計算する時間があるかどうかを確認します。新しいサービスにsrvcStandByMatrixMult.mq5という名前を付けましょう。
カウンタ値を計算するループの後、前のコードに次のブロックを追加する必要があります。
//--- Matrix mult //--- matrix A 1000x2000 int rows_a=1000; int cols_a=2000; //--- matrix B 2000x1000 int rows_b=cols_a; int cols_b=1000; //--- matrix C 1000x1000 int rows_c=rows_a; int cols_c=cols_b; //--- matrix A: size=rows_a*cols_a int size_a=rows_a*cols_a; int size_b=rows_b*cols_b; int size_c=rows_c*cols_c; //--- prepare matrix A double matrix_a[]; ::ArrayResize(matrix_a, rows_a*cols_a); for(int i=0; i<rows_a; i++) for(int j=0; j<cols_a; j++) matrix_a[i*cols_a+j]=(double)(10*::MathRand()/32767); //--- prepare matrix B double matrix_b[]; ::ArrayResize(matrix_b, rows_b*cols_b); for(int i=0; i<rows_b; i++) for(int j=0; j<cols_b; j++) matrix_b[i*cols_b+j]=(double)(10*::MathRand()/32767); //--- CPU: calculate matrix product matrix_a*matrix_b double matrix_c_cpu[]; ulong time_cpu=0; if(!MatrixMult_CPU(matrix_a, matrix_b, matrix_c_cpu, rows_a, cols_a, cols_b, time_cpu)) { ::PrintFormat("Error in calculation on CPU. Error code=%d", ::GetLastError()); return; } ::PrintFormat("time CPU=%d ms", time_cpu);
dStandByMatrixMultサービスを起動し、数秒後に強制停止します。ログに次の行が表示されます。
CS 0 15:17:23.493 dStandByMatrixMult Service "dStandByMatrixMult" starts at: 2022.12.02 15:17:23 CS 0 15:18:17.282 dStandByMatrixMult Service "dStandByMatrixMult" stops at: 2022.12.02 15:18:17 CS 0 15:18:17.282 dStandByMatrixMult Count: 1 CS 0 15:18:17.297 dStandByMatrixMult Count: 2 CS 0 15:18:17.313 dStandByMatrixMult Count: 3 CS 0 15:18:17.328 dStandByMatrixMult Count: 4 CS 0 15:18:17.344 dStandByMatrixMult Count: 5 CS 2 15:18:19.771 dStandByMatrixMult Abnormal termination
ご覧のとおり、mql5プログラムの実行を終了するコマンドは15:18:17.282に到着しました。15:18:19.771にサービス自体が強制終了されました。実際、終了の瞬間からサービスの強制停止までに2.489秒が経過しました。サービスが強制的に停止され、さらに緊急終了した結果は「Abnormal termination」エントリで示されています。
サービスを強制的に停止する(_StopFlag==true)までに3秒しか残っていないため、中断されたループで重要な計算や取引アクションをおこなうことはお勧めしません。
シンプルな例があります。ターミナル自体が閉じられたときにすべてのポジションを決済するサービスがターミナルにあるとします。ターミナルが閉じ、サービスはすべてのアクティブなポジションを清算しようとします。その結果、ターミナルは閉じられ、一部のポジションは開いたままになりますが、私たちはそれを認識していません。
4. 使用例
実際の例に進む前に、取引ターミナルサービスで何ができるかを説明することを提案します。サービスにはほぼすべてのコードを導入できる(禁止されているものを除く)一方、取引ターミナル環境では、権限を区切り、サービスに独自のニッチを与えることにおそらく価値があります。
まず、サービスは他のアクティブなMQL5プログラム(EA、指標、スクリプト)の作業を重複してはなりません。取引セッションの終了時にシグナルによって指値注文を出すEAがあり、これらの指値注文を出すサービスもあるとします。その結果、EA自体のシステムアカウンティングリミット注文が中断される可能性や、マジックナンバーが異なる場合、EAがサービスによって出された注文を見失う可能性があります。
第二に、反対の状況、つまり他のMQL5プログラムとのサービスの衝突を避ける必要があります。取引セッションの終了時にシグナルによって指値注文を出すEAがあり、取引日の終わりにすべてのポジションが決済され、未決注文が削除されるように制御するサービスがあるとします。 ここには利益相反があります。EAが注文を出すと、サービスがすぐに注文を削除します。これはすべて、取引サーバーに対する一種のDDoS攻撃で終わる可能性があります。
一般に、サービスは取引ターミナルの操作に調和して統合されるべきです。取引アルゴリズムをより効率的に使用するために、サービスは、MQL 5プログラムに干渉する代わりにそれらと対話するべきです。
4.1 ログの消去
新しい取引日の開始時に、過去(昨日、一昨日など)に1つ以上のEAによって生成されたログ(操作ログ)のフォルダを消去するタスクがサービスに課されているとします。
ここで必要なツールは何でしょうか。ファイル操作と新しいバーの定義が必要になります。 新しいバーを検出するクラスの詳細については、「新しいバー」イベントハンドラの記事をご覧ください。
次に、ファイル操作を扱いましょう。 ファイルサンドボックスの制限に遭遇するため、ネイティブファイル操作はここでは機能しません。ドキュメントには次が書かれています。
セキュリティ上の理由から、MQL5言語ではファイルの操作が厳密に制御されています。MQL5言語によるファイル操作で使用されるファイルは、ファイルサンドボックスの外に配置することはできません。
MQL5プログラムによってディスクに書き込まれたログファイルは、%MQL5\Logsにあります。幸いなことに、ファイル操作機能を備えたWinAPIを使用できます。
WinAPIは次のディレクティブを使用してインクルードされます。
#include <WinAPI\winapi.mqh>
WinAPIファイルで次の8つの関数を使用します。
- FindFirstFileW()
- FindNextFileW()
- CopyFileW()
- GetFileAttributesW()
- SetFileAttributesW()
- DeleteFileW()
- FindClose()
- GetLastError()
最初の関数は、指定されたフォルダで、指定された名前の最初のファイルを検索します。マスクを名前として代用することが可能です。したがって、フォルダ内のログファイルを見つけるには、名前として「.log」文字列を指定するだけで十分です。
2番目の関数は、最初の関数によって開始された検索を続行します。
3番目の関数は、既存のファイルを新しいファイルにコピーします。
4番目の関数は、指定されたファイルまたはディレクトリのファイルシステム属性を取得します。
5番目の関数は、そのような属性を設定します。
6番目の関数は、指定された名前のファイルを削除します。
7番目の関数は、ファイル検索ハンドルを閉じます。
8番目の関数は、最後のエラーコード値を取得します。
dClearTradeLogs.mq5サービスコードを見てみましょう。
//--- include #include <WinAPI\winapi.mqh> #include "Include\CisNewBar.mqh" //--- defines #define ERROR_FILE_NOT_FOUND 0x2 #define ERROR_NO_MORE_FILES 0x12 #define INVALID_FILE_ATTRIBUTES 0xFFFFFFFF #define FILE_ATTRIBUTE_READONLY 0x1 #define FILE_ATTRIBUTE_DIRECTORY 0x10 #define FILE_ATTRIBUTE_ARCHIVE 0x20 //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input string InpDstPath="G:" ; // Destination drive //+------------------------------------------------------------------+ //| Service program start function | //+------------------------------------------------------------------+ void OnStart() { string program_name=::MQLInfoString(MQL_PROGRAM_NAME); datetime now=::TimeLocal(); ::PrintFormat("Service \"%s\" starts at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); //--- new bar CisNewBar daily_new_bar; daily_new_bar.SetPeriod(PERIOD_D1); daily_new_bar.SetLastBarTime(1); //--- logs path string logs_path=::TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Logs\\"; string mask_path=logs_path+"*.log"; //--- destination folder (if to copy files) string new_folder_name=NULL; uint file_attributes=0; if(::StringLen(InpDstPath)>0) { new_folder_name=InpDstPath+"\\Logs"; //--- check whether a folder exists file_attributes=kernel32::GetFileAttributesW(new_folder_name); bool does_folder_exist=(file_attributes != INVALID_FILE_ATTRIBUTES) && ((file_attributes & FILE_ATTRIBUTE_DIRECTORY) != 0); if(!does_folder_exist) { //--- create a folder int create_res=kernel32::CreateDirectoryW(new_folder_name, 0); if(create_res<1) { ::PrintFormat("Failed CreateDirectoryW() with error: %x", kernel32::GetLastError()); return; } } } //--- main processing loop do { MqlDateTime sToday; ::TimeTradeServer(sToday); sToday.hour=sToday.min=sToday.sec=0; datetime dtToday=::StructToTime(sToday); if(daily_new_bar.isNewBar(dtToday)) { ::PrintFormat("\nToday is: %s", ::TimeToString(dtToday, TIME_DATE)); string todays_log_file_name=::TimeToString(dtToday, TIME_DATE); int replaced=::StringReplace(todays_log_file_name, ".", ""); if(replaced>0) { todays_log_file_name+=".log"; //--- log files FIND_DATAW find_file_data; ::ZeroMemory(find_file_data); HANDLE hFind=kernel32::FindFirstFileW(mask_path, find_file_data); if(hFind==INVALID_HANDLE) { ::PrintFormat("Failed FindFirstFile (hFind) with error: %x", kernel32::GetLastError()); continue; } // List all the files in the directory with some info about them int result=0; uint files_cnt=0; do { string name=""; for(int i=0; i<MAX_PATH; i++) name+=::ShortToString(find_file_data.cFileName[i]); //--- delete any file except today's if(::StringCompare(name, todays_log_file_name)) { string file_name=logs_path+name; //--- if to copy a file before deletion if(::StringLen(new_folder_name)>0) { string new_file_name=new_folder_name+"\\"+name; if(kernel32::CopyFileW(file_name, new_file_name, 0)==0) { ::PrintFormat("Failed CopyFileW() with error: %x", kernel32::GetLastError()); } //--- set READONLY attribute file_attributes=kernel32::GetFileAttributesW(new_file_name); if(file_attributes!=INVALID_FILE_ATTRIBUTES) if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) { file_attributes=kernel32::SetFileAttributesW(new_file_name, file_attributes|FILE_ATTRIBUTE_READONLY); if(!(file_attributes & FILE_ATTRIBUTE_READONLY)) ::PrintFormat("Failed SetFileAttributesW() with error: %x", kernel32::GetLastError()); } } int del_ret=kernel32::DeleteFileW(file_name); if(del_ret>0) files_cnt++; } //--- next file ::ZeroMemory(find_file_data); result= kernel32::FindNextFileW(hFind, find_file_data); } while(result!=0); uint kernel32_last_error=kernel32::GetLastError(); if(kernel32_last_error>0) if(kernel32_last_error!=ERROR_NO_MORE_FILES) ::PrintFormat("Failed FindNextFileW (hFind) with error: %x", kernel32_last_error); ::PrintFormat("Deleted log files: %I32u", files_cnt); int file_close=kernel32::FindClose(hFind); } } ::Sleep(15000); } while(!::IsStopped()); now=::TimeLocal(); ::PrintFormat("Service \"%s\" stops at: %s", program_name, ::TimeToString(now, TIME_DATE|TIME_SECONDS)); } //+------------------------------------------------------------------+
入力変数がファイルのコピー先ディスクを指定する場合、このフォルダの存在を確認した後で、ログファイルを格納するフォルダを作成します。
メイン処理ループでは、 新しい日が来たかどうかを確認します。それからまたループで、現在の日のフラグをスキップしながら、ログファイルを見つけて削除します。ファイルをコピーする必要がある場合は、この可能性を確認し、コピー後に新しいファイルの「読み取り専用」属性を設定します。
ループでは、15秒の一時停止を設定します。これはおそらく、新しい日を決定するための比較的最適な頻度です。
サービスを開始する前は、%MQL5\Logsフォルダはエクスプローラで次のように表示されていました(図2)。
図2:ファイルを削除する前の「%MQL5\Logs」エクスプローラフォルダ
サービスを起動すると、次のメッセージがログに表示されます。
2022.12.05 23:26:59.960 dClearTradeLogs Service "dClearTradeLogs" starts at: 2022.12.05 23:26:59 2022.12.05 23:26:59.960 dClearTradeLogs 2022.12.05 23:26:59.960 dClearTradeLogs Today is: 2022.12.05 2022.12.05 23:26:59.985 dClearTradeLogs Deleted log files: 6
サービスが作業の終了に関してログに何も書き込んでいないことは簡単にわかります。その理由は、サービスの操作がまだ終わっていないからです。中断されるまでループして実行されます。
図3:ファイルを削除した後の「%MQL5\Logs」エクスプローラフォルダ
ログを削除すると、指定したフォルダにはファイルが1つだけ残ります(図3)。当然、ファイルを削除するタスクは、改善してより柔軟にすることができます。たとえば、ファイルを削除する前に、それらを別のディスクにコピーして、必要な情報を永久に失わないようにすることができます。一般に、実装はすでにアルゴリズムの特定の要件に依存しています。現在の例では、ファイルはG:\Logsフォルダにコピーされました(図4)。
図4:ファイルをコピーした後の「G:\Logs」エクスプローラフォルダ
これで、クラスコードでの作業は終了です。次の例では、チャートを表示するタスクをサービスに割り当てましょう。
4.2 チャートの管理
次のタスクに直面していると想像してみましょう。ターミナルには、現在取引されている銘柄、つまりポジションを特徴とする銘柄のチャートが表示されます。
チャートを開くためのルールは非常にシンプルです。銘柄の1つにポジションがある場合は、この銘柄のチャートを開きます。ポジションがない場合、チャートは開かれません。1つの銘柄に複数のポジションがある場合、1つのチャートのみが開かれます。
色もいくつか追加しましょう。チャートの背景色は、ポジションが利益を出している場合は水色になり、損失の場合は薄ピンクになります。ゼロ利益ではラベンダー色を使用します。
このタスクを実行するには、まずサービスコードにループが必要です。このループでは、ポジションとチャートのステータスを監視します。ループは十分に大きくなりました。そのコードをブロックごとに分析してみましょう。
サイクルは2つのブロックに分かれています。
最初のブロックは、ポジションがない場合の状況を処理しています。
int positions_num=::PositionsTotal(); //--- if there are no positions if(positions_num<1) { // close all the charts CChart temp_chart_obj; temp_chart_obj.FirstChart(); long temp_ch_id=temp_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && temp_ch_id>-1; ch_idx++) { long ch_id_to_close=temp_ch_id; temp_chart_obj.NextChart(); temp_ch_id=temp_chart_obj.ChartId(); ::ChartClose(ch_id_to_close); } }
このブロックでは、開いているチャート(存在する場合)を調べて閉じます。以下では、価格チャートのプロパティを処理するためにCChartクラスを使用します。
2番目のブロックはより複雑です。
//--- if there are some positions else { //--- collect unique position symbols CHashSet<string> pos_symbols_set; for(int pos_idx=0; pos_idx<positions_num; pos_idx++) { string curr_pos_symbol=::PositionGetSymbol(pos_idx); if(!pos_symbols_set.Contains(curr_pos_symbol)) { if(!pos_symbols_set.Add(curr_pos_symbol)) ::PrintFormat("Failed to add a symbol \"%s\" to the positions set!", curr_pos_symbol); } } string pos_symbols_arr[]; int unique_pos_symbols_num=pos_symbols_set.Count(); if(pos_symbols_set.CopyTo(pos_symbols_arr)!=unique_pos_symbols_num) continue; //--- collect unique chart symbols and close duplicates CHashMap<string, long> ch_symbols_map; CChart map_chart_obj; map_chart_obj.FirstChart(); long map_ch_id=map_chart_obj.ChartId(); for(int ch_idx=0; ch_idx<MAX_CHARTS && map_ch_id>-1; ch_idx++) { string curr_ch_symbol=map_chart_obj.Symbol(); long ch_id_to_close=0; if(!ch_symbols_map.ContainsKey(curr_ch_symbol)) { if(!ch_symbols_map.Add(curr_ch_symbol, map_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_ch_symbol); } else { //--- if there's a duplicate ch_id_to_close=map_chart_obj.ChartId(); } //--- move to the next chart map_chart_obj.NextChart(); map_ch_id=map_chart_obj.ChartId(); if(ch_id_to_close>0) { ::ChartClose(ch_id_to_close); } } map_chart_obj.Detach(); //--- looking for a chart if there's a position for(int s_pos_idx=0; s_pos_idx<unique_pos_symbols_num; s_pos_idx++) { string curr_pos_symbol=pos_symbols_arr[s_pos_idx]; //--- if there's no chart of the symbol if(!ch_symbols_map.ContainsKey(curr_pos_symbol)) if(::SymbolSelect(curr_pos_symbol, true)) { //--- open a chart of the symbol CChart temp_chart_obj; long temp_ch_id=temp_chart_obj.Open(curr_pos_symbol, PERIOD_H1); if(temp_ch_id<1) ::PrintFormat("Failed to open a chart of the symbol \"%s\"!", curr_pos_symbol); else { if(!ch_symbols_map.Add(curr_pos_symbol, temp_ch_id)) ::PrintFormat("Failed to add a symbol \"%s\" to the charts map!", curr_pos_symbol); temp_chart_obj.Detach(); } } } string ch_symbols_arr[]; long ch_ids_arr[]; int unique_ch_symbols_num=ch_symbols_map.Count(); if(ch_symbols_map.CopyTo(ch_symbols_arr, ch_ids_arr)!=unique_ch_symbols_num) continue; //--- looking for a position if there's a chart for(int s_ch_idx=0; s_ch_idx<unique_ch_symbols_num; s_ch_idx++) { string curr_ch_symbol=ch_symbols_arr[s_ch_idx]; long ch_id_to_close=ch_ids_arr[s_ch_idx]; CChart temp_chart_obj; temp_chart_obj.Attach(ch_id_to_close); //--- if there's no position of the symbol if(!pos_symbols_set.Contains(curr_ch_symbol)) { temp_chart_obj.Close(); } else { CPositionInfo curr_pos_info; //--- calculate a position profit double curr_pos_profit=0.; int pos_num=::PositionsTotal(); for(int pos_idx=0; pos_idx<pos_num; pos_idx++) if(curr_pos_info.SelectByIndex(pos_idx)) { string curr_pos_symbol=curr_pos_info.Symbol(); if(!::StringCompare(curr_ch_symbol, curr_pos_symbol)) curr_pos_profit+=curr_pos_info.Profit()+curr_pos_info.Swap(); } //--- apply a color color profit_clr=clrLavender; if(curr_pos_profit>0.) { profit_clr=clrLightSkyBlue; } else if(curr_pos_profit<0.) { profit_clr=clrLightPink; } if(!temp_chart_obj.ColorBackground(profit_clr)) ::PrintFormat("Failed to apply a profit color for the symbol \"%s\"!", curr_ch_symbol); temp_chart_obj.Redraw(); } temp_chart_obj.Detach(); } //--- tile windows (Alt+R) uchar vk=VK_MENU; uchar scan=0; uint flags[]= {0, KEYEVENTF_KEYUP}; ulong extra_info=0; uchar Key='R'; for(int r_idx=0; r_idx<2; r_idx++) { user32::keybd_event(vk, scan, flags[r_idx], extra_info); ::Sleep(10); user32::keybd_event(Key, scan, flags[r_idx], extra_info); } }
まず、ポジションが開いている銘柄の一意の値を収集します。このタスクにはCHashSet<T>クラスの機能が適しています。このクラスは、型Tの順序付けられていない動的データセットの実装であり、各値の一意性が必要です。取得した一意の値を文字列配列にコピーして、後で簡単にアクセスできるようにします。
次の段階で、ユニークな銘柄値を収集してチャートを開きます。重複するチャートがある場合は途中で閉じます。2つのEURUSDチャートがあるとします。これは、1つのチャートを残し、別のチャートを閉じることを意味します。ここでは、CHashMap<TKey,TValue>クラスのインスタンスが適用されます。このクラスは動的ハッシュテーブルの実装であり、そのデータは順序付けされていないキーと値のペアとして格納され、キーの一意性が要求されます。
残るループは2つだけです。最初のループでは、ポジション銘柄の配列に沿って移動し、そのチャートがあるかどうかを確認します。そうでない場合は、開きます。2番目のループでは、開かれているチャートの銘柄の配列に沿って移動し、ポジションが各銘柄に対応するかどうかを確認します。USDJPYチャートが開かれているが、ポジションがないとします。その場合、USDJPYはクローズされます。同じループで、タスクの開始時に決定された背景色を設定するために、ポジションの利益を計算します。ポジションプロパティにアクセスしてその値を取得するには、標準ライブラリのCPositionInfoクラスを使用しました。
最後に、チャートウィンドウをタイルとして配置して視覚的な魅力を加えましょう。これを実現するには、WinAPI、つまりキーストロークをシミュレートするkeybd_event()関数を使用します。
それだけです。後はdActivePositionsChartsサービスを起動するだけです。
4.3 カスタム銘柄とクウォート
サービスの利点の1つは、価格チャートを使用せずにバックグラウンドで作業できることです。例として、このセクションでは、サービスを使用してカスタム銘柄とそのティック履歴を作成し、新しいティックを生成する方法を示します。
をカスタム銘柄としてドルインデックス使用します。
4.3.1 ドルインデックスと構成
ドルインデックスは、他の6つの通貨のバスケットに対する米ドルの価値を反映する合成指数です。
- EUR (57.6%)
- JPY (13.6%)
- GBP (11.9%)
- CAD (9.1%)
- SEK (4.2%)
- CHF (3.6%)
指数の式は、これらの通貨に対する米ドルの為替レートの幾何加重平均であり、補正係数が適用されます。
USDX = 50.14348112 * EURUSD-0.576 * USDJPY0.136 * GBPUSD-0.119 * USDCAD0.091 * USDSEK0.042 * USDCHF0.036
式に基づいて、クウォートのUSDが建値通貨である場合、ペアのクウォートが負の累乗に引き上げられ、クウォートのUSDが基軸通貨である場合、ペアのクウォートが正の累乗に引き上げられたとします。.
通貨のバスケットは、次のように概略的に表示できます(図5)。
図5.ドルインデックス(DXY)の通貨バスケット
ドルインデックスは、インターコンチネンタル取引所(ICE)で取引される先物の原資産です。株価指数先物は、約15秒ごとに計算されます。計算のための価格は、指数に含まれる通貨ペアの板情報における最高の買値と最低の売値で取得されます。
4.3.2 ドルインデックス、サービス
計算に必要なものはすべて揃ったので、サービスコードを開始します。ただし、最初に、このサービスは段階的に機能することに注意してください。最初の段階では、合成指数のティックとバーの履歴を形成し、2番目の段階では、新しいティックを処理します。明らかに、最初の段階は過去に関連しており、2番目の段階は現在に関連しています。
dDxySymbol.mq5という名前のMQL5プログラム(サービス)テンプレートを作成しましょう。
以下を入力変数として定義します。
input datetime InpStartDay=D'01.10.2022'; // Start date input int InpDxyDigits=3; // Index digits
最初のものは、銘柄を作成するために取得しようとするクウォートの履歴の始まりを定義します。 つまり、2022年10月1日以降のクウォート履歴をダウンロードすることになります。
2つ目は、銘柄のクウォート精度を設定します。
したがって、指数の操作を開始するには、合成クウォートを表示するためのベースとなるカスタム銘柄を作成する必要があります。DXYは指数銘柄の名前です。このリソースには、カスタム銘柄に関する多くの資料があります。「MQL5クックブック – カスタム銘柄を使用した取引ストラテジーストレステスト」稿で定義されているCiCustomSymbolクラスについて説明します。
以下は、DXY合成指数の作成作業が進行中のコードブロックです。
//--- create a custom symbol string custom_symbol="DXY", custom_group="Dollar Index"; CiCustomSymbol custom_symbol_obj; const uint batch_size = 1e6; const bool is_selected = true; int code = custom_symbol_obj.Create(custom_symbol, custom_group, NULL, batch_size, is_selected); ::PrintFormat("Custom symbol \"%s\", code: %d", custom_symbol, code); if(code < 0) return;
DXY銘柄が以前に作成されておらず、カスタムターミナル銘柄のリストにない場合、CiCustomSymbol::Create()メソッドはコード1を返します。DXY銘柄が既に銘柄の中にある場合はコード0になります。銘柄を作成できない場合は、コード-1でエラーが発生します。カスタム銘柄の作成中にエラーが発生した場合、サービスは動作を停止します。
合成指数を作成した後、いくつかのプロパティを設定します。
//--- Integer properties //--- sector ENUM_SYMBOL_SECTOR symbol_sector = SECTOR_CURRENCY; if(!custom_symbol_obj.SetProperty(SYMBOL_SECTOR, symbol_sector)) { ::PrintFormat("Failed to set a sector for the custom symbol \"%s\"", custom_symbol); return; } //--- background color color symbol_background_clr = clrKhaki; if(!custom_symbol_obj.SetProperty(SYMBOL_BACKGROUND_COLOR, symbol_background_clr)) { ::PrintFormat("Failed to set a background color for the custom symbol \"%s\"", custom_symbol); return; } //--- chart mode ENUM_SYMBOL_CHART_MODE symbol_ch_mode=SYMBOL_CHART_MODE_BID; if(!custom_symbol_obj.SetProperty(SYMBOL_CHART_MODE, symbol_ch_mode)) { ::PrintFormat("Failed to set a chart mode for the custom symbol \"%s\"", custom_symbol); return; } //--- digits if(!custom_symbol_obj.SetProperty(SYMBOL_DIGITS, InpDxyDigits)) { ::PrintFormat("Failed to set digits for the custom symbol \"%s\"", custom_symbol); return; } //--- trade mode ENUM_SYMBOL_TRADE_MODE symbol_trade_mode = SYMBOL_TRADE_MODE_DISABLED; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_MODE, symbol_trade_mode)) { ::PrintFormat("Failed to disable trade for the custom symbol \"%s\"", custom_symbol); return; }
次のプロパティはENUM_SYMBOL_INFO_INTEGER型です。
- SYMBOL_SECTOR
- SYMBOL_BACKGROUND_COLOR
- SYMBOL_CHART_MODE
- SYMBOL_DIGITS
- SYMBOL_TRADE_MODE.
最後のプロパティは、取引モードを担当します。合成指数は取引が許可されないため、プロパティはSYMBOL_TRADE_MODE_DISABLEDに設定されます。テスターで銘柄ごとに戦略を確認する必要がある場合は、プロパティを有効にする必要があります(SYMBOL_TRADE_MODE_FULL)。
//--- Double properties //--- point double symbol_point = 1./::MathPow(10, InpDxyDigits); if(!custom_symbol_obj.SetProperty(SYMBOL_POINT, symbol_point)) { ::PrintFormat("Failed to to set a point value for the custom symbol \"%s\"", custom_symbol); return; } //--- tick size double symbol_tick_size = symbol_point; if(!custom_symbol_obj.SetProperty(SYMBOL_TRADE_TICK_SIZE, symbol_tick_size)) { ::PrintFormat("Failed to to set a tick size for the custom symbol \"%s\"", custom_symbol); return; }
次のプロパティはENUM_SYMBOL_INFO_DOUBLE型です。
- SYMBOL_POINT
- SYMBOL_TRADE_TICK_SIZE
//--- String properties //--- category string symbol_category="Currency indices"; if(!custom_symbol_obj.SetProperty(SYMBOL_CATEGORY, symbol_category)) { ::PrintFormat("Failed to to set a category for the custom symbol \"%s\"", custom_symbol); return; } //--- country string symbol_country= "US"; if(!custom_symbol_obj.SetProperty(SYMBOL_COUNTRY, symbol_country)) { ::PrintFormat("Failed to to set a country for the custom symbol \"%s\"", custom_symbol); return; } //--- description string symbol_description= "Synthetic US Dollar Index"; if(!custom_symbol_obj.SetProperty(SYMBOL_DESCRIPTION, symbol_description)) { ::PrintFormat("Failed to to set a description for the custom symbol \"%s\"", custom_symbol); return; } //--- exchange string symbol_exchange= "ICE"; if(!custom_symbol_obj.SetProperty(SYMBOL_EXCHANGE, symbol_exchange)) { ::PrintFormat("Failed to to set an exchange for the custom symbol \"%s\"", custom_symbol); return; } //--- page string symbol_page = "https://www.ice.com/forex/usdx"; if(!custom_symbol_obj.SetProperty(SYMBOL_PAGE, symbol_page)) { ::PrintFormat("Failed to to set a page for the custom symbol \"%s\"", custom_symbol); return; } //--- path string symbol_path="Custom\\"+custom_group+"\\"+custom_symbol; if(!custom_symbol_obj.SetProperty(SYMBOL_PATH, symbol_path)) { ::PrintFormat("Failed to to set a path for the custom symbol \"%s\"", custom_symbol); return; }
次のプロパティはENUM_SYMBOL_INFO_STRING型です。
- SYMBOL_CATEGORY
- SYMBOL_COUNTRY
- SYMBOL_DESCRIPTION
- SYMBOL_EXCHANGE
- SYMBOL_PAGE
- SYMBOL_PATH
最後のプロパティは、銘柄の木のパスを担当します。合成指数の作成中に、文字のグループと銘柄名が指定されました。したがって、このプロパティは同一であるためスキップできます。
もちろん、ティックを収集せずに合成指数の方程式を直接設定することもできますが、それでは例の意味が失われてしまいます。また、指数の価格は定期的に計算されます。現在の例では、カウント期間は10秒です。
次のブロックに移りましょう。これは取引履歴の確認です。ここでは、バーの履歴を確認し、ティックを読み込むという2つのタスクを解決します。次の方法でバーを確認してみましょう。
//--- check quotes history CBaseSymbol base_symbols[BASE_SYMBOLS_NUM]; const string symbol_names[]= { "EURUSD", "USDJPY", "GBPUSD", "USDCAD", "USDSEK", "USDCHF" }; ENUM_TIMEFRAMES curr_tf=PERIOD_M1; ::Print("\nChecking of quotes history is running..."); for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; if(ptr_base_symbol.Init(curr_symbol_name, curr_tf, InpStartDay)) { ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); int check_load_code=ptr_base_symbol.CheckLoadHistory(); ::PrintFormat(" Checking code: %I32d", check_load_code); ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); if(check_load_code<0) { ::PrintFormat("Failed to load quotes history for the symbol \"%s\"", curr_symbol_name); return; } } }
ループしてクウォートを処理する必要がある6つの銘柄があります。CBaseSymbolクラスは便宜上作成されました。
//+------------------------------------------------------------------+ //| Class CBaseSymbol | //+------------------------------------------------------------------+ class CBaseSymbol : public CObject { //--- === Data members === --- private: CSymbolInfo m_symbol; ENUM_TIMEFRAMES m_tf; matrix m_ticks_mx; datetime m_start_date; ulong m_last_idx; //--- === Methods === --- public: //--- constructor/destructor void CBaseSymbol(void); void ~CBaseSymbol(void) {}; //--- bool Init(const string _symbol, const ENUM_TIMEFRAMES _tf, datetime start_date); int CheckLoadHistory(void); bool LoadTicks(const datetime _stop_date, const uint _flags); matrix GetTicks(void) const { return m_ticks_mx; }; bool SearchTickLessOrEqual(const double _dbl_time, vector &_res_row); bool CopyLastTick(vector &_res_row); };
このクラスはバーとティックの履歴を扱います。これは非常に重要なタスクです。そうしないと、合成指数を作成するためのデータがありません。
ティックをロードしましょう。
//--- try to load ticks ::Print("\nLoading of ticks is running..."); now=::TimeCurrent(); uint flags=COPY_TICKS_INFO | COPY_TICKS_TIME_MS | COPY_TICKS_BID | COPY_TICKS_ASK; double first_tick_dbl_time=0.; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; string curr_symbol_name=symbol_names[s_idx]; ::PrintFormat("\n Symbol #%hu: \"%s\"", s_idx+1, curr_symbol_name); ulong start_cnt=::GetTickCount64(); ::ResetLastError(); if(!ptr_base_symbol.LoadTicks(now, flags)) { ::PrintFormat("Failed to load ticks for the symbol \"%s\" , error: %d", curr_symbol_name, ::GetLastError()); return; } ulong time_elapsed_ms=::GetTickCount64()-start_cnt; ::PrintFormat(" Time elapsed: %0.3f sec", time_elapsed_ms/MS_IN_SEC); //--- looking for the 1st tick matrix ticks_mx=ptr_base_symbol.GetTicks(); double tick_dbl_time=ticks_mx[0][0]; if(tick_dbl_time>first_tick_dbl_time) first_tick_dbl_time=tick_dbl_time; }
ティックをロードするために、matrix::CopyTicksRange()ネイティブ関数が使用されました。便利なのは、flagsで定義されたティック構造にこれらの列のみをロードできることです。リソースを節約するという問題は、何百万ものティックを要求する場合に非常に重要です。
COPY_TICKS_INFO = 1, // ticks caused by Bid and/or Ask changes COPY_TICKS_TRADE = 2, // ticks caused by Last and Volume changes COPY_TICKS_ALL = 3, // all ticks that have changes COPY_TICKS_TIME_MS = 1<<8, // time in milliseconds COPY_TICKS_BID = 1<<9, // Bid price COPY_TICKS_ASK = 1<<10, // Ask price COPY_TICKS_LAST = 1<<11, // Last price COPY_TICKS_VOLUME = 1<<12, // volume COPY_TICKS_FLAGS = 1<<13, // tick flags
履歴を確認し、ティックをログにロードする段階を、時間コストの観点から説明します。
CS 0 12:01:11.802 dDxySymbol Checking of quotes history is running... CS 0 12:01:11.802 dDxySymbol CS 0 12:01:11.802 dDxySymbol Symbol #1: "EURUSD" CS 0 12:01:14.476 dDxySymbol Checking code: 1 CS 0 12:01:14.476 dDxySymbol Time elapsed: 2.688 sec CS 0 12:01:14.476 dDxySymbol CS 0 12:01:14.476 dDxySymbol Symbol #2: "USDJPY" CS 0 12:01:17.148 dDxySymbol Checking code: 1 CS 0 12:01:17.148 dDxySymbol Time elapsed: 2.672 sec CS 0 12:01:17.148 dDxySymbol CS 0 12:01:17.148 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:01:19.068 dDxySymbol Checking code: 1 CS 0 12:01:19.068 dDxySymbol Time elapsed: 1.922 sec CS 0 12:01:19.068 dDxySymbol CS 0 12:01:19.068 dDxySymbol Symbol #4: "USDCAD" CS 0 12:01:21.209 dDxySymbol Checking code: 1 CS 0 12:01:21.209 dDxySymbol Time elapsed: 2.140 sec CS 0 12:01:21.209 dDxySymbol CS 0 12:01:21.209 dDxySymbol Symbol #5: "USDSEK" CS 0 12:01:22.631 dDxySymbol Checking code: 1 CS 0 12:01:22.631 dDxySymbol Time elapsed: 1.422 sec CS 0 12:01:22.631 dDxySymbol CS 0 12:01:22.631 dDxySymbol Symbol #6: "USDCHF" CS 0 12:01:24.162 dDxySymbol Checking code: 1 CS 0 12:01:24.162 dDxySymbol Time elapsed: 1.531 sec CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Loading of ticks is running... CS 0 12:01:24.162 dDxySymbol CS 0 12:01:24.162 dDxySymbol Symbol #1: "EURUSD" CS 0 12:02:27.204 dDxySymbol Time elapsed: 63.032 sec CS 0 12:02:27.492 dDxySymbol CS 0 12:02:27.492 dDxySymbol Symbol #2: "USDJPY" CS 0 12:02:32.587 dDxySymbol Time elapsed: 5.094 sec CS 0 12:02:32.938 dDxySymbol CS 0 12:02:32.938 dDxySymbol Symbol #3: "GBPUSD" CS 0 12:02:37.675 dDxySymbol Time elapsed: 4.734 sec CS 0 12:02:38.285 dDxySymbol CS 0 12:02:38.285 dDxySymbol Symbol #4: "USDCAD" CS 0 12:02:43.223 dDxySymbol Time elapsed: 4.937 sec CS 0 12:02:43.624 dDxySymbol CS 0 12:02:43.624 dDxySymbol Symbol #5: "USDSEK" CS 0 12:03:18.484 dDxySymbol Time elapsed: 34.860 sec CS 0 12:03:19.596 dDxySymbol CS 0 12:03:19.596 dDxySymbol Symbol #6: "USDCHF" CS 0 12:03:24.317 dDxySymbol Time elapsed: 4.719 sec
ティックが受信された後、DXY合成指数のティック履歴を形成します。このプロセスは、次のブロックでおこなわれます。
//--- create a custom symbol ticks history ::Print("\nCustom symbol ticks history is being formed..."); long first_tick_time_sec=(long)(first_tick_dbl_time/MS_IN_SEC); long first_tick_time_ms=(long)first_tick_dbl_time%(long)MS_IN_SEC; ::PrintFormat(" First tick time: %s.%d", ::TimeToString((datetime)first_tick_time_sec, TIME_DATE|TIME_SECONDS), first_tick_time_ms); double active_tick_dbl_time=first_tick_dbl_time; double now_dbl_time=MS_IN_SEC*now; uint ticks_cnt=0; uint arr_size=0.5e8; MqlTick ticks_arr[]; ::ArrayResize(ticks_arr, arr_size); ::ZeroMemory(ticks_arr); matrix base_prices_mx=matrix::Zeros(BASE_SYMBOLS_NUM, 2); do { //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.SearchTickLessOrEqual(active_tick_dbl_time, tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick; CalcIndexPrices(active_tick_dbl_time, base_prices_mx, last_ind_tick); arr_size=ticks_arr.Size(); if(ticks_cnt>=arr_size) { uint new_size=(uint)(arr_size+0.1*arr_size); if(::ArrayResize(ticks_arr, new_size)!=new_size) continue; } ticks_arr[ticks_cnt]=last_ind_tick; ticks_cnt++; } active_tick_dbl_time+=TICK_PAUSE; } while(active_tick_dbl_time<now_dbl_time); ::ArrayResize(ticks_arr, ticks_cnt); int ticks_replaced=custom_symbol_obj.TicksReplace(ticks_arr, true);
一時点(active_tick_dbl_time)を設定するループの最後に10秒を追加します。これは、指数を構成するすべての銘柄のティックを取得するための一種の「タイムスタンプ」です。
そのため、各銘柄の目的のティックの検索は、過去の特定の時点に基づいています。CBaseSymbol::SearchTickLessOrEqual()メソッドは、active_tick_dbl_time値までに到着したティックを提供します。
指数の各コンポーネントからのティックが受信されると、ティック価格はすでにbase_prices_mx行列にあります。
CalcIndexPrices()関数は、その時点で準備済みの指数ティックの値を返します。
ティックが作成されると、CiCustomSymbol::TicksReplace()メソッドを使用してティックデータベースが更新されます。
サービスは、次のブロックで現在のみを処理します。
//--- main processing loop ::Print("\nA new tick processing is active..."); do { ::ZeroMemory(base_prices_mx); //--- collect base symbols ticks bool all_ticks_ok=true; for(ushort s_idx=0; s_idx<BASE_SYMBOLS_NUM; s_idx++) { CBaseSymbol *ptr_base_symbol=&base_symbols[s_idx]; vector tick_prices_vc; bool to_break_loop=false; if(!ptr_base_symbol.CopyLastTick(tick_prices_vc)) to_break_loop=true; else { if(!base_prices_mx.Row(tick_prices_vc, s_idx)) to_break_loop=true; } if(to_break_loop) { all_ticks_ok=false; break; } } //--- calculate index prices if(all_ticks_ok) { MqlTick last_ind_tick, ticks_to_add[1]; now=::TimeCurrent(); now_dbl_time=MS_IN_SEC*now; CalcIndexPrices(now_dbl_time, base_prices_mx, last_ind_tick); ticks_to_add[0]=last_ind_tick; int ticks_added=custom_symbol_obj.TicksAdd(ticks_to_add, true); } ::Sleep(TICK_PAUSE); } while(!::IsStopped());
ブロックのタスクは、前のブロックのタスクと似ていますが、少し簡単です。10秒ごとに、銘柄のティックデータを取得し、指数価格を計算する必要があります。指数では、買い呼び値はすべての銘柄のビッドに基づいて計算され、売り呼び値はすべてのアスクに基づいて計算されます。
dDxySymbolサービスを起動した後、しばらくするとカスタムDXY銘柄チャートを開くことができます(図6)。
図6:休日を含むDXYカスタム銘柄チャート
チャートでは、土曜日が赤い縦線で強調表示されています。土曜日と日曜日に、サービスは履歴のティックを計算し続けていることがわかりましたが、これはおそらく完全には正しくありません。サービスコードに時間制限(曜日)を追加する必要があります。このタスクをCheckDayOfWeek()関数に割り当てましょう。
合成指数チャートは次のようになります(図7)。バグが修正されたようです。
図7.休日なしのDXYカスタム銘柄チャート
これで、dDxySymbolサービスの作業は完了です。
結論
この記事では、「サービス」として知られるMQL5プログラムタイプのいくつかの機能をハイライトしました。このタイプのMQL5プログラムは、チャートに結合せず、独立して動作するという点で異なります。サービスの性質上、他のEAやスクリプトと競合する可能性があり、おそらく程度は低いですが指標と競合する可能性があります。したがって、MetaTrader5環境でサービスプログラムの権利と義務を定義するタスクは、開発者の肩にかかっています。
アーカイブには、%MQL5\\Servicesフォルダに配置できるソースが含まれています。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/11826
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索