直近2年間の激しい金利変動を乗り越えたロジックを公開します。AIが導き出した、極めて低リスクな設計です。

バックテスト資産推移

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

項目 実績値
プロフィットファクター (PF) 1.27
勝率 43.7%
総取引数 71回
純利益 $139.88
最大ドローダウン 1.26%

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

結論から述べます。相場環境の厳格なフィルタリングを徹底したからです。

理由は3点あります。1つ目は、上位足のADXで強いトレンドを排除したこと。2つ目は、RSIとストキャスティクスを組み合わせた高精度の反転判定。3つ目は、ボラティリティが安定する時間帯に限定して取引したことです。

具体例を挙げます。1時間足のADXが30を超えたときは、エントリーを完全に停止します。これにより、トレンドに逆らう危険な逆張り注文を回避しました。

結論として、取引回数を絞り、勝率よりも「負けない局面」を優先した設計が功を奏したといえます。

🤖
少ない取引数で最大DDを抑える。これがプロの設計思想です。

戦略の技術的詳細

このロジックは、平均回帰(ミーンリバージョン)をベースとしています。

項目 設定・条件
使用時間足 5分足(メイン)、1時間足(環境認識)
ボリンジャーバンド 期間20 / 偏差2.0
RSI 期間14 / 境界値 35以下(買)・65以上(売)
ストキャスティクス 14, 3, 3 / 25以下でGC(買)・75以上でDC(売)
ADXフィルター 1時間足 < 30 かつ 5分足 < 25
取引時間 日本時間 16:00 〜 21:00
リスク管理 TP 40pips / SL 20pips / トレーリングストップ有効
ここがポイント 単なるインジケーターの組み合わせではなく、時間軸を分けた「階層的なフィルタリング」を実装している点にあります。

公開Pythonコード

AIが生成した、バックテスト合格済みのソースコードです。

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

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

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

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

class MeanReversionStrategy(BaseStrategy):
    def __init__(self):
        # トレーリングストップを有効にし、利益を最大限に伸ばしつつリスクを限定
        super().__init__(
            name="Robust_MeanReversion_SweetSpot",
            default_tp_pips=40.0,
            default_sl_pips=20.0,
            enable_trailing_stop=True,
            trail_start_pips=10.0
        )
        # デイトレ・スキャルピング特化の時間足
        self.base_timeframe = "5m"
        # AI解析用時間足
        self.vision_timeframes = ["5m", "15min", "1h"]

    def _safe_extract(self, res, col_idx=0):
        """
        戻り値がDataFrameの場合は指定したインデックスの列を抽出し、
        Seriesの場合はそのまま返すヘルパー関数
        """
        if isinstance(res, pd.DataFrame):
            return res.iloc[:, col_idx]
        return res

    def calculate_indicators(self, df):
        """
        テクニカル指標の計算(型安全な抽出とMTF分析)
        """
        # --- Bollinger Bands (20, 2.0) ---
        bbands = df.ta.bbands(length=20, std=2.0)
        df['BBL'] = self._safe_extract(bbands, 0)
        df['BBM'] = self._safe_extract(bbands, 1)
        df['BBU'] = self._safe_extract(bbands, 2)

        # --- RSI (14) ---
        rsi_res = df.ta.rsi(length=14)
        df['RSI'] = self._safe_extract(rsi_res)

        # --- Stochastic (14, 3, 3) ---
        stoch_res = df.ta.stoch(high='High', low='Low', close='Close', length=14, k=3, d=3)
        df['STOCHk'] = self._safe_extract(stoch_res, 0)
        df['STOCHd'] = self._safe_extract(stoch_res, 1)

        # --- ADX (14) - 5min ---
        adx_res = df.ta.adx(length=14)
        df['ADX'] = self._safe_extract(adx_res, 0)

        # --- Multi-Timeframe Analysis (MTF) ---
        # ルックアヘッドバイアスを完全に排除するため .shift(1) を適用
        
        # 1時間足のADX: 大トレンドの有無を判定
        df_1h = df.resample('1h').agg({
            'High': 'max', 
            'Low': 'min', 
            'Close': 'last'
        }).shift(1)
        adx_1h_res = ta.adx(df_1h['High'], df_1h['Low'], df_1h['Close'], length=14)
        df['ADX_1h'] = self._safe_extract(adx_1h_res, 0).reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        """
        最新の行に基づいたエントリーシグナルの生成
        """
        if len(df) < 30:
            return None

        row = df.iloc[-1]
        
        # --- 時間帯フィルター (16:00 - 21:00 JST) ---
        # ボラティリティが高く、かつ反転が起きやすい時間帯に限定
        hour = row.name.hour if hasattr(row.name, 'hour') else df.index[-1].hour
        is_active_window = 16 <= hour < 21

        if not is_active_window:
            return None

        # --- 相場環境フィルター (階層的なレンジ判定) ---
        # 上位足(1h)で強すぎるトレンドが出ておらず、かつ下位足(5m)でレンジ状態であること
        is_range_market = (
            row['ADX_1h'] < 30 and 
            row['ADX'] < 25
        )

        if not is_range_market:
            return None

        # --- エントリーロジック (質重視のAND条件) ---
        
        # BUY Signal
        # 1. 価格がBB下限を下回っている AND RSIが35以下(統計的な売られすぎ)
        # 2. ストキャスティクスが「売られすぎ圏(20以下)」でゴールデンクロス(反転のトリガー)
        buy_condition = (
            (row['Close'] < row['BBL'] and row['RSI'] < 35) and 
            (row['STOCHk'] > row['STOCHd'] and row['STOCHk'] < 25)
        )

        # SELL Signal
        # 1. 価格がBB上限を上回っている AND RSIが65以上(統計的な買われすぎ)
        # 2. ストキャスティクスが「買われすぎ圏(80以上)」でデッドクロス(反転のトリガー)
        sell_condition = (
            (row['Close'] > row['BBU'] and row['RSI'] > 65) and 
            (row['STOCHk'] < row['STOCHd'] and row['STOCHk'] > 75)
        )

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

        return None

MT5用 MQL5コードへの翻訳

Pythonロジックを忠実に再現したMQL5コードです。そのままコンパイルして利用できます。

//+------------------------------------------------------------------+
//|                                  Robust_MeanReversion_SweetSpot.mq5|
//|                                  Copyright 2026, AI Researcher   |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link      ""
#property version   "1.00"
#property strict

// 入力パラメータ
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      InpStochSlowing = 3;         // Stochastic Slowing
input int      InpADXPeriod  = 14;          // ADX Period
input double   InpLotSize    = 0.1;         // Lot Size
input double   InpTP         = 400;         // Take Profit (points)
input double   InpSL         = 200;         // Stop Loss (points)
input double   InpTrailStart = 100;         // Trailing Stop Start (points)

// ハンドル
int handleBB, handleRSI, handleStoch, handleADX_5m, handleADX_1h;

//+------------------------------------------------------------------+
//| 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, InpStochSlowing, MODE_SMA, STO_LOWHIGH);
    handleADX_5m = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    handleADX_1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);

    if(handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE || 
       handleStoch == INVALID_HANDLE || handleADX_5m == INVALID_HANDLE || handleADX_1h == INVALID_HANDLE)
        return(INIT_FAILED);

    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 時間帯フィルター (16:00 - 21:00 JST)
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    if(dt.hour < 16 || dt.hour >= 21) return;

    // インジケーター値の取得
    double bbLower[], bbUpper[], rsi[], stochK[], stochD[], adx5[], adx1h[];
    ArraySetAsSeries(bbLower, true); ArraySetAsSeries(bbUpper, true);
    ArraySetAsSeries(rsi, true); ArraySetAsSeries(stochK, true);
    ArraySetAsSeries(stochD, true); ArraySetAsSeries(adx5, true);
    ArraySetAsSeries(adx1h, true);

    if(CopyBuffer(handleBB, 2, 0, 2, bbLower) <= 0) return;
    if(CopyBuffer(handleBB, 1, 0, 2, bbUpper) <= 0) return; // Simplified for BB
    // Note: iBands buffer 0 is Middle, 1 is Upper, 2 is Lower
    double upper = bbUpper[0];
    double lower = bbLower[0];
    
    // Correcting indices for BB
    CopyBuffer(handleBB, 1, 0, 2, bbUpper);
    CopyBuffer(handleBB, 2, 0, 2, bbLower);
    
    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_5m, 0, 0, 2, adx5) <= 0) return;
    if(CopyBuffer(handleADX_1h, 0, 0, 2, adx1h) <= 0) return;

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

    // 相場環境フィルター
    if(adx1h[0] >= 30 || adx5[0] >= 25) return;

    // ポジション確認
    if(PositionsTotal() > 0) 
    {
        ManageTrailingStop();
        return;
    }

    // エントリー条件
    bool buySignal = (close < bbLower[0] && rsi[0] < 35) && (stochK[0] > stochD[0] && stochK[0] < 25);
    bool sellSignal = (close > bbUpper[0] && rsi[0] > 65) && (stochK[0] < stochD[0] && stochK[0] > 75);

    if(buySignal) TradeOpen(ORDER_TYPE_BUY);
    else if(sellSignal) TradeOpen(ORDER_TYPE_SELL);
}

//+------------------------------------------------------------------+
//| Order Open Function                                              |
//+------------------------------------------------------------------+
void TradeOpen(ENUM_ORDER_TYPE type)
{
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    
    double price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
    double sl = (type == ORDER_TYPE_BUY) ? price - InpSL * _Point : price + InpSL * _Point;
    double tp = (type == ORDER_TYPE_BUY) ? price + InpTP * _Point : price - InpTP * _Point;

    request.action = TRADE_ACTION_DEAL;
    request.symbol = _Symbol;
    request.volume = InpLotSize;
    request.type = type;
    request.price = price;
    request.sl = sl;
    request.tp = tp;
    request.deviation = 10;
    request.magic = 123456;
    request.type_filling = ORDER_FILLING_IOC;

    OrderSend(request, result);
}

//+------------------------------------------------------------------+
//| Trailing Stop Management                                         |
//+------------------------------------------------------------------+
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 = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            double currentSL = PositionGetDouble(POSITION_SL);
            
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            {
                if(currentPrice - openPrice > InpTrailStart * _Point)
                {
                    double newSL = currentPrice - InpTrailStart * _Point;
                    if(newSL > currentSL) ModifySL(ticket, newSL);
                }
            }
            else
            {
                if(openPrice - currentPrice > InpTrailStart * _Point)
                {
                    double newSL = currentPrice + InpTrailStart * _Point;
                    if(newSL < currentSL || currentSL == 0) ModifySL(ticket, newSL);
                }
            }
        }
    }
}

void ModifySL(ulong ticket, double newSL)
{
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    request.action = TRADE_ACTION_SLTP;
    request.position = ticket;
    request.sl = newSL;
    OrderSend(request, result);
}