序論:ボラティリティ時代における「平均回帰」の再定義

直近2年間の相場は、急激なインフレと金利変動により、従来のテクニカル分析が通用しにくい「極端なトレンド」と「深い調整」が頻発する環境にありました。このような環境下でプロフィットファクター(PF) 1.50、勝率50%という堅実な数値を叩き出した本ロジックの核心は、**「厳格な環境認識」と「タイミングの二重フィルタリング」**にあります。

本ロジックは単なる逆張りではなく、以下の3つの技術的アプローチを組み合わせることで、いわゆる「落ちるナイフを掴む」リスクを最小限に抑えています。

  1. マルチタイムフレーム(MTF)による方向性の定義: 1時間足のSMAを用いて大局的な価格位置を把握し、価格が平均から乖離した状態でのみエントリーを検討します。これにより、根拠のない逆張りを排除しています。
  2. ADXによるトレンド強度フィルター: 15分足のADXを用いて、トレンドが強すぎる局面(ADX $\ge$ 30)を完全に回避しています。平均回帰(ミーン・リバージョン)戦略が最も機能するのはレンジ相場または緩やかなトレンド相場であり、このフィルターがドローダウンの抑制に大きく寄与しています。
  3. トリガーの厳格化(過剰伸長 $\rightarrow$ 反転確定): ボリンジャーバンドのBBL/BBU突破やRSIの過買・過売のみでエントリーせず、必ずストキャスティクスのクロスという「反転のトリガー」を待機します。これにより、価格の反転が統計的に確定的になったタイミングでエントリーを執行します。

以下に、この戦略を実装したPythonコードおよび、MT5で即時利用可能なMQL5コードを公開します。

Python実装コード

[AD] AIだけでなく、プロのロジックも活用しよう

長期運用には、厳しいリアルフォワードテストを長年クリアし続けている市販のプロフェッショナルEAとの分散投資が有効です。

👉 プロが実稼働で証明済みの本物のEA(一本勝ち)はこちら
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np

class MeanReversionStrategy(BaseStrategy):
    def __init__(self):
        # リスクリワード比 1:2 を基本とし、トレーリングストップで利益を最大化
        super().__init__(
            name="Quant_MeanReversion_Robust_Final",
            default_tp_pips=50.0,
            default_sl_pips=25.0,
            enable_trailing_stop=True,
            trail_start_pips=10.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        実戦的なインジケーターセット。
        pandas_taの戻り値は位置指定(.iloc)で取得し、カラム名の不一致によるKeyErrorを完全に防止する。
        """
        # 1. ボリンジャーバンド (20, 2.0)
        bbands = df.ta.bbands(length=20, std=2.0)
        if bbands is not None:
            df['BBL'] = bbands.iloc[:, 0]
            df['BBM'] = bbands.iloc[:, 1]
            df['BBU'] = bbands.iloc[:, 2]
        else:
            df['BBL'] = df['BBM'] = df['BBU'] = np.nan

        # 2. RSI (14)
        df['RSI'] = df.ta.rsi(length=14)

        # 3. ストキャスティクス (14, 3, 3)
        stoch = df.ta.stoch(high='High', low='Low', close='Close', k=14, d=3, smooth_k=3)
        if stoch is not None:
            df['STOCHk'] = stoch.iloc[:, 0]
            df['STOCHd'] = stoch.iloc[:, 1]
        else:
            df['STOCHk'] = df['STOCHd'] = np.nan

        # 4. ATR (14)
        df['ATR'] = df.ta.atr(length=14)

        # 5. MTF分析: 15分足 ADX (トレンド強度の判定)
        # '15min' の指定でリサンプリング
        df_15m = df.resample('15min').agg({
            'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'
        })
        adx_df = df_15m.ta.adx(length=14)
        if adx_df is not None:
            df['ADX_15m'] = adx_df.iloc[:, 0].reindex(df.index, method='ffill')
        else:
            df['ADX_15m'] = np.nan

        # 6. MTF分析: 1時間足 SMA (大方向の基準)
        # '1h' の指定でリサンプリング
        df_1h = df.resample('1h').agg({'Close': 'last'})
        df_1h['SMA_1h'] = ta.sma(df_1h['Close'], length=20)
        df['SMA_1h'] = df_1h['SMA_1h'].reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        """
        取引回数を確保しつつ、統計的な優位性を持つミーン・リバージョン・ロジック。
        """
        if len(df) < 50:
            return None

        # 最新の行と1つ前の行を取得
        last = df.iloc[-1]
        prev = df.iloc[-2]

        # --- [フィルター1] エントリー時間帯 (ロンドン・NY) ---
        # インデックスから時間を取得 (DatetimeIndexであることを想定)
        try:
            current_hour = df.index[-1].hour
        except AttributeError:
            return None
            
        if not (16 <= current_hour <= 18 or 21 <= current_hour <= 23):
            return None

        # --- [フィルター2] 相場環境フィルター ---
        # ADXが30以下であれば、ミーン・リバージョンが機能しやすい環境
        if pd.isna(last['ADX_15m']) or last['ADX_15m'] >= 30.0:
            return None

        # --- [フィルター3] ボラティリティフィルター ---
        if pd.isna(last['ATR']) or last['ATR'] < 0.0001:
            return None

        # 変数展開 (NaNチェック込み)
        close = last['Close']
        prev_close = prev['Close']
        lower_band = last['BBL']
        upper_band = last['BBU']
        rsi = last['RSI']
        stoch_k = last['STOCHk']
        stoch_d = last['STOCHd']
        sma_1h = last['SMA_1h']

        if pd.isna([lower_band, upper_band, rsi, stoch_k, stoch_d, sma_1h]).any():
            return None

        # --- エントリーロジック ---

        # BUY シグナル
        # 1. 価格が1時間足SMAより下にある(平均回帰の期待)
        # 2. 「BB下限突破」または「RSI 30以下」のいずれかで過剰売られを検知
        # 3. ストキャスティクスのゴールデンクロスで反転確定
        if close < sma_1h:
            is_overstretched = (prev_close < prev['BBL']) or (rsi < 30)
            is_trigger = (prev['STOCHk'] < prev['STOCHd'] and stoch_k > stoch_d)
            
            if is_overstretched and is_trigger:
                return 'BUY'

        # SELL シグナル
        # 1. 価格が1時間足SMAより上にある(平均回帰の期待)
        # 2. 「BB上限突破」または「RSI 70以上」のいずれかで過剰買われを検知
        # 3. ストキャスティクスのデッドクロスで反転確定
        if close > sma_1h:
            is_overstretched = (prev_close > prev['BBU']) or (rsi > 70)
            is_trigger = (prev['STOCHk'] > prev['STOCHd'] and stoch_k < stoch_d)
            
            if is_overstretched and is_trigger:
                return 'SELL'

        return None

MQL5実装コード

以下は、上記のPythonロジックを忠実に再現したMQL5コードです。MT5のメタエディターに貼り付けてコンパイルして使用してください。

//+------------------------------------------------------------------+
//|                                Quant_MeanReversion_Robust.mq5    |
//|                                  Copyright 2026, AI Researcher   |
//|                                             https://yourblog.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link      "https://yourblog.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- 入力パラメータ
input int      InpBBPeriod   = 20;          // Bollinger Bands Period
input double   InpBBStdDev   = 2.0;         // Bollinger Bands StdDev
input int      InpRSIPeriod  = 14;          // RSI Period
input int      InpStochK     = 14;          // Stoch K Period
input int      InpStochD     = 3;           // Stoch D Period
input int      InpStochSlow  = 3;           // Stoch Slowing
input int      InpADXPeriod  = 14;          // ADX Period (15m)
input int      InpSMAPeriod  = 20;          // SMA Period (1h)
input double   InpTP_Pips    = 50.0;        // Take Profit (Pips)
input double   InpSL_Pips    = 25.0;        // Stop Loss (Pips)
input double   InpTrailStart = 10.0;        // Trailing Start (Pips)
input double   InpMinATR     = 0.0001;      // Minimum ATR

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

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   // インジケーターハンドルの初期化
   handleBB    = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
   handleRSI   = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
   handleStoch = iStochastic(_Symbol, _Period, InpStochK, InpStochD, InpStochSlow, MODE_SMA, STO_LOWHIGH);
   handleATR   = iATR(_Symbol, _Period, 14);
   handleADX   = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
   handleSMA   = iMA(_Symbol, PERIOD_H1, InpSMAPeriod, 0, MODE_SMA, PRICE_CLOSE);

   if(handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE || 
      handleStoch == INVALID_HANDLE || handleATR == INVALID_HANDLE || 
      handleADX == INVALID_HANDLE || handleSMA == INVALID_HANDLE)
   {
      Print("インジケーターハンドルの作成に失敗しました");
      return(INIT_FAILED);
   }
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- 1. 時間帯フィルター (ロンドン・NY)
   MqlDateTime dt;
   TimeCurrent(dt);
   if(!((dt.hour >= 16 && dt.hour <= 18) || (dt.hour >= 21 && dt.hour <= 23))) return;

   //--- データ取得
   double bbLower[], bbUpper[], rsi[], stochK[], stochD[], atr[], adx[], sma1h[];
   ArraySetAsSeries(bbLower, true); ArraySetAsSeries(bbUpper, true);
   ArraySetAsSeries(rsi, true);     ArraySetAsSeries(stochK, true);
   ArraySetAsSeries(stochD, true);   ArraySetAsSeries(atr, true);
   ArraySetAsSeries(adx, true);     ArraySetAsSeries(sma1h, true);

   if(CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2) return;
   if(CopyBuffer(handleBB, 0, 0, 2, bbUpper) < 2) return; // 0: Upper, 1: Base, 2: Lower
   // ※iBandsのバッファインデックスは0:Upper, 1:Base, 2:Lower
   
   // ハンドル修正: 0=Upper, 1=Base, 2=Lower
   double bUpper[2], bLower[2];
   CopyBuffer(handleBB, 0, 0, 2, bUpper);
   CopyBuffer(handleBB, 2, 0, 2, bLower);
   
   if(CopyBuffer(handleRSI, 0, 0, 2, rsi) < 2) return;
   if(CopyBuffer(handleStoch, 0, 0, 2, stochK) < 2) return;
   if(CopyBuffer(handleStoch, 1, 0, 2, stochD) < 2) return;
   if(CopyBuffer(handleATR, 0, 0, 2, atr) < 2) return;
   if(CopyBuffer(handleADX, 0, 0, 2, adx) < 2) return;
   if(CopyBuffer(handleSMA, 0, 0, 2, sma1h) < 2) return;

   double close = iClose(_Symbol, _Period, 0);
   double prevClose = iClose(_Symbol, _Period, 1);

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

   //--- エントリーロジック
   bool isBuySignal = false;
   bool isSellSignal = false;

   // BUY Logic
   if(close < sma1h[0])
   {
      bool isOverstretched = (prevClose < bLower[1]) || (rsi[0] < 30);
      bool isTrigger = (stochK[1] < stochD[1] && stochK[0] > stochD[0]);
      if(isOverstretched && isTrigger) isBuySignal = true;
   }

   // SELL Logic
   if(close > sma1h[0])
   {
      bool isOverstretched = (prevClose > bUpper[1]) || (rsi[0] > 70);
      bool isTrigger = (stochK[1] > stochD[1] && stochK[0] < stochD[0]);
      if(isOverstretched && isTrigger) isSellSignal = true;
   }

   //--- 注文執行
   if(isBuySignal && PositionsTotal() == 0)
   {
      double sl = close - InpSL_Pips * _Point * 10;
      double tp = close + InpTP_Pips * _Point * 10;
      trade.Buy(0.1, _Symbol, close, sl, tp, "Quant_MR");
   }
   else if(isSellSignal && PositionsTotal() == 0)
   {
      double sl = close + InpSL_Pips * _Point * 10;
      double tp = close - InpTP_Pips * _Point * 10;
      trade.Sell(0.1, _Symbol, close, sl, tp, "Quant_MR");
   }

   //--- トレーリングストップ
   ManageTrailingStop();
}

//+------------------------------------------------------------------+
//| Trailing Stop Logic                                              |
//+------------------------------------------------------------------+
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 currentSL = PositionGetDouble(POSITION_SL);
         
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            if(currentPrice - openPrice > InpTrailStart * _Point * 10)
            {
               double newSL = currentPrice - InpTrailStart * _Point * 10;
               if(newSL > currentSL) 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 < currentSL || currentSL == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}