
初心者からプロまでMQL5をマスターする(第6回):エキスパートアドバイザー開発の基礎
はじめに
ついにエキスパートアドバイザー(EA)作成の段階に到達しました。いわば、ルビコン川を渡ったのです。
この記事を最大限に活用するためには、以下の概念に慣れていることが望ましいです。
- 変数(ローカル変数・グローバル変数)
- 関数とそのパラメータ(参照渡しと値渡しの両方)
- 配列(シリアル配列の基本理解を含む)
- 論理演算子、算術演算子、条件分岐(if、switch、三項演算子)、ループ(主にforループ、whileやdo...whileも知っていると便利)
プログラマーの視点から見れば、EAは前回の記事で扱ったインジケーターよりもそれほど複雑ではありません。取引ロジックも基本的には、ある条件をチェックし、満たされたらアクションを実行する(通常はサーバーに注文を送信する)流れです。重要なのは注文の構造を理解し、注文送信関数を知り、取引に必要なデータへアクセスできることです。
重要:この記事で紹介するEAは、あくまでプログラミング原理の説明を目的としており、実際の取引や利益を出すためのものではありません。もし実際の口座で使う予定がある場合は、意思決定アルゴリズムを改善する必要があります。そうしなければ損失が発生する恐れがあります。
実際、このコードはエントリーロジックを改善しても実取引には適しません。最初の例はリクエスト送信やサーバー応答のエラーハンドリングが一切ありません。これはコードを簡単にし理解しやすくするための意図的な省略であり、素早い試作や基本戦略ロジックのテストに限った使用を想定しています。2つ目のEAは多少の検証を含みますが、市場での公開や安定した実取引には不十分です。問題が発生した場合に適切に対処(単にエラー報告して停止するだけでなく)する必要があります。
完全に機能する、かつ市場での公開に耐えうるEAについては次回の記事で扱います。そのEAは必要な検証やより複雑なロジックを備えています。今回の記事では取引自動化の基礎を中心に説明します。インジケーターを使わないEAと、標準の移動平均線インジケーターを使うEAの2つを作成します。前者は予約注文、後者は成行注文での取引です。
EAテンプレート
すべてのEAは、通常MQL5ウィザード(図1)を使用して、空のテンプレートを作成することから始まります。
図1:MQLウィザード - 最初の画面
MQLウィザードには、テンプレートからEAを作成する(上部のオプション)、またはより高度で構造化されたバージョンを生成するという2つの主なオプションがあります。初心者プログラマーの場合は最初のオプション、つまりテンプレートを選択することを強くお勧めします。
より高度な生成バージョンはオブジェクト指向で、複数のファイルに分割されています。追加のツールや経験がなければ、理解するのがかなり難しく、自分の取引ロジックに合わせてカスタマイズするのはさらに困難です。そのため、オブジェクト指向プログラミング(OOP)の概念や実践をしっかり理解してから、このバージョンを使うことをお勧めします。生成されたコードを確認することは、「何が可能かを見る」という点で勉強になるかもしれませんが、それはあくまで多くの実装のうちの1つであり、自動生成に最適化されたものだということを覚えておいてください。そのクラス構造の細部を完全に理解できる頃には、自分でコードテンプレートを書くほうが好ましくなるでしょう。当然、自分で書いたコードを編集するほうが他人のコードを解読するよりもはるかに簡単です。そして、おそらく自分のテンプレートはウィザードが提供するものと同じか、それ以上に良いものになるでしょう。
ウィザードが追加できる多くのオプション関数(図2と図3)は必須ではありませんが、非常に役立つことが多いです。たとえば、3番目のウィザード画面(図2)にある関数は、サーバーがシグナルを受信した時、ポジションが開かれた時などの取引操作中にトリガーされるイベント(OnTradeやOnTradeTransaction)、タイマーイベント(OnTimer)、ボタンの押下やオブジェクトの作成などのチャート操作(OnChartEvent)、およびオーダーブックの更新(OnBookEvent)を扱うことができます。
図2:EAの作成 - ウィザードの3番目の画面(追加のEA機能)
ストラテジーテスター内でのみ使用され、通常の運用時には使われない特殊な関数もあります(図3)。これらは主に、テスター上でのみ動作し、リアル口座では動作しないデモ版に役立ちます。テスト中により詳細なログが必要だったり、別のデータソースから情報を取得したい場合に使うことがあります。個人的にはこれらの関数はあまり使いませんが、適切な状況では非常に有用です。
図3:EAの作成 - ウィザードの 4 番目の画面 (テスター専用の操作機能)
図2の関数のうちいくつかは、今後の記事でより詳しく解説しますが、図3の関数についてはご自身で探求していただく形にします。
ウィザードを使ってEAを作成すると、生成されるファイルには必ず最低でも3つの関数が含まれています(例1)。
//+------------------------------------------------------------------+ //| FirstExpert.mq5 | //| Oleg Fedorov (aka certain) | //| mailto:coder.fedorov@gmail.com | //+------------------------------------------------------------------+ #property copyright "Oleg Fedorov (aka certain)" #property link "mailto:coder.fedorov@gmail.com" #property version "1.00" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
例1:ウィザードによって作成されたEAの最小限のテンプレート
- OnInit:インジケーター開発でおなじみの関数で、初期設定に使われます。プログラムの開始時に一度だけ実行されます。
- OnDeinit:こちらも馴染みがあるかと思いますが、EAが停止されたときに呼び出される関数です。EAが作成したグラフィカルオブジェクトの削除やファイルのクローズ、インジケーターリソースの解放など、終了処理をおこなうために使います。
- OnTick:すべてのティックで実行される関数で(インジケーターのOnCalculateに似ています)、ここでEAのコアロジックが動作します。
これらのうち、EAに必須なのはOnTickだけです。EAがシンプルな場合は、OnInitやOnDeinitは省略しても問題ありません。
MetaTrader 5自動売買における重要な用語
MetaTrader 5で自動売買システムを開発する際に、すべてのプログラマーが理解しておくべき用語や概念があります。これらの用語は取引ロジックや関数名に関連しています。順番に見ていきましょう。
注文(order):特定の価格で特定の銘柄を買うまたは売る意図をサーバーに伝えるメッセージです。
取引のあらゆる変更(たとえば成行注文の発注やストップロスの変更)はすべて注文を通じておこなわれます。注文には即時執行(成行注文のように現在の市場価格で売買する)と、価格条件が満たされたときに発動する予約注文(ストップ注文やリミット注文など)があります。
予約注文にはストップロス、テイクプロフィット、買い/売りストップ、買い/売りリミット、買い/売りストップリミットなどがあります。注文処理に関連する関数例としては、OrderSend(注文をサーバーに送信)、OrderGetInteger(チケット番号や作成時刻など注文の整数パラメータを取得)などがあります。
約定(deal):注文の実際の約定(取引成立)です。
MetaTrader 5における取引成立は主に履歴に関連し、注文が約定した瞬間を指します。取引成立はサーバー上で行われるため直接操作することはできませんが、約定価格や時刻などの履歴情報を取得できます。関数例として、HistoryDealGetDouble(価格などの取引のdoubleパラメータを取得)、HistoryDealsTotal(履歴内の取引の合計数を返す)などがあります。
ポジション(position):特定の銘柄に対する複数の取引の結果としての保有状況です。
MetaTrader 5は当初、各銘柄ごとにポジションは1つだけという設計でした。しかし、実際の動作は口座タイプによります。ネットティング口座ではすべての取引が1つのポジションに反映されますが、ヘッジ口座では取引ごとにポジションが作成され(ストップ注文は除く)、同じ銘柄で複数のポジションや逆方向のポジションが存在する場合があります。この場合、ネットの買い・売りの合計量を自分で計算する必要があります。ポジションは注文によって修正可能で、全決済・一部決済、ストップロスやテイクプロフィットの調整などをおこなうことができます。ポジション関連の関数例として、PositionSelectByTicket(チケットでポジションを選択)およびPositionGetString(銘柄名やユーザーコメントなど文字列パラメータを取得)があります。
すべての取引成立は注文の実行結果であり、すべてのポジションは1つ以上の取引成立の累積結果を表します。
イベント(event):プログラム環境における重要な出来事のことです。
取引リクエストの送信はイベントですし、サーバーがリクエストを受理したこと、ユーザーがチャートをクリックしたこと、チャートのスケールが変わったこと、新しいティックが発生したこともすべてイベントです。これらのうちいくつかは、OnInit(初期化イベント)やOnTick(ティックイベント)のように、Onで始まる標準的なハンドラ関数で処理されます。
その他のイベントは定数識別子を使って処理されます。つまり、まずグローバルなイベントハンドラ関数の1つが呼ばれます。たとえば、OnChartEventはチャートに関するすべてのイベントで呼び出され、その中でイベントコード変数と定数を比較して具体的なイベントタイプを判別します。これらの関数に渡されるパラメータによってイベントの詳細がわかります。本記事ではこれら細かなイベントの詳細には触れません。
MetaTrader 5における自動売買の基本原則
まずは、MetaTrader 5で取引がどのように実際に動いているかを高い視点から見てみましょう。重要な事実から始めます。
あなたのコンピュータ上で動作する端末ソフトウェアと、あなたの資金を使って取引を実行するサーバーソフトウェアは別々のプログラムです。これらはネットワーク(通常はインターネット)を介してのみ通信しています。
したがって、[買]や[売]をクリックすると、次のような一連のイベントが起きます。
- 端末は特別なデータパケットを生成し、MqlTradeRequestという特別な構造体に必要な情報を入力します。
- この構造体はOrderSend(同期モード)またはOrderSendAsync(非同期モード)を使ってサーバーに送信され、取引注文が形成されます。
- サーバーはこのパケットを受け取り、条件をすべて満たしているかをチェックします。例えば、マッチする価格があるか、残高が十分かなど。
- 問題なければ、注文は他のトレーダーの注文とともに注文キューに入れられ、実行を待ちます。
- 確認メッセージが端末に返されます。
- 市場が指定された価格レベルに達すると、サーバーは注文を約定させ、そのイベントをログに記録します。
- サーバーは約定結果を端末に送信します。
- 端末はMqlTradeResultいう構造体の形で結果を受け取り、TradeやTradeTransactionといった対応するイベントを発生させます。
- 端末はサーバーからのエラーをチェックします(MqlTradeResult構造体のretcodeフィールドを確認)。
- 問題がなければ、端末は内部変数、ログ、グラフィカルなチャートを更新します。
- 結果として、該当する銘柄の新しいポジション(または更新されたポジション)があなたのポートフォリオに表示されます。
この一連の流れは、図4のような簡略化した図で視覚化できます。
図4:取引注文処理図
非同期データ転送モード
端末とサーバーのやり取りにおいて、端末は少なくとも2回ネットワーク通信をおこなう必要があることに気づいたかもしれません。1回はデータを送信するため、もう1回はサーバーからの応答を受け取るためです。OrderSend関数を使う場合、この処理は基本的に同期的です。つまり、EAは応答を待ち、その間インターネットの帯域やCPU時間などシステムリソースを占有します。
しかし、CPUの視点から見るとネットワーク処理は非常に遅いものです。取引スクリプトは通常数百マイクロ秒(例:200μs = 0.0002秒)で実行されますが、ネットワーク通信は通常ミリ秒単位(例:20ms = 0.02秒)で測られ、少なくとも100倍遅いです。これに加えてサーバー側の処理時間やメンテナンス・技術的な問題による予期せぬ遅延が発生することもあります。最悪の場合、取引リクエストを送信してから応答を受け取るまでに数秒、場合によっては数分かかることもあります。もし複数のEAがこの間ずっと待機状態であれば、CPUリソースの多くが無駄に消費されてしまいます。
この非効率性を解消するために、MetaTraderは特別な非同期取引モードを提供しています。非同期とは、EAが取引リクエストを送信した後、応答を待たずに別の処理(スリープ、計算、その他のタスク)を続けられることを意味します。サーバーの応答が到着すると、端末はTradeTransactionイベント(続いてTradeイベント)を発生させます。EAはそのとき「目を覚まし」、応答を処理してさらなる取引判断を行うことができます。この方法の利点は明らかです。
同期モード、非同期モードのどちらでも、取引エラーはOnTradeTransaction関数で処理されます。コードが複雑になるわけではなく、一部のロジックがOnTickからOnTradeTransactionに移動するだけです。もしこのコードを別の関数にまとめておけば、呼び出しや受け渡しで問題は一切起きません。したがって、同期モードと非同期モードの選択は完全にあなたの好みや課題次第です。両モードで使われるデータ構造は同じままです。
取引開始
ここでは、インサイドバーを検出して取引をおこなうFOREX市場向けのEAを作成するとしましょう。念のため説明すると、インサイドバーとは、前の(より大きな)ローソク足の高値と安値の範囲内に完全に収まっているローソク足のことです。このEAはローソク足ごとに1回動作し、インサイドバーのパターンを検出すると同時に、以下の2つの予約注文を出します。
- より大きなローソク足の高値から数ポイント(設定可能)上に買いストップ注文
- 同じ距離だけそのローソク足の安値より下に売りストップ注文
- 各注文の有効期限は2本のローソク足分で、この期間内に約定しなければ削除されます。
- 両方の注文のストップロスはより大きなローソク足の中心点に置きます
- テイクプロフィットはより大きなローソク足の値幅の7/8に設定します
- 取引数量は最小ロットとします
この初期バージョンのEAはコードをわかりやすく保つため、追加条件は省略します。後で拡張できる骨組みとなるフレームワークを作成します。まず、ウィザードを使ってEAテンプレートを作成し、第3画面のすべてのチェックボックスは外しておきます(このバージョンではサーバー応答を処理しないため)。生成されるコードは例1に似たものになります。EAを設定・最適化可能にするため、4つの入力パラメータを定義します。予約注文を置くための高値/安値からの距離(inp_PipsToExtremum)、ストップロスとテイクプロフィットを置くための距離の係数(inp_StopCoefficientとinp_TakeCoefficient)、約定しなかった注文を削除するまでのローソク足本数(inp_BarsForOrderExpired)です。さらに、EA用のマジックナンバーも宣言します。これは自分の注文を他のEAや手動で出した注文と区別するために使います。
//--- declare and initialize input parameters input int inp_PipsToExtremum = 2; input double inp_TakeCoeffcient = 0.875; input double inp_StopCoeffcient = 0.5; input int inp_BarsForOrderExpired = 2; //--- declare and initialize global variables #define EXPERT_MAGIC 11223344
シナリオ2:EAの入力パラメータとマジックナンバーの説明
注意:例2のコードは、EAファイルの一番上、#propertyディレクティブの直後に配置する必要があります。
この例の残りのコードはOnTick関数内に配置されます。今のところ、他のすべての関数は空のままにしておきます。OnTick本体に配置する必要があるコードは次のとおりです。
/**************************************************************** * Please note: this Expert Advisor uses standard functions * * to access price/time data. Therefore, it's convenient to * * work with series as arrays (time and prices). * ****************************************************************/ string symbolName = Symbol(); ENUM_TIMEFRAMES period = PERIOD_CURRENT; //--- Define a new candlestick (Operations only at the start of a new candlestick) static datetime timePreviousBar = 0; // Time of the previous candlestick datetime timeCurrentBar; // Time of the current candlestick // Get the time of the current candlestick using the standard function timeCurrentBar = iTime( symbolName, // Symbol name period, // Period 0 // Candlestick index (remember it's series) ); if(timeCurrentBar==timePreviousBar) { // If the time of the current and previous candlesticks match return; // Exit the function and do nothing } // Otherwise the current candlestick becomes the previous one, // so as not to trade on the next tick timePreviousBar = timeCurrentBar; //--- Prepare data for trading double volume=SymbolInfoDouble(symbolName,SYMBOL_VOLUME_MIN); // Volume (lots) - get minimum allowed volume // Candlestick extrema double high[],low[]; // Declare arrays // Declare that arrays are series ArraySetAsSeries(high,true); ArraySetAsSeries(low,true); // Fill arrays with values of first two closed candlesticks // (start copying with index 1 // as we only need closed candlesticks; use 2 values) CopyHigh(symbolName,period,1,2,high); CopyLow(symbolName,period,1,2,low); double lengthPreviousBar; // The range of the "long" bar MqlTradeRequest request; // Request structure MqlTradeResult result; // Server response structure if( // If the first closed bar is inside high[0]<high[1] && low[0]>low[1] ) { // Calculate the range lengthPreviousBar=high[1]-low[1]; // Timeseries have right-to-left indexing //--- Prepare data for a buy order request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume deal request.type =ORDER_TYPE_BUY_STOP; // order action (buy) request.price =high[1] + inp_PipsToExtremum*Point(); // buy price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =high[1]-lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =high[1]+lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a buy order to the server OrderSend(request,result); // For asynchronous mode you need to use OrderSendAsync(request,result); //--- Clear the request and response structures for reuse ZeroMemory(request); ZeroMemory(result); //--- Prepare data for a sell order. Parameers are the same as in the previous function. request.action =TRADE_ACTION_PENDING; // order type (pending) request.symbol =symbolName; // symbol name request.volume =volume; // volume request.type =ORDER_TYPE_SELL_STOP; // order action (sell) request.price =low[1] - inp_PipsToExtremum*Point(); // sell price // Optional parameters request.deviation =5; // acceptable deviation from the price request.magic =EXPERT_MAGIC; // EA's magic number request.type_time =ORDER_TIME_SPECIFIED; // Parameter is required to set the lifetime request.expiration =timeCurrentBar+ PeriodSeconds()*inp_BarsForOrderExpired; // Order lifetime request.sl =low[1]+lengthPreviousBar*inp_StopCoeffcient; // Stop Loss request.tp =low[1]-lengthPreviousBar*inp_TakeCoeffcient; // Take Profit //--- Send a sell order to the server OrderSend(request,result); }
例3:このEAのOnTick関数にはすべての取引ロジックが含まれている
標準のPoint関数は、現在のチャートのポイントサイズを返します。たとえば、ブローカーが5桁の価格を提供している場合、EURUSDのポイントは0.00001となり、USDJPYでは0.001となります。関数iTime、iHigh、iLowを使うと、特定のローソク足(右から左へ0が現在のバー)の時間、高値、安値を取得できます。この例では、新しいバーの判定にiTimeを使って現在時刻を取得しています。高値と安値を取得するには、配列コピー関数のCopyHighとCopyLowを使用しました。
コードは、新しいバーのチェック部分と取引部分(準備フェーズから始まる)の主に2つのセクションに分かれています。取引ブロックはさらに買い注文用と売り注文用のほぼ同一の2つのセグメントに分かれています。両者の構造設定や注文送信ロジックは非常に似ているため、共通部分を別の関数にまとめ、注文種別や実行価格(価格、TP、SL)だけ分岐させるようリファクタリングするのが理にかなっています。しかしこの例では、コードの簡潔さや再利用性よりも、わかりやすさと読みやすさを優先してあえて分けています。
処理の各段階はコメント(//---)で区切られており、その段階内のコメントは普通のスタイルで書かれています。取引ロジックは大きく分けて、リクエスト構造体の作成部分と送信部分の2つです。リクエスト構造体を埋める際に必須なのは最初の5つのフィールドだけです。
また、この例のように注文の有効期限を使う場合は、request.type_timeとrequest.expirationの両方を必ず設定する必要があります。前者を未設定にすると、後者は自動的に無視されてしまいます。
このEAの動作をテストしたい場合は、デモ口座で任意の時間足で実行可能です(1分足でも動作しますが、実際のパフォーマンスは選択した銘柄のスプレッドに依存します)。または、MetaEditorで<Ctrl> + <F5>を押してストラテジーテスターで過去データを使ったバックテストを起動してください。完全なソースコードは、添付ファイルTrendPendings.mq5にあります。
標準インジケーターの使用
例3のEAはインジケーターを使いませんでしたが、常にそうとは限りません。標準インジケーターをベースにした戦略には主に2つの方法があります。組み込みのインジケーター関数を使う方法と、インジケータークラスを使う方法です。ここではまず組み込み関数を使う方法から説明します。
例えば単純移動平均(SMA)をベースにしたEAを作成するとしましょう。この記事の目的は利益を出す戦略を作ることではなく、基本的な取引ロジックを示すことです。ですので、コードはできるだけシンプルで読みやすく保ちます。これに基づいて、取引ルールを次のように定義します。
- 前回のEAと同様、新しいバーが形成されたときのみ取引を検討する。
- 買いの場合は、前のローソク足の終値が移動平均線の上にあること。
- 売りの場合は、前のローソク足の終値が移動平均線の下にあること。
- フィルターとして移動平均線の傾きを使う。直近の2本のバーで移動平均線が上昇していれば買い、下降していれば売り。
- 逆のシグナルが出たらポジションを決済する。
- 保護用のストップロスはシグナルとなったローソク足の高値(売りの場合)または安値(買いの場合)に設定する。
- ヘッジ口座でもシンボルごとにポジションは1つだけ許可し、シグナルが出てもすでにポジションがあればスキップする。
図5は、このEAで使用されるフィルタリング原理を示しています。
図5:移動平均ベースのEAで使用されるローソク足フィルタリング原理
このEAでは、コードをできるだけシンプルでわかりやすく保つことを目指し続けますが、実際の運用に近づけるためにエラーチェックを増やしていきます。
移動平均のすべてのパラメータに加え、注文価格から許容される最大の価格乖離も入力パラメータとして追加します。さらに、インジケーターのハンドル用のグローバル変数を宣言します(この点は後ほど説明します)。そして、EAのマジックナンバーも定義します。
//--- declare and initialize global variables #define EXPERT_MAGIC 3345677 input int inp_maPeriod = 3; // MA period input int inp_maShift = 0; // Shift input ENUM_MA_METHOD inp_maMethod = MODE_SMA; // Calculation mode input ENUM_APPLIED_PRICE inp_maAppliedPrice = PRICE_CLOSE; // Applied price input int inp_deviation = 5; // Max price deviation from the request price in points //--- MA indicator handle int g_maHandle;
例4:移動平均線を使った取引のためのEAのグローバル変数
EAや他のインジケーターで何かインジケーターを使う前に、以下の3つのことをおこなう必要があります。
-
インジケーターを初期化し、そのハンドルを取得すること。これは通常、組み込みのインジケーター関数(例えば移動平均の場合はiMA)やカスタムインジケーター用のiCustomを使っておこないます。この初期化は通常、OnInit関数内で実施します。
プログラミングにおける「ハンドル」とは、コードがアクセスできるリソースへの参照やポインタのようなものです。インジケーターにアクセスし、そのデータを必要に応じて取得するための「チケット番号」のように考えてください。 - インジケーターの値を使う前に、最新のデータを取得すること。これは通常、インジケーターのバッファごとに1つの配列を作り、CopyBuffer関数を使ってその配列にデータをコピーすることでおこないます。
- 取得した配列のデータを使うこと。
- メモリリークや不要なリソース消費を避けるために、プログラム終了時にインジケーターのハンドルを解放すること。これは重要で、OnDeinit関数内でIndicatorRelease関数を使っておこないます。
この特定のEAでは、OnInit関数とOnDeinit関数は非常に単純で、異常または複雑なロジックは含まれていません。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Before an action that could potentially cause an error, reset // built-in _LastError variable to default // (assuming there's no error yet) ResetLastError(); //--- The standard iMA function returns the indicator handle g_maHandle = iMA( _Symbol, // Symbol PERIOD_CURRENT, // Chart period inp_maPeriod, // MA period inp_maShift, // MA shift inp_maMethod, // MA calculation method inp_maAppliedPrice // Applied price ); // inp_maAppliedPrice in general case can be // either a price type as in this example, // (from ENUM_APPLIED_PRICE), // or a handle of another indicator //--- if the handle is not created if(g_maHandle==INVALID_HANDLE) { //--- report failure and output error code PrintFormat("Failed to crate iMA indicator handle for the pair %s/%s, error code is %d", _Symbol, EnumToString(_Period), GetLastError() // Output error code ); //--- If an error occurs, terminate the EA early return(INIT_FAILED); } //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Release resources occupied by the indicator if(g_maHandle!=INVALID_HANDLE) IndicatorRelease(g_maHandle); }
例5:EAにおけるインジケーターの初期化と初期化解除
一点だけ注意していただきたいのは、初期化関数内で使われているResetLastError関数とGetLastError関数のペアの使い方です。ResetLastErrorはシステム変数「_LastError」を「エラーなし」の状態にリセットし、GetLastErrorは直近で発生したエラーがあればそのエラーコードを取得するための関数です。
それ以外は非常にシンプルです。インジケーター(iMAを含む)の初期化関数は、有効なインジケーターハンドルを返すか、ハンドルが取得できなかった場合は特殊な定数「INVALID_HANDLE」を返します。この仕組みにより、何か問題が起きたことを検知し、適切にエラー処理(この場合はエラーメッセージの表示)をおこなうことができます。OnInitがINIT_FAILEDを返した場合、EA(やインジケーター)は起動しません。そして実際、移動平均インジケーターの有効な参照を得られなければ、処理を停止するのが正しい対応です。
OnTick関数については、ステップごとに分解して説明します。最初の部分は変数の宣言と初期化です。
//--- Declare and initialize variables MqlTradeRequest requestMakePosition; // Request structure for opening a new position MqlTradeRequest requestClosePosition; // Request structure for closing an existing position MqlTradeResult result; // Structure for receiving the server's response MqlTradeCheckResult checkResult; // Structure for validating the request before sending bool positionExists = false; // Flag indicating if a position exists bool tradingNeeds = false; // Flag indicating whether trading is allowed ENUM_POSITION_TYPE positionType; // Type of currently open position ENUM_POSITION_TYPE tradingType; // Desired position type (used for comparison) ENUM_ORDER_TYPE orderType; // Desired order type double requestPrice=0; // Entry price for the future position /* The MqlRates structure contains all candle data: open, close, high, and low prices, tick volume, real volume, spread, and time. In this example, I decided to demonstrate how to fill the entire structure at once, instead of retrieving each value separately. */ MqlRates rates[]; // Array of price data used for evaluating trading conditions double maValues[]; // Array of MA values // Declare data arrays as series ArraySetAsSeries(rates,true); ArraySetAsSeries(maValues,true);
例6:OnTick関数のローカル変数
バーが出現したかどうかの同様のチェックがあります。
//--- Check whether there's a new bar static datetime previousTime = iTime(_Symbol,PERIOD_CURRENT,0); datetime currentTime = iTime(_Symbol,PERIOD_CURRENT,0); if(previousTime==currentTime) { return; } previousTime=currentTime;
例7:新しいバーかどうか確認する
次に、必要なデータを特別な関数を使って取得します。ここでは、たとえば端末が必要なデータをまだ読み込めていないなどのエラーが起こる可能性があると想定し、分岐演算子を使ってそれらの潜在的なエラーを処理しています。
//--- Prepare data for processing // Copy the quotes of two bars, starting from the first one if(CopyRates(_Symbol,PERIOD_CURRENT,1,2,rates)<=0) { PrintFormat("Data error for symbol %s, error code is %d", _Symbol, GetLastError()); return; } // Copy the values of the moving average indicator buffer if(CopyBuffer(g_maHandle,0,1,2,maValues)<=0) { PrintFormat("Error getting indicator data, error code is %d", GetLastError()); return; }
例8:現在のインジケーターデータとクオートをローカル配列にコピーする
そして現在の銘柄に対して、標準のPositionSelect関数を使ってポジションを選択します。めいがラごとにポジションは1つだけという前提なので大きな問題はないはずですが、念のため何が問題になる可能性があるかを考えます。少なくとも、そのポジションが自分のEAによって開かれたものであるかを確認する必要があります。
//--- Determine if there is an open position if(PositionSelect(_Symbol)) { // Set the open position flag - for further processing positionExists = true; // Save the type of the open position positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE); // Check if the position has been opened by our EA requestClosePosition.magic = PositionGetInteger(POSITION_MAGIC); // I didn't create a separate variable for // the existing position magic number if(requestClosePosition.magic!= EXPERT_MAGIC) { // Some other EA started trading our symbol. Let it do so... return; } // if(requestClosePosition.magic!= EXPERT_MAGIC) } // if(PositionSelect(_Symbol))
例9:現在のポジションデータの取得
これで取引条件のチェックが可能になります。この例では、チェック結果を別々の変数に保存し、その後、買いか売りかの最終判断にそれらの変数を使います。大規模で複雑なソリューションでは、この方法はまず柔軟性のために有効であり、次に最終的なコードが短くなるという利点がありますここでは主に後者の理由でこの手法を使っています。最終的なアルゴリズムがあまり明確でないため、異なる方法で可読性を高めようとしているのです。
//--- Check trading conditions, if( // Conditions for BUY rates[0].close>maValues[0] // If the first candlestick closed above MA && maValues[0]>maValues[1] // and the MA slope is upwards ) { // Set the trade flag tradingNeeds = true; // and inform the EA about the direction (here - BUY) tradingType = POSITION_TYPE_BUY; // to check the direction of the open direction orderType = ORDER_TYPE_BUY; // to trade in the right direction // calculate the deal price requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_ASK); } else if( // conditions for SELL rates[0].close<maValues[0] && maValues[0]<maValues[1] ) { tradingNeeds = true; tradingType = POSITION_TYPE_SELL; orderType = ORDER_TYPE_SELL; requestPrice = SymbolInfoDouble(_Symbol,SYMBOL_BID); }
例10:取引条件の確認
以下のコードは、現在の時点で取引を実行すべきかどうかを判断します。判断は3つの重要な質問に基づいています。
- 取引の準備は整っているか:つまり、ローソク足の終値が移動平均線を超えて閉じているかどうか。この条件は変数tradingNeedsで管理されます。もし「いいえ」(tradingNeeds == false)なら、取引はおこなわれません。
- すでにポジションがあるか:これは変数positionExistsでチェックされます。ポジションがなければ取引を進め、あれば次のチェックに進みます。
- 既存のポジションは新しい取引シグナルと同じ方向か、反対方向か:これはtradingTypeとpositionTypeを比較して判断します。等しい場合は同方向なので新しい取引はおこないません。異なる場合は現在のポジションは逆方向であり、新規取引を始める前に決済しなければなりません。
この意思決定ロジックはフローチャート(図6)に視覚化されています。
図6:取引アルゴリズムの主な決定ポイントを表すフローチャート
MQL5では、ポジションのクローズもオープンも成行注文を送信することでおこないます。この方法はすでにご存知の通り、取引リクエスト構造体に必要事項を記入してサーバーに送信するというものです。これら2つのの違いは、ポジションをクローズする際には、そのポジションのチケット番号を指定し、既存ポジションのパラメータを正確にリクエストにコピーする必要がある点です。新規ポジションをオープンする場合はコピーするものがなく、古いポジションのチケットもないため、そのような情報を渡す必要がありません。
予約注文を使った前の例と比べると、ここで異なるフィールドは2つあります。
- actionフィールドは、以前はTRADE_ACTION_PENDINGだったが、今はTRADE_ACTION_DEALになっている
- typeフィールドは、予約注文ではなく成行注文(ORDER_TYPE_BUYまたはORDER_TYPE_SELL)を表している
コードの各部分と図6のフローチャートの分岐ロジックの対応が分かりやすいように、サンプルコードには色分けが施されています。
また、例3との主な違いが2点あります。取引リクエストを送信する前に、OrderCheck関数で構造体の検証をおこない、不正なフィールドの記入ミスを検出します。検証結果は戻りコード(retcode)とテキストによる説明(comment)として得られます。リクエスト送信後は、サーバーが注文を受け入れたかどうかをチェックし、エラーがあれば適切なメッセージで報告します。
// If the setup is to trade if(tradingNeeds) { // If there is a position if(positionExists) { // And it is opposite to the desired direction of trade if(positionType != tradingType) { //--- Close the position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(requestClosePosition); ZeroMemory(checkResult); ZeroMemory(result); //--- set operation parameters // Get position ticket requestClosePosition.position = PositionGetInteger(POSITION_TICKET); // Closing a position is just a trade requestClosePosition.action = TRADE_ACTION_DEAL; // position type is opposite to current trading direction, // therefore, for the closing deal, we can use the current order type requestClosePosition.type = orderType; // Current price requestClosePosition.price = requestPrice; // Operation volume must match the current position volume requestClosePosition.volume = PositionGetDouble(POSITION_VOLUME); // Set acceptable deviation from the current price requestClosePosition.deviation = inp_deviation; // Symbol requestClosePosition.symbol = Symbol(); // Position magic number requestClosePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestClosePosition,checkResult)) { // If the structure is filled incorrectly, display a message PrintFormat("Error when checking an order to close position: %d - %s",checkResult.retcode, checkResult.comment); } else { // Send order if(!OrderSend(requestClosePosition,result)) { // If position closing failed, report PrintFormat("Error closing position: %d - %s",result.retcode,result.comment); } // if(!OrderSend) } // else (!OrderCheck) } // if(positionType != tradingType) else { // Position opened in the same direction as the trade signal. Do not trade return; } // else(positionType != tradingType) } // if(positionExists) //--- Open a new position //--- Clear all participating structures, otherwise you may get an "invalid request" error ZeroMemory(result); ZeroMemory(checkResult); ZeroMemory(requestMakePosition); // Fill the request structure requestMakePosition.action = TRADE_ACTION_DEAL; requestMakePosition.symbol = Symbol(); requestMakePosition.volume = SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); requestMakePosition.type = orderType; // While waiting for position to close, the price could have changed requestMakePosition.price = orderType == ORDER_TYPE_BUY ? SymbolInfoDouble(_Symbol,SYMBOL_ASK) : SymbolInfoDouble(_Symbol,SYMBOL_BID) ; requestMakePosition.sl = orderType == ORDER_TYPE_BUY ? rates[0].low : rates[0].high; requestMakePosition.deviation = inp_deviation; requestMakePosition.magic = EXPERT_MAGIC; if(!OrderCheck(requestMakePosition,checkResult)) { // If the structure check fails, report a check error PrintFormat("Error when checking a new position order: %d - %s",checkResult.retcode, checkResult.comment); } else { if(!OrderSend(requestMakePosition,result)) { // If position opening failed, report an error PrintFormat("Error opening position: %d - %s",result.retcode,result.comment); } // if(!OrderSend(requestMakePosition // Trading completed, reset flag just in case tradingNeeds = false; } // else (!OrderCheck(requestMakePosition)) } // if(tradingNeeds)
例11:メインの取引コード(大部分は構造体への値の設定とエラー検査に費やされている)
この例の完全なソースコードは添付ファイル「MADeals.mq5」に含まれています。
標準ライブラリのインジケータークラスの使用
標準インジケーターのクラスは<Include\Indicators>フォルダにあります。すべてまとめて読み込みたい場合は<Include\Indicators\Indicators.mqh>(ファイル名の末尾にsが付く点に注意)をインポートします。またはグループごとに読み込むことも可能で、たとえばTrend.mqh、Oscillators.mqh、Volumes.mqh、BillWilliams.mqhなどがあります。時系列アクセス用クラスはTimeSeries.mqh、カスタムインジケーター用のクラスはCustom.mqhに分かれています。
フォルダ内のその他のファイルは補助モジュールであり、オブジェクト指向プログラミングに慣れていない方にはあまり使いどころがないかもしれません。フォルダ内の各「機能的」ファイルには関連する複数のクラスが含まれているのが一般的で、クラス名は一貫した命名規則に従っています。接頭辞に「C」が付き、インジケーター作成関数の名前と同じ名前が続きます。たとえば移動平均用のクラスはCiMAと呼ばれ、Trend.mqhに含まれています。
これらのクラスの使い方は、MQL5の組み込みインジケーター関数と非常によく似ています。主な違いはメソッドの呼び出し方や名前です。最初の段階(作成)では、Createメソッドを呼び出し、インジケーターに必要なパラメータを渡します。次の段階(データ取得)では、通常パラメータなしでRefreshメソッドを使います。必要に応じて、更新する時間足を指定することもできます(例:OBJ_PERIOD_D1 | OBJ_PERIOD_H1)。使用中はGetDataメソッドを使い、通常はバッファ番号とローソク足のインデックスの2つのパラメータを渡します(インデックスは時系列モデルに従い、右から左に増えます)。
例12では、CiMAクラスを使った最小限のEAを紹介しています。このEAは単に最初にクローズしたローソク足の移動平均の値を出力するだけです。このクラスベースの方法が実際の取引戦略でどのように使えるか見たい場合は、前のセクションのEA(MADeals.mq5)を新しいファイルにコピーし、該当する行を例12のコードに置き換えてみてください。
#include <Indicators\Indicators.mqh> CiMA g_ma; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Create the indicator g_ma.Create(_Symbol,PERIOD_CURRENT,3,0,MODE_SMA,PRICE_CLOSE); //--- return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Comment(""); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- Get data g_ma.Refresh(); //--- Use Comment( NormalizeDouble( g_ma.GetData(0,1), _Digits ) ); } //+------------------------------------------------------------------+
例12:CiMAクラス(移動平均インジケーター)の使用
結論
この記事を読んだことで、ローソク足データのみを使ったシンプルな取引戦略から、標準インジケーターのバッファを使ってシグナルを取得する戦略まで、あらゆる単純な取引戦略を素早くプロトタイプできるEAを作成できるようになったはずです。内容が難しすぎたと感じなければよかったのですが、もし感じた場合は、以前の記事の内容を復習すると理解が深まるかもしれません。
次回の記事では、「マーケット」で公開できるレベルの技術的に完成されたEAを紹介する予定です。このEAは、今回の記事の2番目の例よりもさらに多くの検証チェックを含み、より堅牢で信頼性の高いものになります。構造も多少変わり、OnTick関数がビジネスロジックの唯一の中心ではなくなり、コードを整理するための追加関数が登場します。最も重要なのは、注文発注時のエラー(リクオートなど)を扱う機能が加わることです。これを実現するために、OnTick関数を再構成し、EAの各「ステージ」(例:取引発注、新しいバーの待機、ロットサイズ計算など)に直接アクセスできるようにします。また、TradeTransactionイベントを使ってサーバーからの応答を追跡します。結果として、OOPに深く踏み込むことなく機能的に整理され、容易に修正可能なテンプレートが完成し、あらゆる複雑さのEAを作成できるようになります。
以下は、本連載の過去の記事のリストです。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/15727
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。




- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
とても分かりやすい記事で、多くのことが説明されている。特に最後の方で、クラスを通してインジケータを使用する方法が紹介されています!シンプルなTSの開発でプロトタイプをテストすることを検討します。