MENU

【PO-EA#3】ドローダウンを論理的に削る。ATRステップ・トレールでPF1.46へ

ドローダウンを論理的に削る。ATRステップ・トレール実装でPF 1.46へ進化

前回は、AI(Gemini)に指示書を作ってもらい、AI(Claude)にプログラムを作ってもらった「パーフェクトオーダーEA」にATRに基づいたTPとSLを追加し、5年間のバックテストを完走させ、プロフィットファクターを1.20まで改善させました。

ですがプロフィットファクターはまだ良い数値とは言えず、さらに最大ドローダウンが32.29%とリスクの高い数値でした。

今回は、最大ドローダウンをさらに改善させるため、前回に引き続き「ATR(Average True Range)」を用いてドローダウンの改善を目指します。

目次

なぜドローダウンが32%も出たのか?その正体を暴く

まずは、ドローダウンが高い理由を考えてみましょう。

簡単に思いつくのは、以下でしょうか。

  • 勝率が低い
  • ボラティリティの波に呑まれるSL(損切り)設定

勝率が低い|トレンドフォロー型EAの宿命「利益の吐き出し」

勝率が低い事自体は問題ありませんが、勝率が低い=損失がでているということです。

今回は、ATRにより利確幅や損失幅が変動するとはいえリスクリワード比率が1:2なので、雑に考えて、勝率は33.4%以上必要です。

勝つと+100円、負けると-50円の場合

トントンになるのが2回負けて1回勝つ(勝率33.3%)なので、勝率33.4%以上必要

ボラティリティの波に呑まれるSL(損切り)設定

これは少し盲点かもしれません。

ATRを用いてTPやSLを計算しているので、ボラティリティが大きいときにSLにかかると、それだけ損失が大きくなります。

例えばボラティリティが小さい時ばかりTPにかかり、ボラティリティが大きいときにSLにかかると、仮に勝率が高めであったとしても、最終結果が損失になってしまう可能性もあります。

「ATRステップ・トレール」で負けない土台を作る

勝率の低さと、損失を減らす両方を解決する、トレーリングストップを実装します。

前回説明した通り、パーフェクトオーダーはトレンドフォロー型なので、その時のボラティリティにより伸び幅が違います。

これを踏まえて、今回はATRを用いたトレーリングストップで、かつステップ式を採用します。

Tips

トレーリングストップには、追従式とステップ式があります。

追従式は、トレーリングストップを開始後は、1pointでも利益方向に動いたらSLを追従させていきます。

少しでも利益を確保できるメリットがありますが、SLが一定の距離で追従していくので、押し目などでSLにかかってしまう場合があります。

ステップ式は、ある一定の間隔で階段状にSLを引き上げていく方式です。

一定間隔ごとに利益を確保するので、追従式ほど利益確保の度合いは薄いですが、押し目などでSLにかかりにくいメリットがあります。

戦略1:まずはリスクをゼロにする「建値移動」

まずは、トレーリングストップの開始で建値にSLを移動させ、リスクをゼロにしてしまいます。

利益方向に動いた後、TPにかからずに下がってしまっても、リスクがゼロなので安心というわけですね。

戦略2:相場の呼吸を邪魔しない「ステップ式追従」

パーフェクトオーダーはトレンドフォロー型、つまりポジション保有中に何度か押し目ポイントが発生する可能性があります。

せっかくまだ伸びるかもしれないのに追従させたSLで刈られてしまうのはもったいないので、ある程度利益方向に動いたときに移動させるステップ式を採用というわけです。

ATRトレーリングストップの実装内容|実戦仕様の実装:ストップレベルの壁を突破する

それでは実装です。

ちょっとトレール判定部分が長いです。

// パラメータ設定
input string ATR_TrailConfig = "";              //=== ATR トレール設定 ===
input bool ATR_TrailON = true;                  //├ON/OFF
input double ATR_TrailMult = 1.0;               //└倍率

void OnTick()
  {
   (中略)
   
   // MA決済の上に追加
   // ポジションがある場合はトレールチェック
   if(ATR_TrailON && hasPosition)
     {
      CheckATRTrail();
     }
   
   (中略)
  }

//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+

void CheckATRTrail(void)
  {
   RefreshRates();
   
   // 確定足のATRを使用
   double ATR = iATR(_Symbol, _Period, ATR_Period, 1);
   double trailValue = ATR*ATR_TrailMult;
   
   double stopLevel = MarketInfo(_Symbol, MODE_STOPLEVEL) * _Point;
   double adjust = 10 * _Point;
   double minDistance = stopLevel + adjust;
   
   // ストップレベルを考慮した最終的なトレール幅
   double finalTrailDist = MathMax(NormalizeDouble(trailValue, _Digits), NormalizeDouble(minDistance, _Digits));
   
   for(int i = OrdersTotal()-1; i>=0 && !IsStopped(); i--)
     {
      if(!OrderSelect(i, SELECT_BY_POS))
         continue;
      if(OrderSymbol()!=_Symbol)
         continue;
      if(OrderMagicNumber()!=MagicNumber)
         continue;
      if(OrderCloseTime()>0)
         continue;
      int type = OrderType();
      if(type!=OP_BUY && type!=OP_SELL)
         continue;
      
      double currSL = OrderStopLoss();
      
      if(type==OP_BUY)
        {
         // トレール未開始(SLが建値より損失)
         if(NormalizeDouble(OrderStopLoss(), _Digits)==0 ||
            NormalizeDouble(OrderStopLoss(), _Digits)<NormalizeDouble(OrderOpenPrice(), _Digits))
           {
            if(NormalizeDouble(Bid, _Digits)>=NormalizeDouble(OrderOpenPrice()+finalTrailDist, _Digits))
              {
               PrintFormat("[%d] トレール開始 | %d", __LINE__, OrderTicket());
               PrintFormat("[%d] Bid : %s | ATR : %s | 判定値幅 : %s | 建値 : %s",
                           __LINE__, DoubleToStr(Bid, _Digits), DoubleToStr(ATR, _Digits),
                           DoubleToStr(finalTrailDist, _Digits), DoubleToStr(OrderOpenPrice(), _Digits));
               
               bool result = OrderModify(OrderTicket(), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(OrderTakeProfit(), _Digits), OrderExpiration());
      
               if(result)
                 {
                  PrintFormat("[%d] トレール注文成功 | チケット : #%d | 更新したSL価格 : %s",
                              __LINE__, OrderTicket(), DoubleToStr(OrderStopLoss(), _Digits));
                 }
               else
                 {
                  int error = GetLastError();
                  PrintFormat("[%d] トレール注文失敗 | チケット : #%d | 理由 : %d | 詳細 : %s",
                              __LINE__, OrderTicket(), error, ErrorDescription(error));
                 }
              }
           }
         else
           {
            double SL = OrderStopLoss()+finalTrailDist;
            
            if(NormalizeDouble(Bid-finalTrailDist, _Digits)>=NormalizeDouble(SL, _Digits))
              {
               PrintFormat("[%d] トレール更新 | %d", __LINE__, OrderTicket());
               PrintFormat("[%d] Bid : %s | ATR : %s | 判定値幅 : %s | 現在のSL : %s",
                           __LINE__, DoubleToStr(Bid, _Digits), DoubleToStr(ATR, _Digits),
                           DoubleToStr(finalTrailDist, _Digits), DoubleToStr(OrderStopLoss(), _Digits));
               
               bool result = OrderModify(OrderTicket(), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(SL, _Digits), NormalizeDouble(OrderTakeProfit(), _Digits), OrderExpiration());
      
               if(result)
                 {
                  PrintFormat("[%d] トレール注文成功 | チケット : #%d | 更新したSL価格 : %s",
                              __LINE__, OrderTicket(), DoubleToStr(OrderStopLoss(), _Digits));
                 }
               else
                 {
                  int error = GetLastError();
                  PrintFormat("[%d] トレール注文失敗 | チケット : #%d | 理由 : %d | 詳細 : %s",
                              __LINE__, OrderTicket(), error, ErrorDescription(error));
                 }
              }
           }
        }
      else
        {
         // トレール未開始(SLが建値より損失)
         if(NormalizeDouble(OrderStopLoss(), _Digits)==0 ||
            NormalizeDouble(OrderStopLoss(), _Digits)>NormalizeDouble(OrderOpenPrice(), _Digits))
           {
            if(NormalizeDouble(Ask, _Digits)<=NormalizeDouble(OrderOpenPrice()-finalTrailDist, _Digits))
              {
               PrintFormat("[%d] トレール開始 | %d", __LINE__, OrderTicket());
               PrintFormat("[%d] Ask : %s | ATR : %s | 判定値幅 : %s | 建値 : %s",
                           __LINE__, DoubleToStr(Ask, _Digits), DoubleToStr(ATR, _Digits),
                           DoubleToStr(finalTrailDist, _Digits), DoubleToStr(OrderOpenPrice(), _Digits));
               
               bool result = OrderModify(OrderTicket(), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(OrderTakeProfit(), _Digits), OrderExpiration());
      
               if(result)
                 {
                  PrintFormat("[%d] トレール注文成功 | チケット : #%d | 更新したSL価格 : %s",
                              __LINE__, OrderTicket(), DoubleToStr(OrderStopLoss(), _Digits));
                 }
               else
                 {
                  int error = GetLastError();
                  PrintFormat("[%d] トレール注文失敗 | チケット : #%d | 理由 : %d | 詳細 : %s",
                              __LINE__, OrderTicket(), error, ErrorDescription(error));
                 }
              }
           }
         else
           {
            double SL = OrderStopLoss()-finalTrailDist;
            
            if(NormalizeDouble(Ask+finalTrailDist, _Digits)<=NormalizeDouble(SL, _Digits))
              {
               PrintFormat("[%d] トレール更新 | %d", __LINE__, OrderTicket());
               PrintFormat("[%d] Ask : %s | ATR : %s | 判定値幅 : %s | 現在のSL : %s",
                           __LINE__, DoubleToStr(Ask, _Digits), DoubleToStr(ATR, _Digits),
                           DoubleToStr(finalTrailDist, _Digits), DoubleToStr(OrderStopLoss(), _Digits));
               
               bool result = OrderModify(OrderTicket(), NormalizeDouble(OrderOpenPrice(), _Digits), NormalizeDouble(SL, _Digits), NormalizeDouble(OrderTakeProfit(), _Digits), OrderExpiration());
      
               if(result)
                 {
                  PrintFormat("[%d] トレール注文成功 | チケット : #%d | 更新したSL価格 : %s",
                              __LINE__, OrderTicket(), DoubleToStr(OrderStopLoss(), _Digits));
                 }
               else
                 {
                  int error = GetLastError();
                  PrintFormat("[%d] トレール注文失敗 | チケット : #%d | 理由 : %d | 詳細 : %s",
                              __LINE__, OrderTicket(), error, ErrorDescription(error));
                 }
              }
           }
        }
     }
  }

コード

ポイントは以下の5点です。

  1. パラメータ設定でATRの倍率を変更できるようにする(初期値は1.0倍です)
  2. ストップレベルに抵触したときは、ストップレベル+1.0pipsを判定値幅にする
    ※ストップレベルとは、注文を出せる最低限の価格幅のこと
  3. SLを発注できるので、トレール未開始かどうかの判定条件を追加する(SLが0、またはSLが建値未満の場合はトレール未開始)
  4. トレール未開始の場合は、建値からATRのn倍以上利益方向に動いたらSLを建値に移動させ、トレール開始
  5. トレール開始以降は、SLをATRのn倍ごとにステップ移動

初心者キラー「エラー130」を回避する+1pipsのバッファ

前回、ストップレベルに抵触した際はエントリーを見送る仕様にしましたが、トレーリングストップにおいては話が別です。

せっかくの利益を守るためのトレールを諦めるのは、あまりにももったいない。

そこで今回は、ルールに従いつつもストップレベルを回避する『+1.0pipsの調整ロジック』を組み込みました。

この調整ロジックがないと、設定次第ではトレールが1度も発動しないという自体にもなり得ますのでかなり重要なポイントです。

また、トレール未開始の時の処理と、トレール開始後の処理が少し違うので、しっかり書き分けます。

積み増しを見据えた「複数ポジション管理」の土台作り

ついでに、最大保有ポジション数の制限も切り替えるようにしちゃいました。

トレールで利益を確保していくので、積み増ししていったらどうなるのかの検証をしてみても面白そうですよね。

// パラメータ設定
input bool     OnlyOnePos = true;              // 最大保有ポジションの制限

// OnTick内にある、エントリー条件判定時のif文を以下に変更
if(!OnlyOnePos || !hasPosition)
  {
   // エントリー処理
   (中略)
  }

検証結果:PFは1.46へ、ドローダウンは24%へ劇的改善

さて、それではバックテストです。

設定は前回と同じく以下の通り。

  • 通貨ペア:USDJPY
  • 時間足:1時間足
  • スプレッド:20point(2.0pips)
  • 期間:2021年1月1日~2025年12月31日(5年間)
  • 初期資金:10万円

トレールの設定は初期値のまま1.0倍です。

バックテスト結果はこうなりました。

ATRトレーリングストップを導入したEAのバックテスト結果。ドローダウンが緩和され、より右肩上がりの資産曲線になった。
下がり幅がなだらかになりました

前回の結果も比較できるように貼りましょう。

前回のATRによる出口戦略を導入したEAのバックテスト結果。ドローダウンが目立つ。

右肩上がりの資産曲線が示す「ロジックの正しさ」

前回よりもより滑らかな右肩上がりの資産曲線になりましたね。

前半は前回の資産曲線よりもなだらかな上昇になっていますが、74から87にかけてのドローダウンが浅くなり、その他のギザギザしていた資産曲線の上下動もなだらかになりました。精神衛生的に嬉しい変化です。

数値データ比較:論理は期待を裏切らない

数値データは、こんな感じに変化しました。

項目実装前実装後
取引数160160
プロフィットファクター1.201.46
純益+78888.37円+98636.08円
最大ドローダウン32.29%(61518.70円)24.65%(48459.88円)
勝率37.50%48.12%
期待利得493.05618.48

数値で比較してもわかりやすく改善していますね。

初期証拠金が10万円で、5年で約10万円プラスということで、結構良い伸び率ですね。

まとめ:AIに作ってもらったEAも、改良を加えれば戦えるかも?

AIに作ってもらった最初のEAは破綻してしまいましたが、決済ロジックの変更やトレーリングストップの実装によって、かなり成績が改善されました。

スプレッドをそこまで厳しく設定していない状態でこの成績なので、現段階では即戦力!とは言えませんが、カスタマイズをしっかりすれば戦えるEAになりそうですね。

次なる挑戦:TPの撤廃とAIフィルターの魂入れ

一先ず大きな機能の追加は完了として、次はパラメータ設定の最適化を行っていきましょう。

せっかくトレーリングストップを実装したので、TPは無効化できるように追加する予定です。

カーブフィッティングに気をつけながら、さらに良い成績を追求します!

今回作成したコード

今回作成したMQL4コードはこちらです。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

開発エンジニア。「AIでEAを作ったが動かない」という壁を突破するための、修正のコツや検証技術を発信しています。作成代行や販売経験を活かし、皆さんが自分のロジックを自由に形にできるための小さな「燈」を灯します。 (※現在は作成代行の受付は行っておりません)

コメント

コメントする

CAPTCHA


目次