English Deutsch
preview
MQL5での取引戦略の自動化(第24回):リスク管理とトレーリングストップを備えたロンドンセッションブレイクアウトシステム

MQL5での取引戦略の自動化(第24回):リスク管理とトレーリングストップを備えたロンドンセッションブレイクアウトシステム

MetaTrader 5トレーディング |
78 14
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第23回)では、MetaQuotes Language 5 (MQL5)でエンベロープトレンド取引向けのゾーンリカバリー(Zone Recovery)システムを強化し、トレーリングストップおよびマルチバスケット取引を導入して、利益保護とシグナル処理の精度を向上させました。今回の第24回では、ロンドン市場開場前のレンジを検出し、ペンディング注文(指値・逆指値注文)を自動的に配置する「ロンドンセッションブレイクアウトシステムを開発します。このシステムには、リスクリワード比率、ドローダウン制限、リアルタイム監視用のコントロールパネルなど、複数のリスク管理機能を組み込みます。本記事では以下のトピックを扱います。

  1. ロンドンセッションブレイクアウト戦略の理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

この記事を読み終える頃には、高度なリスク管理機能を備えた完全なMQL5ブレイクアウトプログラムを作成できるようになります。それでは、さっそく始めましょう。


ロンドンセッションブレイクアウト戦略の理解

ロンドンセッションブレイクアウト戦略は、ロンドン市場の開場時に発生するボラティリティの上昇を狙う手法です。ロンドン市場が開く前の時間帯に形成される価格レンジを特定し、そのレンジからのブレイクアウトを捉えるためにペンディング注文を配置します。この戦略が重要である理由は、ロンドンセッションが一般的に高い流動性と活発な価格変動を伴うため、安定した利益機会を提供する可能性が高い点にあります。ただし、偽のブレイクアウトやドローダウンを回避するためには、慎重なリスク管理が欠かせません。

本システムでは、ロンドン開場前の高値と安値を計算し、一定のオフセットを加えた位置に買いストップおよび売りストップ注文を設定します。さらに、利益確定にはリスクリワード比率を基にしたテイクプロフィットを採用し、利益を確保するためのトレーリングストップを組み込みます。また、保有ポジション数および1日のドローダウンに上限を設け、資金を保護します。リアルタイム監視をおこなうためのコントロールパネルと、セッション特有の条件チェックを組み合わせることで、取引が定義された範囲内でのみおこなわれるよう制御します。これにより、変化する市場環境にも柔軟に対応できるシステムを構築します。要約すると、私たちが目指すのは次のような構成を持つシステムです。

戦略設計図


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、インジケーターフォルダを見つけ、[新規]タブをクリックして、表示される手順に従ってファイルを作成します。ファイルが作成されたら、コーディング環境でプログラムをより柔軟にするための入力パラメータや構造体を宣言していきます。

//+------------------------------------------------------------------+
//|                                        London 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"
#property strict

#include <Trade\Trade.mqh> //--- Include Trade library for trading operations

//--- Enumerations
enum ENUM_TRADE_TYPE {     //--- Enumeration for trade types
   TRADE_ALL,              // All Trades (Buy and Sell)
   TRADE_BUY_ONLY,         // Buy Trades Only
   TRADE_SELL_ONLY         // Sell Trades Only
};

//--- Input parameters
sinput group "General EA Settings"
input double inpTradeLotsize = 0.01; // Lotsize
input ENUM_TRADE_TYPE TradeType = TRADE_ALL; // Trade Type Selection
sinput int MagicNumber = 12345;     // Magic Number
input double RRRatio = 1.0;        // Risk to Reward Ratio
input int StopLossPoints = 500;    // Stop loss in points
input int OrderOffsetPoints = 10;   // Points offset for Orders
input bool DeleteOppositeOrder = true; // Delete opposite order when one is activated?
input bool UseTrailing = false;    // Use Trailing Stop?
input int TrailingPoints = 50;     // Trailing Points (distance)
input int MinProfitPoints = 100;   // Minimum Profit Points to start trailing

sinput group "London Session Settings"
input int LondonStartHour = 9;        // London Start Hour
input int LondonStartMinute = 0;      // London Start Minute
input int LondonEndHour = 8;          // London End Hour
input int LondonEndMinute = 0;        // London End Minute
input int MinRangePoints = 100;       // Min Pre-London Range in points
input int MaxRangePoints = 300;       // Max Pre-London Range in points

sinput group "Risk Management"
input int MaxOpenTrades = 2;       // Maximum simultaneous open trades
input double MaxDailyDrawdownPercent = 5.0; // Max daily drawdown % to stop trading

//--- Structures
struct PositionInfo {      //--- Structure for position information
   ulong ticket;           // Position ticket
   double openPrice;      // Entry price
   double londonRange;    // Pre-London range in points for this position
   datetime sessionID;    // Session identifier (day)
   bool trailingActive;   // Trailing active flag
};

ロンドンセッションブレイクアウトシステムを実装するには、まず、<Trade\Trade.mqh>ライブラリをインクルードし、取引タイプを定義する列挙型、入力パラメータ、ポジション管理用の構造体を定義します。<Trade\Trade.mqh>をインクルードすることで、注文発注やポジション変更などの取引操作をおこなうためのCTradeクラスを利用できます。列挙型ENUM_TRADE_TYPEには、売買両方をおこなうTRADE_ALL、買いのみのTRADE_BUY_ONLY、売りのみのTRADE_SELL_ONLYを定義し、取引方向を制限できるようにします。

次に、入力パラメータを「General EA Settings」「London Session Settings」「Risk Management」の各グループに分けて設定します。「General EA Settings」では、ロットサイズ(inpTradeLotsize)を0.01に設定し、取引タイプ(TradeType)は列挙型を使用してデフォルトをTRADE_ALLにします。EAの取引を識別するためにMagicNumberを12345に設定し、リスクリワード比率(RRRatio)を1.0に、ストップロス距離(StopLossPoints)を500に設定します。エントリー位置のオフセット(OrderOffsetPoints)を10に設定し、片方の注文が成立した際に反対の指値注文を削除するかどうかを制御するDeleteOppositeOrderをtrueにします。トレーリングストップの使用を切り替えるUseTrailingはfalseに設定し、トレーリングの距離(TrailingPoints)を50に、トレーリングを開始するための最低利益幅(MinProfitPoints)を100に設定します。

「London Session Settings」では、ロンドンセッションの開始時刻としてLondonStartHourを9、LondonStartMinuteを0に設定し、終了時刻としてLondonEndHourを8、LondonEndMinuteを0に設定します。プレロンドン期間のレンジ幅を確認するために、最小レンジ(MinRangePoints)を100、最大レンジ(MaxRangePoints)を300に設定します。「Risk Management」では、同時に保有できる最大ポジション数を制限するためにMaxOpenTradesを2に設定し、1日の最大ドローダウン率を制御するためにMaxDailyDrawdownPercentを5.0に設定します。また、ポジションを追跡するための構造体「PositionInfo」を定義します。この構造体では、ポジションチケットを保持するticket、エントリー価格を保持するopenPrice、そのポジションにおけるプレロンドンレンジ幅を保持するlondonRange、セッション識別用の日付を保持するsessionID、そしてトレーリングが有効かどうかを示すbool値trailingActiveを管理します。コンパイルすると以下の出力が得られます。

入力セット

このように入力項目を体系的に設定したうえで、次にプログラム全体で使用するいくつかのグローバル変数を定義します。

//--- Global variables
CTrade obj_Trade;                 //--- Trade object
double PreLondonHigh = 0.0;       //--- Pre-London session high
double PreLondonLow = 0.0;        //--- Pre-London session low
datetime PreLondonHighTime = 0;   //--- Time of Pre-London high
datetime PreLondonLowTime = 0;    //--- Time of Pre-London low
ulong buyOrderTicket = 0;         //--- Buy stop order ticket
ulong sellOrderTicket = 0;        //--- Sell stop order ticket
bool panelVisible = true;         //--- Panel visibility flag
double LondonRangePoints = 0.0;   //--- Current session's Pre-London range
PositionInfo positionList[];      //--- Array to store position info
datetime lastCheckedDay = 0;      //--- Last checked day
bool noTradeToday = false;        //--- Flag to prevent trading today
bool sessionChecksDone = false;   //--- Flag for session checks completion
datetime analysisTime = 0;        //--- Time for London analysis
double dailyDrawdown = 0.0;       //--- Current daily drawdown
bool isTrailing = false;          //--- Global flag for any trailing active
const int PreLondonStartHour = 3; //--- Fixed Pre-London Start Hour
const int PreLondonStartMinute = 0; //--- Fixed Pre-London Start Minute

ここではプログラムで使用するグローバル変数を定義します。obj_Tradeは取引操作をおこなうためのCTradeオブジェクトです。PreLondonHighとPreLondonLowはレンジの高値・安値を格納するdouble型変数で、PreLondonHighTimeとPreLondonLowTimeはそれぞれの時刻を表すdatetimes型です。buyOrderTicketおよびsellOrderTicketは指値注文チケットを格納するulong型で、panelVisibleはパネル表示の有無を示すbool型です。LondonRangePointsは現在セッションのプレ・ロンドンレンジ幅を表すdouble型で、positionListはPositionInfo構造体の配列です。lastCheckedDayは最終確認日を示すdatetime、noTradeTodayは当日の取引を禁止するフラグのbool、sessionChecksDoneはセッションチェック完了フラグのboolです。analysisTimeはロンドン分析の時刻を表すdatetime、dailyDrawdownは1日あたりのドローダウンを表すdouble、isTrailingは任意のトレーリングが有効かを示すboolです。最後にPreLondonStartHourとPreLondonStartMinuteはプレロンドン開始時刻を示す定数のint型として定義します。

これでプログラムのグローバル変数が定義できたので、次にコントロールパネルを作成します。これは最も簡単なステップであり、その後により複雑な取引ロジックへ進みます。ここではまず、パネル作成に必要な関数を定義します。

//+------------------------------------------------------------------+
//| Create a rectangle label for the panel background                |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xD, int yD, int xS, int yS,
                    color clrBg, int widthBorder, color clrBorder = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT, ENUM_LINE_STYLE borderStyle = STYLE_SOLID) {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
        Print(__FUNCTION__, ": failed to create rec label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS); //--- Set x-size
    ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS); //--- Set y-size
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg); //--- Set background color
    ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
    ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle); //--- Set border style
    ObjectSetInteger(0, objName, OBJPROP_WIDTH, widthBorder); //--- Set border width
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBorder); //--- Set border color
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create a text label for panel elements                           |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xD, int yD,
                 string txt, color clrTxt = clrBlack, int fontSize = 10,
                 string font = "Arial") {
    ResetLastError();              //--- Reset last error
    if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) { //--- Create label
        Print(__FUNCTION__, ": failed to create the label! Error code = ", _LastError); //--- Log creation failure
        return false;              //--- Return failure
    }
    ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD); //--- Set x-distance
    ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD); //--- Set y-distance
    ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner
    ObjectSetString(0, objName, OBJPROP_TEXT, txt); //--- Set text
    ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt); //--- Set color
    ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
    ObjectSetString(0, objName, OBJPROP_FONT, font); //--- Set font
    ObjectSetInteger(0, objName, OBJPROP_BACK, false); //--- Set foreground
    ObjectSetInteger(0, objName, OBJPROP_STATE, false); //--- Set state
    ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); //--- Disable selectable
    ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); //--- Disable selected
    ChartRedraw(0);                //--- Redraw chart
    return true;                   //--- Return success
}

ここでは、コントロールパネルのユーザーインターフェース(UI)要素を作成するためのユーティリティ関数を実装します。まず、createRecLabel関数を作成し、パネルの背景となる矩形ラベルを生成します。この関数はいくつかのパラメータを受け取ります。最初にResetLastError関数でエラーをリセットし、ObjectCreate関数を使用してOBJ_RECTANGLE_LABELオブジェクトを作成します。作成に失敗した場合はPrint関数でエラーをログに記録し、falseを返します。その後、ObjectSetInteger関数を使用してOBJPROP_XDISTANCEなど、必要なすべての整数型プロパティを設定します。設定が完了したらChartRedraw関数でチャートを再描画し、trueを返します。

次に、createLabel関数を作成し、パネル内にテキストラベルを生成します。この関数は「objName」「xD」「yD」「txt」「clrTxt」「fontSize」「font」といったパラメータを受け取ります。最初にResetLastErrorでエラーをリセットし、ObjectCreateでOBJ_LABELオブジェクトを作成します。作成に失敗した場合はPrintでログを出力し、falseを返します。続いて、createRecLabel関数と同様にObjectSetInteger関数で位置やサイズなどのプロパティを設定しますが、テキストラベルではさらにObjectSetString関数を使用してOBJPROP_TEXTおよびOBJPROP_FONTプロパティも設定します。最後にチャートを再描画し、trueを返します。これらの関数を使用することで、セッションデータやプログラムの状態を監視するための動的なコントロールパネルを構築および更新できるようになります。

string panelPrefix = "LondonPanel_"; //--- Prefix for panel objects

//+------------------------------------------------------------------+
//| Create the information panel                                     |
//+------------------------------------------------------------------+
void CreatePanel() {
   createRecLabel(panelPrefix + "Background", 10, 10, 270, 200, clrMidnightBlue, 1, clrSilver); //--- Create background
   createLabel(panelPrefix + "Title", 20, 15, "London Breakout Control Center", clrGold, 12); //--- Create title
   createLabel(panelPrefix + "RangePoints", 20, 40, "Range (points): ", clrWhite, 10); //--- Create range label
   createLabel(panelPrefix + "HighPrice", 20, 60, "High Price: ", clrWhite); //--- Create high price label
   createLabel(panelPrefix + "LowPrice", 20, 80, "Low Price: ", clrWhite); //--- Create low price label
   createLabel(panelPrefix + "BuyLevel", 20, 100, "Buy Level: ", clrWhite); //--- Create buy level label
   createLabel(panelPrefix + "SellLevel", 20, 120, "Sell Level: ", clrWhite); //--- Create sell level label
   createLabel(panelPrefix + "AccountBalance", 20, 140, "Balance: ", clrWhite); //--- Create balance label
   createLabel(panelPrefix + "AccountEquity", 20, 160, "Equity: ", clrWhite); //--- Create equity label
   createLabel(panelPrefix + "CurrentDrawdown", 20, 180, "Drawdown (%): ", clrWhite); //--- Create drawdown label
   createRecLabel(panelPrefix + "Hide", 250, 10, 30, 22, clrCrimson, 1, clrNONE); //--- Create hide button
   createLabel(panelPrefix + "HideText", 258, 12, CharToString(251), clrWhite, 13, "Wingdings"); //--- Create hide text
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_SELECTABLE, true); //--- Make hide selectable
   ObjectSetInteger(0, panelPrefix + "Hide", OBJPROP_STATE, true); //--- Set hide state
}

//+------------------------------------------------------------------+
//| Update panel with current data                                   |
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- Format range text
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- Update range text
   
   string highText = "High Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh, _Digits) : "N/A"); //--- Format high text
   ObjectSetString(0, panelPrefix + "HighPrice", OBJPROP_TEXT, highText); //--- Update high text
   
   string lowText = "Low Price: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow, _Digits) : "N/A"); //--- Format low text
   ObjectSetString(0, panelPrefix + "LowPrice", OBJPROP_TEXT, lowText); //--- Update low text
   
   string buyText = "Buy Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonHigh + OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format buy text
   ObjectSetString(0, panelPrefix + "BuyLevel", OBJPROP_TEXT, buyText); //--- Update buy text
   
   string sellText = "Sell Level: " + (LondonRangePoints > 0 ? DoubleToString(PreLondonLow - OrderOffsetPoints * _Point, _Digits) : "N/A"); //--- Format sell text
   ObjectSetString(0, panelPrefix + "SellLevel", OBJPROP_TEXT, sellText); //--- Update sell text
   
   string balanceText = "Balance: " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2); //--- Format balance text
   ObjectSetString(0, panelPrefix + "AccountBalance", OBJPROP_TEXT, balanceText); //--- Update balance text
   
   string equityText = "Equity: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); //--- Format equity text
   ObjectSetString(0, panelPrefix + "AccountEquity", OBJPROP_TEXT, equityText); //--- Update equity text
   
   string ddText = "Drawdown (%): " + DoubleToString(dailyDrawdown, 2); //--- Format drawdown text
   ObjectSetString(0, panelPrefix + "CurrentDrawdown", OBJPROP_TEXT, ddText); //--- Update drawdown text
   ObjectSetInteger(0, panelPrefix + "CurrentDrawdown", OBJPROP_COLOR, dailyDrawdown > MaxDailyDrawdownPercent / 2 ? clrYellow : clrWhite); //--- Set drawdown color
}

ここでは、すべてのパネルオブジェクト名に共通の接頭辞を付けて整理しやすくするために、文字列「panelPrefix」を「LondonPanel_」として定義します。これにより、コントロールパネル内のオブジェクトを一元的に識別できるようになります。次に、情報パネルのUIを構築するCreatePanel関数を作成します。まず、createRecLabel関数を呼び出して、「panelPrefix + "Background"」という名前のパネル背景を生成します。位置は(10,10)、サイズは270×200、背景色はclrMidnightBlue、枠線の幅は1、枠の色はシルバーとします。続いて、createLabel関数を使用して、タイトル「London Breakout Control Center」を位置(20,15)、文字色ゴールド、フォントサイズ12で追加します。さらに、レンジ、最高値、最安値、買いレベル、売りレベル、残高、純資産、ドローダウンの各ラベルを、それぞれの位置に白色・フォントサイズ10で配置します。

非表示ボタンを作成するために、createRecLabelを呼び出し、「panelPrefix + "Hide"」という名前で、位置(250,10)、サイズ30×22、背景色clrCrimsonの矩形を作成します。次に、createLabel関数を使って、「panelPrefix + "HideText"」という名前で、CharToString(251)(Wingdingsフォント)を位置(258,12)、文字色clrWhite、フォントサイズ13で追加します。この非表示ボタンをインタラクティブにするために、ObjectSetInteger関数を使用してOBJPROP_SELECTABLEおよびOBJPROP_STATEをtrueに設定します。使用するWingdingsコードはデザインの好みに応じて選択可能です。以下に使用できるWingdingsコード一覧を示します。

WINGDINGSコード

次に、現在のデータでパネルを更新するUpdatePanel関数を実装します。まず、LondonRangePointsの値をDoubleToStringでフォーマットし、値が0の場合は「Calculating...」と表示するようにします。その後、ObjectSetString関数で「panelPrefix + "RangePoints"」のテキストを更新します。同様に、最高値、最安値、買いレベル(PreLondonHigh + OrderOffsetPoints * _Point )、売りレベル(PreLondonLow - OrderOffsetPoints * _Point)、残高(AccountInfoDouble(ACCOUNT_BALANCE))、純資産(AccountInfoDouble(ACCOUNT_BALANCE))、およびドローダウン(dailyDrawdown)をそれぞれフォーマットして更新します。

ドローダウンの色は、ObjectSetIntegerを使用して、dailyDrawdownがMaxDailyDrawdownPercent / 2を超える場合は黄色、それ以外の場合は白に設定します。最後に、これらの関数を初期化関数内で呼び出すことで、パネルを動作可能な状態にします。

//+------------------------------------------------------------------+
//| Initialize EA                                                    |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber); //--- Set magic number
   ArrayFree(positionList);           //--- Free position list
   CreatePanel();                     //--- Create panel
   panelVisible = true;               //--- Set panel visible
   return(INIT_SUCCEEDED);            //--- Return success
}

//+------------------------------------------------------------------+
//| Deinitialize EA                                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
   ArrayFree(positionList);           //--- Free position list
}

OnInitイベントハンドラ内では、まず取引オブジェクトを使用してマジックナンバーを設定します。その後、ArrayFree関数を使ってポジションリストを解放し、CreatePanel関数を呼び出してパネルを生成します。パネルの作成が完了したら、パネルの表示フラグをtrueに設定します。次に、OnDeinitイベントハンドラ内では、ObjectsDeleteAll関数を使用して、指定した接頭辞を持つすべてのオブジェクトを削除します。さらに、もう使用しないポジションリスト配列をArrayFreeで解放します。コンパイルすると、次の結果が得られます。

初期パネル

パネルを作成できたので、次はキャンセルボタンに動きを加えて、クリックされた際にパネルを削除できるようにします。これを実現するために、OnChartEventイベントハンドラ内で処理をおこないます。

//+------------------------------------------------------------------+
//| Handle chart events (e.g., panel close)                          |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   if (id == CHARTEVENT_OBJECT_CLICK && sparam == panelPrefix + "Hide") { //--- Check hide click
      panelVisible = false;           //--- Set panel hidden
      ObjectsDeleteAll(0, "LondonPanel_"); //--- Delete panel objects
      ChartRedraw(0);                 //--- Redraw chart
   }
}

OnChartEventイベントハンドラ内では、まずイベントIDがオブジェクトクリックであるかどうかを確認します。クリックされたオブジェクトが非表示ボタンまたはキャンセルボタンであった場合、パネルの表示フラグをfalseに設定します。その後、パネルを構成するすべてのオブジェクトを削除し、変更を反映させるためにチャートを再描画します。コンパイルすると、次の結果が得られます。

パネルを閉じる

可視化からわかるように、パネルは正常に動作し、クリック操作によって閉じられるようになりました。次のステップとして、パネルをさらに拡張し、すべての要素が初期化されるように更新していきます。そのためには、日次レンジを設定する関数と、ドローダウンを計算する機能を実装する必要があります。

//+------------------------------------------------------------------+
//| Check if it's a new trading day                                  |
//+------------------------------------------------------------------+
bool IsNewDay(datetime currentBarTime) {
   MqlDateTime barTime;               //--- Bar time structure
   TimeToStruct(currentBarTime, barTime); //--- Convert time
   datetime currentDay = StringToTime(StringFormat("%04d.%02d.%02d", barTime.year, barTime.mon, barTime.day)); //--- Get current day
   if (currentDay != lastCheckedDay) { //--- Check new day
      lastCheckedDay = currentDay;    //--- Update last day
      sessionChecksDone = false;      //--- Reset checks
      noTradeToday = false;           //--- Reset no trade
      buyOrderTicket = 0;             //--- Reset buy ticket
      sellOrderTicket = 0;            //--- Reset sell ticket
      LondonRangePoints = 0.0;        //--- Reset range
      return true;                    //--- Return new day
   }
   return false;                      //--- Return not new day
}

//+------------------------------------------------------------------+
//| Update daily drawdown                                            |
//+------------------------------------------------------------------+
void UpdateDailyDrawdown() {
   static double maxEquity = 0.0;     //--- Max equity tracker
   double equity = AccountInfoDouble(ACCOUNT_EQUITY); //--- Get equity
   if (equity > maxEquity) maxEquity = equity; //--- Update max equity
   dailyDrawdown = (maxEquity - equity) / maxEquity * 100; //--- Calculate drawdown
   if (dailyDrawdown >= MaxDailyDrawdownPercent) noTradeToday = true; //--- Set no trade if exceeded
}

まず、IsNewDay関数を実装し、新しい取引日かどうかを確認します。この関数では、MqlDateTime構造体「barTime」を作成し、TimeToStruct関数を使用してcurrentBarTimeをその構造体に変換します。次に、barTime.year、barTime.mon、barTime.dayをStringFormatで結合し、StringToTime関数を使ってcurrentDayを算出します。currentDayがlastCheckedDayと異なった場合、lastCheckedDayを更新し、sessionChecksDoneとnoTradeTodayをfalseにリセットします。さらに、buyOrderTicketとsellOrderTicketを0に戻し、LondonRangePointsを0.0に設定して初期化します。その後、trueを返します。もし同じ日であれば、変更はおこなわずfalseを返します。この関数により、セッション分析や取引フラグが毎日自動的にリセットされます。

次に、日次リスクを監視するためのUpdateDailyDrawdown関数を実装します。この関数では、ピーク時の純資産を追跡するために、静的変数maxEquityを0.0で初期化します。現在の純資産をAccountInfoDouble(ACCOUNT_EQUITY)で取得し、もし現在の純資産がこれまでの最大値を上回る場合はmaxEquityを更新します。その後、maxEquityからの減少率を計算してdailyDrawdown(日次ドローダウン)を算出します。このドローダウンがMaxDailyDrawdownPercentに以上の場合、noTradeTodayをtrueに設定して、その日の取引を停止します。これにより、過剰な損失を防ぎ、リスク管理を強化することができます。最後に、これらの関数をOnTickイベントハンドラ内で呼び出し、リアルタイムでデータを更新しながらパネルに反映させます。

//+------------------------------------------------------------------+
//| Main tick handler                                                |
//+------------------------------------------------------------------+
void OnTick() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   IsNewDay(currentBarTime);          //--- Check new day
   
   UpdatePanel();                     //--- Update panel
   UpdateDailyDrawdown();             //--- Update drawdown
}

これにより、毎日の取引状況やリスク状態が自動的にモニタリングされ、最新情報が常にパネルに表示されるようになります。プログラムを実行すると、次の結果が得られます。

初期化されたパネル

可視化から、パネルが最新のデータで更新され、現在のステータスを表示していることが確認できます。ここからは、より複雑なロジックであるセッションレンジの定義に進みます。まず、取引条件を確認し、条件が満たされた場合に注文を出す処理を実装します。その後、ポジション管理のロジックを追加していく流れになります。そこで、まずレンジを定義し、そのレンジを視覚化し、そして注文を出すための関数が必要になります。そのためのロジックを以下に実装します。

//+------------------------------------------------------------------+
//| Fixed lot size                                                   |
//+------------------------------------------------------------------+
double CalculateLotSize(double entryPrice, double stopLossPrice) {
   return NormalizeDouble(inpTradeLotsize, 2); //--- Normalize lot size
}

//+------------------------------------------------------------------+
//| Calculate session range (high-low) in points                     |
//+------------------------------------------------------------------+
double GetRange(datetime startTime, datetime endTime, double &highVal, double &lowVal, datetime &highTime, datetime &lowTime) {
   int startBar = iBarShift(_Symbol, _Period, startTime, true); //--- Get start bar
   int endBar = iBarShift(_Symbol, _Period, endTime, true); //--- Get end bar
   if (startBar == -1 || endBar == -1 || startBar < endBar) return -1; //--- Invalid bars

   int highestBar = iHighest(_Symbol, _Period, MODE_HIGH, startBar - endBar + 1, endBar); //--- Get highest bar
   int lowestBar = iLowest(_Symbol, _Period, MODE_LOW, startBar - endBar + 1, endBar); //--- Get lowest bar
   highVal = iHigh(_Symbol, _Period, highestBar); //--- Set high value
   lowVal = iLow(_Symbol, _Period, lowestBar); //--- Set low value
   highTime = iTime(_Symbol, _Period, highestBar); //--- Set high time
   lowTime = iTime(_Symbol, _Period, lowestBar); //--- Set low time
   return (highVal - lowVal) / _Point; //--- Return range in points
}

//+------------------------------------------------------------------+
//| Place pending buy/sell stop orders                               |
//+------------------------------------------------------------------+
void PlacePendingOrders(double preLondonHigh, double preLondonLow, datetime sessionID) {
   double buyPrice = preLondonHigh + OrderOffsetPoints * _Point; //--- Calculate buy price
   double sellPrice = preLondonLow - OrderOffsetPoints * _Point; //--- Calculate sell price
   double slPoints = StopLossPoints; //--- Set SL points
   double buySL = buyPrice - slPoints * _Point; //--- Calculate buy SL
   double sellSL = sellPrice + slPoints * _Point; //--- Calculate sell SL
   double tpPoints = slPoints * RRRatio; //--- Calculate TP points
   double buyTP = buyPrice + tpPoints * _Point; //--- Calculate buy TP
   double sellTP = sellPrice - tpPoints * _Point; //--- Calculate sell TP
   double lotSizeBuy = CalculateLotSize(buyPrice, buySL); //--- Calculate buy lot
   double lotSizeSell = CalculateLotSize(sellPrice, sellSL); //--- Calculate sell lot

   if (TradeType == TRADE_ALL || TradeType == TRADE_BUY_ONLY) { //--- Check buy trade
      obj_Trade.BuyStop(lotSizeBuy, buyPrice, _Symbol, buySL, buyTP, 0, 0, "Buy Stop - London"); //--- Place buy stop
      buyOrderTicket = obj_Trade.ResultOrder(); //--- Get buy ticket
   }

   if (TradeType == TRADE_ALL || TradeType == TRADE_SELL_ONLY) { //--- Check sell trade
      obj_Trade.SellStop(lotSizeSell, sellPrice, _Symbol, sellSL, sellTP, 0, 0, "Sell Stop - London"); //--- Place sell stop
      sellOrderTicket = obj_Trade.ResultOrder(); //--- Get sell ticket
   }
}

//+------------------------------------------------------------------+
//| Draw session ranges on the chart                                 |
//+------------------------------------------------------------------+
void DrawSessionRanges(datetime preLondonStart, datetime londonEnd) {
   string sessionID = "Sess_" + IntegerToString(lastCheckedDay); //--- Session ID

   string preRectName = "PreRect_" + sessionID; //--- Rectangle name
   ObjectCreate(0, preRectName, OBJ_RECTANGLE, 0, PreLondonHighTime, PreLondonHigh, PreLondonLowTime, PreLondonLow); //--- Create rectangle
   ObjectSetInteger(0, preRectName, OBJPROP_COLOR, clrTeal); //--- Set color
   ObjectSetInteger(0, preRectName, OBJPROP_FILL, true); //--- Enable fill
   ObjectSetInteger(0, preRectName, OBJPROP_BACK, true); //--- Set background

   string preTopLineName = "PreTopLine_" + sessionID; //--- Top line name
   ObjectCreate(0, preTopLineName, OBJ_TREND, 0, preLondonStart, PreLondonHigh, londonEnd, PreLondonHigh); //--- Create top line
   ObjectSetInteger(0, preTopLineName, OBJPROP_COLOR, clrBlack); //--- Set color
   ObjectSetInteger(0, preTopLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preTopLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preTopLineName, OBJPROP_BACK, true); //--- Set background

   string preBotLineName = "PreBottomLine_" + sessionID; //--- Bottom line name
   ObjectCreate(0, preBotLineName, OBJ_TREND, 0, preLondonStart, PreLondonLow, londonEnd, PreLondonLow); //--- Create bottom line
   ObjectSetInteger(0, preBotLineName, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(0, preBotLineName, OBJPROP_WIDTH, 1); //--- Set width
   ObjectSetInteger(0, preBotLineName, OBJPROP_RAY_RIGHT, false); //--- Disable ray
   ObjectSetInteger(0, preBotLineName, OBJPROP_BACK, true); //--- Set background
}

レンジの計算、注文、チャート描画をおこなえるようにするため、まずは固定ロットサイズを計算するCalculateLotSize関数から始めます。この関数はentryPriceとstopLossPriceをパラメータとして受け取りますが、固定ロットを使用する場合はこれらの値は使いません。戻り値として、inpTradeLotsizeをNormalizeDouble関数で小数点第2位に丸めた値を返し、すべての取引で一貫したロットサイズを維持します。口座タイプによっては小数点以下の桁数を変更しても構いません。

次に、プレロンドンセッションのレンジを計算するGetRange関数を作成します。ここでは、iBarShift関数を使ってstartTimeとendTimeからstartBarとendBarを取得し、値が無効または「startBar < endBar」の場合は-1を返します。続いて、指定したバー範囲内での最高値バーをiHighest関数(MODE_HIGH)で、最安値バーをiLowest関数(MODE_LOW)で求めます。iHigh関数を使ってhighestBarの高値をhighValとし、iLow関数でlowestBarの安値をlowValとして取得します。また、highestBarの時間をiTimeでhighTime、lowestBarの時間をlowTimeとして記録します。最後に、レンジ幅を「(highVal-lowVal) / _Point」で計算し、その値を返します。

続いて、買いおよび売りのストップ注文を設定するPlacePendingOrders関数を定義します。この関数では、まずbuyPriceを「preLondonHigh + OrderOffsetPoints * _Point」として計算し、sellPriceを「preLondonLow - OrderOffsetPoints * _Point」として計算します。ストップロス幅を表すslPointsにはStopLossPointsを設定し、buySLは「buyPrice - slPoints * _Point」、sellSLは「sellPrice + slPoints * _Point」とします。さらに、テイクプロフィット幅tpPointsを「slPoints * RRRatio」として求め、buyTPは「buyPrice + tpPoints * _Point」、sellTPは「sellPrice - tpPoints * _Point」として設定します。ロットサイズについては、CalculateLotSize関数を使ってlotSizeBuyとlotSizeSellを算出します。

TradeTypeがTRADE_ALLまたはTRADE_BUY_ONLYの場合、obj_Trade.BuyStopを使って買いストップ注文を出します。パラメータとしてlotSizeBuy、buyPrice、buySL、buyTP、そして注文名「Buy Stop - London」を指定し、発注結果の注文チケットをResultOrderからbuyOrderTicketに保存します。同様に、TradeTypeがTRADE_ALLまたはTRADE_SELL_ONLYの場合は売りストップ注文を出します。

最後に、セッションレンジをチャート上に可視化するDrawSessionRanges関数を実装します。まず、sessionIDをIntegerToString(lastCheckedDay)を使用して「Sess_」に日付を付加した文字列として作成します。プレロンドンのレンジ矩形preRectNameは「PreRect_ + sessionID」とし、ObjectCreateを使ってOBJ_RECTANGLEとして生成します。座標にはPreLondonHighTime、PreLondonHighからPreLondonLowTime、PreLondonLowを指定し、OBJPROP_COLORをclrTeal、OBJPROP_FILLをtrue、OBJPROP_BACKをtrueに設定します。

次に、上限ラインpreTopLineNameを「PreTopLine_ + sessionID」として作成し、ObjectCreateでOBJ_TRENDを使用してpreLondonStart、PreLondonHighからlondonEnd、PreLondonHighまでを描画します。OBJPROP_COLORはclrBlack、OBJPROP_WIDTHは1、OBJPROP_RAY_RIGHTはfalse、OBJPROP_BACKはtrueに設定します。同様に、下限ラインpreBotLineNameを「PreBottomLine_ + sessionID」として作成し、preLondonStart、PreLondonLowからlondonEnd、PreLondonLowまでを範囲として赤色で描画します。これで、これらの関数を利用して取引条件をチェックする関数を定義できるようになります。

//+------------------------------------------------------------------+
//| Check trading conditions and place orders                        |
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- Time structure
   TimeToStruct(currentTime, timeStruct); //--- Convert time
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- Get today

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- Pre-London start
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- London start
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- London end
   analysisTime = londonStart;        //--- Set analysis time

   if (currentTime < analysisTime) return; //--- Exit if before analysis

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- Get range
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- Check range limits
      noTradeToday = true;            //--- Set no trade
      sessionChecksDone = true;       //--- Set checks done
      DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
      return;                         //--- Exit
   }

   LondonRangePoints = preLondonRange; //--- Set range points
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- Place orders
   noTradeToday = true;               //--- Set no trade
   sessionChecksDone = true;          //--- Set checks done
   DrawSessionRanges(preLondonStart, londonEnd); //--- Draw ranges
}

CheckTradingConditions関数を実装し、ロンドンセッションブレイクアウトシステムにおけるセッション条件の判定と注文をおこないます。まず、MqlDateTime構造体timeStructを作成し、TimeToStruct関数を使って現在時刻をtimeStructに変換します。次に、timeStruct.year、timeStruct.mon、timeStruct.dayをStringFormatで結合し、StringToTime関数を用いてtodayを算出します。その後、preLondonStartをtodayにPreLondonStartHourとPreLondonStartMinuteを秒換算して加算した値、londonStartをtodayにLondonStartHourとLondonStartMinuteを加算した値、londonEndをtodayにLondonEndHourとLondonEndMinuteを加算した値として設定します。analysisTimeをlondonStartに設定し、現在時刻がこれより前であれば処理を終了します。

次に、GetRange関数を呼び出し、preLondonStart、currentTime、および参照渡しのPreLondonHigh、PreLondonLow、PreLondonHighTime、PreLondonLowTimeを引数として渡し、preLondonRangeを取得します。preLondonRangeがMinRangePointsより小さい、またはMaxRangePointsより大きい場合、noTradeTodayとsessionChecksDoneをtrueに設定し、DrawSessionRanges関数をpreLondonStartとlondonEndを引数として呼び出した後、処理を終了します。一方で、preLondonRangeが有効範囲内であれば、LondonRangePointsにpreLondonRangeを代入し、PlacePendingOrders関数をPreLondonHigh、PreLondonLow、およびtodayを引数として呼び出します。その後、noTradeTodayとsessionChecksDoneをtrueに設定し、DrawSessionRangesを再度呼び出します。このようにして、取引は有効なセッションレンジ内でのみおこなわれるようになります。シグナルのOnTickイベントハンドラで関数を呼び出すことができます。

if (!noTradeToday && !sessionChecksDone) { //--- Check trading conditions
   CheckTradingConditions(TimeCurrent()); //--- Check conditions
}

本日まだ取引がおこなわれておらず、かつセッションチェックも完了していない場合、現在時刻に対して取引条件を確認するために関数を呼び出します。プログラムを実行すると、以下の結果が得られます。

範囲設定

この結果から、レンジが設定され、ペンディング注文が正しく出されたことが確認できます。レンジ幅は100ポイントであり、これは取引条件を満たしています。次のステップでは、注文済みの取引を管理していく処理に進みますが、その前に、どちらか一方の注文が約定(アクティブ)になった際に、残っているもう一方のペンディング注文を削除し、アクティブになったポジションをポジションリストに追加して管理できるようにします。

//+------------------------------------------------------------------+
//| Delete opposite pending order when one is filled                 |
//+------------------------------------------------------------------+
void CheckAndDeleteOppositeOrder() {
   if (!DeleteOppositeOrder || TradeType != TRADE_ALL) return; //--- Exit if not applicable

   bool buyOrderExists = false;       //--- Buy exists flag
   bool sellOrderExists = false;      //--- Sell exists flag

   for (int i = OrdersTotal() - 1; i >= 0; i--) { //--- Iterate through orders
      ulong orderTicket = OrderGetTicket(i); //--- Get ticket
      if (OrderSelect(orderTicket)) { //--- Select order
         if (OrderGetString(ORDER_SYMBOL) == _Symbol && OrderGetInteger(ORDER_MAGIC) == MagicNumber) { //--- Check symbol and magic
            if (orderTicket == buyOrderTicket) buyOrderExists = true; //--- Set buy exists
            if (orderTicket == sellOrderTicket) sellOrderExists = true; //--- Set sell exists
         }
      }
   }

   if (!buyOrderExists && sellOrderExists && sellOrderTicket != 0) { //--- Check delete sell
      obj_Trade.OrderDelete(sellOrderTicket); //--- Delete sell order
   } else if (!sellOrderExists && buyOrderExists && buyOrderTicket != 0) { //--- Check delete buy
      obj_Trade.OrderDelete(buyOrderTicket); //--- Delete buy order
   }
}

//+------------------------------------------------------------------+
//| Add position to tracking list when opened                        |
//+------------------------------------------------------------------+
void AddPositionToList(ulong ticket, double openPrice, double londonRange, datetime sessionID) {
   if (londonRange <= 0) return;      //--- Exit if invalid range
   int index = ArraySize(positionList); //--- Get current size
   ArrayResize(positionList, index + 1); //--- Resize array
   positionList[index].ticket = ticket; //--- Set ticket
   positionList[index].openPrice = openPrice; //--- Set open price
   positionList[index].londonRange = londonRange; //--- Set range
   positionList[index].sessionID = sessionID; //--- Set session ID
   positionList[index].trailingActive = false; //--- Set trailing inactive
}

まず、どちらか一方の注文が約定した際に反対方向のペンディング注文を削除するためのCheckAndDeleteOppositeOrder関数を実装します。この関数では、DeleteOppositeOrderがfalseの場合、またはTradeTypeがTRADE_ALL以外の場合は、何もせずに処理を終了します。次に、buyOrderExistsとsellOrderExistsをfalseで初期化します。OrdersTotalを使って注文数を取得し、ループで後方から順にOrderGetTicketを呼び出して各注文チケットを取得します。OrderGetStringとOrderGetIntegerを使って注文が_SymbolおよびMagicNumberと一致し、そのチケットがbuyOrderTicketまたはsellOrderTicketと一致した場合、それぞれbuyOrderExistsまたはsellOrderExistsをtrueに設定します。

買い注文がなくなっていて売り注文が存在する場合は、obj_Trade.OrderDeleteを使用してsellOrderTicketを削除します。同様に、売り注文がなくなっていて買い注文が存在する場合は、buyOrderTicketを削除します。この関数は、どちらか一方の注文がトリガーされた際に反対方向のペンディング注文を削除し、約定後は一方向のみの取引となるようにします。

次に、オープンしたポジションを追跡するためのAddPositionToList関数を作成します。この関数では、londonRangeが0以下の場合、まだレンジが設定されていないため処理を終了します。次に、positionListの現在のサイズをArraySizeで取得し、ArrayResizeを使って配列を1つ拡張します。そして、positionList[index].ticket、openPrice、londonRange、sessionIDを設定し、trailingActiveをfalseに初期化します。これにより、トレーリングストップやセッションごとのデータを管理するためのポジションリストを維持することができます。これで、このロジックをOnTickイベントハンドラ内に実装できます。

CheckAndDeleteOppositeOrder();     //--- Delete opposite order

// Add untracked positions
for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions
   ulong ticket = PositionGetTicket(i); //--- Get ticket
   if (PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check position
      bool tracked = false;        //--- Tracked flag
      for (int j = 0; j < ArraySize(positionList); j++) { //--- Check list
         if (positionList[j].ticket == ticket) tracked = true; //--- Set tracked
      }
      if (!tracked) {              //--- If not tracked
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
         AddPositionToList(ticket, openPrice, LondonRangePoints, lastCheckedDay); //--- Add to list
      }
   }
}

ここでは、CheckAndDeleteOppositeOrder関数を呼び出してペンディング注文を管理し、一方の方向の注文が約定した場合に、DeleteOppositeOrderの入力設定に従って反対方向の注文を削除することで、相反する取引が発生しないようにします。

次に、まだ追跡されていないポジションをpositionListに追加し、すべての関連するオープンポジションがトレーリングストップの監視対象となるようにします。PositionsTotalを使ってすべてのポジションをループし、PositionGetTicketで各ticketを取得します。PositionSelectByTicketが成功し、そのポジションがPositionGetStringとPositionGetIntegerを使って_SymbolおよびMagicNumberと一致する場合、まずtrackedフラグをfalseに設定します。次に、ArraySizeを使ってpositionListの要素数を取得し、内部ループでpositionList[j].ticketと照合して、そのticketがすでに登録されているかを確認します。trackedがfalseのままであれば、PositionGetDoubleを使ってPOSITION_PRICE_OPENからopenPriceを取得し、AddPositionToListをticket、openPrice、LondonRangePoints、lastCheckedDayの各引数とともに呼び出します。これにより、すべての該当ポジションが重複することなくpositionListに追加され、管理対象として適切に追跡されるようになります。以下はその結果です。

ペンディング注文の削除

ここまでは完璧です。次にこれらのポジションを管理する関数を定義します。

//+------------------------------------------------------------------+
//| Remove position from tracking list when closed                   |
//+------------------------------------------------------------------+
void RemovePositionFromList(ulong ticket) {
   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through list
      if (positionList[i].ticket == ticket) { //--- Match ticket
         for (int j = i; j < ArraySize(positionList) - 1; j++) { //--- Shift elements
            positionList[j] = positionList[j + 1]; //--- Copy next
         }
         ArrayResize(positionList, ArraySize(positionList) - 1); //--- Resize array
         break;                   //--- Exit loop
      }
   }
}

//+------------------------------------------------------------------+
//| Manage trailing stops                                            |
//+------------------------------------------------------------------+
void ManagePositions() {
   if (PositionsTotal() == 0 || !UseTrailing) return; //--- Exit if no positions or no trailing
   isTrailing = false;                //--- Reset trailing flag

   double currentBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get bid
   double currentAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get ask
   double point = _Point;             //--- Get point value

   for (int i = 0; i < ArraySize(positionList); i++) { //--- Iterate through positions
      ulong ticket = positionList[i].ticket; //--- Get ticket
      if (!PositionSelectByTicket(ticket)) { //--- Select position
         RemovePositionFromList(ticket); //--- Remove if not selected
         continue;                    //--- Skip
      }

      if (PositionGetString(POSITION_SYMBOL) != _Symbol || PositionGetInteger(POSITION_MAGIC) != MagicNumber) continue; //--- Skip if not matching

      double openPrice = positionList[i].openPrice; //--- Get open price
      long positionType = PositionGetInteger(POSITION_TYPE); //--- Get type
      double currentPrice = (positionType == POSITION_TYPE_BUY) ? currentBid : currentAsk; //--- Get current price
      double profitPoints = (positionType == POSITION_TYPE_BUY) ? (currentPrice - openPrice) / point : (openPrice - currentPrice) / point; //--- Calculate profit points

      if (profitPoints >= MinProfitPoints + TrailingPoints) { //--- Check for trailing
         double newSL = 0.0;          //--- New SL variable
         if (positionType == POSITION_TYPE_BUY) { //--- Buy position
            newSL = currentPrice - TrailingPoints * point; //--- Calculate new SL
         } else {                     //--- Sell position
            newSL = currentPrice + TrailingPoints * point; //--- Calculate new SL
         }
         double currentSL = PositionGetDouble(POSITION_SL); //--- Get current SL
         if ((positionType == POSITION_TYPE_BUY && newSL > currentSL + point) || (positionType == POSITION_TYPE_SELL && newSL < currentSL - point)) { //--- Check move condition
            if (obj_Trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), PositionGetDouble(POSITION_TP))) { //--- Modify position
               positionList[i].trailingActive = true; //--- Set trailing active
               isTrailing = true;        //--- Set global trailing
            }
         }
      }
   }
}

ここでは、クローズされたポジションを追跡リストから削除し、トレーリングストップを管理するための関数を実装します。まず、ポジションがクローズされた際にpositionList配列を整理するためのRemovePositionFromList関数を作成します。この関数はticketをパラメータとして受け取り、ArraySizeでpositionListの要素数を取得してループします。positionList[i].ticketが引数のticketと一致した場合、内部ループでpositionList[j + 1]をpositionList[j]にコピーして要素を詰め、ArrayResizeを使って配列サイズを1つ減らします。その後、breakでループを抜けます。この関数により、クローズ済みポジションの不要なチェックを防ぎ、リストを常に最新の状態に保つことができます。特に、トレーリングや決済後の整理時に重要な処理です。

次に、オープン中の取引に対してトレーリングストップを処理するManagePositions関数を作成します。PositionsTotalが0、またはUseTrailingがfalseの場合は早期に終了します。続いてisTrailingをfalseにリセットし、SymbolInfoDoubleを使ってSYMBOL_BIDとSYMBOL_ASKからcurrentBidとcurrentAskを取得し、pointを_Pointとして取得します。次にArraySizeでpositionListの要素数を取得してループし、各ticketをPositionSelectByTicket関数で選択します。選択に失敗した場合はRemovePositionFromListを呼び出して次へ進みます。ポジションがPositionGetStringおよびPositionGetIntegerを使って_SymbolまたはMagicNumberと一致しない場合もスキップします。openPriceをpositionList[i]から取得し、PositionGetIntegerでpositionTypeを取得します。currentPriceをポジションタイプに応じて決定し、profitPointsをその差をpointで割って計算します。

profitPointsが「MinProfitPoints + TrailingPoints」以上である場合、買いポジションなら「currentPrice - TrailingPoints * point」、売りポジションなら「currentPrice + TrailingPoints * pointをnewSL」として計算します。PositionGetDoubleでcurrentSLを取得し、newSLがcurrentSLより少なくともpoint分有利であれば、PositionGetDoubleから取得した現在のTPとともにobj_Trade.PositionModifyでポジションを修正します。成功した場合、positionList[i].trailingActiveとisTrailingをtrueに設定します。これにより、利益を確保しながら、利益が伸びる取引を継続できるようになります。あとは、この関数をティックごとに呼び出してポジション管理をおこなうだけです。コンパイルすると、次の結果が得られます。

ポジション管理

画像から分かるように、取引条件の確認、注文の発注、そしてそれらの管理が取引戦略に従って正しくおこなわれており、これによって目的を達成できていることが確認できます。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

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

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

まとめとして、私たちはMQL5でロンドンセッションブレイクアウトシステムを開発しました。このシステムは、事前セッションのレンジを分析してペンディング注文を発注し、カスタマイズ可能なリスクリワード比率、トレーリングストップ、多重取引制限を組み込み、さらにコントロールパネルでレンジ、レベル、ドローダウンをリアルタイムに監視できるようにしています。PositionInfo構造体のようなモジュール化されたコンポーネントを通じて、ブレイクアウト取引における規律あるアプローチを提供しており、セッション時間やリスクパラメータを調整することで、柔軟に改良可能です。

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

ここで提示した概念と実装を活用することで、あなた自身の取引スタイルに合わせてこのブレイクアウトシステムを適応させ、アルゴリズム取引戦略を強化することができます。取引をお楽しみください。 

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

添付されたファイル |
最後のコメント | ディスカッションに移動 (14)
Allan Munene Mutiiria
Allan Munene Mutiiria | 10 8月 2025 において 22:20
Kyle Young Sangster ストラテジーテスターで動かして みた。毎日レンジを見つけ、チャートにボックスを描画します。しかし、毎日トレードはしない(するはずの)。ヶ月半の間、たった3回しかトレードしていない。



つ目の問題は、コントロール・パネルの高値・安値レベルと売買レベルが更新されないことです。


高値/安値レンジのレベルは明らかにチャート上に表示されているので、売買レベルもチャート上に表示され、高値/安値レンジのレベルから直接派生しているため、コントロールパネルで更新されるはずだと思います。

これを正しく動作させるために、何か提案はありますか?

よろしくお願いします。

あなたの2つ目の問題については、記事で説明されていますが、あなたの問題が貧弱なテストデータから生じていると仮定し、ヒントを与えると、レンジが計算中である場合、ロンドン・レンジ・セッションまたは入力で定義したセッションを設定するのに十分なデータがあるまで、常に "Calculating... "ステータスが表示されます。デフォルトの設定を使用していると仮定すると、ロンドン前の時間は3時間で、共有されたスクリーンショットの時間は2月13日で、22:00の2小節後は2*15分=30となり、したがって22:30はレンジ計算の時間外であるため、最初のセッションがまだ見つかっていない場合を除き、前の設定されたレンジがまだ有効であるため、パネル上のデータはまだ表示されているはずです。以下を参照してください:

const int PreLondonStartHour = 3; //--- ロンドン・スタート前の固定時間
const int PreLondonStartMinute = 0; //--- ロンドン・スタート前の分を固定

レンジを見つけるための以下のロジックを参照してください。

//+------------------------------------------------------------------+
//| 取引条件を確認し、注文を出す
//+------------------------------------------------------------------+
void CheckTradingConditions(datetime currentTime) {
   MqlDateTime timeStruct;            //--- 時間構造
   TimeToStruct(currentTime, timeStruct); //--- 時間を変換する
   datetime today = StringToTime(StringFormat("%04d.%02d.%02d", timeStruct.year, timeStruct.mon, timeStruct.day)); //--- 今日入手する

   datetime preLondonStart = today + PreLondonStartHour * 3600 + PreLondonStartMinute * 60; //--- ロンドン・プレ・スタート
   datetime londonStart = today + LondonStartHour * 3600 + LondonStartMinute * 60; //--- ロンドン・スタート
   datetime londonEnd = today + LondonEndHour * 3600 + LondonEndMinute * 60; //--- ロンドン終了
   analysisTime = londonStart;        //--- 分析時間の設定

   if (currentTime < analysisTime) return; //--- 分析前なら終了

   double preLondonRange = GetRange(preLondonStart, currentTime, PreLondonHigh, PreLondonLow, PreLondonHighTime, PreLondonLowTime); //--- 範囲を取得する
   if (preLondonRange < MinRangePoints || preLondonRange > MaxRangePoints) { //--- 範囲制限のチェック
      noTradeToday = true;            //--- ノートレードを設定する
      sessionChecksDone = true;       //--- セット・チェック完了
      DrawSessionRanges(preLondonStart, londonEnd); //--- 描画範囲
      return;                         //--- 終了
   }

   LondonRangePoints = preLondonRange; //--- レンジポイントの設定
   PlacePendingOrders(PreLondonHigh, PreLondonLow, today); //--- 注文を出す
   noTradeToday = true;               //--- ノートレードを設定する
   sessionChecksDone = true;          //--- セット・チェック完了
   DrawSessionRanges(preLondonStart, londonEnd); //--- 描画範囲
}

そしてどのように設定されるのか。

//+------------------------------------------------------------------+
//| 現在のデータでパネルを更新|
//+------------------------------------------------------------------+
void UpdatePanel() {
   string rangeText = "Range (points): " + (LondonRangePoints > 0 ? DoubleToString(LondonRangePoints, 0) : "Calculating..."); //--- 範囲テキストをフォーマットする
   ObjectSetString(0, panelPrefix + "RangePoints", OBJPROP_TEXT, rangeText); //--- 範囲テキストを更新する

   //---

}

下の画像をご覧ください。あなたのテストの年がわかりませんが、2025年とします。あなたのケースのように2020年であれば、そのための質の高いデータがありませんので、いずれにせよ、2025年を使用し、その結果、範囲計算は真夜中から始まるはずです。

23:55

画像から、23時55分のデータはそのままであることがわかります。しかし、午前0時になるとリセットされます。下をご覧ください。

深夜データ 00:00

他の範囲計算のために午前0時にデータをリセットしていることがわかります。実際、範囲計算が終了したとき、視覚化することによって、実際に何が行われたかを知ることができます。例えば、デフォルトの設定を使用した場合、0300 時から 0800 時までのラネッグ・バーが表示されます。以下をご覧ください:

レンジ時間

これでまたはっきりしたかと思います。ご自分の取引スタイルに合わせて、すべてを調整することができます。あなたが直面している問題を避けるためには、信頼できるテストデータを使用することをお勧めします。ありがとうございました。

Kyle Young Sangster
Kyle Young Sangster | 10 8月 2025 において 22:34
コードのどこで変数「MaxOpenTrades」を使用するつもりでしたか?定義されていますが、参照されていません。
Kyle Young Sangster
Kyle Young Sangster | 10 8月 2025 において 22:52
Allan Munene Mutiiria #:

2つ目の問題については、記事で説明されていますが、テストデータが不十分であったことが問題であると仮定し、ヒントを提供すると、レンジが計算中である場合、ロンドンレンジのセッションまたは入力で定義したセッションを設定するのに十分なデータがあるまで、常に "Calculating... "のステータスが表示されます。デフォルトの設定を使用していると仮定すると、ロンドン前の時間は3時間で、共有されたスクリーンショットの時間は2月13日で、22:00の2小節後は2*15分=30となり、したがって22:30はレンジ計算の時間外であるため、最初のセッションがまだ見つかっていない場合を除き、前の設定されたレンジがまだ有効であるため、パネル上のデータはまだ表示されているはずです。以下を参照してください:

レンジを見つけるために、以下のロジックを参照する必要があるかもしれません。

そして、どのように設定されるのか。

下の画像を見てください。あなたのテストの年がわからないのですが、2025年とします。あなたのケースのように2020年であれば、そのための質の高いデータがないので、いずれにせよ、2025年を使用し、したがって範囲計算は真夜中から始まるはずです。


画像から、23時55分のデータはそのままであることがわかります。しかし、午前0時になるとリセットされます。下記参照。

他の範囲計算のために午前0時にデータをリセットしていることがわかります。実際、範囲計算が終了したとき、視覚化することによって、実際に何が行われたかを知ることができます。例えば、デフォルトの設定を使用した場合、0300 時から 0800 時までのラネッグ・バーが表示されます。以下をご覧ください:

これでまたはっきりしたかと思います。ご自分の取引スタイルに合わせて、すべてを調整することができます。あなたが直面している問題を避けるためには、信頼できるテストデータを使用することをお勧めします。ありがとうございます。



はい、記事を読み、私が説明した問題にぶつかるまで、私自身のコピーのコーディングに従いました。私が見たのは、デフォルトの時間帯でもパネルが更新されないことでした。私のスクリーンショットは、チャート上にボックスが描かれ、データが収集されているにもかかわらず、パネルが更新されていないことを示すものでした。さらに、ログには無効な価格やレベルに関するエラーメッセージはありませんでした。

私のバージョンにログメッセージを追加しました。そこから、レンジが大きすぎたり小さすぎたりするとパネルが更新されないことがわかりました。

テストデータの品質を再チェックしてみます。また、どのペアでテストされたかをご指摘いただきありがとうございます。

ご助力に感謝いたします。

Allan Munene Mutiiria
Allan Munene Mutiiria | 10 8月 2025 において 23:49
Kyle Young Sangster #:



はい、記事を読み、私が説明したような問題にぶつかるまで、自分なりにコーディングしました。私が見たのは、デフォルトの時間帯でもパネルが更新されなかったことです。私のスクリーンショットは、チャート上にボックスが描かれ、データが収集されているにもかかわらず、パネルが更新されていないことを示すものでした。さらに、ログには無効な価格やレベルに関するエラーメッセージはありませんでした。

私のバージョンにログメッセージを追加しました。そこから、レンジが大きすぎたり小さすぎたりするとパネルが更新されないことがわかりました。

テストデータの品質を再チェックしてみます。また、どのペアでテストされたかをご指摘いただきありがとうございます。

ご助力に感謝いたします。

もちろんです。

Torsten Busch
Torsten Busch | 11 11月 2025 において 21:01

コードを共有していただきありがとうございます。

私自身もセッション依存のEAを書いたことがありますので、このコードが機能するのは、お使いのブローカーが常にGMT+1のタイムゾーンにあり、かつ英国夏時間を 使用している場合のみです。

それ以外のケースでは、開始時間は機能しません。なぜでしょうか?ロンドン・セッションは英国時間の午前8時に始まるからです。冬はGMT8:00、夏はGMT7:00です。

TimeCurrent() は、あなたのローカル時間を返すのではなく、常に取引サーバーからの時間を返します。

MQL5入門(第19回):ウォルフ波動の自動検出 MQL5入門(第19回):ウォルフ波動の自動検出
本記事では、強気(上昇)および弱気(下降)のウォルフ波動パターンをプログラムで識別し、MQL5を使用して取引する方法を紹介します。ウォルフ波動構造をプログラムで検出し、それに基づいて取引の実行方法を詳しく解説します。これには、主要なスイングポイントの検出、パターンルールの検証、シグナルに基づくエキスパートアドバイザー(EA)の準備が含まれます。
知っておくべきMQL5ウィザードのテクニック(第76回): Awesome Oscillatorのパターンとエンベロープチャネルを教師あり学習で利用する 知っておくべきMQL5ウィザードのテクニック(第76回): Awesome Oscillatorのパターンとエンベロープチャネルを教師あり学習で利用する
前回の記事では、オーサムオシレータ(AO: Awesome Oscillator)とエンベロープチャネル(Envelopes Channel)のインディケーターの組み合わせを紹介しましたが、今回はこのペアリングを教師あり学習でどのように強化できるかを見ていきます。Awesome OscillatorとEnvelope Channelは、トレンドの把握とサポート/レジスタンスの補完的な組み合わせです。私たちの教師あり学習アプローチでは、CNN(畳み込みニューラルネットワーク)を使用し、ドット積カーネルとクロスタイムアテンションを活用してカーネルとチャネルのサイズを決定します。通常どおり、この処理はMQL5ウィザードでエキスパートアドバイザー(EA)を組み立てる際に利用できるカスタムシグナルクラスファイル内でおこないます。
MQL5取引ツール(第6回):パルスアニメーションとコントロールを備えたダイナミックホログラフィックダッシュボード MQL5取引ツール(第6回):パルスアニメーションとコントロールを備えたダイナミックホログラフィックダッシュボード
本記事では、MQL5で動的なホログラフィックダッシュボードを作成し、RSIやボラティリティアラート、ソートオプションを使用して銘柄と時間足を監視します。さらに、パルスアニメーション、インタラクティブボタン、ホログラフィック効果を追加して、ツールを視覚的に魅力的で反応の良いものにします。
データサイエンスとML(第46回):PythonでN-BEATSを使った株式市場予測 データサイエンスとML(第46回):PythonでN-BEATSを使った株式市場予測
N-BEATSは、時系列予測のために設計された革新的なディープラーニングモデルです。このモデルは、ARIMAやPROPHET、VARなどの従来の時系列予測モデルを超えることを目指して公開されました。本記事では、このモデルについて説明し、株式市場の予測にどのように活用できるかを紹介します。