MQL5での取引戦略の自動化(第23回):トレーリングとバスケットロジックによるゾーンリカバリ
はじめに
前回の記事(第22回)では、MetaQuotes Language 5 (MQL5)を用いて、RSI (Relative Strength Index)とEnvelopesを活用したゾーンリカバリー(Zone Recovery)システムを開発し、自動取引と構造化されたリカバリーゾーンによる損失管理をおこないました。第23回では、この戦略をさらに改良し、利益を動的に確保するためのトレーリングストップと、複数の取引シグナルを効率的に処理するマルチバスケットシステムを組み込み、変動の大きい市場環境への適応性を高めます。本記事では以下のトピックを扱います。
これを通じて、テストおよびさらなるカスタマイズに対応可能な高度なMQL5取引システムを完成させます。それでは始めましょう。
強化されたトレーリングストップとマルチバスケットアーキテクチャの理解
私たちが強化しているゾーンリカバリー戦略は、市場が逆行した場合に定義された価格範囲内でカウンタ取引をおこない、潜在的な損失を利益に変えることを目的としています。今回、これを2つの重要な要素で強化します。トレーリングストップとマルチバスケット取引です。トレーリングストップは、市場が有利に動いた際に利益を確定するために不可欠です。これにより、取引を早期にクローズせず、利益を保護できます。これは、特に価格変動の大きいトレンド相場では非常に重要です。マルチバスケット取引も同様に重要で、複数の独立した取引シグナルを同時に管理できるため、より多くのチャンスを捉えつつ、各取引グループごとにリスクを整理して管理できます。以下を参照してください。

これらの改良は、市場の動きに応じてストップロスレベルを動的に調整するトレーリングストップ機構を統合することで実現します。これにより、取引の成長余地を保ちながら利益を確保できます。マルチバスケット取引では、各取引インスタンスに固有の識別子を付与するシステムを導入し、複数のゾーンリカバリーシステムサイクルを同時に重複なく追跡し、管理できるようにします。これらの機能を既存の相対力指数(RSI)およびエンベロープインジケーターを用いた精度の高い取引エントリーと組み合わせ、トレーリングストップとバスケットシステムが連携して利益保護と取引能力を最適化します。これにより、戦略全体がより堅牢になり、さまざまな市場条件に適応可能となります。これらの改良を実際に実装していきましょう。
MQL5での実装
MQL5でこれらの改良を実装するために、トレーリングストップ機能用の追加ユーザー入力を設定し、複数のリカバリインスタンスを扱うために、最大保有注文数の制限も名称を変更します。
input group "======= EA GENERAL SETTINGS =======" input TradingLotSizeOptions lotOption = UNFIXED_LOTSIZE; // Lot Size Option input double initialLotSize = 0.01; // Initial Lot Size input double riskPercentage = 1.0; // Risk Percentage (%) input int riskPoints = 300; // Risk Points input int baseMagicNumber = 123456789; // Base Magic Number input int maxInitialPositions = 1; // Maximum Initial Positions (Baskets/Signals) input double zoneTargetPoints = 600; // Zone Target Points input double zoneSizePoints = 300; // Zone Size Points input bool enableInitialTrailing = true; // Enable Trailing Stop for Initial Positions input int trailingStopPoints = 50; // Trailing Stop Points input int minProfitPoints = 50; // Minimum Profit Points to Start Trailing
MQL5での Envelopes Trend取引向けゾーンリカバリーシステムの強化は、まず「EA GENERAL SETTINGS」グループの入力パラメータを更新し、トレーリングストップおよびマルチバスケット取引に対応させることから始めます。入力パラメータには、以下の4つの重要な変更を加えます。magicNumberをbaseMagicNumberに名称変更し、123456789に設定します。これにより、複数のトレードバスケット用に一意のマジックナンバーを生成する出発点となり、マルチバスケットシステムで各バスケットを個別に追跡できます。次に、maxOrdersをmaxInitialPositionsに置き換え、1に設定します。これにより、初期のバスケット数を制限し、複数の取引シグナルを効率的に管理できるようにします。
3番目に、enableInitialTrailingを追加し、ブール値をtrueに設定します。これにより、初期ポジションに対してトレーリングストップを有効または無効にすることができ、新しい利益確保機能を制御可能にします。4番目に、trailingStopPointsを50、minProfitPointsを50に設定します。これにより、トレーリングストップの距離と、トレーリングを有効化するために必要な最小利益を定義し、動的な利益保護を実装します。これらの変更により、システムは複数のバスケットを管理し、利益を効果的に保護できるようになり、さらなる強化の基盤が整います。変更点を明確に示すことで、追跡を容易にし、混乱を避けます。コンパイル後、以下の入力セットが得られます。

入力を追加した後、MarketZoneTraderクラスを前方宣言しておくことで、基盤クラスからアクセスできるようにします。これは、複数のトレードインスタンスを扱うために必要です。
//--- Forward Declaration of MarketZoneTrader class MarketZoneTrader;
ここでは、MarketZoneTraderクラスの前方宣言を導入します。この宣言は、BasketManagerクラス定義の前に追加します。BasketManagerクラスは、このクラスの直後に定義する予定で、MarketZoneTraderを参照できるようにするためです。完全なクラス定義がまだなくても参照できるようにするため、この前方宣言が必要です。この変更は、マルチバスケットシステムの新機能に必須です。BasketManagerが管理するマルチバスケットシステムにおいては、異なるバスケット取引用に複数のMarketZoneTraderインスタンスを作成し、管理する必要があります。先にMarketZoneTraderを宣言することで、新しいクラス内で使用した際にコンパイラが認識でき、複数の同時取引サイクルを効率的にサポートできるようになります。その後、マネージャークラスを定義することができます。
//--- Basket Manager Class to Handle Multiple Traders class BasketManager { private: MarketZoneTrader* m_traders[]; //--- Array of trader instances int m_handleRsi; //--- RSI indicator handle int m_handleEnvUpper; //--- Upper Envelopes handle int m_handleEnvLower; //--- Lower Envelopes handle double m_rsiBuffer[]; //--- RSI data buffer double m_envUpperBandBuffer[]; //--- Upper Envelopes buffer double m_envLowerBandBuffer[]; //--- Lower Envelopes buffer string m_symbol; //--- Trading symbol int m_baseMagicNumber; //--- Base magic number int m_maxInitialPositions; //--- Maximum baskets (signals) //--- Initialize Indicators bool initializeIndicators() { m_handleRsi = iRSI(m_symbol, PERIOD_CURRENT, 8, PRICE_CLOSE); if (m_handleRsi == INVALID_HANDLE) { Print("Failed to initialize RSI indicator"); return false; } m_handleEnvUpper = iEnvelopes(m_symbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); if (m_handleEnvUpper == INVALID_HANDLE) { Print("Failed to initialize upper Envelopes indicator"); return false; } m_handleEnvLower = iEnvelopes(m_symbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); if (m_handleEnvLower == INVALID_HANDLE) { Print("Failed to initialize lower Envelopes indicator"); return false; } ArraySetAsSeries(m_rsiBuffer, true); ArraySetAsSeries(m_envUpperBandBuffer, true); ArraySetAsSeries(m_envLowerBandBuffer, true); return true; } }
バスケット取引の管理を容易にするために、MarketZoneTraderクラスの複数のインスタンスとインジケーターデータを管理するためのprivateメンバーを持つBasketManagerクラスを定義します。まず、個々のバスケット取引を保持するために、MarketZoneTraderポインタの配列「m_traders」を作成します。各バスケットは独立したゾーンリカバリーサイクルを表します。この変更は非常に重要であり、前バージョンの単一インスタンス方式とは異なり、複数の取引シグナルを同時に管理できるようになります。また、RSIおよびエンベロープのインジケーターハンドルを保持するために、m_handleRsi、m_handleEnvUpper、m_handleEnvLowerを宣言します。RSIおよびエンベロープのデータを格納するために、m_rsiBuffer、m_envUpperBandBuffer、m_envLowerBandBuffer配列を用意し、インジケーターの管理をMarketZoneTraderからBasketManagerに移すことで、バスケット全体で一元的に制御できるようにします。
さらに、取引銘柄を保持するm_symbol、各バスケットごとに一意のマジックナンバーを生成するためのm_baseMagicNumber、アクティブなバスケット数を制限するm_maxInitialPositionsを追加します。これらは新しい入力パラメータ「maxInitialPositions」に対応しています。nitializeIndicators関数では、RSIインジケーターをiRSI関数で期間8に設定し、EnvelopesインジケーターをiEnvelopes関数で上限バンドを期間150・偏差0.1、下限バンドを期間95・偏差1.4で設定します。いずれもINVALID_HANDLEをチェックし、失敗時にはPrint関数でログ出力をおこないます。また、m_rsiBuffer、m_envUpperBandBuffer、m_envLowerBandBuffer配列をArraySetAsSeries関数を使用して時系列配列として設定します。この新しいクラス構造により、複数のトレードバスケットを効率的に連携し、管理でき、全バスケットでインジケーターデータを集中管理することで、一貫したシグナル生成が可能になります。続いて、各バスケットのポジションをカウントしやすくするためのロジックを追加し、不要なバスケットをクリーンアップする処理を実装します。
//--- Count Active Baskets int countActiveBaskets() { int count = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() != MarketZoneTrader::INACTIVE) { count++; } } return count; } //--- Cleanup Terminated Baskets void cleanupTerminatedBaskets() { int newSize = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL && m_traders[i].getCurrentState() == MarketZoneTrader::INACTIVE) { delete m_traders[i]; m_traders[i] = NULL; } if (m_traders[i] != NULL) newSize++; } MarketZoneTrader* temp[]; ArrayResize(temp, newSize); int index = 0; for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { temp[index] = m_traders[i]; index++; } } ArrayFree(m_traders); ArrayResize(m_traders, newSize); for (int i = 0; i < newSize; i++) { m_traders[i] = temp[i]; } ArrayFree(temp); }
ここでは、BasketManagerクラスに新たにcountActiveBaskets関数とcleanupTerminatedBaskets関数の2つを追加します。まず、アクティブなバスケット取引の数を追跡するためにcountActiveBaskets関数を作成します。count変数を0に初期化し、ArraySize関数を使用してm_traders配列をループ処理します。各m_tradersの要素がNULLでない場合、その要素の状態をgetCurrentStateを介して取得し、その値がMarketZoneTrader::INACTIVEでないかを確認します。アクティブな場合は、countをインクリメントします。最後にcountを返すことで、現在稼働中のバスケット数を監視できるようにします。これは、新しいバスケットを開く際に、m_maxInitialPositionsの制限内に収まっていることを保証するために重要です。
次に、非アクティブなバスケットを削除してメモリを最適化するためのcleanupTerminatedBaskets関数を作成します。まず、m_traders配列をループしてNULLでないエントリをカウントします。各トレーダーがNULLでなく、かつgetCurrentStateの戻り値がMarketZoneTrader::INACTIVEの場合、deleteを使用してメモリを解放し、そのエントリをNULLに設定します。残っているNULLでないトレーダーの数をnewSizeで追跡します。次に、一時配列tempを作成し、ArrayResize関数でnewSizeにリサイズします。m_tradersからtempにNULLでないトレーダーをindexカウンタを使用してコピーします。その後、m_tradersをArrayFreeでクリアし、newSizeにリサイズして、tempからトレーダーを戻します。最後に、ArrayFreeを使用してtempを解放します。このクリーンアップ処理により、終了したバスケットが確実に削除され、システムは効率的な状態を維持し、新しい取引にすぐ対応できるようになります。続いて、publicアクセス修飾子のセクションに移り、クラスメンバーや要素を初期化・破棄する際のコンストラクタおよびデストラクタの扱いを変更します。
public: BasketManager(string symbol, int baseMagic, int maxInitPos) { m_symbol = symbol; m_baseMagicNumber = baseMagic; m_maxInitialPositions = maxInitPos; ArrayResize(m_traders, 0); m_handleRsi = INVALID_HANDLE; m_handleEnvUpper = INVALID_HANDLE; m_handleEnvLower = INVALID_HANDLE; } ~BasketManager() { for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) delete m_traders[i]; } ArrayFree(m_traders); cleanupIndicators(); }
まず、BasketManagerのコンストラクタを定義します。このコンストラクタはsymbol、baseMagic、maxInitPosの3つのパラメータを受け取ります。これらをそれぞれm_symbol、m_baseMagicNumber、m_maxInitialPositionsに代入し、取引シンボル、一意のバスケット識別用ベースマジックナンバー、そしてアクティブなバスケットの最大数を設定します。 ArrayResize関数を使用してm_traders配列をサイズ0に初期化し、インジケーターセットアップの準備として、m_handleRsi、m_handleEnvUpper、m_handleEnvLowerをINVALID_HANDLEに設定します。このコンストラクタは、マルチバスケットシステムを構成するために非常に重要な要素です。
次に、リソースをクリーンアップするためのデストラクタ「~BasketManagerを」作成します。一般的に、デストラクタはチルダ(~)を接頭辞として持つことを、ここで改めて確認しておきます。ArraySize関数を使用してm_traders配列をループし、NULLでないMarketZoneTraderインスタンスをdeleteで削除してメモリを解放します。続いて、ArrayFreeでm_traders配列をクリアし、cleanupIndicatorsを呼び出してインジケーターハンドルおよびバッファを解放します。これにより、EA停止時にシステムがクリーンに終了し、メモリリークを防止することができます。前バージョンでは、メモリリークが発生していることが判明した後に、削除ロジックをOnDeinitイベントハンドラ内に直接追加する必要がありましたが、今回は最初からメモリリーク対策が必要であると分かっているため、早い段階で追加しています。続いて、初期化ロジックを変更し、既存のポジションをそれぞれのバスケットに読み込めるようにする必要があります。これを実現するために、以下のロジックを実装しています。
bool initialize() { if (!initializeIndicators()) return false; //--- Load existing positions into baskets int totalPositions = PositionsTotal(); for (int i = 0; i < totalPositions; i++) { ulong ticket = PositionGetTicket(i); if (PositionSelectByTicket(ticket)) { if (PositionGetString(POSITION_SYMBOL) == m_symbol) { long magic = PositionGetInteger(POSITION_MAGIC); if (magic >= m_baseMagicNumber && magic < m_baseMagicNumber + m_maxInitialPositions) { //--- Check if basket already exists for this magic bool exists = false; for (int j = 0; j < ArraySize(m_traders); j++) { if (m_traders[j] != NULL && m_traders[j].getMagicNumber() == magic) { exists = true; break; } } if (!exists && countActiveBaskets() < m_maxInitialPositions) { createNewBasket(magic, ticket); } } } } } Print("BasketManager initialized with ", ArraySize(m_traders), " existing baskets"); return true; } /* //--- PREVIOUS INITIALIZATION int initialize() { //--- Initialization Start m_tradeExecutor.SetExpertMagicNumber(m_tradeConfig.tradeIdentifier); //--- Set magic number int totalPositions = PositionsTotal(); //--- Get total positions for (int i = 0; i < totalPositions; i++) { //--- Iterate positions ulong ticket = PositionGetTicket(i); //--- Get ticket if (PositionSelectByTicket(ticket)) { //--- Select position if (PositionGetString(POSITION_SYMBOL) == m_tradeConfig.marketSymbol && PositionGetInteger(POSITION_MAGIC) == m_tradeConfig.tradeIdentifier) { //--- Check symbol and magic if (activateTrade(ticket)) { //--- Activate position Print("Existing position activated: Ticket=", ticket); //--- Log activation } else { Print("Failed to activate existing position: Ticket=", ticket); //--- Log failure } } } } m_handleRsi = iRSI(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 8, PRICE_CLOSE); //--- Initialize RSI if (m_handleRsi == INVALID_HANDLE) { //--- Check RSI Print("Failed to initialize RSI indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvUpper = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 150, 0, MODE_SMA, PRICE_CLOSE, 0.1); //--- Initialize upper Envelopes if (m_handleEnvUpper == INVALID_HANDLE) { //--- Check upper Envelopes Print("Failed to initialize upper Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } m_handleEnvLower = iEnvelopes(m_tradeConfig.marketSymbol, PERIOD_CURRENT, 95, 0, MODE_SMA, PRICE_CLOSE, 1.4); //--- Initialize lower Envelopes if (m_handleEnvLower == INVALID_HANDLE) { //--- Check lower Envelopes Print("Failed to initialize lower Envelopes indicator"); //--- Log failure return INIT_FAILED; //--- Return failure } ArraySetAsSeries(m_rsiBuffer, true); //--- Set RSI buffer ArraySetAsSeries(m_envUpperBandBuffer, true); //--- Set upper Envelopes buffer ArraySetAsSeries(m_envLowerBandBuffer, true); //--- Set lower Envelopes buffer Print("EA initialized successfully"); //--- Log success return INIT_SUCCEEDED; //--- Return success //--- Initialization End } */
ここでは、BasketManagerクラス内に更新版のinitialize関数を実装し、インジケーターを初期化し、既存のポジションをそれぞれのバスケットに読み込むことで、マルチバスケット取引機能をサポートします。まず、initializeIndicatorsを呼び出してRSIおよびエンベロープインジケーターをセットアップします。失敗した場合はfalseを返して、システムが必要なマーケットデータを確実に取得できるようにします。従来のバージョンでは、インジケーターのセットアップをMarketZoneTraderのinitialize関数内で個別に処理していましたが、今回はBasketManagerに集約することで、複数のバスケット間でインジケータデータを共有できるようにしました。次に、PositionsTotal関数を使用して既存のポジションを確認し、各ポジションをループ処理します。各ポジションのチケットをPositionGetTicket関数で取得します。
PositionSelectByTicketが成功し、そのポジションの銘柄がPositionGetStringを介して取得した値とm_symbolに一致する場合、そのマジックナンバーをPositionGetIntegerで取得します。このマジックナンバーがm_baseMagicNumberからm_baseMagicNumber + m_maxInitialPositionsの範囲内にあるかを確認します。次に、同じマジックナンバーを持つバスケットが既に存在するかどうかを確認するために、m_traders配列をループし、NULLでないエントリに対してgetMagicNumberを呼び出します。もし該当するバスケットが存在せず、かつcountActiveBasketsの結果がm_maxInitialPositionsを下回っている場合、createNewBasketをマジックナンバーとチケットを引数に呼び出し、そのポジションを新しいバスケットにロードします。最後に、Print関数を使用してm_tradersのArraySizeを出力し、初期化されたバスケット数をログに記録します。その後、trueを返して初期化完了を示します。プログラムを実行すると、次の結果が得られます。

ここからはティック処理に進みます。processTick関数内では、各ティックごとに既存のバスケットを処理し、新しいシグナルが確定した際に新しいバスケットを作成する必要があります。これは、確定シグナルに基づいて取引を開始するだけでよかった従来バージョンとは異なります。
void processTick() { //--- Process existing baskets for (int i = 0; i < ArraySize(m_traders); i++) { if (m_traders[i] != NULL) { m_traders[i].processTick(m_rsiBuffer, m_envUpperBandBuffer, m_envLowerBandBuffer); } } cleanupTerminatedBaskets(); //--- Check for new signals on new bar if (!isNewBar()) return; if (!CopyBuffer(m_handleRsi, 0, 0, 3, m_rsiBuffer)) { Print("Error loading RSI data. Reverting."); return; } if (!CopyBuffer(m_handleEnvUpper, 0, 0, 3, m_envUpperBandBuffer)) { Print("Error loading upper envelopes data. Reverting."); return; } if (!CopyBuffer(m_handleEnvLower, 1, 0, 3, m_envLowerBandBuffer)) { Print("Error loading lower envelopes data. Reverting."); return; } const int rsiOverbought = 70; const int rsiOversold = 30; int ticket = -1; ENUM_ORDER_TYPE signalType = (ENUM_ORDER_TYPE)-1; double askPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_ASK), Digits()); double bidPrice = NormalizeDouble(SymbolInfoDouble(m_symbol, SYMBOL_BID), Digits()); if (m_rsiBuffer[1] < rsiOversold && m_rsiBuffer[2] > rsiOversold && m_rsiBuffer[0] < rsiOversold) { if (askPrice > m_envUpperBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_BUY; } } } else if (m_rsiBuffer[1] > rsiOverbought && m_rsiBuffer[2] < rsiOverbought && m_rsiBuffer[0] > rsiOverbought) { if (bidPrice < m_envLowerBandBuffer[0]) { if (countActiveBaskets() < m_maxInitialPositions) { signalType = ORDER_TYPE_SELL; } } } if (signalType != (ENUM_ORDER_TYPE)-1) { //--- Create new basket with unique magic number int newMagic = m_baseMagicNumber + ArraySize(m_traders); if (newMagic < m_baseMagicNumber + m_maxInitialPositions) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, newMagic); ticket = newTrader.openInitialOrder(signalType); //--- Open INITIAL position if (ticket > 0 && newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("New basket created: Magic=", newMagic, ", Ticket=", ticket, ", Type=", EnumToString(signalType)); } else { delete newTrader; Print("Failed to create new basket: Ticket=", ticket); } } else { Print("Maximum initial positions (baskets) reached: ", m_maxInitialPositions); } } }
この関数内では、まずArraySize関数を使用してm_traders配列をループし、各NULLでないMarketZoneTraderインスタンスに対してprocessTick関数を呼び出します。この際、m_rsiBuffer、m_envUpperBandBuffer、およびm_envLowerBandBufferを引数として渡し、各バスケットのロジックを個別に処理します。これは、単一の取引サイクルを直接管理していた従来バージョンのprocessTickとは異なります。続いて、cleanupTerminatedBasketsを呼び出して非アクティブなバスケットを削除し、リソースを効率的に使用できるようにします。次に、isNewBarを使用して新しいバーが確定したときのみ、新規シグナルを確認します。結果がfalseの場合はリソース節約のために処理を終了します。
その後、CopyBufferを使用してm_handleRsi、m_handleEnvUpper、およびm_handleEnvLowerのインジケータデータをそれぞれのバッファに読み込みます。データの読み込みに失敗した場合は、Printでエラーログを出力し、処理を終了します。従来バージョンではこの処理をMarketZoneTrader内でおこなっていましたが、今回はBasketManager側で集中管理するように変更されています。次に、rsiOverboughtを70、rsiOversoldを30に設定し、ticketおよびsignalTypeを初期化します。SymbolInfoDouble関数を用いてSYMBOL_ASKおよびSYMBOL_BIDからaskPriceとbidPriceを取得し、NormalizeDoublee関数で正規化します。
買いシグナルの場合、m_rsiBufferが売られ過ぎを示し、かつaskPriceがm_envUpperBandBufferを上回っているとき、countActiveBasketsがm_maxInitialPositionsを下回っていればsignalTypeをORDER_TYPE_BUYに設定します。売りシグナルの場合、m_rsiBufferが買われ過ぎを示し、かつbidPriceがm_envLowerBandBufferを下回っているとき、signalTypeをORDER_TYPE_SELLに設定します。有効なsignalTypeが存在する場合、m_baseMagicNumberにArraySize(m_traders)を加算して一意のマジックナンバーを生成します。この値がm_maxInitialPositionsの範囲内であれば、新しいマジックナンバーと入力パラメータを用いて新しいMarketZoneTraderをインスタンス化します。
その後、openInitialOrderをsignalTypeを引数に呼び出し、戻り値のticketが有効であり、かつactivateTradeが成功した場合、ArrayResizeでm_traders配列を拡張して新しいトレーダーを追加します。成功時にはPrintとEnumToStringを使用してログに出力します。一方、失敗した場合はそのトレーダーをdeleteで削除して失敗をログに記録し、バスケット制限に達している場合にはその旨を通知します。新しい取引が始まった後、それらに対応する新しいバスケットを作成する必要があります。そのためのロジックを以下に実装します。
private: void createNewBasket(long magic, ulong ticket) { MarketZoneTrader* newTrader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, zoneTargetPoints, zoneSizePoints, magic); if (newTrader.activateTrade(ticket)) { int size = ArraySize(m_traders); ArrayResize(m_traders, size + 1); m_traders[size] = newTrader; Print("Existing position loaded into basket: Magic=", magic, ", Ticket=", ticket); } else { delete newTrader; Print("Failed to load existing position into basket: Ticket=", ticket); } }
BasketManagerクラスのprivateセクション内に、新たにcreateNewBasket関数を実装します。この関数は、既存ポジションに対して新しいバスケットを作成、管理するために追加されたもので、マルチバスケット取引機能をサポートする重要な要素です。まず、入力パラメータlotOption、initialLotSize、riskPercentage、riskPoints、zoneTargetPoints、zoneSizePoints、および指定されたmagicナンバーを用いて、新しいMarketZoneTraderインスタンスnewTraderを作成します。これにより、一意のバスケットが構成されます。従来バージョンでは、ユーザー入力を初期化段階で設定しており、ゾーンが1つのインスタンスのみだったため、すべての新規ポジションに同じ設定が適用されていました。しかし今回の実装では、各バスケットを新しいクラスインスタンスとして整理することで、個別の管理と拡張性を実現しています。以下のコードは、比較を容易にするためにその実装部分を示したものです。
//--- PREVIOUS VERSION OF NEW CLASS INSTANCE //--- Global Instance MarketZoneTrader *trader = NULL; //--- Declare trader instance int OnInit() { //--- EA Initialization Start trader = new MarketZoneTrader(lotOption, initialLotSize, riskPercentage, riskPoints, maxOrders, restrictMaxOrders, zoneTargetPoints, zoneSizePoints); //--- Create trader instance return trader.initialize(); //--- Initialize EA //--- EA Initialization End }
次に、newTraderに対してactivateTradeを呼び出し、与えられたticketを用いて既存のポジションをバスケットにロードします。成功した場合、ArraySizeでm_traders配列の現在サイズを取得し、ArrayResizeで1つ拡張して、newTraderを新しいスロットに追加します。成功時には、Printでmagicとticketの値を含めてログに出力します。activateTradeが失敗した場合は、newTraderをdeleteで削除してメモリを解放し、Printで失敗をログに記録します。この関数により、既存のポジションをそれぞれのバスケットに整理できるようになり、従来バージョンの単一インスタンス方式とは異なり、マルチバスケットシステムの主要機能が実現されます。このクラスにより、バスケットを効率的に管理できるようになります。続いて、基盤クラスを改良し、新しいマルチバスケット機能およびトレーリングストップ機能に対応させます。まずは、そのメンバーから見ていきましょう。
//--- Modified MarketZoneTrader Class class MarketZoneTrader { private: enum TradeState { INACTIVE, RUNNING, TERMINATING }; struct TradeMetrics { bool operationSuccess; double totalVolume; double netProfitLoss; }; struct ZoneBoundaries { double zoneHigh; double zoneLow; double zoneTargetHigh; double zoneTargetLow; }; struct TradeConfig { string marketSymbol; double openPrice; double initialVolume; long tradeIdentifier; string initialTradeLabel; //--- Label for initial positions string recoveryTradeLabel; //--- Label for recovery positions ulong activeTickets[]; ENUM_ORDER_TYPE direction; double zoneProfitSpan; double zoneRecoverySpan; double accumulatedBuyVolume; double accumulatedSellVolume; TradeState currentState; bool hasRecoveryTrades; //--- Flag to track recovery trades double trailingStopLevel; //--- Virtual trailing stop level }; struct LossTracker { double tradeLossTracker; }; TradeConfig m_tradeConfig; ZoneBoundaries m_zoneBounds; LossTracker m_lossTracker; string m_lastError; int m_errorStatus; CTrade m_tradeExecutor; TradingLotSizeOptions m_lotOption; double m_initialLotSize; double m_riskPercentage; int m_riskPoints; double m_zoneTargetPoints; double m_zoneSizePoints; }
ここでは、プログラムを強化するために、MarketZoneTraderクラスのprivateセクションを修正し、トレーリングストップおよび改善された取引ラベル機能をサポートする新しい機能を追加します。コア構造は維持しつつ、TradeConfig構造体に重要な変更を加え、強化された戦略に対応させます。TradeStateenumeration(INACTIVE、RUNNING、TERMINATING)はそのまま保持し、TradeMetrics、ZoneBoundaries、LossTracker構造体も前バージョンと同様に維持します。これらは、取引状態、パフォーマンス指標、ゾーン境界、損失追跡を引き続き管理します。
TradeConfig構造体では、initialTradeLabelおよびrecoveryTradeLabelの2つの新しい文字列変数を追加します。これらのラベルにより、初期取引とリカバリ取引を個別にタグ付けでき、各バスケット取引内での識別と追跡が容易になります。特に、新システムで複数バスケット取引を管理する場合に有用です。さらに、ブール値のhasRecoveryTradesを追加し、バスケット取引にリカバリー取引が含まれるかどうかを追跡します。これはトレーリングストップを適切に有効化または無効化するために重要です。加えて、各バスケットの仮想トレーリングストップレベルを格納するtrailingStopLevel(double型)を追加し、初期取引に対して動的な利益保護を可能にします。
メンバー変数では、m_tradeConfig、m_zoneBounds、m_lossTracker、m_lastError、m_errorStatus、m_tradeExecutor、m_lotOption、m_initialLotSize、m_riskPercentage、m_riskPoints、m_zoneTargetPoints、m_zoneSizePointsを保持しますが、それぞれの役割は新しいトレーリングストップおよびマルチバスケット機能をサポートするように更新されます。特に、m_handleRsiやm_rsiBufferなどのインジケータ関連変数は削除され、現在はBasketManagerクラスで集中管理されるため、各トレーダーは個々のバスケット取引の運用に専念できるようになります。コンストラクタおよびデストラクタでは、新機能を扱えるようにいくつかの変数をわずかに変更する必要があります。
public: MarketZoneTrader(TradingLotSizeOptions lotOpt, double initLot, double riskPct, int riskPts, double targetPts, double sizePts, long magic) { m_tradeConfig.currentState = INACTIVE; ArrayResize(m_tradeConfig.activeTickets, 0); m_tradeConfig.zoneProfitSpan = targetPts * _Point; m_tradeConfig.zoneRecoverySpan = sizePts * _Point; m_lossTracker.tradeLossTracker = 0.0; m_lotOption = lotOpt; m_initialLotSize = initLot; m_riskPercentage = riskPct; m_riskPoints = riskPts; m_zoneTargetPoints = targetPts; m_zoneSizePoints = sizePts; m_tradeConfig.marketSymbol = _Symbol; m_tradeConfig.tradeIdentifier = magic; m_tradeConfig.initialTradeLabel = "EA_INITIAL_" + IntegerToString(magic); //--- Label for initial positions m_tradeConfig.recoveryTradeLabel = "EA_RECOVERY_" + IntegerToString(magic); //--- Label for recovery positions m_tradeConfig.hasRecoveryTrades = false; //--- Initialize recovery flag m_tradeConfig.trailingStopLevel = 0.0; //--- Initialize trailing stop m_tradeExecutor.SetExpertMagicNumber(magic); } ~MarketZoneTrader() { ArrayFree(m_tradeConfig.activeTickets); }
まず、MarketZoneTraderのconstructorから始めます。今回は追加のmagicパラメータを受け取り、各バスケット取引に一意のマジックナンバーを割り当てられるようになっています。従来バージョンでは固定のマジックナンバーを使用していました。改善された取引ラベル機能をサポートするため、m_tradeConfig.initialTradeLabelをEA_INITIALにmagicを付加(IntegerToStringを使用)し、m_tradeConfig.recoveryTradeLabelをEA_RECOVERYにmagicを付加して設定します。これにより、各バスケット取引内で初期取引とリカバリー取引を個別に識別できるようになります。さらに、リカバリー取引の状態を追跡するためにm_tradeConfig.hasRecoveryTradesをfalseに初期化し、仮想トレーリングストップ用のm_tradeConfig.trailingStopLevelを0.0に設定します。これらはいずれも新しい機能です。最後に、m_tradeExecutorをSetExpertMagicNumberでmagicを用いて設定します。主要な変更点はハイライトしており、確認が容易です。
次に、~MarketZoneTraderデストラクタを従来バージョンのcleanupと比較して簡略化します。現在はm_tradeConfig.activeTicketsをArrayFreeでクリアするだけです。インジケータのクリーンアップはBasketManagerで集中管理されるため、デストラクタの役割はバスケット取引固有のリソースに焦点を当てる形に縮小されます。続いて、初期取引に対してトレーリングストップレベルとリカバリー状態を初期化できるよう、取引をアクティブ化する関数を更新します。
bool activateTrade(ulong ticket) { m_tradeConfig.hasRecoveryTrades = false; m_tradeConfig.trailingStopLevel = 0.0; //--- THE REST OF THE LOGIC REMAINS return true; }
ここでは、最初の取引に対してトレーリングストップレベルを0に、リカバリー状態をfalseに初期化するロジックを追加します。これは、このポジションがバスケット取引内で最初のポジションであることを示すためです。最後に、初期ポジションを開く関数を追加できます。
int openInitialOrder(ENUM_ORDER_TYPE orderType) { //--- Open INITIAL position based on signal int ticket; double openPrice; if (orderType == ORDER_TYPE_BUY) { openPrice = NormalizeDouble(getMarketAsk(), Digits()); } else if (orderType == ORDER_TYPE_SELL) { openPrice = NormalizeDouble(getMarketBid(), Digits()); } else { Print("Invalid order type [Magic=", m_tradeConfig.tradeIdentifier, "]"); return -1; } double lotSize = 0; if (m_lotOption == FIXED_LOTSIZE) { lotSize = m_initialLotSize; } else if (m_lotOption == UNFIXED_LOTSIZE) { lotSize = calculateLotSize(m_riskPercentage, m_riskPoints); } if (lotSize <= 0) { Print("Invalid lot size [Magic=", m_tradeConfig.tradeIdentifier, "]: ", lotSize); return -1; } if (m_tradeExecutor.PositionOpen(m_tradeConfig.marketSymbol, orderType, lotSize, openPrice, 0, 0, m_tradeConfig.initialTradeLabel)) { ticket = (int)m_tradeExecutor.ResultOrder(); Print("INITIAL trade opened [Magic=", m_tradeConfig.tradeIdentifier, "]: Ticket=", ticket, ", Type=", EnumToString(orderType), ", Volume=", lotSize); } else { ticket = -1; Print("Failed to open INITIAL order [Magic=", m_tradeConfig.tradeIdentifier, "]: Type=", EnumToString(orderType), ", Volume=", lotSize); } return ticket; }
ここでは、MarketZoneTraderクラスのパブリックセクションに新たにopenInitialOrder関数を実装します。この関数は、特定のバスケット取引に対して初期ポジションをオープンし、明確に識別できるようにすることで、マルチバスケットおよび改善された取引ラベル機能をサポートします。まず、ticketおよびopenPriceを初期化します。orderTypeがORDER_TYPE_BUYの場合、getMarketAskを使用してopenPriceを取得しNormalizeDoubleとDigitsで正規化します。ORDER_TYPE_SELLの場合はgetMarketBidを使用します。orderTypeが無効な場合は、Printでm_tradeConfig.tradeIdentifierを含むエラーログを出力し、-1を返します。
次に、m_lotOptionに基づいてlotSizeを決定します。FIXED_LOTSIZEの場合はm_initialLotSizeを使用し、UNFIXED_LOTSIZEの場合は、calculateLotSizeを呼び出し、m_riskPercentageおよびm_riskPointsを用いて計算します。lotSizeが無効な場合は、Printでエラーを記録し、-1を返します。その後、m_tradeExecutor.PositionOpenを使用してポジションをオープンします。引数にはm_tradeConfig.marketSymbol、orderType、lotSize、openPrice、および初期取引を明確に識別するためのm_tradeConfig.initialTradeLabelを渡します。成功した場合、ResultOrderでticketを設定し、Printでm_tradeConfig.tradeIdentifierおよびEnumToStringを含めてログに記録します。失敗した場合は、ticketを「-1」に設定し、エラーをログに出力します。最後に、ticketを返します。従来バージョンのopenOrder関数とは異なり、この関数は新しいinitialTradeLabelを使用し、初期ポジションにのみ焦点を当てています。これにより、マルチバスケットシステムに沿った動作が可能になります。コンパイルすると、次の結果が得られます。

画像から、初期取引をオープンし、それに対応する新しいバスケット取引インスタンスを作成できることが確認できます。次に、ポジションのトレーリングストップ機能を管理できるように、トレーリングロジックを実装する必要があります。
void evaluateMarketTick() { if (m_tradeConfig.currentState == INACTIVE) return; if (m_tradeConfig.currentState == TERMINATING) { finalizePosition(); return; } double currentPrice; double profitPoints = 0.0; //--- Handle BUY initial position if (m_tradeConfig.direction == ORDER_TYPE_BUY) { currentPrice = getMarketBid(); profitPoints = (currentPrice - m_tradeConfig.openPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice - trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop > m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice <= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " <= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice > m_zoneBounds.zoneTargetHigh) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " > TargetHigh=", m_zoneBounds.zoneTargetHigh); finalizePosition(); return; } else if (currentPrice < m_zoneBounds.zoneLow) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Bid=", currentPrice, " < ZoneLow=", m_zoneBounds.zoneLow); triggerRecoveryTrade(ORDER_TYPE_SELL, currentPrice); } } //--- Handle SELL initial position else if (m_tradeConfig.direction == ORDER_TYPE_SELL) { currentPrice = getMarketAsk(); profitPoints = (m_tradeConfig.openPrice - currentPrice) / _Point; //--- Trailing Stop Logic for Initial Position if (enableInitialTrailing && !m_tradeConfig.hasRecoveryTrades && profitPoints >= minProfitPoints) { //--- Calculate desired trailing stop level double newTrailingStop = currentPrice + trailingStopPoints * _Point; //--- Start or update trailing stop if profit exceeds minProfitPoints + trailingStopPoints if (profitPoints >= minProfitPoints + trailingStopPoints) { if (m_tradeConfig.trailingStopLevel == 0.0 || newTrailingStop < m_tradeConfig.trailingStopLevel) { m_tradeConfig.trailingStopLevel = newTrailingStop; Print("Trailing stop updated [Magic=", m_tradeConfig.tradeIdentifier, "]: Level=", m_tradeConfig.trailingStopLevel, ", Profit=", profitPoints, " points"); } } //--- Check if price has hit trailing stop if (m_tradeConfig.trailingStopLevel > 0.0 && currentPrice >= m_tradeConfig.trailingStopLevel) { Print("Trailing stop triggered [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " >= TrailingStop=", m_tradeConfig.trailingStopLevel); finalizePosition(); return; } } //--- Zone Recovery Logic if (currentPrice < m_zoneBounds.zoneTargetLow) { Print("Closing position [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " < TargetLow=", m_zoneBounds.zoneTargetLow); finalizePosition(); return; } else if (currentPrice > m_zoneBounds.zoneHigh) { Print("Triggering RECOVERY trade [Magic=", m_tradeConfig.tradeIdentifier, "]: Ask=", currentPrice, " > ZoneHigh=", m_zoneBounds.zoneHigh); triggerRecoveryTrade(ORDER_TYPE_BUY, currentPrice); } } }
ここでは、プログラムを強化し、evaluateMarketTick関数を更新して、既存のゾーンリカバリーのロジックを維持しつつトレーリングストップロジックを組み込みます。まず、m_tradeConfig.currentStateがINACTIVEまたはTERMINATINGかを確認し、従来どおり処理を終了するか、finalizePositionを呼び出します。買いポジションの場合(m_tradeConfig.directionがORDER_TYPE_BUY)、getMarketBidでcurrentPriceを取得し、profitPointsを「(currentPrice - m_tradeConfig.openPrice) / _Point」で計算します。新しいトレーリングストップロジックでは、enableInitialTrailingがtrue、m_tradeConfig.hasRecoveryTradesがfalse、かつprofitPointsがminProfitPoints以上である場合に、newTrailingStopを「currentPrice - trailingStopPoints * _Point」で計算します。さらに、profitPointsが「minProfitPoints + trailingStopPoints」を超え、かつm_tradeConfig.trailingStopLevelが0.0またはnewTrailingStopより低い場合、m_tradeConfig.trailingStopLevelを更新し、Printでログに出力します。
m_tradeConfig.trailingStopLevelが設定され、currentPriceがそれを下回った場合、トリガーをログに記録し、finalizePositionを呼び出してポジションをクローズします。ゾーンリカバリーロジックは従来どおり維持され、currentPriceがm_zoneBounds.zoneTargetHighを超えた場合にポジションをクローズし、m_zoneBounds.zoneLowを下回った場合にはtriggerRecoveryTradeで売りリカバリー取引を発動します。
売りポジションの場合(m_tradeConfig.directionがORDER_TYPE_SELL)、getMarketAskでcurrentPriceを取得し、profitPointsを逆に計算します。トレーリングストップロジックは買いの場合と同様で、newTrailingStopを「currentPrice + trailingStopPoints *_Point」で設定し、条件を満たせばm_tradeConfig.trailingStopLevelを更新し、currentPriceがそれを超えた場合にポジションをクローズします。ゾーンリカバリーロジックは、currentPriceがm_zoneBounds.zoneTargetLowを下回った場合にポジションをクローズし、m_zoneBounds.zoneHighを上回った場合には買いリカバリートレードを発動します。物理的なトレーリングストップは使用せず、システム全体を完全に制御できるようにしています。これにより、すべてのインスタンスを監視、管理することが可能です。以下は、トレーリングストップ機能を実行した後の出力例です。

画像から、ポジションをトレーリングし、価格がトレーリングレベルまで戻ったときにクローズできることが確認できます。最後に、BasketManagerのインスタンスを作成し、それをグローバルに管理に使用します。
//--- Global Instance BasketManager *manager = NULL; int OnInit() { manager = new BasketManager(_Symbol, baseMagicNumber, maxInitialPositions); if (!manager.initialize()) { delete manager; manager = NULL; return INIT_FAILED; } return INIT_SUCCEEDED; } void OnDeinit(const int reason) { if (manager != NULL) { delete manager; manager = NULL; Print("EA deinitialized"); } } void OnTick() { if (manager != NULL) { manager.processTick(); } }
グローバルインスタンスとイベントハンドラを更新し、以前バージョンで使用していたMarketZoneTraderクラスの代わりに新しいBasketManagerクラスを使用します。これにより、複数のバスケット取引を集中管理することで、マルチバスケット取引機能をサポートします。まず、グローバルにBasketManagerクラスのポインタmanagerを宣言し、NULLで初期化します。従来はMarketZoneTraderのtraderポインタを使用していました。この変更は重要で、単一インスタンス方式だった以前のバージョンとは異なり、1つのマネージャーで複数のバスケット取引を管理できるようになります。
OnInitイベントハンドラでは、managerの新しいBasketManagerインスタンスを作成し、引数として_Symbol、baseMagicNumber、maxInitialPositionsを渡して、現在のチャート、バスケット識別用のユニークなマジックナンバー、および最大バスケット数を設定します。次にmanager.initializeを呼び出してインジケーターをセットアップし、既存ポジションをロードします。失敗した場合はmanagerを削除してNULLに設定し、INIT_FAILEDを返します。成功した場合はINIT_SUCCEEDEDを返します。
OnDeinitイベントハンドラでは、managerがNULLでない場合、deleteで削除し、NULLに設定して、Printで初期化解除をログに出力します。OnTickでは、managerがNULLでないことを確認した上で、manager.processTickを呼び出し、すべてのバスケット取引のマーケットティックを処理します。これにより、従来のtrader.processTick呼び出しを置き換え、複数のバスケット取引のティック処理を集中管理できるようになります。コンパイルすると、次の結果が得られます。

画像から、提供されたマジックナンバーから構築された異なるラベルを使用して、個別のシグナルバスケットを作成して管理できることがわかります。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。
バックテスト
徹底的なバックテストの結果、次の結果が得られました。
バックテストグラフ

バックテストレポート

結論
まとめとして、今回の改良により、MQL5におけるEnvelopes Trend取引向けのゾーンリカバリーシステムに、トレーリングストップおよびマルチバスケット取引機能を導入しました。第22回の基礎を踏まえ、BasketManagerクラスや更新されたMarketZoneTrader関数などの新しいコンポーネントを追加することで、より柔軟で堅牢な取引フレームワークを構築しています。これらの改良により、trailingStopPointsやmaxInitialPositionsなどのパラメータを調整することで、さらにカスタマイズ可能なシステムとなります。
免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。
これらの改良を活用することで、本システムをさらに洗練させたり、そのアーキテクチャを応用して新しい戦略を作成することができ、アルゴリズム取引のスキル向上に役立ちます。取引をお楽しみください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18778
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
MQL5における特異スペクトル解析
知っておくべきMQL5ウィザードのテクニック(第74回): 教師あり学習で一目均衡表とADX Wilderのパターンを利用する
MQL5で自己最適化エキスパートアドバイザーを構築する(第8回):複数戦略分析(2) - 加重投票方策
グラフ理論:ダイクストラ法を取引に適用する
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
初値売り取引が成立しません。
取引ロジックと関係がありますか?