多通貨エキスパートアドバイザーの開発(第2回):取引戦略の仮想ポジションへの移行
はじめに
前回の記事では、様々な取引戦略と同時に機能する多通貨EAの開発に着手しました。最初の段階では2種類の戦略しかありませんでした。これらは同じ取引アイデアの実装を表し、同じ取引商品(銘柄)とチャート期間(時間枠)で動作しました。両者の違いはパラメータの数値のみでした。
また、希望する最大ドローダウンレベル(入金の10%)に基づいて、ポジションの最適なサイズを決定しました。それぞれの戦略についてこれを別々におこないました。この2つの戦略を組み合わせると、所定のドローダウンレベルを維持するために、建てたポジションのサイズを小さくしなければなりませんでした。減少幅は2つの戦略については小さいものでしたが、何十、何百もの戦略インスタンスを組み合わせたい場合はどうするのでしょうか。戦略によっては、ブローカーが許容するポジションの最小サイズより小さい値にポジションサイズを縮小しなければならないことも十分にあり得ます。この場合、これらの戦略は取引に参加できません。どうすればうまくいくのでしょうか。
そのためには、戦略から、独自にポジションを建てたり、予約注文を出したりする権利を奪います。戦略は仮想取引をおこなうだけでよいのです。つまり、ある大きさのポジションをどのレベルで建てるべきかを記憶し、要求に応じて今どの程度の数量を建てるべきかを報告します。すべての戦略を調査し、所定のドローダウンを維持するためのスケーリングを考慮して、必要な総量を計算した後にのみ、実際のマーケットポジションを建てます。
今は、この方法の適合性をテストすることにしか興味がなく、その実装の効率性には興味がありません。したがって、この記事の枠組みの中で、この方法の少なくともいくつかの実用的な実装を開発しようと試みます。間違いを避ける方法に関する知識がすでにあるため、後で建築の観点からより美しいものを構築するのに役立ちます。
これを実装してみましょう。
過去の業績の再検討
CAdvisor EAクラスを開発しました。このクラスは、取引戦略のインスタンスの配列(より正確には、インスタンスへのポインタ)を格納します。これにより、メインプログラムにEAのインスタンスを1つ作成し、戦略クラスのインスタンスを複数追加することができます。この配列はCStrategy基本クラスのオブジェクトへのポインタを格納するので、CStrategyから継承された子孫クラスのオブジェクトへのポインタを格納することができます。私たちの場合、CSimpleVolumesStrategyという子孫クラスを1つ作成し、その2つのオブジェクトをEAのこの配列に追加しました。
示しやすくするために、便利な名前に同意しましょう。
- EAは最終的なmq5ファイルで、コンパイル後、テスターや端末で実行するのに適した実行可能なex5ファイルになる
- EAはプログラムで宣言されたCAdvisorクラスのオブジェクトで、1つのプログラムで1つのEAインスタンスだけを使用する
- 戦略は、戦略の基本クラスであるCStrategyから継承された子クラスのオブジェクト
また、オブジェクト(戦略やその他のクラス)へのポインタは、以前に作成されたオブジェクト(簡略化されたもの)のメモリ上の位置に関する情報であることを思い出してみましょう。これにより、オブジェクトを関数に渡したり、新しい変数や配列要素を代入したりする際に、同じオブジェクトを別のメモリロケーションに再作成することを避けることができます。
そのため、EAでは戦略オブジェクトへのポインタを配列に格納し、この配列を埋める際に戦略オブジェクトのコピーが作成されないようにしています。そして、戦略配列の要素にアクセスするときは、元の戦略オブジェクトにアクセスします。
EAの作業は以下の段階から構成されていました。
- 静的メモリ領域のEAが作成される
- プログラムが初期化されると、2つの戦略が動的メモリに作成され、それらへのポインタがEAに格納される
- プログラムの実行中、EAはCStrategy::Tick()メソッドを呼び出すことで、必要な取引アクションを実行するために各戦略を連続的に呼び出す
- プログラムの初期化を解除する際に、EAが動的メモリから戦略オブジェクトを削除する
始める前に、EAとEAクラスを少し修正しておきましょう。EAでは、エキスパートが動的メモリ領域に作成されるようにします。
CAdvisor expert; // EA object CAdvisor *expert; // Pointer to the EA object int OnInit() { expert = new CAdvisor(); // Create EA object // The rest of the code from OnInit() ... }
EAクラスでは、デストラクタ(EAオブジェクトがメモリから削除されたときに自動的に呼び出される関数)を作成します。デストラクタは、CAdvisor::Deinit()メソッドのストラテジオブジェクトを動的メモリから削除する操作を受け取ります。このメソッドは必要ありません。削除しましょう。戦略の数を格納するクラス変数m_strategiesCountも削除します。必要に応じてArraySize()を使用することができます。
class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor // ... }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects for(int i = 0; i < ArraySize(m_strategies); i++) { delete m_strategies[i]; } }
OnDeinit()関数の中で、CAdvisor::Deinit()メソッドをEAオブジェクトの削除に置き換えます。
void OnDeinit(const int reason) { expert.Deinit(); delete expert; }
ロードマップ
取引戦略が自らマーケットポジションを建てることができなくなったのであれば、次のようになります。
- 戦略の仮想ポジションに関する情報を格納するオブジェクトが必要
- 仮想ポジションの情報を実際のマーケットポジションに変換するオブジェクトが必要
仮想ポジションのためのオブジェクトは戦略の不可欠な一部であるべきで、いくつかあるべきです。したがって、最初の新しいクラスCVirtualOrderを呼び出し、これらのオブジェクトの配列をCStrategy戦略クラスに追加しましょう。CStrategyはまた、未決済の仮想ポジションの変化を表すプロパティと、その値を取得設定するためのメソッドを取得します。このプロパティは、戦略が現在どちらの状態にあるかを決定します。
- 変化なし:オープン仮想ボリューム全体が市場にリリースされた
- 変化あり:仮想ボリュームが市場のボリュームと一致しないため、実際の市場ポジションのボリュームを調整する必要がある
現在、誰かが実際のポジションを建てる責任を負っているので、m_magicマジックナンバープロパティを戦略基本クラスから削除することができます。将来的には、最も基本的な戦略のクラスをさらにクリーンアップする予定ですが、今のところは部分的なクリーンアップにとどめます。
これで、基となる戦略クラスは次のようになります。
#include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) CVirtualOrder m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders double m_volumeTotal; // Total volume of open positions and orders bool m_isChanged; // Sign of changes in open virtual positions void CountOrders(); // Calculate the number and volumes of open positions and orders public: // Constructor CStrategy(string p_symbol = "", ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT, double p_fixedLot = 0.01); virtual void Tick(); // Main method - handling OnTick events virtual double Volume(); // Total volume of virtual positions virtual string Symbol(); // Strategy symbol (only one for a single strategy so far) virtual bool IsChanged(); // Are there any changes in open virtual positions? virtual void ResetChanges(); // Reset the sign of changes in open virtual positions };
すでにSymbol()、IsChanged()、ResetChanges()メソッドを実装することができます。
//+------------------------------------------------------------------+ //| Strategy symbol | //+------------------------------------------------------------------+ string CStrategy::Symbol() { return m_symbol; } //+------------------------------------------------------------------+ //| Are there any changes to open virtual positions? | //+------------------------------------------------------------------+ bool CStrategy::IsChanged() { return m_isChanged; } //+------------------------------------------------------------------+ //| Reset the flag for changes in virtual positions | //+------------------------------------------------------------------+ void CStrategy::ResetChanges() { m_isChanged = false; }
残りのメソッド(Tick()、Volume()、CountOrders())は、後で基本クラスの子孫かクラス自体に実装します。
2つ目の新しいクラスは、戦略の仮想ポジションを市場にもたらすことに関与するオブジェクトで、CReceiverと呼ばれます。このオブジェクトが機能するためには、すべてのEA戦略にアクセスし、どの銘柄とどの出来高を使用して実際のポジションを建てるべきかを見つけることができなければなりません。このようなオブジェクトは、1つのEAに1つあれば十分です。CReceiverオブジェクトは、建てたマーケットポジションに設定されるマジックナンバーを持つべきです。
#include "Strategy.mqh" //+------------------------------------------------------------------+ //| Base class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CReceiver { protected: CStrategy *m_strategies[]; // Array of strategies ulong m_magic; // Magic public: CReceiver(ulong p_magic = 0); // Constructor virtual void Add(CStrategy *strategy); // Adding strategy virtual bool Correct(); // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) { ArrayResize(m_strategies, 0, 128); } //+------------------------------------------------------------------+ //| Add strategy | //+------------------------------------------------------------------+ void CReceiver::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); } //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CReceiver::Correct() { return true; }
この基本クラスは、特定のボリューム調整メカニズムの実装を含みません。したがって、このクラスの異なる子孫で、調整の異なる実装を作成することができるでしょう。このクラスオブジェクトは、今のところ、マーケットポジションを建てる戦略のスタブとして機能します。調整メカニズムをデバッグするにはこれが必要になります。つまり、戦略自身が実際の取引をおこなうEAがどのポジションを建てるのか、戦略が仮想取引のみをおこなうEAがどのポジションを建てるのかを比較する必要があります。
そこで、前回の戦略そのものが実際の取引をおこなうEAを2つ用意します。
最初のEAでは、戦略は1つのインスタンスになります。そのパラメータで、最適化のための単一戦略インスタンスのパラメータを指定することができます。
2番目のEAには、1つ目のEAの最適化の結果として得られた、あらかじめ定義されたパラメータを持つ取引戦略のインスタンスが複数含まれます。
戦略パラメータを最適化するEA
前回は、CStrategyクラスのオブジェクトの形ではなく、戦略の実装を使用して戦略のパラメータを最適化しました。しかし、今ではすでにCSimpleVolumesStrategyという既成のクラスがあるので、EAにこの戦略のインスタンスを1つ組み込む別のプログラムを作成しましょう。このクラスは、戦略自体が市場ポジションを開くことを強調するために、少し異なる名前で呼ばれます。CSimpleVolumesStrategyの代わりにCSimpleVolumesMarketStrategyを使用し、現在のフォルダのSimpleVolumesMarketStrategy.mqhファイルに保存します。
EAファイルでは、戦略のパラメータをEAの入力変数からロードさせ、EAオブジェクトに戦略のインスタンスを1つ追加します。戦略パラメータを最適化できるEAを手に入れます。
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" #include "VolumeReceiver.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input string symbol_ = "EURGBP"; // Trading instrument (symbol) input ENUM_TIMEFRAMES timeframe_ = PERIOD_H1; // Chart period input group "=== Opening signal parameters" input int signalPeriod_ = 130; // Number of candles for volume averaging input double signalDeviation_ = 0.9; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.4; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 0; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 475; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magic_ = 27181; // Magic CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(); expert.Add(new CSimpleVolumesMarketStrategy( magic_, symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete expert; }
SimpleVolumesMarketExpertSingle.mq5ファイルの現在のフォルダに保存しましょう。
では、課題の実施を単純化するために、取引戦略を少し複雑にしてみましょう。未決注文ではなく、マーケットポジションを使用する戦略を仮想取引に移行する方が簡単でしょう。現在の戦略のバージョンは、未決済注文でのみ機能します。openDistance_戦略にパラメータ値分析を追加してみましょう。もしそれがゼロより上なら、戦略はBUY_STOPとSELL_STOPの予約注文を出します。もしそれがゼロより下であれば、戦略はBUY_LIMITとSELL_LIMITの予約注文を出します。この値がゼロの場合、成り行き注文を出します。
これをおこなうには、CSimpleVolumesMarketStrategy::OpenBuyOrder()とCSimpleVolumesMarketStrategy::OpenSellOrder()メソッドのコードを変更するだけです。
void CSimpleVolumesMarketStrategy::OpenBuyOrder() { // Previous code in the method ... // Order volume double lot = m_fixedLot; // Set a pending order bool res = false; if(openDistance_ > 0) { res = trade.BuyStop(lot, ...); } else if(openDistance_ < 0) { res = trade.BuyLimit(lot, ...); } else { res = trade.Buy(lot, ...); } if(!res) { Print("Error opening order"); } }
戦略に必要なもう1つの変更は、初期化コードをInit()メソッドから戦略のコンストラクタに移すことです。これは、そのコードが戦略コンストラクタ内に配置されていると仮定して、EAが戦略の初期化メソッドを呼び出さないようにするために必要なことです。
新しいEAをコンパイルし、3つの銘柄(EURGBP、GBPUSD、EURUSD)を使用してH1に最適化するように設定してみましょう。
図1:パラメータ[EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01]のテスト結果
最適化の結果からパラメータの良いオプションをいくつか選択し(例えば、各銘柄について3つのオプション)、選択したパラメータを持つ戦略のインスタンスを9つ作成する2番目のEAを作成してみましょう。それぞれの事例について、1つの戦略のドローダウンが10%を超えない最適なポジションのサイズを計算します。計算メソッドは前回の記事で説明しました。
EAのパフォーマンスの変化を示すために、対象とする戦略を設定できるようにします。そのためにまず、9つの要素からなる配列にすべての戦略インスタンスを配置します。戦略配列の初期インデックスを設定するstartIndex_入力を追加しましょう。totalStrategies_ パラメータは、startIndex_を起点として、配列からいくつの戦略を連続して起動するかを決定します。初期化の最後に、配列から対応する戦略をEAオブジェクトに追加します。
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" input int startIndex_ = 0; // Starting index input int totalStrategies_ = 1; // Number of strategies input double depoPart_ = 1.0; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Check if the parameters are correct if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) { return INIT_PARAMETERS_INCORRECT; } // Create and fill the array of strategy instances CStrategy *strategies[9]; strategies[0] = new CSimpleVolumesMarketStrategy( magic_ + 0, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 13, 0.3, 1.0, 0, 10500, 465, 1000, 3); strategies[1] = new CSimpleVolumesMarketStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 17, 1.7, 0.5, 0, 16500, 220, 1000, 3); strategies[2] = new CSimpleVolumesMarketStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 51, 0.5, 1.1, 0, 19500, 370, 22000, 3); strategies[3] = new CSimpleVolumesMarketStrategy( magic_ + 3, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.25 * depoPart_, 2), 80, 1.1, 0.2, 0, 6000, 1190, 1000, 3); strategies[4] = new CSimpleVolumesMarketStrategy( magic_ + 4, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 128, 2.0, 0.9, 0, 2000, 1170, 1000, 3); strategies[5] = new CSimpleVolumesMarketStrategy( magic_ + 5, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.14 * depoPart_, 2), 13, 1.5, 0.8, 0, 2500, 1375, 1000, 3); strategies[6] = new CSimpleVolumesMarketStrategy( magic_ + 6, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.23 * depoPart_, 2), 24, 0.1, 0.3, 0, 7500, 2400, 24000, 3); strategies[7] = new CSimpleVolumesMarketStrategy( magic_ + 7, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.20 * depoPart_, 2), 18, 0.2, 0.4, 0, 19500, 1480, 6000, 3); strategies[8] = new CSimpleVolumesMarketStrategy( magic_ + 8, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.22 * depoPart_, 2), 128, 0.7, 0.3, 0, 3000, 170, 42000, 3); expert = new CAdvisor(); // Add the necessary strategies to the EA for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) { expert.Add(strategies[i]); } return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
このおかげで、戦略配列の戦略の初期インデックスを最適化することで、戦略のインスタンスごとに結果を得ることができます。10万ドルの初期入金で開始すると、結果は以下のようになります。
図2:9つの戦略インスタンスの単一実行結果
ドローダウンは、建てるポジションの最適なサイズを選択する際に計画したとおり、初期入金の約1%、すなわち約1000ドルであることは明らかです。平均シャープレシオは1.3です。
では、すべてのインスタンスをオンにして、ドローダウンを1000ドルに維持するために、適切なdepoPart_乗数を選択してみましょう。depoPart_ = 0.38の場合、ドローダウンは許容範囲内にとどまります。
図3:9つの戦略インスタンスの同時運用テスト結果
戦略の単一コピーの結果と、すべてのコピーの同時作業の結果を比較すると、同じドローダウンで利益が約3倍に増加し、シャープレシオも1.3から2.84に増加したことがわかります。
さて、仕事の中核に集中しましょう。
仮想ポジションのクラス(注文)
約束したCVirtualOrderクラスを作成し、ポジションのすべてのプロパティを格納するフィールドを追加します。
class CVirtualOrder { private: //--- Order (position) properties ulong m_id; // Unique ID string m_symbol; // Symbol double m_lot; // Volume ENUM_ORDER_TYPE m_type; // Type double m_openPrice; // Open price double m_stopLoss; // StopLoss level double m_takeProfit; // TakeProfit level string m_comment; // Comment datetime m_openTime; // Open time //--- Closed order (position) properties double m_closePrice; // Close price datetime m_closeTime; // Close time string m_closeReason; // Closure reason double m_point; // Point value bool m_isStopLoss; // StopLoss activation property bool m_isTakeProfit;// TakeProfit activation property };
各仮想ポジションは一意のIDを持つべきです。そこで、s_count クラスのstatic変数を追加して、プログラム中に生成されたすべてのポジションオブジェクトの数をカウントすることにしましょう。新しいポジションオブジェクトが作成されると、このカウンタは1だけインクリメントされ、この値がユニークなポジション番号となります。s_countの初期値を0に設定します。
また、価格情報のためにCsymbolInfoクラスのオブジェクトが必要になります。これもクラスのstaticメンバーにしましょう。
class CVirtualOrder { private: static int s_count; static CSymbolInfo s_symbolInfo; //--- Order (position) properties ... }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo;
仮想ポジションオブジェクトの作成と仮想ポジションを「建てる」のは異なる操作であることに注意すべきです。ポジションオブジェクトは事前に作成し、戦略が仮想ポジションをオープンしたい瞬間を待つことができます。この時、ポジションのプロパティには、銘柄、出来高、始値などの現在の値が入力されます。戦略がポジションのクローズを決定すると、オブジェクトはクローズプロパティの値(価格、時間、クローズの理由)を保存します。仮想ポジションを建てる次の操作では、オブジェクトの同じインスタンスを使用し、そのクローズプロパティをクリアして、銘柄、出来高、始値などの新しい値で再度埋めることができます。
このクラスにメソッドを追加してみましょう。仮想ポジションを開いたり閉じたりするpublicメソッドとコンストラクタが必要です。ポジションの状態(開いているのか、どの方向なのか)と、その最も重要なプロパティ(出来高と現在の利益)を確認するメソッドも便利です。
class CVirtualOrder { //--- Previous code... public: CVirtualOrder(); // Constructor //--- Methods for checking the order (position) status bool IsOpen(); // Is the order open? bool IsMarketOrder(); // Is this a market position? bool IsBuyOrder(); // Is this an open BUY position? bool IsSellOrder(); // Is this an open SELL position? //--- Methods for obtaining order (position) properties double Volume(); // Volume with direction double Profit(); // Current profit //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = true); // Opening an order (position) bool Close(); // Closing an order (position) };
これらのメソッドのいくつかは、実装が非常にシンプルで短いので、例えばクラス宣言のすぐ内側に置くことができます。
class CVirtualOrder : public CObject { // ... //--- Methods for checking the order (position) status bool IsOpen() { // Is the order open? return(this.m_openTime > 0 && this.m_closeTime == 0); }; bool IsMarketOrder() { // Is this a market position? return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL); } // ... };
コンストラクタは、初期化リストを通じて、一意なIDを除く仮想ポジションのすべてのプロパティに空の(明らかに無効という意味での)値を代入します。すでに述べたように、コンストラクタでは、IDは以前に作成されたクラスオブジェクトのカウンタの値から取得した値が割り当てられます。この値は、EA全体を通してそのまま維持されます。割り当てる前に、作成されたオブジェクトのカウンタをインクリメントします。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder() : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_symbol(""), m_lot(0), m_type(-1), m_openPrice(0), m_stopLoss(0), m_takeProfit(0), m_openTime(0), m_comment(""), m_closePrice(0), m_closeTime(0), m_closeReason(""), m_point(0) { }
CVirtualOrderクラスのメソッドをさらに実装する前に、少し先を見て、このクラスをどのように使用するかを考えてみましょう。実際のマーケットポジションを建てる戦略オブジェクトはあります。さらに、オープンマーケットポジションの最大数(戦略パラメータで設定)もわかっています。仮想ポジションに移行しますが、そうなると、その数も限られてきます。つまり、戦略内に仮想ポジションオブジェクトの配列を作成し、戦略を初期化する際に必要な数の仮想ポジションをその配列に入れ、その後、この配列のみを操作することができます。
新しいポジションを建てる条件が整ったら、まだ建てていない仮想ポジションを配列から取り出し、ポジションに変えます。ポジションを強制的にクローズする条件が整えば、クローズポジションに転換します。
仮想ポジションがある限り、どの戦略もこれらのオブジェクトをティックごとに同じように処理しなければなりません。すべてを順番に調べ、StopLossまたはTakeProfitレベルに達したかどうかを確認し、Yesの場合はポジションをクローズします。この同一性によって、仮想ビヘイビアポジションを処理する実装を独自のクラスに移し、戦略から対応するメソッドだけを呼び出すことができます。
CVirtualOrderクラスはTick()メソッドを受け取ります。Tick()メソッドは、ポジションをクローズするための条件を確認し、もし条件が満たされれば、ポジションはクローズ状態に移行します。ポジションの状態に変化があれば、このメソッドはtrueを返します。
また、複数の仮想ポジションオブジェクトを一度に処理するTick()静的メソッドも追加しましょう。そのようなオブジェクトの配列へのリンクをパラメータとして受け取ります。Tick()メソッドは、各配列オブジェクトに対して呼び出されます。少なくとも1つの仮想ポジションがクローズしていれば、最終的にtrueが返されます。
class CVirtualOrder { private: //... public: //... //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = false ); // Open order (position) bool Tick(); // Handle tick for an order (position) bool Close(); // Close an order (position) static bool Tick(CVirtualOrder &orders[]); // Handle a tick for the array of virtual orders }; //... //+------------------------------------------------------------------+ //| Handle a tick of a single virtual order (position) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick() { if(IsMarketOrder()) { // If this is a market virtual position if(CheckClose()) { // Check if SL or TP levels have been reached Close(); // Close when reached return true; // Return the fact that there are changes in open positions } } return false; } //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick(CVirtualOrder &orders[]) { bool isChanged = false; // We assume that there will be no changes for(int i = 0; i < ArraySize(orders); i++) { // For all orders (positions) isChanged |= orders[i].Tick(); // Check and close if necessary } return isChanged; } //+------------------------------------------------------------------+
このコードを現在のフォルダのVirtualOrder.mqhファイルに保存しましょう。
シンプルな取引戦略クラスの改善
次に、取引戦略クラスに戻り、仮想ポジションを扱えるように変更を加えましょう。すでに合意したように、基となるCStrategyクラスには、仮想ポジションオブジェクトを格納するためのm_orders[]配列がすでにあります。したがって、CSimpleVolumesStrategyクラスでも利用可能です。この戦略にはm_maxCountOfOrdersパラメータがあり、これは同時に建てるポジションの最大数を決定します。そして、コンストラクタの仮想ポジションの配列のサイズを、このパラメータと等しく設定します。
次に、OpenBuyOrder()メソッドとOpenSellOrder()メソッドで実際のポジションを建てるのを、仮想ポジションを建てるのに置き換えるだけです。現在のところ、実際の予約注文の発注を置き換えるものは何もないので、これらの操作をコメントアウトするだけにします。
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenBuyOrder() { // ... if(m_openDistance > 0) { /* // Set BUY STOP pending order res = trade.BuyStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set BUY LIMIT pending order res = trade.BuyLimit(lot, ...); */ } else { // Open a virtual BUY position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... } //+------------------------------------------------------------------+ //| Open SELL order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenSellOrder() { // ... if(m_openDistance > 0) { /* // Set SELL STOP pending order res = trade.SellStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set SELL LIMIT pending order res = trade.SellLimit(lot, ...); */ } else { // Open a virtual SELL position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... }
現在のフォルダのSimpleVolumesStrategy.mqhファイルに変更を保存します。
オープンボリュームをマーケットポジションに変換するクラスの作成
オープンボリュームをマーケットポジションに変換するオブジェクトの基本クラスはすでに作成しました。これまでのところ、使用されている戦略の配列を入力する以外何もおこなわれません。次に、マーケットにポジションを建てるための特定の実装を含む派生クラスを書く必要があります。CVolumeReceiverクラスを作成しましょう。Correct()メソッドを実装するために、かなり多くのコードを追加する必要があります。これをいくつかのprotectedクラスのメソッドに分解します。
#include "Receiver.mqh" //+------------------------------------------------------------------+ //| Class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CVolumeReceiver : public CReceiver { protected: bool m_isNetting; // Is this a netting account? string m_symbols[]; // Array of used symbols double m_minMargin; // Minimum margin for opening CPositionInfo m_position; CSymbolInfo m_symbolInfo; CTrade m_trade; // Filling the array of open market volumes by symbols void FillSymbolVolumes(double &oldVolumes[]); // Correction of open volumes using the array of volumes virtual bool Correct(double &symbolVolumes[]); // Volume correction for this symbol bool CorrectPosition(string symbol, double oldVolume, double diffVolume); // Auxiliary methods bool ClearOpen(string symbol, double diffVolume); bool AddBuy(string symbol, double volume); bool AddSell(string symbol, double volume); bool CloseBuyPartial(string symbol, double volume); bool CloseSellPartial(string symbol, double volume); bool CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type); bool CloseFull(string symbol = ""); bool FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type); public: CVolumeReceiver(ulong p_magic, double p_minMargin = 100); // Constructor virtual void Add(CStrategy *strategy) override; // Add strategy virtual bool Correct() override; // Adjustment of open volumes };
オープンボリューム補正法の一般的なアルゴリズムは以下の通りです。
- 使用した各銘柄について、すべての戦略を調べ、使用した各銘柄のオープンボリュームの合計を計算します。結果のnewVolumes配列は、次のオーバーロードされたCorrect()メソッドに渡されます。
//+------------------------------------------------------------------+ //| Adjustment of open volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct() { int symbolsTotal = ArraySize(m_symbols); double newVolumes[]; ArrayResize(newVolumes, symbolsTotal); ArrayInitialize(newVolumes, 0); for(int j = 0; j < symbolsTotal; j++) { // For each used symbol for(int i = 0; i < ArraySize(m_strategies); i++) { // Iterate through all strategies if(m_strategies[i].Symbol() == m_symbols[j]) { // If the strategy uses this symbol newVolumes[j] += m_strategies[i].Volume(); // Add its open volume } } } // Call correction of open volumes using the array of volumes return Correct(newVolumes); }
- 各銘柄について、その銘柄のポジションの数量をどの程度変更するかを定義します。必要に応じて、この銘柄のボリューム補正メソッドを呼び出します。
//+------------------------------------------------------------------+ //| Adjusting open volumes using the array of volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct(double &newVolumes[]) { // ... bool res = true; // For each symbol for(int j = 0; j < ArraySize(m_symbols); j++) { // ... // Define how much the volume of open positions for the symbol should be changed double oldVolume = oldVolumes[j]; double newVolume = newVolumes[j]; // ... double diffVolume = newVolume - oldVolume; // If there is a need to adjust the volume for a given symbol, then do that if(MathAbs(diffVolume) > 0.001) { res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume); } } return res; }
- 1つの銘柄について、前回のオープンボリュームと必要な変化の値に基づいて、実行する必要のある取引操作の種類(追加、クローズ、再オープン)を決定し、対応する補助メソッドを呼び出します。
//+------------------------------------------------------------------+ //| Adjust volume by the symbol | //+------------------------------------------------------------------+ bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) { bool res = false; // ... double volume = MathAbs(diffVolume); if(oldVolume > 0) { // Have BUY position if(diffVolume > 0) { // New BUY position res = AddBuy(symbol, volume); } else if(diffVolume < 0) { // New SELL position if(volume < oldVolume) { res = CloseBuyPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > oldVolume) { res = AddSell(symbol, volume - oldVolume); } } } } else if(oldVolume < 0) { // Have SELL position if(diffVolume < 0) { // New SELL position res = AddSell(symbol, volume); } else if(diffVolume > 0) { // New BUY position if(volume < -oldVolume) { res = CloseSellPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > -oldVolume) { res = AddBuy(symbol, volume + oldVolume); } } } } else { // No old position res = ClearOpen(symbol, diffVolume); } return res; }
現在のフォルダのVolumeReceiver.mqhファイルにコードを保存します。
1つの戦略と仮想ポジションを持つEA
SimpleVolumesMarketExpertSingle.mq5ファイルに基づいて、仮想ポジションを持つ取引戦略のインスタンスを1つ使用するEAを作成します。EAのコンストラクタを呼び出す際に、新しいCVolumeReceiverクラスのオブジェクトを渡し、作成された戦略のクラスを置き換えるために、必要なファイルを接続する必要があります。
#include "Advisor.mqh" #include "SimpleVolumesStrategy.mqh" #include "VolumeReceiver.mqh" // Input parameters... CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(new CVolumeReceiver(magic_)); expert.Add(new CSimpleVolumesStrategy( symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
このコードを現在のフォルダのSimpleVolumesExpertSingle.mq5ファイルに保存します。
リアル取引と仮想取引の比較
同じ戦略のEAを、同じ戦略パラメータを使用して、短い時間間隔で起動してみましょう。ただし、実際のポジションを建てる方法は、直接建てる方法と仮想ポジション経由の2通りあります。これらの結果をレポートに保存し、両方のEAがおこなった取引のリストを見ます。
図4:2つのEAによる取引(仮想ポジションあり、仮想ポジションなし)
幅を小さくするため、銘柄(常にEURGBP)、出来高(常に0.01)など、すべての行で同じ値を持つ列を表から削除しました。見てわかるように、最初のポジションはどちらのケースでも同じ価格、同じ時点で建てられています。売りポジション(2018.03.02 15:46:47売りイン)を建て、買いポジション(2018.03.06 13:56:04買いイン)を建てた場合、仮想ポジションを介して動作するEAは、単に前の売りポジション(2018.03.06 13:56:04買いアウト)をクローズします。最初のEAでは、異なる方向のポジションに対してスワップを支払い続けましたが、2番目のEAではそうではなかったため、全体的な結果はこれより改善しました。
複数の戦略と仮想ポジションを持つEA
SimpleVolumesMarketExpert.mq5ファイルのEAで同様の操作をおこなってみましょう。必要なファイルをインクルードします。EAのコンストラクタを呼び出す際に、新しいCVolumeReceiverクラスのオブジェクトを与え、作成された戦略のクラスを置き換えます。結果をSimpleVolumesExpert.mq5ファイルに保存し、結果を見ます。
図5:9つの戦略インスタンスと仮想ポジションを使用したEAの結果
これらの結果を、仮想ポジションを使用しない類似のEAの結果と比較すると、いくつかの指標に改善が見られます。利益はわずかに増加し、ドローダウンは減少し、シャープレシオとプロフィットファクターも増加しています。
結論
目標達成に向けてまた一歩前進しました。仮想ポジション戦略の使用に移行することで、多数の取引戦略が互いに干渉することなく連携する能力が高まりました。これによって、戦略の各インスタンスを使用して個別に取引するのに比べ、取引に必要な最低保証金を低くすることも可能になります。もう1つのうれしいボーナスは、ネッティング勘定に携わる機会があることです。
しかし、まだやるべきことはたくさんあります。例えば、これまでのところ、マーケットポジションを建てる戦略のみが実装されているが、予約注文は実装されていません。金銭管理に関する問題も将来に残されます。現時点では、ボリュームを固定し、最適なポジションサイズを手動で選択しています。一度に複数の銘柄を扱う戦略(より単純な単一銘柄戦略に分割できない戦略)は、この操作構造を同様に使用することはできません。
アップデートをお楽しみに。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14107
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索