💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
今回は、8年半のバックテストを破綻せずに生き抜いたロジックをご紹介します。 資産曲線は緩やかで、成績は伸び悩みました。 しかし、最大ドローダウンを極限まで抑えた点に価値があります。 未完成の原石として、研究素材にご活用ください。

バックテスト資産推移

これが過酷な全期間8年半の市場変動を破綻せず生き抜いた資産推移グラフ(長期検証証拠)です。

項目 実績値
プロフィットファクター (PF) 1.20
勝率 44.0%
総取引数 50 回
純利益 (0.1ロット/1万通貨時) ¥10977.34
最大ドローダウン 1.91%

今回の検証結果の「限界」と「ダメ出し」

このロジックの最大の弱点は、取引回数の少なさです。 8年半で50回という数字は、実戦では機会損失が大きすぎます。 PF 1.20という数値も、運用効率としては低いです。

要因は、フィルターを掛けすぎたことにあります。 ADXによるレンジ判定と時間帯制限が、多くのチャンスを排除しました。 結果として、破綻は免れたものの、利益を伸ばす力に欠けています。 堅実さを追求しすぎて、収益性が犠牲になった形です。

🤖
守備力は100点ですが、攻撃力は10点といったところですね。

ロジックの技術的詳細

本ロジックは、ボラティリティが安定したレンジ相場での「平均回帰」を狙います。 以下の条件をすべて満たしたときにエントリーします。

項目 条件・設定内容
時間帯制限 UTC 0時から9時までのみ動作
環境認識(レンジ) 5分足ADX < 20 かつ 1時間足ADX < 25
ボラティリティ制限 BB Widthが移動平均の1.1倍未満であること
買い条件 終値 $\le$ BB下限 かつ StochRSIが20%以下でGC かつ 陽線/下ヒゲ
売り条件 終値 $\ge$ BB上限 かつ StochRSIが80%以上でDC かつ 陰線/上ヒゲ
決済ルール TP 30pips / SL 30pips / トレーリングストップ有効

どう改善すべきか(次なる展望)

このコードをベースに改造するなら、フィルターの緩和を推奨します。 例えば、1時間足のADX制限を少し緩めるだけでも、取引数は増えるはずです。 また、固定のTP/SLではなく、ATRに基づいた可変幅の導入も有効です。

プロのEAは、ここに「通貨強弱」や「上位足のレジサポライン」という独自の環境認識を加えています。 単純な指標の組み合わせに、価格帯の概念を足すことで化ける可能性があります。

まずはこのコードをMT5に移植し、ご自身の分析手法を組み合わせてみてください。

Pythonソースコード

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

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

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

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

class EquilibriumMeanReversion(BaseStrategy):
    def __init__(self):
        # 【クオンツ調剤】PF(プロフィットファクター)とリカバリーファクターの最大化
        # 平均回帰戦略の特性上, 勝率を高めつつ, 最大DDを抑制するためにTP/SLを最適化
        # TPを30pipsに設定し, 期待値の高い「中心線への回帰」を確実に獲る設計に変更
        super().__init__(
            name="Equilibrium Mean Reversion Optimized", 
            default_tp_pips=30.0, 
            default_sl_pips=30.0, 
            enable_trailing_stop=True, 
            trail_start_pips=10.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        # --- 5分足インジケーター ---
        # ADX: トレンド強度判定
        df.ta.adx(length=14, append=True)
        
        # Bollinger Bands: 境界判定
        # 【重要】KeyError 'BBU_20_2.0' を完全に排除するため, 動的カラム抽出を採用
        bb = df.ta.bbands(length=20, std=2.0)
        # pandas_taの命名規則に関わらず, BBU/BBM/BBLを含むカラムを正確に抽出してリネーム
        df['BBU'] = bb.filter(like='BBU').iloc[:, 0]
        df['BBM'] = bb.filter(like='BBM').iloc[:, 0]
        df['BBL'] = bb.filter(like='BBL').iloc[:, 0]
        
        # StochRSI: モメンタム反転判定
        df.ta.stochrsi(length=14, rsi_length=14, k=3, d=3, append=True)
        
        # ATR: ボラティリティ指標 (Volume不使用ルール準拠)
        df.ta.atr(length=14, append=True)

        # BB Width 計算: ボラティリティの収縮・拡大を定量化
        df['bb_width'] = (df['BBU'] - df['BBL']) / df['BBM']
        df['bb_width_ma'] = df['bb_width'].rolling(window=20).mean()

        # --- MTF実装 (1h ADX) ---
        # ルックアヘッドバイアス排除のため, リサンプル後にshift(1)を適用
        df_h1_ohlc = df[['Open', 'High', 'Low', 'Close']].resample('1h').agg({
            'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'
        })
        h1_adx_res = ta.adx(df_h1_ohlc['High'], df_h1_ohlc['Low'], df_h1_ohlc['Close'], length=14)
        if h1_adx_res is not None:
            # 確定した1時間足のADXのみを使用
            h1_adx_series = h1_adx_res['ADX_14']
            df['h1_adx'] = h1_adx_series.shift(1).reindex(df.index, method='ffill')
        else:
            df['h1_adx'] = np.nan

        return df

    def generate_signal(self, df):
        # インジケーター計算期間を考慮し, 十分なデータがあるか確認
        if len(df) < 20:
            return None
            
        curr = df.iloc[-1]
        prev = df.iloc[-2]
        
        # --- A. 相場環境フィルター (The Guard) ---
        # 時間帯フィルター: ボラティリティが安定し, レンジを形成しやすい時間帯 (UTC 0-9)
        hour = curr.name.hour if hasattr(curr.name, 'hour') else pd.Timestamp(curr.name).hour
        if not (0 <= hour <= 9):
            return None

        # 【クオンツ調整】レンジ判定の厳格化
        # 5分足ADX < 20 かつ 1時間足ADX < 25 の時のみエントリーを許可し, トレンド相場でのDDを防止
        is_range = (curr['ADX_14'] < 20) and (curr['h1_adx'] < 25)
        if not is_range:
            return None
            
        # BB Width安定判定: 急激なボラティリティ拡大(スクイーズからのブレイク)を回避
        is_stable = curr['bb_width'] < (curr['bb_width_ma'] * 1.1)
        if not is_stable:
            return None

        # --- B. エントリー・ロジック (The Trigger) ---
        # pandas_ta StochRSI カラム名の動的取得
        k_col = 'STOCHRSIk_14_14_3_3'
        d_col = 'STOCHRSId_14_14_3_3'
        
        if k_col not in curr:
            return None

        k = curr[k_col]
        d = curr[d_col]
        prev_k = prev[k_col]
        prev_d = prev[d_col]
        
        # Long Entry: 
        # 1. 終値がBB下限(BBL)以下で反発の兆候
        # 2. StochRSIが20%以下の過売域でゴールデンクロス
        # 3. 陽線確定 または 下ヒゲ発生
        is_bullish = (curr['Close'] > curr['Open']) or (curr['Low'] < min(curr['Open'], curr['Close']))
        if (curr['Close'] <= curr['BBL']) and \
           (k < 20) and (prev_k <= prev_d) and (k > d) and \
           is_bullish:
            return 'BUY'

        # Short Entry:
        # 1. 終値がBB上限(BBU)以上で反落の兆候
        # 2. StochRSIが80%以上の過買域でデッドクロス
        # 3. 陰線確定 または 上ヒゲ発生
        is_bearish = (curr['Close'] < curr['Open']) or (curr['High'] > max(curr['Open'], curr['Close']))
        if (curr['Close'] >= curr['BBU']) and \
           (k > 80) and (prev_k >= prev_d) and (k < d) and \
           is_bearish:
            return 'SELL'

        return None

MQL5コード(MT5用)

//+------------------------------------------------------------------+
//|                                  EquilibriumMeanReversion.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      InpADXPeriod    = 14;          // ADX Period
input int      InpBBPeriod     = 20;          // Bollinger Bands Period
input double   InpBBStdDev     = 2.0;         // Bollinger Bands StdDev
input int      InpStochRSIPeriod = 14;        // StochRSI Period
input double   InpTP           = 300;         // Take Profit (points)
input double   InpSL           = 300;         // Stop Loss (points)
input double   InpTrailStart   = 100;         // Trailing Start (points)

//--- Global Variables
int      handleADX5m, handleADX1h, handleBB, handleRSI;
CTrade   trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleADX5m = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    handleADX1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
    handleBB    = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
    handleRSI   = iRSI(_Symbol, PERIOD_M5, InpStochRSIPeriod, PRICE_CLOSE);
    
    if(handleADX5m == INVALID_HANDLE || handleADX1h == INVALID_HANDLE || 
       handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE)
        return(INIT_FAILED);
        
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // Time Filter (UTC 0-9)
    MqlDateTime dt;
    TimeCurrent(dt);
    if(dt.hour < 0 || dt.hour > 9) return;

    // Indicator Buffers
    double adx5[], adx1h[], bbUpper[], bbLower[], bbMid[], rsi[];
    ArraySetAsSeries(adx5, true);
    ArraySetAsSeries(adx1h, true);
    ArraySetAsSeries(bbUpper, true);
    ArraySetAsSeries(bbLower, true);
    ArraySetAsSeries(bbMid, true);
    ArraySetAsSeries(rsi, true);

    if(CopyBuffer(handleADX5m, 0, 0, 2, adx5) < 2) return;
    if(CopyBuffer(handleADX1h, 0, 0, 2, adx1h) < 2) return;
    if(CopyBuffer(handleBB, 1, 0, 2, bbUpper) < 2) return;
    if(CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2) return;
    if(CopyBuffer(handleBB, 0, 0, 2, bbMid) < 2) return;
    if(CopyBuffer(handleRSI, 0, 0, 20, rsi) < 20) return;

    // StochRSI Calculation (Simplified)
    double stochK = calculateStochK(rsi, 0);
    double prevStochK = calculateStochK(rsi, 1);
    double stochD = (stochK + prevStochK) / 2.0; // Simple D

    // Range Filter
    if(adx5[0] >= 20 || adx1h[0] >= 25) return;

    // BB Width Stability
    double bbWidth = (bbUpper[0] - bbLower[0]) / bbMid[0];
    // Note: For simplicity, BB Width MA is omitted in this translation, 
    // but could be added with a simple rolling buffer.

    MqlRates rates[];
    ArraySetAsSeries(rates, true);
    if(CopyRates(_Symbol, PERIOD_M5, 0, 2, rates) < 2) return;

    // Entry Logic
    bool isBullish = (rates[0].close > rates[0].open) || (rates[0].low < MathMin(rates[0].open, rates[0].close));
    bool isBearish = (rates[0].close < rates[0].open) || (rates[0].high > MathMax(rates[0].open, rates[0].close));

    if(rates[0].close <= bbLower[0] && stochK < 20 && prevStochK <= stochD && stochK > stochD && isBullish)
    {
        if(!PositionSelect(_Symbol))
            trade.Buy(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), 
                      SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpSL * _Point, 
                      SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTP * _Point);
    }
    else if(rates[0].close >= bbUpper[0] && stochK > 80 && prevStochK >= stochD && stochK < stochD && isBearish)
    {
        if(!PositionSelect(_Symbol))
            trade.Sell(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID), 
                       SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpSL * _Point, 
                       SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTP * _Point);
    }
    
    // Trailing Stop
    manageTrailingStop();
}

double calculateStochK(double &rsi_buf[], int shift)
{
    double min_rsi = rsi_buf[shift];
    double max_rsi = rsi_buf[shift];
    for(int i=shift; i<shift+14; i++)
    {
        if(rsi_buf[i] < min_rsi) min_rsi = rsi_buf[i];
        if(rsi_buf[i] > max_rsi) max_rsi = rsi_buf[i];
    }
    if(max_rsi == min_rsi) return 50.0;
    return ((rsi_buf[shift] - min_rsi) / (max_rsi - min_rsi)) * 100.0;
}

void manageTrailingStop()
{
    if(!PositionSelect(_Symbol)) return;
    double price = PositionGetDouble(POSITION_PRICE_OPEN);
    double current_sl = PositionGetDouble(POSITION_SL);
    double current_price = PositionGetDouble(POSITION_PRICE_CURRENT);
    
    if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
    {
        if(current_price - price > InpTrailStart * _Point)
        {
            double new_sl = current_price - InpTrailStart * _Point;
            if(new_sl > current_sl) trade.PositionModify(_Symbol, new_sl, PositionGetDouble(POSITION_TP));
        }
    }
    else
    {
        if(price - current_price > InpTrailStart * _Point)
        {
            double new_sl = current_price + InpTrailStart * _Point;
            if(new_sl < current_sl || current_sl == 0) trade.PositionModify(_Symbol, new_sl, PositionGetDouble(POSITION_TP));
        }
    }
}