直近2年間の激しいインフレ相場を勝ち抜いた自動売買ロジックを公開します。 このEAはプロフィットファクター1.57を記録しました。 結論から述べると、厳格なボラティリティ判定とマルチタイムフレーム分析が勝利の要因です。

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

理由はこのロジックが「単なる逆張り」ではないからです。 多くの逆張りEAは強いトレンドに巻き込まれ、破綻します。 しかし、本ロジックは相場の強度をADXで判定します。 強いトレンド時はエントリー条件をさらに厳しくします。 これにより、大損失を避ける仕組みを構築しました。

具体的に導入しているフィルターは以下の通りです。

  • 時間帯制限: 流動性が高い日本時間21時から23時に限定。
  • ボラティリティ判定: ATRを用いて、動きがない相場を排除。
  • トレンド強度フィルター: ADXが50を超える局面では乖離率を重視。
  • マルチタイムフレーム: 15分足と1時間足で方向性を確認。
🤖
フィルターを盛りすぎると取引回数が減りますが、質は格段に上がります。

これらの条件が組み合わさり、高精度のエントリーを実現しました。 結果として、金利変動の激しい局面でも安定して利益を残せたのです。

本ロジックの強みまとめ

  1. 根拠のない逆張りを排除。
  2. ATRによる低ボラティリティ相場の回避。
  3. MTF分析による環境認識の徹底。
  4. トレーリングストップによる利益の最大化。

公開Pythonコード

以下がバックテストを合格した元となるPythonコードです。

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

今回の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="MeanReversionQuantStrategy_V3",
            default_tp_pips=80.0, 
            default_sl_pips=30.0,
            enable_trailing_stop=True,
            trail_start_pips=20.0
        )
        
        self.base_timeframe = "5min"
        self.vision_timeframes = ["5min", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        高速ベクトル演算による指標計算。
        """
        # --- 5min Indicators ---
        # Bollinger Bands (20, 2.0)
        bb = df.ta.bbands(length=20, std=2.0)
        df['BBL'] = bb.iloc[:, 0] # Lower Band
        df['BBU'] = bb.iloc[:, 2] # Upper Band
        
        # RSI (14)
        df['RSI'] = df.ta.rsi(length=14)
        
        # Stochastic (14, 3, 3)
        stoch = df.ta.stoch(k=14, d=3, smooth_k=3)
        df['STOCHk'] = stoch.iloc[:, 0]
        
        # ADX (14) - トレンド強度確認用
        adx_df = df.ta.adx(length=14)
        df['ADX'] = adx_df.iloc[:, 0]
        
        # ATR (14) - ボラティリティ確認用
        df['ATR'] = df.ta.atr(length=14)

        # --- Multi-Timeframe Analysis (MTF) ---
        # 未来のカンニングを防ぐため .shift(1) を適用
        
        # 15min: 価格の乖離率(移動平均からの距離)
        df_15m_close = df['Close'].resample('15min').last().shift(1)
        res_15m = pd.DataFrame({'Close': df_15m_close})
        res_15m['sma_15m'] = res_15m['Close'].rolling(window=20).mean()
        res_15m['dist_15m'] = (res_15m['Close'] - res_15m['sma_15m']) / res_15m['sma_15m'] * 100
        
        # 1h: 長期トレンド方向
        df_1h_close = df['Close'].resample('1h').last().shift(1)
        res_1h = pd.DataFrame({'Close': df_1h_close})
        res_1h['sma_1h'] = res_1h['Close'].rolling(window=20).mean()
        res_1h['trend_1h'] = res_1h['Close'] > res_1h['sma_1h']

        # 元のDataFrameに結合
        df = df.join(res_15m[['dist_15m']].reindex(df.index, method='ffill'))
        df = df.join(res_1h[['trend_1h']].reindex(df.index, method='ffill'))

        return df

    def generate_signal(self, df):
        """
        過剰伸展からの反発を狙う高効率シグナル。
        """
        if len(df) < 30:
            return None

        last = df.iloc[-1]
        prev = df.iloc[-2]
        
        # --- 1. エントリー時間帯の限定 (JST 21:00 - 23:00) ---
        current_hour = last.name.hour if hasattr(last.name, 'hour') else df.index[-1].hour
        if not (21 <= current_hour <= 23):
            return None

        # --- 2. 相場環境フィルター ---
        # 極端な低ボラティリティを除外
        avg_atr = df['ATR'].tail(100).mean()
        if last['ATR'] < (avg_atr * 0.5):
            return None
            
        # トレンドが強すぎる(ADX > 50)場合は、さらに深い乖離を待つためフィルターを調整
        is_hyper_trend = last['ADX'] > 50

        # --- 3. エントリーロジック (Aggressive Mean Reversion) ---
        
        # BUY Signal:
        # - 価格がBB下限を下回っている (必須)
        # - RSIが30以下、またはStochKが20以下 (いずれかで過剰売られを判定)
        # - 【トリガー】RSIが反転上昇し始めた (前回の値より高い)
        # - 15分足で価格が平均より下に位置している
        if (last['Close'] < last['BBL'] and 
            (last['RSI'] < 32 or last['STOCHk'] < 22) and 
            last['RSI'] > prev['RSI'] and 
            last['dist_15m'] < 0):
            
            # ハイパートレンド時はさらに厳しい条件(BB下限を大きく突き抜けていること)を追加
            if is_hyper_trend and last['Close'] > last['BBL'] * 0.998:
                return None
            return 'BUY'

        # SELL Signal:
        # - 価格がBB上限を上回っている (必須)
        # - RSIが70以上、またはStochKが80以上 (いずれかで過剰買われを判定)
        # - 【トリガー】RSIが反転下落し始めた (前回の値より低い)
        # - 15分足で価格が平均より上に位置している
        if (last['Close'] > last['BBU'] and 
            (last['RSI'] > 68 or last['STOCHk'] > 78) and 
            last['RSI'] < prev['RSI'] and 
            last['dist_15m'] > 0):
            
            # ハイパートレンド時はさらに厳しい条件(BB上限を大きく突き抜けていること)を追加
            if is_hyper_trend and last['Close'] < last['BBU'] * 1.002:
                return None
            return 'SELL'

        return None

MQL5コードへの翻訳

MT5でそのまま動作するように実装したコードです。 時間帯の設定はサーバー時間に合わせて調整してください。

//+------------------------------------------------------------------+
//|                                  MeanReversionQuantStrategy.mq5 |
//|                                  Copyright 2026, System Trader   |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, System Trader"
#property link      "https://your-blog-url.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input parameters
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 double   InpTP         = 800;         // Take Profit (Points)
input double   InpSL         = 300;         // Stop Loss (Points)
input double   InpTrailStart = 200;         // Trailing Stop Start (Points)
input int      InpStartHour  = 21;          // Entry Start Hour (JST approx)
input int      InpEndHour    = 23;          // Entry End Hour (JST approx)

//--- Indicator Handles
int hBB, hRSI, hStoch, hADX, hATR, hSMA15, hSMA1h;
CTrade trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    hBB    = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBDev, PRICE_CLOSE);
    hRSI   = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
    hStoch = iStochastic(_Symbol, PERIOD_M5, InpStochK, InpStochD, InpStochS, MODE_SMA, STO_LOWHIGH);
    hADX   = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    hATR   = iATR(_Symbol, PERIOD_M5, InpATRPeriod);
    hSMA15 = iMA(_Symbol, PERIOD_M15, 20, 0, MODE_SMA, PRICE_CLOSE);
    hSMA1h = iMA(_Symbol, PERIOD_H1, 20, 0, MODE_SMA, PRICE_CLOSE);
    
    if(hBB == INVALID_HANDLE || hRSI == INVALID_HANDLE || hADX == INVALID_HANDLE) return INIT_FAILED;
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    if(dt.hour < InpStartHour || dt.hour > InpEndHour) return;

    double bbLow[], bbUp[], rsi[], stochK[], adx[], atr[];
    double sma15[], sma1h[], closeM5[], close15[], close1h[];
    
    ArraySetAsSeries(bbLow, true); ArraySetAsSeries(bbUp, true);
    ArraySetAsSeries(rsi, true); ArraySetAsSeries(stochK, true);
    ArraySetAsSeries(adx, true); ArraySetAsSeries(atr, true);
    ArraySetAsSeries(sma15, true); ArraySetAsSeries(sma1h, true);
    ArraySetAsSeries(closeM5, true); ArraySetAsSeries(close15, true); ArraySetAsSeries(close1h, true);

    if(CopyBuffer(hBB, 2, 0, 3, bbLow) < 0) return;
    if(CopyBuffer(hBB, 1, 0, 3, bbUp) < 0) return;
    if(CopyBuffer(hRSI, 0, 0, 3, rsi) < 0) return;
    if(CopyBuffer(hStoch, 0, 0, 3, stochK) < 0) return;
    if(CopyBuffer(hADX, 0, 0, 3, adx) < 0) return;
    if(CopyBuffer(hATR, 0, 0, 100, atr) < 0) return;
    if(CopyBuffer(hSMA15, 0, 0, 3, sma15) < 0) return;
    if(CopyBuffer(hSMA1h, 0, 0, 3, sma1h) < 0) return;
    if(CopyClose(_Symbol, PERIOD_M5, 0, 3, closeM5) < 0) return;
    if(CopyClose(_Symbol, PERIOD_M15, 0, 3, close15) < 0) return;
    if(CopyClose(_Symbol, PERIOD_H1, 0, 3, close1h) < 0) return;

    double avgAtr = 0;
    for(int i=0; i<100; i++) avgAtr += atr[i];
    avgAtr /= 100;

    if(atr[0] < avgAtr * 0.5) return;

    bool isHyperTrend = (adx[0] > 50);
    double dist15 = (close15[0] - sma15[0]) / sma15[0] * 100;

    //--- Buy Logic
    if(closeM5[0] < bbLow[0] && (rsi[0] < 32 || stochK[0] < 22) && rsi[0] > rsi[1] && dist15 < 0)
    {
        if(!(isHyperTrend && closeM5[0] > bbLow[0] * 0.998))
        {
            if(PositionsTotal() == 0)
                trade.Buy(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), 
                          SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point, 
                          SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point);
        }
    }

    //--- Sell Logic
    if(closeM5[0] > bbUp[0] && (rsi[0] > 68 || stochK[0] > 78) && rsi[0] < rsi[1] && dist15 > 0)
    {
        if(!(isHyperTrend && closeM5[0] < bbUp[0] * 1.002))
        {
            if(PositionsTotal() == 0)
                trade.Sell(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID), 
                           SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point, 
                           SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point);
        }
    }

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