
多通貨エキスパートアドバイザーの開発(第1回):複数取引戦略の連携
現役時代には、さまざまな取引戦略を扱わなければなりませんでした。原則として、EAは1つの取引アイデアだけを実行します。通常、1つの端末で多くのEAを安定的に連携させるのは難しいため、少数の優れたEAだけを選ばざるを得ません。しかし、このような理由で完全に実行可能な戦略を捨ててしまうのは、やはり残念なことです。どうすれば両者を協力させることができるのでしょうか。
問題の定義
何が欲しいのか、何を持っているのかを決める必要があります。
私たちは次を持っています(あるいは、ほとんど持っています)。
- 既製のEAコードまたは取引操作を実行するための定式化されたルールの形で提供される、様々な銘柄や時間枠で機能する様々な取引戦略
- 初期入金
- 最大許容ドローダウン
私たちは次を望んでいます。
- 選択したすべてのストラテジーを1つの口座で複数の銘柄と時間枠で連携する
- 入金をすべてに均等に、または指定された比率に従って分配する
- 最大許容ドローダウンを遵守するために、建てたポジションの数量を自動的に計算する
- 端末の再起動を正しく処理する
- MetaTrader 5および4で起動する
オブジェクト指向のアプローチ、MQL5とMetaTrader 5の標準テスターを使用します。
目の前の課題はかなり大きいので、ステップごとに解決していきます。
最初の段階では、単純な取引のアイデアを考えてみましょう。それを使用して簡単なEAを作ります。それを最適化し、最適な2組のパラメータを選択します。元のシンプルなEAのコピーを2つ含むEAを作成し、その結果を見ます。
取引アイデアから戦略へ
実験的に次のようなアイデアを考えてみましょう。
ある銘柄について集中的な取引が始まると、その銘柄についての取引が低調なときよりも、単位時間当たりの価格が大きく変動することがあるとします。そして、取引が活発化し、価格が何らかの方向に変化したことを確認すれば、おそらく近い将来、同じ方向に変化するでしょう。これで利益を上げましょう。
取引戦略は、取引アイデアに基づくポジション開閉のための一連のルールです。未知のパラメータは含まれていません。この一連のルールによって、ストラテジーが稼働しているどの時点でも、ポジションを建てるべきかどうか、建てる場合はどのポジションを建てるべきかを判断できるはずです。
そのアイデアを戦略に変えてみましょう。まず第一に、取引強度の増加をどうにかして検出する必要があります。これがなければ、いつポジションを建てるべきか判断できません。これには、ティックボリューム、つまり、現在のローソク足の間に端末が受信した新しい価格の数を使用します。ティックボリュームが大きければ大きいほど、取引が激しくなっている証拠とみなされます。しかし、銘柄によって強さは大きく異なります。従って、ティックボリュームに単一のレベルを設定することはできず、その超過分を集中取引の開始とみなします。そして、このレベルを決定するには、いくつかのローソク足の平均ティックボリュームから始めることができます。少し考えてみましたが、次のような説明ができます。
ローソク足のティックボリュームが、現在のローソク足の方向の平均ボリュームを超えた瞬間に、ペンディングオーダーを発注します。各注文には有効期限があり、それを過ぎると削除されます。未決注文がポジションになった場合、指定されたストップロスとテイクプロフィットのレベルに達した時のみ決済されます。ティックボリュームが平均をさらに上回った場合、すでに開いている未決注文に加えて追加注文が発注される可能性があります。
これはより詳細な説明ですが、完全なものではありません。そのため、もう一度読み直し、明確でない箇所をすべて強調します。そこではより詳細な説明が必要です。
以下がその質問です。
- 予約注文を出す...出すべき予約注文の種類
- ... 平均出来高...ローソク足の平均出来高を計算する方法
- ...平均出来高を超える...平均出来高の超過を判断する方法
- ..ティックボリュームがさらに平均を上回る場合...-より大きな超過を判断する方法
- ...追加注文も可能-可能な注文の合計数
どんな予約注文を出すべきか:この考えに基づいて、価格がローソク足の始まりから同じ方向に動き続けることを願っています。例えば、現在価格がローソク足の始点よりも高ければ、予約買い注文を出します。BUY_LIMITを建てた場合、それが機能するためには、まず価格が少し戻り(下落)、そして建てたポジションが利益を上げるためには、価格が再び上昇する必要があります。BUY_STOPを建てた場合、ポジションを建てるには、価格がもう少し動き続け(上昇し)、さらに上昇して利益を上げる必要があります。
これらの選択肢のうち、どちらが優れているかはすぐにはわかりません。したがって、簡単にするために、常に逆指値注文(BUY_STOPとSELL_STOP)を建てるすることにしましょう。将来的には、この値をストラテジーのパラメータとし、その値によってどの注文を出すかを決定することができます。
ローソク足の平均出来高を計算する方法:平均出来高を計算するには、その出来高が平均計算に含まれるローソク足を選択する必要があります。最後に閉じたローソク足の連続した数を見てみましょう。そして、ローソク足の本数を設定すれば、平均ティックボリュームを計算することができます。
平均出来高の超過を判断する方法:以下の条件
V > V_avr
(ここで
Vは現在のローソク足のティックボリューム、
V_avrは平均ティックボリューム)
を取ると、約半数のローソク足がこの条件を満たすことになります。この考え方に基づけば、出来高が平均を大きく上回ったときだけ注文を出せばよいことになります。そうでなければ、これまでのローソク足とは異なり、このローソク足がより激しい取引の兆候であるとはまだ考えられません。例えば、以下の式を使用することができます。
V > V_avr + D * V_avr
ここでDは数値の比率です。D = 1の場合、現在の出来高が平均の2倍を超えたときに注文を出し、例えばD = 2の場合、現在の出来高が平均の3倍を超えたときに注文を出します。
ただし、この条件を適用できるのは1つの注文だけです。2つ目以降の注文に適用すると、最初の注文の直後に注文が開始されるからです。これは、数量がより大きい注文を1つ出すだけで置き換えることができます。
より大きな超過を判断する方法: そのために、条件式にもう1つパラメータを追加してみましょう。
V > V_avr + D * V_avr + N * D * V_avr
そして、2番目の注文が1番目の注文の後に開くためには(つまり、N=1)、次の条件を満たす必要があります。
V > V_avr + 2 * D * V_avr
1番目の注文(N = 0)を開くと、方程式はすでに知られている形になります。
V > V_avr + D * V_avr
最後に、冒頭の方程式を修正します。同じDではなく、2つの独立したパラメータDとD_addを最初の注文とそれ以降の注文にします。
V > V_avr + D * V_avr + N * D_add * V_avr
V > V_avr * (1 + D + N * D_add)
これによって、戦略の最適なパラメータを選択する自由度が高まると思われます。
もし私たちの条件がN値を注文とポジションの総数として使用するならば、各予約注文は別々のポジションに変わり、すでに建てられているポジションの出来高を増加させないことを意味します。したがって、今のところ、このような戦略の適用範囲は、ポジションを独立会計(「ヘッジ」)で処理する勘定に限定せざるを得ありません。
すべてが明確になったら、異なる値が取り得る量を列挙します。これらが戦略の入力となります。注文を出すには、出来高、現在価格からの距離、有効期限、損切りと利食いのレベルを知る必要があることを考慮に入れましょう。そして、次のような記述があります。
EAはヘッジ口座の特定の銘柄と期間(時間枠)で実行されます。
入力を設定します。
- k:出来高平均用ローソク足の本数
- D:1番目の注文を出す際の平均からの相対的乖離
- D_add:2番目以降の注文を出す際の平均からの相対的乖離
- 価格から未決注文までの距離
- 損切り(ポイント単位)
- 利食い(ポイント単位)
- 予約注文の有効期限(分単位)
- N_max:同時に出す注文の最大数
- 単一注文量
予約注文とポジションの数(N)を求めます。
もしそれがN_maxより小さい場合:
直近K本のローソク足の平均ティックボリュームを計算し、V_avr値を取得します。
V > V_avr * (1 + D + N * D_add)の条件を満たす場合:
現在のローソク足の価格変動の方向を決定します。価格が上昇した場合、BUY_STOP注文を出し、そうでなければSELL_STOP注文を出します。
パラメータで指定された距離、有効期限、StopLossとTakeProfitのレベルで未決注文を発注します。
取引戦略の実行
コードを書き始めましょう。まず、すべてのパラメータをリストアップし、わかりやすくするためにグループに分け、各パラメータにコメントを付けます。これらのコメント(ある場合)は、EA起動時のパラメータダイアログとストラテジーテスターのパラメータタブに、私たちが選んだ変数名の代わりに表示されます。
とりあえず、デフォルト値をいくつか設定しておいて、最適化中に最良なものを探します。
input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // 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 magicN_ = 27181; // Magic
EAは取引操作をおこなうので、CTradeクラスのグローバルオブジェクトを作成します。オブジェクトメソッドを呼び出して、未決注文を発注します。
CTrade trade; // Object for performing trading operations
グローバル変数(またはオブジェクト)は、EAコード内の関数の外側で宣言された変数(またはオブジェクト)であることに留意してください。そのため、すべてのEA関数で利用できます。グローバル端末変数と混同しないでください。
新規注文のパラメータを計算するには、現在の価格と、EAが起動するその他の銘柄のプロパティを取得する必要があります。そのためには、CsymbolInfoクラスのグローバルオブジェクトを作成します。
CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties
また、未決注文とポジションの数も数える必要があります。これを実現するために、未決注文とポジションのデータを取得するために使用するCOrderInfoクラスとCPositionInfoクラスのグローバルオブジェクトを作成しましょう。数量そのものを2つのグローバル変数、countOrdersとcountPositionsに格納します。
COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions
複数のローソク足の平均ティックボリュームを計算するには、例えば、iVolumesテクニカル指標を使用できます。その値を取得するには、この指標のハンドル(EAで使用する他のすべての指標のうち、この指標のシリアル番号を格納する整数)を格納する変数が必要です。平均出来高を求めるには、まず指標バッファの値をあらかじめ用意した配列にコピーする必要があります。この配列もグローバルにします。
int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves)
これで、OnInit() EA初期化関数とOnTick()ティック処理関数に進むことができます。
初期化中にできることは以下の通りです。
- ティックボリュームを取得する指標を読み込み、そのハンドルを記憶する
- 平均出来高を計算するために、ローソク足の本数に応じて受信配列のサイズを設定し、そのアドレスを時系列のように設定する
- 取引オブジェクトを通じて注文を発注するためのマジックナンバーを設定する
初期化関数はこのようになります。
int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); }
ストラテジーの説明によると、ティック処理関数で未決注文とポジションの数を見つけることから始める必要があります。これを別のUpdateCounts()関数として実装してみましょう。この関数では、すべての未決ポジションと注文を調べ、マジックナンバーがEAのものと一致するものだけをカウントします。
void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } }
次に、未決ポジションと注文の数が、設定で指定した数を超えていないことを確認します。この場合、新規注文の条件が満たされているかどうかを確認する必要があります。この確認は、別のSignalForOpen()関数として実装します。これは3つの可能な値のいずれかを返します。
- +1:BUY_STOP注文を出すシグナル
- 0:シグナルなし
- -1:SELL_STOP注文を出すシグナル
未決注文を出すために、 OpenBuyOrder()とOpenSellOrder()の2つの関数を書きます。
これでOnTick()関数の完全な実装を書くことができます。
void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } }
この後、残りの関数の実装を追加すれば、EAコードの完成です。現在のフォルダのSimpleVolumes.mq5ファイルに保存しましょう。
#include <Trade\OrderInfo.mqh> #include <Trade\PositionInfo.mqh> #include <Trade\SymbolInfo.mqh> #include <Trade\Trade.mqh> input group "=== Opening signal parameters" input int signalPeriod_ = 48; // Number of candles for volume averaging input double signalDeviation_ = 1.0; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.0; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 200; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 75; // 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 magicN_ = 27181; // Magic CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) //+------------------------------------------------------------------+ //| Initialization function of the expert | //+------------------------------------------------------------------+ int OnInit() { // Load the indicator to get tick volumes iVolumesHandle = iVolumes(Symbol(), PERIOD_CURRENT, VOLUME_TICK); // Set the size of the tick volume receiving array and the required addressing ArrayResize(volumes, signalPeriod_); ArraySetAsSeries(volumes, true); // Set Magic Number for placing orders via 'trade' trade.SetExpertMagicNumber(magicN_); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| "Tick" event handler function | //+------------------------------------------------------------------+ void OnTick() { // Count open positions and orders UpdateCounts(); // If their number is less than allowed if(countOrders + countPositions < maxCountOfOrders_) { // Get an open signal int signal = SignalForOpen(); if(signal == 1) { // If there is a buy signal, then OpenBuyOrder(); // open the BUY_STOP order } else if(signal == -1) { // If there is a sell signal, then OpenSellOrder(); // open the SELL_STOP order } } } //+------------------------------------------------------------------+ //| Calculate the number of open orders and positions | //+------------------------------------------------------------------+ void UpdateCounts() { // Reset position and order counters countPositions = 0; countOrders = 0; // Loop through all positions for(int i = 0; i < PositionsTotal(); i++) { // If the position with index i is selected successfully and its Magic is ours, then we count it if(positionInfo.SelectByIndex(i) && positionInfo.Magic() == magicN_) { countPositions++; } } // Loop through all orders for(int i = 0; i < OrdersTotal(); i++) { // If the order with index i is selected successfully and its Magic is the one we need, then we consider it if(orderInfo.SelectByIndex(i) && orderInfo.Magic() == magicN_) { countOrders++; } } } //+------------------------------------------------------------------+ //| Open the BUY_STOP order | //+------------------------------------------------------------------+ void OpenBuyOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = ask + distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price - stopLevel_ * point, digits); double tp = NormalizeDouble(price + (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.BuyStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Open the SELL_STOP order | //+------------------------------------------------------------------+ void OpenSellOrder() { // Update symbol current price data symbolInfo.Name(Symbol()); symbolInfo.RefreshRates(); // Retrieve the necessary symbol and price data double point = symbolInfo.Point(); int digits = symbolInfo.Digits(); double bid = symbolInfo.Bid(); double ask = symbolInfo.Ask(); int spread = symbolInfo.Spread(); // Let's make sure that the opening distance is not less than the spread int distance = MathMax(openDistance_, spread); // Opening price double price = bid - distance * point; // StopLoss and TakeProfit levels double sl = NormalizeDouble(price + stopLevel_ * point, digits); double tp = NormalizeDouble(price - (takeLevel_ + spread) * point, digits); // Expiration time datetime expiration = TimeCurrent() + ordersExpiration_ * 60; // Order volume double lot = fixedLot_; // Set a pending order bool res = trade.SellStop(lot, NormalizeDouble(price, digits), Symbol(), NormalizeDouble(sl, digits), NormalizeDouble(tp, digits), ORDER_TIME_SPECIFIED, expiration); if(!res) { Print("Error opening order"); } } //+------------------------------------------------------------------+ //| Signal for opening pending orders | //+------------------------------------------------------------------+ int SignalForOpen() { // By default, there is no signal int signal = 0; // Copy volume values from the indicator buffer to the receiving array int res = CopyBuffer(iVolumesHandle, 0, 0, signalPeriod_, volumes); // If the required amount of numbers have been copied if(res == signalPeriod_) { // Calculate their average value double avrVolume = ArrayAverage(volumes); // If the current volume exceeds the specified level, then if(volumes[0] > avrVolume * (1 + signalDeviation_ + (countOrders + countPositions) * signaAddlDeviation_)) { // if the opening price of the candle is less than the current (closing) price, then if(iOpen(Symbol(), PERIOD_CURRENT, 0) < iClose(Symbol(), PERIOD_CURRENT, 0)) { signal = 1; // buy signal } else { signal = -1; // otherwise, sell signal } } } return signal; } //+------------------------------------------------------------------+ //| Number array average value | //+------------------------------------------------------------------+ double ArrayAverage(const double &array[]) { double s = 0; int total = ArraySize(array); for(int i = 0; i < total; i++) { s += array[i]; } return s / MathMax(1, total); } //+------------------------------------------------------------------+
それでは、2018-01-01から2023-01-01までのMetaQuotes相場におけるEURGBP H1のEAパラメータを、100,000ドルの開始保証金と0.01の最小ロットで最適化し始めましょう。同じEAを異なるブローカーの気配値でテストした場合、結果が若干異なる可能性があることに注意してください。これらの結果が大きく異なることもあります。
次のような結果となる2つの素敵なパラメータセットを選択してみましょう。
図1:130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01のテスト結果
図2: 159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01のテスト結果
このテストが多額の初期入金で実施されたのは偶然ではありません。その理由は、EAが一定の取引量を持つポジションを建てた場合、ドローダウンが利用可能な資金よりも大きくなると、取引が早期に終了する可能性があるからです。この場合、同じパラメータを使用しながら、損失を回避するために未決ポジションの数量を合理的に減らす(または、同等の意味で、初期入金を増やす)ことが可能であったかどうかはわかりません。
例を見てみましょう。初期入金が1,000ドルだとします。テスターで実行したところ、次のような結果が出ました。
- 最終入金額は11,000ドル(利益1,000%、EAが獲得した+10,000ドルを最初の1,000ドルに上乗せ)
- ドローダウンの最大絶対額は2,000ドル
明らかに、EAが保証金を2,000ドル以上に増やした後に、このようなドローダウンが起こったのは幸運でした。そのため、テスター実行が完了し、これらの結果を見ることができました。もしこのようなドローダウンがもっと早く起きていたら(例えば、テスト期間の開始時期を別のものにしていたら)、預金全額を失っていたでしょう。
もし手動でおこなうのであれば、パラメータで量を変更したり、スタート時の預託金を増やしたりして、再度実行を開始することができます。しかし、最適化中に実行されるのであれば、これは不可能です。この場合、資金管理の設定が正しく選択されていないために、潜在的に良いパラメータセットが拒否される可能性があります。このような結果になる可能性を減らすために、最初は非常に多額の初期入金と最小取引量で最適化を実行することができます。
例に戻ると、初期入金が100,000ドルだった場合、2,000ドルのドローダウンを繰り返しても、預金全体の損失は発生せず、テスターはこのような結果を受け取ることになります。そして、私たちが許容できる最大ドローダウンが10%であるならば、初期入金は少なくとも20,000ドルであるべきだと計算できます。この場合、収益性はわずか50%(最初の20,000ドルに対してEAが+10,000ドル獲得)となります。
初期入金を10,000ドル、許容ドローダウンを初期入金の10%とした場合のパラメータの2つの選択された組み合わせについて同様の計算をしてみましょう。
パラメータ | ロット | 損失率 | 利益 | 許容 ドローダウン | 許容 ロット | 許容 ゲイン |
---|---|---|---|---|---|---|
L | D | P | Da | La = L * (Da / D) | Pa = P * (Da / D) | |
[130, 0.9, 1.4, 231, 3750, 50, 600, 3, 0.01] | 0.01 | 28.70 (0.04%) | 260.41 | 1000 (10%) | 0.34 | 9073 (91%) |
[159, 1.7, 0.8, 248, 3600, 495, 39000, 3, 0.01] | 0.01 | 92.72 (0.09%) | 666.23 | 1000 (10%) | 0.10 | 7185 (72%) |
見てわかるように、どちらの入力オプションもほぼ同じようなリターン(~80%)を得ることができます。最初のオプションは、絶対額で見れば収入は少ないですが、ドローダウンは小さくなります。したがって、この場合、建てたポジションの量を2番目のオプションよりも増やすことができます。これにより、収益は増えますが、ドローダウンも大きくなります。
そこで、いくつかの有望な入力の組み合わせを見つけました。それを1つのEAにまとめます。
基本戦略クラス
すべてのストラテジーに固有のプロパティとメソッドを集めたCStrategyクラスを作りましょう。例えば、どのようなストラテジーも、指標との関係にかかわらず、何らかの銘柄と時間枠を持ちます。また、各戦略にポジションを建てる際のマジックナンバーと、1ポジションのサイズを割り当てます。ここでは簡単のため、ポジションサイズが可変のストラテジーの運用については考えないことにします。これは後で必ずやります。
必要なメソッドのうち、ストラテジーパラメータを初期化するコンストラクタ、初期化メソッド、OnTickイベントハンドラだけが特定できます。次のようなコードが得られます。
class CStrategy : public CObject { protected: ulong m_magic; // Magic string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) public: // Constructor CStrategy(ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot); virtual int Init() = 0; // Strategy initialization - handling OnInit events virtual void Tick() = 0; // Main method - handling OnTick events };
Init()メソッドとTick()メソッドは、純粋な仮想メソッドとして宣言されています(メソッドヘッダの後に= 0が付く)。つまり、これらのメソッドの実装をCStrategyクラスには書かないということです。このクラスに基づいて、Init()メソッドとTick()メソッドが必ず存在し、特定の取引ルールの実装を含む子孫クラスを作成します。
クラスの説明ができました。その後に、必要なコンストラクタの実装を追加します。これはストラテジーオブジェクトが作成されるときに自動的に呼び出されるメソッド関数なので、ストラテジーパラメータが初期化されていることを確認する必要があるのはこのメソッドです。コンストラクタは4つのパラメータを受け取り、その値を初期化リストを通じて対応するクラスメンバー変数に代入します。
CStrategy::CStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot) : // Initialization list m_magic(p_magic), m_symbol(p_symbol), m_timeframe(p_timeframe), m_fixedLot(p_fixedLot) {}
このコードを現在のフォルダのStrategy.mqhファイルに保存します。
取引戦略クラス
元のシンプルなEAのロジックを、新しい子孫クラスCSimpleVolumesStrategyに移してみましょう。そのためには、すべての入力変数とグローバル変数をクラスのメンバーにします。fixedLot_変数とmagicN_変数をCStrategy基本クラスから継承したm_fixedLotとm_magic基本クラスメンバーに置き換えます。
#include "Strategy.mqh" class CSimpleVolumeStrategy : public CStrategy { //--- Open signal parameters int signalPeriod_; // Number of candles for volume averaging double signalDeviation_; // Relative deviation from the average to open the first order double signaAddlDeviation_; // Relative deviation from the average for opening the second and subsequent orders //--- Pending order parameters int openDistance_; // Distance from price to pending order double stopLevel_; // Stop Loss (in points) double takeLevel_; // Take Profit (in points) int ordersExpiration_; // Pending order expiration time (in minutes) //--- Money management parameters int maxCountOfOrders_; // Maximum number of simultaneously open orders CTrade trade; // Object for performing trading operations COrderInfo orderInfo; // Object for receiving information about placed orders CPositionInfo positionInfo; // Object for receiving information about open positions int countOrders; // Number of placed pending orders int countPositions; // Number of open positions CSymbolInfo symbolInfo; // Object for obtaining data on the symbol properties int iVolumesHandle; // Tick volume indicator handle double volumes[]; // Receiver array of indicator values (volumes themselves) };
OnInit()関数とOnTick()関数はInit()とTick()のpublicメソッドになり、その他の関数はすべて CSimpleVolumesStrategyクラスの新しいprivateメソッドになります。publicメソッドは、例えばEAオブジェクトのメソッドなど、外部のコードからストラテジーのために呼び出すことができます。privateメソッドは、指定されたクラスのメソッドからのみ呼び出すことができます。クラスの説明にメソッドヘッダーを追加してみましょう。
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code double volumes[]; // Receiver array of indicator values (volumes themselves) //--- Methods void UpdateCounts(); // Calculate the number of open orders and positions int SignalForOpen(); // Signal for opening pending orders void OpenBuyOrder(); // Open the BUY_STOP order void OpenSellOrder(); // Open the SELL_STOP order double ArrayAverage( const double &array[]); // Average value of the number array public: //--- Public methods virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
これらの関数の実装がある場所では、「CSimpleVolumesStrategy::」という接頭辞を関数の名前に追加し、コンパイラにこれらが単なる関数ではなく、クラスの関数メソッドであることを明確にします。
class CSimpleVolumeStrategy : public CStrategy { // Class description listing properties and methods... }; int CSimpleVolumeStrategy::Init() { // Function code ... } void CSimpleVolumeStrategy::Tick() { // Function code ... } void CSimpleVolumeStrategy::UpdateCounts() { // Function code ... } int CSimpleVolumeStrategy::SignalForOpen() { // Function code ... } void CSimpleVolumeStrategy::OpenBuyOrder() { // Function code ... } void CSimpleVolumeStrategy::OpenSellOrder() { // Function code ... } double CSimpleVolumeStrategy::ArrayAverage(const double &array[]) { // Function code ... }
元のシンプルなEAでは、入力値は宣言された時点で代入されていました。コンパイルされたEAを起動すると、入力パラメータダイアログの値(コードで設定された値ではない)が代入されていました。これはクラスの説明ではできないので、コンストラクタの出番となります。
必要なパラメータのリストを持つコンストラクタを作成しましょう。コンストラクタもpublicでなければなりません。そうでなければ、外部コードからストラテジーオブジェクトを作成することができません。
class CSimpleVolumeStrategy : public CStrategy { private: //--- ... previous code public: //--- Public methods CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders ); // Constructor virtual int Init(); // Strategy initialization method virtual void Tick(); // OnTick event handler };
クラスの説明ができました。コンストラクタ以外のメソッドはすでに実装されているので、追加しましょう。最も単純なケースでは、このクラスのコンストラクタは、受け取ったパラメータの値をクラスの対応するメンバーに代入するだけです。さらに、最初の4つのパラメータは、基本クラスのコンストラクタを呼び出すことでこれを実行します。
CSimpleVolumeStrategy::CSimpleVolumeStrategy( ulong p_magic, string p_symbol, ENUM_TIMEFRAMES p_timeframe, double p_fixedLot, int p_signalPeriod, double p_signalDeviation, double p_signaAddlDeviation, int p_openDistance, double p_stopLevel, double p_takeLevel, int p_ordersExpiration, int p_maxCountOfOrders) : // Initialization list CStrategy(p_magic, p_symbol, p_timeframe, p_fixedLot), // Call the base class constructor signalPeriod_(p_signalPeriod), signalDeviation_(p_signalDeviation), signaAddlDeviation_(p_signaAddlDeviation), openDistance_(p_openDistance), stopLevel_(p_stopLevel), takeLevel_(p_takeLevel), ordersExpiration_(p_ordersExpiration), maxCountOfOrders_(p_maxCountOfOrders) {}
やり残したことはほとんどありません。fixedLot_とmagicN_を、それらが満たされるすべての場所でm_fixedLotとm_magicに名前変更します。Symbol()現在銘柄を取得する関数の使用をm_symbol基本クラス変数に、PERIOD_CURRENT定数をm_timeframeに置き換えます。このコードを現在のフォルダのSimpleVolumesStrategy.mqhファイルに保存します。
EAクラス
CAdvisorの基本クラスを作りましょう。その目的は、特定の取引戦略のオブジェクトのリストを保存し、そのイベントハンドラを起動することです。このクラスにはCExpertという名前の方が適切ですが、標準ライブラリですでに使用されているので、代わりにCAdvisorを使用することにします。
#include "Strategy.mqh" class CAdvisor : public CObject { protected: CStrategy *m_strategies[]; // Array of trading strategies int m_strategiesCount;// Number of strategies public: virtual int Init(); // EA initialization method virtual void Tick(); // OnTick event handler virtual void Deinit(); // Deinitialization method void AddStrategy(CStrategy &strategy); // Strategy adding method };
Init()メソッドとTick()メソッドでは、m_strategies[]配列のすべてのストラテジーがループ処理され、対応するイベント処理メソッドが呼び出されます。
void CAdvisor::Tick(void) { // Call OnTick handling for all strategies for(int i = 0; i < m_strategiesCount; i++) { m_strategies[i].Tick(); } }
戦略追加メソッドでは、まさにこれが起こります。
void CAdvisor::AddStrategy(CStrategy &strategy) { // Increase the strategy number counter by 1 m_strategiesCount = ArraySize(m_strategies) + 1; // Increase the size of the strategies array ArrayResize(m_strategies, m_strategiesCount); // Write a pointer to the strategy object to the last element m_strategies[m_strategiesCount - 1] = GetPointer(strategy); }
このコードを現在のフォルダのAdvisor.mqhファイルに保存しましょう。このクラスに基づいて、複数のストラテジーを管理するための特定のメソッドを実装した子孫を作成することが可能になります。しかし今のところは、この基本クラスだけに限定し、個々のストラテジーの仕事には一切干渉しないことにします。
複数のストラテジーを持つ取引EA
取引EAを書くには、(CAdvisorクラスの)グローバルEAオブジェクトを作成するだけです。
OnInit()初期化イベントハンドラで、選択したパラメータでストラテジーオブジェクトを作成し、EAオブジェクトに追加します。この後、EAオブジェクトのInit()メソッドを呼び出し、すべてのストラテジーを初期化します。
OnTick()とOnDeinit()イベントハンドラは、EAオブジェクトの対応するメソッドを呼び出すだけです。
#include "Advisor.mqh" #include "SimpleVolumesStartegy.mqh" input double depoPart_ = 0.8; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert.AddStrategy(...); expert.AddStrategy(...); int res = expert.Init(); // Initialization of all EA strategies return(res); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { expert.Deinit(); } //+------------------------------------------------------------------+
では、ストラテジーオブジェクトの作成についてもう少し詳しく見てみましょう。ストラテジーの各インスタンスは、それぞれ独自の注文とポジションを建てて考慮するため、それぞれ異なるマジックを持つ必要があります。Magicはストラテジーのコンストラクタの最初のパラメータです。したがって、異なるMagicを保証するために、magic_パラメータで指定された元のMagicに異なる数値を加えます。
expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy(magic_ + 2, ...));
2番目と3番目のコンストラクタパラメータは、銘柄と期間です。EURGBP H1で最適化をおこなったので、これらの具体的な値を示しています。
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, ...)); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, ...));
次に重要なパラメータは、建てるポジションのサイズです。すでに2つの戦略(0.34と0.10)について適切なサイズを計算しています。しかし、これは1万ドルの10%までのドローダウンに対応するサイズであり、ストラテジーは別々に運用されます。2つのストラテジーが同時に機能する場合、最初のストラテジーのドローダウンが2番目のストラテジーのドローダウンに加算されることがあります。最悪の場合、規定の10%以内に収めるためには、建てたポジションのサイズを半分にしなければなりません。しかし、2つのストラテジーのドローダウンが一致しない、あるいは多少補い合うということも起こり得ます。この場合、ポジションサイズを少し小さくしても10%を超えることはありません。そこで、還元乗数をEAパラメータ(depoPart_)とし、最適な値を選択することにしましょう。
ストラテジーコンストラクタの残りのパラメータは、シンプルなEAを最適化した後に選択した値のセットです。最終結果は以下の通りです。
expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.34 * depoPart_, 2), 130, 0.9, 1.4, 231, 3750, 50, 600, 3) ); expert.AddStrategy(new CSimpleVolumeStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.10 * depoPart_, 2), 159, 1.7, 0.8, 248, 3600, 495, 39000, 3) );
出来上がったコードを現在のフォルダのSimpleVolumesExpert.mq5ファイルに保存します。
テスト結果
組み合わされたEAをテストする前に、最初のパラメータセットによる戦略では約91%の利益が得られ、2番目のパラメータセットでは72%の利益が得られたはずであることを覚えておきましょう(10,000ドルの初期預金と最適ロットで10%の最大ドローダウン(1,000ドル)の場合)。
所定のドローダウンを維持するという基準に従ってdepoPart_パラメータの最適値を選択し、以下の結果を得ます。
図3:組み合わせEAの作動結果
テスト期間終了時の残高は約22,400ドルで、124%の利益を得たことになります。これは、このストラテジーの個々のインスタンスを実行したときよりも多いものです。既存の取引戦略を変更することなく、その戦略だけで取引結果を改善することができました。
結論
私たちは、目標達成に向けた小さな一歩を踏み出したにすぎありません。このアプローチが取引の質を向上させることができるという確信が得られました。今のところ、EAには多くの重要な点が欠けています。
例えば、ポジションの決済を一切制御せず、バーの始まりを正確に判断する必要なく機能しているとともに、重い計算を一切使用しない非常にシンプルなストラテジーを見てみました。端末を再起動した後に状態を復元するには、ポジションと注文を数えることを除いて、追加の努力は必要はありませんが、これはEAがおこなうことができます。ただし、すべての戦略がそう単純なものではないでしょう。さらに、このEAはネッティング口座では機能せず、反対側のポジションを同時に建てたままにすることができます。異なる銘柄での活動は考えていません。などなど...。
これらの点は、実際の取引を始める前に必ず考慮する必要があります。新しい記事にご期待ください。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/14026





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