
MQL5での取引戦略の自動化(第11回):マルチレベルグリッド取引システムの開発
はじめに
前回の記事(第10回)では、MetaQuotes Language 5 (MQL5)を使用し、移動平均とモメンタムフィルターを組み合わせたトレンドフラットモメンタム戦略を自動化するエキスパートアドバイザー(EA)を開発しました。今回の第11回では、市場の変動を活用するために階層的なグリッド手法を用いたマルチレベルグリッド取引システムの構築に焦点を当てます。この記事は次のトピックに沿って構成されます。
この記事を読み終える頃には、マルチレベルグリッド取引の構造を十分に理解し、実運用に対応可能なEAを完成させることができるでしょう。それでは、さっそく始めましょう。
マルチレベルグリッドシステムのアーキテクチャを理解する
マルチレベルグリッド取引システムとは、価格レベルの範囲にわたってあらかじめ設定された間隔で一連の買い注文と売り注文を配置することで、市場のボラティリティを活用する構造的な手法です。これから実装しようとしているこの戦略は、市場の方向性を予測することを目的とするのではなく、市場が上昇、下降、または横ばいのいずれに動いても、その自然な価格変動から利益を得ることを目指します。
このコンセプトに基づいて、私たちのプログラムでは、マルチレベルグリッド戦略をモジュール化された設計を通じて実装します。この設計では、シグナルの検出、注文の実行、リスク管理を分離することで、それぞれの機能を明確に保ちます。システム開発においては、まず、取引シグナルを特定するための移動平均などの主要パラメータを初期化し、初期ロットサイズ、グリッドの間隔、利益確定(テイクプロフィット)レベルといった取引の詳細をまとめる「バスケット構造体」を設定します。
市場が変化する中で、プログラムは価格の動きを監視し、新たな取引を発動したり、既存のポジションを管理したりします。あらかじめ定めた条件に基づいて、各グリッドレベルで注文を追加し、リスクパラメータを動的に調整していきます。また、このアーキテクチャには、損益分岐価格の再計算、利益確定の目標変更、利益確定目標やリスク閾値に到達した際のポジション決済などの機能も含まれます。このような構造化された設計によって、プログラムは明確に分離された管理しやすいコンポーネントで構成され、グリッドの各レイヤーが統合的に機能する、リスク管理された取引戦略を実現します。そしてこの戦略は、堅牢なバックテストや実際の運用にも対応可能なものとなります。簡単に言えば、アーキテクチャは次のようになります。
MQL5での実装
MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲータに移動して、Indicatorsフォルダを見つけ、[新規作成]タブをクリックして、表示される手順に従ってファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用するグローバル変数をいくつか宣言する必要があります。
//+------------------------------------------------------------------+ //| Copyright 2025, Forex Algo-Trader, Allan. | //| "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 description "This EA trades multiple signals with grid strategy using baskets" #property strict #include <Trade/Trade.mqh> //--- Includes the standard trading library for executing trades CTrade obj_Trade; //--- Instantiates the CTrade object used for managing trade operations //--- Closure Mode Enumeration and Inputs enum ClosureMode { CLOSE_BY_PROFIT, //--- Use total profit (in currency) to close positions CLOSE_BY_POINTS //--- Use points threshold from breakeven to close positions }; input group "General EA Settings" input ClosureMode closureMode = CLOSE_BY_POINTS; input double inpLotSize = 0.01; input long inpMagicNo = 1234567; input int inpTp_Points = 100; input int inpGridSize = 100; input double inpMultiplier = 2.0; input int inpBreakevenPts = 50; input int maxBaskets = 5; input group "MA Indicator Settings" //--- Begins the input group for Moving Average indicator settings input int inpMAPeriod = 21; //--- Period used for the Moving Average calculation
ここでは、プログラムの基礎となるコンポーネントを構築し、シームレスな取引実行と戦略的なポジション管理を可能にします。まず、Trade/Trade.mqhライブラリをインクルードし、取引実行に必要な基本機能にアクセスできるようにします。取引操作を簡素化するために、CTradeオブジェクトをobj_Tradeとしてインスタンス化し、自動化戦略内で注文の発注、変更、決済を効率的におこなえるようにします。
次に、取引の終了方法に柔軟性を持たせるために、ClosureMode列挙型を定義します。プログラムは2つのモードで動作可能です。CLOSE_BY_PROFITは、口座通貨で累積利益が所定の閾値に達したときにすべてのポジションを決済します。一方、CLOSE_BY_POINTSは、損益分岐点からの一定の距離に基づいてポジションを決済します。これにより、市場の挙動やリスク許容度に応じて、エグジット戦略を動的に調整することが可能となります。
続いて、「General EA Settings」セクションの下に構造化されたinput項目を導入し、ユーザーが取引戦略をカスタマイズできるようにします。初期ロット数を制御するためにinpLotSizeを定義し、EAが発注する取引を他の戦略と区別するためにユニークな識別子としてinpMagicNoを使用します。グリッド方式の取引においては、各取引の利益確定レベルを決定するinpTp_Pointsを設定し、連続するグリッド注文の間隔を定めるためにinpGridSizeを定義します。inpMultiplierパラメータは、ロットサイズを段階的に拡大するために使用され、適応型のグリッド拡張を実現することで、利益の最大化とリスクのバランスを図ります。さらにリスク管理を強化するため、ある程度の利益が発生した後に損益分岐点に移行するためのinpBreakevenPtsを設定し、同時に管理可能な独立したグリッド構造の最大数を制限するmaxBasketsを用意します。
取引フィルタリングを強化するため、「MA Indicator Settings」の下で移動平均(MA)メカニズムを組み込みます。ここでは、移動平均を計算する期間数を指定するinpMAPeriodを定義します。これにより、グリッド取引が現在の市場トレンドと整合するようになり、不利な相場条件を除外し、より市場のモメンタムに沿ったエントリーが可能になります。最後に、多数のシグナルインスタンスを処理する必要があるため、バスケット構造体を定義する準備をします。
//--- Basket Structure struct BasketInfo { int basketId; //--- Unique basket identifier (e.g., 1, 2, 3...) long magic; //--- Unique magic number for this basket to differentiate its trades int direction; //--- Direction of the basket: POSITION_TYPE_BUY or POSITION_TYPE_SELL double initialLotSize; //--- The initial lot size assigned to the basket double currentLotSize; //--- The current lot size for subsequent grid trades double gridSize; //--- The next grid level price for the basket double takeProfit; //--- The current take-profit price for the basket datetime signalTime; //--- Timestamp of the signal to avoid duplicate trade entries };
ここでは、各グリッドバスケットを個別に整理・管理するために、BasketInfo構造体を定義します。各バスケットを追跡するために一意のbasketIdを割り当て、magicを使用して、他の取引と混同しないように当EAの取引を識別可能にします。directionを用いて取引の方向(買い戦略か売り戦略か)を決定します。
initialLotSizeはバスケット内で最初の取引に使用されるロットサイズを設定し、currentLotSizeはその後の取引において動的に調整されます。gridSizeによって取引間の間隔を定め、takeProfitによって利益確定の目標値を設定します。重複するエントリーを防ぐために、signalTimeを使ってシグナルのタイミングを記録します。このように構造体を定義した後、この構造体を用いた配列を宣言し、いくつかの初期グローバル変数を設定することができます。
BasketInfo baskets[]; //--- Dynamic array to store active basket information int nextBasketId = 1; //--- Counter for assigning unique IDs to new baskets long baseMagic = inpMagicNo;//--- Base magic number obtained from user input double takeProfitPts = inpTp_Points * _Point; //--- Convert take profit points into price units double gridSize_Spacing = inpGridSize * _Point; //--- Convert grid size spacing from points into price units double profitTotal_inCurrency = 100; //--- Target profit in account currency for closing positions //--- Global Variables int totalBars = 0; //--- Stores the total number of bars processed so far int handle; //--- Handle for the Moving Average indicator double maData[]; //--- Array to store Moving Average indicator data
動的配列baskets[]を使用して、アクティブなバスケット情報を格納し、複数のポジションを効率的に追跡できるようにします。nextBasketId変数は、新しいバスケットごとに一意の識別子を割り当て、baseMagicはユーザー定義のマジックナンバーを使って、システム内のすべての取引を識別可能にします。ユーザー入力を価格単位に変換するために、inpTp_PointsおよびinpGridSizeを_Pointと掛け合わせて、takeProfitPtsおよびgridSize_Spacingの精密な制御を可能にします。profitTotal_inCurrency変数は、通貨単位の決済モードを使用する際に、すべてのポジションを決済するために必要な利益の閾値を定義します。
テクニカル分析のために、totalBarsを初期化して処理済みの価格バーの数を追跡し、handleには移動平均インジケーターのハンドルを保持します。maData[]は計算された移動平均値を格納するための配列です。これらの準備が整ったところで、プログラム全体で必要に応じて使用する関数プロトタイプをいくつか定義することができます。
//--- Function Prototypes void InitializeBaskets(); //--- Prototype for basket initialization function (if used) void CheckAndCloseProfitTargets(); //--- Prototype to check and close positions if profit target is reached void CheckForNewSignal(double ask, double bid); //--- Prototype to check for new trading signals based on price bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction); //--- Prototype to execute the initial trade for a basket void ManageGridPositions(int basketIdx, double ask, double bid); //--- Prototype to manage and add grid positions for an active basket void UpdateMovingAverage(); //--- Prototype to update the Moving Average indicator data bool IsNewBar(); //--- Prototype to check whether a new bar has formed double CalculateBreakevenPrice(int basketId); //--- Prototype to calculate the weighted breakeven price for a basket void CheckBreakevenClose(int basketIdx, double ask, double bid); //--- Prototype to check and close positions based on breakeven criteria void CloseBasketPositions(int basketId); //--- Prototype to close all positions within a basket string GetPositionComment(int basketId, bool isInitial); //--- Prototype to generate a comment for a position based on basket and trade type int CountBasketPositions(int basketId); //--- Prototype to count the number of open positions in a basket
ここでは、マルチレベルグリッド取引システムの中核となる操作を定義する関数プロトタイプを定義します。これらの関数は、取引の実行、ポジション管理、およびリスク管理を効率的に構造化するために、モジュール性を確保します。まず、InitializeBaskets関数は、アクティブなバスケットを追跡するためのシステム準備をおこないます。CheckAndCloseProfitTargets関数は、あらかじめ定義された利益条件が満たされたときにポジションを決済する役割を担います。新たな取引機会を検出するために、CheckForNewSignal関数は価格レベルを評価し、新たな取引シグナルを実行すべきかを判断します。
ExecuteInitialTrade関数は、バスケット内の最初の取引を管理し、ManageGridPositions関数は、市場の動きに応じてグリッドレベルを体系的に拡張する役割を果たします。UpdateMovingAverage関数は、移動平均インジケーターのデータを取得および処理し、シグナル生成をサポートします。取引管理の観点から、IsNewBar関数は新しい価格バーが形成されたかを判定し、アクションを新しいデータに対してのみ実行することで、最適な実行タイミングを確保します。CalculateBreakevenPrice関数は、バスケットの加重損益分岐価格を計算し、CheckBreakevenCloseは、損益分岐条件が満たされたかどうかを判断してポジションを決済するかどうかを決定します。
バスケット内のポジション管理のために、CloseBasketPositions関数は、必要に応じてすべてのポジションを適切に決済する役割を担います。GetPositionComment関数は、構造化された取引コメントを提供し、取引の追跡を容易にします。CountBasketPositions関数は、バスケット内のアクティブなポジション数を監視し、システムが定義されたリスク制限内で運用されているかを確認します。
これで、まずはシグナル生成専用として使用する移動平均の初期化から開始できます。
//+------------------------------------------------------------------+ //--- Expert initialization function //+------------------------------------------------------------------+ int OnInit() { handle = iMA(_Symbol, _Period, inpMAPeriod, 0, MODE_SMA, PRICE_CLOSE); //--- Initialize the Moving Average indicator with specified period and parameters if(handle == INVALID_HANDLE) { Print("ERROR: Unable to initialize Moving Average indicator!"); //--- Log error if indicator initialization fails return(INIT_FAILED); //--- Terminate initialization with a failure code } ArraySetAsSeries(maData, true); //--- Set the moving average data array as a time series (newest data at index 0) ArrayResize(baskets, 0); //--- Initialize the baskets array as empty at startup obj_Trade.SetExpertMagicNumber(baseMagic); //--- Set the default magic number for trade operations return(INIT_SUCCEEDED); //--- Signal that initialization completed successfully }
OnInitイベントハンドラでは、まずiMA関数を使用して移動平均インジケーターを初期化します。ここでは、指定された期間およびパラメータを適用し、トレンドに基づいたデータを取得します。インジケーターのハンドルが無効(INVALID_HANDLE)であった場合は、エラーメッセージをログに出力し、INIT_FAILEDを返して初期化処理を終了します。これにより、必要なデータが取得できない状態でEAが実行されるのを防ぎます。
次に、ArraySetAsSeries関数を使って移動平均データの配列を設定し、最新の値がインデックス0に格納されるようにします。これにより、データへのアクセスが効率的になります。その後、baskets配列のサイズをゼロに変更し、新しい取引が発生した際に動的にメモリを確保できるよう準備します。最後に、SetExpertMagicNumberメソッドを使用して取引オブジェクトにベースのマジックナンバーを割り当て、EAが発注した取引を一意に追跡・管理できるようにします。すべてのコンポーネントが正常に初期化された場合は、INIT_SUCCEEDEDを返し、EAが実行の準備が整ったことを示します。
また、データを格納したため、プログラムが不要になった際には、OnDeinitイベントハンドラ内でIndicatorRelease関数を呼び出すことで、インジケーターに関連するリソースを解放できます。
//+------------------------------------------------------------------+ //--- Expert deinitialization function //+------------------------------------------------------------------+ void OnDeinit(const int reason) { IndicatorRelease(handle); //--- Release the indicator handle to free up resources when the EA is removed }
次に、OnTickイベントハンドラ内でティックごとにデータを処理していきます。ただし、プログラムは毎ティックではなく1本のバーにつき1回だけ実行されるようにしたいため、そのための関数を定義する必要があります。
//+------------------------------------------------------------------+ //--- Expert tick function //+------------------------------------------------------------------+ void OnTick() { if(IsNewBar()) { //--- Execute logic only when a new bar is detected } }
関数のプロトタイプは以下のとおりです。
//+------------------------------------------------------------------+ //--- Check for New Bar //+------------------------------------------------------------------+ bool IsNewBar() { int bars = iBars(_Symbol, _Period); //--- Get the current number of bars on the chart for the symbol and period if(bars > totalBars) { //--- Compare the current number of bars with the previously stored total totalBars = bars; //--- Update the stored bar count to the new value return true; //--- Return true to indicate a new bar has formed } return false; //--- Return false if no new bar has been detected }
ここでは、チャート上で新しいバーが形成されたかどうかを確認するためのIsNewBar関数を定義します。これは、EAが新しい価格データに基づいてのみ処理を実行し、不要な再計算を防ぐために不可欠な機能です。まず、iBars関数を使用して、現在のチャート上に存在するバーの総数を取得します。iBarsは、アクティブな通貨ペアと時間足に対して、過去のバーの数を返します。その後、この値を、以前に記録されたバーの数を保持しているtotalBars変数と比較します。
現在のバー数がtotalBarsに記録されている値より大きい場合、これは新しいバーが形成されたことを意味します。この場合、新しいバー数でtotalBarsを更新し、trueを返して、EAがバー単位の計算や取引ロジックを実行すべきタイミングであることを示します。一方、新しいバーが検出されなかった場合は、falseを返し、同じバー上での不要な処理を防ぎます
新しいバーが検出されたら、次は移動平均データを取得し、後続の処理に使用する必要があります。そのためには関数を使います。
//+------------------------------------------------------------------+ //--- Update Moving Average //+------------------------------------------------------------------+ void UpdateMovingAverage() { if(CopyBuffer(handle, 0, 1, 3, maData) < 0) { //--- Copy the latest 3 values from the Moving Average indicator buffer into the maData array Print("Error: Unable to update Moving Average data."); //--- Log an error if copying the indicator data fails } }
UpdateMovingAverage関数は、EAが移動平均インジケーターから最新の値を取得できるようにするためのものです。この関数では、CopyBuffer関数を使用して、移動平均インジケーターのバッファから直近3本分の値を抽出し、maData配列に格納します。パラメータでは、インジケーターのハンドル(handle)、バッファインデックス(メインの移動平均ラインの0)、開始位置に(現在形成中のバーをスキップするため1)、取得するデータの数(3)、結果を保存する配列(maData)を指定します。
データの取得に失敗した場合、Print関数を使ってエラーメッセージを出力し、移動平均のデータ取得に問題があったことをログに記録します。これにより、EAが不完全または欠落したデータに基づいて判断をおこなうことを避け、処理の信頼性を確保できます。この関数は、移動平均データを取得してその後のシグナル生成処理に利用するために呼び出されます。
UpdateMovingAverage(); //--- Update the Moving Average data for the current bar double ask = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK), _Digits); //--- Get and normalize the current ask price double bid = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID), _Digits); //--- Get and normalize the current bid price //--- Check for new signals and create baskets accordingly CheckForNewSignal(ask, bid);
インジケータデータを取得した後、SymbolInfoDouble関数を使用して現在のAskおよびBid価格を取得します。それぞれにSYMBOL_ASKとSYMBOL_BID定数を指定します。価格は通常、小数点以下に複数桁あるため、銘柄ごとの価格精度に合わせて正しくフォーマットする必要があります。そこで、NormalizeDouble関数と_Digitsパラメータを使って、取得した価格を適切な小数点桁数に丸めます。
最後に、正規化されたAskとBidの価格を引数としてCheckForNewSignal関数を呼び出します。以下は関数のコードスニペットです。
//+------------------------------------------------------------------+ //--- Check for New Crossover Signal //+------------------------------------------------------------------+ void CheckForNewSignal(double ask, double bid) { double close1 = iClose(_Symbol, _Period, 1); //--- Retrieve the close price of the previous bar double close2 = iClose(_Symbol, _Period, 2); //--- Retrieve the close price of the bar before the previous one datetime currentBarTime = iTime(_Symbol, _Period, 1); //--- Get the time of the current bar if(ArraySize(baskets) >= maxBaskets) return; //--- Exit if the maximum allowed baskets are already active //--- Buy signal: current bar closes above the MA while the previous closed below it if(close1 > maData[1] && close2 < maData[1]) { //--- Check if this signal was already processed by comparing signal times in existing baskets for(int i = 0; i < ArraySize(baskets); i++) { if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function } int basketIdx = ArraySize(baskets); //--- Index for the new basket equals the current array size ArrayResize(baskets, basketIdx + 1); //--- Increase the size of the baskets array to add a new basket if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_BUY)){ baskets[basketIdx].signalTime = currentBarTime; //--- Record the time of the signal after a successful trade } } //--- Sell signal: current bar closes below the MA while the previous closed above it else if(close1 < maData[1] && close2 > maData[1]) { //--- Check for duplicate signals by verifying the signal time in active baskets for(int i = 0; i < ArraySize(baskets); i++) { if(baskets[i].signalTime == currentBarTime) return; //--- Signal already acted upon; exit the function } int basketIdx = ArraySize(baskets); //--- Determine the index for the new basket ArrayResize(baskets, basketIdx + 1); //--- Resize the baskets array to accommodate the new basket if (ExecuteInitialTrade(basketIdx, ask, bid, POSITION_TYPE_SELL)){ baskets[basketIdx].signalTime = currentBarTime; //--- Record the signal time for the new sell basket } } }
CheckForNewSignal関数では、まずiClose関数を使用して、直近2本のバーの終値を取得します。これにより、クロスオーバーが発生したかどうかを判断します。また、iTime関数を使って最新バーのタイムスタンプも取得し、同じシグナルを複数回処理しないようにします。
処理を進める前に、アクティブなバスケットの数がmaxBasketsの上限に達していないかどうかを確認します。上限に達している場合、この関数は戻り、過剰な取引の積み重ねを防ぎます。買いシグナルについては、直近の終値が移動平均より上で、1つ前の終値が移動平均より下であるかを確認します。このクロスオーバーの条件が満たされていれば、既存のバスケットを繰り返し確認し、そのシグナルがすでに処理されていないかどうかをチェックします。新しいシグナルである場合、baskets配列のサイズを増やし、次に使えるインデックスに新しいバスケットを格納し、ExecuteInitialTrade関数をPOSITION_TYPE_BUYを指定して呼び出します。取引が正常に実行された場合は、シグナルのタイムスタンプを記録し、重複したエントリーを防ぎます。
売りシグナルについても同様に、直近の終値が移動平均より下で、1つ前の終値が移動平均より上であるかどうかを確認します。この条件が満たされ、かつ重複シグナルが存在しない場合、baskets配列を拡張し、ExecuteInitialTrade関数をPOSITION_TYPE_SELLを指定して呼び出し、シグナルのタイムスタンプを記録します。取引を実行する関数は以下のとおりです。
//+------------------------------------------------------------------+ //--- Execute Initial Trade //+------------------------------------------------------------------+ bool ExecuteInitialTrade(int basketIdx, double ask, double bid, int direction) { baskets[basketIdx].basketId = nextBasketId++; //--- Assign a unique basket ID and increment the counter baskets[basketIdx].magic = baseMagic + baskets[basketIdx].basketId * 10000; //--- Calculate a unique magic number for the basket baskets[basketIdx].initialLotSize = inpLotSize; //--- Set the initial lot size for the basket from input baskets[basketIdx].currentLotSize = inpLotSize; //--- Initialize current lot size to the same as the initial lot size baskets[basketIdx].direction = direction; //--- Set the trade direction (buy or sell) for the basket bool isTradeExecuted = false; //--- Initialize flag to track if the trade was successfully executed string comment = GetPositionComment(baskets[basketIdx].basketId, true); //--- Generate a comment string indicating an initial trade obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Set the trade object's magic number to the basket's unique value if(direction == POSITION_TYPE_BUY) { baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Set the grid level for subsequent buy orders below the current ask price baskets[basketIdx].takeProfit = ask + takeProfitPts; //--- Calculate the take profit level for the buy order if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) { Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY at ", ask, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful buy order details isTradeExecuted = true; //--- Mark the trade as executed successfully } else { Print("Basket ", baskets[basketIdx].basketId, ": Initial BUY failed, error: ", GetLastError()); //--- Log the error if the buy order fails ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails } } else if(direction == POSITION_TYPE_SELL) { baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Set the grid level for subsequent sell orders above the current bid price baskets[basketIdx].takeProfit = bid - takeProfitPts; //--- Calculate the take profit level for the sell order if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) { Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL at ", bid, " | Magic: ", baskets[basketIdx].magic); //--- Log the successful sell order details isTradeExecuted = true; //--- Mark the trade as executed successfully } else { Print("Basket ", baskets[basketIdx].basketId, ": Initial SELL failed, error: ", GetLastError()); //--- Log the error if the sell order fails ArrayResize(baskets, ArraySize(baskets) - 1); //--- Remove the basket if trade execution fails } } return (isTradeExecuted); //--- Return the status of the trade execution }
ExecuteInitialTrade関数は、各バスケットに一意の識別子を割り当て、個別のマジックナンバーを設定し、注文を発注する前に主要な取引パラメータを初期化するために定義されます。まず、nextBasketId変数をインクリメントしてbasketIdを割り当てます。次に、baseMagicにスケーリングされたオフセットを加えることで、バスケットごとにユニークなマジックナンバーを生成し、それぞれのバスケットが独立して動作するようにします。初期ロットサイズおよび現在のロットサイズはともにinpLotSizeに設定し、バスケットにおける基本の取引サイズを確立します。また、directionを記録することで、買いバスケットか売りバスケットかを区別します。
取引が識別可能であるようにするため、GetPositionComment関数を呼び出して説明的なコメントを生成し、バスケットのマジックナンバーをSetExpertMagicNumberメソッドを通じて取引オブジェクトに適用します。関数は以下のように定義されており、StringFormat関数で三項演算子を使用してコメントを生成しています。
//+------------------------------------------------------------------+ //--- Generate Position Comment //+------------------------------------------------------------------+ string GetPositionComment(int basketId, bool isInitial) { return StringFormat("Basket_%d_%s", basketId, isInitial ? "Initial" : "Grid"); //--- Generate a standardized comment string for a position indicating basket ID and trade type }
directionがPOSITION_TYPE_BUYの場合、グリッドレベルはAsk価格からgridSize_Spacingを差し引いて計算され、利益確定レベルはAsk価格にtakeProfitPtsを加算して決定されます。その後、CTradeクラスのBuy関数を使用して注文を出します。成功した場合、Print関数を使って取引の詳細をログに記録し、取引が実行されたことをマークします。取引が失敗した場合は、GetLastError関数でエラーを取得してログに記録し、ArrayResize関数を使用してbaskets配列のサイズを縮小し、失敗したバスケットを削除します。
売り取引(POSITION_TYPE_SELL)の場合、グリッドレベルをBid価格にgridSize_Spacingを加えて計算し、利益確定レベルはBid価格からtakeProfitPtsを差し引いて決定します。取引はSell関数を使用して実行されます。買い注文と同様、取引が成功した場合はPrint関数でログを記録し、失敗した場合はGetLastError を用いてエラーを出力し、ArrayResizeでbaskets配列から該当バスケットを削除します。
いずれの取引も実行する前に、配列のサイズが十分であることを確認するためにArrayResizeを呼び出してサイズを拡張します。最後に、取引が正常に実行された場合はtrueを返し、失敗した場合はfalseを返します。プログラムを実行すると、次の結果が得られます。
画像から、バスケットやシグナルに従って初期ポジションが確定されていることが確認できます。次に、これらのポジションを個別に管理していく必要があります。そのために、各バスケットを個別に処理するための繰り返し処理としてforループを使用します。
//--- Loop through all active baskets to manage grid positions and potential closures for(int i = 0; i < ArraySize(baskets); i++) { ManageGridPositions(i, ask, bid); //--- Manage grid trading for the current basket }
ここでは、forループを使ってアクティブなすべてのバスケットを順番に処理し、それぞれのバスケットを適切に管理します。ArraySize関数はbaskets配列の現在の要素数を取得し、ループの上限を決定します。これにより、配列の範囲を超えることなく、存在するすべてのバスケットを処理できるようになります。各バスケットに対しては、インデックス番号と正規化されたask、bid価格を引数としてManageGridPositions関数を呼び出します。関数は次のとおりです。
//+------------------------------------------------------------------+ //--- Manage Grid Positions //+------------------------------------------------------------------+ void ManageGridPositions(int basketIdx, double ask, double bid) { bool newPositionOpened = false; //--- Flag to track if a new grid position has been opened string comment = GetPositionComment(baskets[basketIdx].basketId, false); //--- Generate a comment for grid trades in this basket obj_Trade.SetExpertMagicNumber(baskets[basketIdx].magic); //--- Ensure the trade object uses the basket's unique magic number if(baskets[basketIdx].direction == POSITION_TYPE_BUY) { if(ask <= baskets[basketIdx].gridSize) { //--- Check if the ask price has reached the grid level for a buy order baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the defined multiplier if(obj_Trade.Buy(baskets[basketIdx].currentLotSize, _Symbol, ask, 0, baskets[basketIdx].takeProfit, comment)) { newPositionOpened = true; //--- Set flag if the grid buy order is successfully executed Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY at ", ask); //--- Log the grid buy execution details baskets[basketIdx].gridSize = ask - gridSize_Spacing; //--- Adjust the grid level for the next potential buy order } else { Print("Basket ", baskets[basketIdx].basketId, ": Grid BUY failed, error: ", GetLastError()); //--- Log an error if the grid buy order fails } } } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) { if(bid >= baskets[basketIdx].gridSize) { //--- Check if the bid price has reached the grid level for a sell order baskets[basketIdx].currentLotSize *= inpMultiplier; //--- Increase the lot size based on the multiplier for grid orders if(obj_Trade.Sell(baskets[basketIdx].currentLotSize, _Symbol, bid, 0, baskets[basketIdx].takeProfit, comment)) { newPositionOpened = true; //--- Set flag if the grid sell order is successfully executed Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL at ", bid); //--- Log the grid sell execution details baskets[basketIdx].gridSize = bid + gridSize_Spacing; //--- Adjust the grid level for the next potential sell order } else { Print("Basket ", baskets[basketIdx].basketId, ": Grid SELL failed, error: ", GetLastError()); //--- Log an error if the grid sell order fails } } } //--- If a new grid position was opened and there are multiple positions, adjust the take profit to breakeven if(newPositionOpened && CountBasketPositions(baskets[basketIdx].basketId) > 1) { double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the weighted breakeven price for the basket double newTP = (baskets[basketIdx].direction == POSITION_TYPE_BUY) ? breakevenPrice + (inpBreakevenPts * _Point) : //--- Set new TP for buy positions breakevenPrice - (inpBreakevenPts * _Point); //--- Set new TP for sell positions baskets[basketIdx].takeProfit = newTP; //--- Update the basket's take profit level with the new value for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to update TP where necessary ulong ticket = PositionGetTicket(j); //--- Get the ticket number for the current position if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == baskets[basketIdx].magic) { //--- Identify positions that belong to the current basket if(!obj_Trade.PositionModify(ticket, 0, newTP)) { //--- Attempt to modify the position's take profit level Print("Basket ", baskets[basketIdx].basketId, ": Failed to modify TP for ticket ", ticket); //--- Log error if modifying TP fails } } } Print("Basket ", baskets[basketIdx].basketId, ": Breakeven = ", breakevenPrice, ", New TP = ", newTP); //--- Log the new breakeven and take profit levels } }
ここでは、アクティブな各バスケット内でグリッド取引を動的に管理するためのManageGridPositions関数を実装します。適切な価格レベルで新しいグリッドポジションを実行し、必要に応じて利益調整をおこなうことを目的としています。まず、新しいグリッド取引が実行されたかどうかを追跡するために、newPositionOpenedフラグを初期化します。GetPositionComment関数を使って、取引タイプ(初期またはグリッド)に応じたコメント文字列を生成し、SetExpertMagicNumber関数でバスケット固有のマジックナンバーを割り当て、バスケット内の全取引が正しく追跡されるようにします。
買いバスケットの場合、Ask価格がgridSizeの閾値以下に下落したかどうかをチェックします。この条件が満たされた場合、currentLotSizeにinpMultiplierを掛けてロットサイズを調整し、obj_TradeのBuyメソッドで買い注文を試みます。取引が成功した場合は、gridSizeをgridSize_Spacing分だけ減らして次の買い取引の位置を設定し、Printで成功をログに記録します。注文が失敗した場合は、GetLastErrorでエラーを取得してログに出力します。
売りバスケットの場合も同様の処理をおこないますが、Bid価格がgridSizeの閾値以上に上昇したかどうかを確認します。条件を満たした場合は、currentLotSizeにinpMultiplierを掛けてロットサイズを調整し、Sell関数で売り注文を実行します。成功したらgridSizeにgridSize_Spacingを加算して次の売りレベルを設定し、Print関数で成功ログを記録します。失敗した場合はGetLastError関数を使用してエラーをログに残します。
新たにグリッドポジションが追加され、バスケットが複数のポジションを保有するようになった場合、利益確定レベルを損益分岐レベルに調整します。まず、CalculateBreakevenPrice関数を呼び出して利益確定価格を算出します。その後、バスケットの方向に応じて新たな利益確定レベルを計算します。
- 買いバスケットの場合、利益確定は損益分岐価格にinpBreakevenPts(価格ポイントに変換)を加算することによって設定されます。
- 売りバスケットの場合、損益分岐価格からinpBreakevenPtsを差し引くことで利益確定額が調整されます。
次に、PositionsTotal関数を使用してすべてのポジションをループ処理し、それぞれのチケット番号をPositionGetTicket関数で取得します。PositionSelectByTicket関数でポジションを選択し、PositionGetString関数でその銘柄を確認します。さらに、POSITION_MAGICパラメータで正しいバスケットに属しているかどうかを確認します。確認できたら、PositionModifyメソッドで利益確定レベルを修正します。この修正に失敗した場合はエラーをログに記録します。
最後に、計算した損益分岐価格と更新後の利益確定レベルをPrint関数でログに出力します。これにより、グリッド取引戦略は動的に適応しつつ、効率的な決済ポイントを維持します。平均価格を計算する関数は次のとおりです。
//+------------------------------------------------------------------+ //--- Calculate Weighted Breakeven Price for a Basket //+------------------------------------------------------------------+ double CalculateBreakevenPrice(int basketId) { double weightedSum = 0.0; //--- Initialize sum for weighted prices double totalLots = 0.0; //--- Initialize sum for total lot sizes for(int i = 0; i < PositionsTotal(); i++) { //--- Loop over all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position if(PositionSelectByTicket(ticket) && PositionGetString(POSITION_SYMBOL) == _Symbol && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket double lot = PositionGetDouble(POSITION_VOLUME); //--- Get the lot size of the position double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Get the open price of the position weightedSum += openPrice * lot; //--- Add the weighted price to the sum totalLots += lot; //--- Add the lot size to the total lots } } return (totalLots > 0) ? (weightedSum / totalLots) : 0; //--- Return the weighted average price (breakeven) or 0 if no positions found }
CalculateBreakevenPrice関数は、指定されたバスケット内のすべてのポジションの取引数量で加重平均した損益分岐価格を計算し、利益確定レベルを動的に調整できるように実装します。まず、加重価格の合計を格納するためのweightedSumと、バスケット内のすべてのポジションの合計ロット数を追跡するためのtotalLotsを初期化します。次に、すべてのポジションを反復処理します。
各ポジションについては、PositionGetTicket関数でチケット番号を取得し、PositionSelectByTicketでそのポジションを選択します。現在の取引銘柄に属しているかどうかを確認し、さらにポジションのコメント文字列に対してStringFind関数を用いてバスケットIDを含むかどうかをチェックします。コメントには「"Basket_"+IntegerToString(basketId)」が含まれている必要があり、これにより同一バスケットのポジションであることが判別されます。
ポジションが検証されたら、PositionGetDoubleを使用し、POSITION_VOLUMEでロットサイズを、POSITION_PRICE_OPEN始値を取得します。始値にロットサイズを掛けた値をweightedSumに加算し、ロット数が大きいポジションほど損益分岐価格に大きな影響を与えるようにします。同時に、ロットサイズの合計をtotalLotsに加算します。
すべてのポジションをループ処理した後、weightedSumをtotalLotsで割ることで加重平均の損益分岐価格を算出します。バスケット内にポジションが存在しない場合(totalLotsが0の場合)は、ゼロ除算エラーを防ぐために0を返します。プログラムを実行すると、次の結果が得られます。
画像から、バスケットがそれぞれ独立して管理されており、グリッドの追加や価格の平均化がおこなわれていることが確認できます。たとえば、バスケット2では、すべてのポジションの利益確定レベルが0.68074に統一されています。このことは、以下のように操作ログからも確認できます。
画像から、バスケット4に対してグリッドの買いポジションが新たに追加されると、同時に利益確定価格も修正されていることが確認できます。すでに利益確定レベルは調整されていますが、安全のために(必須ではありませんが)設定したモードに基づいてポジションを決済する処理をおこなう必要があります。以下のように対応します。
if(closureMode == CLOSE_BY_PROFIT) CheckAndCloseProfitTargets(); //--- If using profit target closure mode, check for profit conditions if(closureMode == CLOSE_BY_POINTS && CountBasketPositions(baskets[i].basketId) > 1) { CheckBreakevenClose(i, ask, bid); //--- If using points-based closure and multiple positions exist, check breakeven conditions }
ここでは、選択されたclosureModeに基づいて取引の決済処理をおこないます。closureModeがCLOSE_BY_PROFITに設定されている場合は、CheckAndCloseProfitTargets関数を呼び出し、利益目標に達したバスケットを決済します。CLOSE_BY_POINTSに設定されている場合は、まずCountBasketPositions関数を使ってバスケットに複数のポジションが存在することを確認し、条件を満たしていればCheckBreakevenClose関数を呼び出して、損益分岐価格での取引決済を実行します。関数は以下の通りです。
//+------------------------------------------------------------------+ //--- Check and Close Profit Targets (for CLOSE_BY_PROFIT mode) //+------------------------------------------------------------------+ void CheckAndCloseProfitTargets() { for(int i = 0; i < ArraySize(baskets); i++) { //--- Loop through each active basket int posCount = CountBasketPositions(baskets[i].basketId); //--- Count how many positions belong to the current basket if(posCount <= 1) continue; //--- Skip baskets with only one position as profit target checks apply to multiple positions double totalProfit = 0; //--- Initialize the total profit accumulator for the basket for(int j = PositionsTotal() - 1; j >= 0; j--) { //--- Loop through all open positions to sum their profits ulong ticket = PositionGetTicket(j); //--- Get the ticket for the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(baskets[i].basketId)) >= 0) { //--- Check if the position is part of the current basket totalProfit += PositionGetDouble(POSITION_PROFIT); //--- Add the position's profit to the basket's total profit } } if(totalProfit >= profitTotal_inCurrency) { //--- Check if the accumulated profit meets or exceeds the profit target Print("Basket ", baskets[i].basketId, ": Profit target reached (", totalProfit, ")"); //--- Log that the profit target has been reached for the basket CloseBasketPositions(baskets[i].basketId); //--- Close all positions in the basket to secure the profits } } }
ここでは、CLOSE_BY_PROFITモードにおいて、バスケットが利益目標に到達した場合に決済処理を実行します。basketsをループで走査し、それぞれに対してCountBasketPositions関数を使って複数のポジションが存在することを確認します。その後、該当バスケット内のすべてのポジションについてPositionGetDouble(POSITION_PROFIT)を使用して利益を合計します。合計利益がprofitTotal_inCurrencyに達するか、それを上回った場合は、イベントをログに記録し、CloseBasketPositions関数を呼び出して利益を確保します。CountBasketPositions関数は以下のように定義されています。
//+------------------------------------------------------------------+ //--- Count Positions in a Basket //+------------------------------------------------------------------+ int CountBasketPositions(int basketId) { int count = 0; //--- Initialize the counter for positions in the basket for(int i = 0; i < PositionsTotal(); i++) { //--- Loop through all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket for the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Check if the position belongs to the specified basket count++; //--- Increment the counter if a matching position is found } } return count; //--- Return the total number of positions in the basket }
CountBasketPositions関数は、特定のバスケットに含まれるポジション数をカウントするために使用します。すべてのポジションをループ処理し、PositionGetTicket関数で各ポジションのチケット番号を取得します。その後、POSITION_COMMENTにバスケットIDが含まれているかどうかを確認します。一致する場合は、countをインクリメントします。最後に、そのバスケットに属するポジションの総数を返します。CloseBasketPositions関数の定義は次のとおりです。
//+------------------------------------------------------------------+ //--- Close All Positions in a Basket //+------------------------------------------------------------------+ void CloseBasketPositions(int basketId) { for(int i = PositionsTotal() - 1; i >= 0; i--) { //--- Loop backwards through all open positions ulong ticket = PositionGetTicket(i); //--- Retrieve the ticket of the current position if(PositionSelectByTicket(ticket) && StringFind(PositionGetString(POSITION_COMMENT), "Basket_" + IntegerToString(basketId)) >= 0) { //--- Identify if the position belongs to the specified basket if(obj_Trade.PositionClose(ticket)) { //--- Attempt to close the position Print("Basket ", basketId, ": Closed position ticket ", ticket); //--- Log the successful closure of the position } } } }
同じロジックを用いて、すべてのポジションをループ処理し、それぞれを検証したうえで、PositionCloseメソッドを使って決済します。最後に、定義された目標レベルを超えたときにポジションを強制的に決済する役割を持つ関数が続きます。
//+------------------------------------------------------------------+ //--- Check Breakeven Close //+------------------------------------------------------------------+ void CheckBreakevenClose(int basketIdx, double ask, double bid) { double breakevenPrice = CalculateBreakevenPrice(baskets[basketIdx].basketId); //--- Calculate the breakeven price for the basket if(baskets[basketIdx].direction == POSITION_TYPE_BUY) { if(bid >= breakevenPrice + (inpBreakevenPts * _Point)) { //--- Check if the bid price exceeds breakeven plus threshold for buy positions Print("Basket ", baskets[basketIdx].basketId, ": Closing BUY positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket } } else if(baskets[basketIdx].direction == POSITION_TYPE_SELL) { if(ask <= breakevenPrice - (inpBreakevenPts * _Point)) { //--- Check if the ask price is below breakeven minus threshold for sell positions Print("Basket ", baskets[basketIdx].basketId, ": Closing SELL positions at breakeven + points"); //--- Log that breakeven condition is met for closing positions CloseBasketPositions(baskets[basketIdx].basketId); //--- Close all positions for the basket } } }
ここでは、CheckBreakevenClose関数を用いて損益分岐点に基づいた決済処理を実装します。まず、CalculateBreakevenPrice関数を使って損益分岐価格を算出します。バスケットが買い方向の場合、Bid価格が損益分岐価格に「inpBreakevenPts *_Point」を加えた値を上回ったときに、イベントをログに記録し、CloseBasketPositions関数を呼び出して利益を確定させます。同様に、売りバスケットでは、Ask価格が損益分岐価格から閾値を引いたレベルを下回った場合に決済を実行します。これにより、価格が損益分岐条件に一致した際に、ポジションが確実に決済されるようになります。
最後に、利益確定によってポジションが決済されたことで、実体のない「空のシェル(バスケット)」がシステム内に残ってしまいます。これらの要素を整理してクリーンな状態を保つため、要素を含まない空のバスケットを識別し、削除する必要があります。このために、以下のロジックを実装します。
//--- Remove inactive baskets that no longer have any open positions for(int i = ArraySize(baskets) - 1; i >= 0; i--) { if(CountBasketPositions(baskets[i].basketId) == 0) { Print("Removing inactive basket ID: ", baskets[i].basketId); //--- Log the removal of an inactive basket for(int j = i; j < ArraySize(baskets) - 1; j++) { baskets[j] = baskets[j + 1]; //--- Shift basket elements down to fill the gap } ArrayResize(baskets, ArraySize(baskets) - 1); //--- Resize the baskets array to remove the empty slot } }
ここでは、すでにポジションを保有していない非アクティブなバスケットを効率的に削除できるように処理をおこないます。baskets配列を逆順でループ処理することで、要素削除時のインデックスずれを回避します。各バスケットについて、CountBasketPositions関数を使用してポジションが残っているかどうかを確認します。空のバスケットが見つかった場合には、削除をログに記録し、配列の後続要素を前方にシフトさせて構造を維持します。最後に、ArrayResizeを使って配列のサイズを調整することで、不要なメモリ使用を防ぎ、アクティブなバスケットのみが追跡される状態を確保します。このアプローチにより、バスケット管理の効率が保たれ、システム内の無駄が排除されます。実行すると、次の結果が得られます。
画像から、不要なバスケットの削除が効率的に行われており、グリッドポジションの管理も適切に実施されていることが確認できます。これにより、本稿の目的を達成できたと言えます。残された作業はプログラムのバックテストであり、それについては次のセクションで取り扱います。
バックテスト
2023年の1年間にわたるデフォルト設定での詳細なバックテストの結果は、以下のとおりです。
バックテストグラフ
バックテストレポート
結論
まとめとして、私たちはマルチレベルグリッド取引を効率的に管理するMQL5 EAを開発しました。本システムは、階層化された取引エントリー、動的なグリッド調整、および構造化されたリカバリー機能を備えています。スケーラブルなグリッド間隔、制御されたロットの増加、損益分岐点での決済を組み合わせることで、市場の変動に柔軟に対応しながら、リスクとリターンの最適化を実現しています。
免責条項:本記事は教育目的のみを意図したものです。取引には大きな財務リスクが伴い、市場の状況は予測できない場合があります。実際の運用前には十分なバックテストとリスク管理が不可欠です。
これらの技術を活用することで、アルゴリズム取引のスキルを向上させ、グリッドベースの戦略をさらに洗練させることができます。長期的な成功を目指して、継続的にテストと最適化をおこなってください。ご健闘をお祈りします。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/17350





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
非常に優れたコードで、非常に高速なEAです!
残念ながら、ロットサイズの計算に問題があります - 小数を含む乗数(1.3、1.5など)は、乗数が1または2でない場合にロットサイズがエラーコード 4756を与えることがあるため、MQL注文関数で問題を引き起こす可能性があります。
ロットサイズの計算をわずかに変更し、すべての乗数値に対してオーダー関数に入力するロットサイズが適切に計算されるようにできれば、あまりに素晴らしいことです。
ロットサイズの計算をわずかに変更し、すべての乗数値に対してオーダー関数に入力するロットサイズが適切に計算されるようにできれば、あまりに素晴らしいことです。
親切なフィードバックに感謝します。もちろんです。
こんにちは、
記事を読んで、有用であることがわかりました。しかし、最初のポジションのTPの分離について、私は見ていないか、あるいは見逃しているようです。
ありがとうございました。
こんにちは、
記事を読んで、有用であることがわかりました。しかし、最初のポジションのTPの分離については、私は見ていないか、あるいは見逃しているようです。
ありがとうございました。
もちろんです。