💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
システムトレードの開発は困難な道のりです。多くの開発者が、短期間の好成績に惑わされて破綻を経験します。今回は、10年という全期間の市場ショックを生き抜いたロジックをご紹介します。成績こそ控えめですが、破綻せずに資産を守り抜いた強靭な設計が特徴です。

もともとは、逆張り手法における「落ちてくるナイフ」を掴む恐怖から開発を始めました。ボリンジャーバンドの端でエントリーしても、強いトレンドが出ると底なしに資金を減らします。このダマシを排除するため、時間軸を重ねた厳格なフィルターを構築することに心血を注ぎました。

このコードをベースにすれば、MT5のEA開発に費やす数百時間を節約できます。検証済みの堅牢な土台があることで、あなた独自の改良を加える作業に集中できるはずです。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。

項目 実績値
プロフィットファクター (PF) 1.26
勝率 38.5%
総取引数 26 回
純利益 (0.1ロット/1万通貨時) ¥4883.82
最大ドローダウン 1.06%
リカバリーファクター 0.46
期待利得 (Expected Payoff) 0.77

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

このロジックの最大の弱点は、取引回数の少なさです。10年間で26回という数字は、フィルターが厳しすぎると言わざるを得ません。機会損失が非常に大きく、資産の増加スピードは極めて緩やかです。

また、勝率が4割を切っている点も課題です。損小利大を徹底していますが、連敗時の心理的ストレスは避けられません。純粋な統計上の優位性はありますが、運用効率の面では不十分な結果となりました。

🤖
10年生き残ったのはすごいけれど、これだけ取引が少ないと、気が遠くなるね。

ロジックの技術的詳細

本ロジックは、3つの時間軸を組み合わせた「適応型平均回帰エンジン」です。

項目 設定内容 役割
上位足 (1時間足) ADX < 20 マクロなトレンドの不在を確認
中位足 (15分足) ADX < 15 / RSI < 25 or > 75 マイクロなレンジ判定と売買圏の特定
執行足 (5分足) BB (30, 2.5) への回帰 正確なエントリータイミングの計測
確定条件 ローソク足の方向性 (陽線/陰線) 逆行の勢いが止まったことを確認
リスク管理 TP 35pips / SL 18pips 低ドローダウンを維持する固定比率

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

このロジックを「化けさせる」には、環境認識の緩和が必要です。例えば、ADXの閾値を少し上げるだけで、取引回数を劇的に増やせます。もちろん、それにより最大ドローダウンが増えるリスクは伴います。

また、プロのEAはここに「ボラティリティ・フィルター」を導入しています。ATRなどで相場の変動幅を測定し、利確幅を動的に変更すれば、収益性はさらに向上するはずです。

このコードは完成品ではなく、堅牢な「骨組み」です。フィルターの数値を調整し、自分なりの最適解を見つけ出す研究素材として活用してください。

Pythonコードの公開

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

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

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

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

class AdaptiveMeanReversionEngine(BaseStrategy):
    """
    Quantitative Surgical Optimization: Version 4.0
    Focus: PF improvement (0.82 -> 1.20+) and MaxDD reduction (14.6% -> <10%)
    
    Key Adjustments:
    1. Regime Filter: Tightened ADX thresholds to eliminate 'quasi-trends' (False Signals).
    2. Signal Quality: Shifted BB period from 20 to 30 and StdDev to 2.5 to target extreme exhaustion.
    3. Momentum Filter: RSI boundaries shifted to 25/75 for higher probability reversal points.
    4. Execution Trigger: Added 'Candle Direction' confirmation (Price Action) to filter noise.
    5. RR Optimization: Adjusted TP/SL ratio to increase Payoff Ratio while capping MaxDD.
    """
    def __init__(self):
        super().__init__(
            name="Adaptive Mean-Reversion Engine V4",
            default_tp_pips=35.0,  # Slightly increased to improve Payoff Ratio
            default_sl_pips=18.0,  # Tightened to protect MaxDD and improve RF
            enable_trailing_stop=True,
            trail_start_pips=10.0  # Optimized for early profit locking
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        # Ensure float types for all price columns
        for col in ['Open', 'High', 'Low', 'Close']:
            df[col] = df[col].astype(float)

        # --- 1. HTF (1h) Indicators: Macro Regime Filter ---
        df_h1 = df[['Open', 'High', 'Low', 'Close']].resample('1h').last()
        adx_h1 = ta.adx(df_h1['High'], df_h1['Low'], df_h1['Close'], length=14)
        # Positional indexing to prevent KeyError
        h1_adx_series = adx_h1.iloc[:, 0].shift(1) 
        df['HTF_ADX'] = h1_adx_series.reindex(df.index, method='ffill')

        # --- 2. LTF (15min) Indicators: Setup Filter ---
        df_15m = df[['Open', 'High', 'Low', 'Close']].resample('15min').last()
        
        # ADX for LTF Range confirmation
        adx_15m = ta.adx(df_15m['High'], df_15m['Low'], df_15m['Close'], length=14)
        # BBands for LTF extremes (Period 30, Std 2.5 for higher quality filtered signals)
        bb_15m = ta.bbands(df_15m['Close'], length=30, std=2.5)
        # RSI for deeper exhaustion (14)
        rsi_15m = ta.rsi(df_15m['Close'], length=14)
        
        # Shift(1) and Reindex to avoid look-ahead bias
        df['LTF_ADX'] = adx_15m.iloc[:, 0].shift(1).reindex(df.index, method='ffill')
        df['LTF_BBL'] = bb_15m.iloc[:, 0].shift(1).reindex(df.index, method='ffill')
        df['LTF_BBU'] = bb_15m.iloc[:, 2].shift(1).reindex(df.index, method='ffill')
        df['LTF_RSI'] = rsi_15m.shift(1).reindex(df.index, method='ffill')

        # --- 3. Base (5m) Indicators: Precision Trigger ---
        # Adjusted to Period 30, Std 2.5 for consistency with LTF filter
        bb_5m = ta.bbands(df['Close'], length=30, std=2.5)
        df['BBL_5M'] = bb_5m.iloc[:, 0] 
        df['BBU_5M'] = bb_5m.iloc[:, 2] 
        
        return df

    def generate_signal(self, df):
        if len(df) < 60:
            return None

        # Analysis of current and previous candles
        curr = df.iloc[-1]
        prev = df.iloc[-2]

        # --- Quantitative Regime Filter (Surgical Adjustment) ---
        # Only trade in extremely flat markets.
        # HTF_ADX < 20: Macro non-trending
        # LTF_ADX < 15: Micro range-bound
        is_range_regime = (curr['HTF_ADX'] < 20) and (curr['LTF_ADX'] < 15)
        
        if not is_range_regime:
            return None

        # --- BUY Signal Logic ---
        # 1. LTF RSI is deeply oversold (< 25)
        # 2. Trigger: 5m Price closes back inside Lower BB (Re-entry)
        # 3. Confirmation: Current candle must be bullish (Close > Open) to filter falling knives
        if (curr['LTF_RSI'] < 25 and 
            prev['Close'] < prev['BBL_5M'] and 
            curr['Close'] > curr['BBL_5M'] and 
            curr['Close'] > curr['Open']):
            
            return 'BUY'

        # --- SELL Signal Logic ---
        # 1. LTF RSI is deeply overbought (> 75)
        # 2. Trigger: 5m Price closes back inside Upper BB (Re-entry)
        # 3. Confirmation: Current candle must be bearish (Close < Open) to filter breakout spikes
        elif (curr['LTF_RSI'] > 75 and 
              prev['Close'] > prev['BBU_5M'] and 
              curr['Close'] < curr['BBU_5M'] and 
              curr['Close'] < curr['Open']):
            
            return 'SELL'

        return None

MQL5(MT5用)コードへの翻訳

//+------------------------------------------------------------------+
//|                                 AdaptiveMeanReversionEngine.mq5 |
//|                                  Copyright 2026, System Trader   |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, System Trader"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

// 入力パラメータ
input int      InpBBPeriod = 30;          // BB Period
input double   InpBBStdDev = 2.5;         // BB StdDev
input int      InpRSIPeriod = 14;         // RSI Period
input int      InpADXPeriod = 14;         // ADX Period
input double   InpTP_Pips = 35.0;         // Take Profit (Pips)
input double   InpSL_Pips = 18.0;         // Stop Loss (Pips)
input double   InpLotSize = 0.1;          // Lot Size

// ハンドル
int hADX_H1, hADX_M15, hRSI_M15, hBB_M5;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    hADX_H1  = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
    hADX_M15 = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
    hRSI_M15 = iRSI(_Symbol, PERIOD_M15, InpRSIPeriod, PRICE_CLOSE);
    hBB_M5   = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
    
    if(hADX_H1 == INVALID_HANDLE || hADX_M15 == INVALID_HANDLE || 
       hRSI_M15 == INVALID_HANDLE || hBB_M5 == INVALID_HANDLE) 
       return(INIT_FAILED);
       
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 新しい足の確定を待つ(5分足ベース)
    static datetime last_bar = 0;
    datetime current_bar = iTime(_Symbol, PERIOD_M5, 0);
    if(last_bar == current_bar) return;
    last_bar = current_bar;

    double adx_h1[], adx_m15[], rsi_m15[], bb_upper[], bb_lower[], bb_mid[];
    MqlRates rates_m5[];
    
    ArraySetAsSeries(adx_h1, true);
    ArraySetAsSeries(adx_m15, true);
    ArraySetAsSeries(rsi_m15, true);
    ArraySetAsSeries(bb_upper, true);
    ArraySetAsSeries(bb_lower, true);
    ArraySetAsSeries(rates_m5, true);

    if(CopyBuffer(hADX_H1, 0, 1, 1, adx_h1) < 1) return;
    if(CopyBuffer(hADX_M15, 0, 1, 1, adx_m15) < 1) return;
    if(CopyBuffer(hRSI_M15, 0, 1, 1, rsi_m15) < 1) return;
    if(CopyBuffer(hBB_M5, 1, 1, 2, bb_upper) < 2) return;
    if(CopyBuffer(hBB_M5, 2, 1, 2, bb_lower) < 2) return;
    if(CopyRates(_Symbol, PERIOD_M5, 1, 2, rates_m5) < 2) return;

    // レジームフィルター
    bool is_range = (adx_h1[0] < 20.0 && adx_m15[0] < 15.0);
    if(!is_range) return;

    double close_curr = rates_m5[0].close;
    double open_curr  = rates_m5[0].open;
    double close_prev = rates_m5[1].close;

    // BUY Signal
    if(rsi_m15[0] < 25.0 && close_prev < bb_lower[1] && 
       close_curr > bb_lower[0] && close_curr > open_curr)
    {
        ExecuteTrade(ORDER_TYPE_BUY);
    }
    // SELL Signal
    else if(rsi_m15[0] > 75.0 && close_prev > bb_upper[1] && 
             close_curr < bb_upper[0] && close_curr < open_curr)
    {
        ExecuteTrade(ORDER_TYPE_SELL);
    }
}

//+------------------------------------------------------------------+
//| Trade Execution                                                  |
//+------------------------------------------------------------------+
void ExecuteTrade(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_Pips * _Point * 10 : price + InpSL_Pips * _Point * 10;
    double tp = (type == ORDER_TYPE_BUY) ? price + InpTP_Pips * _Point * 10 : price - InpTP_Pips * _Point * 10;

    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);
}