English Русский Deutsch
preview
MQL5における修正グリッドヘッジEA(第1部):シンプルなヘッジEAを作る

MQL5における修正グリッドヘッジEA(第1部):シンプルなヘッジEAを作る

MetaTrader 5トレーディングシステム | 20 3月 2024, 13:34
149 0
Kailash Bai Mina
Kailash Bai Mina

はじめに

エキスパートアドバイザー(EA)を使って取引の世界に飛び込もうと思っても、「危険なヘッジング/グリッド/マーチンゲールは使わない」というセリフにぶつかり続けていませんか。このような戦略について何を大騒ぎしているのかと思われるかもしれません。これらはなぜ危険だと言われ続けるのか。その主張の背後にある事実は何なのでしょうか。これらの戦略をもっと安全にするために手を加えることはできないだろうかと考えていらっしゃるかもしれません。 それに、そもそもなぜトレーダーはこのような戦略をとるのでしょうか。何が良くて、何が悪いのでしょう。このような考えが頭をよぎったのなら、ここが正しい場所です。答え探しが、もうすぐ終わります。

まず、シンプルなヘッジEAを作成します。グリッドヘッジEAという大きなプロジェクトに向けた第一歩と考えてください。古典的なグリッド戦略とヘッジ戦略のクールなミックスになるでしょう。この記事を読み終わる頃には、基本的なヘッジ戦略の立て方と、この戦略が世間で言われているほど儲かるものなのかどうかを知ることができるでしょう。

しかし、そこで立ち止まるつもりはありません。この連載を通して、これらの戦略を徹底的に探っていきます。何がうまくいき、何がうまくいかず、どう組み合わせればもっといいものができるかを考えていきます。私たちの目標は何でしょうか。それは、これらの伝統的な戦略に新たなひねりを加え、自動売買で確実な利益を上げるために利用できるかどうかを確認することです。ついてきて、一緒に見つけてください。

この記事で取り上げる内容を簡単に紹介しましょう。

  1. 古典的ヘッジ戦略について
  2. 古典的ヘッジ戦略の自動化
  3. 古典的ヘッジ戦略のバックテスト
  4. 結論


古典的ヘッジ戦略について

何よりもまず、先に進む前に戦略について話し合うべきです。

まず、簡潔化のために、1000の価格水準で買いポジションを建て、損切りを950に置き、1050で利食いします。つまり、損切りに当たれば50ドルの損失、利食いに当たれば50ドルの利益となります。これで利食いに当たり、戦略はここで利益を得て終了します。しかし、損切りに当たれば50ドルの損失となります。さて、950ですぐに売りポジションを置き、利食いを900、損切りを1000に設定します。この新しい売りポジションが利食いに当たれば、...50ドルの利益を得ますが、すでに50ドルを失っているため、純益は0ドルです。損切り(価格水準1000)に当たれば、再び50ドルを失うため、合計損失は100ドルになります。この新しい買いポジションがTPに当たれば、私たちは50ドルを手にし、ネットゲインの合計は-50ドル-50ドル+50ドル=-50ドル、つまり50ドルの損失となり、損切りに当たれば、合計は-50ドル-50ドル-50ドル=-150ドル、つまり150ドルの損失となります。

簡単にするため、今のところスプレッドと手数料は無視しています。

ここで何が起こっているのか、どうすればこのような100%になるのかと思われるかもしれません。 しかし、ロットサイズという大きなものが欠けています。連続したポジションのロットサイズを増やしたらどうなるでしょうか。では、戦略を見直しましょう。

1000で0.01ロット(最小可能)の買いポジションを建てます。

  • 利食い(1050)に当たれば、50ドルの利益を得て、戦略はここで終了となります。
  • 損切り(950)に当たれば50ドルの損失です。

損切りに当たれば、上記のルールに従って戦略は終了しません。戦略に従って、950に0.02(倍)ロットの売りポジションを即座に建てます。

  • 利食い(900)に当たれば、利益は-50ドル+100ドル=50ドルとなり、戦略はここで終了となります。
  • 損切り(1000)に当たれば、50ドル+100ドル=合計150ドルの損失となります。

損切りに当たれば、上記のルールに従って戦略は終了しません。戦略に従って、すぐに1000でロットサイズ0.04(再び2倍)の買いポジションを建てます。

  • 利食い(1050)に当たれば、-50ドル-100ドル+200ドル=50ドルの純利益を得ることになり、戦略はここで終了します。
  • 損切り(950)に当たれば、50ドル+100ドル+150ドル=合計350ドルの損失となります。

損切りに当たれば、上記のルールに従って戦略は終了しません。戦略に従って、950に0.08(再び2倍)ロットの売りポジションを即座に建てます。

  • 利食い(900)に当たれば、-50ドル-100ドル-150ドル+400ドル=50ドルの純利益を得ることになり、戦略はここで終了となります。
  • 損切り(1000)に当たれば、50ドル+100ドル+150ドル+200ドル=合計500ドルを失うことになります。

... 

すでにお気づきかもしれませんが、いずれにせよ、戦略終了時には50ドルの利益を得ます。そうでなければ、戦略は続行されます。この戦略は、900か1050のどちらかで利食いするまで続けます。価格は最終的にこの2つのポイントのどちらかに到達し、確実に50ドルの利益を得ます。

上記のケースでは、まず買いポジションを建てましたたが、買いポジションから始めることは必須ではありません。あるいは、0.01の売りポジションで戦略を開始することもできます。

実際、売りポジションから始めるというこの選択肢は非常に重要で、後で戦略を修正し、可能な限り柔軟性を持たせるようにします。例えば、上記のサイクルのエントリーポイント(このケースでは最初の買い)を定義する必要があるかもしれませんが、そのエントリーポイントを買いポジションだけに制限するのは問題があります。

売りポジションから始める戦略は、上記の買いポジションから始める場合とまったく対称になります。より明確に説明すると、私たちの戦略は次のようになります。

  • 利食い(900)に当たれば、50ドルの利益を得て、戦略はここで終わります。
  • 損切り(1000)に当たれば、50ドルの損失です。

損切りに当たれば、上記のルールに従って戦略は終了しません。戦略に従って、すぐに1000で0.02(倍)ロットの買いポジションを建てます。

  • 利食い(1050)に当たれば、利益は-50ドル+100ドル=50ドルとなり、戦略はここで終了となります。
  • 損切り(950)に当たれば、50ドル+100ドル=合計150ドルの損失となります。

損切りに当たれば、上記のルールに従って戦略は終了しません。戦略に従って、950で0.04(再び2倍)ロットの売りポジションを即座に建てます。

  • 利食い(900)に当たれば、トータルで-50ドル-100ドル+200ドル=50ドルの利益を得ることになり、戦略はここで終了となります。
  • 損切り(1000)に当たれば、50ドル+100ドル+150ドル=合計350ドルを失うことになります。

などなど。

繰り返しになりますが、この戦略は900か1050のどちらかの価格水準に達したときに終了します。その価格水準に達しない場合は、最終的にその価格水準に達するまで戦略を継続します。

注:ロットサイズを2倍にすることは義務ではありません。倍率はいくらでも増やせますが、2倍を下回る倍率では、上記のいずれのケースでも利益を保証することはできません。簡単にするために2を選びましたが、後で戦略を最適化する際に変更する可能性もあります。

これで古典的ヘッジスターテジーの説明は終わりです。


古典的ヘッジスターテジーの自動化について

まず、EAの作成をどのように進めるか、その計画を検討する必要があります。実は、これにはかなり多くのアプローチがあります。大きく分けて、以下の2つのアプローチがあります。

  • アプローチその1:戦略が指定する4つのレベル(変数)を定義し、これらのラインが戦略が指定する価格と再びクロスするたびにポジションを建てます。
  • アプローチその2:未決注文を使用し、その未決注文がいつ約定されたかを検出し、その時にさらに未決注文を発注します。

    どちらのアプローチもほぼ同等であり、どちらが若干優れているかは議論の余地があるが、コード化しやすく理解しやすいアプローチ#1についてのみ説明します。


    古典的ヘッジ戦略の自動化

    まず、グローバル空間でいくつかの変数を宣言します。

    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;

    1. isPositionBuyはブール変数で、次のポジションタイプが買いポジションか売りポジションかを決定します。
    2. buyTPは、買いポジションの利食いであるAとBの間の距離です(pips単位)。
    3. sellTPは、売りポジションの利食いであるCとDの間の距離(pips単位)であり、C、Dは後で定義します。
    4. buySellDiffはBとCの間の距離で、買い価格水準と売り価格水準です(pips単位は)。
    5. intialLotSizeは最初のポジションのロットサイズです。
    6. lotSizeMultiplierは、連続するポジションのロットサイズの倍率です。
    A、B、C、Dは基本的に上から順に価格水準です。

    注:これらの変数は後で戦略を最適化するために使われます。

    例えば、buyTP、sellTP、buySellDiffを15pipsに設定しましたが、後でこれらを変更し、最適な利益とドローダウンが得られる値を確認します。

    これらは、後に最適化に使用される入力変数です。

    ここで、グローバル空間にさらにいくつかの変数を作成します。

    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;

    1. まず、A、B、C、Dという4つのレベルをdouble型変数として定義しました。
      • A: すべての買いポジションの利食いの価格水準を表します。
      • B:すべての買いポジションの始値レベルと、すべての売りポジションの損切りを表します。
      • C: すべての売りポジションの始値レベルと、すべての買いポジションの損切りを表します。
      • D: これは、すべての売りポジションの利食いの価格水準を表します。
    2. isPositionBuy: trueとfalseの2つの値をとるブール変数で、trueは初期ポジションが買いであることを表し、falseは初期ポジションが売りであることを表します。
    3. hedgeCycleRunning:trueは、ヘッジサイクルが1つ実行中であること、つまり最初の注文が建てられたが、上で定義したAまたはDの価格レベルにまだ到達していないことを表し、falseは、価格レベルがAまたはDのいずれかに到達し、新しいサイクルが開始されることを表します。また、この変数はデフォルトでfalseになります。
    4. lastPositionLotSize:その名前が示すように、このdouble型変数には、常に最後に建てられた注文のロットサイズが格納され、サイクルが開始されていない場合は、後で設定する入力変数initialLotSizeと同じ値が格納されます。

    次に、以下の関数を作成します。 
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+

    関数の型はvoidで、つまり何も返す必要はありません。この関数は次のように動作します。

    まず、isPositionBuy変数(bool)を入力変数initialPositionBuyに等しく設定します。これにより、各サイクルの開始時にどのポジションタイプを配置するかがわかります。どちらも同じ変数なのに、なぜ2つも必要なのかと思われるかもしれませんが、isPositionBuyを代わりに変更する(上のコードブロックの最後の行)ことにご注意ください。ただし、initialPositionBuyは常に固定されており、変更することはありません。

    次に、initialPriceという名前の新しい変数(double型)を定義し、三項演算子を使ってAskまたはBidと等しくなるように設定します。isPositionBuyがtrueなら、initialPriceはその時点のAskPriceと等しくなり、そうでなければBidPriceとなります。

    次に、先に簡単に説明した変数(double型)、すなわちA、B、C、D変数を三項演算子を使って以下のように定義します。

    1. isPositionBuyがTrueの場合:
      • Aは、initialPricebuyTP(入力変数)の合計に等しく、buyTPは(_Point*10)の係数で乗算される。ここで、_Pointは実際には事前定義された関数「Point()」
      • BはinitialPriceに等しい
      • CはinitialPriceからbuySellDiff(入力変数)を引いたものに等しく、buySellDiffには(_Point*10)の係数が乗算される
      • DはinitialPriceからbuySellDiffsellTPの合計を引いたものに(_Point*10)を掛けたものに等しい

    2. isPositionBuyがFalseの場合:
      • AはinitialPriceと(buySellDiff+buyTP)の合計に(_Point*10)の係数を掛けたものに等しい
      • BはinitialPricebuySellDiffの合計に等しく、buySellDiffには(_Point*10)の係数が乗算される
      • CはinitialPriceに等しい
      • DはinitialPriceからsellDiffを引いたものに等しく、sellTP には (_Point*10) の係数が乗算される

    さて、視覚化のために、ObjectCreateを使ってチャート上にA、B、C、Dの価格水準を表す線を引き、ObjectSetIntegerを使ってその色プロパティをclrGreenに設定します(他の色でもかまいません)。

    さて、isPositionBuyという変数によって買いにも売りにもなる初期注文を開く必要があります。そのために、positionType、SL、TPの3つの変数を定義します。

    1. positionType:この変数の型はENUM_ORDER_TYPEであり、以下の表に従って0から8までの整数値を取ることができる、定義済みのカスタム変数型です。

      整数値 識別子
       0 ORDER_TYPE_BUY
       1 ORDER_TYPE_SELL
       2 ORDER_TYPE_BUY_LIMIT
       3 ORDER_TYPE_SELL_LIMIT
       4 ORDER_TYPE_BUY_STOP
       5 ORDER_TYPE_SELL_STOP
       6 ORDER_TYPE_BUY_STOP_LIMIT
       7 ORDER_TYPE_SELL_STOP_LIMIT
       8 ORDER_TYPE_CLOSE_BY

      0はORDER_TYPE_BUYを表し、1はORDER_TYPE_SELLを表します。必要なのはこの2つだけです。整数値は覚えにくいので、識別子を使うことにします。

    2. SLisPositionBuyがtrueの場合は価格水準Cに等しく、そうでない場合はBに等しい。

    3. TPisPositionBuyがtrueの場合は価格水準Aに等しく、そうでない場合はDに等しい。

    この3つの変数を使って、次のようにポジションを建てます。

    まず、#includeを使って標準取引ライブラリをインポートします。

    #include <Trade/Trade.mqh>
    

    ポジションを建てる直前に、CTradeクラスのインスタンスを作成します。

    CTrade trade;
    
    trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);

    そして、そのインスタンスを使って、PositionOpen関数で次のパラメータを指定してポジションを建てます。

    1. SymbolはEAが接続されている現在の銘柄を示します。
    2. positionTypeは、先に定義したENUM_ORDER_TYPE変数です。
    3. 初期ロットサイズは入力変数です。
    4. initialPriceは注文開始価格で、Ask(買いポジションの場合)またはBid(売りポジションの場合)です。
    5. 最後にSLとTPの価格水準を提示します。

    これにより、買いまたは売りのポジションが建てられます。さて、ポジションが建てられた後、そのロットサイズをlastPositionLotSizeというグローバルスペースに定義された変数に格納し、このロットと入力からの乗数を使用して、次のポジションのロットサイズを計算できるようにします。

    これで、やるべきことがあと2つ残ります。

    if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
    isPositionBuy = isPositionBuy ? false : true;

    ここでは、ポジションの発注に成功した場合のみ、hedgeCycleRunningの値をtrueに設定します。これは、tradeというCTradeインスタンスのResultRetcode()関数によって決定され、配置が成功したことを示す10009を返します(これらのすべてのリターンコードをここで見ることができます)。hedgeCycleRunningを使用する理由は、さらなるコードで説明します。

    最後にもうひとつ、三項演算子を使ってisPositionBuyの値を交互に変えています。falseならtrueになるし、逆もまた然りです。私たちの戦略では、最初のポジションが建てられると、買いの後に売りが建てられ、売りの後に買いが建てられます。

    これで、基本的に重要な関数StartHedgeCycle()の説明を終わります。この関数は何度も使用することになります。

    では、最後のコードを書いていきましょう。

    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = newPositionLotSize;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Bid >= A || _Ask <= D)
           {
            hedgeCycleRunning = false;
           }
       }
    //+------------------------------------------------------------------+

    最初の2行は説明不要で、その時点のAskとBidを格納する_Askと_Bid(double型変数)を定義しているだけです。

    次に、if文を使用して、変数hedgeCycleRunningがfalseであれば、StartHedgeCycle()関数を使用してヘッジサイクルを開始します。StartHedgeCycle()関数が何をするかはすでに知っていますが、要約すると次のようになります。

    1. A、B、C、Dの価格水準を定義します。
    2. 視覚化のため、A、B、C、Dの価格水準に緑色の水平線を引きます。
    3. ポジションを建てます。
    4. このポジションのロットサイズを、グローバル空間で定義された変数lastPositionLotSizeに格納し、どこでも使用できるようにします。
    5. hedgeCycleRunningをtrueに設定します(以前はfalse)。これがまさにStartHedgeCycle()関数を実行した理由です。
    6. 最後に、isPositionBuy変数をtrueからfalseに、falseからtrueに交互に変えます。

    StartHedgeCycle()は、hedgeCycleRunningがfalseの場合に実行し、関数の最後でfalseに変更するため、1回しか実行しません。したがって、hedgeCycleRunningを再びfalseに設定しない限り、StartHedgeCycle()は再び実行されません。

    次の2つのif文は今は省略しましょう。後でまた触れます。最後のif文を見てみましょう。

    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }

    これはサイクルの再スタートを処理します。先ほど説明したように、hedgeCycleRunningをtrueに設定すると、サイクルが再開され、前回説明したことがすべて再び起こります。さらに、サイクルが再スタートしたときに、前のサイクルのすべてのポジションが利食い(買いポジションであろうと売りポジションであろうと)で決済されるようにしました。

    サイクルスタート、サイクルエンド、サイクルリスタートは処理できましたが、主要な部分である、価格が下からBレベル、または上からCレベルに達したときに注文を開始する処理がまだ欠けています。ポジションタイプは、買いポジションはBレベル、売りポジションはCレベルで建てるような代替的なものでなければなりません。

    これを処理するコードを読み飛ばしたので、それに戻りましょう。

    if(_Bid <= C && !isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }
    
    if(_Ask >= B && isPositionBuy)
       {
        double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
        CTrade trade;
        trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
        lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
        isPositionBuy = isPositionBuy ? false : true;
       }

     つまり、これら2つのif文は、サイクルの間(最初のポジションが置かれてからサイクルがまだ終了していない間)の注文のオープンを処理します。

    1. 最初のIF文:これは売り注文を出すためのものです。Bid変数(その時点のBid価格を含む)がCレベル以下で、isPositionBuyがfalseの場合、newPositionLotSizeというdouble変数を定義します。これは、lastPositionLotSizeにlotSizeMultiplierを掛けたものに等しく設定され、その後、NormalizeDoubleと呼ばれる定義済みの関数を使用して、ダブル値が小数点以下2桁まで正規化されます。

      次に、tradeというCTradeインスタンスから定義済みの関数PositionOpen()を使用して、newPositionLotSizeをパラメータとして指定して売りポジションを建てます。最後に、lastPositionLotSizeをこの新しいロットサイズに設定し(正規化なし)、さらにポジションを増やすことができるようにします。最後に、isPositionBuyをtrueからfalse、またはfalseからtrueに交互に設定します。 

    2. 2番目のIF文:これは買い注文を出す処理です。Ask価格(その時点のAsk価格を含む変数)がBレベル以上で、isPositionBuyがtrueの場合、newPositionLotSizeというdouble変数を定義します。newPositionLotSizeをlastPositionLotSizeにlotSizeMultiplierを掛けた値に設定し、前回と同様に、定義済みの関数NormalizeDoubleを使用して小数点以下2桁までdouble値を正規化します。

      次に、tradeというCTradeインスタンスから定義済みの関数PositionOpen()を使い、newPositionLotSizeをパラメータとして買いポジションを建てます。最後に、lastPositionLotSizeをこの新しいロットサイズ(正規化なし)に設定し、さらなるポジションで乗算できるようにします。最後に、isPositionBuyをtrueからfalse、またはfalseからtrueに交互に変えます。

    ここで注意すべき点が2つあります。

    • 最初のIF文では、Bid価格を使用し、BidがC以下になり、isBuyPositionがfalseになったときにポジションを建てるとしています。なぜBidなのでしょうか。

      仮にAsk価格を使用すると、前の買いポジションは決済されますが、新しい売りポジションは建てられない可能性があります。これは、買いがAskで始まり、Bidで決済されることが分かっているためで、Bidが上からCの価格水準ラインを超えるか等しくなったときに買いが決済される可能性が残されています。これで買い注文はポジションを建てるときに設定した損切りで決済されますが、売り注文はまだ建てられていません。つまり、AskとBidの両方の価格が上昇した場合、私たちの戦略は貫かれていなかったことになります。それがAskの代わりにBidを使った理由です。

      対称的に、2つ目のIF文では、Ask価格を使用し、AskがB以上でisBuyPositionがtrueのときにポジションを建てることを述べています。なぜここでAskを使ったのでしょうか。

      Bid価格を使用する場合、前の売りポジションは決済されますが、新しい買いポジションは建てられない可能性があります。売りはBidで始まり、Askで決済されることが分かっているため、AskがBの価格水準ラインを下から横切るか等しくなったときに売りが決済される可能性があります。しかし、まだ買いは始まっていません。つまり、AskとBidの両方の価格が下がれば、私たちの戦略は貫かれていないことになります。だからBidではなくAskを使ったのです。

      つまり、重要なのは、買い/売りポジションが決済された場合、戦略が正しく機能するためには、すぐに連続した売り/買いポジションを建てなければならないということです。

    • 両方のIF文において、lastPositionLotSizeの値を設定する際に、newPositionLotSizeではなく、(lastPositionLotSize*lotSizeMultiplier)と等しくすることを述べました。これは、定義済みのNormalizeDouble()関数を使用して、(lastPositionLotSize*lotSizeMultiplier)を小数点以下2桁まで正規化した値と等しくなります。
      NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2)
      なぜそんなことをしたのでしょうか。実際、これを正規化された値と等しく設定すると、戦略は正しく実行されます。たとえば、最初のロットサイズ0.01と乗数1.5を設定すると、最初のロットサイズは当然0.01になり、次のロットサイズは0.01*1.5=0.015になります。もちろん、証券会社によって許可されていない0.015のロットサイズを開くことはできません。乗数は、0.015ではない0.01の整数倍である必要があります。それがまさに、ロットサイズを2まで正規化した理由です。小数点以下の桁が0.01になるので、lastPositionLotSizeに与える値には、0.01 (0.010)または0.015の2つのオプションがあります。0.01 (0.010)を選択すると、次回ポジションを配置するときに、正規化後に0.01*1.5=0.015を使用します。これが、0.01になり、これが継続します。そこで、乗数1.5を使用し、0.01のロットサイズで開始しましたが、ロットサイズが増加することはなく、すべてのポジションが0.01のロットサイズで配置されるというループにはまりました。つまり、lastPositionLotSizeを0.01 (0.010)と等しくしてはいけないということです。したがって、代わりに他のオプション0.015を選択します。つまり、正規化前の値を選択します。

      これが、lastPositionLotSizeNormalizeDouble (lastPositionLotSize*lotSizeMultiplier,2)ではなく(lastPositionLotSize*lotSizeMultiplier)に設定する理由です。

    最後に、コード全体は次のようになります。

    #include <Trade/Trade.mqh>
    
    input bool initialPositionBuy = true;
    input double buyTP = 15;
    input double sellTP = 15;
    input double buySellDiff = 15;
    input double initialLotSize = 0.01;
    input double lotSizeMultiplier = 2;
    
    
    
    double A, B, C, D;
    bool isPositionBuy;
    bool hedgeCycleRunning = false;
    double lastPositionLotSize;
    //+------------------------------------------------------------------+
    //| Expert initialization function                                   |
    //+------------------------------------------------------------------+
    int OnInit()
       {
        return(INIT_SUCCEEDED);
       }
    
    //+------------------------------------------------------------------+
    //| Expert deinitialization function                                 |
    //+------------------------------------------------------------------+
    void OnDeinit(const int reason)
       {
        ObjectDelete(0, "A");
        ObjectDelete(0, "B");
        ObjectDelete(0, "C");
        ObjectDelete(0, "D");
       }
    
    //+------------------------------------------------------------------+
    //| Expert tick function                                             |
    //+------------------------------------------------------------------+
    void OnTick()
       {
        double _Ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        double _Bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
    
        if(!hedgeCycleRunning)
           {
            StartHedgeCycle();
           }
    
        if(_Bid <= C && !isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_SELL, newPositionLotSize, _Bid, B, D);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
        if(_Ask >= B && isPositionBuy)
           {
            double newPositionLotSize = NormalizeDouble(lastPositionLotSize * lotSizeMultiplier, 2);
            CTrade trade;
            trade.PositionOpen(_Symbol, ORDER_TYPE_BUY, newPositionLotSize, _Ask, C, A);
            lastPositionLotSize = lastPositionLotSize * lotSizeMultiplier;
            isPositionBuy = isPositionBuy ? false : true;
           }
        
    if(_Bid >= A || _Ask <= D)
       {
        hedgeCycleRunning = false;
       }
       }
    //+------------------------------------------------------------------+
    
    //+------------------------------------------------------------------+
    //| Hedge Cycle Intialization Function                               |
    //+------------------------------------------------------------------+
    void StartHedgeCycle()
       {
        isPositionBuy = initialPositionBuy;
        double initialPrice = isPositionBuy ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
        A = isPositionBuy ? initialPrice + buyTP * _Point * 10 : initialPrice + (buySellDiff + buyTP) * _Point * 10;
        B = isPositionBuy ? initialPrice : initialPrice + buySellDiff * _Point * 10;
        C = isPositionBuy ? initialPrice - buySellDiff * _Point * 10 : initialPrice;
        D = isPositionBuy ? initialPrice - (buySellDiff + sellTP) * _Point * 10 : initialPrice - sellTP * _Point * 10;
    
        ObjectCreate(0, "A", OBJ_HLINE, 0, 0, A);
        ObjectSetInteger(0, "A", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "B", OBJ_HLINE, 0, 0, B);
        ObjectSetInteger(0, "B", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "C", OBJ_HLINE, 0, 0, C);
        ObjectSetInteger(0, "C", OBJPROP_COLOR, clrGreen);
        ObjectCreate(0, "D", OBJ_HLINE, 0, 0, D);
        ObjectSetInteger(0, "D", OBJPROP_COLOR, clrGreen);
    
        ENUM_ORDER_TYPE positionType = isPositionBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
        double SL = isPositionBuy ? C : B;
        double TP = isPositionBuy ? A : D;
        CTrade trade;
        trade.PositionOpen(_Symbol, positionType, initialLotSize, initialPrice, SL, TP);
        
        lastPositionLotSize = initialLotSize;
        if(trade.ResultRetcode() == 10009) hedgeCycleRunning = true;
        isPositionBuy = isPositionBuy ? false : true;
       }
    //+------------------------------------------------------------------+
    

    以上で、古典的なヘッジ戦略を自動化する方法についての説明を終えます。


    古典的ヘッジ戦略のバックテスト

    さて、私たちの戦略に自動的に従うEAを作成したので、それをテストして結果を見ることが道理にかなっています。

    戦略をテストするために、以下の入力パラメータを使用します。

    1. initialBuyPosition: true
    2. buyTP:15
    3. sellTP:15
    4. buySellDiff:15
    5. initialLotSize:0.01
    6. lotSizeMultiplier:2.0

    2023年1月1日から2023年12月6日まで EURUSDで、レバレッジ1:500、入金額 10,000ドルでテストします。期間について疑問がある場合は、戦略には無関係なので、任意の期間を選択します(結果にはまったく影響しません)。以下の結果を見てみましょう。


    グラフを見ただけで、これは儲かる戦略だと思われるかもしれませんが、他のデータを見て、グラフのいくつかの点について議論してみましょう。

    ご覧のとおり、純利益は1470.62ドルで、粗利益は13,153.68 ドル、粗損失は11683.06ドルでした。

    また、残高ドローダウンと資本ドローダウンについても見てみましょう。

    残高ドローダウン(絶対) $1170.10
    残高ドローダウン(最大) $1563.12 (15.04%)
    残高ドローダウン(相対) 15.04% ($1563.13)
    資本ドローダウン(絶対) $2388.66
    資本ドローダウン(最大) $2781.97 (26.77%)
    資本ドローダウン(相対) 26.77% ($2781.97)

    これらの用語を理解しましょう。

    1. 残高ドローダウン(絶対):初期資金(この場合1万ドル)から最低残高(残高の最低点(谷の残高))を差し引いた差額です。
    2. 残高ドローダウン最大値:残高の最高点(ピーク残高)から残高の最低点(トラフ残高)を引いた差です。
    3. 残高ドローダウン相対値:残高の最高点(ピーク残高)のうち、残高のドローダウンが最大となる割合です。

    資本の定義は対称的です。

    1. 資本ドローダウン(絶対):このケースでは1万ドルである初期資本から、資本の最低点(トラフ資本)である最低資本を差し引いた差額です。
    2. 資本ドローダウン(最大):資本の最高点(ピーク資本)から最低点(トラフ資本)を引いた差です。
    3. 資本ドローダウン(相対):これは、資本の最高点(ピーク資本)のうち、資本のドローダウンが最大となる割合です。

    以下は6つの統計の計算式です。


    上記のデータを分析すると、資本のドローダウンでカバーできるため、残高のドローダウンは最も心配する必要がなくなります。ある意味で、残高ドローダウンは資本ドローダウンのサブセットと言えます。また、資本ドローダウンは、当戦略に従った場合の最大の問題です。なぜなら、各注文でロットサイズを2倍にしているため、ロットサイズが指数関数的に大きくなるからです。

    建てられたポジションの数 次のポジションのロットサイズ(建てられる前) 次のポジションの必要証拠金(EURUSD) 
    0 0.01 $2.16 
    1 0.02 $4.32 
    2 0.04 $8.64
    3 0.08 $17.28
    4 0.16 $34.56
    5 0.32 $69.12
    6 0.64 $138.24
    7 1.28 $276.48
    8 2.56 $552.96
    9 5.12 $1105.92
    10 10.24 $2211.84
    11 20.48 $4423.68
    12 40.96 $8847.36
    13 80.92 $17694.72
    14 163.84 $35389.44

    ここでの調査では、現在EURUSDを取引ペアとして利用しています。0.01ロットの必要証拠金は2.16ドルですが、この数字は変更される可能性があります。

    さらに深く掘り下げると、注目すべき傾向が観察されます。次のポジションに必要な証拠金は指数関数的に増加します。例えば、12回目の注文の後、資金的なボトルネックにぶつかりました。必要証拠金は17,694.44ドルにまで膨れ上がり、初期投資額が10,000ドルだったことを考えると、とても手が届かない数字です。このシナリオでは、損切りは考慮されていません。

    さらに分解してみましょう。1回の取引につき15pipsに設定した損切りを含めると、最初の12回の取引で負けた場合、累積ロットサイズはなんと81.91(一連の取引の合計)となります。0.01+0.02+0.04+...+20.48+40.96)。これは、EURUSDの10pipsあたり1ドル、ロットサイズ0.01で計算すると、合計12,286.5ドルの損失となります。単純な計算です。(81.91/0.01) * 1.5 = $12,286.5.損失は初期資金を上回るだけでなく、EURUSDに1万ドルを投資して1サイクルで12ポジションを維持することも不可能になります。

    少し違うシナリオを考えてみましょう。EURUSDの1万ドルで合計11ポジションを維持できるでしょうか。

    総ポジション数が10になったとしましょう。これは、すでに9つのポジションで損切りに遭遇し、10番目のポジションを失おうとしていることを意味します。11番目のポジションを建てる場合、10ポジションの合計ロットサイズは10.23となり、1,534.5ドルの損失となります。これは、EURUSDのレートとロットサイズを考慮し、前回と同じ方法で計算されます。次のポジションに必要な証拠金は4,423.68ドルとなります。これらの金額を合計すると、5,958.18ドルとなり、基準額の10,000ドルを余裕で下回ります。したがって、合計10ポジションを存続させ、11番目のポジションを建てることは可能です。

    ただし、疑問が生じます。同じ1万ドルで合計12ポジションまで伸ばすことは可能でしょうか。

    そのために、11ポジションの瀬戸際まで到達したと仮定しましょう。ここでは、すでに10ポジションで損失を被っており、11番目のポジションも失いかけています。これら11ポジションの合計ロットサイズは20.47で、3,070.5ドルの損失となりました。12番目のポジションに必要な証拠金の8,847.36ドルを加えると、総支出は11,917.86ドルとなり、初期投資を上回ります。したがって、すでに11番がプレーしている状態で12番を建てるのは、財政的に無理があるのは明らかです。すでに3,070.5ドルを失っており、手元には6,929.5ドルしか残っていません。

    バックテストの統計から、EURUSDのような比較的安定した通貨に1万ドルを投資した場合でも、この戦略は破綻寸前まで不安定であることが観察されます。最大連続損失は10で、11位という悲惨なポジションまであと数pipsだったことを示しています。11番目のポジションも損切りにヒットすれば、戦略は破綻し、大きな損失につながります。

    レポートでは、絶対ドローダウンは2,388.66ドルと記されています。11番目のポジションの損切りに達していたら、損失は3,070.5ドルに拡大していたでしょう。この結果、戦略失敗まであと681.84ドル(3,070.5ドル-2,388.66ドル)となりました。

    しかし、これまで私たちが見落としていた重要な要素、つまりスプレッドがあります。この変数が利益に大きな影響を与える可能性があることは、以下の画像で強調した、我々のレポートの2つの具体例で証明されています。

    画像の赤い丸にご注目ください。このようなケースでは、取引に勝利したにもかかわらず(勝利とは、最後の取引で最高ロットを確保したことを意味する)、利益を実現できませんでした。この異常はスプレッドに起因します。その変化しやすい性質が、戦略をさらに複雑にしており、本連載の次のセクションでより詳細な分析をおこなう必要があります。

    古典的なヘッジ戦略の限界も考慮しなければなりません。重大な欠点の1つは、利食い水準に早期に到達しない場合、多数の注文を維持するためにかなりの保有能力が必要になることです。この戦略が利益を確保できるのは、ロットサイズ倍率が2以上の場合(スプレッドを無視して、buyTP=sellTP=buySellDiffの場合)のみです。2未満だと、注文数が増えるにつれて利益がマイナスになるリスクがあります。次回は、このようなダイナミクスと、古典的なヘッジ戦略を最適化して最大のリターンを得る方法を探ります。


    結論

    本連載の最初のセクションでは、かなり複雑な戦略について説明し、MQL5のEAを使って自動化しましたた。高いロットサイズのポジションを持つには高い保有能力が必要です。これは投資家にとって常に可能であるとは限らず、また大きなドローダウンに直面する可能性があるため非常にリスクが高くなります。これらの制限を克服するためには、戦略を最適化する必要があります。

    これまでは、lotSizeMultiplier、initialLotSize、buySellDiff、sellTP、buyTPを任意の固定値で使用してきましたが、この戦略を最適化し、可能な限り最大のリターンをもたらすこれらの入力パラメータの最適値を見つけることができます。また、最初は買いポジションで始めるのが得策なのか、売りポジションで始めるのが得策なのかも、通貨や市場の状況によって異なるでしょう。というわけで、連載の次のセクションでは、役に立つことをたくさん取り上げる予定なので、お楽しみに。 

    私の記事を読んでくださってありがとうございます。これらの情報が、読者の努力に役立ち、有益なものであることを願っています。 私の次回作について何かアイデアや提案がありましたら、ご遠慮なく共有してください。

    ハッピーコーディング!ハッピートレード!


    MetaQuotes Ltdにより英語から翻訳されました。
    元の記事: https://www.mql5.com/en/articles/13845

    添付されたファイル |
    MQL5を使ったシンプルな多通貨エキスパートアドバイザーの作り方(第5回): ケルトナーチャネルのボリンジャーバンド—指標シグナル MQL5を使ったシンプルな多通貨エキスパートアドバイザーの作り方(第5回): ケルトナーチャネルのボリンジャーバンド—指標シグナル
    この記事の多通貨エキスパートアドバイザー(EA)は、1つの銘柄チャートからのみ複数の銘柄ペアの取引(注文を出す、注文を決済する、トレーリングストップロスとトレーリングプロフィットなどで注文を管理するなど)ができるEAまたは自動売買ロボットです。この記事では、2つの指標、この場合はケルトナーチャネルのボリンジャーバンド®からのシグナルを使用します。
    プログラミングパラダイムについて(第1部):プライスアクションエキスパートアドバイザー開発の手続き型アプローチ プログラミングパラダイムについて(第1部):プライスアクションエキスパートアドバイザー開発の手続き型アプローチ
    プログラミングパラダイムとMQL5コードへの応用について学びます。この記事では、手続き型プログラミングの具体的な方法について、実践的な例を通して説明します。EMA指標とローソク足の価格データを使って、プライスアクションエキスパートアドバイザー(EA)を開発する方法を学びます。さらに、この記事では関数型プログラミングのパラダイムについても紹介しています。
    データサイエンスと機械学習(第16回):決定木を見直す データサイエンスと機械学習(第16回):決定木を見直す
    連載「データサイエンスと機械学習」の最新作で、決定木の複雑な世界に飛び込みましょう。戦略的な洞察を求めるトレーダーのために、この記事は包括的な総括として、市場動向の分析において決定木が果たす強力な役割に光を当てています。これらのアルゴリズム木の根と枝を探り、取引の意思決定を強化する可能性を解き明かします。決定木について新たな視点から学び、複雑な金融市場をナビゲートする上で、決定木をどのように味方にできるかを発見しましょう。
    ソフトウェア開発とMQL5におけるデザインパターン(第3回):振る舞いパターン1 ソフトウェア開発とMQL5におけるデザインパターン(第3回):振る舞いパターン1
    デザインパターンの新しい記事として、その1タイプである振る舞いパターンを取り上げ、作成されたオブジェクト間の通信を効果的に構築する方法について説明します。これらの振る舞いパターンを完成させることで、再利用可能かつ拡張可能で、テストされたソフトウェアをどのように作成し、構築できるかを理解できるようになります。