導入:なぜこのロジックは「激動の2年」を生き抜いたのか

AI研究者の視点から見て、このロジックが直近2年間のインフレ局面や金利変動という極めてボラティリティの高い相場でPF 1.86という高水準な数値を叩き出した理由は、**「徹底した環境認識によるエントリー制限」**にあります。

多くのEAが陥る罠は、トレンド相場で逆張りを仕掛け、踏み上げられることです。しかし、本ロジックは以下の3重のフィルターによって、極めて期待値の高い局面のみを抽出しています。

  1. 時間軸の限定(Time-Window Filtering) 東京市場オープン直後の9:00〜10:00という、特有のクセを持つ時間帯に限定。ノイズの多い時間帯を排除し、短期的なリバウンドが発生しやすい時間枠に絞り込んでいます。
  2. ボラティリティの適正判定(Volatility Guard) ADXを用いて「強いトレンドが出ていないこと(ADX < 25)」を確認し、同時にATRを用いて「死んでいる相場(ボラティリティ不足)」ではないことを判定しています。これにより、トレンド追随相場での大損失を回避しつつ、効率的なリバウンドを狙う設計となっています。
  3. マルチタイムフレーム(MTF)による方向性合致 5分足のシグナルだけでなく、15分足のSMAをフィルターとして採用。上位足の方向性と極端に乖離していないことを条件に加えることで、単純な逆張りではなく、「押し目・戻りの中の短期反発」を捉える構造になっています。

結論として、このEAは「勝つこと」よりも「負ける局面を徹底的に避けること」に特化した、非常に冷静な設計と言えます。

公開:ベースとなったPythonコード

本ロジックの根幹となるPythonコードを公開します。データ分析およびバックテストに使用された実装です。

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

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

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

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

class MeanReversionTokyoStrategy(BaseStrategy):
    def __init__(self):
        # トレーリングストップを有効化し、利益を最大化させる設計
        super().__init__(
            name="MeanReversion_Tokyo_Quant_v2",
            default_tp_pips=30.0,
            default_sl_pips=15.0,
            enable_trailing_stop=True,
            trail_start_pips=10.0
        )
        # 5分足ベースのデイトレ・スキャルピング設定
        self.base_timeframe = "5m"
        # AI画像認識および分析用のマルチタイムフレーム定義
        self.vision_timeframes = ["5min", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        テクニカル指標を計算し、カラム名を固定して結合することでKeyErrorを防止。
        """
        # --- 1. ボラティリティ & トレンドフィルター ---
        # ADX: トレンドの強さを判定
        adx_df = df.ta.adx(high='High', low='Low', close='Close', length=14)
        df['ADX'] = adx_df.iloc[:, 0] # 1番目のカラム(ADX_14)をADXとして固定
        
        # ATR: ボラティリティの絶対値を判定
        df['ATR'] = df.ta.atr(length=14)
        
        # --- 2. 過剰伸長判定インジケーター ---
        # ボリンジャーバンド (20期間, 2標準偏差)
        bbands_df = df.ta.bbands(length=20, std=2.0)
        # カラム名が動的なため、接頭辞で抽出して固定名に割り当て
        df['BBL'] = bbands_df.filter(like='BBL').iloc[:, 0]
        df['BBM'] = bbands_df.filter(like='BBM').iloc[:, 0]
        df['BBU'] = bbands_df.filter(like='BBU').iloc[:, 0]
        
        # RSI (14期間)
        df['RSI'] = df.ta.rsi(length=14)
        
        # ストキャスティクス (14, 3, 3)
        stoch_df = df.ta.stoch(high='High', low='Low', close='Close', length=14, k=3, d=3)
        df['STOCHk'] = stoch_df.filter(like='STOCHk').iloc[:, 0]
        
        # --- 3. マルチタイムフレーム (MTF) 分析 ---
        # 未来のカンニングを防ぐため、resample直後に .shift(1) を適用
        # 15分足の終値を5分足にリインデックス
        df['close_15min'] = df['Close'].resample('15min').last().shift(1).reindex(df.index, method='ffill')
        # 1時間足の終値を5分足にリインデックス
        df['close_1h'] = df['Close'].resample('1h').last().shift(1).reindex(df.index, method='ffill')
        
        # 上位足の方向性判定
        df['sma_15min'] = df['close_15min'].rolling(window=12).mean() 
        
        return df

    def generate_signal(self, df):
        """
        固定化したカラム名に基づいたエントリーシグナルの生成。
        """
        if len(df) < 30:
            return None
            
        last = df.iloc[-1]
        
        # --- [フィルター1] 時間帯制限 (東京オープン 9:00〜10:00 JST) ---
        current_hour = df.index[-1].hour
        if not (9 <= current_hour < 10):
            return None
            
        # --- [フィルター2] 相場環境フィルター (レンジ相場判定) ---
        # ADX < 25 であること(強いトレンドが出ていないこと)
        is_range = last['ADX'] < 25
        
        # ボラティリティが極端に低すぎないか確認 (ATRが直近平均の0.5倍以上)
        atr_ma = df['ATR'].rolling(window=100).mean().iloc[-1]
        is_volatile_enough = last['ATR'] > (atr_ma * 0.5) if not pd.isna(atr_ma) else True
        
        if not (is_range and is_volatile_enough):
            return None

        # --- [フィルター3] MTFフィルター ---
        mtf_buy_ok = last['Close'] > (last['sma_15min'] * 0.998)
        mtf_sell_ok = last['Close'] < (last['sma_15min'] * 1.002)

        # --- [エントリーロジック] ミーン・リバージョン ---
        # 買い条件: BB下限突破 + RSI売られすぎ + Stoch売られすぎ + MTF
        buy_condition = (
            (last['Close'] < last['BBL']) and 
            (last['RSI'] < 30) and 
            (last['STOCHk'] < 20) and
            mtf_buy_ok
        )
        
        # 売り条件: BB上限突破 + RSI買われすぎ + Stoch買われすぎ + MTF
        sell_condition = (
            (last['Close'] > last['BBU']) and 
            (last['RSI'] > 70) and 
            (last['STOCHk'] > 80) and
            mtf_sell_ok
        )
        
        if buy_condition:
            return 'BUY'
        elif sell_condition:
            return 'SELL'
            
        return None

実装:MQL5コード (.mq5) への翻訳

上記のPythonロジックをMT5でそのまま動作するように翻訳したコードです。 時間設定(InpStartHour / InpEndHour)は、ご利用のブローカーのサーバー時間に合わせて調整してください。

//+------------------------------------------------------------------+
//|                                  MeanReversion_Tokyo_Quant_v2.mq5|
//|                                                                  |
//+------------------------------------------------------------------+
#property copyright "AI Quant Researcher"
#property link      "https://yourblog.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input Parameters
input int      InpStartHour = 2;         // Entry Start Hour (Broker Server Time)
input int      InpEndHour   = 3;         // Entry End Hour (Broker Server Time)
input double   InpTP_Pips   = 30.0;      // Take Profit in Pips
input double   InpSL_Pips   = 15.0;      // Stop Loss in Pips
input double   InpTrailStart = 10.0;     // Trailing Stop Start in Pips
input double   InpLotSize   = 0.1;       // Trade Lot Size

//--- Indicator Handles
int handleADX, handleATR, handleBB, handleRSI, handleStoch, handleSMA15;

CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // ハンドルの初期化
    handleADX    = iADX(_Symbol, PERIOD_CURRENT, 14);
    handleATR    = iATR(_Symbol, PERIOD_CURRENT, 14);
    handleBB     = iBands(_Symbol, PERIOD_CURRENT, 20, 0, 2.0, PRICE_CLOSE);
    handleRSI    = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
    handleStoch  = iStochastic(_Symbol, PERIOD_CURRENT, 14, 3, 3, MODE_SMA, STO_LOWHIGH);
    handleSMA15  = iMA(_Symbol, PERIOD_M15, 12, 0, MODE_SMA, PRICE_CLOSE);

    if(handleADX == INVALID_HANDLE || handleBB == INVALID_HANDLE || handleSMA15 == INVALID_HANDLE) {
        Print("Indicator initialization failed");
        return(INIT_FAILED);
    }
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 1. 時間帯制限
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    if(!(dt.hour >= InpStartHour && dt.hour < InpEndHour)) return;

    // 2. データ取得
    double adx[], atr[], bbUpper[], bbLower[], rsi[], stochK[], sma15[];
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(atr, true);
    ArraySetAsSeries(bbUpper, true);
    ArraySetAsSeries(bbLower, true);
    ArraySetAsSeries(rsi, true);
    ArraySetAsSeries(stochK, true);
    ArraySetAsSeries(sma15, true);

    if(CopyBuffer(handleADX, 0, 0, 2, adx) < 2) return;
    if(CopyBuffer(handleATR, 0, 0, 101, atr) < 101) return;
    if(CopyBuffer(handleBB, 1, 0, 1, bbUpper) < 1) return;
    if(CopyBuffer(handleBB, 2, 0, 1, bbLower) < 1) return;
    if(CopyBuffer(handleRSI, 0, 0, 1, rsi) < 1) return;
    if(CopyBuffer(handleStoch, 0, 0, 1, stochK) < 1) return;
    if(CopyBuffer(handleSMA15, 0, 0, 1, sma15) < 1) return;

    double close = iClose(_Symbol, PERIOD_CURRENT, 0);

    // 3. 相場環境フィルター
    bool isRange = adx[0] < 25.0;
    
    double sumATR = 0;
    for(int i=0; i<100; i++) sumATR += atr[i];
    double atrMA = sumATR / 100.0;
    bool isVolatileEnough = (atr[0] > atrMA * 0.5);

    if(!isRange || !isVolatileEnough) return;

    // 4. MTFフィルター
    bool mtfBuyOk = close > (sma15[0] * 0.998);
    bool mtfSellOk = close < (sma15[0] * 1.002);

    // 5. エントリーロジック
    if(PositionsTotal() == 0)
    {
        // BUY Condition
        if(close < bbLower[0] && rsi[0] < 30 && stochK[0] < 20 && mtfBuyOk)
        {
            double sl = close - InpSL_Pips * _Point * 10;
            double tp = close + InpTP_Pips * _Point * 10;
            trade.Buy(InpLotSize, _Symbol, close, sl, tp, "TokyoMeanRev_Buy");
        }
        // SELL Condition
        else if(close > bbUpper[0] && rsi[0] > 70 && stochK[0] > 80 && mtfSellOk)
        {
            double sl = close + InpSL_Pips * _Point * 10;
            double tp = close - InpTP_Pips * _Point * 10;
            trade.Sell(InpLotSize, _Symbol, close, sl, tp, "TokyoMeanRev_Sell");
        }
    }
    
    // 6. トレーリングストップ
    ManageTrailingStop();
}

//+------------------------------------------------------------------+
//| Trailing Stop Management                                         |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
    for(int i=PositionsTotal()-1; i>=0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket))
        {
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            {
                double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
                double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
                if(price - openPrice > InpTrailStart * _Point * 10)
                {
                    double newSL = price - InpSL_Pips * _Point * 10;
                    if(newSL > PositionGetDouble(POSITION_SL))
                        trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
            else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            {
                double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
                double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
                if(openPrice - price > InpTrailStart * _Point * 10)
                {
                    double newSL = price + InpSL_Pips * _Point * 10;
                    if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)
                        trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}