直近2年間の激しい金利変動相場で生き残るロジックは、徹底したフィルター設定にあります。

結論から述べます。このロジックが勝ち抜いた理由は、短期的な逆張りに中期的なトレンド方向を掛け合わせた点にあります。単なるボリンジャーバンドの反発を狙うのではなく、15分足の方向性と合致したときのみエントリーします。さらにADXを用いて極端なトレンド相場を回避する設計です。この厳格な絞り込みにより、最大ドローダウンを1.19%という極めて低い水準に抑え込みました。

バックテスト資産推移

これが直近2年間を生き抜いた資産推移の証拠です。

項目 実績値
プロフィットファクター (PF) 1.38
勝率 48.1%
総取引数 81回
純利益 $170.28
最大ドローダウン 1.19%

なぜこのロジックは生き残れたのか

結論から述べます。 15分足の上位トレンド方向に合わせ、5分足での逆張り(押し目買い・戻り売り)を徹底したからです。

理由は3点あります。 1つ目はマルチタイムフレーム(MTF)分析による大局トレンドの固定です。 2つ目はADXを用いた強烈なトレンド相場(バンドウォーク)の回避です。 3つ目はストキャスティクスとRSIによる反発タイミングの厳格化です。

具体例を挙げます。 15分足の20日SMAより価格が上の場合のみ買いを許可し、さらにADXが45を超えるハイパートレンド時はあえて静観します。 価格がボリンジャーバンド下限にタッチし、ストキャスティクスK線がD線を上抜いた瞬間を狙い撃ちします。

結論として、徹底的なフィルターでダマシを削減したことが、最大DD 1.19%という優れた成績をもたらしました。

🤖
取引回数は少ないですが、その分1回あたりの期待値が高くなっています。

戦略の技術的詳細

本ロジックの判定条件をまとめました。

評価項目 条件・設定 目的
時間帯フィルター 16:00 〜 23:00 (JST) 欧州・NYの流動性が高いメインタイムを狙う
大局トレンド 15m Close $\gtrless$ SMA(20) 15分足のトレンド方向への合流
環境認識 (ADX) ADX(14) < 45 強すぎるトレンド相場での逆張り破綻を回避
エントリー基準 BB Lower/Upper + Stoch Cross 買われすぎ・売られすぎの極値反発を捕捉
オシレーター RSI(14) < 40 (Buy) / > 60 (Sell) ダマシエントリーの多重カット
ここがポイント: 低ドローダウンを実現できたのは、ボラティリティの判定と時間帯制限(ロンドン・NY時間)を組み合わせたためです。

公開Pythonコード

AIが生成し、バックテストをクリアした元のロジックです。

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

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

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

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

class MeanReversionQuantStrategy(BaseStrategy):
    def __init__(self):
        # リスクリワード比を改善し、利益を最大化する設定
        super().__init__(
            name="TrendFollowing_MeanRev_v6",
            default_tp_pips=45.0, 
            default_sl_pips=15.0, 
            enable_trailing_stop=True, 
            trail_start_pips=8.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        テクニカル指標の計算。
        KeyErrorを完全に排除するため、ilocによる位置指定抽出を徹底。
        """
        # --- 5分足の指標 ---
        # ボリンジャーバンド (20, 2.0) -> [BBL, BBM, BBU]
        bbands = df.ta.bbands(length=20, std=2.0)
        df['BBL'] = bbands.iloc[:, 0]
        df['BBM'] = bbands.iloc[:, 1]
        df['BBU'] = bbands.iloc[:, 2]
        
        # RSI (14)
        df['RSI'] = df.ta.rsi(length=14)
        
        # ストキャスティクス (14, 3, 3) -> [STOCHk, STOCHd]
        stoch = df.ta.stoch(high='High', low='Low', close='Close', k=14, d=3, smooth_k=3)
        df['STOCHk'] = stoch.iloc[:, 0]
        df['STOCHd'] = stoch.iloc[:, 1]
        
        # ADX (14) -> [ADX, DMP, DMN]
        adx = df.ta.adx(length=14)
        df['ADX'] = adx.iloc[:, 0]
        
        # ATR (14)
        df['ATR'] = df.ta.atr(length=14)

        # --- マルチタイムフレーム (MTF) 分析 ---
        # 未来のカンニングを防止するため必ず .shift(1) を適用
        # 15分足のトレンド判定 (SMA20)
        df_15m_close = df['Close'].resample('15min').last().shift(1)
        df_15m_sma = df_15m_close.rolling(window=20).mean()
        df['Close_15m'] = df_15m_close.reindex(df.index, method='ffill')
        df['SMA_15m'] = df_15m_sma.reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        """
        トレンド方向への押し目買い・戻り売りを狙う高効率ロジック
        """
        if len(df) < 30:
            return None

        last = df.iloc[-1]
        
        # --- 1. エントリー時間帯の拡大 (ロンドン〜NYのメインタイム) ---
        current_hour = last.name.hour if hasattr(last.name, 'hour') else df.index[-1].hour
        is_trade_window = (16 <= current_hour < 23)
        if not is_trade_window:
            return None

        # --- 2. 相場環境フィルター ---
        # ADXが30以下ならレンジ、30-45ならトレンド相場。
        # 極端なトレンド (ADX > 45) では逆張りを禁止し、バンドウォークを回避。
        is_not_hyper_trend = last['ADX'] < 45
        has_volatility = last['ATR'] > 0
        if not (is_not_hyper_trend and has_volatility):
            return None

        # --- 3. MTFフィルター (15分足のトレンド方向への合流) ---
        # 15分足が上昇トレンドの時に5分足で売られすぎを狙う (Buy on Dip)
        # 15分足が下降トレンドの時に5分足で買われすぎを狙う (Sell on Rally)
        is_15m_bullish = last['Close_15m'] > last['SMA_15m']
        is_15m_bearish = last['Close_15m'] < last['SMA_15m']

        # --- 4. エントリーロジック (トリガーの最適化) ---
        
        # 【BUYシグナル】
        # 1. 価格水準: 価格がBBL付近にあり、売られすぎの状態
        # 2. モメンタム: ストキャスティクス K線がD線を上抜いた (最重要トリガー)
        # 3. 補強: RSIが40以下であること
        # 4. 方向性: 15分足が上昇トレンドであること(押し目買い)
        buy_condition = (
            (last['Low'] <= last['BBL'] * 1.0005) and 
            (last['STOCHk'] > last['STOCHd']) and 
            (last['RSI'] < 40) and 
            (is_15m_bullish)
        )

        # 【SELLシグナル】
        # 1. 価格水準: 価格がBBU付近にあり、買われすぎの状態
        # 2. モメンタム: ストキャスティクス K線がD線を下抜いた (最重要トリガー)
        # 3. 補強: RSIが60以上であること
        # 4. 方向性: 15分足が下降トレンドであること(戻り売り)
        sell_condition = (
            (last['High'] >= last['BBU'] * 0.9995) and 
            (last['STOCHk'] < last['STOCHd']) and 
            (last['RSI'] > 60) and 
            (is_15m_bearish)
        )

        if buy_condition:
            return 'BUY'
        elif sell_condition:
            return 'SELL'
        
        return None

MT5用 MQL5コードへの翻訳

このPythonロジックをそのままMT5で動作するように翻訳しました。.mq5ファイルとして保存して使用してください。

//+------------------------------------------------------------------+
//|                                      TrendFollowing_MeanRev_v6.mq5|
//|                                  Copyright 2026, Expert Developer |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Expert Developer"
#property link      "https://your-blog-url.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

// 入力パラメータ
input double   InpTP = 45.0;          // Take Profit (pips)
input double   InpSL = 15.0;          // Stop Loss (pips)
input double   InpTrailStart = 8.0;   // Trailing Stop Start (pips)
input int      InpBBPeriod = 20;      // Bollinger Bands Period
input double   InpBBDev = 2.0;        // Bollinger Bands Deviation
input int      InpRSIPeriod = 14;     // RSI Period
input int      InpStochK = 14;        // Stochastic K Period
input int      InpStochD = 3;         // Stochastic D Period
input int      InpStochS = 3;         // Stochastic Slowing
input int      InpADXPeriod = 14;     // ADX Period
input int      InpATRPeriod = 14;     // ATR Period
input int      InpSMA15Period = 20;   // 15m SMA Period

// グローバル変数
int handleBB, handleRSI, handleStoch, handleADX, handleATR, handleSMA15;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleBB    = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBDev, PRICE_CLOSE);
    handleRSI   = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
    handleStoch = iStochastic(_Symbol, PERIOD_M5, InpStochK, InpStochD, InpStochS, MODE_SMA, STO_LOWHIGH);
    handleADX   = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    handleATR   = iATR(_Symbol, PERIOD_M5, InpATRPeriod);
    handleSMA15 = iMA(_Symbol, PERIOD_M15, InpSMA15Period, 0, MODE_SMA, PRICE_CLOSE);

    if(handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE || handleSMA15 == INVALID_HANDLE)
        return(INIT_FAILED);

    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    
    // 1. 時間帯フィルター (16時〜23時)
    if(dt.hour < 16 || dt.hour >= 23) return;

    // データバッファ
    double bbl[], bbu[], rsi[], stochK[], stochD[], adx[], atr[], sma15[], close15[];
    ArraySetAsSeries(bbl, true);
    ArraySetAsSeries(bbu, true);
    ArraySetAsSeries(rsi, true);
    ArraySetAsSeries(stochK, true);
    ArraySetAsSeries(stochD, true);
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(atr, true);
    ArraySetAsSeries(sma15, true);
    ArraySetAsSeries(close15, true);

    if(CopyBuffer(handleBB, 2, 0, 2, bbl) <= 0) return; // Lower Band
    if(CopyBuffer(handleBB, 1, 0, 2, bbu) <= 0) return; // Upper Band (Note: index depends on implementation)
    // Correction for iBands buffers: 0=BASE_LINE, 1=UPPER_BAND, 2=LOWER_BAND
    CopyBuffer(handleBB, 1, 0, 2, bbu); 
    CopyBuffer(handleBB, 2, 0, 2, bbl);

    if(CopyBuffer(handleRSI, 0, 0, 2, rsi) <= 0) return;
    if(CopyBuffer(handleStoch, 0, 0, 2, stochK) <= 0) return;
    if(CopyBuffer(handleStoch, 1, 0, 2, stochD) <= 0) return;
    if(CopyBuffer(handleADX, 0, 0, 2, adx) <= 0) return;
    if(CopyBuffer(handleATR, 0, 0, 2, atr) <= 0) return;
    if(CopyBuffer(handleSMA15, 0, 1, 1, sma15) <= 0) return; // Shift 1 to avoid look-ahead
    
    double close15_val = iClose(_Symbol, PERIOD_M15, 1);
    double current_low = iLow(_Symbol, PERIOD_M5, 0);
    double current_high = iHigh(_Symbol, PERIOD_M5, 0);

    // 2. 相場環境フィルター
    if(adx[0] >= 45 || atr[0] <= 0) return;

    // 3. MTFトレンド判定
    bool is15mBullish = (close15_val > sma15[0]);
    bool is15mBearish = (close15_val < sma15[0]);

    // エントリー条件判定
    bool buyCondition = (current_low <= bbl[0] * 1.0005) && (stochK[0] > stochD[0]) && (rsi[0] < 40) && is15mBullish;
    bool sellCondition = (current_high >= bbu[0] * 0.9995) && (stochK[0] < stochD[0]) && (rsi[0] > 60) && is15mBearish;

    if(PositionsTotal() == 0)
    {
        if(buyCondition)
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point * 10;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point * 10;
            trade.Buy(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), sl, tp);
        }
        else if(sellCondition)
        {
            double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point * 10;
            double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point * 10;
            trade.Sell(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID), sl, tp);
        }
    }
    
    // トレーリングストップ処理
    ManageTrailingStop();
}

void ManageTrailingStop()
{
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket))
        {
            double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
            double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
            double sl = PositionGetDouble(POSITION_SL);
            
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            {
                if(currentPrice - openPrice > InpTrailStart * _Point * 10)
                {
                    double newSL = currentPrice - InpTrailStart * _Point * 10;
                    if(newSL > sl) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
            else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            {
                if(openPrice - currentPrice > InpTrailStart * _Point * 10)
                {
                    double newSL = currentPrice + InpTrailStart * _Point * 10;
                    if(newSL < sl || sl == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}