
MQL5での取引戦略の自動化(第6回):スマートマネートレーディングのためのオーダーブロック検出の習得
はじめに
前回の記事(第5回)では、移動平均のクロスオーバーとRSIフィルターを組み合わせた「Adaptive Crossover RSI Trading Suite Strategy」を開発し、高確率の取引機会を見つける方法を紹介しました。今回の第6回では、MetaQuotes Language 5 (MQL5)を使った純粋なプライスアクション分析に焦点を当て、自動オーダーブロック検出システムを構築します。これはスマートマネートレーディングで活用される強力なツールです。この戦略は、大口プレイヤーがポジションを蓄積・分配する重要なゾーンである「オーダーブロック」を特定し、トレーダーが潜在的な反転やトレンド継続を予測するのに役立ちます。
従来のインジケーターとは異なり、この手法は価格構造のみに依存し、過去の価格動向に基づいて強気・弱気のオーダーブロックを動的に検出します。システムはこれらのゾーンをチャート上に直接表示し、市場の状況と取引のセットアップを明確に示します。この記事では、オーダーブロックの定義からMQL5への実装、バックテストによる有効性の検証、パフォーマンス分析までのステップを順を追って説明します。以下の構成で進めていきます。
- 戦略の設計図
- MQL5での実装
- バックテスト
- 結論
この記事を読み終える頃には、オーダーブロック検出の自動化に関する確かな基盤が身につき、スマートマネーの概念を取引アルゴリズムに組み込むことができるようになるでしょう。それでは、始めましょう。
戦略の設計図
まずは、価格が明確なトレンド方向を持たずに一定の範囲内で推移する「レンジ(保合い)」相場を特定することから始めます。これをおこなうために、価格が大きくブレイクアウトしないエリアを市場からスキャンします。レンジからのブレイクアウトを検出したら、その範囲内でオーダーブロックが形成されるかどうかを評価します。検証プロセスでは、ブレイクアウト直前の3本のローソク足をチェックし、それらが勢いのある動きを示しているかを確認します。勢いのある動きがあれば、ブレイクアウト方向に基づいてオーダーブロックを強気(ブル)か弱気(ベア)に分類します。上方向へのブレイクアウトであれば強気のオーダーブロック、下方向であれば弱気のオーダーブロックと判断します。検証が完了したら、今後の参考のためにそのオーダーブロックをチャート上にプロットします。以下はその例です。
直前の3本のローソク足に勢いのある動きが見られない場合は、オーダーブロックとしては認定せず、レンジ相場だけを描画します。これにより、弱いまたは重要度の低いゾーンを誤ってマーキングすることを防ぎます。有効なオーダーブロックをマーキングした後は、価格の動きを継続的に監視します。価格が以前に検証済みのオーダーブロックまで戻ってきた場合は、最初のブレイクアウト方向に沿った取引を実行し、トレンドの継続を期待します。ただし、もしオーダーブロックが直近の重要な価格ポイントを超えてしまった場合は、そのオーダーブロックを有効なリストから削除します。これにより、常に関連性が高く新鮮なゾーンのみで取引をおこなうことができます。このように構造化されたアプローチにより、弱いブレイクアウトを除外し、スマートマネーの動きに沿った高確率の取引セットアップに集中できるようになります。
MQL5での実装
オーダーブロックの識別をMQL5で実装するためには、処理全体で必要となるいくつかのグローバル変数を定義する必要があります。
#include <Trade/Trade.mqh> CTrade obj_Trade; // Struct to hold both the price and the index of the high or low struct PriceIndex { double price; int index; }; // Global variables to track the range and breakout state PriceIndex highestHigh = {0, 0}; // Stores the highest high of the range PriceIndex lowestLow = {0, 0}; // Stores the lowest low of the range bool breakoutDetected = false; // Tracks if a breakout has occurred double impulseLow = 0.0; double impulseHigh = 0.0; int breakoutBarIndex = -1; // To track the bar at which breakout occurred datetime breakoutTime = 0; // To store the breakout time string totalOBs_names[]; datetime totalOBs_dates[]; bool totalOBs_is_signals[]; #define OB_Prefix "OB REC " #define CLR_UP clrLime #define CLR_DOWN clrRed bool is_OB_UP = false; bool is_OB_DOWN = false;
まず、Trade.mqhライブラリをインクルードし、CTradeオブジェクト(obj_Trade)を作成して、取引の実行を処理します。価格レベルとそれに対応するインデックスの両方を格納するためのPriceIndex構造体を定義します。これにより、レンジ相場内の最高値と最低値を追跡できるようになります。グローバル変数「highestHigh」と「lowestLow」はこれらのキーレベルを格納し、「breakoutDetected」フラグはブレイクアウトが発生したかどうかを示します。
インパルスを検証するために、「impulseLow」と「impulseHigh」という変数を導入します。これらはブレイクアウトの強さを判断するのに役立ちます。また、「breakoutBarIndex」はブレイクアウトが発生した正確なバー(ローソク足)を追跡し、「breakoutTime」はそのタイムスタンプを保持します。オーダーブロックの管理のために、「totalOBs_names」、「totalOBs_dates」、「totalOBs_is_signals」の3つのグローバル配列を維持します。これらの配列には、注文ブロック名、それぞれのタイムスタンプ、および有効な取引シグナルであるかどうかが格納されます。
オーダーブロックの名前には接頭辞として「OB_Prefix」を定義し、買い(ブル)のオーダーブロックにはライム色を示すCLR_UP、売り(ベア)のオーダーブロックには赤色を示すCLR_DOWNの色コードを割り当てます。さらに、最後に検出されたオーダーブロックが買いか売りかを判別するために、ブール変数のis_OB_UPとis_OB_DOWNを使って管理します。プログラムの初期化時には既存のオーダーブロックを追跡する必要はないため、初期状態は空のままにしておきます。したがって、オーダーブロックの管理・検出ロジックは、OnTickイベントハンドラの中で直接制御・実行していきます。
//+------------------------------------------------------------------+ //| Expert ontick function | //+------------------------------------------------------------------+ void OnTick() { static bool isNewBar = false; int currBars = iBars(_Symbol, _Period); static int prevBars = currBars; // Detect a new bar if (prevBars == currBars) { isNewBar = false; } else if (prevBars != currBars) { isNewBar = true; prevBars = currBars; } if (!isNewBar) return; // Process only on a new bar int rangeCandles = 7; // Initial number of candles to check double maxDeviation = 50; // Max deviation between highs and lows in points int startingIndex = 1; // Starting index for the scan int waitBars = 3; //--- }
OnTickイベントハンドラ内では、まず「currBars」と「prevBars」を使って新しいバーが形成されたかどうかを検出します。新しいバーが現れた場合にのみ、フラグ変数isNewBarをtrueに設定し、新しいバーが確認できなければ処理を早期に終了します。次に、レンジを検出するために最低でも解析するローソク足の本数として「rangeCandles = 7」を設定します。これは直近7本のローソク足を対象にレンジの有無を判断するという意味です。また、maxDeviationは50ポイントに設定し、レンジ内の最高値と最安値の差がこの値以内であればレンジと見なします。スキャンの開始位置は「startingIndex = 1」とし、これは直近で確定した最新のローソク足からスキャンを始めることを示しています。さらに、オーダーブロックの検証をおこなうまでに待つべきバー数を「waitBars = 3」と設定し、これによって誤検知を減らす工夫をしています。これらの初期設定を終えた後、次のステップとしてレンジ相場の範囲を検出し、その価格帯を取得して、オーダーブロックの有効性を判定する準備をします。
// Check for consolidation or extend the range if (!breakoutDetected) { if (highestHigh.price == 0 && lowestLow.price == 0) { // If range is not yet established, look for consolidation if (IsConsolidationEqualHighsAndLows(rangeCandles, maxDeviation, startingIndex)) { GetHighestHigh(rangeCandles, startingIndex, highestHigh); GetLowestLow(rangeCandles, startingIndex, lowestLow); Print("Consolidation range established: Highest High = ", highestHigh.price, " at index ", highestHigh.index, " and Lowest Low = ", lowestLow.price, " at index ", lowestLow.index); } } else { // Extend the range if the current bar's prices remain within the range ExtendRangeIfWithinLimits(); } }
新しいバーが形成されるたびに、レンジが発生しているか、あるいは既存のレンジをブレイクアウトが起きていなければ拡張できるかを確認します。highestHigh.priceとlowestLow.priceの両方がゼロの場合は、まだレンジ相場が確立されていないことを意味します。この場合、IsConsolidationEqualHighsAndLows関数を呼び出して、直近のrangeCandles本のローソク足が許容されるmaxDeviationの範囲内でレンジを形成しているかどうかをチェックします。レンジが確認できたら、GetHighestHighとGetLowestLow関数を使って、そのレンジ内の正確な最高値と最安値を求め、それぞれの価格と該当するバーのインデックスを記録します。
すでにレンジが確立されている場合は、現在のバーが設定されたレンジの範囲内に収まっているかどうかをExtendRangeIfWithinLimits関数で確認します。この関数は、ブレイクアウトが起きていない限り、レンジを動的に調整し拡張する役割を担います。以下はカスタム関数のコードスニペットの実装です。
// Function to detect consolidation where both highs and lows are nearly equal bool IsConsolidationEqualHighsAndLows(int rangeCandles, double maxDeviation, int startingIndex) { // Loop through the last `rangeCandles` to check if highs and lows are nearly equal for (int i = startingIndex; i < startingIndex + rangeCandles - 1; i++) { // Compare the high of the current candle with the next one if (MathAbs(high(i) - high(i + 1)) > maxDeviation * Point()) { return false; // If the high difference is greater than allowed, it's not a consolidation } // Compare the low of the current candle with the next one if (MathAbs(low(i) - low(i + 1)) > maxDeviation * Point()) { return false; // If the low difference is greater than allowed, it's not a consolidation } } // If both highs and lows are nearly equal, it's a consolidation range return true; }
IsConsolidationEqualHighsAndLowsというブール型関数を定義します。この関数は、直近のrangeCandles本のローソク足の高値と安値が、指定されたmaxDeviation内でほぼ等しいかどうかを検証することで、レンジを検出する役割を担います。処理は、startingIndexから始めて各バーを順にループしながら、隣り合うローソク足同士の高値と安値を比較する形でおこないます。
forループ内ではMathAbs関数を使って、現在のバーの高値(high(i))と次のバーの高値の絶対差を計算します。この差が、ポイント単位に変換したmaxDeviation(Point)を超えた場合、すぐにfalseを返し、高値が十分に近くないためレンジとは認められないと判断します。同様に、安値同士(low(i)とlow(i + 1))もMathAbsを用いて差をチェックし、許容範囲内かどうかを確認します。いずれかの比較で許容範囲を超えた場合は、早期にfalseを返して関数を終了します。すべての高値と安値の差が許容範囲内であれば、trueを返し、有効なレンジ相場であると判定します。次に定義する関数は、レンジ内の最高値と最安値を取得する処理を担当します。
// Function to get the highest high and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetHighestHigh(int rangeCandles, int startingIndex, PriceIndex &highestHighRef) { highestHighRef.price = high(startingIndex); // Start by assuming the first candle's high is the highest highestHighRef.index = startingIndex; // The index of the highest high (starting with the `startingIndex`) // Loop through the candles and find the highest high and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (high(i) > highestHighRef.price) { highestHighRef.price = high(i); // Update highest high highestHighRef.index = i; // Update index of highest high } } } // Function to get the lowest low and its index in the last `rangeCandles` candles, starting from `startingIndex` void GetLowestLow(int rangeCandles, int startingIndex, PriceIndex &lowestLowRef) { lowestLowRef.price = low(startingIndex); // Start by assuming the first candle's low is the lowest lowestLowRef.index = startingIndex; // The index of the lowest low (starting with the `startingIndex`) // Loop through the candles and find the lowest low and its index for (int i = startingIndex + 1; i < startingIndex + rangeCandles; i++) { if (low(i) < lowestLowRef.price) { lowestLowRef.price = low(i); // Update lowest low lowestLowRef.index = i; // Update index of lowest low } } }
GetHighestHigh関数は、直近のrangeCandles本のローソク足のうち、startingIndexから開始して最高値(high)が最も高いバーの価格とそのインデックスを特定する役割を持ちます。まず、highestHighRef.priceにレンジ内の最初のローソク足の高値(high(startingIndex))を設定し、highestHighRef.indexにもstartingIndexを設定します。次に、指定された範囲内の残りのローソク足を順に調べ、現在の最高値highestHighRef.priceより高い価格があれば、highestHighRef.priceとhighestHighRef.indexの両方を更新します。この関数は、レンジ相場の上限を決定するのに役立ちます。
同様に、GetLowestLow関数は同じ範囲内で最も安い安値(low)とそのインデックスを見つける役割を果たします。lowestLowRef.priceにレンジ内の最初のローソク足の安値(low(startingIndex))、lowestLowRef.indexにstartingIndexを設定します。ループを通じて、もし現在のlowestLowRef.priceよりも低い価格があれば、両方の値を更新します。この関数は、レンジ相場の下限を決定します。最後に、レンジを拡張する関数があります。
// Function to extend the range if the latest bar remains within the range limits void ExtendRangeIfWithinLimits() { double currentHigh = high(1); // Get the high of the latest closed bar double currentLow = low(1); // Get the low of the latest closed bar if (currentHigh <= highestHigh.price && currentLow >= lowestLow.price) { // Extend the range if the current bar is within the established range Print("Range extended: Including candle with High = ", currentHigh, " and Low = ", currentLow); } else { Print("No extension possible. The current bar is outside the range."); } }
ここでは、ExtendRangeIfWithinLimits関数が、新しいバーが既存のレンジ相場内に収まっている限り、以前に特定されたレンジ相場が有効であり続けることを保証します。まず、「high(1)」関数と「low(1)」関数を使用して、直近で確定したローソク足の高値と安値を取得します。次に、currentHighがhighestHigh.price以下であり、かつcurrentLowがlowestLow.price以上であるかどうかを確認します。両方の条件が満たされていれば、レンジは拡張され、新しいローソク足が既存の範囲内に含まれていることを示す確認メッセージが出力されます。
それ以外の場合、新しいローソク足が設定されたレンジ外に移動すると、拡張はおこなわれず、レンジを拡張できないことを示すメッセージが出力されます。この関数は、有効な統合ゾーンを維持する上で重要な役割を果たし、市場が事前定義された範囲内に留まっている場合に不要なブレイクアウトの検出を防止します。
バーの価格データを取得する役割を担う定義済み関数も使用しました。以下にコードスニペットを示します。
//--- One-line functions to access price data double high(int index) { return iHigh(_Symbol, _Period, index); } double low(int index) { return iLow(_Symbol, _Period, index); } double open(int index) { return iOpen(_Symbol, _Period, index); } double close(int index) { return iClose(_Symbol, _Period, index); } datetime time(int index) { return iTime(_Symbol, _Period, index); }
これらの1行関数「high」「low」「open」「close」「time」は、過去のバーの価格および時間データを取得するためのシンプルなラッパー関数です。各関数は、それぞれ対応するMQL5の組み込み関数((iHigh、iLow、iOpen、iClose、iTime)を呼び出して、指定された「index」に対する値を取得します。「high」関数は特定のバーの高値を返し、「low」関数は安値を返します。同様に、「open」は始値を取得し、「close」は終値を取得します。「time」関数はそのバーのタイムスタンプを返します。これらの関数を使うことで、コードの可読性を高め、プログラム全体で過去データへのアクセスをよりクリーンかつ構造的におこなえるようにしています。
これらの関数を用意することで、次のコードスニペットのように、レンジ相場が形成された際のブレイクアウトをチェックできるようになります。
// Check for breakout if a consolidation range is established if (highestHigh.price > 0 && lowestLow.price > 0) { breakoutDetected = CheckRangeBreak(highestHigh, lowestLow); }
ここでは、レンジ相場が形成されている場合に、カスタム関数「CheckRangeBreak」を使ってレンジブレイクアウトをチェックし、その結果を「breakoutDetected」変数に保存します。関数の実装は以下の通りです。
// Function to check for range breaks bool CheckRangeBreak(PriceIndex &highestHighRef, PriceIndex &lowestLowRef) { double closingPrice = close(1); // Get the closing price of the current candle if (closingPrice > highestHighRef.price) { Print("Range break upwards detected. Closing price ", closingPrice, " is above the highest high: ", highestHighRef.price); return true; // Breakout detected } else if (closingPrice < lowestLowRef.price) { Print("Range break downwards detected. Closing price ", closingPrice, " is below the lowest low: ", lowestLowRef.price); return true; // Breakout detected } return false; // No breakout }
ブール型型のCheckRangeBreak関数では、現在のローソク足のclosingPrice(終値)を、highestHighRef.price(最高値の参照)およびlowestLowRef.price(最安値の参照)と比較します。closingPriceがhighestHighRef.priceよりも高ければ、上方向へのブレイクアウトを検出します。逆に、closingPriceがlowestLowRef.priceよりも低ければ、下方向へのブレイクアウトと判断します。どちらかの条件が満たされた場合、trueを返し、ブレイクアウトの方向を出力します。どちらの条件も満たさない場合は、falseを返します。
この変数を使うことで、ブレイクアウトを検出し、次の可能性のあるレンジ相場に備えてレンジ相場の状態をリセットする処理を以下のように実装できます。
// Reset state after breakout if (breakoutDetected) { Print("Breakout detected. Resetting for the next range."); breakoutBarIndex = 1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = TimeCurrent(); impulseHigh = highestHigh.price; impulseLow = lowestLow.price; breakoutDetected = false; highestHigh.price = 0; highestHigh.index = 0; lowestLow.price = 0; lowestLow.index = 0; }
ブレイクアウトが検出された後は、次のレンジに備えて状態をリセットします。まず、breakoutBarIndexを1に設定し、直近で確定したバーを参照します。また、TimeCurrent関数を使ってbreakoutTimeを現在の時刻に更新します。impulseHighとimpulseLowには、直前のレンジでのhighestHigh.price(最高値)およびlowestLow.price(最安値)をそれぞれ設定します。その後、breakoutDetectedをfalseに設定し、highestHighとlowestLowの価格およびインデックスの両方を0にリセットします。これにより、次のレンジ検出の準備が整います。この状態で、今度はインパルスに基づく有効なオーダーブロックの検出に進むことができます。
if (breakoutBarIndex >= 0 && TimeCurrent() > breakoutTime + waitBars * PeriodSeconds()) { DetectImpulsiveMovement(impulseHigh,impulseLow,waitBars,1); bool is_OB_Valid = is_OB_DOWN || is_OB_UP; datetime time1 = iTime(_Symbol,_Period,rangeCandles+waitBars+1); double price1 = impulseHigh; int visibleBars = (int)ChartGetInteger(0,CHART_VISIBLE_BARS); datetime time2 = is_OB_Valid ? time1 + (visibleBars/1)*PeriodSeconds() : time(waitBars+1); double price2 = impulseLow; string obNAME = OB_Prefix+"("+TimeToString(time1)+")"; color obClr = clrBlack; if (is_OB_Valid){obClr = is_OB_UP ? CLR_UP : CLR_DOWN;} else if (!is_OB_Valid){obClr = clrBlue;} string obText = ""; if (is_OB_Valid){obText = is_OB_UP ? "Bullish Order Block"+ShortToString(0x2BED) : "Bearish Order Block"+ShortToString(0x2BEF);} else if (!is_OB_Valid){obText = "Range";} //--- }
ここではまず、「breakoutBarIndex」が0以上であること、かつ現在時刻が「breakoutTime」よりも後であり、その差が「waitBars × PeriodSeconds関数による1バーの秒数」で計算された待機期間を超えているかをチェックします。この条件が満たされた場合、関数「DetectImpulsiveMovement」を呼び出して、インパルス的な市場の動きを検出します。この関数にはimpulseHigh、impulseLow、waitBars、および固定値の1を引数として渡します。
次に、オーダーブロックを検証するために、is_OB_DOWNまたはis_OB_UPのいずれかがtrueであるかを確認し、その結果をis_OB_Validに格納します。バーの時刻は、銘柄と時間足における特定バーの時間を返すiTime関数を使って取得し、time1に保存します。このバーの価格はimpulseHighに保存され、後の計算で使用されます。続いて、ChartGetInteger関数を使って、チャート上に表示されているバーの数を取得します。この関数にはCHART_VISIBLE_BARSというパラメータを使います。次に、time2を計算します。これはオーダーブロックが有効かどうかに応じて変化します。is_OB_Validがtrueの場合、time2はtime1から表示バー数分の時間(バー数 × PeriodSeconds)を加えた時刻になります。無効な場合は、time(waitBars+1)によって次のバーの時間を使います。この分岐は三項演算子を使っておこないます。
price2にはimpulseLowが設定されます。その後、OB_PrefixにTimeToString関数で整形した時間を付加して、オーダーブロックの名前を生成します。オーダーブロックの色は、デフォルトではobClrにより黒色に設定されます。ただし、オーダーブロックが有効な場合は、上昇方向ならCLR_UP、下降方向ならCLR_DOWNに色が変更されます。無効な場合は青色に設定されます。
オーダーブロックのテキスト(obText)は、方向に応じて設定されます。オーダーブロックが有効な場合は、「Bullish Order Block」または「Bearish Order Block」というラベルが付き、それぞれに対応するUnicode記号(上昇で0x2BED、下降で0x2BEF)をShortToString関数で変換して表示します。有効でない場合は「Range」というラベルになります。これらのUnicode記号は以下の通りです。
インパルス的な動きを検知する関数は以下の通りです。
// Function to detect impulsive movement after breakout void DetectImpulsiveMovement(double breakoutHigh, double breakoutLow, int impulseBars, double impulseThreshold) { double range = breakoutHigh - breakoutLow; // Calculate the breakout range double impulseThresholdPrice = range * impulseThreshold; // Threshold for impulsive move // Check for the price movement in the next `impulseBars` bars after breakout for (int i = 1; i <= impulseBars; i++) { double closePrice = close(i); // Get the close price of the bar // Check if the price moves significantly beyond the breakout high if (closePrice >= breakoutHigh + impulseThresholdPrice) { is_OB_UP = true; Print("Impulsive upward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutHigh + impulseThresholdPrice); return; } // Check if the price moves significantly below the breakout low else if (closePrice <= breakoutLow - impulseThresholdPrice) { is_OB_DOWN = true; Print("Impulsive downward movement detected: Close Price = ", closePrice, ", Threshold = ", breakoutLow - impulseThresholdPrice); return; } } // If no impulsive movement is detected is_OB_UP = false; is_OB_DOWN = false; Print("No impulsive movement detected after breakout."); }
この関数内では、ブレイクアウト後に価格がインパルス的に動いたかどうかを検出するため、まずrangeをbreakoutHighからbreakoutLowを引くことで計算します。次に、impulseThresholdPriceを、rangeにimpulseThresholdを掛けて算出します。これは、どれだけ価格が動けばインパルスと見なすかを定義するものです。続いて、forループを使って、次のimpulseBars本分のバーにわたって価格の動きをチェックします。
各バーについては、close(i)関数を使ってclosePrice(終値)を取得します。この関数は、i番目のバーの終値を返します。終値がbreakoutHighよりもimpulseThresholdPrice以上高ければ、インパルス的な上昇と判断し、is_OB_UPをtrueに設定して、検出したことを出力します。同様に、終値がbreakoutLowよりもimpulseThresholdPrice以上低ければ、インパルス的な下落と判断し、is_OB_DOWNをtrueに設定し、結果を出力します。
すべてのバーをチェックしてもインパルス的な動きが検出されなかった場合は、is_OB_UPとis_OB_DOWNの両方をfalseに設定し、インパルス的な動きが検出されなかったことを出力します。この処理の後は、チャート上にレンジとオーダーブロックを描画する準備が整います。
if (!is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); } } else if (is_OB_Valid){ if (ObjectFind(0,obNAME) < 0){ CreateRec(obNAME,time1,price1,time2,price2,obClr,obText); Print("Old ArraySize = ",ArraySize(totalOBs_names)); ArrayResize(totalOBs_names,ArraySize(totalOBs_names)+1); Print("New ArraySize = ",ArraySize(totalOBs_names)); totalOBs_names[ArraySize(totalOBs_names)-1] = obNAME; ArrayPrint(totalOBs_names); Print("Old ArraySize = ",ArraySize(totalOBs_dates)); ArrayResize(totalOBs_dates,ArraySize(totalOBs_dates)+1); Print("New ArraySize = ",ArraySize(totalOBs_dates)); totalOBs_dates[ArraySize(totalOBs_dates)-1] = time2; ArrayPrint(totalOBs_dates); Print("Old ArraySize = ",ArraySize(totalOBs_is_signals)); ArrayResize(totalOBs_is_signals,ArraySize(totalOBs_is_signals)+1); Print("New ArraySize = ",ArraySize(totalOBs_is_signals)); totalOBs_is_signals[ArraySize(totalOBs_is_signals)-1] = false; ArrayPrint(totalOBs_is_signals); } } breakoutBarIndex = -1; // Use the current bar's index (index 1 refers to the most recent completed bar) breakoutTime = 0; impulseHigh = 0; impulseLow = 0; is_OB_UP = false; is_OB_DOWN = false;
ここでは、まずオーダーブロックが有効(is_OB_Valid)かどうかを確認します。オーダーブロックが無効な場合は、「obNAME」という名前のオブジェクトがすでにチャート上に存在するかどうかをObjectFind関数で確認します。この関数が負の値を返した場合(=オブジェクトが存在しない場合)、CreateRec関数を呼び出して、指定されたパラメータ(時間、価格、色、テキストなど)を使ってオーダーブロックをチャート上に作成します。
オーダーブロックが有効な場合も、同様にオブジェクトが存在するかを確認し、存在しない場合にはそれを作成します。その後、オーダーブロックのデータ管理をおこないます。具体的には、ArrayResize関数を使ってtotalOBs_names(オーダーブロックの名前を格納)、totalOBs_dates(オーダーブロックのタイムスタンプを格納)、totalOBs_is_signals(各オーダーブロックが有効なシグナルかどうか(初期値はfalse))の3つの配列のサイズを調整します。配列のサイズを変更した後、ArraySize関数で古いサイズと新しいサイズを出力し、ArrayPrint関数を使って配列の中身を表示します。最後に、ブレイクアウトの状態をリセットします。具体的には、breakoutBarIndexを-1に設定し、breakoutTime、impulseHigh、impulseLowをすべて0にリセットし、オーダーブロック方向のフラグis_OB_UPおよびis_OB_DOWNをfalseに設定します。
なお、チャート上にテキスト付きの長方形を作成するために、カスタム関数CreateRecを使用しています。その実装は以下の通りです。
void CreateRec(string objName,datetime time1,double price1, datetime time2,double price2,color clr,string txt){ if (ObjectFind(0,objName) < 0){ ObjectCreate(0,objName,OBJ_RECTANGLE,0,time1,price1,time2,price2); Print("SUCCESS CREATING OBJECT >",objName,"< WITH"," T1: ",time1,", P1: ",price1, ", T2: ",time2,", P2: ",price2); ObjectSetInteger(0,objName,OBJPROP_TIME,0,time1); ObjectSetDouble(0,objName,OBJPROP_PRICE,0,price1); ObjectSetInteger(0,objName,OBJPROP_TIME,1,time2); ObjectSetDouble(0,objName,OBJPROP_PRICE,1,price2); ObjectSetInteger(0,objName,OBJPROP_FILL,true); ObjectSetInteger(0,objName,OBJPROP_COLOR,clr); ObjectSetInteger(0,objName,OBJPROP_BACK,false); // Calculate the center position of the rectangle datetime midTime = time1 + (time2 - time1) / 2; double midPrice = (price1 + price2) / 2; // Create a descriptive text label centered in the rectangle string description = txt; string textObjName = objName + description; // Unique name for the text object if (ObjectFind(0, textObjName) < 0) { ObjectCreate(0, textObjName, OBJ_TEXT, 0, midTime, midPrice); ObjectSetString(0, textObjName, OBJPROP_TEXT, description); ObjectSetInteger(0, textObjName, OBJPROP_COLOR, clrBlack); ObjectSetInteger(0, textObjName, OBJPROP_FONTSIZE, 15); ObjectSetInteger(0, textObjName, OBJPROP_ANCHOR, ANCHOR_CENTER); Print("SUCCESS CREATING LABEL >", textObjName, "< WITH TEXT: ", description); } ChartRedraw(0); } }
定義したCreateRec関数では、まずObjectFind関数を使って、指定されたオブジェクト名(objName)のオブジェクトがすでに存在するかを確認します。存在しない場合は、ObjectCreate関数を使用して、指定された時間と価格の位置に長方形を作成します。オブジェクトの種類はOBJ_RECTANGLEで定義され、色や塗りつぶし、表示設定などのプロパティはObjectSetIntegerやObjectSetDouble関数で設定します。その後、長方形の中央位置を計算し、その中央にラベルを作成します。ラベルもObjectCreate関数で生成し、種類はOBJ_TEXTです。ラベルにはテキスト、色、サイズ、アンカー(表示位置)などのプロパティを設定します。最後に、ChartRedraw関数を呼び出してチャートを更新します。すでに同名のオブジェクトやラベルが存在する場合には、何もせずスキップします。
これでオーダーブロックの描画が完了したので、次は、価格がオーダーブロックに再接触したかどうかを判断し、その範囲をブレイクする動きがあればポジションをエントリーするといったロジックへ進む準備が整います。
for (int j=ArraySize(totalOBs_names)-1; j>=0; j--){ string obNAME = totalOBs_names[j]; bool obExist = false; //Print("name = ",fvgNAME," >",ArraySize(totalFVGs)," >",j); //ArrayPrint(totalFVGs); //ArrayPrint(barTIMES); double obHigh = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,0); double obLow = ObjectGetDouble(0,obNAME,OBJPROP_PRICE,1); datetime objTime1 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,0); datetime objTime2 = (datetime)ObjectGetInteger(0,obNAME,OBJPROP_TIME,1); color obColor = (color)ObjectGetInteger(0,obNAME,OBJPROP_COLOR); if (time(1) < objTime2){ //Print("FOUND: ",obNAME," @ bar ",j,", H: ",obHigh,", L: ",obLow); obExist = true; } double Ask = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_ASK),_Digits); double Bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits); if (obColor == CLR_UP && Ask > obHigh && close(1) > obHigh && open(1) < obHigh && !totalOBs_is_signals[j]){ Print("BUY SIGNAL For (",obNAME,") Now @ ",Ask); double sl = Bid - 1500*_Point; double tp = Bid + 1500*_Point; obj_Trade.Buy(0.01,_Symbol,Ask,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } else if (obColor == CLR_DOWN && Bid < obLow && close(1) < obLow && open(1) > obLow && !totalOBs_is_signals[j]){ Print("SELL SIGNAL For (",obNAME,") Now @ ",Bid); double sl = Ask + 1500*_Point; double tp = Ask - 1500*_Point; obj_Trade.Sell(0.01,_Symbol,Bid,sl,tp); totalOBs_is_signals[j] = true; ArrayPrint(totalOBs_names,_Digits," [< >] "); ArrayPrint(totalOBs_is_signals,_Digits," [< >] "); } if (obExist == false){ bool removeName = ArrayRemove(totalOBs_names,0,1); bool removeTime = ArrayRemove(totalOBs_dates,0,1); bool remove_isSignal = ArrayRemove(totalOBs_is_signals,0,1); if (removeName && removeTime && remove_isSignal){ Print("Success removing the OB DATA from arrays. New Data as below:"); Print("Total Sizes => OBs: ",ArraySize(totalOBs_names),", TIMEs: ",ArraySize(totalOBs_dates),", SIGNALs: ",ArraySize(totalOBs_is_signals)); ArrayPrint(totalOBs_names); ArrayPrint(totalOBs_dates); ArrayPrint(totalOBs_is_signals); } } }
ここでは、totalOBs_names配列をループして、各オーダーブロック(obNAME)を処理します。まず、ObjectGetDouble関数とObjectGetInteger関数を使って、オーダーブロックの高値・安値の価格、タイムスタンプ、および色を取得します。その後、現在の時刻がオーダーブロックの終了時間よりも前かどうかを確認します。この時間条件が満たされている場合は、オーダーブロックの色と価格の条件に基づいて、買いまたは売りシグナルのチェックに進みます。条件が満たされていれば、obj_Trade.Buyまたはobj_Trade.Sell関数を使って取引を実行し、そのオーダーブロックがすでにシグナルとして機能したことを示すため、totalOBs_is_signals配列を更新します。こうすることで、価格がリトレースして再び同じオーダーブロックに触れても二重で取引がおこなわれないようにします。
一方で、オーダーブロックが時間条件を満たさない(=すでに終了している)場合には、ArrayRemove関数を使って、totalOBs_names、totalOBs_dates、totalOBs_is_signalsの各配列からそのオブジェクトを削除します。削除が成功した場合、更新された配列のサイズと内容を出力します。これが私たちが現在達成したマイルストーンです。
画像から、オーダーブロックが検出され、取引が実行されていることがわかります。これにより、私たちの目的は達成されました。これについては次のセクションで扱います。
バックテストと最適化
徹底的なバックテストの結果、次の結果が得られました。
バックテストグラフ
バックテストレポート
こちらは、2024年の1年間にわたるストラテジーのバックテスト全体を紹介する動画形式の資料です。
結論
本記事では、オーダーブロック検出を活用したスマートマネー取引戦略のための、洗練されたMQL5 EA開発プロセスを紹介しました。ダイナミックなレンジ分析、プライスアクション、リアルタイムのブレイクアウト検出などのツールを取り入れることで、重要なサポート・レジスタンスレベルを特定し、実行可能なトレードシグナルを生成し、正確に注文管理をおこなうプログラムを構築しました。
免責条項:本記事は教育目的で提供されています。取引には重大な経済的リスクが伴い、市場の動きは非常に予測困難です。本記事で紹介した戦略は構造的なアプローチを提供するものですが、将来の利益を保証するものではありません。実際の取引をおこなう前に、適切なテストとリスク管理が不可欠です。
これらの手法を応用することで、より効果的なトレーディングシステムを構築し、市場分析のアプローチを洗練させ、アルゴリズム取引を次のレベルへと引き上げることが可能になります。取引の成功をお祈りします。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17135
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





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