MQL5入門(第38回):MQL5のAPIとWebRequest関数の習得(XII)
はじめに
連載「MQL5入門」の第38回へようこそ。前回の記事では、MQL5を使用して外部システムと通信するための堅牢なフレームワークの構築に重点を置きました。APIエンドポイントの作成、WebRequest関数の動作、タイムスタンプや署名といった認証要素の準備、そしてサーバーレスポンスの受信および解析について学びました。これらの概念は、あらゆる非公開APIや機密性の高いAPIと連携する際に不可欠です。
本記事も、前回までと同様に、実践的なプロジェクト形式のアプローチを採用しています。理論を個別に説明するのではなく、各概念を実際のワークフローの中で実践します。この方法により、個々の要素がどのように機能するのかだけでなく、それらが実際の統合環境でどのように連携するのかも理解しやすくなります。本記事はAPIシリーズの締めくくりとなるものであり、これまでの学習の集大成となります。
本記事では、Binanceのような外部サービスと連携し、市場データを取得して分析するMQL5のエキスパートアドバイザー(EA)を作成します。プロジェクトは、対象となる暗号資産を選択し、APIを利用してローソク足データを取得するところから始まります。直近で確定した強気の包み足パターンを検出するために、直近で確定した30分足3本を取得します。また、このパターンをプログラムで識別する仕組みについて解説し、MQL5を使用して正確に検出する方法を学びます。さらに、これらの条件に基づいてAPI経由で実際に注文を送信する仕組みについても説明します。
このプロジェクトを通じて、市場データの取得と分析の方法、MetaTrader 5がどのように安全なリクエストを生成するのか、そして売買パターンがどのようにプログラムで評価されるのかに焦点を当てます。本プロジェクトは、MQL5が従来のMetaTrader内のチャート依存ロジックにとどまらず、MetaTrader 5と外部プラットフォームをつなぐ橋として機能できることを示しています。 この記事を読み終える頃には、ローソク足データの取得、強気の包み足のようなローソク足パターンの理解、そして市場状況に応じて動作するEAを準備するための、明確で再利用可能なワークフローを習得できるでしょう。また、このアプローチを他のパターンや時間足、さらに高度な自動化シナリオへ応用するための確かな理解も得られます。
注記:
本記事は教育目的のみを意図したものです。すべての例、プロジェクト、およびデモンストレーションは、実際の統合シナリオにおいてMQL5がどのように機能するかを説明するためのものです。ライブ注文の実行や金融取引を推奨または勧誘するものではありません。焦点はあくまでも学習、実験、および管理された教育環境においてMQL5のAPI連携やWebRequest関数の利用方法を理解することにあります。
ローソク足データの取得と処理
このEAを作成する最初の段階は、直近3本のローソク足の詳細情報を取得することです。通常であれば、対象銘柄はすでにMetaTrader 5のチャート上に存在するため、この作業は比較的簡単です。しかし、本プロジェクトではデータをBinanceから直接取得するため、分析対象の資産はMetaTrader 5上には存在しません。そのため、MetaTrader 5に組み込まれている価格系列やインジケーター機能に依存することができません。 この問題を解決するために、まずBinanceへAPIリクエストを送信し、ローソク足データを取得します。このアプローチは、本連載第29回で紹介した手法とよく似ています。第29回では、MetaTrader 5のチャートデータを利用するのではなく、サーバーから返されたレスポンスから直接ローソク足データを処理しました。今回も同様に、Binanceから生のローソク足データを取得し、それをEA内部で手動処理します。
EAが最初におこなう処理は、Binance上で選択した銘柄の最新ローソク足データを取得するためのAPIリクエストを送信することです。レスポンスを受け取った後は、Binanceがローソク足データをどのようなJSON構造で返しているのかを理解する必要があります。Binanceでは、各ローソク足が複数の値を含む構造化配列として返されます。 JSONの構造を理解した後、さまざまなMQL5の文字列処理関数を利用して必要な主要データを抽出し、項目ごとに整理・グループ化します。これらの関数によって、各ローソク足の始値(Open)、高値(High)、安値(Low)、終値(Close)、および開始時刻などの重要なデータを整理し、グループ化できます。そして最後に確定したローソク足を特定することで、そのOHLCデータが強気かどうかを迅速に評価できます。
ここで重要なのは、APIを利用できるからといって無制限にリクエストを送信してはいけないという点です。BinanceのAPIには厳格なレート制限が設けられており、過度に頻繁なリクエストを送信すると、一時的な制限やリクエスト拒否が発生する可能性があります。そのため、リクエストを送信するタイミングは、その正確性と同じくらい重要です。 本EAは確定済みの30分足を対象としているため、最も効率的な方法は、新しい30分足が開始されるタイミングでのみAPIリクエストを送信することです。この時点では、直前のローソク足が完全に確定しており、そのデータは正確で、すでに確定しています。もしそれ以外のタイミングでリクエストを送信した場合、未確定のローソク足データを取得してしまうか、あるいは新しい情報を得られない無駄なAPIコールになってしまいます。
APIリクエストのタイミングを30分足の開始時刻に合わせることで、不要なネットワーク通信を最小限に抑え、Binanceのレート制限を遵守しながら、常に完全に確定したローソク足データを分析できるようになります。また、この方法により、新しい情報が利用可能になったときのみEAが分析を実行するため、EAの動作効率と予測可能性も維持されます。
例:
datetime last_bar_time = 0; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime current_m30_time = iTime(_Symbol,PERIOD_M30,0); if(current_m30_time != last_bar_time) { //SEND REQUEST last_bar_time = current_m30_time; } }
解説:
直近で処理した30分足の開始時刻を追跡するために、まずEAは変数を宣言します。この値は0で初期化されるため、EAは最初の有効なローソク足を容易に識別でき、新しいAPIリクエストが必要かどうかを判断できます。 選択された銘柄に対して新しいティックが到着するたびに、EAのメイン関数が実行されます。ティックは価格のわずかな変化を表しており、この関数は継続的に市場の状態を評価します。この処理の中で、EAは最新の30分足の開始時刻を取得します。これにより、現在形成中のローソク足の開始時点を示す明確な基準が得られます。
次にEAは、保存されている前回のローソク足時刻と現在のローソク足の開始時刻を比較します。この2つの時刻が異なる場合は、前回の確認以降に新しいローソク足が開始されたことを意味します。この状況になると、ローソク足データを取得するためのAPIリクエストが送信されます。EAは新しいローソク足が始まったときにのみリクエストを送信するため、不要なAPIコールを排除し、APIの利用制限下でも効率的に動作できます。 APIリクエストを送信した後、EAは現在のローソク足の開始時刻をlast_bar_time変数に保存します。これにより、同じローソク足が複数回処理されることを防ぎ、次の30分足が開始されたときにのみ新しいリクエストが送信されるようになります。この方法によって、データ取得の効率性と正確性が維持されます。
比喩的な説明
毎日の出来事をノートに記録していると想像してください。同じ日の内容を二重に記録しないように、その日の記録は一度だけおこないます。また、最後に記録した日付を別にメモしておき、現在の日付が新しい日なのか、それともすでに記録済みの日なのかを判断します。 この例では、ティックの到着は一日に何度もカレンダーを確認する行為に相当します。カレンダーを見るたびに記録するのではなく、日付が実際に変わったときだけ新しい記録を追加します。EAにおいては、この日付に相当するものが現在の30分足の開始時刻であり、それが処理を実行すべきタイミングを示しています。
新しい日が始まったときに出来事を記録するのと同じように、EAも新しいローソク足が開始されたときにのみAPIリクエストを送信し、新しいローソク足データを取得します。そして記録が終わったら最後に記録した日付を更新するように、EAも保存している時刻を更新し、同じローソク足が再び処理されることを防ぎます。この仕組みによって、EAは各ローソク足につき1回だけAPIリクエストを実行し、記録を整理された状態に保ちながら、効率的かつ正確に動作します。
例:datetime last_bar_time = 0; string method_bar = "GET"; string url_bar = "https://api.binance.com/api/v3/klines?symbol=BTCUSDT&interval=30m&limit=3"; string headers_bar = ""; int time_out_bar = 5000; char data_bar[]; char result_bar[]; string result_headers_bar; string server_result; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- datetime current_m30_time = iTime(_Symbol,PERIOD_M30,0); if(current_m30_time != last_bar_time) { WebRequest(method_bar, url_bar, headers_bar, time_out_bar, data_bar, result_bar, result_headers_bar); server_result = CharArrayToString(result_bar); Print(server_result); last_bar_time = current_m30_time; } }
出力:

解説:
最初のセクションでは、Binance APIを呼び出すために必要な変数を定義します。methodにはGETを設定することで、このリクエストがサーバー上のデータを読み取るためのものであり、データの更新や変更をおこなうものではないことを明確にしています。url変数には、ローソク足データを提供するBinanceのエンドポイントアドレスが格納されます。アドレス自体はサーバーの場所を示し、パス部分はサーバー上の特定のリソースを指定します。この例では、ローソク足情報を提供するエンドポイントを指しています。URLのクエリ文字列には、サーバーへの追加指示が含まれています。たとえば、limitは返却するローソク足の本数を指定し、intervalは各ローソク足の時間足を決定し、symbolパラメータはデータを取得する対象の取引ペアを指定します。
今回のリクエストには認証情報や追加メタデータが必要ないため、headersは空のまま設定されています。timeoutは、応答が返ってこない場合にプログラムがリクエストを中止するまでの待機時間を定義します。また、リクエストデータ、レスポンスヘッダー、およびサーバーから返される生データを受け取るための配列も用意します。さらに、取得したレスポンスを解析しやすくするため、変換後のレスポンスを格納する文字列変数も準備します。その後、必要なパラメータを指定してWebRequest関数を呼び出します。これにより、指定したURL、ヘッダー、およびタイムアウト値を使用してBinanceサーバーへGETリクエストが送信されます。WebRequest関数は、リクエストの実行結果に関する情報を返すだけでなく、サーバーから返された生のレスポンスデータをレスポンス格納用の配列へ保存します。続いて、そのバイトデータを文字列へ変換し、人が読みやすい形式にします。この文字列を表示すると、ローソク足データを含む完全なJSONレスポンスを確認できます。
比喩的な説明
図書館に電話をかけて、過去3日分の記録を依頼すると想像してください。図書館の建物そのものがサーバーアドレスに相当し、履歴情報を管理している特定の部署がAPIパスに相当します。依頼をするときには、どの都市の情報が必要なのか、何日分の記録が必要なのか、どの程度詳細な情報が欲しいのかを伝えます。これらの指示は、URL内のsymbol、interval、limitといったパラメータに相当します。 その後、依頼書を送付して返事を待ちます。返答を受け取るためのトレーをあらかじめ用意しておくでしょう。ところが、届いた返答はすぐに読める形式ではなく、特殊な記号で暗号化されたメッセージになっています。そこで、そのメッセージを人が読める形式へ変換し、過去3日間の天気や出来事を確認します。
EAもこれと同じように動作します。あらかじめ設定されたURLとクエリパラメータを使用してBinanceへGETリクエストを送信し、処理が完了するまで待機します。その後、ローソク足データを生のバイト列として受け取り、それを読みやすいテキスト形式へ変換して出力します。この仕組みによって、EAは選択した取引ペアの最新のローソク足情報を取得できるようになります。
Binanceのローソク足データからOHLCと時刻情報を抽出する
ここでは、Binanceサーバーから返されるレスポンスから必要なデータを抽出することに焦点を当てます。API連携において重要なポイントの一つは、JSONデータ形式はプラットフォーム間で必ずしも共通ではないということです。そのため、ローソク足データの構造はサービスごとに異なる場合があります。 第29回でも説明したように、サーバーレスポンスを扱う際の第一歩は、返されるデータの正確な構造を理解することです。Binanceのローソク足データでは、各ローソク足は構造化されたJSON配列として送られます。その中には、開始時刻、始値、高値、安値、終値、出来高、その他のメタデータが含まれています。売買判断をおこなう前に、レスポンス内のどの位置がOHLC値およびローソク足の開始時刻に対応しているのかを明確に把握しておく必要があります。
データ構造を理解した後の次のステップは、レスポンスのクリーンアップです。これは、JSON形式には含まれているものの計算には不要な文字を取り除く作業を指します。例えば、角括弧、カンマ、引用符などが該当します。生のレスポンスを整理した後は、関連するデータをまとめていきます。たとえば、1本のローソク足に対応する始値、高値、安値、終値の各値は、単一の論理的なデータセットとして扱えるようにグループ化する必要があります。 抽出した値を配列へ整理することで、MQL5内でデータをはるかに扱いやすくできます。それぞれの配列は、開始時刻やOHLC値など特定の種類の情報を保持するために使用されます。このように整理することで、各値の比較や強気・弱気ローソク足の判定、さらには直近で確定したローソク足に基づく売買ロジックの適用が可能になります。
Binanceはローソク足データをJSON配列形式で返します。レスポンスは最上位レベルに複数の内部配列を持つ配列構造となっています。各内部配列が1本のローソク足を表しています。最も単純化すると、構造は次のようになります。
[ [array 1], [array 2], [array 3] ]
各内部配列には、1本のローソク足に関するすべてのデータが格納されています。これらの値は決められた順序で配置されているため、内部配列内の各位置は常に同じ種類のデータを表します。
例:
int array_count; string candle_data[]; string first_bar_data; string first_bar_data_array[]; string second_bar_data; string second_bar_data_array[]; string third_bar_data; string third_bar_data_array[];
if(current_m30_time != last_bar_time) { WebRequest(method_bar, url_bar, headers_bar, time_out_bar, data_bar, result_bar, result_headers_bar); server_result = CharArrayToString(result_bar); // Print(server_result); array_count = StringSplit(server_result,']', candle_data); first_bar_data = candle_data[0]; StringReplace(first_bar_data,"[[",""); StringReplace(first_bar_data,"\"",""); StringSplit(first_bar_data,',',first_bar_data_array); second_bar_data = candle_data[1]; StringReplace(second_bar_data,",[",""); StringReplace(second_bar_data,"\"",""); StringSplit(second_bar_data,',',second_bar_data_array); third_bar_data = candle_data[2]; StringReplace(third_bar_data,",[",""); StringReplace(third_bar_data,"\"",""); StringSplit(third_bar_data,',',third_bar_data_array); last_bar_time = current_m30_time; }
解説:
サーバーからのレスポンスは変数を使用して処理されます。最初の3本のローソク足の値はそれぞれ別々の配列に格納され、区切り部分がカウントされ、各ローソク足のブロックは文字列配列に保存されます。レスポンスを閉じ角括弧で分割することで、JSON構文の一部を残しながらも、ローソク足ごとに1つの要素を持つ配列が生成されます。 最初のローソク足を処理するために、ローソク足データ配列の最初の要素を取得します。この文字列には開始角括弧や引用符などの不要な文字が含まれているため、それらを取り除いてデータを整形します。整形後の文字列はカンマを区切り文字として分割され、それぞれのインデックスが個別のローソク足データを表す配列になります。2本目のローソク足についてもまったく同じ処理をおこないます。対応する配列要素を取得し、不要な文字を取り除いた後、カンマで分割します。これにより、最初のローソク足と同じ構造を持ち、完全に独立したデータとして扱えるようになります。
3本目のローソク足も同様の手順で処理されます。不要な文字を削除し、実際のテキストデータを抽出し、整形済みの文字列を個々の値へ分割します。この一連の処理が完了すると、それぞれのローソク足はデータが順序通りに格納された独自の配列を持つようになります。
比喩的な説明
サーバーから返されたレスポンスを、複数のレシートが連続して印刷された長い紙のロールだと想像してください。それぞれのレシートが1本のローソク足を表しています。しかし最初の状態では、すべてが1本のロール紙に繋がっているため、内容を確認したり利用したりすることが困難です。そこで最初におこなうのは、ロール紙を個々のレシートに切り分ける作業です。これはレスポンスを複数の部分に分割する処理に相当します。こうして各レシートは1本のローソク足を表すようになりますが、まだ余分なラベルや枠線、記号などが残っています。
次に最初のレシートから、装飾用の枠線や引用符など不要な部分を取り除き、本当に必要な内容だけを残します。整理が終わったら、レシートを1行ずつ読み取り、それぞれの情報を別々の欄へ記録します。これによって内容が分かりやすく整理されます。続いて2枚目のレシートにも同じ処理をおこないます。他のレシートから切り離し、不要な装飾を取り除き、内容を個別の項目へ整理します。3枚目のレシートもまったく同じ手順で処理されます。こうすることで、すべてのレシートが同じ方法で統一的に処理されることが保証されます。
最初は雑然とした1本の長いロール紙だったものが、最終的には整理された複数の文書へと変換されます。それぞれの記録には明確な情報が含まれており、比較や分析、意思決定に利用できるようになります。これはまさに、EAが信頼して利用できる構造化されたローソク足データを、生のサーバーレスポンスから生成する過程そのものです。次に、取得した値を個別の変数へ保存する必要があります。ローソク足データはすでに整理され、配列へ格納されているため、それぞれの値を対応する変数へ簡単に割り当てることができます。たとえば、開始時刻は専用の変数へ保存し、始値、高値、安値、終値についてもそれぞれ独立した変数として保存できます。
例:
long bar1_time_s; datetime bar1_time; long bar2_time_s; datetime bar2_time; long bar3_time_s; datetime bar3_time; double bar1_open; double bar2_open; double bar3_open; double bar1_high; double bar2_high; double bar3_high; double bar1_low; double bar2_low; double bar3_low; double bar1_close; double bar2_close; double bar3_close;
if(current_m30_time != last_bar_time) { WebRequest(method_bar, url_bar, headers_bar, time_out_bar, data_bar, result_bar, result_headers_bar); server_result = CharArrayToString(result_bar); // Print(server_result); array_count = StringSplit(server_result,']', candle_data); first_bar_data = candle_data[0]; StringReplace(first_bar_data,"[[",""); StringReplace(first_bar_data,"\"",""); StringSplit(first_bar_data,',',first_bar_data_array); second_bar_data = candle_data[1]; StringReplace(second_bar_data,",[",""); StringReplace(second_bar_data,"\"",""); StringSplit(second_bar_data,',',second_bar_data_array); third_bar_data = candle_data[2]; StringReplace(third_bar_data,",[",""); StringReplace(third_bar_data,"\"",""); StringSplit(third_bar_data,',',third_bar_data_array); bar1_time_s = (long)StringToInteger(first_bar_data_array[0])/1000; bar1_time = (datetime)bar1_time_s; bar2_time_s = (long)StringToInteger(second_bar_data_array[0])/1000; bar2_time = (datetime)bar2_time_s; bar3_time_s = (long)StringToInteger(third_bar_data_array[0])/1000; bar3_time = (datetime)bar3_time_s; bar1_open = StringToDouble(first_bar_data_array[1]); bar2_open = StringToDouble(second_bar_data_array[1]); bar3_open = StringToDouble(third_bar_data_array[1]); bar1_high = StringToDouble(first_bar_data_array[2]); bar2_high = StringToDouble(second_bar_data_array[2]); bar3_high = StringToDouble(third_bar_data_array[2]); bar1_low = StringToDouble(first_bar_data_array[3]); bar2_low = StringToDouble(second_bar_data_array[3]); bar3_low = StringToDouble(third_bar_data_array[3]); bar1_close = StringToDouble(first_bar_data_array[4]); bar2_close = StringToDouble(second_bar_data_array[4]); bar3_close = StringToDouble(third_bar_data_array[4]); last_bar_time = current_m30_time; }
解説:
このコードのセクションでは、取得したローソク足データを、時刻情報および価格情報を表す明確な変数へ割り当てていきます。実際に値を代入する前に、各ローソク足の開始時刻とOHLC値を格納するための変数を宣言します。時刻については、各ローソク足ごとに2つの変数が用意されます。1つは生のタイムスタンプ値を一時的に保持するlong型変数で、もう1つはMetaTraderが理解できる形式へ変換した時刻を保存するdatetime型変数です。価格データについては、各ローソク足の値を保存するために個別のdouble型変数を宣言します。事前にこれらの変数を定義しておくことで、すべてのデータに明確な格納先が確保され、プログラム全体のデータ構造も分かりやすくなります。
各ローソク足のデータは、Binanceが定義している順序に従って配列へ格納されています。最初の要素であるインデックス0には、ローソク足の開始時刻が保存されています。この値は最初はミリ秒単位の文字列として取得されます。MQL5で利用できる形式へ変換するため、まず文字列を整数へ変換し、その後1000で割ることでミリ秒を秒へ変換します。これによりミリ秒部分が除去され、タイムスタンプは秒単位になります。変換後の値は安全にdatetime型へキャストできます。この処理はすべてのローソク足に対して実行されるため、開始時刻はMetaTraderの時間システムと正しく同期された状態になります。
ローソク足配列の次の要素であるインデックス1には始値が格納されています。この値は文字列からdouble型へ変換され、対応する始値変数へ保存されます。インデックス2には高値が格納されており、同じ方法で変換されます。インデックス3には安値、インデックス4には終値が格納されています。これらもすべて文字列からdouble型へ変換され、数値計算や比較処理に利用できるようになります。
すべてのローソク足配列は同じインデックス構造を持っています。開始時刻は常にインデックス0で表され、始値はインデックス1、高値はインデックス2、安値はインデックス3、終値はインデックス4で表されます。この固定された構造のおかげで、EAは混乱することなく各値を正確に読み取り、適切な変数へ割り当てることができます。この段階が完了すると、Binance APIから取得した生のテキストデータはすべて適切な型を持つ変数へ変換されています。時刻情報はMetaTraderと自然に連携できるdatetime型として保存され、価格情報は分析可能な数値型として保存されます。この最終的な変換によって、EAは直近の確定ローソク足が強気かどうかといったローソク足の方向性を正確に評価できるようになります。
比喩的な説明
サーバーから3通の封筒が届くと想像してください。それぞれの封筒には30分足の要約情報が入っています。各封筒の中身は決まった構成になっています。ローソク足の開始時刻は1行目に、始値は2行目に、最高値は3行目に、最安値は4行目に、終値は5行目にあります。まず、この情報を利用する前に封筒を棚の上へ並べると考えてみてください。しかし、すぐに利用するには封筒を開けて、それぞれの内容を適切な棚へ整理して置かなければなりません。最初の行にはローソク足の開始時刻が書かれています。これは封筒の中に入っている小さなカードのようなものです。しかし、その時刻はミリ秒形式で書かれているため、あなたの棚に置いてある時計とは形式が異なります。そこでまず、自分のシステムが理解できる形式へ変換してから、対応する棚へ収納します。
続いて、始値、高値、安値、終値が書かれた残りの行も、それぞれ対応する棚へ整理して配置します。3つの封筒すべてに対して同じ作業をすることで、各棚には同じ種類の情報だけが集められます。最終的には、本棚全体が統一された分かりやすい構造となり、分析が容易になります。次の段階では、各ローソク足から取り出した同じ種類のデータを専用の配列へまとめます。棚から取り出した最初のカードをすべて一列に並べ、次に「高値」カードを別の列に、「安値」カードを3列目に、そして「終値」カードを4列目に並べます。この構造によって、データの利用が大幅に簡単になります。
各配列には同じ種類の情報だけが格納されます。たとえば、直近の始値を確認したい場合や、過去3本のローソク足の高値を比較したい場合でも、個々のローソク足を1本ずつ調べる必要はありません。対応する配列を参照するだけで済みます。この整理方法によって分析処理は高速化され、時間情報や価格情報、その他のローソク足の特性に基づいたロジックを簡単に適用できるようになります。
例:
if(current_m30_time != last_bar_time) { WebRequest(method_bar, url_bar, headers_bar, time_out_bar, data_bar, result_bar, result_headers_bar); server_result = CharArrayToString(result_bar); // Print(server_result); array_count = StringSplit(server_result,']', candle_data); first_bar_data = candle_data[0]; StringReplace(first_bar_data,"[[",""); StringReplace(first_bar_data,"\"",""); StringSplit(first_bar_data,',',first_bar_data_array); second_bar_data = candle_data[1]; StringReplace(second_bar_data,",[",""); StringReplace(second_bar_data,"\"",""); StringSplit(second_bar_data,',',second_bar_data_array); third_bar_data = candle_data[2]; StringReplace(third_bar_data,",[",""); StringReplace(third_bar_data,"\"",""); StringSplit(third_bar_data,',',third_bar_data_array); bar1_time_s = (long)StringToInteger(first_bar_data_array[0])/1000; bar1_time = (datetime)bar1_time_s; bar2_time_s = (long)StringToInteger(second_bar_data_array[0])/1000; bar2_time = (datetime)bar2_time_s; bar3_time_s = (long)StringToInteger(third_bar_data_array[0])/1000; bar3_time = (datetime)bar3_time_s; bar1_open = StringToDouble(first_bar_data_array[1]); bar2_open = StringToDouble(second_bar_data_array[1]); bar3_open = StringToDouble(third_bar_data_array[1]); bar1_high = StringToDouble(first_bar_data_array[2]); bar2_high = StringToDouble(second_bar_data_array[2]); bar3_high = StringToDouble(third_bar_data_array[2]); bar1_low = StringToDouble(first_bar_data_array[3]); bar2_low = StringToDouble(second_bar_data_array[3]); bar3_low = StringToDouble(third_bar_data_array[3]); bar1_close = StringToDouble(first_bar_data_array[4]); bar2_close = StringToDouble(second_bar_data_array[4]); bar3_close = StringToDouble(third_bar_data_array[4]); datetime OpenTime[3] = {bar1_time, bar2_time, bar3_time}; double OpenPrice[3] = {bar1_open, bar2_open, bar3_open}; double ClosePrice[3] = {bar1_close, bar2_close, bar3_close}; double LowPrice[3] = {bar1_low, bar2_low, bar3_low}; double HighPrice[3] = {bar1_high, bar2_high, bar3_high}; last_bar_time = current_m30_time; }
解説:
EAは、3本のローソク足から取得したデータを整理するために、同じ種類の情報をまとめてグループ化します。最も古いローソク足、中間のローソク足、そして最新のローソク足の開始時刻は1つのグループにまとめられるため、各ローソク足ごとに異なる値を個別に扱うことなく、簡単にアクセスしたり参照したりできます。始値についても同じ方法が適用されます。EAは始値をまとめて管理することで、各ローソク足の始値を素早く比較できます。終値も同様にグループ化され、さらに各ローソク足の高値と安値もそれぞれまとめられます。これらの関連する値を整理することによって、トレンド分析、高値・安値の比較、あるいは強気パターンや弱気パターンの検出といった計算が大幅に容易になります。
本質的には、この方法によって表形式に整理されます。そこでは、時刻、始値、終値、安値、高値がそれぞれ一貫して列として管理され、各ローソク足は1行として表現されます。これによりデータ管理が簡単になるだけでなく、EAにおける自動売買ロジックやローソク足パターン分析の基盤も構築されます。
比喩的な説明
Binanceの直近3本の30分足に対応する3つの箱があると想像してください。各箱には5つの情報が入っています。ローソク足の開始時刻、始値、終値、最安値、最高値です。
すべての情報が混在した3つの箱をそのまま持ち運ぶ代わりに、あなたは情報の種類ごとに整理することにしました。まず、3つの箱に入っている開始時刻をすべて1か所にまとめます。同じように、始値、終値、最安値、最高値もそれぞれまとめます。こうすると、2本目のローソク足の始値や、3本のローソク足の中での最高値を確認したい場合に、各箱を順番に探し回る必要がなくなります。すべてが整理されているため、データへのアクセス、比較、分析を迅速かつ簡単におこなうことができます。
例:if(ClosePrice[0] < OpenPrice[0] && ClosePrice[1] > OpenPrice[1] && ClosePrice[1] > HighPrice[0]) { Print("BULLISH ENGULFING CONFIRMED"); //SEND A BUY ORDER TO BINANCE } else { // NO BULLSH ENGULFING Print("NO BULLISH ENGULFING CONFIRMED"); }
解説:
この条件は、先ほど作成したローソク足配列を用いて、強気の包み足を検出することを目的としています。ここでは特に直近2本の確定したローソク足のみが対象となります。この構造において、インデックス0は3本のうち最も古いローソク足(3本目)、インデックス1は中間のローソク足(2本目)、インデックス2は現在形成中のローソク足を表します。ただしインデックス2は未確定であるため、このパターン判定には使用されません。
条件の最初の部分では、直前のローソク足(インデックス0)が陰線であるかどうかを判定します。これは始値と終値を比較することでおこなわれ、終値が始値より低い場合、そのローソク足は期間中に下落したことを意味します。次の部分では、直近で確定したローソク足(インデックス1、2本目)が陽線であるかどうかを判定します。これは終値が始値より高い場合に成立し、そのローソク足が上昇トレンドで終了したことを示します。
最後の条件では、前の陰線が現在の陽線によって完全に包み込まれているかを確認します。このサンプルでは、陽線の終値が前のローソク足の高値を上回っているかどうかを確認しています。この条件が満たされる場合、直近の陽線が前のローソク足の値動きを上回っていることになり、強気の包み足が形成されたと判断されます。すべての条件が成立した場合、コードは「BULLISH ENGULFING CONFIRMED」と出力し、パターンが検出されたことを示します。実際の取引環境では、このタイミングでBinanceへ買い注文を送信することが可能です。
一方で、いずれかの条件が満たされない場合はelseブロックが実行され、「NO BULLISH ENGULFING CONFIRMED」と表示され、強気の包み足パターンが成立していないことが示されます。このロジックは、未確定の現在のローソク足(インデックス2)を除外しながら、整理されたローソク足データを用いて強気の包み足を機械的に判定するシンプルかつ明確な方法となっています。
Binance APIを通じた注文の自動化
この章の次のステップでは、強気の包み足パターンを検出できるようになったことを受けて、自動的に取引を実行する方法に進みます。ここでは、MQL5のWebRequest関数を使用し、API経由でBinanceへ注文を送信する方法に焦点を当てます。前回の記事では、署名の生成に使用したのはタイムスタンプとシークレットキーのみでした。これは口座残高を取得するためのクエリ文字列に、タイムスタンプと生成された署名だけが含まれていたため、それで十分でした。しかし今回は、注文の送信にはタイムスタンプだけではなく、より多くの情報が必要になります。
Binance APIを通じて注文を出す際のクエリ文字列には、銘柄、注文方向(売買)、注文タイプ、数量、そしてタイムスタンプなどの複数の要素が含まれます。ここで重要なのは、クエリ文字列に使用されるすべてのパラメータを署名生成時にも必ず同じ順序と同じ内容で使用する必要があるという点です。もし署名生成の過程でいずれかのパラメータが欠けていた場合、Binanceはリクエストを拒否します。
例:
string url_t = "https://api.binance.com/api/v3/time"; string headers_t = ""; char result_t[]; string response_headers_t; char data_t[]; string time; string pattern = "{\"serverTime\":"; int pattern_lenght; int end; string server_time = ""; string time_stamp = ""; string symbol; string sQty; string side; string params; string message; string secrete_key;
if(ClosePrice[0] < OpenPrice[0] && ClosePrice[1] > OpenPrice[1] && ClosePrice[1] > HighPrice[0]) { // Print("BULLISH ENGULFING CONFIRMED"); //SEND A BUY ORDER TO BINANCE WebRequest("GET", url_t, headers_t, 5000, data_t, result_t, response_headers_t); time = CharArrayToString(result_t); pattern_lenght = StringFind(time,pattern); pattern_lenght += StringLen(pattern); end = StringFind(time,"}",pattern_lenght + 1); server_time = StringSubstr(time,pattern_lenght,end - pattern_lenght); time_stamp = "timestamp=" + server_time; symbol = "DOGEUSDT"; sQty = DoubleToString(6, 8); StringReplace(sQty, ",", "."); side = "BUY"; params = "symbol=" + symbol + "&side=" + side + "&type=MARKET" + ""eOrderQty=" + sQty + "&" + time_stamp; message = params; secrete_key = "AbcdefGhmb91TXxKK0Ct51sjh9312345eIdLkRpYThugopIMe4EZVjaOCYabcdef";
解説:
まず最初に、Binanceのサーバー時刻エンドポイントへリクエストを送信するための一連の変数を準備します。URLには、現在のサーバー時刻を取得するためのBinance APIパスが指定されています。このエンドポイントは非常に重要で、Binanceではタイムスタンプをローカルシステム時刻ではなく、自身のサーバー時刻から取得することを要求しているためです。このリクエストには認証が不要なため、headers変数は空のままに設定されています。また、サーバーから返される生のレスポンスおよびリクエストデータは文字配列に格納され、サーバーが返すヘッダー情報はレスポンスヘッダー用の変数に保存されます。
次にWebRequest関数をGETメソッドで呼び出し、サーバー時刻URLへリクエストを送信します。Binanceは応答として、現在のサーバー時刻(ミリ秒単位)を含むJSONデータを返します。このレスポンスは生のバイトデータとしてresult配列に格納されます。その後、このバイトデータを文字列へ変換することで、人間が読める形式へ変換します。これにより、Binanceから返されたサーバー時刻を含む完全なJSONレスポンスが文字列として取得されます。この時刻は、認証付きリクエストにおいて最初に必要となる重要なパラメータとなります。
次に、複数の文字列変数を使用してレスポンスからタイムスタンプを抽出します。Binanceがサーバー時刻に使用するJSONキーに一致するパターン文字列をあらかじめ定義しておきます。さらに、レスポンス内でタイムスタンプの開始位置と終了位置を特定するための変数も用意します。抽出されたサーバー時刻と最終的に整形されたタイムスタンプパラメータは、それぞれ空の文字列変数に保存されます。
プログラムはまず、レスポンス文字列内でサーバー時刻フィールドの開始位置を示すパターンを検索します。パターンが見つかると、その長さを加算し、ラベル部分を除いた純粋な数値部分から抽出が開始されるように調整します。その後、数値の終端を示す閉じ括弧の位置を検索し、その範囲に基づいてサブストリングを取得します。こうして抽出されるのは、Binanceが提供するミリ秒単位のタイムスタンプ値のみです。取得した値は、APIリクエストで使用できるように「timestamp=」という形式を先頭に付与して整形されます。これにより、署名生成および注文リクエストに使用するタイムスタンプパラメータが完成します。
続いて、注文に必要な残りのパラメータを定義します。銘柄を格納する文字列変数、数量を格納する文字列変数、注文方向(売買)を示す変数が用意されます。また、すべてのパラメータを結合するためのクエリ文字列用変数も用意されます。さらに、最終的に署名対象となるメッセージ文字列と、署名生成に使用する秘密鍵も別の変数に格納されます。 symbol変数には取引対象となる通貨ペアが設定されます。
数量(quantity)はBinanceの数値形式に合わせて文字列へ変換され、小数点形式も適切に整形されます(カンマはドットに置換されます)。side変数には注文方向が設定されます。その後、必要なすべてのパラメータを「&」で連結し、1つのクエリ文字列を構築します。この中には数量、注文タイプ、方向、銘柄、タイムスタンプが含まれます。BinanceのAPI定義で規定されているように、各引数は「&」記号で接続されます。
この完成した文字列がmessage変数へ格納され、Binanceへ送信される署名対象のリクエスト全体を表します。秘密鍵は署名生成のために使用されます。 この段階で、署名生成に必要な要素はすべて揃っています。重要なのは、Binanceの仕様ではクエリ文字列全体を使用して署名を生成しなければならないという点です。timestampだけでは不十分であり、すべてのパラメータが完全に一致していなければなりません。もし1つでも欠けている場合、署名は無効となり、リクエストは認証エラーとして拒否されます。
例:uchar uMsg[], uSKey[]; int blockSize = 64; uchar ipad[], opad[]; uchar innerData[], innerHash[]; uchar outerData[], finalHash[]; string API_KEY = "abcdefkuoeX7gUvCb8nX0Ros4Jsabcdef3kA5nw8e12345udVZwWYHgOjyabcdef"; string headers = "X-MBX-APIKEY: " + API_KEY + "\r\n"; char result[]; string response_headers; char datas[]; string url; int status; string server_response;
if(ClosePrice[0] < OpenPrice[0] && ClosePrice[1] > OpenPrice[1] && ClosePrice[1] > HighPrice[0]) { // Print("BULLISH ENGULFING CONFIRMED"); //SEND A BUY ORDER TO BINANCE WebRequest("GET", url_t, headers_t, 5000, data_t, result_t, response_headers_t); time = CharArrayToString(result_t); pattern_lenght = StringFind(time,pattern); pattern_lenght += StringLen(pattern); end = StringFind(time,"}",pattern_lenght + 1); server_time = StringSubstr(time,pattern_lenght,end - pattern_lenght); time_stamp = "timestamp=" + server_time; symbol = "DOGEUSDT"; sQty = DoubleToString(6, 8); StringReplace(sQty, ",", "."); side = "BUY"; params = "symbol=" + symbol + "&side=" + side + "&type=MARKET" + ""eOrderQty=" + sQty + "&" + time_stamp; message = params; secrete_key = "AbcdefGhmb91TXxKK0Ct51sjh9312345eIdLkRpYThugopIMe4EZVjaOCYabcdef"; StringToCharArray(message, uMsg, 0, StringLen(message), CP_UTF8); StringToCharArray(secrete_key, uSKey, 0, StringLen(secrete_key), CP_UTF8); ArrayResize(ipad, blockSize); ArrayResize(opad, blockSize); for(int i = 0; i < blockSize; i++) { ipad[i] = uSKey [i] ^ 0x36; opad[i] = uSKey [i] ^ 0x5C; } ArrayCopy(innerData, ipad); ArrayCopy(innerData, uMsg, blockSize); CryptEncode(CRYPT_HASH_SHA256, innerData, uSKey, innerHash); ArrayCopy(outerData, opad); ArrayCopy(outerData, innerHash, blockSize); CryptEncode(CRYPT_HASH_SHA256, outerData, uSKey, finalHash); string signature = ""; for(int i = 0; i < ArraySize(finalHash); i++) signature += StringFormat("%02x", finalHash[i]); Print(signature); string query_string = ""; query_string += "&signature=" + signature; url = "https://api.binance.com/api/v3/order?" + params + "&signature=" + signature; status = WebRequest("POST", url, headers, 5000, datas, result, response_headers); server_response = CharArrayToString(result); Print(server_response); }
解説:
最初の変数セットでは、最終的なAPIリクエストと認証処理の準備をおこないます。messageおよびsecret_keyは、生のバイト形式の配列としてそれぞれ格納されます。これは、暗号学的ハッシュ処理が文字列ではなくバイト単位でおこなわれるためです。HMAC操作におけるSHA256アルゴリズムの標準ブロックサイズは64バイトに設定されています。このため、内側パディング用と外側パディング用に、それぞれ別のパディング配列を用意します。HMAC処理はいくつかの重要な要素から構成されるため、ハッシュ結果や中間データを保存するための追加配列も定義されます。このように分割しておくことで、HMACの各ステップを追いやすくなります。
秘密鍵とAPIキー文字列はそれぞれ独立して定義されます。APIキーはリクエストヘッダー内でアカウントを識別するために使用されますが、署名そのものには含まれません。その後、必要なBinance形式に従ってヘッダー文字列を作成し、改行を含めて整形します。このヘッダーは注文リクエスト送信時に使用されます。続いて、サーバーレスポンスを処理するための変数群も定義されます。これには最終レスポンステキストを保持する変数、生のレスポンスデータを格納する配列、レスポンスヘッダー用の文字列などが含まれます。また、URL、ステータスコード、最終レスポンス文字列も後続処理のために準備されます。
次に、メッセージ文字列とシークレットキーからバイト配列を生成します。このステップは署名生成処理において非常に重要です。UTF-8エンコーディングを使用することで、バイト表現がBinanceの期待する形式と一致するようにします。この時点で、クエリ文字列とシークレットキーの両方が生のバイトデータへ変換されます。SHA256のブロックサイズに合わせて、内側および外側パディング配列をリサイズします。その後、ループ処理によって固定の16進値とシークレットキーのバイト列を組み合わせて各パディングが構築されます。1つは内側パディング、もう1つは外側パディングとして使用されます。この変換により、シークレットキーが安全にハッシュ計算に組み込まれ、HMAC標準に準拠した形になります。
次に、内側のデータが構築されます。これは、内側パディングの後ろにメッセージバイト列を追加することで作成されます。このデータはHMAC計算の最初のステップを表します。この内側のデータからSHA256ハッシュを生成し、インナーハッシュを取得します。この時点で秘密鍵の影響はすでに安全に反映されています。続いてアウターパディングをコピーし、その後ろにインナーハッシュを結合して外側のデータを作成します。これはHMAC処理における最終入力データです。この外側のデータに対して再度SHA256ハッシュを適用することで、最終的なハッシュが生成されます。これがリクエストの正当性を証明する最終的な暗号署名となります。
生成されたハッシュはまだ生のバイト列であるため、可読性のある16進文字列へ変換されます。各バイトを2桁の16進数として整形し、文字列へ追加していきます。この最終文字列がBinanceが要求する正しい署名フォーマットになります。デバッグや検証のために出力することも可能です。署名が完成したら、それをクエリ文字列に追加パラメータとして付与します。
Binanceの仕様では、署名はヘッダーではなくURLパラメータとして送信する必要があります。そのため、Binanceの注文エンドポイント、事前に生成したパラメータ、そして署名を組み合わせて最終的なリクエストURLを構築します。その後、完全に署名されたURLと認証ヘッダーを使用して、WebRequest関数からPOSTリクエストをBinanceへ送信します。リクエスト処理が完了すると、レスポンスは生のバイトデータとして受け取られます。
結論
この記事では、MQL5におけるBinance APIの全体的なプロセスを扱いました。ローソク足データの取得と整理から始まり、強気の包み足パターンの識別、そして認証付きトレード注文の送信までを一通り解説しました。署名付きリクエストの構築方法、パラメータの正しい取り扱い方、そしてWebRequest関数を用いた実際の取引実行方法についても学びました。Binance APIを通じて注文を出すという最終ステップをもって、本記事はAPIシリーズの締めくくりとなり、MetaTrader 5における市場データ分析から実際の取引執行までの一連の流れが完成します。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21061
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
プライスアクション分析ツールキットの開発(第58回):レンジ収縮分析および成熟度分類モジュール
MQL5取引ツール(第15回):Canvas/ja/ぼかし効果、影描画、滑らかなマウスホイールスクロール
初心者からエキスパートへ:流動性ベースの取引戦略の構築
グラフ理論:取引における幅優先探索(BFS)/ja/応用
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索