平均足を使ったプロフェッショナルな取引システムの構築(第2回):EAの開発
はじめに
本記事は、連載「平均足を使ったプロフェッショナルな取引システムの構築」の第2回です。第1回では、MetaQuotes Language 5 (MQL5)を用いて、カスタムインジケーター開発のベストプラクティスに沿った独自の平均足インジケーターを作成しました。本記事ではその次のステップとして、「Zen Breakout EA」を開発します。このEAは、前回作成したカスタム平均足インジケータとー、MetaTrader 5標準のフラクタル(Fractals)インジケーターを組み合わせ、信頼性の高いブレイクアウトシグナルを生成します。
コンセプトは非常にシンプルです。
- 強い陽の平均足がフラクタルインジケーターで検出された直近のスイングハイを上抜けて確定した場合、EAはロングポジションを建てる
- 強い陰の平均足が直近のスイングローを下抜けて確定した場合、EAはショートポジションを建てる
EAが建てる各ポジションには、明確に定義されたストップロス(SL)とテイクプロフィット(TP)が設定されており、リスクリワード比も自由に調整可能です。この記事を読み終える頃には、以下の内容を理解できるでしょう。
- カスタムインジケーターと標準インジケーターをEAに接続する方法
- 平均足とフラクタルを用いたブレイクアウトエントリーロジックの実装方法
- 柔軟なポジションサイズ管理(手動設定または口座リスク割合に基づく方式)の適用方法
- EAとインジケーターを単一ファイルとしてパッケージ化し、容易に配布できるようにする方法
戦略コンセプト
Zen Breakout戦略は、モメンタム検出とブレイクアウト確認を組み合わせた手法です。
- ロングのエントリー条件
ロングのエントリー条件は、大陽線の平均足ローソク足が直近のスイングハイを上抜けてクローズしたときに成立します。

- ショートのエントリー条件
ショートのエントリー条件は、大陰線の平均足ローソク足が直近のスイングローを下抜けてクローズしたときに成立します。

- ストップロスの設定
ロングポジションでは、ストップロスはブレイクアウトが確定した足の安値に設定します。

ショートポジションでは、ストップロスはブレイクアウトが確定した足の高値に設定します。

- テイクプロフィットの配置
テイクプロフィットは、設定可能なリスクリワード比に基づいて定義します。たとえば、1取引あたりのリスクが100ポイントで、リスクリワード比が1:2の場合、テイクプロフィットはエントリーから200ポイント離れた位置になります。
EAの準備
Zen Breakoutは取引シグナルを生成するために、2つのインジケーターから直接データを読み取ります。
- フラクタルインジケーター
フラクタルインジケーターはMetaTrader 5ターミナルに標準搭載されており、市場の直近スイングハイとスイングローを識別するためによく使われます。本EAではMQL5のiFractals()関数を使ってインジケーターを初期化し、以降の処理で使用するハンドルを取得します。
- カスタム平均足インジケーター
第1回で作成したカスタム平均足インジケーターを、強いモメンタムを伴うブレイクアウトの検出に使用します。プログラムからアクセスするにはMQL5のiCustom()関数でインジケーターを初期化し、そのハンドルを取得します。さらに、配布を容易にするためにインジケーターをEA内のリソースとしてパッケージ化し、単一の自己完結型ファイルとして配布できるようにします。
以下の設定可能な入力パラメータをZen Breakout EAに追加して、柔軟性を高めます。
- magicNumber
マジックナンバーは、EAが建てる各ポジションに割り当てる一意の識別子です。これにより、EAは手動や他のEAが建てたポジションと自分のポジションを区別し、自身のポジションのみを変更や決済するようにできます。
- timeFrame
timeFrameはEAが動作するチャートの時間足を指定します。MetaTrader 5で利用可能な21種類の時間足(M1〜MN1)から選択できます。
- lotSizeMode
lotSizeModeは新規ポジションのロットサイズ計算方法を決定します。
- Manual:ユーザーがlotSizeで固定ロットを指定する
- Auto:EAは口座残高とriskPerTradePercentに基づきロットサイズを自動計算する
- riskPerTradePercent
riskPerTradePercentは1取引あたり口座残高に対してリスクとする割合を指定します(lotSizeModeがAutoのときにのみ使用)。たとえば口座残高が$10000でこのパラメータが1.0に設定されている場合、ストップロスがヒットしたときの損失が$100(口座残高の1%)となるようにポジションサイズを算出します。
- lotSize
lotSizeはすべての新規トレードに適用する固定ロットサイズを指定します(lotSizeModeがManualのときにのみ使用)。たとえばlotSizeが0.5に設定されている場合、各新規ポジションは0.5ロットで建てられます。
- RRr(リスクリワード比)
RRrは各取引のリスクとリワードの比率を定義します。ユーザーは事前定義された7つの比率から選択でき、テイクプロフィットが到達した場合に期待利益が期待損失を上回るように設定します。
EA作成のステップバイステップガイド
本記事は、基本的なプログラミング概念に既に精通しており、MetaTrader5およびMetaEditor上でMQL5言語を使った実務経験があることを前提としています。これらの基礎事項は扱わないため、すぐにEAの作成に取りかかります。 MetaEditorで空のソースファイルを用意してください。準備が整い次第、コーディングを開始します。 まずは初期の雛形コードから始め、これを基盤としてEA全体を構築していきます。
//+------------------------------------------------------------------+ //| zenBreakout.mq5 | //| Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian | //| https://www.mql5.com/ja/users/chachaian | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ja/users/chachaian" #property version "1.10" //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ } //--- Utility functions //+------------------------------------------------------------------+
次のステップは、EA専用のカスタム関数を追加することです。これらの関数をOnTick()関数の直下に追加してください。以降のEA実装では、ソースコード内からこれらの関数を順次呼び出して処理を進めていきます。
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ } //--- Utility functions //+------------------------------------------------------------------+ //| This function configures the chart's appearance. | | //+------------------------------------------------------------------+ bool ConfigureChartAppearance() { if(!ChartSetInteger(0, CHART_COLOR_BACKGROUND, clrWhite)){ Print("Error while setting chart background, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_SHOW_GRID, false)){ Print("Error while setting chart grid, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_MODE, CHART_LINE)){ Print("Error while setting chart mode, ", GetLastError()); return false; } if(!ChartSetInteger(0, CHART_COLOR_FOREGROUND, clrBlack)){ Print("Error while setting chart foreground, ", GetLastError()); return false; } return true; } //+-------------------------------------------------------------------------+ //| Function to generate a unique graphical object name with a given prefix | | //+-------------------------------------------------------------------------+ string GenerateUniqueName(string prefix){ int attempt = 0; string uniqueName; while(true) { uniqueName = prefix + IntegerToString(MathRand() + attempt); if(ObjectFind(0, uniqueName) < 0) break; attempt++; } return uniqueName; } //+-------------------------------------------------------------------------+ //| Returns true if Heikin Ashi candle is bullish and has no lower wick | | //+-------------------------------------------------------------------------+ bool IsBullishBreakoutCandle(int index) { if(index < 0 || index >= ArraySize(heikinAshiOpen)) return false; double open = heikinAshiOpen[index]; double close = heikinAshiClose[index]; double low = heikinAshiLow[index]; //--- Candle must be bullish and have no lower wick return (close > open && low >= MathMin(open, close)); } //+-------------------------------------------------------------------------+ //| Returns true if Heikin Ashi candle is bearish and has no upper wick | | //+-------------------------------------------------------------------------+ bool IsBearishBreakoutCandle(int index) { if(index < 0 || index >= ArraySize(heikinAshiOpen)) return false; double open = heikinAshiOpen[index]; double close = heikinAshiClose[index]; double high = heikinAshiHigh[index]; //--- Candle must be bearish and have no upper wick return (close < open && high <= MathMax(open, close)); } //+----------------------------------------------------------------------------------------------+ //| Returns the index of the most recent swing high before 'fromIndex'. Returns -1 if not found | | //+----------------------------------------------------------------------------------------------+ int FindMostRecentSwingHighIndex(int fromIndex) { if(fromIndex <= 0 || fromIndex >= ArraySize(swingHighs)) fromIndex = 1; for(int i = fromIndex; i < ArraySize(swingHighs); i++) { if(swingHighs[i] != EMPTY_VALUE) return i; } return -1; //--- No swing high found } //+----------------------------------------------------------------------------------------------+ //| Returns the index of the most recent swing low before 'fromIndex'. Returns -1 if not found | | //+----------------------------------------------------------------------------------------------+ int FindMostRecentSwingLowIndex(int fromIndex) { if(fromIndex <= 0 || fromIndex >= ArraySize(swingLows)) fromIndex = 1; for(int i = fromIndex; i < ArraySize(swingLows); i++) { if(swingLows[i] != EMPTY_VALUE) return i; } return -1; // No swing low found } //+------------------------------------------------------------------+ //| This function detects a bullish signal | //+------------------------------------------------------------------+ bool IsBullishSignal(datetime &timeStart, int &indexStart, datetime &timeEnd, int &indexEnd) { indexStart = FindMostRecentSwingHighIndex(1); double recentSwingHigh = iHigh(_Symbol, timeframe, indexStart); double previousHeikinAshiCandleClose = heikinAshiClose[1]; double previousHeikinAshiCandleOpen = heikinAshiOpen[1]; if(IsBullishBreakoutCandle(1)){ if(previousHeikinAshiCandleClose > recentSwingHigh && previousHeikinAshiCandleOpen < recentSwingHigh){ timeStart = iTime(_Symbol, timeframe, indexStart); indexEnd = 0; timeEnd = iTime(_Symbol, timeframe, indexEnd); return true; } } return false; } //+------------------------------------------------------------------+ //| This function detects a bearish signal | //+------------------------------------------------------------------+ bool IsBearishSignal(datetime &timeStart, int &indexStart, datetime &timeEnd, int &indexEnd) { indexStart = FindMostRecentSwingLowIndex(1); double recentSwingLow = iLow(_Symbol, timeframe, indexStart); double previousHeikinAshiCandleClose = heikinAshiClose[1]; double previousHeikinAshiCandleOpen = heikinAshiOpen[1]; if(IsBearishBreakoutCandle(1)){ if(previousHeikinAshiCandleClose < recentSwingLow && previousHeikinAshiCandleOpen > recentSwingLow){ timeStart = iTime(_Symbol, timeframe, indexStart); indexEnd = 0; timeEnd = iTime(_Symbol, timeframe, indexEnd); return true; } } return false; } //+-------------------------------------------------------------------+ //| Function to check if there's a new bar on a given chart timeframe | | //+-------------------------------------------------------------------+ bool IsNewBar(string symbol, ENUM_TIMEFRAMES tf, datetime &lastTm) { datetime currentTime = iTime(symbol, tf, 0); if(currentTime != lastTm){ lastTm = currentTime; return true; } return false; } //+------------------------------------------------------------------+ //| To check if there is an active buy position opened by this EA | | //+------------------------------------------------------------------+ bool IsThereAnActiveBuyPosition(ulong magicNm){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == magicNm && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){ return true; } } } return false; } //+------------------------------------------------------------------+ //| To check if there is an active sell position opened by this EA | | //+------------------------------------------------------------------+ bool IsThereAnActiveSellPosition(ulong mgcNumber){ for(int i = PositionsTotal() - 1; i >= 0; i--){ ulong ticket = PositionGetTicket(i); if(ticket == 0){ Print("Error while fetching position ticket ", _LastError); continue; }else{ if(PositionGetInteger(POSITION_MAGIC) == mgcNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){ return true; } } } return false; } //+------------------------------------------------------------------+ //| To open a buy position | //+------------------------------------------------------------------+ bool OpenBuy(){ double rewardValue = 1.0; switch(RRr){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY; ENUM_ORDER_TYPE action = ORDER_TYPE_BUY; double stopLevel = iLow(_Symbol, timeframe, 1); double askPrice = AppData.askPrice; double bidPrice = AppData.bidPrice; double stopDistance = askPrice - stopLevel; double targetLevel = askPrice + (stopDistance * rewardValue); double lotSz = AppData.amountAtRisk / (AppData.contractSize * stopDistance); if(lotSizeMode == MODE_AUTO){ lotSz = NormalizeDouble(lotSz, 2); }else{ lotSz = NormalizeDouble(lotSize, 2); } if(!Trade.Buy(lotSz, _Symbol, askPrice, stopLevel, targetLevel)){ Print("Error while opening a long position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); AppData.tradeInfo.orderTicket = result.order; AppData.tradeInfo.type = action; AppData.tradeInfo.posType = positionType; AppData.tradeInfo.entryPrice = result.price; AppData.tradeInfo.takeProfitLevel = targetLevel; AppData.tradeInfo.stopLossLevel = stopLevel; AppData.tradeInfo.openTime = AppData.currentGmtTime; AppData.tradeInfo.lotSize = lotSz; return true; } return false; } //+------------------------------------------------------------------+ //| To open a sell position | //+------------------------------------------------------------------+ bool OpenSel(){ double rewardValue = 1.0; switch(RRr){ case ONE_TO_ONE: rewardValue = 1.0; break; case ONE_TO_ONEandHALF: rewardValue = 1.5; break; case ONE_TO_TWO: rewardValue = 2.0; break; case ONE_TO_THREE: rewardValue = 3.0; break; case ONE_TO_FOUR: rewardValue = 4.0; break; case ONE_TO_FIVE: rewardValue = 5.0; break; case ONE_TO_SIX: rewardValue = 6.0; break; default: rewardValue = 1.0; break; } ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL; ENUM_ORDER_TYPE action = ORDER_TYPE_SELL; double stopLevel = iHigh(_Symbol, timeframe, 1); double bidPrice = AppData.bidPrice; double askPrice = AppData.askPrice; double stopDistance = stopLevel - bidPrice; double targetLevel = bidPrice - (stopDistance * rewardValue); double lotSz = AppData.amountAtRisk / (AppData.contractSize * stopDistance); if(lotSizeMode == MODE_AUTO){ lotSz = NormalizeDouble(lotSz, 2); }else{ lotSz = NormalizeDouble(lotSize, 2); } if(!Trade.Sell(lotSz, _Symbol, bidPrice, stopLevel, targetLevel)){ Print("Error while opening a short position, ", GetLastError()); Print(Trade.ResultRetcode()); Print(Trade.ResultComment()); return false; }else{ MqlTradeResult result = {}; Trade.Result(result); AppData.tradeInfo.orderTicket = result.order; AppData.tradeInfo.type = action; AppData.tradeInfo.posType = positionType; AppData.tradeInfo.entryPrice = result.price; AppData.tradeInfo.takeProfitLevel = targetLevel; AppData.tradeInfo.stopLossLevel = stopLevel; AppData.tradeInfo.openTime = AppData.currentGmtTime; AppData.tradeInfo.lotSize = lotSz; return true; } return false; } //+------------------------------------------------------------------+
現時点でEAをコンパイルしようとすると、いくつかのコンパイルエラーが発生するはずです。これは、追加した関数の多くが、まだ定義していない変数を参照しているためです。これらの変数は後ほどグローバルスコープに追加します。ここではまず、各関数の役割と動作内容を確認していきます。
- ConfigureChartAppearance
この関数は、EAを実行する前にチャートの外観を整えるためのものです。背景色を白に変更し、視認性向上のためにグリッドを非表示にし、チャートタイプをラインチャートへ切り替え、さらに前景色を黒に設定し、視認性の高いシンプルな表示に整えます。
- GenerateUniqueName
この関数は、EAが描画する各オブジェクトに対して固有の名前を生成するために使用します。これにより、同じ名前のオブジェクトが上書きされることを防げます。文字列を入力として受け取り、固有の識別子に変換するアルゴリズムを適用して一意のオブジェクト名を生成します。
- IsBullishBreakoutCandle
この関数は、指定されたインデックスの平均足が、強気ブレイクアウトの条件を満たすかどうかを判定します。判定条件は以下のとおりです。
- ローソク足の終値が始値より高いこと
- 下ヒゲが存在しないこと
両方の条件を満たす場合、関数はtrueを返し、その平均足が強気ブレイクアウト足であると判断します。
- IsBearishBreakoutCandle
この関数は、指定されたインデックスの平均足が、弱気ブレイクアウト条件を満たすかどうかを判定します。
- FindMostRecentSwingHighIndex
この関数の主目的は、直近のスイングハイのインデックスを取得することです。フラクタルインジケーターの上側フラクタル(バッファ番号0)から値を取得し、指定インデックスより前で最も新しいスイングハイを検索します。
- FindMostRecentSwingLowIndex
この関数は、直近のスイングローのインデックスを取得するために使用します。
- IsBullishSignal
この関数は、有効な強気ブレイクアウトシグナルが形成されたかどうかを判定します。まず直近のスイングハイとその価格を取得し、次に直前の平均足ローソク足の始値と終値を確認します。最後のローソク足が下ヒゲのない陽線で、かつ終値がスイングハイを上抜け、始値がスイングハイより下にある場合、開始時刻と終了時刻を記録し、trueを返します。それ以外の場合はfalseを返します。
- IsBearishSignal
この関数は、有効な弱気ブレイクアウトシグナルが形成されたかどうかを判定します。
- IsNewBar
この関数は、指定した銘柄と時間足において新しいバーが生成されたかどうかを判定します。現在バーの始値時刻を前回記録した時刻と比較し、異なる場合は記録を更新してtrueを返し、同じ場合はfalseを返します。
- IsThereAnActiveBuyPosition
この関数は、指定したマジックナンバーに基づいて、このEAによる有効な買いエントリーが存在するかどうかを確認します。現在の全オープンポジションを反復処理し、該当する買いポジションが見つかればtrue、存在しなければfalseを返します。
- IsThereAnActiveSellPosition
この関数は、指定したマジックナンバーに基づいて、このEAによる有効な売りエントリーが存在するかどうかを確認します。
- OpenBuy
この関数は、EAのリスクリワード設定およびリスク管理ルールに従って買いエントリーする役割を持ちます。まず、ユーザーが指定したリスクリワード比(1:1、1:1.5、1:2など)に基づいて報酬倍率を決定します。次に、前のローソク足の安値をストップロスとし、現在のAsk価格との差分からリスク距離を算出します。その距離に報酬倍率を掛け合わせてテイクプロフィットを計算します。これにより、取引が指定されたリスク対リワードレシオに従うことが保証されます。
次にロットサイズを決定します。ロット計算モードが自動の場合は、リスク許容量、契約サイズ、ストップ距離に基づいてロットを算出し、小数2桁に正規化します。手動モードであれば、ユーザーが指定した固定ロットを使用します。
その後、計算したパラメータを使用して買い注文の送信を試行します。注文が正常に約定した場合は、チケット番号、注文タイプ、エントリ価格、SL、TP、ロット、時刻などの詳細情報をAppData.tradeInfoに保存します。そうでない場合は、デバッグしやすい詳細なエラーメッセージを出力し、falseを返します。
この関数は、EAのリスク管理、報酬計算、注文送信処理を一元的にまとめた、Zen Breakout EAの中核的なコンポーネントです。
- OpenSel
この関数はOpenBuyと同様に動作しますが、ロングではなくショートポジションを建てます。
EAの構造が整ったので、次のステップでは入力パラメータを定義します。これらはプログラムファイルの最上部、#propertyディレクティブの直下に宣言します。次のコードブロックをその位置に追加してください。
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ja/users/chachaian" #property version "1.10" //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; input group "Risk Management" input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO; input double riskPerTradePercent = 1.0; input double lotSize = 0.1; input ENUM_RISK_REWARD_RATIO RRr = ONE_TO_ONEandHALF; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
すでに「EAの準備」セクションでこれらのパラメータを定義および説明しているため、ここでは繰り返しません。 次に、いくつかの入力パラメータ用にカスタム列挙型を作成します。列挙型を使用すると、EAを設定する際にユーザーが事前定義されたオプションをドロップダウンリストから選択できるようになります。 カスタム列挙型は、#propertyディレクティブの直下、入力パラメータの定義の上に記述します。
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ja/users/chachaian" #property version "1.10" //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; ...
コードには、EAの設定をより直感的にするための2つのカスタム列挙型が含まれています。
- ENUM_RISK_REWARD_RATIO
この列挙型は、7種類のプリセットリスクリワード比(1:1から1:6まで)を定義します。これにより、トレーダーは値を手入力するのではなく、ドロップダウンから希望の比率を選択するだけで設定できます。
- ENUM_LOT_SIZE_INPUT_MODE
この列挙型は、EAがロットサイズをどのように計算するかを決定します。MODE_MANUALはユーザーが固定ロットを設定するモードで、MODE_AUTOは口座残高とリスク比率に基づいてロットサイズを自動計算するモードです。
次に、zenBreakoutというマクロを定義します。このマクロはEAの名前を文字列として保持し、後でカスタム関数GenerateUniqueName()内で、新しいグラフィカルオブジェクトに固有名を付与する際に使用されます。 マクロ定義は、既存の#propertyディレクティブの直下に配置してください。
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ja/users/chachaian" #property version "1.10" //--- Macros #define zenBreakout "zenBreakout" //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; ...
次に、カスタム列挙型の定義の直下に、必要なライブラリをインクルードします。
... //--- Custom enumerations enum ENUM_RISK_REWARD_RATIO { ONE_TO_ONE, ONE_TO_ONEandHALF, ONE_TO_TWO, ONE_TO_THREE, ONE_TO_FOUR, ONE_TO_FIVE, ONE_TO_SIX }; enum ENUM_LOT_SIZE_INPUT_MODE { MODE_MANUAL, MODE_AUTO }; //--- Libraries #include <Trade\Trade.mqh> #include <ChartObjects\ChartObjectsLines.mqh> //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; ...
- Trade.mqh
- ChartObjectsLines.mqh
チャート上にラインオブジェクトを作成および管理するクラスへのアクセスを提供します。後ほど、直近のスイングポイントの上下で確認されたブレイクアウトを表示するために使用します。
次に、入力パラメータの直下に2つのデータ構造を定義し、EAが使用する主要情報を整理し、格納します。
... //--- Input parameters input group "Information" input ulong magicNumber = 254700680002; input ENUM_TIMEFRAMES timeframe = PERIOD_CURRENT; input group "Risk Management" input ENUM_LOT_SIZE_INPUT_MODE lotSizeMode = MODE_AUTO; input double riskPerTradePercent = 1.0; input double lotSize = 0.1; input ENUM_RISK_REWARD_RATIO RRr = ONE_TO_ONEandHALF; //--- Data Structures struct MqlTradeInfo { ulong orderTicket; ENUM_ORDER_TYPE type; ENUM_POSITION_TYPE posType; double entryPrice; double takeProfitLevel; double stopLossLevel; datetime openTime; double lotSize; }; struct MqlAppData { double bidPrice; double askPrice; double currentBalance; double currentEquity; datetime currentGmtTime; datetime lastDailyCheckTime; datetime lastBarOpenTime; double contractSize; long digitValue; double amountAtRisk; MqlTradeInfo tradeInfo; }; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
- MqlTradeInfo
このデータ構造体は、アクティブなポジションに関するすべての詳細を保持します。具体的には、チケット番号、注文タイプ、ポジションタイプ、エントリー価格、ストップロス、テイクプロフィット、ロットサイズ、建玉時間などが含まれます。
- MqlAppData
このデータ構造体は、アプリケーションレベルのデータを格納するコンテナとして機能します。具体的には、Bid/Ask価格、口座残高、エクイティ、GMT時間、直近バーの開始時刻、契約サイズ、通貨ペアの小数桁数、取引あたりのリスク額などが含まれます。さらに、MqlTradeInfoのインスタンスも含まれるため、口座情報と取引情報を一元的に管理できます。
次に、グローバル変数を宣言します。これらはEA全体でアクセス可能で、データ構造の直下に宣言します。
... //--- Data Structures struct MqlTradeInfo { ulong orderTicket; ENUM_ORDER_TYPE type; ENUM_POSITION_TYPE posType; double entryPrice; double takeProfitLevel; double stopLossLevel; datetime openTime; double lotSize; }; struct MqlAppData { double bidPrice; double askPrice; double currentBalance; double currentEquity; datetime currentGmtTime; datetime lastDailyCheckTime; datetime lastBarOpenTime; double contractSize; long digitValue; double amountAtRisk; MqlTradeInfo tradeInfo; }; //--- Global variables CTrade Trade; CChartObjectTrend TrendLine; MqlAppData AppData; int heikinAshiIndicatorHandle; double heikinAshiOpen []; double heikinAshiHigh []; double heikinAshiLow []; double heikinAshiClose []; int fractalsIndicatorHandle; double swingHighs []; double swingLows []; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ return(INIT_SUCCEEDED); } ...
- CTrade Trade
CTradeクラスのインスタンスです。注文やエントリー、ポジションの決済と修正などの取引操作を扱うために使用します。
- CChartObjectTrend Trendline
チャート上に描画や操作できるトレンドラインオブジェクトを表します。
- MqlAppData AppData
MqlAppData構造体のインスタンスで、コード内のどこからでもアプリケーションレベルのデータを格納や参照できます。
また、カスタム平均足インジケーターおよび組み込みフラクタルインジケーターから取得した値を保持するための、インジケーター用ハンドルや配列も宣言します。これらはリアルタイムのインジケーター値を保持し、EAが価格変動を分析して有効なブレイクアウトを検出する際に使用します。
OnTick()関数内では、最初にグローバル変数を更新し、毎ティックの最新の市場データおよび口座情報を反映させます。以下のコードブロックをOnTick()関数の冒頭に追加します。
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables AppData.bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); AppData.askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.amountAtRisk = (riskPerTradePercent/100.0) * AppData.currentBalance; } ...
- AppData.bidPrice
チャートが表示している特定の金融商品の現在のBid価格を取得して保存します。
- AppData.askPrice
チャートが表示している特定の金融商品の現在のAsk価格を取得して保存します。
- AppData.currentBalance
価格が変動するたびに、現在の口座残高を取得して記録します。
- AppData.currentEquity
リアルタイムの口座エクイティを取得して保持します。
- AppData.amountAtRisk
riskPerTradePercentパラメータと現在の口座残高に基づき、次回の取引でリスクにさらす金額を計算します。
データ構造とグローバル変数の準備が整ったら、次は戦略を支えるインジケーターを導入します。まず、第1回で作成したカスタム平均足インジケーターと、MetaTrader 5標準のフラクタルインジケーターのインジケーターハンドルを初期化します。ハンドルが初期化できたら、それぞれのバッファからリアルタイムデータを読み取り、EAが有効なブレイクアウトシグナルを検出できるようにします。メモリ効率を考慮して、不要になったインジケーターハンドルは適宜解放します。さらに、カスタム平均足インジケーターをEAファイル内のリソースとしてパッケージ化することで、ユーザーが個別にインジケーターをインストールせずとも、EA単体で動作するようにします。
カスタム平均足インジケーターをリソースとしてパッケージ化する前に、まず自分で作成する必要があります。MetaEditorで新しい空のインジケーターソースファイルを作成し、名前をheikinAshiindicator.mq5としてください。次に、添付のインジケーターソースコードをコピー&ペーストし、コンパイルします。コンパイルが成功すると、MetaTraderはheikinAshiindicator.ex5ファイルを生成します。このコンパイル済みファイルをリソースとしてパッケージ化することで、EAの一部として配布可能になります。
... #property copyright "Copyright 2025, MetaQuotes Ltd. Developer is Chacha Ian" #property link "https://www.mql5.com/ja/users/chachaian" #property version "1.10" #resource "\\Indicators\\heikinAshiIndicator.ex5" ...
これにより、コンパイラはコンパイル済みインジケーターファイル(heikinAshiIndicator.ex5)をEAに組み込むよう指示します。この方法を用いることで、ユーザーはインジケーターを自分でIndicatorsフォルダにインストールする必要がなくなります。コンパイル時にファイルが存在する限り、EAは常にこのインジケーターにアクセスできます。これにより配布が容易になり、エンドユーザーにとってもシームレスなインストール体験が提供されます。
次に、OnTick()関数内でインジケーターハンドルを初期化します。これにより、EAはカスタム平均足インジケーターと組み込みフラクタルインジケーターの両方からリアルタイムデータにアクセスできるようになります。
... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Initialize global variables AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.lastDailyCheckTime = iTime(_Symbol, PERIOD_D1, 0); AppData.lastBarOpenTime = 0; AppData.digitValue = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); AppData.contractSize = SymbolInfoDouble (_Symbol, SYMBOL_TRADE_CONTRACT_SIZE); //--- Initialize the Heikin Ashi indicator heikinAshiIndicatorHandle = iCustom(_Symbol, timeframe, "::Indicators\\heikinAshiIndicator.ex5"); if(heikinAshiIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Heikin Ashi Indicator: ", GetLastError()); return INIT_FAILED; } //--- Initialize the Fractals indicator fractalsIndicatorHandle = iFractals(_Symbol, timeframe); if(fractalsIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Fractals Indicator: ", GetLastError()); return INIT_FAILED; } } ...
EAで使用する2つのインジケーターを初期化するコードを追加しました。
- 平均足インジケーター
iCustom()を使用して、パッケージ化したカスタム平均足インジケーターを読み込みます。ハンドルが正常に作成されない場合、EAはエキスパートジャーナルにエラーメッセージを出力し、実行を停止します。これはデバッグに役立つだけでなく、主要なシグナルソースが存在しない状態でEAが動作するのを防ぐためにも重要です。
- フラクタルインジケーター
インジケーターが初期化できたら、次はOnTick()関数内で各バッファからデータを読み取ります。 以下のコードブロックを、OnTick()関数内でグローバル変数の代入文の直下に追加してください。
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ //--- Scope variables AppData.bidPrice = SymbolInfoDouble (_Symbol, SYMBOL_BID); AppData.askPrice = SymbolInfoDouble (_Symbol, SYMBOL_ASK); AppData.currentBalance = AccountInfoDouble(ACCOUNT_BALANCE); AppData.currentEquity = AccountInfoDouble(ACCOUNT_EQUITY); AppData.amountAtRisk = (riskPerTradePercent/100.0) * AppData.currentBalance; //--- Get a few Heikin Ashi values int copiedHeikinAshiOpen = CopyBuffer(heikinAshiIndicatorHandle, 0, 0, 10, heikinAshiOpen); if(copiedHeikinAshiOpen == -1){ Print("Error while copying Heikin Ashi Open prices: ", GetLastError()); return; } int copiedHeikinAshiHigh = CopyBuffer(heikinAshiIndicatorHandle, 1, 0, 10, heikinAshiHigh); if(copiedHeikinAshiHigh == -1){ Print("Error while copying Heikin Ashi High prices: ", GetLastError()); return; } int copiedHeikinAshiLow = CopyBuffer(heikinAshiIndicatorHandle, 2, 0, 10, heikinAshiLow); if(copiedHeikinAshiLow == -1){ Print("Error while copying Heikin Ashi Low prices: ", GetLastError()); return; } int copiedHeikinAshiClose = CopyBuffer(heikinAshiIndicatorHandle, 3, 0, 10, heikinAshiClose); if(copiedHeikinAshiClose == -1){ Print("Error while copying Heikin Ashi Close prices: ", GetLastError()); return; } //--- Get the latest Fractals indicator values int copiedSwingHighs = CopyBuffer(fractalsIndicatorHandle, 0, 0, 200, swingHighs); if(copiedSwingHighs == -1){ Print("Error while copying fractal's indicator swing highs: ", GetLastError()); } int copiedSwingLows = CopyBuffer(fractalsIndicatorHandle, 1, 0, 200, swingLows); if(copiedSwingLows == -1){ Print("Error while copying fractal's indicator swing lows: ", GetLastError()); } } ...
CopyBuffer()を使用して、平均足インジケーターから最新10本分の始値、高値、安値、終値を取得します。同様に、フラクタルインジケーターからは最新200本分のスイングハイおよびスイングローをコピーします。いずれの場合も、コピーに失敗した場合はエラーをログに記録しますが、プログラムの実行は停止しません。
次に、ArraySetAsSeries()を使用してデータ配列を系列として設定します。この関数は配列のインデックス方向を反転させ、要素0が直近バーのデータを示し、インデックスが大きくなるほど古いバーを示すようにします。 以下のコードブロックを、インジケーターハンドルの初期化セクションの直下に追加してください。
... //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit(){ ... //--- Initialize the Heikin Ashi indicator heikinAshiIndicatorHandle = iCustom(_Symbol, timeframe, "::Indicators\\heikinAshiIndicator.ex5"); if(heikinAshiIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Heikin Ashi Indicator: ", GetLastError()); return INIT_FAILED; } //--- Initialize the Fractals indicator fractalsIndicatorHandle = iFractals(_Symbol, timeframe); if(fractalsIndicatorHandle == INVALID_HANDLE){ Print("Error while initializing The Fractals Indicator: ", GetLastError()); return INIT_FAILED; } //--- Set Arrays as series ArraySetAsSeries(heikinAshiOpen, true); ArraySetAsSeries(heikinAshiHigh, true); ArraySetAsSeries(heikinAshiLow, true); ArraySetAsSeries(heikinAshiClose, true); ArraySetAsSeries(swingHighs, true); ArraySetAsSeries(swingLows, true); return(INIT_SUCCEEDED); } ...
この設定をおこなうことで、EAは常に最新の値に簡単かつ安定してアクセスできるようになります。
インジケーターを扱う際の最後のステップは、EAがチャートから解除されたときに、インジケーターのリソースを適切に解放することです。これはOnDeinit()関数内でおこないます。この関数はEAがチャートから解除されると自動的に実行されます。IndicatorRelease()をインジケーターハンドルに対して呼び出すことで、占有していたメモリを解放します。
... //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Free up memory used by indicators if(heikinAshiIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(heikinAshiIndicatorHandle); } if(fractalsIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(fractalsIndicatorHandle); } } ...
カスタムインジケーターまたは組み込みインジケーターを使用するすべてのEAにこのクリーンアップ手順を含めることは常に良い習慣です。
インジケーターハンドルを解放する前に、EAが作成した可能性のあるチャート上のグラフィカルオブジェクトをすべて削除することも推奨されます。これにより、EAがチャートから解除された後にトレンドラインなどのオブジェクトが残らないようにできます。OnDeinit()関数内でObjectsDeleteAll(0)を呼び出すことでこれを実現します。次に、メモリ解放処理の直前にこの処理を追加しましょう。
... //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason){ //--- Delete all graphical objects ObjectsDeleteAll(0); //--- Free up memory used by indicators if(heikinAshiIndicatorHandle != INVALID_HANDLE){ IndicatorRelease(heikinAshiIndicatorHandle); } ... } ...
この段階では、必要なグローバル変数がすべて正しく定義および初期化されているため、ソースコードをコンパイルしてもエラーは発生しなくなります。
ついに、EAの核心である取引ロジックに到達しました。このコードブロックは新しいバーが形成されたときのみ実行され、シグナルはローソク足の確定時に一度だけ評価されるようになります。既存のコードの直下、OnTick()関数内にこのコアロジックを挿入しましょう。
... //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick(){ ... //--- Get the latest Fractals indicator values int copiedSwingHighs = CopyBuffer(fractalsIndicatorHandle, 0, 0, 200, swingHighs); if(copiedSwingHighs == -1){ Print("Error while copying fractal's indicator swing highs: ", GetLastError()); } int copiedSwingLows = CopyBuffer(fractalsIndicatorHandle, 1, 0, 200, swingLows); if(copiedSwingLows == -1){ Print("Error while copying fractal's indicator swing lows: ", GetLastError()); } //--- Run this block on new bar open if(IsNewBar(_Symbol, timeframe, AppData.lastBarOpenTime)){ datetime timeStart = 0; int indexStart = 0; datetime timeEnd = 0; int indexEnd = 0; //--- Handle Bullish Signals if(IsBullishSignal(timeStart, indexStart, timeEnd, indexEnd)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenBuy(); } double high = iHigh(_Symbol, timeframe, indexStart); TrendLine.Create(0, GenerateUniqueName(zenBreakout), 0, timeStart, high, timeEnd, high); } //--- Handle Bearish Signals if(IsBearishSignal(timeStart, indexStart, timeEnd, indexEnd)){ if(!IsThereAnActiveBuyPosition(magicNumber) && !IsThereAnActiveSellPosition(magicNumber)){ OpenSel(); } double low = iLow (_Symbol, timeframe, indexStart); TrendLine.Create(0, GenerateUniqueName(zenBreakout), 0, timeStart, low, timeEnd, low); } } } ...
このセクション内では、まず補助変数(timeStart、indexStart、timeEnd、indexEnd)を宣言します。有効なシグナルが検出されたときにこれらに値が格納されます。
次に、カスタム関数「IsBullishSignal()」を呼び出します。強気シグナルが検出され、かつアクティブな買いポジションや売りポジションが存在しない場合、EAはOpenBuy()を呼び出して新しい買いポジションを建てます。その直後、TrendLine.Create()を使用して、検出されたバー範囲にトレンドラインを描画し、シグナルの発生箇所を視覚的にマークします。
弱気シグナルの場合も同様のロジックが適用されますが、この場合はEAがOpenSel()を呼び出して売りポジションを建て、検出されたスイング安値にトレンドラインを描画します。
このブロック全体は非常に重要です。なぜなら、ここで先に設定した入力パラメータ、インジケーター、グローバル変数が一つにまとまり、最終的に実際に取引可能な判断を生成するからです。
ここまで読まれたことおめでとうございます。この時点で、EAは完全に構築されており、エラーなくコンパイルできるはずです。添付のソースファイルをダウンロードし、ご自身の実装と比較することをお勧めします。これにより、見落としたステップやタイプミスを確認でき、私たちが構築したものとコードが一致していることを確認できます。自身のコードを丁寧に見直して比較することは、EAをチャート上でテストする前に理解を深め、自信を高める非常に良い方法です。
テスト
金(ゴールド)を対象に、2025年1月1日から2025年8月31日までの期間でバックテストを実施しました。入力パラメータは次の通りに設定しました。
- magicNumber:254700680002
- timeFrame:H1
- lotSizeMode:MODE_AUTO
- riskPerTradePercent:1.0
- lotSize:0.1
- RRr:ONE_TO_ONEandHALF
初期口座残高$100,000で開始したところ、8か月間のバックテストにおいて、エクイティは約12%強の成長を示しました。

以下はエクイティカーブです。

エクイティカーブを見ると、現状の戦略は概ねブレイクイーブンであることがわかります。これは、戦略が完全に損失になるわけではないことを示す前向きなサインです。今後は、パラメータの最適化や、取引時間帯などの高度なシグナルフィルターを組み込むことで、さらにパフォーマンスを改善できる可能性があります。
結論
これでEAの開発を無事に完了しました。入力パラメータ、列挙型、グローバル変数の設定から、インジケーターの初期化、バッファからのデータ取得、コア取引ロジックの実装に至るまで、すべての手順を順を追って確認しました。
現在、カスタム平均足ロジックに基づき自動で取引をおこなう、完全に機能するEAが完成しています。金(ゴールド)を対象におこなったバックテストでは、比較的安定したエクイティカーブが得られ、ロジックが意図通りに機能し、EAがエラーなくコンパイルできることが確認できました。これは非常に重要なマイルストーンです。
ここからは、パラメータの最適化により収益性の改善を目指したり、取引セッションやボラティリティチェックなどの追加フィルターを組み込んだりすることも可能です。今回の作業は、実際の市場環境に対応できる、より高度な自動取引システムを構築するための堅固な基盤となります。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18810
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
プライスアクション分析ツールキットの開発(第41回):MQL5で統計的価格レベルEAを構築する
初心者からエキスパートへ:MQL5を使ったアニメーションニュース見出し(XI) - ニュース取引における相関
MQL5でのデータベースの簡素化(第2回):メタプログラミングを使用してエンティティを作成する
MQL5でのAI搭載取引システムの構築(第1回):AI API向けJSON処理の実装
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索