💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買システムの開発は困難な道のりです。多くのロジックが、たった一度の相場変動で破綻します。今回は、10年のバックテストを生き抜いたロジックを公開します。成績は控えめですが、極めて堅牢な設計です。

もともと、私はブレイクアウト手法のダマシに悩まされていました。トレンドを追うほど、往復ビンタに合う日々が続いたからです。そこで視点を変え、価格の「定常性」に着目しました。価格が一定の範囲に戻る性質を、数学的に捉えたいと考えたのが始まりです。フィルターを厳しくしすぎた時期もあり、取引回数が激減して絶望しました。試行錯誤を繰り返し、ようやく実用的なバランスに到達しました。

このコードをベースにすれば、MT5のEA開発にかかる数百時間を節約できます。自分だけのオリジナルEAを作るための、最高の土台として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。

項目 実績値
プロフィットファクター (PF) 1.32
勝率 47.4%
総取引数 215 回
純利益 (0.1ロット/1万通貨時) ¥78607.66
最大ドローダウン 2.49%
リカバリーファクター 3.16
期待利得 (Expected Payoff) 0.70

今回の検証結果の「限界」と「ダメ出し」

このロジックは、安定感こそ抜群ですが、爆発力に欠けます。PF1.32という数値は、プロの視点では「平凡」な成績です。勝率が50%を下回るため、精神的な負荷がかかる局面があります。

最大の弱点は、取引回数の少なさです。10年で215回という頻度は、年平均でわずか21回しか取引していません。これは、定常性判定フィルターを厳しく設定しすぎた結果です。強いトレンド相場を完全に排除したため、大きな利益を取り逃しています。安全性を追求しすぎて、機会損失を招いた形となりました。

🤖
「守り」に特化しすぎたせいで、稼ぎきれないもどかしさがありますね。

ロジックの技術的詳細

本ロジックは、ADXとボリンジャーバンドを組み合わせた平均回帰戦略です。

項目 設定・条件
ベース時間足 5分足
トレンドフィルター ADX(14) < 30 かつ 上位足ADX(1h) < 35
定常性判定 BBミドルラインの傾き(Slope) < 0.15
ボラティリティ制限 ATR(14)/ATR(100) が 0.5 〜 2.0 の範囲内
買いエントリー Low < BBL かつ RSI(14) < 35 かつ Close > BBL
売りエントリー High > BBU かつ RSI(14) > 65 かつ Close < BBU
決済ルール TP: 40pips / SL: 30pips / トレーリングストップあり

どう改善すべきか(次なる展望)

このコードをベースに、さらに利益を伸ばす余地は十分にあります。まずは、定常性判定の閾値を動的に変更する仕組みを導入すべきです。相場のボラティリティに合わせて、Slopeの制限値を変動させます。これで、取引回数を適切に増やせるはずです。

また、上位足の環境認識に、価格帯別出来高や重要レジサポラインを加えたいところです。単純な指標だけでなく、壁となる価格帯を認識させれば、勝率は向上します。プロのEAは、こうした独自のフィルターで精度を高めています。ぜひ、あなた自身のアイデアでこの「原石」を磨き上げてください。

公開Pythonコード

🔍 プロはどうやってダマシを回避しているのか?

今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。

👉 プロのフィルターロジックが詰まった実績EA(BeeOne_USDJPY)を参考にする

from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np

class ASMRStrategy(BaseStrategy):
    """
    Adaptive Stationary Mean-Reversion (ASMR) Strategy - Recovery Version
    - 【改善点】取引回数不足を解消するため、定常性判定フィルターを「厳格」から「適正」へ緩和。
    - 【改善点】RSI閾値を30/70 → 35/65へ拡大し、エントリー機会を増加。
    - 【改善点】時間帯フィルターを撤廃し、インジケーターによる定常性判定のみで取引を制御。
    - 【改善点】ペイオフレシオ向上のため、TP/SL比率を再設計。
    """

    def __init__(self):
        # PFおよびリカバリーファクター向上のため、損切を適正化し、利確を期待値に基づき調整
        super().__init__(
            name="ASMR_Strategy_Recovery", 
            default_tp_pips=40.0, 
            default_sl_pips=30.0, 
            enable_trailing_stop=True, 
            trail_start_pips=15.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        # --- 1. 基本インジケーター計算 (5m) ---
        # ADX: トレンド強度 (14期間)
        adx_df = df.ta.adx(length=14)
        if adx_df is not None:
            df['ADX'] = adx_df.iloc[:, 0] # インデックスによるアクセスでカラム名エラーを完全に排除
        
        # Bollinger Bands: 価格境界 (20期間, 2.0標準偏差)
        bb_df = df.ta.bbands(length=20, std=2.0)
        if bb_df is not None:
            df['BBL'] = bb_df.iloc[:, 0]
            df['BBM'] = bb_df.iloc[:, 1]
            df['BBU'] = bb_df.iloc[:, 2]
        
        # RSI: 過熱感 (14期間)
        df['RSI'] = df.ta.rsi(length=14)
        
        # ATR: ボラティリティ
        df['ATR_14'] = df.ta.atr(length=14)
        df['ATR_100'] = df.ta.atr(length=100)
        
        # BB-Midline Slope (正規化傾き)
        # 閾値を緩和するため、分母に微小値を加え安定化
        df['BB_Slope'] = df['BBM'].diff(3) / (df['ATR_14'] + 1e-9)
        
        # ATR Ratio (ボラティリティの相対的安定性)
        df['Vol_Ratio'] = df['ATR_14'] / (df['ATR_100'] + 1e-9)
        
        # --- 2. MTFフィルター実装 (1時間足) ---
        h1_close = df['Close'].resample('1h').last().shift(1)
        h1_high = df['High'].resample('1h').max().shift(1)
        h1_low = df['Low'].resample('1h').min().shift(1)
        
        df_h1 = pd.DataFrame({'High': h1_high, 'Low': h1_low, 'Close': h1_close}, index=h1_close.index)
        adx_h1_df = df_h1.ta.adx(length=14)
        
        if adx_h1_df is not None:
            df['ADX_1h'] = adx_h1_df.iloc[:, 0].reindex(df.index, method='ffill')
        else:
            df['ADX_1h'] = np.nan
            
        return df

    def generate_signal(self, df):
        if len(df) < 100:
            return None
            
        last = df.iloc[-1]
        
        # --- 【外科的調整】フィルター条件の緩和による取引回数の確保 ---
        # 1. ADX(5m) < 30: レンジ判定を緩め、緩やかなトレンド局面も許容 (取引数UP)
        # 2. ADX(1h) < 35: 上位足のトレンド制限を緩和
        # 3. BB_Slope < 0.15: 完全に水平でなくとも、一定の範囲内で回帰を狙う (取引数UP)
        # 4. Vol_Ratio (0.5 ~ 2.0): ボラティリティ変動への耐性を拡大
        # 5. 時間帯フィルターの撤廃: インジケーターによる判定を優先し、24時間チャンスを捕捉
        
        is_stationary = (
            (last['ADX'] < 30) and 
            (last['ADX_1h'] < 35) and 
            (abs(last['BB_Slope']) < 0.15) and 
            (0.5 < last['Vol_Ratio'] < 2.0)
        )
        
        if not is_stationary:
            return None

        # --- 【外科的調整】エントリートリガーの感度向上 ---
        # RSI閾値を 30/70 -> 35/65 に変更し、エントリー頻度を向上させつつ逆張り優位性を維持
        
        # 買いトリガー: LowがBBLを割り込み、RSIが35以下、かつ終値がBBLより上で確定(回帰)
        if (last['Low'] < last['BBL']) and \
           (last['RSI'] < 35) and \
           (last['Close'] > last['BBL']):
            return 'BUY'
            
        # 売りトリガー: HighがBBUを突き抜け、RSIが65以上、かつ終値がBBUより下で確定(回帰)
        if (last['High'] > last['BBU']) and \
           (last['RSI'] > 65) and \
           (last['Close'] < last['BBU']):
            return 'SELL'
            
        return None

MQL5コードへの翻訳

//+------------------------------------------------------------------+
//|                                     ASMR_Strategy_Recovery.mq5   |
//|                                  Copyright 2026, Quant Engineer  |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- 入力パラメータ
input int      InpADXPeriod   = 14;          // ADX Period
input int      InpBBPeriod    = 20;          // Bollinger Bands Period
input double   InpBBStdDev    = 2.0;         // Bollinger Bands StdDev
input int      InpRSIPeriod   = 14;          // RSI Period
input int      InpATRPeriod   = 14;          // ATR Period
input int      InpATRLong     = 100;         // ATR Long Period
input double   InpTP_Pips     = 40.0;        // Take Profit (Pips)
input double   InpSL_Pips     = 30.0;        // Stop Loss (Pips)
input double   InpTrailStart  = 15.0;        // Trailing Start (Pips)
input double   InpLotSize     = 0.1;         // Lot Size

//--- ハンドル
int handleADX, handleBB, handleRSI, handleATR14, handleATR100, handleADX_H1;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handleADX    = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
   handleBB     = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
   handleRSI    = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
   handleATR14  = iATR(_Symbol, PERIOD_M5, InpATRPeriod);
   handleATR100 = iATR(_Symbol, PERIOD_M5, InpATRLong);
   handleADX_H1 = iADX(_Symbol, PERIOD_H1, InpADXPeriod);

   if(handleADX == INVALID_HANDLE || handleBB == INVALID_HANDLE || 
      handleRSI == INVALID_HANDLE || handleADX_H1 == INVALID_HANDLE)
      return(INIT_FAILED);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 新しい足の確定時に判定を行うため、ここでは簡易的に最新値で判定
   // 実際には新足判定ロジックを推奨
   
   double adx[], bbUpper[], bbLower[], bbMid[], rsi[], atr14[], atr100[], adxH1[];
   ArraySetAsSeries(adx, true);
   ArraySetAsSeries(bbUpper, true);
   ArraySetAsSeries(bbLower, true);
   ArraySetAsSeries(bbMid, true);
   ArraySetAsSeries(rsi, true);
   ArraySetAsSeries(atr14, true);
   ArraySetAsSeries(atr100, true);
   ArraySetAsSeries(adxH1, true);

   if(CopyBuffer(handleADX, 0, 0, 3, adx) < 3) return;
   if(CopyBuffer(handleBB, 1, 0, 3, bbUpper) < 3) return;
   if(CopyBuffer(handleBB, 2, 0, 3, bbLower) < 3) return;
   if(CopyBuffer(handleBB, 0, 0, 3, bbMid) < 3) return;
   if(CopyBuffer(handleRSI, 0, 0, 3, rsi) < 3) return;
   if(CopyBuffer(handleATR14, 0, 0, 3, atr14) < 3) return;
   if(CopyBuffer(handleATR100, 0, 0, 3, atr100) < 3) return;
   if(CopyBuffer(handleADX_H1, 0, 0, 3, adxH1) < 3) return;

   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   if(CopyRates(_Symbol, PERIOD_M5, 0, 4, rates) < 4) return;

   // --- 定常性判定 (Stationarity) ---
   double slope = (bbMid[0] - bbMid[3]) / (atr14[0] + 1e-9);
   double volRatio = atr14[0] / (atr100[0] + 1e-9);
   
   bool isStationary = (adx[0] < 30) && 
                       (adxH1[0] < 35) && 
                       (MathAbs(slope) < 0.15) && 
                       (volRatio > 0.5 && volRatio < 2.0);

   if(!isStationary) return;

   // --- ポジション確認 ---
   bool hasPosition = PositionSelect(_Symbol);

   if(!hasPosition)
   {
      // 買いトリガー
      if(rates[0].low < bbLower[0] && rsi[0] < 35 && rates[0].close > bbLower[0])
      {
         double sl = rates[0].close - InpSL_Pips * _Point * 10;
         double tp = rates[0].close + InpTP_Pips * _Point * 10;
         trade.Buy(InpLotSize, _Symbol, rates[0].close, sl, tp, "ASMR Buy");
      }
      // 売りトリガー
      else if(rates[0].high > bbUpper[0] && rsi[0] > 65 && rates[0].close < bbUpper[0])
      {
         double sl = rates[0].close + InpSL_Pips * _Point * 10;
         double tp = rates[0].close - InpTP_Pips * _Point * 10;
         trade.Sell(InpLotSize, _Symbol, rates[0].close, sl, tp, "ASMR Sell");
      }
   }
   else
   {
      // トレーリングストップ実装
      ManageTrailingStop();
   }
}

//+------------------------------------------------------------------+
//| Trailing Stop Logic                                              |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   if(!PositionSelect(_Symbol)) return;
   
   double currentPrice = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
   double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
   double stopLoss = PositionGetDouble(POSITION_SL);
   
   if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
   {
      if(currentPrice - openPrice > InpTrailStart * _Point * 10)
      {
         double newSL = currentPrice - InpTrailStart * _Point * 10;
         if(newSL > stopLoss) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
      }
   }
   else
   {
      if(openPrice - currentPrice > InpTrailStart * _Point * 10)
      {
         double newSL = currentPrice + InpTrailStart * _Point * 10;
         if(newSL < stopLoss || stopLoss == 0) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
      }
   }
}