【FX検証】前日の高値・安値ブレイク手法の検証~戦略の最適化とウォークフォワード~

MQLプログラミング

前回の記事では、戦略のコード化を行いました。今回の記事では、このトレード戦略を最適化する様子を記述していきます。

最適化する戦略のおさらい

では、戦略のどの部分を最適化するのかをおさらいしていきましょう。

【仕掛けルール】

X日間の高値の最大値をY時間足の実体で更新したらロング
X日間の安値の最小値をY時間足の実体で更新したらショート

最適化対象パラメータ:X、Y

ただし、パラメータYは多期間多市場テストの一環で堅牢性を確かめるとともに、最適化するものとする。

【手仕舞いルール】

ドテン戦略

コードの再調整

前回公開したコードでは、Xのパラメーターは最適化できたのですが、時間足の最適化はできないように(手動ではできるが)なっていました。

input int slippage = 10;
input int day_count = 3;
input ENUM_TIMEFRAMES timeframe = PERIOD_H1; // 時間足のパラメータを追加
extern int magicnumber = 123;

int currentTradeType = -1; // -1: 保有ポジションなし, 0: ロングポジション, 1: ショートポジション

void OnTick()
{
    double highest = iHigh(NULL, timeframe, iHighest(NULL, timeframe, MODE_HIGH, day_count, 1));
    double lowest = iLow(NULL, timeframe, iLowest(NULL, timeframe, MODE_LOW, day_count, 1));
    double close = iClose(NULL, timeframe, 0);

    if (currentTradeType == 0 && close < lowest)
        close_long(); // ロング手仕舞いの条件 ドテン
    else if (currentTradeType == 1 && close > highest)
        close_short(); // ショート手仕舞いの条件 ドテン
    else if (currentTradeType == -1 && close > highest) // ロングの条件
    {
        printf("ロング");
        OrderSend(Symbol(), OP_BUY, 1, Ask, slippage, 0, 0, NULL, magicnumber, 0, clrRed);
        currentTradeType = 0; // ロングポジションをトラッキング
    }
    else if (currentTradeType == -1 && close < lowest) // ショートの条件
    {
        printf("ショート");
        OrderSend(Symbol(), OP_SELL, 1, Bid, slippage, 0, 0, NULL, magicnumber, 0, clrBlue);
        currentTradeType = 1; // ショートポジションをトラッキング
    }

    Comment(day_count + 1, "期間の最高値は", highest, "|",
            day_count + 1, "期間の最安値は", lowest);
}

void close_long()
{
    for (int i = 0; i < OrdersTotal(); i++)
    {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false)
            break;
        if (OrderSymbol() != Symbol() || OrderMagicNumber() != magicnumber)
            continue;
        if (OrderType() == OP_BUY)
        {
            double lots = OrderLots();
            OrderClose(OrderTicket(), lots, Bid, slippage, clrRed);
            currentTradeType = -1; // 保有ポジションなしにリセット
        }
    }
}

void close_short()
{
    for (int i = 0; i < OrdersTotal(); i++)
    {
        if (OrderSelect(i, SELECT_BY_POS, MODE_TRADES) == false)
            break;
        if (OrderSymbol() != Symbol() || OrderMagicNumber() != magicnumber)
            continue;
        if (OrderType() == OP_SELL)
        {
            double lots = OrderLots();
            OrderClose(OrderTicket(), lots, Ask, slippage, clrBlue);
            currentTradeType = -1; // 保有ポジションなしにリセット
        }
    }
}

このようにコードを組み直すことによって、

このように、MT4の最適化機能を利用できるようになります。

最適化のステップについて

そもそも今回の最適化を行う理由は、主に以下の2つです。

  • EAの堅牢性を測り、そもそも検証に足る手法かを調べる→合格基準を明確化する→いきなりモンキーテストでもいいか
  • 最適化したトレード手法のフォワードテスト結果と、モンキーテストの同期間のフォワードテスト結果を比較し、優位性を調べる→上位10%

EAの堅牢性について

最適化を行った際に、

  • ほとんどのパラメータの組み合わせでマイナス利益を出している
  • パラメータの最適値の前後でトレード成績が大幅に変わってしまっている

以上の状態だった場合、そのEAは堅牢とは言えません。

そこで今回は堅牢性を測るテストとして、最適化におけるパラメータの組み合わせの70%以上が利益を挙げられていなかった場合は、堅牢性のないEAだとして不合格とします。

つまり、具体的にはこのような形で堅牢性を測っていきます。

  1. 2015-2016の最適化→パラメータの組み合わせの70%以上がプラス利益なら合格
    不合格の場合は、EAの設計自体をやり直す
  2. 2015-2016で最適化したEAを2017年のデータでフォワードテスト
  3. モンキーEAを作って2017年のデータを集計し、EAのフォワードテストの結果と比較
    モンキーEAより作成したEAが優れていたら合格、不合格の場合は設計からやり直し
  4. 2016-2017が最適化期間、2018がアウト期間のウォークフォワードテスト→最適化期間でのパラメータの組み合わせで堅牢性テスト、不合格の場合設計やり直し
  5. これを2021-2022が最適化期間、2023がアウト期間のウォークフォワードテストまで繰り返す
  6. ウォークフォワードテストでは、2017-2023の7年分のフォワードテスト結果を得られる
    この中の70%-80%つまり、5年分で利益が出ていればウォークフォワードテスト合格

こう見るとかなり突破が難しそうなテストだが、テスト内容を妥協することに何のメリットもないため、これに従って実行していきます。

最適化変数のスキャンレンジについて

最適化を行うにあたって、まず変数のスキャンレンジを決めなければなりません。

まず時間足のスキャンレンジは、1分足や5分足の使用をしたくない点やデイトレ戦略にしたい点を考慮して、以下のようにしたいところです。

  • スタート:15Minutes
  • ステップ:空欄
  • ストップ:1Day

でも、違う時間足だと得られるトレードサンプルの数に差が出てきて、適切にパラメータ群を評価することができません。

したがって、ここでは考え方を変えて、「一つの時間足を前提とした手法を作り上げる」ことにしましょう。

つまり、「パラメータ」×「時間足」の2軸で最適化を行うのではなく、時間足を固定して「パラメータ」だけの最適化を行います。

また、今回はデイトレード戦略を開発したいという前提があるので、4時間足以上をエントリー足として使うことはないでしょう。

加えて、今回の手法では1分足や5分足を使いたくないという要望があるので、今回の手法では30分足を採用します。

また、すべてのフォワードテストで十分なサンプル数(最低でも100)が欲しいという観点でも、時間足は短めに設定したい。

次にday_countのスキャンレンジです。スタートは1だとして、ステップやストップをどのように設定するかは裁量的な要素が入ってしまいます。

今回は30分足を採用しているので、24時間前つまり47をストップにして、1ステップずつ最適化作業をしていきます。

最適化ステップ

もう一度最適化のプロセスを確認していきましょう。

  1. 2015.1-2016.12の最適化→パラメータの組み合わせの70%以上がプラス利益なら合格
    不合格の場合は、EAの設計自体をやり直す
  2. 2015-2016で最適化したEAを2017.1-2017.6でフォワードテスト
  3. モンキーEAを作って2017.1-2017.6を集計し、EAのフォワードテストの結果と比較
    モンキーEAより作成したEAが優れていたら合格、不合格の場合は設計からやり直し
  4. 2015.7-2017.6が最適化期間、2017.7-2017.12がアウト期間のウォークフォワードテスト→最適化期間でのパラメータの組み合わせで堅牢性テスト、不合格の場合設計やり直し
  5. これを2021-2022が最適化期間、2023.1-2023.6がアウト期間のウォークフォワードテストまで繰り返す
  6. ウォークフォワードテストでは、2017-2023.6までの13回分のフォワードテスト結果を得られる
    この中の70%-80%つまり、10回分で利益が出ていればウォークフォワードテスト合格

ステップ1:2015-2016の最適化

このステップの合格基準は「パラメータの組み合わせの70%以上がプラス利益であること」です。

最適化を行ったところ、13個(1~13)のパラメータでマイナス利益をだしたので、72%がプラス利益となり、合格となった。

ステップ2:2015-2016で最適化したEAを2017.1-2017.6でフォワードテスト

では、最適化したパラメータでフォワードテストを行っていきます。ここで問題となるのは、どのパラメータを採用するかです。

過剰最適化を防ぐためにも、今回は藍崎さんのnoteを参考に採用する「最良結果を中心に1~5%の範囲で、最も低い結果を持つパラメータ」を設定していきます。

今回最良結果は「41」なので、39から43までの間で最も低い結果をもつパラメータを採用します。つまり、「40」を採用します。

では、パラメータの数値を「40」にしてフォワードテストを行っていきます。以下は、フォワードテストの結果です。

  • 純益 -3722.72
  • 総利益 19042.27
  • 総損失 -22764.99
  • プロフィットファクタ 0.84
  • 期待利得 -32.94
  • 絶対ドローダウン 4917.86
  • 最大ドローダウン 8829.24 (63.47%)
  • 相対ドローダウン 63.47% (8829.24)
  • 総取引数 113
  • 売りポジション(勝率%) 57 (31.58%)
  • 買いポジション(勝率%) 56 (42.86%)
  • 勝率(%) 42 (37.17%)
  • 負率 (%) 71 (62.83%)
  • 最大勝トレード 2258.82
  • 最大敗トレード -2076.14
  • 平均勝トレード 453.39
  • 平均敗トレード -320.63
  • 最大連勝(金額) 6 (3256.08)
  • 最大連敗(金額) 11 (-4946.72)
  • 最大連勝(トレード数) 3256.08 (6)
  • 最大連敗(トレード数) -4946.72 (11)
  • 平均連勝 2
  • 平均連敗 3

3:モンキーEAを作成する

//+------------------------------------------------------------------+
//|                                                 Mi_Randomkun.mq4 |
//|                                                     minagachi FX |
//|                                            https://minagachi.com |
//+------------------------------------------------------------------+
#property copyright "minagachi FX"
#property link      "https://minagachi.com"
#property version   "1.00"
#property strict

#define MAGICRANDOM 10000008  //マジックナンバー
input double Lot = 0.1;       // ロット数
//input int Wait_Max = 1440;    // 最大待機時間(分)→つまり頻度
input double BuyProbability = 0.5;//買いの発生確率(0.0から1.0のあいだ)
input int Max_Trade_Per_Month = 45;//実際のトレードデータによる月の最大トレード数
input int Min_Trade_Per_Month = 28;//実際のトレードデータによる月の最小トレード数
//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   if(!IsTesting()){
      MessageBox("ランダムくんはバックテストでないとトレードしません。");
   }
   MathSrand(GetTickCount()); // 乱数シード
   return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   //処理なし
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // バックテストかどうかチェック
   if(!IsTesting()){
      return;
   }

   // 変数宣言
   static bool initialized = false; // 初回処理判定
   static datetime time_trade;      // トレード時刻

   // 初回処理のみ
   if(!initialized){
      // トレード時刻を算出
      time_trade = CalcTimeTrade();
      initialized = true;
   }

   // トレード時刻になったら
   if(TimeCurrent() > time_trade){

      // ポジションなしの場合は新規エントリー
      if(CalcOrders() == 0){
         DoEntry();
         // トレード時刻を更新
         time_trade = CalcTimeTrade();
      }

      // ポジション保有中の場合はイグジット
      else{
         DoExit();
         // トレード時刻を更新
         time_trade = CalcTimeTrade();
      }
   }
}

//+------------------------------------------------------------------+
//|関数 次回のトレード時刻をランダムで算出                               |
//+------------------------------------------------------------------+
datetime CalcTimeTrade(){
   // 待機時間(0~1440分)をランダムで決める
   int Wait_Max=30*24*60/Max_Trade_Per_Month/2;
   int Wait_Min=30*24*60/Min_Trade_Per_Month/2;
   
   Print("WaitMaxは",Wait_Max,"WaitMinは",Wait_Min);
   int time_wait = MathRand()%(Wait_Max - Wait_Min + 1) + Wait_Min;
   // 秒に換算
   time_wait = time_wait * 60;

   // トレード時刻を計算(現在時刻+待機時間)
   datetime time_next = TimeCurrent() + (datetime)time_wait;
   return time_next;
}

//+------------------------------------------------------------------+
//|関数 ポジション数計算                                               |
//+------------------------------------------------------------------+
int CalcOrders(){
   bool res;
   int num = 0;
   for(int i = 0; i < OrdersTotal(); i++){
      res = OrderSelect(i, SELECT_BY_POS, MODE_TRADES);
      if(OrderSymbol() == Symbol() && OrderMagicNumber() == MAGICRANDOM){
         num++;
      }
   }
   return(num);
}

//+------------------------------------------------------------------+
//|関数 エントリー処理                                                 |
//+------------------------------------------------------------------+
void DoEntry(){
   bool res;
   // エントリー方向をランダムに決定
   /*
   Print("MathRandの値は",MathRand(),"INT_MAXの値は",INT_MAX);
   
   double randNumber = MathRand()/(double)INT_MAX;//0から1の間の擬似乱数を生成
   Print("randNumberは",randNumber);
   
   */
   double randNumber = (double)(MathRand()%99 + 1) / 100;//整数型どうしの割り算の結果は整数型だから明示的にキャスト(変換)が必要
   Print("ManthRand()%99+1 / 100=",(MathRand()%99 + 1)/100);
   Print("RandNumberは",randNumber);
    int direction = (randNumber < BuyProbability) ? OP_BUY : OP_SELL;
    Print("注文の方向は",direction);//どっちの注文が入っているか

   // ロング
   if(direction == OP_BUY){
      res = OrderSend(Symbol(), OP_BUY, Lot, Ask, 0, 0, 0, "", MAGICRANDOM, 0, clrRed);
      return;
   }

   // ショート
   if(direction == OP_SELL){
      res = OrderSend(Symbol(), OP_SELL, Lot, Bid, 0, 0, 0, "", MAGICRANDOM, 0, clrBlue);
      return;
   }
}

//+------------------------------------------------------------------+
//|関数 イグジット処理                                                 |
//+------------------------------------------------------------------+
void DoExit(){
   bool res;
   for(int i=0; i < OrdersTotal(); i++){
      res = OrderSelect(i, SELECT_BY_POS, MODE_TRADES);
      if(OrderMagicNumber() != MAGICRANDOM || OrderSymbol() != Symbol()){
         continue;
      }
      if(OrderType() == OP_BUY){
         res = OrderClose(OrderTicket(), OrderLots(), Bid, 3, clrWhite);
         break;
      }
      if(OrderType() == OP_SELL){
         res = OrderClose(OrderTicket(), OrderLots(), Ask, 3, clrWhite);
         break;
      }
   }
}

このテスト用にモンキーEAを作成しようとしていましたが、なかなか理想とするものが作れず困っていたところ、みながち!FXさんがまさに僕がやろうとしていることをコード化していて、助かりました。

少しだけ、カスタマイズさせてもらって使用しています。

このモンキーEAにパラメータを入力していきます。

その結果このようになりました。

その次にこのモンキーEAのトレード結果をQuantAnalyzerに取り込んで、トレード結果を100個に複製します。(モンテカルロ分析)

この100個のモンキーEAを利益総額順に上から並べて、僕が作ったEAのテスト結果が上位10%に入っていれば、モンキーテストは合格とし、これ以降のウォークフォワードテストに進みます。

バックテストのレポートをHTML形式で保存して、QuantAnalyzerに取り込みます。

ソフトでテスト結果を100個に増やすと以下のようになります。

今回作成したEAは純益 -3722.72なので、どう考えても優位性はありませんね。

ということは、戦略自体を変更する必要がありそうです。