English
preview
MQL5での取引戦略の自動化(第36回):リテストとインパルスモデルによる需給取引

MQL5での取引戦略の自動化(第36回):リテストとインパルスモデルによる需給取引

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

はじめに

前回の記事(第35回)では、MetaQuotes Language 5 (MQL5)を使用して、ブレーカーブロック取引システムを開発しました。このシステムでは、レンジ相場(持ち合い)を特定し、スイングポイントを用いてブレーカーブロックを検証し、カスタマイズ可能なリスクパラメータと視覚的なフィードバックを備えたリテスト取引を実装しました。この第36回では、リテストとインパルスモデルを活用した需給(S&D: Supply and Demand)取引システムを開発します。このモデルは、レンジ相場によって需給ゾーンを検出し、インパルスムーブの発生によってそれらを検証し、トレンド確認を伴うリテスト時に取引を実行します。また、動的なチャート表示による視覚的な補助も提供します。本記事では以下のトピックを扱います。

  1. 需給取引戦略のフレームワークの理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

この記事を読み終える頃には、需給ゾーンのリテストを取引する実用的なMQL5戦略を手に入れ、自分好みにカスタマイズできるようになるでしょう。それでは、さっそく始めていきましょう。


需給取引戦略のフレームワークの理解

需給戦略は、通常、価格が保ち合いした後に顕著な買い(需要)や売り(供給)が発生した重要な価格領域を特定する戦略です。インパルス(急激な値動き)によってゾーンの有効性が確認された後、トレーダーはそのリテストを狙って取引をおこないます。一般的には、下降トレンド中には価格が需要ゾーンへ戻ってきた際に買いエントリーをおこない、上昇トレンド中には供給ゾーンで売りエントリーをおこない、反発を期待します。リスクとリワードの水準を明確に定義することで、高い確率が期待できる取引セットアップを活用することが可能になります。以下に、考えられるさまざまなセットアップの例を示します。

供給ゾーンのセットアップ

供給ゾーンのセットアップ

需要ゾーンのセットアップ

需要ゾーンのセットアップ

本システムの計画としては、指定した本数のローソク足を基にレンジ相場を検出し、価格変動幅に基づく倍率閾値を用いたインパルスムーブによってゾーンを検証し、さらに任意でトレンド確認をおこなったうえでエントリーを確定します。また、ゾーンの状態を追跡するロジックを実装し、カスタマイズ可能なストップロスおよびテイクプロフィット設定を用いてリテスト時に取引を実行します。加えて、動的なラベルとカラー表示によるゾーンの可視化をおこない、正確な需給取引を可能にするシステムを構築します。簡単に言うと、下図のように目標を視覚的に表現できます。

需要と供給の枠組み


MQL5での実装

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

//+------------------------------------------------------------------+
//|                                         Supply and Demand EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   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 strict

#include <Trade/Trade.mqh>                         //--- Include Trade library for position management
CTrade obj_Trade;                                  //--- Instantiate trade object for order operations

実装は、まず「#include <Trade/Trade.mqh>」を使用して取引ライブラリを読み込むことから始めます。このライブラリには、売買などの取引操作を管理するための組み込み関数が用意されています。次に、CTradeクラスを用いてobj_Tradeという取引オブジェクトを初期化します。これにより、エキスパートアドバイザー(EA)は売買注文をプログラムから自動的に実行できるようになります。この設定により、手動での操作を必要とせずに効率的な注文実行が可能となります。次に、いくつかのユーザー入力パラメータの分類を可能にする列挙体を宣言できます。

//+------------------------------------------------------------------+
//| Enum for trading tested zones                                    |
//+------------------------------------------------------------------+
enum TradeTestedZonesMode {                        // Define modes for trading tested zones
   NoRetrade,                                      // Trade zones only once
   LimitedRetrade,                                 // Trade zones up to a maximum number of times
   UnlimitedRetrade                                // Trade zones as long as they are valid
};

//+------------------------------------------------------------------+
//| Enum for broken zones validation                                 |
//+------------------------------------------------------------------+
enum BrokenZonesMode {                             // Define modes for broken zones validation
   AllowBroken,                                    // Zones can be marked as broken
   NoBroken                                        // Zones remain testable regardless of price break
};

//+------------------------------------------------------------------+
//| Enum for zone size restriction                                   |
//+------------------------------------------------------------------+
enum ZoneSizeMode {                                // Define modes for zone size restrictions
   NoRestriction,                                  // No restriction on zone size
   EnforceLimits                                   // Enforce minimum and maximum zone points
};

//+------------------------------------------------------------------+
//| Enum for trend confirmation                                      |
//+------------------------------------------------------------------+
enum TrendConfirmationMode {                       // Define modes for trend confirmation
   NoConfirmation,                                 // No trend confirmation required
   ConfirmTrend                                    // Confirm trend before trading on tap
};

ここでは、取引動作やゾーン検証を設定するための主要な列挙型を事前に宣言します。まず、TradeTestedZonesMode列挙型を複数のオプションを持つ形で作成します。この列挙型には、ゾーンを一度だけ取引するNoRetrade、設定した回数まで取引できるLimitedRetrade、そしてゾーンが有効である限り取引をおこなうUnlimitedRetradeが含まれます。これらのオプションは、ゾーンをどの程度の頻度で取引できるかを制御します。次に、BrokenZonesMode列挙型をオプション付きで定義します。この列挙型には、価格がゾーンを突破した場合にそのゾーンをブレイク済みとして扱うAllowBrokenと、ブレイク後もゾーンをテスト可能な状態として維持するNoBrokenが含まれます。これにより、ブレイクアウト後のゾーンの有効性を判断します。続いて、ZoneSizeMode列挙型をゾーンサイズを制御するオプションを持つ形で実装します。ここでは、ゾーンサイズに制限を設けないNoRestrictionと、ゾーンサイズを指定した範囲内に制限するEnforceLimitsを定義し、サイズ要件を満たさないゾーンを除外します。

最後に、TrendConfirmationMode列挙型をトレンド確認のためのオプションを含めて追加します。この列挙型には、トレンド確認をおこなわないNoConfirmationと、トレンドの検証を必須とするConfirmTrendが含まれます。これにより、システムはゾーンの取引や検証ルールを柔軟に設定できる構成になります。これらの列挙型を使用して、ユーザー入力を作成することができます。

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input double tradeLotSize = 0.01;                   // Trade size in lots
input bool   enableTrading = true;                  // Enable automated trading
input bool   enableTrailingStop = true;             // Enable trailing stop
input double trailingStopPoints = 30;               // Trailing stop points
input double minProfitToTrail = 50;                 // Minimum trailing points
input int    uniqueMagicNumber = 12345;             // Magic Number
input int    consolidationBars = 5;                 // Consolidation range bars
input double maxConsolidationSpread = 30;           // Maximum allowed spread in points for consolidation
input double stopLossDistance = 200;                // Stop loss in points
input double takeProfitDistance = 400;              // Take profit in points
input double minMoveAwayPoints = 50;                // Minimum points price must move away before zone is ready
input bool   deleteBrokenZonesFromChart = false;    // Delete broken zones from chart
input bool   deleteExpiredZonesFromChart = false;   // Delete expired zones from chart
input int    zoneExtensionBars = 150;               // Number of bars to extend zones to the right
input bool   enableImpulseValidation = true;        // Enable impulse move validation
input int    impulseCheckBars = 3;                  // Number of bars to check for impulsive move
input double impulseMultiplier = 1.0;               // Multiplier for impulsive threshold
input TradeTestedZonesMode tradeTestedMode = NoRetrade; // Mode for trading tested zones
input int    maxTradesPerZone = 2;                  // Maximum trades per zone for LimitedRetrade
input BrokenZonesMode brokenZoneMode = AllowBroken; // Mode for broken zones validation
input color  demandZoneColor = clrBlue;             // Color for untested demand zones
input color  supplyZoneColor = clrRed;              // Color for untested supply zones
input color  testedDemandZoneColor = clrBlueViolet; // Color for tested demand zones
input color  testedSupplyZoneColor = clrOrange;     // Color for tested supply zones
input color  brokenZoneColor = clrDarkGray;         // Color for broken zones
input color  labelTextColor = clrBlack;             // Color for text labels
input ZoneSizeMode zoneSizeRestriction = NoRestriction; // Zone size restriction mode
input double minZonePoints = 50;                    // Minimum zone size in points
input double maxZonePoints = 300;                   // Maximum zone size in points
input TrendConfirmationMode trendConfirmation = NoConfirmation; // Trend confirmation mode
input int    trendLookbackBars = 10;                // Number of bars for trend confirmation
input double minTrendPoints = 1;                    // Minimum points for trend confirmation

ここでは、システムの取引および可視化の動作を定義するための設定用入力パラメータを作成します。すべてを簡単で分かりやすくするために、自己説明的なコメントも追加しています。最後に、複数の供給および需要ゾーンを管理するため、ゾーンの情報を管理しやすく格納する構造体を宣言する必要があります。

//+------------------------------------------------------------------+
//| Structure for zone information                                   |
//+------------------------------------------------------------------+
struct SDZone {                                    //--- Define structure for supply/demand zones
   double   high;                                  //--- Store zone high price
   double   low;                                   //--- Store zone low price
   datetime startTime;                             //--- Store zone start time
   datetime endTime;                               //--- Store zone end time
   datetime breakoutTime;                          //--- Store breakout time
   bool     isDemand;                              //--- Indicate demand (true) or supply (false)
   bool     tested;                                //--- Track if zone was tested
   bool     broken;                                //--- Track if zone was broken
   bool     readyForTest;                          //--- Track if zone is ready for testing
   int      tradeCount;                            //--- Track number of trades on zone
   string   name;                                  //--- Store zone object name
};

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
SDZone zones[];                                    //--- Store active supply/demand zones
SDZone potentialZones[];                           //--- Store potential zones awaiting validation
int    maxZones = 50;                              //--- Set maximum number of zones to track

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(uniqueMagicNumber); //--- Set magic number for trade identification
   return(INIT_SUCCEEDED);                        //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ObjectsDeleteAll(0, "SDZone_");                //--- Remove all zone objects from chart
   ChartRedraw(0);                                //--- Redraw chart to clear objects
}

まず、ゾーンの詳細を格納するためにSDZone構造体を作成します。この構造体には、高値と安値、開始時間と終了時間、ブレイクアウト時間、需要/供給タイプを示すフラグ(isDemand)、テスト済み状態(tested)、ブレイク済み状態(broken)、テスト準備完了フラグ(readyForTest)、取引回数(tradeCount)、およびオブジェクト名(name)が含まれます。次に、グローバル変数を初期化します。アクティブな供給および需要ゾーンを格納するzones配列、検証待ちのゾーンを格納するpotentialZones配列、そして追跡するゾーンの上限を制限するmaxZonesを50に設定します。この値は、時間足や設定に応じて増減させることが可能であり、ここでは任意の標準値として選んでいます。

OnInitイベントハンドラでは、obj_Tradeに対してSetExpertMagicNumberをuniqueMagicNumberで呼び出し、取引にタグを付けます。初期化が成功した場合はINIT_SUCCEEDEDを返します。OnDeinit関数では、ObjectsDeleteAllを使用して「SDZone_」という接頭辞を持つすべてのチャートオブジェクトを削除します。すべてのオブジェクトをこの接頭辞で命名するためです。その後、ChartRedrawを呼び出してチャートを更新し、リソースをきれいに解放します。これで、ゾーンの検出と管理に役立ついくつかの補助関数を定義できるようになります。まずはゾーンを検出するロジックから始めますが、その前にゾーンのデバッグを支援する補助関数を用意します。

//+------------------------------------------------------------------+
//| Print zones for debugging                                        |
//+------------------------------------------------------------------+
void PrintZones(SDZone &arr[]) {
   Print("Current zones count: ", ArraySize(arr)); //--- Log total number of zones
   for (int i = 0; i < ArraySize(arr); i++) {     //--- Iterate through zones
      Print("Zone ", i, ": ", arr[i].name, " endTime: ", TimeToString(arr[i].endTime)); //--- Log zone details
   }
}

ゾーンの状態を監視するために、PrintZones関数を作成します。この関数はSDZone配列を引数に取り、ArraySizeを用いて配列内のゾーンの総数をPrintで出力します。その後、配列を順に処理し、各ゾーンのインデックス、名前、および終了時間をPrintとTimeToStringを使用して出力し、明確に追跡できるようにします。これで、ゾーンを検出するコアロジックを開発できる準備が整いました。

//+------------------------------------------------------------------+
//| Detect supply and demand zones                                   |
//+------------------------------------------------------------------+
void DetectZones() {
   int startIndex = consolidationBars + 1;                 //--- Set start index for consolidation check
   if (iBars(_Symbol, _Period) < startIndex + 1) return;   //--- Exit if insufficient bars
   bool isConsolidated = true;                             //--- Assume consolidation
   double highPrice = iHigh(_Symbol, _Period, startIndex); //--- Initialize high price
   double lowPrice = iLow(_Symbol, _Period, startIndex);   //--- Initialize low price
   for (int i = startIndex - 1; i >= 2; i--) {             //--- Iterate through consolidation bars
      highPrice = MathMax(highPrice, iHigh(_Symbol, _Period, i)); //--- Update highest high
      lowPrice = MathMin(lowPrice, iLow(_Symbol, _Period, i)); //--- Update lowest low
      if (highPrice - lowPrice > maxConsolidationSpread * _Point) { //--- Check spread limit
         isConsolidated = false;                           //--- Mark as not consolidated
         break;                                            //--- Exit loop
      }
   }
   if (isConsolidated) {                                   //--- Confirm consolidation
      double closePrice = iClose(_Symbol, _Period, 1);     //--- Get last closed bar price
      double breakoutLow = iLow(_Symbol, _Period, 1);      //--- Get breakout bar low
      double breakoutHigh = iHigh(_Symbol, _Period, 1);    //--- Get breakout bar high
      bool isDemandZone = closePrice > highPrice && breakoutLow >= lowPrice; //--- Check demand zone
      bool isSupplyZone = closePrice < lowPrice && breakoutHigh <= highPrice; //--- Check supply zone
      if (isDemandZone || isSupplyZone) {                   //--- Validate zone type
         double zoneSize = (highPrice - lowPrice) / _Point; //--- Calculate zone size
         if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) return; //--- Check size restrictions
         datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last bar time
         bool overlaps = false;                             //--- Initialize overlap flag
         for (int j = 0; j < ArraySize(zones); j++) {       //--- Check existing zones
            if (lastClosedBarTime < zones[j].endTime) {     //--- Check time overlap
               double maxLow = MathMax(lowPrice, zones[j].low); //--- Find max low
               double minHigh = MathMin(highPrice, zones[j].high); //--- Find min high
               if (maxLow <= minHigh) {                     //--- Check price overlap
                  overlaps = true;                          //--- Mark as overlapping
                  break;                                    //--- Exit loop
               }
            }
         }
         bool duplicate = false;                        //--- Initialize duplicate flag
         for (int j = 0; j < ArraySize(zones); j++) {   //--- Check for duplicates
            if (lastClosedBarTime < zones[j].endTime) { //--- Check time
               if (MathAbs(zones[j].high - highPrice) < _Point && MathAbs(zones[j].low - lowPrice) < _Point) { //--- Check price match
                  duplicate = true;                     //--- Mark as duplicate
                  break;                                //--- Exit loop
               }
            }
         }
         if (overlaps || duplicate) return;             //--- Skip overlapping or duplicate zones
         if (enableImpulseValidation) {                 //--- Check impulse validation
            bool pot_overlaps = false;                  //--- Initialize potential overlap flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential zones
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time overlap
                  double maxLow = MathMax(lowPrice, potentialZones[j].low); //--- Find max low
                  double minHigh = MathMin(highPrice, potentialZones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {              //--- Check price overlap
                     pot_overlaps = true;               //--- Mark as overlapping
                     break;                             //--- Exit loop
                  }
               }
            }
            bool pot_duplicate = false;           //--- Initialize potential duplicate flag
            for (int j = 0; j < ArraySize(potentialZones); j++) { //--- Check potential duplicates
               if (lastClosedBarTime < potentialZones[j].endTime) { //--- Check time
                  if (MathAbs(potentialZones[j].high - highPrice) < _Point && MathAbs(potentialZones[j].low - lowPrice) < _Point) { //--- Check price match
                     pot_duplicate = true;      //--- Mark as duplicate
                     break;                     //--- Exit loop
                  }
               }
            }
            if (pot_overlaps || pot_duplicate) return; //--- Skip overlapping or duplicate potential zones
            int potCount = ArraySize(potentialZones); //--- Get potential zones count
            ArrayResize(potentialZones, potCount + 1); //--- Resize potential zones array
            potentialZones[potCount].high = highPrice; //--- Set zone high
            potentialZones[potCount].low = lowPrice; //--- Set zone low
            potentialZones[potCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            potentialZones[potCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            potentialZones[potCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            potentialZones[potCount].isDemand = isDemandZone; //--- Set zone type
            potentialZones[potCount].tested = false; //--- Set untested
            potentialZones[potCount].broken = false; //--- Set not broken
            potentialZones[potCount].readyForTest = false; //--- Set not ready
            potentialZones[potCount].tradeCount = 0; //--- Initialize trade count
            potentialZones[potCount].name = "PotentialZone_" + TimeToString(potentialZones[potCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Potential zone created: ", (isDemandZone ? "Demand" : "Supply"), " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(potentialZones[potCount].endTime)); //--- Log potential zone
         } else {                                 //--- No impulse validation
            int zoneCount = ArraySize(zones);     //--- Get zones count
            if (zoneCount >= maxZones) {          //--- Check max zones limit
               ArrayRemove(zones, 0, 1);          //--- Remove oldest zone
               zoneCount--;                       //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);    //--- Resize zones array
            zones[zoneCount].high = highPrice;    //--- Set zone high
            zones[zoneCount].low = lowPrice;      //--- Set zone low
            zones[zoneCount].startTime = iTime(_Symbol, _Period, startIndex); //--- Set start time
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Set end time
            zones[zoneCount].breakoutTime = iTime(_Symbol, _Period, 1); //--- Set breakout time
            zones[zoneCount].isDemand = isDemandZone; //--- Set zone type
            zones[zoneCount].tested = false;      //--- Set untested
            zones[zoneCount].broken = false;      //--- Set not broken
            zones[zoneCount].readyForTest = false; //--- Set not ready
            zones[zoneCount].tradeCount = 0;      //--- Initialize trade count
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            Print("Zone created: ", (isDemandZone ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", lowPrice, " - ", highPrice, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log zone creation
            PrintZones(zones);                    //--- Print zones for debugging
         }
      }
   }
}

ここでは、システムのゾーン検出ロジックを実装します。DetectZones関数では、まずstartIndexをconsolidationBars+1に設定し、iBars関数で十分なバーが存在しない場合は処理を終了します。レンジ相場が成立していると仮定して(isConsolidatedをtrueに設定)、startIndexのiHighiLow を用いてhighPriceとlowPriceを初期化します。その後、バーを逆方向に繰り返し処理し、MathMaxMathMinを使って高値と安値を更新します。もしレンジが「maxConsolidationSpread×Point」を超えた場合は、isConsolidatedをfalseに設定します。レンジ相場が成立している場合は、最後のバーの終値(iClose)、安値(iLow)、高値(iHigh)を確認し、需要ゾーンの場合は「closePrice > highPrice」かつ「breakoutLow ≥ lowPrice」、供給ゾーンの場合は「closePrice < lowPrice」かつ「breakoutHigh ≤ highPrice」で識別します。

有効なゾーンについては、zoneSizeRestrictionとminZonePoints/maxZonePointsでサイズ制限を確認します。また、zonesおよびpotentialZones内でMathMaxとMathMinを使って重複やオーバーラップがないか確認します。さらにenableImpulseValidationがtrueの場合は、ArrayResizeでpotentialZonesに追加し、high、low、startTime(iTime)、endTime(TimeCurrent + zoneExtensionBars)、name(PotentialZone)などのフィールドを設定し、Printでログ出力します。それ以外の場合は、直接zonesに追加し、maxZonesに達している場合は最も古いゾーンを削除します。追加後はPrintおよびPrintZonesでログ出力し、ゾーンを追跡できるようにします。このようにして、供給および需要ゾーンを検出し、格納するコアロジックを構築できます。このDetectZones関数は、OnInitイベントハンドラで実行してゾーンを検出することが可能です。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   static datetime lastBarTime = 0;                      //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime);      //--- Check for new bar
   if (isNewBar) {                                       //--- Process new bar
      lastBarTime = currentBarTime;                      //--- Update last bar time
      DetectZones();                                     //--- Detect new zones
   }
}

OnTickイベントハンドラでは、iTimeを用いて現在のバーの時間(銘柄と期間のシフト0)をstatic変数lastBarTimeと比較し、新しいバーを追跡します。時間が異なる場合はisNewBarをtrueに設定し、lastBarTimeを更新します。新しいバーが検出された場合は、DetectZones関数を呼び出して、レンジ相場パターンに基づき新しい供給および需要ゾーンを特定します。これにより、以下のようにゾーンを検出できるようになります。

潜在的なゾーンの検出

潜在的な供給および需要ゾーンを検出できるようになったので、次はそれらをラリーの上昇や下降の動き、すなわちインパルスムーブによって検証する必要があります。モジュール化のため、これらの処理全体を関数としてまとめることが可能です。

//+------------------------------------------------------------------+
//| Validate potential zones based on impulsive move                 |
//+------------------------------------------------------------------+
void ValidatePotentialZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1);   //--- Get last closed bar time
   for (int p = ArraySize(potentialZones) - 1; p >= 0; p--) { //--- Iterate potential zones backward
      if (lastClosedBarTime >= potentialZones[p].endTime) {   //--- Check for expired zone
         Print("Potential zone expired and removed from array: ", potentialZones[p].name, " endTime: ", TimeToString(potentialZones[p].endTime)); //--- Log expiration
         ArrayRemove(potentialZones, p, 1);                   //--- Remove expired zone
         continue;                                            //--- Skip to next
      }
      if (TimeCurrent() > potentialZones[p].breakoutTime + impulseCheckBars * PeriodSeconds(_Period)) { //--- Check impulse window
         bool isImpulsive = false;                            //--- Initialize impulsive flag
         int breakoutShift = iBarShift(_Symbol, _Period, potentialZones[p].breakoutTime, false); //--- Get breakout bar shift
         double range = potentialZones[p].high - potentialZones[p].low; //--- Calculate zone range
         double threshold = range * impulseMultiplier;        //--- Calculate impulse threshold
         for (int shift = 1; shift <= impulseCheckBars; shift++) { //--- Check bars after breakout
            if (shift + breakoutShift >= iBars(_Symbol, _Period)) continue; //--- Skip out-of-bounds
            double cl = iClose(_Symbol, _Period, shift);      //--- Get close price
            if (potentialZones[p].isDemand) {                 //--- Check demand zone
               if (cl >= potentialZones[p].high + threshold) { //--- Check bullish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            } else {                                          //--- Check supply zone
               if (cl <= potentialZones[p].low - threshold) { //--- Check bearish impulse
                  isImpulsive = true;                         //--- Set impulsive flag
                  break;                                      //--- Exit loop
               }
            }
         }
         if (isImpulsive) {                                  //--- Process impulsive zone
            double zoneSize = (potentialZones[p].high - potentialZones[p].low) / _Point; //--- Calculate zone size
            if (zoneSizeRestriction == EnforceLimits && (zoneSize < minZonePoints || zoneSize > maxZonePoints)) { //--- Check size limits
               ArrayRemove(potentialZones, p, 1);            //--- Remove invalid zone
               continue;                                     //--- Skip to next
            }
            bool overlaps = false;                           //--- Initialize overlap flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check existing zones
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time overlap
                  double maxLow = MathMax(potentialZones[p].low, zones[j].low); //--- Find max low
                  double minHigh = MathMin(potentialZones[p].high, zones[j].high); //--- Find min high
                  if (maxLow <= minHigh) {                   //--- Check price overlap
                     overlaps = true;                        //--- Mark as overlapping
                     break;                                  //--- Exit loop
                  }
               }
            }
            bool duplicate = false;                          //--- Initialize duplicate flag
            for (int j = 0; j < ArraySize(zones); j++) {     //--- Check for duplicates
               if (lastClosedBarTime < zones[j].endTime) {   //--- Check time
                  if (MathAbs(zones[j].high - potentialZones[p].high) < _Point && MathAbs(zones[j].low - potentialZones[p].low) < _Point) { //--- Check price match
                     duplicate = true;                       //--- Mark as duplicate
                     break;                                  //--- Exit loop
                  }
               }
            }
            if (overlaps || duplicate) {                     //--- Check overlap or duplicate
               Print("Validated zone overlaps or duplicates, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
               ArrayRemove(potentialZones, p, 1);            //--- Remove zone
               continue;                                     //--- Skip to next
            }
            int zoneCount = ArraySize(zones);                //--- Get zones count
            if (zoneCount >= maxZones) {                     //--- Check max zones limit
               ArrayRemove(zones, 0, 1);                     //--- Remove oldest zone
               zoneCount--;                                  //--- Decrease count
            }
            ArrayResize(zones, zoneCount + 1);               //--- Resize zones array
            zones[zoneCount] = potentialZones[p];            //--- Copy potential zone
            zones[zoneCount].name = "SDZone_" + TimeToString(zones[zoneCount].startTime, TIME_DATE|TIME_SECONDS); //--- Set zone name
            zones[zoneCount].endTime = TimeCurrent() + PeriodSeconds(_Period) * zoneExtensionBars; //--- Update end time
            Print("Zone validated: ", (zones[zoneCount].isDemand ? "Demand" : "Supply"), " zone: ", zones[zoneCount].name, " at ", zones[zoneCount].low, " - ", zones[zoneCount].high, " endTime: ", TimeToString(zones[zoneCount].endTime)); //--- Log validation
            ArrayRemove(potentialZones, p, 1);               //--- Remove validated zone
            PrintZones(zones);                               //--- Print zones for debugging
         } else {                                            //--- Zone not impulsive
            Print("Potential zone not impulsive, discarded: ", potentialZones[p].low, " - ", potentialZones[p].high); //--- Log discard
            ArrayRemove(potentialZones, p, 1);               //--- Remove non-impulsive zone
         }
      }
   }
}

ここでは、潜在的な供給および需要ゾーンの検証ロジックを実装する関数を作成します。ValidatePotentialZones関数では、potentialZonesを逆順に処理し、最後にクローズしたバーの時間iTimeのシフト1)がゾーンのendTimeを超えているかを確認します。期限切れのゾーンはArrayRemoveで削除し、その操作をログに出力します。インパルスウィンドウ内のゾーン(TimeCurrent > breakoutTime + impulseCheckBars * PeriodSeconds)については、ゾーンのレンジ(high - low)とインパルス閾値(range * impulseMultiplier)を計算します。その後、ブレイクアウト後のバー(iBarShift)を確認し、需要ゾーンでは終値(iClose)が高値+閾値を超えているか、供給ゾーンでは終値が安値-閾値を下回っているかを判定し、条件を満たした場合はisImpulsiveを設定します。

インパルスが成立した場合は、zoneSizeRestrictionがEnforceLimitsの場合にminZonePointsとmaxZonePointsでゾーンサイズを確認し、zones内でMathMaxMathMinを使って重複やオーバーラップがないかチェックします。条件を満たす場合は、ArrayResizeでゾーンをzonesに移動し、名前をSDZone_に更新して終了時間も設定します。その後、PrintおよびPrintZonesでログ出力し、potentialZonesから削除します。インパルスが成立しなかったゾーンはArrayRemoveで破棄され、ログに出力されます。これにより、インパルスムーブに基づいてゾーンを検証し、ユニークで有効なゾーンのみを保持するシステムが構築されます。この関数をティックイベントハンドラで呼び出すと、次のような結果を得ることができます。

供給と需要ゾーンの検証

ゾーンの検証が可能になったので、次はチャート上でゾーンを管理および可視化し、追跡を容易にします。

//+------------------------------------------------------------------+
//| Update and draw zones                                            |
//+------------------------------------------------------------------+
void UpdateZones() {
   datetime lastClosedBarTime = iTime(_Symbol, _Period, 1); //--- Get last closed bar time
   for (int i = ArraySize(zones) - 1; i >= 0; i--) { //--- Iterate zones backward
      if (lastClosedBarTime >= zones[i].endTime) { //--- Check for expired zone
         Print("Zone expired and removed from array: ", zones[i].name, " endTime: ", TimeToString(zones[i].endTime)); //--- Log expiration
         if (deleteExpiredZonesFromChart) {    //--- Check if deleting expired
            ObjectDelete(0, zones[i].name);    //--- Delete zone rectangle
            ObjectDelete(0, zones[i].name + "Label"); //--- Delete zone label
         }
         ArrayRemove(zones, i, 1);             //--- Remove expired zone
         continue;                             //--- Skip to next
      }
      bool wasReady = zones[i].readyForTest;   //--- Store previous ready status
      if (!zones[i].readyForTest) {            //--- Check if not ready
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         double zoneLevel = zones[i].isDemand ? zones[i].high : zones[i].low; //--- Get zone level
         double distance = zones[i].isDemand ? (currentClose - zoneLevel) : (zoneLevel - currentClose); //--- Calculate distance
         if (distance > minMoveAwayPoints * _Point) { //--- Check move away distance
            zones[i].readyForTest = true;      //--- Set ready for test
         }
      }
      if (!wasReady && zones[i].readyForTest) { //--- Check if newly ready
         Print("Zone ready for test: ", zones[i].name); //--- Log ready status
      }
      if (brokenZoneMode == AllowBroken && !zones[i].tested) { //--- Check if breakable
         double currentClose = iClose(_Symbol, _Period, 1); //--- Get current close
         bool wasBroken = zones[i].broken;     //--- Store previous broken status
         if (zones[i].isDemand) {              //--- Check demand zone
            if (currentClose < zones[i].low) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         } else {                              //--- Check supply zone
            if (currentClose > zones[i].high) { //--- Check if broken
               zones[i].broken = true;         //--- Mark as broken
            }
         }
         if (!wasBroken && zones[i].broken) {  //--- Check if newly broken
            Print("Zone broken in UpdateZones: ", zones[i].name); //--- Log broken zone
            ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, brokenZoneColor); //--- Update zone color
            string labelName = zones[i].name + "Label"; //--- Get label name
            string labelText = zones[i].isDemand ? "Demand Zone (Broken)" : "Supply Zone (Broken)"; //--- Set broken label
            ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Update label text
            if (deleteBrokenZonesFromChart) {  //--- Check if deleting broken
               ObjectDelete(0, zones[i].name); //--- Delete zone rectangle
               ObjectDelete(0, labelName);     //--- Delete zone label
            }
         }
      }
      if (ObjectFind(0, zones[i].name) >= 0 || (!zones[i].broken || !deleteBrokenZonesFromChart)) { //--- Check if drawable
         color zoneColor;                        //--- Initialize zone color
         if (zones[i].tested) {                  //--- Check if tested
            zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         } else if (zones[i].broken) {           //--- Check if broken
            zoneColor = brokenZoneColor;         //--- Set broken color
         } else {                                //--- Untested zone
            zoneColor = zones[i].isDemand ? demandZoneColor : supplyZoneColor; //--- Set untested color
         }
         ObjectCreate(0, zones[i].name, OBJ_RECTANGLE, 0, zones[i].startTime, zones[i].high, zones[i].endTime, zones[i].low); //--- Create zone rectangle
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Set zone color
         ObjectSetInteger(0, zones[i].name, OBJPROP_FILL, true); //--- Enable fill
         ObjectSetInteger(0, zones[i].name, OBJPROP_BACK, true); //--- Set to background
         string labelName = zones[i].name + "Label"; //--- Generate label name
         string labelText = zones[i].isDemand ? "Demand Zone" : "Supply Zone"; //--- Set base label
         if (zones[i].tested) labelText += " (Tested)"; //--- Append tested status
         else if (zones[i].broken) labelText += " (Broken)"; //--- Append broken status
         datetime labelTime = zones[i].startTime + (zones[i].endTime - zones[i].startTime) / 2; //--- Calculate label time
         double labelPrice = (zones[i].high + zones[i].low) / 2; //--- Calculate label price
         ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, labelPrice); //--- Create label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Set label text
         ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelTextColor); //--- Set label color
         ObjectSetInteger(0, labelName, OBJPROP_ANCHOR, ANCHOR_CENTER); //--- Set label anchor
      }
   }
   ChartRedraw(0);                                //--- Redraw chart
}

次に、システムのゾーン管理および可視化ロジックを実装します。UpdateZones関数では、zonesを逆順に処理し、最後にクローズしたバーの時間(iTimeのシフト1)がゾーンのendTimeを超えているかを確認します。期限切れのゾーンはArrayRemoveで削除し、deleteExpiredZonesFromChartがtrueの場合はチャート上のオブジェクト(OBJ_RECTANGLEおよびLabel)も削除し、ログを出力します。これにより、ゾーンが期限切れの場合はもはや管理対象外となります。まだ取引準備が整っていないゾーンについては、現在の終値(iClose)からゾーンの高値(需要)または安値(供給)までの距離を計算し、「minMoveAwayPoints * _Point」を超える場合はreadyForTestをtrueに設定します。新たに取引準備が整った場合はログに出力します。

brokenZoneModeがAllowBrokenで、かつゾーンが未テストの場合、終値が安値(需要)を下回るか高値(供給)を上回った場合はbrokenとしてマークします。ObjectSetIntegerで色をbrokenZoneColorに変更し、ObjectSetStringでラベルを「Demand/Supply Zone (Broken)」に更新します。deleteBrokenZonesFromChartがtrueの場合はオブジェクトを削除し、ログに出力します。描画可能なゾーン(存在している、またはdeleteBrokenZonesFromChartがfalseで破損していないゾーン)については、状態に応じて色(demandZoneColor、supplyZoneColor、testedDemandZoneColor、testedSupplyZoneColor、またはbrokenZoneColor)を設定します。ObjectCreate(OBJ_RECTANGLE)でstartTime、高値、endTime、安値を用いて矩形を描画し、ObjectCreate(OBJ_TEXT)で中央にラベルを追加し、labelTextColorを設定します。その後、ChartRedrawでチャートを更新し、ゾーンの状態を動的に反映させます。この関数をティックイベントハンドラで呼び出すことで、以下の結果を得ることができます。

管理され可視化されたゾーン

ゾーンの管理とチャート上での可視化が可能になったので、次はそれらを追跡し、取引条件が満たされた場合に取引をおこなう処理を作成します。有効なゾーンをループして取引条件を確認する関数を作成します。

//+------------------------------------------------------------------+
//| Trade on zones                                                   |
//+------------------------------------------------------------------+
void TradeOnZones(bool isNewBar) {
   static datetime lastTradeCheck = 0;                   //--- Store last trade check time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   if (!isNewBar || lastTradeCheck == currentBarTime) return; //--- Exit if not new bar or checked
   lastTradeCheck = currentBarTime;                      //--- Update last trade check
   double currentBid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get current bid
   double currentAsk = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get current ask
   for (int i = 0; i < ArraySize(zones); i++) {          //--- Iterate through zones
      if (zones[i].broken) continue;                     //--- Skip broken zones
      if (tradeTestedMode == NoRetrade && zones[i].tested) continue; //--- Skip tested zones
      if (tradeTestedMode == LimitedRetrade && zones[i].tested && zones[i].tradeCount >= maxTradesPerZone) continue; //--- Skip max trades
      if (!zones[i].readyForTest) continue;              //--- Skip not ready zones
      double prevHigh = iHigh(_Symbol, _Period, 1);      //--- Get previous high
      double prevLow = iLow(_Symbol, _Period, 1);        //--- Get previous low
      double prevClose = iClose(_Symbol, _Period, 1);    //--- Get previous close
      bool tapped = false;                               //--- Initialize tap flag
      bool overlap = (prevLow <= zones[i].high && prevHigh >= zones[i].low); //--- Check candle overlap
      if (zones[i].isDemand) {                           //--- Check demand zone
         if (overlap && prevClose > zones[i].high) {     //--- Confirm demand tap
            tapped = true;                               //--- Set tapped flag
         }
      } else {                                           //--- Check supply zone
         if (overlap && prevClose < zones[i].low) {      //--- Confirm supply tap
            tapped = true;                               //--- Set tapped flag
         }
      }
      if (tapped) {                                      //--- Process tapped zone
         bool trendConfirmed = (trendConfirmation == NoConfirmation); //--- Assume no trend confirmation
         if (trendConfirmation == ConfirmTrend) {        //--- Check trend confirmation
            int oldShift = 2 + trendLookbackBars - 1;    //--- Calculate lookback shift
            if (oldShift >= iBars(_Symbol, _Period)) continue; //--- Skip if insufficient bars
            double oldClose = iClose(_Symbol, _Period, oldShift); //--- Get old close
            double recentClose = iClose(_Symbol, _Period, 2); //--- Get recent close
            double minChange = minTrendPoints * _Point; //--- Calculate min trend change
            if (zones[i].isDemand) {                    //--- Check demand trend
               trendConfirmed = (oldClose > recentClose + minChange); //--- Confirm downtrend
            } else {                                    //--- Check supply trend
               trendConfirmed = (oldClose < recentClose - minChange); //--- Confirm uptrend
            }
         }
         if (!trendConfirmed) continue;                 //--- Skip if trend not confirmed
         bool wasTested = zones[i].tested;              //--- Store previous tested status
         if (zones[i].isDemand) {                       //--- Handle demand trade
            double entryPrice = currentAsk;             //--- Set entry at ask
            double stopLossPrice = NormalizeDouble(zones[i].low - stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice + takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Buy(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Buy at Demand Zone"); //--- Execute buy trade
            Print("Buy trade entered at Demand Zone: ", zones[i].name); //--- Log buy trade
         } else {                                       //--- Handle supply trade
            double entryPrice = currentBid;             //--- Set entry at bid
            double stopLossPrice = NormalizeDouble(zones[i].high + stopLossDistance * _Point, _Digits); //--- Set stop loss
            double takeProfitPrice = NormalizeDouble(entryPrice - takeProfitDistance * _Point, _Digits); //--- Set take profit
            obj_Trade.Sell(tradeLotSize, _Symbol, entryPrice, stopLossPrice, takeProfitPrice, "Sell at Supply Zone"); //--- Execute sell trade
            Print("Sell trade entered at Supply Zone: ", zones[i].name); //--- Log sell trade
         }
         zones[i].tested = true;                        //--- Mark zone as tested
         zones[i].tradeCount++;                         //--- Increment trade count
         if (!wasTested && zones[i].tested) {           //--- Check if newly tested
            Print("Zone tested: ", zones[i].name, ", Trade count: ", zones[i].tradeCount); //--- Log tested zone
         }
         color zoneColor = zones[i].isDemand ? testedDemandZoneColor : testedSupplyZoneColor; //--- Set tested color
         ObjectSetInteger(0, zones[i].name, OBJPROP_COLOR, zoneColor); //--- Update zone color
         string labelName = zones[i].name + "Label";                   //--- Get label name
         string labelText = zones[i].isDemand ? "Demand Zone (Tested)" : "Supply Zone (Tested)"; //--- Set tested label
         ObjectSetString(0, labelName, OBJPROP_TEXT, labelText);       //--- Update label text
      }
   }
   ChartRedraw(0);                                       //--- Redraw chart
}

ゾーンのリテストやタップに対する取引ロジックを実装するため、TradeOnZones関数を作成します。まず、static変数lastTradeCheckを用いて新しいバーを追跡し、新しくない場合やすでにチェック済みの場合は処理を終了します。新しいバーの場合はlastTradeCheckをiTimeで更新し、SymbolInfoDoubleNormalizeDouble関数を用いてBidおよびAsk価格を正規化して取得します。次に、zonesを順に処理し、破損済みゾーン、取引上限に達したゾーン(tradeTestedModeおよびmaxTradesPerZoneに基づく)、または取引準備が整っていないゾーンをスキップします。その後、前のバーの高値(iHigh)、安値(iLow)、終値(iClose)がゾーンと重なっているかを確認します。需要ゾーン(isDemand)の場合はoverlapがtrueかつprev「Close > high」でタップを確認し、供給ゾーンの場合はoverlapがtrueかつ「prevClose < low」でタップを確認し、tappedを設定します。

タップが確認された場合、trendConfirmationがConfirmTrendの場合は、trendLookbackBarsに渡る古い終値と直近の終値(iClose)を比較し、「minTrendPoints * _Point」を満たさない場合はスキップします。有効なタップの場合、取引を実行します。需要ゾーンではAskで買い、stopLossを「low - stopLossDistance * _Point」、takeProfitを「エントリー価格 + takeProfitDistance * _Point」に設定し、obj_Trade.Buyで取引を実行しPrintでログ出力します。供給ゾーンではBidで売り、stopLossを「high + stopLossDistance * _Point」、takeProfitを「エントリー価格 - takeProfitDistance * _Point」に設定し、obj_Trade.Sellで取引を実行します。取引後はゾーンをtestedとしてマークし、tradeCountをインクリメントします。新たにテスト済みになった場合はログに出力し、ObjectSetIntegerでゾーンの色をtestedDemandZoneColorまたはtestedSupplyZoneColorに更新し、ObjectSetStringでラベルを「Demand/Supply Zone (Tested)」に更新します。最後に、チャートを再描画します。関数を呼び出すと、次の結果が得られます。

確認されたシグナル

画像からもわかるように、ゾーンのタップを検出し、そのゾーンに対する取引回数やタップ回数を記録しています。これにより、必要に応じて別のタップでも取引をおこなうことが可能です。残っているのは、利益を最大化するためにトレーリングストップを追加することです。これも関数として実装します。

//+------------------------------------------------------------------+
//| Apply trailing stop to open positions                            |
//+------------------------------------------------------------------+
void ApplyTrailingStop() {     
   double point = _Point;                               //--- Get point value
   for (int i = PositionsTotal() - 1; i >= 0; i--) {    //--- Iterate through positions
      if (PositionGetTicket(i) > 0) {                   //--- Check valid ticket
         if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == uniqueMagicNumber) { //--- Verify symbol and magic
            double sl = PositionGetDouble(POSITION_SL); //--- Get current stop loss
            double tp = PositionGetDouble(POSITION_TP); //--- Get current take profit
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open price
            if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position
               double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + trailingStopPoints * point, _Digits); //--- Calculate new stop loss
               if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > minProfitToTrail * point) { //--- Check trailing condition
                  obj_Trade.PositionModify(PositionGetInteger(POSITION_TICKET), newSL, tp); //--- Update stop loss
               }
            }
         }
      }
   }
}

ここでは、ポジションを動的に管理するためのトレーリングストップロジックを実装します。ApplyTrailingStop関数では、まず_Pointでポイント値を取得し、PositionsTotalを用いてオープンポジションを逆順に処理します。各ポジションについて、PositionGetTicketでチケット番号を取得し、PositionGetStringで銘柄を確認し、PositionGetIntegerでマジックナンバーを照合します。

買いポジション(POSITION_TYPE_BUY)では、新しいストップロスを、Bid価格(SymbolInfoDoubleSYMBOL_BID)から「trailingStopPoints * point」を引いた値として計算し、NormalizeDoubleで正規化します。現在のストップロス(PositionGetDouble(POSITION_SL))より高く、かつ利益が「minProfitToTrail * point」を超えている場合に、obj_Trade.PositionModifyで更新します。売りポジションでは、新しいストップロスをAsk価格(SYMBOL_ASK)に「trailingStopPoints * point」を加えた値として計算し、現在のストップロスより低く、利益が閾値を超えている場合に更新します。これにより、有利な価格変動時に利益を確保するためにストップロスを調整できます。この関数は、毎ティック呼び出すことで以下のようにポジション管理をおこなうことができます。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   if (enableTrailingStop) {                      //--- Check if trailing stop enabled
      ApplyTrailingStop();                        //--- Apply trailing stop to positions
   }
   static datetime lastBarTime = 0;               //--- Store last processed bar time
   datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current bar time
   bool isNewBar = (currentBarTime != lastBarTime); //--- Check for new bar
   if (isNewBar) {                                //--- Process new bar
      lastBarTime = currentBarTime;               //--- Update last bar time
      DetectZones();                              //--- Detect new zones
      ValidatePotentialZones();                   //--- Validate potential zones
      UpdateZones();                              //--- Update existing zones
   }
   if (enableTrading) {                           //--- Check if trading enabled
      TradeOnZones(isNewBar);                     //--- Execute trades on zones
   }
}

プログラムを実行すると、以下の結果が得られます。

トレーリングストップが有効

画像からもわかるように、価格が有利に動いた際にトレーリングストップが完全に機能しています。ここでは、前月の両方のゾーンに対する統合テストを示します。

統合需給テストGIF

可視化からもわかるように、プログラムはすべてのエントリー条件を識別および検証し、有効と判断された場合は、それぞれのエントリー条件に基づいて該当するポジションをオープンします。これにより、目的を達成することができます。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

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

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

本記事ではMQL5で需給取引システムを作成しました。このシステムは、レンジ相場を通じて供給および需要ゾーンを検出し、インパルスムーブでゾーンを検証し、トレンド確認とカスタマイズ可能なリスク設定を組み合わせてリテストを取引します。さらに、システムは動的なラベルと色でゾーンを可視化し、効果的なリスク管理のためにトレーリングストップも組み込まれています。

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

この需給戦略を用いることで、リテストのチャンスを取引できる準備が整い、さらに取引の最適化を進めることが可能です。取引をお楽しみください。

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

添付されたファイル |
最後のコメント | ディスカッションに移動 (3)
jiyanazad
jiyanazad | 27 11月 2025 において 20:59
こんにちは、
、エラーもなくコンパイルできました。残念ながら、バックテストでも チャート上でも何も見ることができません。需給ゾーンはプロットされず、シグナルも認識されず、取引も実行されません。デバッグとジャーナルも空です。うまくいった人はいますか?コードが完全で最新かどうかを自動でチェックできますか?
bzaranyika
bzaranyika | 4 12月 2025 において 19:04
1メートルの時間枠で作業
Allan Munene Mutiiria
Allan Munene Mutiiria | 5 12月 2025 において 07:52
bzaranyika #:
1メートルの時間枠で作業
親切なフィードバックに感謝する
古典的な戦略を再構築する(第16回):ダブルボリンジャーバンドブレイクアウト 古典的な戦略を再構築する(第16回):ダブルボリンジャーバンドブレイクアウト
本記事では、古典的なボリンジャーバンドのブレイクアウト戦略を再考し、その弱点を補う手法を紹介します。古典的戦略は、偽のブレイクアウトに弱いというよく知られた課題があります。本記事では、その弱点に対する一つの解決策として「ダブルボリンジャーバンド戦略」を提示します。この比較的知られていない手法は、従来戦略の弱点を補い、市場をより動的に捉える視点を提供します。これにより、従来のルールに縛られた制約を超え、トレーダーにとってより適応力のあるフレームワークを提供できるのです。
MQL5取引ツール(第9回):EA向けスクロール可能ガイド付き初回実行ユーザー設定ウィザードの開発 MQL5取引ツール(第9回):EA向けスクロール可能ガイド付き初回実行ユーザー設定ウィザードの開発
本記事では、エキスパートアドバイザー(EA)向けのMQL5初回実行ユーザー設定ウィザードを開発します。このウィザードはスクロール可能なガイド、インタラクティブなダッシュボード、動的テキストフォーマット、ボタンやチェックボックスなどの視覚的コントロールを備えており、ユーザーが指示に沿って操作し、取引パラメータを効率的に設定できるようにします。ユーザーは、初回実行時にプログラムの内容と操作方法を把握でき、オリエンテーションモデルとして利用できます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する 知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する
本記事は第80回の続編です。前回は、強化学習フレームワーク下で一目均衡表とADXの組み合わせを検証しました。今回は焦点を推論学習に移します。一目均衡表とADXは前回も述べた通り補完的な指標ですが、今回は前回の記事で触れたパイプライン使用に関する結論を再検討します。推論学習には、変分オートエンコーダのβアルゴリズムを用います。また、MQL5ウィザードとの統合を目的として設計されたカスタムシグナルクラスの実装も継続します。