
MQL5での取引戦略の自動化(第9回):アジアブレイクアウト戦略のためのエキスパートアドバイザーの構築
はじめに
前回の記事(第8回)では、正確なフィボナッチ比率に基づいてバタフライハーモニックパターンを利用したリバーサルトレード戦略を、MetaQuotes Language 5 (MQL5)でエキスパートアドバイザー(EA)として構築しました。今回の第9回では、アジアブレイクアウト戦略に焦点を移します。この手法は、セッション中の重要な高値と安値を特定してブレイクアウトゾーンを形成し、トレンドフィルタリングには移動平均を使用し、さらに動的なリスク管理を組み合わせるものです。
本記事では、以下の内容を取り上げます。
最終的に、アジアブレイクアウト戦略を自動化する完全な機能を備えた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関数で注文のキャンセルを試み、成功すれば確認メッセージを、失敗すればエラーメッセージを表示しますプログラムを実行すると、次の結果が得られます。
視覚化から、アジアセッションを特定しチャートに描画し、移動平均の方向に応じて予約注文を配置し、ユーザーが定めた取引時間を過ぎたら未約定の注文や保有ポジションをキャンセルしていることが確認できます。これにより、目的が達成されています。残された作業はプログラムのバックテストであり、それについては次のセクションで取り扱います。
バックテストと最適化
2023年の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





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索