English Deutsch
preview
MQL5での取引戦略の自動化(第9回):アジアブレイクアウト戦略のためのエキスパートアドバイザーの構築

MQL5での取引戦略の自動化(第9回):アジアブレイクアウト戦略のためのエキスパートアドバイザーの構築

MetaTrader 5トレーディング |
113 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第8回)では、正確なフィボナッチ比率に基づいてバタフライハーモニックパターンを利用したリバーサルトレード戦略を、MetaQuotes Language 5 (MQL5)でエキスパートアドバイザー(EA)として構築しました。今回の第9回では、アジアブレイクアウト戦略に焦点を移します。この手法は、セッション中の重要な高値と安値を特定してブレイクアウトゾーンを形成し、トレンドフィルタリングには移動平均を使用し、さらに動的なリスク管理を組み合わせるものです。

本記事では、以下の内容を取り上げます。

  1. 戦略の設計図
  2. MQL5での実装
  3. バックテストと最適化
  4. 結論

    最終的に、アジアブレイクアウト戦略を自動化する完全な機能を備えたEAが完成し、テストや改良を通じて実際の取引に活用できる準備が整います。それでは、さっそく始めましょう。


    戦略設計図

    このプログラムを作成するにあたり、アジア市場の取引時間中に形成される主要な価格レンジを活用したアプローチを設計します。最初のステップは、「セッションボックス」を定義することです。これは、特定の時間帯(通常はグリニッジ標準時(GMT)で23:00〜03:00)内の最高値と最安値を記録することで設定されます。ただし、この時間帯はニーズに応じて自由にカスタマイズ可能です。この定義された範囲は、ブレイクアウトを狙うレンジ相場(保ち合い)を示しています。

    次に、このレンジの上限および下限にブレイクアウトレベルを設定します。相場が上昇トレンドであると確認できる場合(例えば、50期間の移動平均線などを用いてトレンドを判断)、ボックスの上端より少し上に買いのストップ注文を設定します。逆に、下降トレンドが確認できる場合には、ボックスの下端よりわずかに下に売りのストップ注文を配置します。この2方向のセットアップにより、価格がどちらの方向にブレイクしても、それに乗じて取引できるようEAが常に準備された状態になります。

    リスク管理は、この戦略における重要な要素です。ボックスの境界線からわずかに外れた位置にストップロス注文を設定することで、騙しや急な反転による損失から資金を保護します。テイクプロフィットのレベルは、あらかじめ定めたリスクリワード比に基づいて決定されます。また、時間に基づく自動エグジット機能も導入し、たとえば13:00 GMTを過ぎてもポジションが未決済である場合は、自動的に取引を終了させます。このように、セッションレンジの精密な検出、トレンドフィルタリング、堅実なリスク管理を組み合わせることで、市場における重要なブレイクアウトを的確に捉えることができるEAを構築します。要約すると、以下が今回実装したい戦略の全体像です。

    戦略設計図


    MQL5での実装

    MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、インジケーターフォルダを見つけ、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用するグローバル変数をいくつか宣言する必要があります。

    //+------------------------------------------------------------------+
    //|                        Copyright 2025, Forex Algo-Trader, Allan. |
    //|                                 "https://t.me/Forex_Algo_Trader" |
    //+------------------------------------------------------------------+
    #property copyright "Forex Algo-Trader, Allan"
    #property link      "https://t.me/Forex_Algo_Trader"
    #property version   "1.00"
    #property description "This EA trades based on ASIAN BREAKOUT Strategy"
    #property strict
    
    #include <Trade\Trade.mqh>                              //--- Include trade library
    CTrade obj_Trade;                                          //--- Create global trade object
    
    //--- Global indicator handle for the moving average
    int maHandle = INVALID_HANDLE;                         //--- Global MA handle
    
    //==== Input parameters
    //--- Trade and indicator settings
    input double         LotSize              = 0.1;         //--- Trade lot size
    input double         BreakoutOffsetPips   = 10;          //--- Offset in pips for pending orders
    input ENUM_TIMEFRAMES BoxTimeframe         = PERIOD_M15;  //--- Timeframe for box calculation (15 or 30 minutes)
    input int            MA_Period            = 50;          //--- Moving average period for trend filter
    input ENUM_MA_METHOD MA_Method            = MODE_SMA;    //--- MA method (Simple Moving Average)
    input ENUM_APPLIED_PRICE MA_AppliedPrice   = PRICE_CLOSE; //--- Applied price for MA (Close price)
    input double         RiskToReward         = 1.3;         //--- Reward-to-risk multiplier (1:1.3)
    input int            MagicNumber          = 12345;       //--- Magic number (used for order identification)
    
    //--- Session timing settings (GMT) with minutes
    input int            SessionStartHour     = 23;          //--- Session start hour
    input int            SessionStartMinute   = 00;           //--- Session start minute
    input int            SessionEndHour       = 03;           //--- Session end hour
    input int            SessionEndMinute     = 00;           //--- Session end minute
    input int            TradeExitHour        = 13;          //--- Trade exit hour
    input int            TradeExitMinute      = 00;           //--- Trade exit minute
    
    //--- Global variables for storing session box data
    datetime lastBoxSessionEnd = 0;                        //--- Stores the session end time of the last computed box
    bool     boxCalculated     = false;                    //--- Flag: true if session box has been calculated
    bool     ordersPlaced      = false;                    //--- Flag: true if orders have been placed for the session
    double   BoxHigh           = 0.0;                        //--- Highest price during the session
    double   BoxLow            = 0.0;                        //--- Lowest price during the session
    //--- Variables to store the exact times when the session's high and low occurred
    datetime BoxHighTime       = 0;                          //--- Time when the highest price occurred
    datetime BoxLowTime        = 0;                          //--- Time when the lowest price occurred
    

    ここでは、「#include <Trade\Trade.mqh>」を使用して取引ライブラリをインクルードし、組み込みの取引関数にアクセスできるようにします。そして、グローバルな取引オブジェクト「obj_Trade」を作成します。また、移動平均インジケーターのためのハンドル「maHandle」をグローバル変数として宣言し、初期値としてINVALID_HANDLEを設定します。プログラムの柔軟性を高めるために、ユーザーが任意に入力できるパラメータをいくつか用意しています。たとえば、LotSize、BreakoutOffsetPips、BoxTimeframe(ENUM_TIMEFRAMES型を使用)などの取引設定や、MA_Period、MA_Method、MA_AppliedPriceといったインジケーターの設定があります。さらに、RiskToRewardやMagicNumberといったリスク管理に関するパラメータも含まれています。

    また、ユーザーがセッションの時間帯を「時」と「分」で指定できるようにしており、SessionStartHour、SessionStartMinute、SessionEndHour、SessionEndMinute、TradeExitHour、TradeExitMinuteといった入力パラメータを使用します。さらに、セッションボックスのデータを保持するために、BoxHighとBoxLowをグローバル変数として定義し、それらの極値が記録された正確な時刻を格納するためにBoxHighTimeとBoxLowTimeも用意します。加えて、プログラムのロジックを制御するために、boxCalculatedおよびordersPlacedというフラグも宣言しています。次に、OnInitイベントハンドラの中で、移動平均インジケーターのハンドルを初期化します。

    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit(){
       //--- Set the magic number for all trade operations
       obj_Trade.SetExpertMagicNumber(MagicNumber);           //--- Set magic number globally for trades
       //--- Create the Moving Average handle with user-defined parameters
       maHandle = iMA(_Symbol, 0, MA_Period, 0, MA_Method, MA_AppliedPrice); //--- Create MA handle
       if(maHandle == INVALID_HANDLE){                     //--- Check if MA handle creation failed
          Print("Failed to create MA handle.");           //--- Print error message
          return(INIT_FAILED);                             //--- Terminate initialization if error occurs
       }
       return(INIT_SUCCEEDED);                            //--- Return successful initialization
    }

    OnInitイベントハンドラ内では、まずobj_Trade.SetExpertMagicNumber(MagicNumber)メソッドを呼び出すことで、取引オブジェクトにマジックナンバーを設定します。これにより、すべての取引が一意に識別されるようになります。次に、ユーザー定義のパラメータ(MA_Period、MA_Method、MA_AppliedPrice)を使って、iMA関数により移動平均のハンドルを作成します。その後、maHandleがINVALID_HANDLEであるかどうかを確認します。INVALID_HANDLEのままであれば、ハンドルの生成に失敗したことを意味するため、エラーメッセージを出力してINIT_FAILEDを返します。正常に作成された場合は、INIT_SUCCEEDEDを返して初期化の成功を通知します。最後に、プログラムが使用されていないときにリソースを節約するため、作成したハンドルを適切なタイミングで解放する必要があります。

    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason){
       //--- Release the MA handle if valid
       if(maHandle != INVALID_HANDLE)                     //--- Check if MA handle exists
          IndicatorRelease(maHandle);                     //--- Release the MA handle
       //--- Drawn objects remain on the chart for historical reference
    }

    OnDeinit関数内では、まず移動平均のハンドル「maHandle」が有効かどうか(つまりINVALID_HANDLEでないか)どうかを確認します。有効な場合は、IndicatorRelease関数を使ってそのハンドルを解放し、リソースを解放します。次はいよいよメインのイベントハンドラであるOnTickに進みます。この関数では、プログラム全体の制御ロジックを構築していくことになります。

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick(){
       //--- Get the current server time (assumed GMT)
       datetime currentTime = TimeCurrent();              //--- Retrieve current time
       MqlDateTime dt;                                    //--- Declare a structure for time components
       TimeToStruct(currentTime, dt);                     //--- Convert current time to structure
       
       //--- Check if the current time is at or past the session end (using hour and minute)
       if(dt.hour > SessionEndHour || (dt.hour == SessionEndHour && dt.min >= SessionEndMinute)){
          //--- Build the session end time using today's date and user-defined session end time
          MqlDateTime sesEnd;                             //--- Declare a structure for session end time
          sesEnd.year = dt.year;                          //--- Set year
          sesEnd.mon  = dt.mon;                           //--- Set month
          sesEnd.day  = dt.day;                           //--- Set day
          sesEnd.hour = SessionEndHour;                   //--- Set session end hour
          sesEnd.min  = SessionEndMinute;                 //--- Set session end minute
          sesEnd.sec  = 0;                                //--- Set seconds to 0
          datetime sessionEnd = StructToTime(sesEnd);       //--- Convert structure to datetime
          
          //--- Determine the session start time
          datetime sessionStart;                          //--- Declare variable for session start time
          //--- If session start is later than or equal to session end, assume overnight session
          if(SessionStartHour > SessionEndHour || (SessionStartHour == SessionEndHour && SessionStartMinute >= SessionEndMinute)){
             datetime prevDay = sessionEnd - 86400;       //--- Subtract 24 hours to get previous day
             MqlDateTime dtPrev;                          //--- Declare structure for previous day time
             TimeToStruct(prevDay, dtPrev);               //--- Convert previous day time to structure
             dtPrev.hour = SessionStartHour;              //--- Set session start hour for previous day
             dtPrev.min  = SessionStartMinute;            //--- Set session start minute for previous day
             dtPrev.sec  = 0;                             //--- Set seconds to 0
             sessionStart = StructToTime(dtPrev);         //--- Convert structure back to datetime
          }
          else{
             //--- Otherwise, use today's date for session start
             MqlDateTime temp;                           //--- Declare temporary structure
             temp.year = sesEnd.year;                    //--- Set year from session end structure
             temp.mon  = sesEnd.mon;                     //--- Set month from session end structure
             temp.day  = sesEnd.day;                     //--- Set day from session end structure
             temp.hour = SessionStartHour;               //--- Set session start hour
             temp.min  = SessionStartMinute;             //--- Set session start minute
             temp.sec  = 0;                              //--- Set seconds to 0
             sessionStart = StructToTime(temp);          //--- Convert structure to datetime
          }
          
          //--- Recalculate the session box only if this session hasn't been processed before
          if(sessionEnd != lastBoxSessionEnd){
             ComputeBox(sessionStart, sessionEnd);       //--- Compute session box using start and end times
             lastBoxSessionEnd = sessionEnd;              //--- Update last processed session end time
             boxCalculated   = true;                      //--- Set flag indicating the box has been calculated
             ordersPlaced    = false;                     //--- Reset flag for order placement for the new session
          }
       }
    }

    EAティック関数「OnTick」では、最初にTimeCurrentを呼び出して現在のサーバー時間を取得し、次にTimeToStruct関数を使用してそれをMqlDateTime構造に変換して、そのコンポーネントにアクセスできるようにします。次に、現在の「時」と「分」をユーザー定義のSessionEndHourおよびSessionEndMinuteと比較し、現在の時間がセッション終了時刻に達しているか、もしくはそれを過ぎているかを確認します。この条件を満たす場合、sesEndという構造体を作成し、それをStructToTimeに渡してdatetime型のsessionEndを構築します。

    セッションの開始が終了よりも前か後かに応じて、適切なsessionStart時刻を決定します(たとえば、セッションが深夜をまたぐ場合には日付を調整する必要があります)。その後、sessionEndがlastBoxSessionEndと異なっていれば、ボックスを再計算する必要があると判断し、ComputeBox関数を呼び出してセッションボックスを再計算します。同時に、lastBoxSessionEndを更新し、boxCalculatedおよびordersPlacedのフラグをリセットします。セッションボックスの特性(高値・安値・時間など)を計算するためには、カスタム関数を使用しており、そのコードスニペットが以下になります。

    //+------------------------------------------------------------------+
    //| Function: ComputeBox                                             |
    //| Purpose: Calculate the session's highest high and lowest low, and|
    //|          record the times these extremes occurred, using the     |
    //|          specified session start and end times.                  |
    //+------------------------------------------------------------------+
    void ComputeBox(datetime sessionStart, datetime sessionEnd){
       int totalBars = Bars(_Symbol, BoxTimeframe);       //--- Get total number of bars on the specified timeframe
       if(totalBars <= 0){
          Print("No bars available on timeframe ", EnumToString(BoxTimeframe)); //--- Print error if no bars available
          return;                                        //--- Exit if no bars are found
       }
         
       MqlRates rates[];                                 //--- Declare an array to hold bar data
       ArraySetAsSeries(rates, false);                   //--- Set array to non-series order (oldest first)
       int copied = CopyRates(_Symbol, BoxTimeframe, 0, totalBars, rates); //--- Copy bar data into array
       if(copied <= 0){
          Print("Failed to copy rates for box calculation."); //--- Print error if copying fails
          return;                                        //--- Exit if error occurs
       }
         
       double highVal = -DBL_MAX;                        //--- Initialize high value to the lowest possible
       double lowVal  = DBL_MAX;                         //--- Initialize low value to the highest possible
       //--- Reset the times for the session extremes
       BoxHighTime = 0;                                  //--- Reset stored high time
       BoxLowTime  = 0;                                  //--- Reset stored low time
       
       //--- Loop through each bar within the session period to find the extremes
       for(int i = 0; i < copied; i++){
          if(rates[i].time >= sessionStart && rates[i].time <= sessionEnd){
             if(rates[i].high > highVal){
                highVal = rates[i].high;                //--- Update highest price
                BoxHighTime = rates[i].time;            //--- Record time of highest price
             }
             if(rates[i].low < lowVal){
                lowVal = rates[i].low;                  //--- Update lowest price
                BoxLowTime = rates[i].time;             //--- Record time of lowest price
             }
          }
       }
       if(highVal == -DBL_MAX || lowVal == DBL_MAX){
          Print("No valid bars found within the session time range."); //--- Print error if no valid bars found
          return;                                        //--- Exit if invalid data
       }
       BoxHigh = highVal;                                //--- Store final highest price
       BoxLow  = lowVal;                                 //--- Store final lowest price
       Print("Session box computed: High = ", BoxHigh, " at ", TimeToString(BoxHighTime),
             ", Low = ", BoxLow, " at ", TimeToString(BoxLowTime)); //--- Output computed session box data
       
       //--- Draw all session objects (rectangle, horizontal lines, and price labels)
       DrawSessionObjects(sessionStart, sessionEnd);    //--- Call function to draw objects using computed values
    }

    ここでは、セッション中の高値・安値を計算するためのComputeBox(void関数)を定義します。まず、指定された時間足におけるバーの本数をBars関数で取得し、そのバーのデータをCopyRates関数を使ってMqlRates型の配列にコピーします。highValを-DBL_MAXに、lowValをDBL_MAXに初期化します。これにより、実際の有効な価格が見つかった時に必ずこれらの値が更新されるようになります。その後、セッション期間内に該当するバーをループで確認し、それぞれのバーにおいてhighがhighValを上回っていればhighValを更新し、そのバーの時刻をBoxHighTimeに記録します。同様に、lowがlowValを下回っていればlowValを更新し、時刻をBoxLowTimeに記録します。

    すべてのバーを処理したあとで、highValが-DBL_MAXのまま、またはlowValがDBL_MAXのままであれば、有効なバーが見つからなかったことを示すエラーメッセージを出力します。それ以外の場合は、BoxHighとBoxLowに計算された値を代入し、TimeToString関数を使って記録された時刻を読みやすい形式で出力します。最後に、DrawSessionObjects関数を呼び出して、セッションの開始時刻と終了時刻に基づいて、チャート上にボックスと関連オブジェクトを視覚的に描画します。関数の実装は以下のとおりです。

    //+----------------------------------------------------------------------+
    //| Function: DrawSessionObjects                                         |
    //| Purpose: Draw a filled rectangle spanning from the session's high    |
    //|          point to its low point (using exact times), then draw       |
    //|          horizontal lines at the high and low (from sessionStart to  |
    //|          sessionEnd) with price labels at the right. Dynamic styling |
    //|          for font size and line width is based on the current chart  |
    //|          scale.                                                      |
    //+----------------------------------------------------------------------+
    void DrawSessionObjects(datetime sessionStart, datetime sessionEnd){
       int chartScale = (int)ChartGetInteger(0, CHART_SCALE, 0); //--- Retrieve the chart scale (0 to 5)
       int dynamicFontSize = 7 + chartScale * 1;        //--- Base 7, increase by 2 per scale level
       int dynamicLineWidth = (int)MathRound(1 + (chartScale * 2.0 / 5)); //--- Linear interpolation
       
       //--- Create a unique session identifier using the session end time
       string sessionID = "Sess_" + IntegerToString(lastBoxSessionEnd);
       
       //--- Draw the filled rectangle (box) using the recorded high/low times and prices
       string rectName = "SessionRect_" + sessionID;       //--- Unique name for the rectangle
       if(!ObjectCreate(0, rectName, OBJ_RECTANGLE, 0, BoxHighTime, BoxHigh, BoxLowTime, BoxLow))
          Print("Failed to create rectangle: ", rectName); //--- Print error if creation fails
       ObjectSetInteger(0, rectName, OBJPROP_COLOR, clrThistle); //--- Set rectangle color to blue
       ObjectSetInteger(0, rectName, OBJPROP_FILL, true);       //--- Enable filling of the rectangle
       ObjectSetInteger(0, rectName, OBJPROP_BACK, true);       //--- Draw rectangle in background
       
       //--- Draw the top horizontal line spanning from sessionStart to sessionEnd at the session high
       string topLineName = "SessionTopLine_" + sessionID; //--- Unique name for the top line
       if(!ObjectCreate(0, topLineName, OBJ_TREND, 0, sessionStart, BoxHigh, sessionEnd, BoxHigh))
          Print("Failed to create top line: ", topLineName); //--- Print error if creation fails
       ObjectSetInteger(0, topLineName, OBJPROP_COLOR, clrBlue); //--- Set line color to blue
       ObjectSetInteger(0, topLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, topLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Draw the bottom horizontal line spanning from sessionStart to sessionEnd at the session low
       string bottomLineName = "SessionBottomLine_" + sessionID; //--- Unique name for the bottom line
       if(!ObjectCreate(0, bottomLineName, OBJ_TREND, 0, sessionStart, BoxLow, sessionEnd, BoxLow))
          Print("Failed to create bottom line: ", bottomLineName); //--- Print error if creation fails
       ObjectSetInteger(0, bottomLineName, OBJPROP_COLOR, clrRed); //--- Set line color to blue
       ObjectSetInteger(0, bottomLineName, OBJPROP_WIDTH, dynamicLineWidth); //--- Set line width dynamically
       ObjectSetInteger(0, bottomLineName, OBJPROP_RAY_RIGHT, false); //--- Do not extend line infinitely
       
       //--- Create the top price label at the right edge of the top horizontal line
       string topLabelName = "SessionTopLabel_" + sessionID; //--- Unique name for the top label
       if(!ObjectCreate(0, topLabelName, OBJ_TEXT, 0, sessionEnd, BoxHigh))
          Print("Failed to create top label: ", topLabelName); //--- Print error if creation fails
       ObjectSetString(0, topLabelName, OBJPROP_TEXT," "+DoubleToString(BoxHigh, _Digits)); //--- Set label text to session high price
       ObjectSetInteger(0, topLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, topLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, topLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
       
       //--- Create the bottom price label at the right edge of the bottom horizontal line
       string bottomLabelName = "SessionBottomLabel_" + sessionID; //--- Unique name for the bottom label
       if(!ObjectCreate(0, bottomLabelName, OBJ_TEXT, 0, sessionEnd, BoxLow))
          Print("Failed to create bottom label: ", bottomLabelName); //--- Print error if creation fails
       ObjectSetString(0, bottomLabelName, OBJPROP_TEXT," "+DoubleToString(BoxLow, _Digits)); //--- Set label text to session low price
       ObjectSetInteger(0, bottomLabelName, OBJPROP_COLOR, clrBlack); //--- Set label color to blue
       ObjectSetInteger(0, bottomLabelName, OBJPROP_FONTSIZE, dynamicFontSize); //--- Set dynamic font size for label
       ObjectSetInteger(0, bottomLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Anchor label to the left so text appears to right
    }

    DrawSessionObjects関数では、まずChartGetInteger関数を使って現在のチャートスケール(CHART_SCALE)を取得します。これは0から5の整数値で返され、チャートのズームレベルを示します。このスケール値に基づいて動的なスタイリングパラメータを算出します。フォントサイズは「7 + chartScale * 1」の式で計算され、ベースのサイズ7にスケールレベルごとに+1されていきます。また、線幅はMathRoundを使って線形に補間され、スケールが最大の5の場合には幅が3になるように調整されます。次に、セッションごとのオブジェクト名を一意にするために、lastBoxSessionEndを文字列に変換し、先頭に「Sess_」を付けてセッション識別子を生成します。続いて、ObjectCreate関数を使用して塗りつぶされた長方形(OBJ_RECTANGLE)を描画します。これには、セッション中の高値と安値の時刻(BoxHighTime、BoxLowTime)および価格(BoxHigh、BoxLow)を使用します。この長方形にはclrThistleの色を指定し、OBJPROP_FILLを有効にして塗りつぶしをおこない、OBJPROP_BACKをtrueに設定することで背景レイヤーに配置します。

    その後、2本の水平トレンドラインを描画します。1本はセッションの高値、もう1本は安値に位置し、どちらもsessionStartからsessionEndまでをスパンします。高値のラインはclrBlue、安値のラインはclrRedに設定され、いずれも前述の動的ライン幅を使用し、OBJPROP_RAY_RIGHTをfalseに設定して無限延長はおこないません。最後に、セッション高値と安値の価格ラベルを、チャートの右端(sessionEndの位置)にテキストオブジェクトとして表示します。ラベルにはDoubleToStringを用いて現在の通貨ペアの小数点桁(_Digits)に合わせたフォーマットで価格を表示します。テキストカラーはclrBlack、フォントサイズは前述の動的サイズを使用し、テキストのアンカーを左寄せにすることで、ラベルがアンカーの右側に自然に表示されるようにします。コンパイルすると、次の結果が得られます。

    アジアボックス識別

    画像から確認できるように、セッションボックスが正しく認識され、チャート上に描画されていることが分かります。これにより、次のステップとして、識別されたレンジの境界付近に予約注文を出す処理に進むことができます。次のようなロジックを使います。

    //--- Build the trade exit time using user-defined hour and minute for today
    MqlDateTime exitTimeStruct;                        //--- Declare a structure for exit time
    TimeToStruct(currentTime, exitTimeStruct);         //--- Use current time's date components
    exitTimeStruct.hour = TradeExitHour;               //--- Set trade exit hour
    exitTimeStruct.min  = TradeExitMinute;             //--- Set trade exit minute
    exitTimeStruct.sec  = 0;                           //--- Set seconds to 0
    datetime tradeExitTime = StructToTime(exitTimeStruct); //--- Convert exit time structure to datetime
    
    //--- If the session box is calculated, orders are not placed yet, and current time is before trade exit time, place orders
    if(boxCalculated && !ordersPlaced && currentTime < tradeExitTime){
       double maBuffer[];                           //--- Declare array to hold MA values
       ArraySetAsSeries(maBuffer, true);            //--- Set the array as series (newest first)
       if(CopyBuffer(maHandle, 0, 0, 1, maBuffer) <= 0){  //--- Copy 1 value from the MA buffer
          Print("Failed to copy MA buffer.");       //--- Print error if buffer copy fails
          return;                                   //--- Exit the function if error occurs
       }
       double maValue = maBuffer[0];                 //--- Retrieve the current MA value
       
       double currentPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current bid price
       bool bullish = (currentPrice > maValue);      //--- Determine bullish condition
       bool bearish = (currentPrice < maValue);       //--- Determine bearish condition
       
       double offsetPrice = BreakoutOffsetPips * _Point; //--- Convert pips to price units
       
       //--- If bullish, place a Buy Stop order
       if(bullish){
          double entryPrice = BoxHigh + offsetPrice; //--- Set entry price just above the session high
          double stopLoss   = BoxLow - offsetPrice;    //--- Set stop loss below the session low
          double risk       = entryPrice - stopLoss;     //--- Calculate risk per unit
          double takeProfit = entryPrice + risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.BuyStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Buy Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                        //--- Set flag indicating an order has been placed
          }
          else{
             Print("Buy Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
       //--- If bearish, place a Sell Stop order
       else if(bearish){
          double entryPrice = BoxLow - offsetPrice;  //--- Set entry price just below the session low
          double stopLoss   = BoxHigh + offsetPrice;   //--- Set stop loss above the session high
          double risk       = stopLoss - entryPrice;    //--- Calculate risk per unit
          double takeProfit = entryPrice - risk * RiskToReward; //--- Calculate take profit using risk/reward ratio
          if(obj_Trade.SellStop(LotSize, entryPrice, _Symbol, stopLoss, takeProfit, ORDER_TIME_GTC, 0, "Asian Breakout EA")){
             Print("Placed Sell Stop order at ", entryPrice); //--- Print order confirmation
             ordersPlaced = true;                       //--- Set flag indicating an order has been placed
          }
          else{
             Print("Sell Stop order failed: ", obj_Trade.ResultRetcodeDescription()); //--- Print error if order fails
          }
       }
    }

    ここでは、取引の終了時刻を構築するために、exitTimeStructというMqlDateTime構造体を宣言します。次に、TimeToStruct関数を使って現在時刻を分解し、その構造体にユーザーが指定したTradeExitHourとTradeExitMinuteを代入します(秒は0に設定)。その後、StructToTime関数を呼び出してこの構造体をdatetime型に変換し、tradeExitTimeとして取得します。その後、セッションボックスがすでに計算されていて、まだ注文が出されておらず、なおかつ現在時刻がtradeExitTimeより前である場合に限り、注文処理に進みます。

    まず、移動平均の値を格納するためにmaBufferという配列を宣言し、ArraySetAsSeries関数でこの配列を新しいデータが先頭になるように設定します。そして、CopyBuffer関数を使用して、移動平均インジケーターの最新値をmaHandleから取得し、maBufferに格納します。次に、SymbolInfoDouble関数を使って現在のBid価格を取得し、それと移動平均値を比較することで、市場が強気か弱気かを判断します。この条件に応じて、エントリー価格、ストップロス、テイクプロフィットをBreakoutOffsetPipsパラメータを用いて計算し、強気の場合はobj_Trade.BuyStopメソッド、弱気の場合はobj_Trade.SellStopメソッドを使って、それぞれ買いまたは売りの予約注文を出します。

    最後に、注文が正常に出された場合は確認メッセージを出力し、失敗した場合にはエラーメッセージを表示します。どちらの場合も、注文状況を追跡するためにordersPlacedフラグを適切に更新します。プログラムを実行すると、次の結果が得られます。

    予約注文が確認された

    この関数の処理から分かるように、ブレイクアウトが発生すると、移動平均フィルターの方向に基づいて適切な予約注文(買いストップまたは売りストップ)が発注され、それに対応するストップロスも同時に設定されます。あとは残された処理として、取引時間外になったときに保有ポジションを決済するか、未約定の予約注文を削除するというロジックを実装するだけです。

    //--- If current time is at or past trade exit time, close positions and cancel pending orders
    if(currentTime >= tradeExitTime){
       CloseOpenPositions();                          //--- Close all open positions for this EA
       CancelPendingOrders();                         //--- Cancel all pending orders for this EA
       boxCalculated = false;                         //--- Reset session box calculated flag
       ordersPlaced  = false;                         //--- Reset order placed flag
    }
    

    ここでは、現在の時刻が取引終了時刻に達しているか、またはそれを超えているかを確認します。もし条件を満たしていれば、まずCloseOpenPositions関数を呼び出して、EAに関連するすべての保有ポジションをクローズします。続けて、CancelPendingOrders関数を呼び出し、未約定の予約注文をすべてキャンセルします。これらの関数の処理が完了した後は、boxCalculatedおよびordersPlacedのフラグをfalseにリセットします。これにより、次のセッションに向けてプログラムが準備されます。使用するカスタム関数は次のとおりです。

    //+------------------------------------------------------------------+
    //| Function: CloseOpenPositions                                     |
    //| Purpose: Close all open positions with the set magic number      |
    //+------------------------------------------------------------------+
    void CloseOpenPositions(){
       int totalPositions = PositionsTotal();           //--- Get total number of open positions
       for(int i = totalPositions - 1; i >= 0; i--){      //--- Loop through positions in reverse order
          ulong ticket = PositionGetTicket(i);           //--- Get ticket number for each position
          if(PositionSelectByTicket(ticket)){            //--- Select position by ticket
             if(PositionGetInteger(POSITION_MAGIC) == MagicNumber){ //--- Check if position belongs to this EA
                if(!obj_Trade.PositionClose(ticket))        //--- Attempt to close position
                  Print("Failed to close position ", ticket, ": ", obj_Trade.ResultRetcodeDescription()); //--- Print error if closing fails
                else
                  Print("Closed position ", ticket);    //--- Confirm position closed
             }
          }
       }
    }
      
    //+------------------------------------------------------------------+
    //| Function: CancelPendingOrders                                    |
    //| Purpose: Cancel all pending orders with the set magic number     |
    //+------------------------------------------------------------------+
    void CancelPendingOrders(){
       int totalOrders = OrdersTotal();                 //--- Get total number of pending orders
       for(int i = totalOrders - 1; i >= 0; i--){         //--- Loop through orders in reverse order
          ulong ticket = OrderGetTicket(i);              //--- Get ticket number for each order
          if(OrderSelect(ticket)){                       //--- Select order by ticket
             int type = (int)OrderGetInteger(ORDER_TYPE); //--- Retrieve order type
             if(OrderGetInteger(ORDER_MAGIC) == MagicNumber && //--- Check if order belongs to this EA
                (type == ORDER_TYPE_BUY_STOP || type == ORDER_TYPE_SELL_STOP)){
                if(!obj_Trade.OrderDelete(ticket))         //--- Attempt to delete pending order
                  Print("Failed to cancel pending order ", ticket); //--- Print error if deletion fails
                else
                  Print("Canceled pending order ", ticket); //--- Confirm pending order canceled
             }
          }
       }
    }
    

    関数CloseOpenPositionsでは、まずPositionsTotal関数で保有ポジションの総数を取得し、逆順でループします。各ポジションについてPositionGetTicketでチケット番号を取得し、PositionSelectByTicketでポジションを選択します。次に、ポジションのPOSITION_MAGICがユーザー定義のMagicNumberと一致するか確認し、一致すればobj_Trade.PositionClose関数でポジションを決済し、成功すれば確認メッセージを、失敗すればobj_Trade.ResultRetcodeDescriptionを使ってエラーメッセージを出力します。

    関数「CancelPendingOrders」では、まずOrdersTotal関数で未決注文の総数を取得し、逆順でループします。各注文についてはOrderGetTicketでチケットを取得し、OrderSelectで注文を選択します。注文のORDER_MAGICがMagicNumberと一致し、かつ注文タイプがORDER_TYPE_BUY_STOPまたはORDER_TYPE_SELL_STOPであれば、obj_Trade.OrderDelete関数で注文のキャンセルを試み、成功すれば確認メッセージを、失敗すればエラーメッセージを表示しますプログラムを実行すると、次の結果が得られます。

    戦略GIF

    視覚化から、アジアセッションを特定しチャートに描画し、移動平均の方向に応じて予約注文を配置し、ユーザーが定めた取引時間を過ぎたら未約定の注文や保有ポジションをキャンセルしていることが確認できます。これにより、目的が達成されています。残された作業はプログラムのバックテストであり、それについては次のセクションで取り扱います。


    バックテストと最適化

    2023年の1年間にわたるデフォルト設定での詳細なバックテストの結果は、以下のとおりです。

    バックテストグラフ

    グラフ1

    画像からわかるように、グラフの結果はかなり良好ですが、トレーリングストップ機構を適用することでさらに改善が期待できます。これを実現するために、以下のロジックを用いました。

    //+------------------------------------------------------------------+
    //|        FUNCTION TO APPLY TRAILING STOP                           |
    //+------------------------------------------------------------------+
    void applyTrailingSTOP(double slPoints, CTrade &trade_object,int magicNo=0){
       double buySL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID)-slPoints,_Digits); //--- Calculate SL for buy positions
       double sellSL = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK)+slPoints,_Digits); //--- Calculate SL for sell positions
    
       for (int i = PositionsTotal() - 1; i >= 0; i--){ //--- Iterate through all open positions
          ulong ticket = PositionGetTicket(i);          //--- Get position ticket
          if (ticket > 0){                              //--- If ticket is valid
             if (PositionGetString(POSITION_SYMBOL) == _Symbol &&
                (magicNo == 0 || PositionGetInteger(POSITION_MAGIC) == magicNo)){ //--- Check symbol and magic number
                if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY &&
                   buySL > PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (buySL > PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for buy position if conditions are met
                   trade_object.PositionModify(ticket,buySL,PositionGetDouble(POSITION_TP));
                }
                else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL &&
                   sellSL < PositionGetDouble(POSITION_PRICE_OPEN) &&
                   (sellSL < PositionGetDouble(POSITION_SL) ||
                   PositionGetDouble(POSITION_SL) == 0)){ //--- Modify SL for sell position if conditions are met
                   trade_object.PositionModify(ticket,sellSL,PositionGetDouble(POSITION_TP));
                }
             }
          }
       }
    }
    
    //---- CALL THE FUNCTION IN THE TICK EVENT HANDLER
    
    if (PositionsTotal() > 0){                       //--- If there are open positions
       applyTrailingSTOP(30*_Point,obj_Trade,0);  //--- Apply a trailing stop
    }
    

    関数を適用してテストをおこなった結果、新たな結果は以下の通りです。

    バックテストグラフ

    バックテストグラフ

    バックテストレポート

    バックテスト結果


    結論

    本稿では、アジアブレイクアウト戦略を精密に自動化するMQL5 EAを無事に開発しました。セッションに基づくレンジ検出、移動平均によるトレンドフィルタリング、そして動的なリスク管理を活用することで、重要なレンジの押し固めゾーンを特定し、効率的にブレイクアウトトレードを実行するシステムを構築しました。

    免責条項:本記事は教育目的のみを意図したものです。取引には大きな財務リスクが伴い、市場の状況は予測できない場合があります。概説した戦略はブレイクアウト取引への構造化されたアプローチを提供しますが、収益性を保証するものではありません。本プログラムを実運用する前には、十分なバックテストと適切なリスク管理が必須です。

    これらのテクニックを実装することで、アルゴリズム取引能力を強化し、テクニカル分析スキルを洗練させ、取引戦略をさらに進歩させることができます。皆さんの取引の成功をお祈りしております。

    MetaQuotes Ltdにより英語から翻訳されました。
    元の記事: https://www.mql5.com/en/articles/17239

    添付されたファイル |
    知っておくべきMQL5ウィザードのテクニック(第55回):PER付きSAC 知っておくべきMQL5ウィザードのテクニック(第55回):PER付きSAC
    強化学習において、リプレイバッファは特にDQNやSACのようなオフポリシーアルゴリズムにおいて重要な役割を果たします。これにより、メモリバッファのサンプリング処理が注目されます。たとえばSACのデフォルト設定では、このバッファからランダムにサンプルを取得しますが、Prioritized Experience Replay (PER)を用いることで、TDスコア(時間差分誤差)に基づいてサンプリングを調整することができます。本稿では、強化学習の意義を改めて確認し、いつものように交差検証ではなく、この仮説だけを検証する、ウィザードで組み立てたエキスパートアドバイザー(EA)を用いて考察します。
    MQL5での取引戦略の自動化(第8回):バタフライハーモニックパターンを用いたエキスパートアドバイザーの構築 MQL5での取引戦略の自動化(第8回):バタフライハーモニックパターンを用いたエキスパートアドバイザーの構築
    この記事では、バタフライハーモニックパターンを検出するためのMQL5エキスパートアドバイザー(EA)を構築します。ピボットポイントを特定し、フィボナッチレベルを検証してパターンを確認します。次に、チャート上にパターンを可視化し、確認された際には自動的に取引を実行します。
    MQL5経済指標カレンダーを使った取引(第6回):ニュースイベント分析とカウントダウンタイマーによるトレードエントリーの自動化 MQL5経済指標カレンダーを使った取引(第6回):ニュースイベント分析とカウントダウンタイマーによるトレードエントリーの自動化
    本記事では、MQL5経済指標カレンダーを活用して、ユーザー定義のフィルターと時間オフセットに基づいた自動取引エントリーを実装します。対象となる経済指標イベントを検出し、予想値と前回値の比較により、買うか売るかの判断を下します。動的なカウントダウンタイマーは、ニュース公開までの残り時間を表示し、取引後には自動的にリセットされます。
    MQL5で自己最適化エキスパートアドバイザーを構築する(第6回):ストップアウト防止 MQL5で自己最適化エキスパートアドバイザーを構築する(第6回):ストップアウト防止
    本日は、勝ちトレードでストップアウトされる回数を最小限に抑えるためのアルゴリズム的手法を探るディスカッションにご参加ください。この問題は非常に難易度が高く、取引コミュニティで見られる多くの提案は、明確で一貫したルールに欠けているのが実情です。私たちはこの課題に対してアルゴリズム的なアプローチを用いることで、トレードの収益性を高め、1回あたりの平均損失を減らすことに成功しました。とはいえ、ストップアウトを完全に排除するには、まださらなる改良が必要です。私たちの解決策は、それには至らないものの、誰にとっても試す価値のある良い第一歩です。