English
preview
MQL5での取引戦略の自動化(第42回):セッションベースのオープニングレンジブレイクアウト(ORB)システム

MQL5での取引戦略の自動化(第42回):セッションベースのオープニングレンジブレイクアウト(ORB)システム

MetaTrader 5トレーディング |
20 3
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第41回)では、MetaQuotes Language 5 (MQL5)でローソク足レンジ理論(CRT, Candle Range Theory)に基づく取引システムを開発しました。このシステムでは、指定した時間枠での蓄積レンジを特定し、押しの深さを考慮したブレイクの検出、そして分配フェーズにおけるローソク足の確定を通じた反転確認によってエントリーをおこなう仕組みを組み込みました。今回の第42回では、完全にカスタマイズ可能なセッションベースのオープニングレンジブレイクアウト(ORB)システムを開発します。

このシステムでは、任意のセッション開始時刻と期間を分単位で定義できます。選択した時間足で、その期間の真の高値と安値を自動で取得します。ブレイクアウトを検出し、オプションで複数バーの確定を確認して誤シグナルを減らします。エントリーはブレイクアウトの方向にのみ実行されます。ストップロスやテイクプロフィットは、レンジサイズに基づく動的設定、あるいは固定値の静的設定のいずれかが使用可能です。利益閾値に達した後にトレーリングストップを使用することもできます。また、方向ごとのポジション上限も設定可能です。本記事では以下のトピックを扱います。

  1. オープニングレンジブレイクアウト(ORB)戦略の理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

記事の最後には、ロンドン、ニューヨーク、アジア市場、あるいは任意のカスタムオープニングを含むあらゆる市場セッションで、明確なオープニングレンジブレイクアウトを取引できるMQL5プログラムが完成し、さらにカスタマイズする準備が整います。それでは、さっそく始めましょう。


オープニングレンジブレイクアウト(ORB)戦略の理解

オープニングレンジブレイクアウト(ORB)は、典型的なデイトレード向けモメンタム戦略で、取引セッションの開始時に形成される初期の方向性バイアスを利用します。「オープニングレンジ」とは、市場が開いた直後の最初の数分間(通常5~60分)に形成される高値と安値を指します。その後、価格がレンジの高値を明確に上抜けた場合(強気ブレイクアウト)やレンジの安値を下抜けた場合(弱気ブレイクアウト)に、ブレイク方向に沿ってエントリーします。この戦略の前提はシンプルですが強力です。オープニングレンジは、前日のニュースや注文フローを市場が消化する過程で、買い手と売り手の戦いを反映することが多く、明確なブレイクアウトはどちらかの勢力が市場の主導権を握ったことを示します。この場合、ブレイク方向に持続的なトレンドが発生することがよくあります。システム自体は比較的簡単です。以下に想定される各セットアップの例を示します。

ORB戦略セットアップ

本記事では、任意の銘柄と任意のセッション(ニューヨーク、ロンドン、アジア、あるいはカスタムオープニング)で機能する、セッションに完全対応した柔軟なORBシステムを作成します。  ユーザーは正確な開始時刻を設定できます。たとえば、NYSEでは09:30、ロンドンでは08:00などです。また、レンジ期間を分単位で定義することも可能です。 システムは、選択した時間枠内でその期間の高値と安値を自動計算します。必要に応じて、複数バーの確定確認を有効にして、ブレイクアウトの正当性を検証することも可能です。

アルゴリズムは、1セッションあたり方向ごとに1回のみ取引を実行します。ストップロスおよびテイクプロフィットの計算は、レンジサイズに基づく動的方式と固定値の静的方式の2種類を提供し、リスクリワード比率もカスタマイズ可能です。 利益が一定水準に達した後に発動するポイントベースのトレーリングストップも利用できます。さらに、このシステムは豊富なチャート可視化機能を備えています。これには、塗りつぶし済みのレンジ矩形、セッション開始と終了の垂直マーカー、高値と安値のレベル表示、エントリー矢印などが含まれます。

可視化は、ここまで読んでお気づきの通り、戦略の理解を明確にするために非常に重要です。以下に、想定されるビジュアル表示を簡単に示します。

目標の枠組み


MQL5での実装

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

//+------------------------------------------------------------------+
//|                                ORB Opening Range Breakout EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+
enum SLTP_Method {                                                // Define SL/TP method enum
   Dynamic_Method = 0,                                            // Dynamic based on range size
   Static_Method  = 1                                             // Static based on fixed points
};

enum TrailingTypeEnum {                                           // Define trailing type enum
   Trailing_None   = 0,                                           // None
   Trailing_Points = 1                                            // By Points
};

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input ENUM_TIMEFRAMES RangeTF = PERIOD_M5;                        // Timeframe for Opening Range Calculation
input int RangeDurationMinutes = 30;                              // Duration of Opening Range in Minutes
input string SessionStartTime = "09:00";                          // Session Start Time (HH:MM)
input double TradeVolume = 0.01;                                  // Trade Volume Size
input double RR_Ratio = 2.0;                                      // Risk to Reward Ratio
input SLTP_Method SLTP_Approach = Dynamic_Method;                 // SL/TP Calculation Method
input int SL_Points = 50;                                         // SL Points (for Static Method)
input TrailingTypeEnum TrailingType = Trailing_None;              // Trailing Stop Type
input double Trailing_Stop_Points = 20.0;                         // Trailing Stop in Points
input double Min_Profit_To_Trail_Points = 30.0;                   // Min Profit to Start Trailing in Points
input int UniqueID = 987654321;                                   // Unique Trade Identifier
input int MaxPositionsDir = 1;                                     // Max Positions per Direction
input bool UseBreakoutFilter = true;                              // Use Breakout Confirmation Filter
input int ConfirmBars = 1;                                        // Bars to Confirm Breakout on Close (0 to disable)

実装はまず「#include <Trade\Trade.mqh>」を使用してTradeライブラリをインクルードすることから始めます。これにより、CTradeクラスおよび注文実行やポジション管理に必要な関数が利用可能になります。次に、ユーザーオプションを明確に整理するために2つの列挙型を定義します。SLTP_Method列挙型では、オープニングレンジの実際のサイズに基づいてストップロスおよびテイクプロフィットを計算するDynamic_Methodと、固定ポイントによる計算をおこなうStatic_Methodが用意されています。同様に、TrailingTypeEnum列挙型では、トレーリングを無効にするTrailing_Noneと、利益が最低閾値に達した後にユーザー指定のポイント数でトレーリングを有効にするTrailing_Pointsが用意されています。

次に、プロパティウィンドウから直接プログラムを柔軟に設定できる入力パラメータを宣言します。これには以下が含まれます。これには、オープニングレンジの高値と安値を計算する時間足を選択するRangeTF、セッション開始からオープニングレンジとして計測する分数を設定するRangeDurationMinutes、各セッションの開始時刻を「HH:MM」形式の文字列で定義するSessionStartTime(例:NYSEは「09:30」、ロンドンは「08:00」)、ロットサイズを指定するTradeVolumeなどが含まれます。その他のパラメータも名称から内容が理解できるようにコメントを付けています。この入力セットにより、コードを変更することなく、任意の市場やセッションにシステムを完全に適応させることが可能です。次に必要なのは、グローバル変数の定義です。

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Trade object
datetime sessionStart = 0;                                        //--- Session start time
datetime rangeEndTime = 0;                                        //--- Range end time
double rangeHigh = 0.0;                                           //--- Range high
double rangeLow = 0.0;                                            //--- Range low
bool rangeDefined = false;                                        //--- Range defined flag
bool breakoutHigh = false;                                        //--- Breakout high flag
bool breakoutLow = false;                                         //--- Breakout low flag
double breakoutPrice = 0.0;                                       //--- Breakout price
string highLevelObj = "ORB_HighLevel";                            //--- High level object name
string lowLevelObj = "ORB_LowLevel";                              //--- Low level object name
string highTextObj = "ORB_High_Text";                             //--- High text object
string lowTextObj = "ORB_Low_Text";                               //--- Low text object
bool tradedLong = false;                                          //--- Traded long flag
bool tradedShort = false;                                         //--- Traded short flag
datetime lastConfirmTime = 0;                                     //--- Last confirm time

次に、各取引セッションを通じてプログラムの状態を維持し、オープニングレンジブレイクアウトのロジックを適切に追跡する一連のグローバル変数を宣言します。CTradeクラスからobj_Tradeをインスタンス化し、すべての注文実行やポジション修正を担当させます。タイミング管理用の変数としては、セッション開始の正確な日時を記録するsessionStart、オープニングレンジ期間が終了する時刻を示すrangeEndTimeがあります。オープニングレンジの境界はrangeHigh(初期値0.0)とrangeLowで追跡し、rangeDefinedは現在のセッションのレンジが完全に確定したかどうかを示すブール型フラグとして機能します。その他の変数も名前から概ね理解できるものです。これらの設定が完了したら、システムを初期化するためにマジックナンバーを設定すれば準備は完了です。

//+------------------------------------------------------------------+
//| EA Start Function                                                |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(UniqueID);                      //--- Set magic number
   return(INIT_SUCCEEDED);                                        //--- Return success
}

OnInitイベントハンドラでは、プログラムが初めて読み込まれたとき、またはチャートにアタッチされたときに自動実行されます。ここでは、ユーザー定義のUniqueIDをobj_Tradeオブジェクトのマジックナンバーとして設定するために、obj_Trade.SetExpertMagicNumber(UniqueID)を呼び出します。これにより、プログラムによって開かれたすべての取引にはこの一意の識別子が付与され、複数のプログラムや手動取引が同じ口座でおこなわれている場合でも、正確にフィルタリングおよび管理できるようになります。最後に、INIT_SUCCEEDEDを返すことで、初期化が問題なく完了し、プログラムが正常に動作可能であることをプラットフォームに通知します。また、ロジックが整い次第、可視化のために使用するいくつかのヘルパー関数を定義する予定です。

//+------------------------------------------------------------------+
//| Render Horizontal Level                                          |
//+------------------------------------------------------------------+
void RenderLevel(string objName, double levelVal, color levelClr, string levelDesc) {
   ObjectDelete(ChartID(), objName);                               //--- Delete object
   ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelVal);    //--- Create hline
   ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelClr);  //--- Set color
   ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); //--- Set style
   ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, levelDesc); //--- Set tooltip
   ChartRedraw(ChartID());                                         //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Render Vertical Line                                             |
//+------------------------------------------------------------------+
void RenderVLine(string objName, datetime timeVal, color lineClr, string desc) {
   ObjectDelete(ChartID(), objName);                              //--- Delete object
   ObjectCreate(ChartID(), objName, OBJ_VLINE, 0, timeVal, 0);    //--- Create vline
   ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, lineClr);  //--- Set color
   ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); //--- Set style
   ObjectSetInteger(ChartID(), objName, OBJPROP_WIDTH, 1);        //--- Set width
   ObjectSetInteger(ChartID(), objName, OBJPROP_BACK, true);      //--- Set back
   ObjectSetInteger(ChartID(), objName, OBJPROP_RAY, true);       //--- Set ray
   ObjectSetInteger(ChartID(), objName, OBJPROP_HIDDEN, true);    //--- Set hidden
   ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, desc);    //--- Set tooltip
   ChartRedraw(ChartID());                                        //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Render Text Label                                                |
//+------------------------------------------------------------------+
void RenderText(string objName, datetime timeVal, double priceVal, string textStr, color textClr, int anchorVal) {
   ObjectDelete(ChartID(), objName);                              //--- Delete object
   ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, timeVal, priceVal); //--- Create text
   ObjectSetString(ChartID(), objName, OBJPROP_TEXT, textStr);    //--- Set text
   ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textClr);  //--- Set color
   ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorVal); //--- Set anchor
   ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10);    //--- Set fontsize
   ChartRedraw(ChartID());                                        //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Draw Entry Arrow                                                 |
//+------------------------------------------------------------------+
void DrawEntryArrow(datetime timeVal, double priceVal, bool isBuy) {
   string markerName = "EntryMarker_" + IntegerToString(timeVal); //--- Marker name
   ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, timeVal, priceVal); //--- Create arrow
   int arrowCode = isBuy ? 233 : 234;                             //--- Arrow code
   color arrowClr = isBuy ? clrBlue : clrRed;                     //--- Arrow color
   int anchor = isBuy ? ANCHOR_BOTTOM : ANCHOR_TOP;               //--- Anchor
   ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, arrowCode); //--- Set code
   ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, arrowClr); //--- Set color
   ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, anchor); //--- Set anchor
   ChartRedraw(ChartID());                                        //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Count Active Positions by Type                                   |
//+------------------------------------------------------------------+
int ActivePositions(ENUM_POSITION_TYPE posType) {
   int total = 0;                                                 //--- Init total
   for (int pos = PositionsTotal() - 1; pos >= 0; pos--) {        //--- Iterate positions
      if (PositionGetSymbol(pos) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID && PositionGetInteger(POSITION_TYPE) == posType) { //--- Check position
         total++;                                                    //--- Increment total
      }
   }
   return total;                                                  //--- Return total
}

ここでは、チャートの可視化やポジション管理をおこなういくつかのヘルパー関数を作成します。これにより、オープニングレンジや取引シグナルが明確に表示され、かつコードの整理も保たれます。RenderLevel関数は、レンジの高値と安値の水平線を描画または更新します。指定した名前の既存オブジェクトを削除し、指定された価格レベルに新しいOBJ_HLINE を作成します。色を設定(高値は緑、安値は赤)、点線スタイルを適用し、説明用のツールチップを追加し、チャートを再描画して即座に確認できるようにします。

同様に、RenderVLine関数は、セッション開始やレンジ終了の時間を示す垂直線を描画します。既存のオブジェクトを削除し、指定された日時にOBJ_VLINEを作成します。青色、点線スタイル、幅1、背景表示、右方向へのレイ表示、オブジェクトリストから非表示、正確な時間を示すツールチップを設定し、ChartRedraw関数でチャートを再描画します。RenderText関数は、開始・終了時刻や「ORB High」「ORB Low」などの注釈を含む、カスタマイズ可能なテキストラベルを追加します。既存のテキストオブジェクトを削除し、指定された時間と価格の座標にOBJ_TEXTを作成し、テキスト内容やその他のプロパティを設定します。

取引エントリーにはDrawEntryArrowを実装しています。この関数は、実行時にチャート上に視覚的マーカーを配置します。現在の時刻を使って一意の名前を生成し、OBJ_ARROWを作成します。買いの場合はWingdingsのシンボル233(上矢印)、売りの場合は234(下矢印)を選択します。ロングは青、ショートは赤を適用し、矢印を正しい位置(下または上)に固定してチャートを再描画します。矢印コードはMQL5のWingdingsフォントに対応しており、任意に切り替えることが可能です。

MQL5 WINGDINGSフォントコード

最後に、ActivePositions関数を定義します。これは、このプログラムに属する特定のタイプ(買いまたは売り)のポジションがいくつ開かれているかを正確に数えるための関数です。関数はすべてのポジションを後ろから順にループし、銘柄が一致するか、マジックナンバーが「UniqueID」と一致するか、さらにポジションタイプがPOSITION_TYPE_BUYであるかPOSITION_TYPE_SELLであるかを確認し、該当するポジション数を返します。これで、毎日のレンジをまず定義するところから、戦略の実装を開始できるようになります。

//+------------------------------------------------------------------+
//| Tick Processing Function                                         |
//+------------------------------------------------------------------+
void OnTick() {
   datetime currentTime = TimeCurrent();                          //--- Get current time
   MqlDateTime timeStruct;                                        //--- Time structure
   TimeToStruct(currentTime, timeStruct);                         //--- Convert to struct
   // Determine if a new session has started
   string currentTimeStr = StringFormat("%02d:%02d", timeStruct.hour, timeStruct.min); //--- Format time string
   if (currentTimeStr == SessionStartTime && sessionStart != currentTime - (timeStruct.hour * 3600 + timeStruct.min * 60 + timeStruct.sec)) { //--- Check new session
      sessionStart = currentTime - timeStruct.sec;                //--- Align to minute start
      rangeEndTime = sessionStart + RangeDurationMinutes * 60;    //--- Calc end time
      rangeHigh = 0.0;                                            //--- Reset high
      rangeLow = DBL_MAX;                                         //--- Reset low
      rangeDefined = false;                                       //--- Reset defined
      breakoutHigh = false;                                       //--- Reset high breakout
      breakoutLow = false;                                        //--- Reset low breakout
      tradedLong = false;                                         //--- Reset long traded
      tradedShort = false;                                        //--- Reset short traded
      lastConfirmTime = 0;                                        //--- Reset confirm time
      // Clean previous visuals for current levels
      ObjectDelete(ChartID(), highLevelObj);                      //--- Delete high level
      ObjectDelete(ChartID(), lowLevelObj);                       //--- Delete low level
      ObjectDelete(ChartID(), highTextObj);                       //--- Delete high text
      ObjectDelete(ChartID(), lowTextObj);                        //--- Delete low text
   }
   if (sessionStart == 0) return;                                 //--- Return if no session
   double currBid = SymbolInfoDouble(_Symbol, SYMBOL_BID);        //--- Get bid
   double currAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK);        //--- Get ask
   // Define the opening range
   if (currentTime < rangeEndTime) {                              //--- Check within range
      rangeHigh = MathMax(rangeHigh, iHigh(_Symbol, RangeTF, 0)); //--- Update high
      rangeLow = MathMin(rangeLow, iLow(_Symbol, RangeTF, 0));    //--- Update low
   } else if (!rangeDefined) {                                    //--- Check not defined
      rangeDefined = true;                                        //--- Set defined
      // Draw the opening range rectangle
      string rectObj = "ORB_Rectangle_" + IntegerToString(sessionStart); //--- Rect name
      ObjectCreate(ChartID(), rectObj, OBJ_RECTANGLE, 0, sessionStart, rangeHigh, rangeEndTime, rangeLow); //--- Create rect
      ObjectSetInteger(ChartID(), rectObj, OBJPROP_COLOR, clrLightBlue); //--- Set color
      ObjectSetInteger(ChartID(), rectObj, OBJPROP_FILL, true);   //--- Set fill
      ObjectSetInteger(ChartID(), rectObj, OBJPROP_BACK, true);   //--- Set back
      ObjectSetInteger(ChartID(), rectObj, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
      ChartRedraw(ChartID());                                     //--- Redraw chart
      // Add vertical lines for start and end
      string startVLineObj = "ORB_StartVLine_" + IntegerToString(sessionStart); //--- Start vline name
      RenderVLine(startVLineObj, sessionStart, clrBlue, "ORB Start at " + TimeToString(sessionStart, TIME_MINUTES)); //--- Render start vline
      string endVLineObj = "ORB_EndVLine_" + IntegerToString(sessionStart); //--- End vline name
      RenderVLine(endVLineObj, rangeEndTime, clrBlue, "ORB End at " + TimeToString(rangeEndTime, TIME_MINUTES)); //--- Render end vline
      // Add time text labels for start and end
      double textOffset = (rangeHigh - rangeLow) * 0.05;          //--- Calc offset
      string startTimeTextObj = "ORB_StartTime_Text_" + IntegerToString(sessionStart); //--- Start text name
      RenderText(startTimeTextObj, sessionStart, rangeLow - textOffset, TimeToString(sessionStart, TIME_MINUTES), clrBlue, ANCHOR_UPPER); //--- Render start text
      string endTimeTextObj = "ORB_EndTime_Text_" + IntegerToString(sessionStart); //--- End text name
      RenderText(endTimeTextObj, rangeEndTime, rangeLow - textOffset, TimeToString(rangeEndTime, TIME_MINUTES), clrBlue, ANCHOR_UPPER); //--- Render end text
      // Render high and low levels
      RenderLevel(highLevelObj, rangeHigh, clrGreen, "ORB High"); //--- Render high level
      RenderLevel(lowLevelObj, rangeLow, clrRed, "ORB Low");      //--- Render low level
      // Add text labels
      RenderText(highTextObj, rangeEndTime, rangeHigh, "ORB High", clrGreen, ANCHOR_RIGHT_LOWER); //--- Render high text
      RenderText(lowTextObj, rangeEndTime, rangeLow, "ORB Low", clrRed, ANCHOR_RIGHT_UPPER); //--- Render low text
   } 
}

OnTickイベントハンドラでは、まずTimeCurrentを使ってサーバーの現在時刻をcurrentTimeに取得し、TimeToStructMqlDateTime構造体に変換して、時刻や分などの個別の要素にアクセスできるようにします。その後、StringFormatを使って現在時刻を「HH:MM」形式の文字列に整形し、currentTimeStrに格納します。これにより、時刻情報を簡単に扱える構造が完成します。

struct MqlDateTime {
   int year;           // Year
   int mon;            // Month
   int day;            // Day
   int hour;           // Hour
   int min;            // Minutes
   int sec;            // Seconds
   int day_of_week;    // Day of week (0-Sunday, 1-Monday, ... ,6-Saturday)
   int day_of_year;    // Day number of the year (January 1st is assigned the number value of zero)
};

構造体に分けることで、時間の各要素(時・分など)を簡単に取り出せるようになります。新しい取引セッションの正確な開始を検出するために、この文字列をユーザー定義のSessionStartTimeと比較します。さらに、追加の条件として、sessionStartがすでに当日の同じ分に設定されていないことを確認することで、1日に1回だけ処理が実行されるようにしています(秒単位を引いて正規化)。新しいセッションが始まった場合は、sessionStartをその分の開始時刻に正確に合わせ(残り秒を引く)、rangeEndTime「RangeDurationMinutes × 60秒」で計算します。初期更新を正しくおこなうためにrangeHighを0.0に、rangeLowをdouble型の最大値にリセットし、フラグ(rangeDefined、breakoutHigh、breakoutLow、tradedLong、tradedShort、lastConfirmTime)をすべてクリアします。また、前回のセッションで描画した水平ラインやテキストオブジェクトを削除し、クリーンな状態に準備します。まだアクティブなセッションが検出されていない場合(sessionStart == 0)は、余計な処理を避けるために単に戻ります。それ以外の場合は、SymbolInfoDoubleで現在のBIDとASK価格を取得します。

オープニングレンジ形成期間中(currentTimeがrangeEndTimeより小さい間)、レンジの境界を継続的に更新します。rangeHighは現在の値と、RangeTFの最新バー(シフト0)の高値(iHigh)の最大値に設定し、rangeLowは現在の値と最新バーの安値(iLow)の最小値に設定します。レンジ期間が終了し、まだレンジが確定していない場合(!rangeDefined)、rangeDefinedをtrueに設定し、完成したオープニングレンジの可視化に進みます。

オープニングレンジの可視化では、開始時刻のsessionStartと高値rangeHighから終了時刻のrangeEndTimeと安値rangeLowまで塗りつぶしの長方形を描画し、セッションタイムスタンプに基づくユニーク名、塗りつぶしスタイル、背景配置を適用します。開始時刻と終了時刻には、RenderVLineを使って垂直の青点線を追加し、説明用のツールチップを付けます。時間ラベルはレンジの下に、レンジサイズの5%オフセットで青色、上向きアンカーで配置します。最後に、RenderLevelを使って持続的な水平ラインを描画します。高値は緑、安値は赤で表示し、対応するテキストラベルをレンジ終了時刻の右側にアンカーして配置します。これにより、数時間や数日経過しても、正確なブレイクアウトレベルを常に確認できるようにします。コンパイルすると、次の結果が得られます。

ORBレンジ設立

レンジが確定したら、あとはそのレンジとブレイクアウトを追跡するだけです。いずれかのレンジを突破した時点で方向を判断し、それに応じたブレイクアウトセットアップを決定してエントリーします。とてもシンプルです。そのためのロジックを以下に実装します。

if (!rangeDefined) return;                                     //--- Return if not defined
// Detect breakout
bool justBreached = false;                                     //--- Init just breached
if (currAsk > rangeHigh && !breakoutHigh) {                    //--- Check high breakout
   breakoutHigh = true;                                        //--- Set high breakout
   justBreached = true;                                        //--- Set just breached
   breakoutPrice = currAsk;                                    //--- Set breakout price
} else if (currBid < rangeLow && !breakoutLow) {               //--- Check low breakout
   breakoutLow = true;                                         //--- Set low breakout
   justBreached = true;                                        //--- Set just breached
   breakoutPrice = currBid;                                    //--- Set breakout price
}
if ((breakoutHigh || breakoutLow) && !(tradedLong || tradedShort)) { //--- Check breakout and not traded
   // Confirm breakout with bar closures if enabled
   bool confirmed = false;                                      //--- Init confirmed
   if (ConfirmBars == 0) {                                      //--- Check no confirm
      confirmed = true;                                         //--- Set confirmed
   } else {                                                     //--- Else
      datetime currConfirmTime = iTime(_Symbol, RangeTF, 0);    //--- Get confirm time
      if (currConfirmTime != lastConfirmTime) {                 //--- Check new confirm
         lastConfirmTime = currConfirmTime;                     //--- Update last confirm
         int confirmCount = 0;                                  //--- Init count
         for (int i = 1; i <= ConfirmBars; i++) {               //--- Iterate bars
            double closePrice = iClose(_Symbol, RangeTF, i);    //--- Get close
            if (breakoutHigh && closePrice > rangeHigh) confirmCount++; //--- Check high
            if (breakoutLow && closePrice < rangeLow) confirmCount++; //--- Check low
         }
         if (confirmCount >= ConfirmBars) confirmed = true;     //--- Set confirmed
      }
   }
   if (confirmed && UseBreakoutFilter) {                        //--- Check confirmed and filter
      // Additional filter logic if needed, but for now assume confirmed
   }
   if (confirmed) {                                             //--- Check confirmed
      double sl = 0.0, tp = 0.0;                                //--- Init SL TP
      if (breakoutHigh && ActivePositions(POSITION_TYPE_BUY) < MaxPositionsDir && !tradedLong) { //--- Check long entry
         if (SLTP_Approach == Dynamic_Method) {                  //--- Check dynamic
            double rangeSize = rangeHigh - rangeLow;             //--- Calc range size
            sl = NormalizeDouble(rangeLow, _Digits);             //--- Set SL
            tp = NormalizeDouble(currAsk + rangeSize * RR_Ratio, _Digits); //--- Set TP
         } else {                                                //--- Static
            sl = NormalizeDouble(currAsk - SL_Points * _Point, _Digits); //--- Set SL
            tp = NormalizeDouble(currAsk + (SL_Points * _Point) * RR_Ratio, _Digits); //--- Set TP
         }
         if (obj_Trade.Buy(TradeVolume, _Symbol, currAsk, sl, tp, "ORB Long Breakout")) { //--- Open buy
            if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Check success
               Print("Long Breakout: Entry at ", DoubleToString(currAsk, _Digits), " SL at ", DoubleToString(sl, _Digits), " TP at ", DoubleToString(tp, _Digits)); //--- Log entry
               DrawEntryArrow(currentTime, currBid, true);        //--- Draw arrow
               tradedLong = true;                                 //--- Set long traded
            }
         }
      } else if (breakoutLow && ActivePositions(POSITION_TYPE_SELL) < MaxPositionsDir && !tradedShort) { //--- Check short entry
         if (SLTP_Approach == Dynamic_Method) {                  //--- Check dynamic
            double rangeSize = rangeHigh - rangeLow;             //--- Calc range size
            sl = NormalizeDouble(rangeHigh, _Digits);            //--- Set SL
            tp = NormalizeDouble(currBid - rangeSize * RR_Ratio, _Digits); //--- Set TP
         } else {                                                //--- Static
            sl = NormalizeDouble(currBid + SL_Points * _Point, _Digits); //--- Set SL
            tp = NormalizeDouble(currBid - (SL_Points * _Point) * RR_Ratio, _Digits); //--- Set TP
         }
         if (obj_Trade.Sell(TradeVolume, _Symbol, currBid, sl, tp, "ORB Short Breakout")) { //--- Open sell
            if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Check success
               Print("Short Breakout: Entry at ", DoubleToString(currBid, _Digits), " SL at ", DoubleToString(sl, _Digits), " TP at ", DoubleToString(tp, _Digits)); //--- Log entry
               DrawEntryArrow(currentTime, currAsk, false);       //--- Draw arrow
               tradedShort = true;                                //--- Set short traded
            }
         }
      }
   }
}

オープニングレンジが完全に確定したら、まずrangeDefinedがまだfalseであればすぐに戻り、ブレイクアウトのロジックが早まって実行されないようにします。その後、ブレイクアウトを監視します。現在のAsk価格がrangeHighを上回り、かつbreakoutHighがまだ設定されていない場合は、強気ブレイクアウトとしてbreakoutHighをtrueに設定し、正確なブレイクアウト価格をbreakoutPriceに記録し、新しい突破が発生したことを示します。安値ブレイクアウトについても同様に処理します。少なくとも1方向のブレイクアウトが発生しており、このセッションでまだロング・ショートいずれのエントリーもおこなわれていない場合(!tradedLong && !tradedShort)、確認ステージに進みます。ConfirmBarsが0の場合、ブレイクアウトは即時に確認済みと見なされます。それ以外の場合は、レンジ時間足の各新しいバーごとに(シフト0のiTimeをlastConfirmTimeと比較して検出)、過去のConfirmBars本のバーがレンジ外で決定的に終値を付けたかをカウントします。強気ならrangeHigh以上、弱気ならrangeLow以下です。必要な確認バー数が揃ったときにのみconfirmedをtrueに設定します。

入力パラメータUseBreakoutFilterは尊重されますが、現状では確認結果をそのまま通すだけで、将来的に追加フィルターを組み込む余地があります。確認が済んだら、選択された方法に応じてストップロスとテイクプロフィットを計算します。強気ブレイクアウトの場合、買いポジションがMaxPositionsDir未満でまだロングがない場合、ダイナミックモードではストップロスをレンジ安値(正規化済み)に、テイクプロフィットを現在のAsk価格にレンジ幅×RR_Ratioを加えた位置に設定します。静的モードでは、ストップロスをアスク価格の下に固定のSL_Points、テイクプロフィットを同距離×比率上に置きます。その後、obj_Trade.Buyで買い注文を実行し、成功(TRADE_RETCODE_DONE)した場合、Expertsタブにエントリーの詳細を記録し、DrawEntryArrowで青色の上向き矢印を描画し、tradedLongをtrueに設定してこのセッションでの追加ロングを防ぎます。弱気ブレイクアウトも同様の手順で処理されます。コンパイルすると、次の結果が得られます。

売りORBシグナルを確認

画像から分かるように、ブレイクアウトが発生した時点でエントリーします。次に必要なのは、必要に応じて市場が有利な方向へ動いた際にトレーリングストップを適用し、ポジションを管理することです。

//+------------------------------------------------------------------+
//| Apply Points Trailing Stop                                       |
//+------------------------------------------------------------------+
void ApplyPointsTrailing() {
   double point = _Point;                                         //--- Get point
   for (int i = PositionsTotal() - 1; i >= 0; i--) {              //--- Iterate positions
      if (PositionGetTicket(i) > 0) {                             //--- Check ticket
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID) { //--- Check symbol magic
            double sl = PositionGetDouble(POSITION_SL);              //--- Get SL
            double tp = PositionGetDouble(POSITION_TP);              //--- Get TP
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open
            ulong ticket = PositionGetInteger(POSITION_TICKET);      //--- Get ticket
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Points * point, _Digits); //--- Calc new SL
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Points * point) { //--- Check conditions
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Modify position
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Points * point, _Digits); //--- Calc new SL
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Points * point) { //--- Check conditions
                  obj_Trade.PositionModify(ticket, newSL, tp);       //--- Modify position
               }
            }
         }
      }
   }
}

//--- Call the function per tick in the "OnTick" event handler

if (TrailingType == Trailing_Points && PositionsTotal() > 0) { //--- Check trailing
   ApplyPointsTrailing();                                      //--- Apply trailing
}

ここでは、Trailing_Pointsモードが選択されている場合にストップロスを動的に追従させ、市場が有利な方向に動いた際に利益を保護するためのApplyPointsTrailing関数を実装します。関数の冒頭では、_Pointを使用して銘柄のポイント値をpointに格納します。その後、インデックスの競合を避けながら安全に変更処理をおこなうため、全ポジションを後ろから順にループします。各有効なチケットについて、そのポジションが現在の銘柄に属しており、かつマジックナンバーがUniqueIDであることを確認します。そして、現在のストップロス、テイクプロフィット、建値、チケット番号を取得します。

買いポジションの場合、現在のBidから「Trailing_Stop_Points×point」を引いた値(銘柄の桁数に正規化)を新しいストップロス候補として計算します。この新しいレベルが既存のストップロスより高く(つまりストップを引き締める形で)、かつ未実現利益がMin_Profit_To_Trail_Points×pointを超えている場合にのみ変更を適用します。これにより、十分な利益バッファが生まれてからトレーリングが開始されます。ポジションはobj_Trade.PositionModifyで更新され、元のテイクプロフィットはそのまま保持されます。売りポジションの場合も、まったく同じロジックを反対方向で適用します。最後に、OnTickの末尾でトレーリングが有効か(TrailingType==Trailing_Points)かつポジションが存在するかを確認します。条件を満たす場合、ティックごとにApplyPointsTrailingを呼び出し、遅延なくリアルタイムで利益保護をおこないます。最後に、プログラムをチャートから削除した際には、作成した可視化オブジェクトを削除する必要があります。

//+------------------------------------------------------------------+
//| EA Stop Function                                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int code) {
   ObjectDelete(ChartID(), highLevelObj);                         //--- Delete high level
   ObjectDelete(ChartID(), lowLevelObj);                          //--- Delete low level
   ObjectDelete(ChartID(), highTextObj);                          //--- Delete high text
   ObjectDelete(ChartID(), lowTextObj);                           //--- Delete low text
   // Clean dynamic objects
   ObjectsDeleteAll(ChartID(), "ORB_Rectangle_", OBJ_RECTANGLE);  //--- Delete rectangles
   ObjectsDeleteAll(ChartID(), "ORB_StartVLine_", OBJ_VLINE);     //--- Delete start vlines
   ObjectsDeleteAll(ChartID(), "ORB_EndVLine_", OBJ_VLINE);       //--- Delete end vlines
   ObjectsDeleteAll(ChartID(), "ORB_StartTime_Text_", OBJ_TEXT);  //--- Delete start texts
   ObjectsDeleteAll(ChartID(), "ORB_EndTime_Text_", OBJ_TEXT);    //--- Delete end texts
   ObjectsDeleteAll(ChartID(), "EntryMarker_", OBJ_ARROW);        //--- Delete entry markers
}

OnDeinit関数では、プログラムがチャートから削除されたとき、またはターミナルが終了するときに処理が実行されます。まず、名前で指定された4つの永続オブジェクトを削除します。具体的には、水平ラインであるhighLevelObjとlowLevelObj、そしてそれぞれのテキストラベルであるhighTextObjとlowTextObjです。そのあと、ObjectsDeleteAllを使用して、現在のセッションおよび過去のセッションで動的に作成されたすべてのオブジェクトを削除します。この完全なクリーンアップにより、複数のセッションやチャートの再読み込みによってオブジェクトが蓄積してしまうことを防ぎます。トレーリングストップを有効にしてコンパイルすると、次のような結果になります。

完全なORB GIF

この可視化から分かるように、レンジを定義し、ポジションを取り、必要に応じてトレーリングストップを適用してポジションを管理することで、目的としていた動作を実現できています。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

徹底的なバックテストによって、次の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

本記事ではMQL5を用いて、セッションベースのオープニングレンジブレイクアウト(ORB)システムを開発しました。このシステムでは、セッション開始時刻やオープニングレンジの長さ(分単位)を自由に設定でき、選択した時間足に基づいて実際の高値と安値を自動的に特定します。また、任意で複数バーの終値による確認をおこないながらブレイクアウトを検出し、ブレイクした方向にのみエントリーします。

免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。本プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。

このセッションベースのオープニングレンジブレイクアウト戦略を使えば、任意の市場セッションでのデイトレードのブレイクアウトセットアップに対応でき、今後の取引戦略の最適化にも活用することができます。取引をお楽しみください。

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

添付されたファイル |
最後のコメント | ディスカッションに移動 (3)
linfo2
linfo2 | 26 11月 2025 において 22:36
こんにちは、アランとあなたの継続的な貢献のおかげで、うまくあなたよりもまとめた、私はあなたが開始時間を 自動化するためのソリューションを持っているか疑問に思っていた? 一般的に、私はブローカーの時間変更を使用していますが、私はNZにいるので、それは常に私にとって正確ではありません私のブローカーはAUにあり、austaliasオフセットや東京オフセットとニューヨークオフセットがある時間のような要因があります。私はこれを動作させるためにトラブルを持っていた、任意の提案は高く評価されます。 チャットgptは私に各国サマータイム日付をチェックするsciptを与える、より雄弁なソリューションがあるかどうか疑問に思っていた
Allan Munene Mutiiria
Allan Munene Mutiiria | 27 11月 2025 において 08:09
linfo2 開始時間を 自動化するためのソリューションを持っているか疑問に思っていた? 一般的に、私はブローカーの時間変更を使用していますが、私はNZにいるので、それは常に私にとって正確ではありません私のブローカーはAUにあり、austaliasオフセットや東京オフセットとニューヨークオフセットがある時間のような要因があります。私はこれを動作させるのに苦労している、任意の提案を高く評価。 チャットgptは私に各国サマータイム日付をチェックするsciptを与えるが、より雄弁なソリューションがあるかどうか疑問に思っていた

こんにちは。親切なフィードバックありがとう。ブローカーの時刻の代わりに現地時刻を使用することで、コード内で時刻を定義することができます。例をご覧ください。

現地時間:

TimeLocal()

取引サーバーを使用して、コンピュータの設定に従って直接時間を使用することもできます:

TimeTradeServer()

GMT時間:

TimeGMT()

また、以下のように専用の日時を定義することもできます:

datetime my_time = D'2025.11.27 11:07'
MqlDateTime my_struct_time;

最適な方法をお選びください。ありがとうございました。

Stanislav Korotky
Stanislav Korotky | 27 11月 2025 において 11:46
linfo2 開始時間を 自動化するためのソリューションを持っているか疑問に思っていた? 一般的に、私はブローカーの時間変更を使用していますが、私はNZにいるので、それは常に私にとって正確ではありません私のブローカーはAUにあり、austaliasオフセットや東京オフセットとニューヨークオフセットがある時間のような要因があります。私はこれを動作させるのに苦労している、任意の提案を高く評価した。 チャットgptは私に各国サマータイム日付をチェックするsciptを与える、より雄弁なソリューションがあるかどうか疑問に思っていた

あなたの問題は十分に明確ではありません。しかし、Local TimezonesとLocal Session Hours ライブラリを試すか、もっとシンプルなTimeServerDaylightSavingsを 試すことができます。時間調整なしでは、通常サマータイムやタイムゾーンの切り替えの影響を受ける履歴でストラテジーを確実にテストすることはできません。または、おそらく オンラインでタイムゾーンの変更を検出するためにブローカーのデイライト(サマータイム)スケジュールを決定 したいのでしょう

残念ながら、組み込みのMQL5 APIは、既製の、より雄弁なソリューションを提供していません。

EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
MQL5入門(第28回):MQL5のAPIとWebRequest関数の習得(II) MQL5入門(第28回):MQL5のAPIとWebRequest関数の習得(II)
本記事では、APIとMQL5のWebRequest関数を使用して、外部プラットフォームから価格データを取得および抽出する方法を解説します。URLの構造、APIレスポンスの形式、サーバーデータを可読な文字列へ変換する方法、そしてJSONレスポンスから特定の値を識別および抽出する方法を学びます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
分析型ボリュームプロファイル取引(AVPT):流動性アーキテクチャ、市場メモリ、アルゴリズム実行 分析型ボリュームプロファイル取引(AVPT):流動性アーキテクチャ、市場メモリ、アルゴリズム実行
分析型ボリュームプロファイル取引(AVPT, Analytical Volume Profile Trading)は、流動性構造と市場記憶がプライスアクションに与える影響を分析し、機関投資家のポジション構築や出来高駆動の構造をより深く理解する手法です。POC、HVN、LVN、バリューエリアを可視化することで、受容、拒否、アンバランスゾーンを高い精度で特定できます。