MQL5での取引戦略の自動化(第36回):リテストとインパルスモデルによる需給取引
はじめに
前回の記事(第35回)では、MetaQuotes Language 5 (MQL5)を使用して、ブレーカーブロック取引システムを開発しました。このシステムでは、レンジ相場(持ち合い)を特定し、スイングポイントを用いてブレーカーブロックを検証し、カスタマイズ可能なリスクパラメータと視覚的なフィードバックを備えたリテスト取引を実装しました。この第36回では、リテストとインパルスモデルを活用した需給(S&D: Supply and Demand)取引システムを開発します。このモデルは、レンジ相場によって需給ゾーンを検出し、インパルスムーブの発生によってそれらを検証し、トレンド確認を伴うリテスト時に取引を実行します。また、動的なチャート表示による視覚的な補助も提供します。本記事では以下のトピックを扱います。
この記事を読み終える頃には、需給ゾーンのリテストを取引する実用的な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のiHighとiLow を用いてhighPriceとlowPriceを初期化します。その後、バーを逆方向に繰り返し処理し、MathMaxとMathMinを使って高値と安値を更新します。もしレンジが「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内でMathMaxとMathMinを使って重複やオーバーラップがないかチェックします。条件を満たす場合は、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で更新し、SymbolInfoDoubleとNormalizeDouble関数を用いて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価格(SymbolInfoDoubleのSYMBOL_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 } }
プログラムを実行すると、以下の結果が得られます。

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

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

バックテストレポート

結論
本記事ではMQL5で需給取引システムを作成しました。このシステムは、レンジ相場を通じて供給および需要ゾーンを検出し、インパルスムーブでゾーンを検証し、トレンド確認とカスタマイズ可能なリスク設定を組み合わせてリテストを取引します。さらに、システムは動的なラベルと色でゾーンを可視化し、効果的なリスク管理のためにトレーリングストップも組み込まれています。
免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。本プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。
この需給戦略を用いることで、リテストのチャンスを取引できる準備が整い、さらに取引の最適化を進めることが可能です。取引をお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19674
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
古典的な戦略を再構築する(第16回):ダブルボリンジャーバンドブレイクアウト
MQL5取引ツール(第9回):EA向けスクロール可能ガイド付き初回実行ユーザー設定ウィザードの開発
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
知っておくべきMQL5ウィザードのテクニック(第81回): β-VAE推論学習で一目均衡表とADX-Wilderのパターンを利用する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
、エラーもなくコンパイルできました。残念ながら、バックテストでも チャート上でも何も見ることができません。需給ゾーンはプロットされず、シグナルも認識されず、取引も実行されません。デバッグとジャーナルも空です。うまくいった人はいますか?コードが完全で最新かどうかを自動でチェックできますか?
1メートルの時間枠で作業