MQL5での取引戦略の自動化(第41回):ローソク足レンジ理論(CRT)-蓄積・操作・分配(AMD)
はじめに
前回の記事(第40回)では、MetaQuotes Language 5 (MQL5)を用いたフィボナッチリトレースメント取引システムを開発しました。このシステムでは、日足のレンジまたはルックバック配列からリトレースメントレベルを算出し、強気または弱気のセットアップを特定、価格が指定レベルを横切った際にエントリーをおこない、新しいフィボナッチ計算に応じた任意の決済も可能にしました。本記事では、ローソク足レンジ理論(CRT)取引システムを開発し、蓄積、操作、分配(AMD)の各フェーズを取り入れます。
このシステムでは、指定した時間足で蓄積レンジを特定し、任意のフィルタで操作の深さを確認しながらブレイクを検知します。分配フェーズでは、ローソク足の終値を確認して反転を確定させ、エントリーをおこないます。リスクリワード比に基づく動的または静的なストップロスおよびテイクプロフィットの設定に対応し、必要に応じてトレーリングストップや方向ごとのポジション上限を適用することでリスク管理もおこなうことができます。各フェーズはチャート上に矩形、レベル線、テキストラベルで可視化され、直感的に状態を把握できるようになっています。本記事では以下のトピックを扱います。
これにより、AMDフェーズを組み込んだCRT取引戦略のMQL5版を作成し、カスタマイズ可能な状態で活用できるようになります。さっそく見ていきましょう。
ローソク足レンジ理論(CRT)フレームワークの理解
ローソク足レンジ理論(CRT)は、ローソク足のレンジ内に存在する重要なフェーズを特定することに焦点を当て、高確率の反転トレードを狙うプライスアクション戦略です。CRTでは、市場の動きを蓄積、操作、分配という3つの主要なフェーズに分類します。蓄積フェーズでは、価格が特定のレンジ内で落ち着き、機関投資家のポジション構築がおこなわれることを示唆します。操作フェーズでは、価格がレンジの上下限を一時的に超え、トレーダーを誘い込み弱いポジションを振るい落とす動きが発生します。分配フェーズでは、価格がレンジ内に反転して戻った後、本来の方向性に沿った大きな値動きが展開されます。このアプローチは、重要なブレイクの多くが流動性を作るための偽の動きであり、その後に反対方向への強い値動きが続くという考えに基づいています。
強気のレンジセットアップでは、終値が上方向に位置するローソク足レンジを特定し、下方向への一時的なブレイクを操作フェーズとして捉え、安値を上抜けて反転したタイミングで買いエントリーをおこない、上方向の分配を狙います。逆に、弱気のレンジセットアップでは、下方向に終値が位置するローソク足レンジを確認し、高値を上抜けるブレイクを操作として捉え、高値を下抜けて反転したタイミングで売りエントリーをおこない、下方向の分配を狙います。最小操作深度やローソク足の終値による反転確認などのフィルタを組み込むことで、低品質なセットアップを避け、より信頼度の高い取引に注力できます。以下にCRTセットアップのサンプルを示します。

本システムでは、ユーザー指定の時間足で蓄積レンジを定義します。ブレイクを検知した際には、オプションで設定された操作深度の閾値に基づき検証をおこないます。反転確認は、確認用の時間足における一定本数のローソク足終値で判断します。エントリーは方向ごとのポジション上限を適用して実行されます。ストップロスおよびテイクプロフィットは、リスクリワード比に応じて動的または静的に設定可能です。利益閾値到達後には任意でトレーリングストップも導入できます。すべてのフェーズは、チャート上に矩形、レベル線、ラベルとして可視化され、直感的に状態を把握できるようになっています。以下に想定されるビジュアル表示の例を示します。

MQL5での実装
MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータとグローバル変数をいくつか宣言する必要があります。
//+------------------------------------------------------------------+ //| CRT Candle Range Theory EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #include <Trade\Trade.mqh> //+------------------------------------------------------------------+ //| Enums | //+------------------------------------------------------------------+ enum SLTP_Method { // Define SL/TP method enum Dynamic_Method = 0, // Dynamic based on breach extreme Static_Method = 1 // Static based on fixed points }; enum TrailingTypeEnum { // Define trailing type enum Trailing_None = 0, // None Trailing_Points = 1 // By Points }; //+------------------------------------------------------------------+ //| Input Parameters | //+------------------------------------------------------------------+ input ENUM_TIMEFRAMES RangeTF = PERIOD_H4; // Timeframe for Range Definition input double TradeVolume = 0.01; // Trade Volume Size input double RR_Ratio = 1.3; // Risk to Reward Ratio input SLTP_Method SLTP_Approach = Static_Method; // SL/TP Calculation Method input int SL_Points = 100; // SL Points (for Static Method) input TrailingTypeEnum TrailingType = Trailing_None; // Trailing Stop Type input double Trailing_Stop_Points = 30.0; // Trailing Stop in Points input double Min_Profit_To_Trail_Points = 50.0; // Min Profit to Start Trailing in Points input int UniqueID = 123456789; // Unique Trade Identifier input int MaxPositionsDir = 1; // Max Positions per Direction input ENUM_TIMEFRAMES ConfirmTF = PERIOD_CURRENT; // Confirmation Timeframe (for bar closures) input int ConfirmBars = 1; // Bars to Confirm Reversal on Close (0 to disable) input bool UseManipFilter = true; // Use Manipulation Depth Filter input double MinManipPct = 5.0; // Min Manipulation % of Range (if filter enabled) input double DistribProjPct = 50.0; // Distribution Projection % of Range Duration
実装では、まず「#include <Trade\Trade.mqh>」を使用してTradeライブラリをインクルードします。このライブラリは、取引操作に必要なクラスや関数を提供します。次に、ユーザーが設定可能なオプションを分類するための列挙型を定義します。まずSLTP_Method列挙型を作成し、ブレイクの極値に基づく動的なストップロスとテイクプロフィットの計算にはDynamic_Method、固定値による設定にはStatic_Methodを使用します。また、TrailingTypeEnum列挙型も定義します。Trailing_Noneはトレーリングストップを無効にし、Trailing_Pointsは指定ポイント数でトレーリングを有効にします。これにより柔軟なリスク管理が可能になります。
続いて、エキスパートアドバイザー(EA)のプロパティダイアログからユーザーが調整可能な入力パラメータを宣言します。これには、蓄積レンジを定義する時間足を指定するRangeTF、各エントリーのロットサイズを設定するTradeVolume、リスクリワード比率を決定するRR_Ratio、先ほど定義した列挙型を用いてストップロスとテイクプロフィット方式を選択するSLTP_Approachなどが含まれます。その他のパラメータも、名称から意味が直感的に分かるようになっています。これらの入力により、システムは異なる市場環境やユーザーの好みに応じて柔軟に調整可能となります。コンパイル後には、以下のような入力セットが得られます。

ここまで設定したら、プログラム全体で使用するグローバル変数を定義していきます。
//+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CTrade obj_Trade; //--- Trade object datetime prevRangeTime = 0; //--- Previous range time double rangeMax = 0.0; //--- Range maximum double rangeMin = 0.0; //--- Range minimum bool positiveDirection = false; //--- Positive direction flag bool rangeBreached = false; //--- Range breached flag double breachPoint = 0.0; //--- Breach point string maxLevelObj = "RangeMaxLevel"; //--- Max level object name string minLevelObj = "RangeMinLevel"; //--- Min level object name string maxTextObj = "CRT_High_Text"; //--- CRT high text object string minTextObj = "CRT_Low_Text"; //--- CRT low text object bool tradedSetup = false; //--- Traded setup flag datetime breachTime = 0; //--- Breach time datetime lastConfirmTime = 0; //--- Last confirm time
次に、プログラム全体で状態を保持し、CRTロジックを管理するためのグローバル変数を宣言します。CTradeクラスからobj_Tradeオブジェクトをインスタンス化し、エントリーやポジション変更など、すべての取引関連操作を担当します。蓄積レンジを追跡するための変数として、前回のレンジローソク足のタイムスタンプを保持するprevRangeTime、現在のレンジの高値と安値を格納するrangeMaxおよびrangeMin、レンジローソク足が陽線か陰線かを示すブールフラグpositiveDirectionを定義します。さらに、ブレイク発生を示すrangeBreached、操作中の極値を記録するbreachPoint、同一セットアップでの重複エントリーを防ぐtradedSetupなどのフラグや値も設定します。
チャートオブジェクト名を管理するための文字列変数も用意します。レンジの極値を示す水平線用のmaxLevelObjとminLevelObj、CRTの高値と安値を表示するテキストラベル用のmaxTextObjとminTextObjです。また、操作フェーズの開始時刻を記録するbreachTime、最後の確認ローソク足の時刻を追跡するlastConfirmTimeも設定し、タイミング依存のイベントを正確に監視できるようにします。これで準備が整いました。OnInitイベントハンドラ内でトレード用のマジックナンバーを設定するところから実装を始めます。
//+------------------------------------------------------------------+ //| EA Start Function | //+------------------------------------------------------------------+ int OnInit() { obj_Trade.SetExpertMagicNumber(UniqueID); //--- Set magic number return(INIT_SUCCEEDED); //--- Return success }
OnInitイベントハンドラは、EAが起動したときやチャートにアタッチされたときに実行されます。ここで、obj_Trade.SetExpertMagicNumberをUniqueID入力値で呼び出し、プログラムで開かれるすべての取引に一意の識別子を割り当てます。これにより、取引のフィルタリングや管理が容易になります。初期化が正常に完了したことを示すために、最後にINIT_SUCCEEDEDを返します。これでプログラムの実行準備が整いました。次に、チャート上にラベルやレベルを描画するためのヘルパー関数を定義していきます。以下がそのロジックです。
//+------------------------------------------------------------------+ //| Render Horizontal Level | //+------------------------------------------------------------------+ void RenderLevel(string objName, double levelVal, color levelClr, string levelDesc) { ObjectDelete(ChartID(), objName); //--- Delete object ObjectCreate(ChartID(), objName, OBJ_HLINE, 0, 0, levelVal); //--- Create hline ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, levelClr); //--- Set color ObjectSetInteger(ChartID(), objName, OBJPROP_STYLE, STYLE_DOT); //--- Set style ObjectSetString(ChartID(), objName, OBJPROP_TOOLTIP, levelDesc); //--- Set tooltip ChartRedraw(ChartID()); //--- Redraw chart } //+------------------------------------------------------------------+ //| Render Text Label | //+------------------------------------------------------------------+ void RenderText(string objName, datetime timeVal, double priceVal, string textStr, color textClr, int anchorVal) { ObjectDelete(ChartID(), objName); //--- Delete object ObjectCreate(ChartID(), objName, OBJ_TEXT, 0, timeVal, priceVal); //--- Create text ObjectSetString(ChartID(), objName, OBJPROP_TEXT, textStr); //--- Set text ObjectSetInteger(ChartID(), objName, OBJPROP_COLOR, textClr); //--- Set color ObjectSetInteger(ChartID(), objName, OBJPROP_ANCHOR, anchorVal); //--- Set anchor ObjectSetInteger(ChartID(), objName, OBJPROP_FONTSIZE, 10); //--- Set fontsize ChartRedraw(ChartID()); //--- Redraw chart }
まず、RenderLevel関数を定義し、レンジの高値や安値などの重要な価格レベルを示す水平線をチャート上に描画または更新します。この関数は、オブジェクト名、価格レベル値、色、説明文をパラメータとして受け取ります。関数内では、まずObjectDeleteを使って同名の既存オブジェクトを削除し、重複を避けます。その後、ObjectCreateでOBJ_HLINEタイプの水平線オブジェクトを作成し、指定された価格レベルに配置します。色はObjectSetIntegerとOBJPROP_COLORで設定し、OBJPROP_STYLEにSTYLE_DOTを指定して点線スタイルを適用、OBJPROP_TOOLTIPでツールチップとして説明を追加します。最後にChartRedrawを呼び出して、変更を即座にチャート上に反映させます。
同様に、RenderText関数を作成し、フェーズの識別などの注釈用テキストラベルをチャート上に配置または更新します。この関数はオブジェクト名、時間・価格座標、表示テキスト、色、アンカーポイントをパラメータとして受け取ります。まずObjectDeleteで既存オブジェクトを削除し、続いてObjectCreateでOBJ_TEXTタイプのテキストオブジェクトを指定の時間と価格に作成します。テキスト内容はObjectSetStringとOBJPROP_TEXTで設定し、色はObjectSetIntegerとOBJPROP_COLORで指定します。アンカー位置はOBJPROP_ANCHORで定義し、フォントサイズはOBJPROP_FONTSIZEを10に設定します。最後にChartRedrawを呼び出してラベルを正しく表示させます。これらの関数を用いることで、OnTickイベントハンドラ内でレンジを定義し、チャート上に可視化することが可能になります。
//+------------------------------------------------------------------+ //| Tick Processing Function | //+------------------------------------------------------------------+ void OnTick() { double currBid = SymbolInfoDouble(_Symbol, SYMBOL_BID); //--- Get current bid double currAsk = SymbolInfoDouble(_Symbol, SYMBOL_ASK); //--- Get current ask datetime currRangeTime = iTime(_Symbol, RangeTF, 0); //--- Get current range time if (currRangeTime != prevRangeTime) { //--- Check new range prevRangeTime = currRangeTime; //--- Update prev time double prevMax = iHigh(_Symbol, RangeTF, 1); //--- Get prev high double prevMin = iLow(_Symbol, RangeTF, 1); //--- Get prev low double prevStart = iOpen(_Symbol, RangeTF, 1); //--- Get prev open double prevEnd = iClose(_Symbol, RangeTF, 1); //--- Get prev close rangeMax = prevMax; //--- Set range max rangeMin = prevMin; //--- Set range min positiveDirection = (prevEnd > prevStart); //--- Set direction rangeBreached = false; //--- Reset breached breachPoint = positiveDirection ? rangeMin : rangeMax; //--- Set breach point tradedSetup = false; //--- Reset traded breachTime = 0; //--- Reset breach time lastConfirmTime = 0; //--- Reset confirm time RenderLevel(maxLevelObj, rangeMax, clrOrange, "Range Max"); //--- Render max level RenderLevel(minLevelObj, rangeMin, clrPurple, "Range Min"); //--- Render min level // Add text labels for current CRT High and Low datetime labelTime = currRangeTime; //--- Set label time RenderText(maxTextObj, labelTime, rangeMax, "CRT High", clrOrange, ANCHOR_RIGHT_LOWER); //--- Render high text RenderText(minTextObj, labelTime, rangeMin, "CRT Low", clrPurple, ANCHOR_RIGHT_UPPER); //--- Render low text // Draw background rectangle for the accumulation phase (range candle) with fill true string rangeRectObj = "RangeRectangle_" + IntegerToString(currRangeTime); //--- Range rect name datetime rangeStartTime = iTime(_Symbol, RangeTF, 1); //--- Get start time datetime rangeEndTime = currRangeTime; //--- Set end time ObjectCreate(ChartID(), rangeRectObj, OBJ_RECTANGLE, 0, rangeStartTime, rangeMax, rangeEndTime, rangeMin); //--- Create rect color rectClr = positiveDirection ? clrLightGreen : clrLightPink; //--- Set rect color ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_COLOR, rectClr); //--- Set color ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_FILL, true); //--- Set fill ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_BACK, true); //--- Set back ObjectSetInteger(ChartID(), rangeRectObj, OBJPROP_STYLE, STYLE_SOLID); //--- Set style ChartRedraw(ChartID()); //--- Redraw chart } }
OnTickイベントハンドラでは、まずSymbolInfoDoubleを使用してSYMBOL_BIDの現在のBid価格を取得しcurrBidに代入します。同様にSYMBOL_ASKを用いてAsk価格を取得しcurrAskに代入します。次にiTimeでレンジ時間足の最新バーのタイムスタンプを取得しcurrRangeTimeに格納します。このタイムスタンプがprevRangeTimeと異なる場合、新しいレンジバーが形成されたことを示すため、prevRangeTimeを更新します。前バーの高値はiHighでshift 1を使ってprevMaxに、安値はiLowでprevMinに、始値はiOpenでprevStartに、終値はiCloseでprevEndに取得します。レンジの境界はrangeMaxに高値、rangeMinに安値を設定し、終値が始値を上回ればpositiveDirectionをtrueとして強気セットアップと判断します。rangeBreachedはfalseにリセットし、breachPointは陽線の場合は最小値、陰線の場合は最大値で初期化、tradedSetupをクリアし、breachTimeとlastConfirmTimeをゼロにします。
可視化のため、RenderLevelを呼び出してレンジ最大値をオレンジ色、説明文「Range Max」で描画し、最小値は紫色で「Range Min」とします。ラベルはlabelTimeに現在のレンジ時間を設定し、RenderTextを呼んでCRT Highをオレンジ色で右下アンカーに、CRT Lowを紫色で右上アンカーに描画します。蓄積フェーズをハイライトするため、RangeRectangle_に変換した現在のレンジ時間文字列を結合して一意の矩形名を作成します。前バーの開始時間はiTimeのshift 1でrangeStartTimeに取得し、rangeEndTimeを現在時間に設定します。ObjectCreateでOBJ_RECTANGLEタイプの矩形を作成し、時間と価格の範囲に広げます。陽線の場合はライトグリーン、陰線の場合はライトピンクを選択し、色を適用、塗りつぶしを有効にして背景として設定、スタイルはソリッドにしてChartRedrawでチャートに反映します。マイルストーンごとにコンパイルして動作を確認するのは良いプログラミング習慣です。この時点で、以下のような結果になります。

レンジの設定が正常に完了したことが確認できました。次に、ブレイクを判定しセットアップの取引を実行していきます。
if (rangeMax == 0.0 || rangeMin == 0.0) return; //--- Return if no range bool justBreached = false; //--- Init just breached if (positiveDirection && currBid <= rangeMin) { //--- Check positive breach if (!rangeBreached) { //--- Check not breached rangeBreached = true; //--- Set breached justBreached = true; //--- Set just breached breachTime = TimeCurrent(); //--- Set breach time } breachPoint = MathMin(breachPoint, currBid); //--- Update breach point } else if (!positiveDirection && currBid >= rangeMax) { //--- Check negative breach if (!rangeBreached) { //--- Check not breached rangeBreached = true; //--- Set breached justBreached = true; //--- Set just breached breachTime = TimeCurrent(); //--- Set breach time } breachPoint = MathMax(breachPoint, currBid); //--- Update breach point } if (rangeBreached && !tradedSetup) { //--- Check breached and not traded // Check for confirmed reversal on bar closures bool reversalConfirmed = false; //--- Init confirmed if (ConfirmBars == 0) { //--- Check no confirm reversalConfirmed = true; //--- Set confirmed } else { //--- Else datetime currConfirmTime = iTime(_Symbol, ConfirmTF, 0); //--- Get confirm time if (currConfirmTime != lastConfirmTime) { //--- Check new confirm lastConfirmTime = currConfirmTime; //--- Update last confirm int confirmedCount = 0; //--- Init count for (int i = 1; i <= ConfirmBars; i++) { //--- Iterate bars double confirmClose = iClose(_Symbol, ConfirmTF, i); //--- Get close if (positiveDirection && confirmClose > rangeMin) { //--- Check positive confirmedCount++; //--- Increment count } else if (!positiveDirection && confirmClose < rangeMax) { //--- Check negative confirmedCount++; //--- Increment count } } if (confirmedCount >= ConfirmBars) { //--- Check confirmed reversalConfirmed = true; //--- Set confirmed } } } // Calculate manipulation depth for filter bool manipSufficient = true; //--- Init sufficient double rangeSize = rangeMax - rangeMin; //--- Calc range size double manipDepth = positiveDirection ? (rangeMin - breachPoint) : (breachPoint - rangeMax); //--- Calc depth double manipPct = (manipDepth / rangeSize) * 100.0; //--- Calc percent if (UseManipFilter) { //--- Check filter if (manipPct < MinManipPct) { //--- Check insufficient manipSufficient = false; //--- Set insufficient } } bool justEntered = false; //--- Init entered datetime entryTime = 0; //--- Init entry time double entryPrice = 0.0; //--- Init entry price double gainTarget = 0.0; //--- Init target if (reversalConfirmed && manipSufficient) { //--- Check confirmed and sufficient if (positiveDirection && currBid > rangeMin && ActivePositions(POSITION_TYPE_BUY) < MaxPositionsDir) { //--- Check buy entry double lossStop; //--- Init SL if (SLTP_Approach == Dynamic_Method) { //--- Check dynamic lossStop = NormalizeDouble(breachPoint, _Digits); //--- Set SL double riskDistance = currAsk - breachPoint; //--- Calc risk gainTarget = NormalizeDouble(currAsk + riskDistance * RR_Ratio, _Digits); //--- Set TP } else { //--- Static lossStop = NormalizeDouble(currAsk - SL_Points * _Point, _Digits); //--- Set SL gainTarget = NormalizeDouble(currAsk + SL_Points * RR_Ratio * _Point, _Digits); //--- Set TP } if (obj_Trade.Buy(TradeVolume, _Symbol, currAsk, lossStop, gainTarget, "CRT Positive Entry")) { //--- Open buy if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Check success Print("Positive Signal: Range raided below min, reversed back in (confirmed). Entry at ", DoubleToString(currAsk, _Digits), " SL at ", DoubleToString(lossStop, _Digits), " TP at ", DoubleToString(gainTarget, _Digits)); //--- Log entry Print("Debug: Accumulation Range: ", DoubleToString(rangeSize / _Point, 0), " points. Manipulation Depth: ", DoubleToString(manipDepth / _Point, 0), " points (", DoubleToString(manipPct, 2), "% of range)"); //--- Log debug string markerName = "EntryMarker_" + IntegerToString(TimeCurrent()); //--- Marker name ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, TimeCurrent(), currBid); //--- Create marker ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, 233); //--- Set code ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, clrBlue); //--- Set color ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, ANCHOR_BOTTOM); //--- Set anchor tradedSetup = true; //--- Set traded justEntered = true; //--- Set entered entryTime = TimeCurrent(); //--- Set entry time entryPrice = currAsk; //--- Set entry price } } } else if (!positiveDirection && currBid < rangeMax && ActivePositions(POSITION_TYPE_SELL) < MaxPositionsDir) { //--- Check sell entry double lossStop; //--- Init SL if (SLTP_Approach == Dynamic_Method) { //--- Check dynamic lossStop = NormalizeDouble(breachPoint, _Digits); //--- Set SL double riskDistance = breachPoint - currBid; //--- Calc risk gainTarget = NormalizeDouble(currBid - riskDistance * RR_Ratio, _Digits); //--- Set TP } else { //--- Static lossStop = NormalizeDouble(currBid + SL_Points * _Point, _Digits); //--- Set SL gainTarget = NormalizeDouble(currBid - SL_Points * RR_Ratio * _Point, _Digits); //--- Set TP } if (obj_Trade.Sell(TradeVolume, _Symbol, currBid, lossStop, gainTarget, "CRT Negative Entry")) { //--- Open sell if (obj_Trade.ResultRetcode() == TRADE_RETCODE_DONE) { //--- Check success Print("Negative Signal: Range raided above max, reversed back in (confirmed). Entry at ", DoubleToString(currBid, _Digits), " SL at ", DoubleToString(lossStop, _Digits), " TP at ", DoubleToString(gainTarget, _Digits)); //--- Log entry Print("Debug: Accumulation Range: ", DoubleToString(rangeSize / _Point, 0), " points. Manipulation Depth: ", DoubleToString(manipDepth / _Point, 0), " points (", DoubleToString(manipPct, 2), "% of range)"); //--- Log debug string markerName = "EntryMarker_" + IntegerToString(TimeCurrent()); //--- Marker name ObjectCreate(ChartID(), markerName, OBJ_ARROW, 0, TimeCurrent(), currAsk); //--- Create marker ObjectSetInteger(ChartID(), markerName, OBJPROP_ARROWCODE, 234); //--- Set code ObjectSetInteger(ChartID(), markerName, OBJPROP_COLOR, clrRed); //--- Set color ObjectSetInteger(ChartID(), markerName, OBJPROP_ANCHOR, ANCHOR_TOP); //--- Set anchor tradedSetup = true; //--- Set traded justEntered = true; //--- Set entered entryTime = TimeCurrent(); //--- Set entry time entryPrice = currBid; //--- Set entry price } } } } }
OnTick関数内の処理を続けると、まずレンジが正しく定義されているかを確認します。rangeMaxまたはrangeMinがゼロの場合は不正な状態と判断し、処理を早期に終了します。新たなブレイクを追跡するために、ブール型変数justBreachedをfalseで初期化します。陽線方向のセットアップの場合、現在のBid価格がレンジの最小値以下で、まだブレイクが発生していなければ、rangeBreachedをtrueに設定し、justBreachedをtrueにマーク、さらにTimeCurrent関数でブレイク発生時刻をbreachTimeに記録します。その後、breachPointはMathMin関数を用いて現在値とBidのうち低い方に更新します。陰線方向の場合も同様の処理をおこないます。
ブレイクが発生しており、まだこのセットアップで取引が実行されていない場合は、反転の確認に進みます。まずreversalConfirmedをfalseで初期化します。ConfirmBarsが0の場合は、即座にtrueに設定します。それ以外の場合、確認時間足の最新バーの時刻をiTimeで取得しcurrConfirmTimeに格納します。これがlastConfirmTimeと異なる場合はlastConfirmTimeを更新し、確認ローソク足をカウントします。Shift 1からConfirmBarsまでループし、各終値をiCloseで取得、陽線セットアップでは最小値以上、陰線セットアップでは最大値以下の終値があればカウンターを増やします。カウントがConfirmBars以上であれば、反転を確認済みとします。次に、操作の十分性を評価します。manipSufficientをtrueで初期化し、レンジサイズをrangeMaxとrangeMinの差として計算、操作深さをレンジ端からbreachPointまでの距離として求めます(陽線は引き算、陰線は加算)、パーセンテージは深さをサイズで割り100倍します。UseManipFilterが有効で、パーセンテージがMinManipPct未満の場合はmanipSufficientをfalseに設定します。
続いてエントリー用変数を準備し初期化します。反転が確認され、操作の十分性が満たされている場合、陽線方向の買いエントリー条件をチェックします。Bidがレンジ最小値を上回り、ActivePositions関数で取得した買いポジション(POSITION_TYPE_BUY)がMaxPositionsDir未満であれば、エントリーレベルを計算します。ActivePositions関数はコードをモジュール化するために定義したヘルパー関数で、現在の有効なポジション数を返します。その実装は以下の通りです。
//+------------------------------------------------------------------+ //| Count Active Positions by Type | //+------------------------------------------------------------------+ int ActivePositions(ENUM_POSITION_TYPE posType) { int total = 0; //--- Init total for (int pos = PositionsTotal() - 1; pos >= 0; pos--) { //--- Iterate positions if (PositionGetSymbol(pos) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID && PositionGetInteger(POSITION_TYPE) == posType) { //--- Check position total++; //--- Increment total } } return total; //--- Return total }
関数自体はシンプルです。理解を助けるためにコメントも追加しています。ロジックに戻ると、obj_Trade.Buyを使用して買い注文を試みます。引数には数量、銘柄、Ask、ストップロス、テイクプロフィット、コメントを指定します。ResultRetcodeがTRADE_RETCODE_DONEの場合、注文は成功と判断され、シグナルのログやレンジ・操作深さに関するデバッグ情報をPrintで出力します。その後、ObjectCreateでエントリーマーカーの矢印を作成します。OBJ_ARROWタイプで現在時刻とBid位置に配置、コード233、青色、下側アンカーを設定します。tradedSetupとjustEnteredをtrueにフラグし、エントリー時刻と価格を記録します。MQL5ではWingdingsフォント由来の矢印コードが使用可能で、任意のコードを選択できます。

陰線方向の場合も同様の処理をおこないます。Bidがレンジ最大値を下回り、売りポジション数が制限以下であれば、ストップロスをbreachPointを基準にリスク幅から動的に計算、テイクプロフィットはリスク×比率で算出します。静的設定の場合はBidにSL_Points×ポイントを加減算してストップロスとテイクプロフィットを設定します。obj_Trade.Sellで売り、成功時には同様にログ出力、コード234、赤色、上側アンカーでマーカーを描画し、フラグとエントリー情報を更新します。コンパイル後、以下の結果が得られます。コンパイルすると、次の結果が得られます。

画像から、確認が取れた場合に取引が成立することが確認できます。取引の明確化のため、レベルをチャート上に可視化し、実際の状況を視覚的に追跡できるようにします。
// If just entered trade, draw manipulation rectangle, distribution, and labels (including accumulation) if (justEntered) { //--- Check entered string setupSuffix = IntegerToString(prevRangeTime); //--- Setup suffix // Label the range as Accumulation phase (now only for complete setups) string accumTextUnique = "AccumText_" + setupSuffix; //--- Accum text name double accumPrice = (rangeMax + rangeMin) / 2; //--- Accum price datetime labelTime = prevRangeTime; //--- Label time RenderText(accumTextUnique, labelTime, accumPrice, "Accumulation", clrBlue, ANCHOR_RIGHT); //--- Render accum text // Calculate the manipulation extreme using candle highs/lows between currRangeTime and entryTime int startBar = iBarShift(_Symbol, PERIOD_CURRENT, prevRangeTime); //--- Start bar int endBar = iBarShift(_Symbol, PERIOD_CURRENT, entryTime); //--- End bar if (startBar < 0 || endBar < 0) return; //--- Return invalid if (startBar < endBar) { int temp = startBar; startBar = endBar; endBar = temp; } //--- Swap if needed int barCount = startBar - endBar + 1; //--- Calc bar count double manipExtreme; //--- Init manip extreme double manipStartPrice = positiveDirection ? rangeMin : rangeMax; //--- Manip start if (positiveDirection) { //--- Check positive int lowestBar = iLowest(_Symbol, PERIOD_CURRENT, MODE_LOW, barCount, endBar); //--- Get lowest manipExtreme = iLow(_Symbol, PERIOD_CURRENT, lowestBar); //--- Set extreme } else { //--- Negative int highestBar = iHighest(_Symbol, PERIOD_CURRENT, MODE_HIGH, barCount, endBar); //--- Get highest manipExtreme = iHigh(_Symbol, PERIOD_CURRENT, highestBar); //--- Set extreme } // Draw manipulation rectangle (border only) from CRT end to signal time string manipRectObj = "ManipRectangle_" + setupSuffix; //--- Manip rect name double topPrice = MathMax(manipStartPrice, manipExtreme); //--- Top price double bottomPrice = MathMin(manipStartPrice, manipExtreme); //--- Bottom price ObjectCreate(ChartID(), manipRectObj, OBJ_RECTANGLE, 0, prevRangeTime, topPrice, entryTime, bottomPrice); //--- Create rect ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_COLOR, clrBlue); //--- Set color ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_FILL, false); //--- Set no fill ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_BACK, true); //--- Set back ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_STYLE, STYLE_DOT); //--- Set style ObjectSetInteger(ChartID(), manipRectObj, OBJPROP_WIDTH, 2); //--- Set width ChartRedraw(ChartID()); //--- Redraw chart // Add manipulation text label at breach time string manipTextUnique = "ManipText_" + setupSuffix; //--- Manip text name int anchorManip = positiveDirection ? ANCHOR_RIGHT_UPPER : ANCHOR_RIGHT_LOWER; //--- Manip anchor RenderText(manipTextUnique, breachTime, manipExtreme, "Manipulation", clrBlue, anchorManip); //--- Render manip text // Label and draw distribution string distribTextUnique = "DistribText_" + setupSuffix; //--- Distrib text name color distribClr = positiveDirection ? clrGreen : clrRed; //--- Distrib color int anchor = positiveDirection ? ANCHOR_LEFT_LOWER : ANCHOR_LEFT_UPPER; //--- Distrib anchor RenderText(distribTextUnique, entryTime, entryPrice, "Distribution", distribClr, anchor); //--- Render distrib text // Draw border rectangle (fill false) for distribution phase (% of range duration) string distribRectObj = "DistribRectangle_" + setupSuffix; //--- Distrib rect name datetime rangeStartTime = iTime(_Symbol, RangeTF, 1); //--- Range start datetime rangeEndTime = prevRangeTime; //--- Range end long duration = rangeEndTime - rangeStartTime; //--- Calc duration double projFactor = MathMax(DistribProjPct / 100.0, 0.01); //--- Proj factor datetime projEndTime = entryTime + (datetime)(duration * projFactor); //--- Proj end double topDistrib = MathMax(entryPrice, gainTarget); //--- Top distrib double bottomDistrib = MathMin(entryPrice, gainTarget); //--- Bottom distrib ObjectCreate(ChartID(), distribRectObj, OBJ_RECTANGLE, 0, entryTime, topDistrib, projEndTime, bottomDistrib); //--- Create rect ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_COLOR, distribClr); //--- Set color ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_FILL, false); //--- Set no fill ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_BACK, true); //--- Set back ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_STYLE, STYLE_SOLID); //--- Set style ObjectSetInteger(ChartID(), distribRectObj, OBJPROP_WIDTH, 2); //--- Set width ChartRedraw(ChartID()); //--- Redraw chart }
ここで、justEnteredがtrueで取引が直前に実行された場合、残りのフェーズを可視化します。オブジェクト名用の一意の接尾辞をprevRangeTimeに対してIntegerToStringで作成します。蓄積ラベル用には「AccumText_」に接尾辞を付与して一意のテキスト名を生成し、レンジの最大値と最小値の平均をmidpointPriceとして計算、ラベル時間をprevRangeTimeに設定してRenderTextを呼び出し、右アンカーに青色で「Accumulation」と表示します。操作フェーズの極値を決定するため、開始時刻prevRangeTimeとentryTimeをiBarShiftでバーインデックスに変換し、無効なら早期リターンします。開始インデックスが終了インデックスより大きい場合は入れ替え、バー数を算出します。manipStartPriceは陽線の場合はレンジ最小値、陰線の場合は最大値に設定します。陽線ではiLowestを使って終了バーからバー数分の範囲で最安値のバーを取得し、iLowで価格を取得します。陰線ではiHighest (MODE_HIGH)とiHighで最高値を取得します。
次に操作矩形を描画します。「ManipRectangle_」に接尾辞を付けて一意の名前を作成し、開始価格と極値価格の上限/下限をMathMax/MathMinで決定し、ObjectCreateでOBJ_RECTANGLEとして前回レンジ時間の上からエントリー時間の下まで矩形を作成します。色は青、塗りつぶし無効、背景として配置、スタイルは点線、幅2に設定し、ChartRedrawで描画します。次に操作ラベルは「ManipText_」に接尾辞を付与し、陽線では右上、陰線では右下アンカーを選択してbreachTimeと極値価格に青色で「Manipulation」と表示します。分配フェーズでは「DistribText_」に接尾辞を付与、陽線は緑、陰線は赤色を選び、アンカーは陽線で左下、陰線で左上としてentryTimeとエントリー価格に「Distribution」をRenderTextで表示します。分配矩形も同様の手順で作成し、ChartRedrawで描画します。以下に結果を示します。

画像から、操作フェーズと分配フェーズが明確に可視化されていることが確認できます。次におこなうのは、ポジションが有利に動いた場合にトレーリングストップで管理する処理です。この処理も関数として実装します。
//+------------------------------------------------------------------+ //| Apply Points Trailing Stop | //+------------------------------------------------------------------+ void ApplyPointsTrailing() { double point = _Point; //--- Get point for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate positions if (PositionGetTicket(i) > 0) { //--- Check ticket if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == UniqueID) { //--- Check symbol magic double sl = PositionGetDouble(POSITION_SL); //--- Get SL double tp = PositionGetDouble(POSITION_TP); //--- Get TP double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get open ulong ticket = PositionGetInteger(POSITION_TICKET); //--- Get ticket if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - Trailing_Stop_Points * point, _Digits); //--- Calc new SL if (newSL > sl && SymbolInfoDouble(_Symbol, SYMBOL_BID) - openPrice > Min_Profit_To_Trail_Points * point) { //--- Check conditions obj_Trade.PositionModify(ticket, newSL, tp); //--- Modify position } } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell double newSL = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + Trailing_Stop_Points * point, _Digits); //--- Calc new SL if (newSL < sl && openPrice - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > Min_Profit_To_Trail_Points * point) { //--- Check conditions obj_Trade.PositionModify(ticket, newSL, tp); //--- Modify position } } } } } } //--- Call the function per tick in the "OnTick" event handler if (TrailingType == Trailing_Points && PositionsTotal() > 0) { //--- Check trailing ApplyPointsTrailing(); //--- Apply trailing }
ApplyPointsTrailing関数は、ポイントベースのトレーリングストップを有効化した際にストップロスを調整するために定義します。銘柄の1ポイント値を_Pointでpointに代入します。その後、PositionsTotalですべてのポジションを逆順ループし、修正中のインデックス問題を回避します。各ポジションのチケット有効性はPositionGetTicketで確認します。銘柄が一致するポジション(PositionGetStringでPOSITION_SYMBOLを確認)かつマジックナンバーが一致する場合(PositionGetIntegerでPOSITION_MAGICを確認)、現在のストップロスはPositionGetDoubleでPOSITION_SLから取得、テイクプロフィットはPOSITION_TP、オープン価格はPOSITION_PRICE_OPEN、チケット番号はPOSITION_TICKETで取得します。買いポジション(POSITION_TYPE_BUY)の場合、現在のBid(SYMBOL_BID)をSymbolInfoDoubleで取得し、Trailing_Stop_Points × pointを引いて新しいストップロスを計算し、銘柄の小数桁に正規化します。この新しいストップロスが既存ストップロスを上回り、かつ利益(Bid − Open)がMin_Profit_To_Trail_Points × pointを超える場合、obj_Trade.PositionModifyを用いて新しいストップロスを適用し、テイクプロフィットは変更せずに更新します。
売りポジション(POSITION_TYPE_SELL)についても同様に、新しいストップロスを計算し、現在のストップロスを下回り、利益(Open − Ask)が最小閾値を満たす場合にポジションを更新します。最後に、OnTick関数内でTrailingTypeがTrailing_Pointsに設定され、PositionsTotalでポジションが存在する場合、ApplyPointsTrailingを呼び出して各ティックでトレーリングを適用します。ここで、作成したオブジェクトを初期化解除時に削除して管理します。
//+------------------------------------------------------------------+ //| EA Stop Function | //+------------------------------------------------------------------+ void OnDeinit(const int code) { ObjectDelete(ChartID(), maxLevelObj); //--- Delete max level ObjectDelete(ChartID(), minLevelObj); //--- Delete min level ObjectDelete(ChartID(), maxTextObj); //--- Delete max text ObjectDelete(ChartID(), minTextObj); //--- Delete min text // Clean dynamic rects and texts ObjectsDeleteAll(ChartID(), "RangeRectangle_", OBJ_RECTANGLE); //--- Delete range rects ObjectsDeleteAll(ChartID(), "ManipRectangle_", OBJ_RECTANGLE); //--- Delete manip rects ObjectsDeleteAll(ChartID(), "DistribRectangle_", OBJ_RECTANGLE); //--- Delete distrib rects ObjectsDeleteAll(ChartID(), "AccumText_", OBJ_TEXT); //--- Delete accum texts ObjectsDeleteAll(ChartID(), "ManipText_", OBJ_TEXT); //--- Delete manip texts ObjectsDeleteAll(ChartID(), "DistribText_", OBJ_TEXT); //--- Delete distrib texts }
OnDeinitイベントハンドラでは、プログラムがチャートから削除されたり終了した際に実行されます。まず、静的に作成したチャートオブジェクトを個別に削除します。ChartIDで現在のチャート識別子を指定し、最大レンジ水平線はmaxLevelObj、最小レンジ水平線はminLevelObj、CRT高値ラベルはmaxTextObj、CRT安値ラベルはminTextObjをObjectDeleteで削除します。
次に、動的に生成されたオブジェクトについてはObjectsDeleteAllを用いてチャート上の対象オブジェクトをまとめて削除します。蓄積フェーズ用の「RangeRectangle_」で始まるOBJ_RECTANGLE、操作フェーズ用のManipRectangle_、分配フェーズ用のDistribRectangle_をすべて削除し、さらにテキストオブジェクトも「AccumText_」(蓄積ラベル)、「ManipText_」(操作ラベル)、「DistribText_」(分配ラベル)で始まるOBJ_TEXT をすべて削除します。これにより、残留する可視要素を残さず完全にクリーンアップされます。コンパイルすると、トレーリングストップが有効な状態で次の結果が得られます。

画像から、必要に応じてトレーリングストップを適用してポジションを管理できていることが確認できます。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。
バックテスト
徹底的なバックテストによって、次の結果が得られました。
バックテストグラフ

バックテストレポート

結論
本記事ではMQL5でローソク足レンジ理論(CRT)取引システムを構築しました。このシステムは、指定された時間枠で蓄積レンジを特定し、操作深度フィルタを用いたブレイク検出、バーのクローズによる反転確認、そして分配フェーズでの取引実行をおこないます。ストップロスおよびテイクプロフィットはリスクリワード比に基づき、動的または固定で設定可能です。
免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。本プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。
蓄積、操作、分配(AMD)の各フェーズを組み込んだCRT戦略により、反転のチャンスを捉えたトレードが可能となり、さらなる最適化の余地を残した状態で取引に臨むことができます。取引をお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20323
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MetaTrader 5機械学習の設計図(第6回):実務で使えるキャッシュシステムの設計
ケンドールのタウ係数と距離相関を用いたVGTの市場ポジショニング分析コード
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
プライスアクション分析ツールキットの開発(第51回):ローソク足パターン発見のための革新的なチャート検索技術
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
ありがとう。
あなたは、それぞれの新しい蓄積段階の開始がどのように検出されるかを説明していませんでした。
それは可視化されたようにろうそくの範囲です。
これが可視化されたローソク足の範囲だ。
これは質問の答えになりません - どのように蓄積フェーズ(チャートの異なるセクションでフェーズが何度も発生するため、それぞれ、すべて)の開始を見つけるのですか?これは時間の問題であり、価格帯の問題ではない。視覚化についても同様である。