💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は困難な道です。多くのロジックが短期的な利益を上げても、数年で破綻します。今回は10年のバックテストを破綻せずに生き抜いたロジックを公開します。ただし、成績は決して派手ではありません。伸び悩んでいる点や、明確な弱点が見えたロジックであることを最初にお伝えします。

もともと、私はブレイクアウト手法に傾倒していました。しかし、レンジ相場での「だまし」に何度も資金を削られました。そこで、徹底的に「相場の端」だけを狙う逆張り手法を模索しました。インジケーターの組み合わせを数え切れないほど試し、ようやくたどり着いたのが今回のロジックです。

🤖
完璧な聖杯を探すのではなく、まずは「死なないこと」に特化させました。

このコードを土台にしてください。MT5開発にかかる数百時間を節約できます。自分だけのオリジナルEAを作る最高の素材になるはずです。

バックテスト分析 - equity

<CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.35
勝率 50.0%
総取引数 24 回
純利益 (0.1ロット/1万通貨時) ¥10004.46
最大ドローダウン 1.81%
リカバリーファクター 0.55
期待利得 (Expected Payoff) 0.68

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

この成績の最大の弱点は、取引回数の少なさです。10年で24回という数字は、実戦では機会損失が大きすぎます。フィルターを厳しくしすぎた結果、利益を伸ばすチャンスまで捨ててしまいました。

PF1.35という数字も、爆発力に欠けます。最大ドローダウンが低いのは、単にトレードしていない期間が長いからです。効率的に資産を増やす仕組みとしては、まだ不十分と言えます。

ロジックの技術的詳細

本ロジックは、相場の「過熱感」と「停滞感」を同時に判定する仕組みです。

要素 条件・指標 設定値・判定基準
環境認識(MTF) 上位足ADX (1h, 4h, 1d) すべて25以下(トレンド不在を重視)
環境認識(執行足) Choppiness Index & ADX CHOP > 50 かつ ADX < 20
ボラティリティ ATR / ATR_SMA ATR < ATR_SMA * 1.5 (安定した変動幅)
エントリートリガー BB & Keltner Channel 価格が両方のバンドの外側に位置すること
最終確認 CCI (24) +/- 200からの反転(極端な売られすぎ/買われすぎ)
ポイントは「BBとケルトナーチャネルの両方」を突き抜けた点です。これにより、単なるボラティリティ拡大によるブレイクアウトを排除しています。

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

ここから改造して、性能を上げる余地は十分にあります。例えば、ADXの閾値を少し緩めることで、取引回数を増やせるかもしれません。また、時間帯フィルターを追加し、流動性の低い時間帯を避ける構成も有効です。

プロのEAは、ここに独自の環境認識を組み合わせています。例えば、通貨強弱の分析や、重要指標の回避ロジックなどです。この無料コードをベースに、あなただけのフィルターを付け足してください。

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

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

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

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

class MeanReversionSynergy(BaseStrategy):
    """
    Mean Reversion Synergy (MRS) Strategy - High-Efficiency Surgical Version
    - Execution Timeframe: 15m
    - Objective: Elevate PF >= 1.20 and RF >= 3.00 by eliminating "False Signs".
    - Surgical Fixes:
        1. Signal Quality Hardening: Reverted from "OR" to "AND" logic for BB and KC. 
           Price must now be outside BOTH bands to confirm a true extreme overshoot.
        2. Over-trading Correction: Increased indicator periods from 20 to 24 to smooth noise 
           and reduced trade frequency from 1318 to a high-conviction subset.
        3. Momentum Precision: Raised CCI exhaustion thresholds to +/- 200, capturing only 
           the most acute price exhaustion points.
        4. Environment Shielding: Changed environment filter to "AND" logic (CHOP > 50 AND ADX < 20). 
           This strictly prohibits trading in anything other than a confirmed range.
        5. MTF Tightening: Lowered HTF ADX threshold to 25 to avoid "trend-traps" on higher timeframes.
        6. Expectancy Optimization: Adjusted TP to 70 and SL to 25 to improve the Payoff Ratio 
           and increase the Recovery Factor.
    """
    def __init__(self):
        # Quant Tuning: Optimized for high-quality swings.
        # TP=70 / SL=25 creates a strong mathematical edge (Payoff Ratio 2.8) 
        # to ensure a high PF even with a lower trade frequency.
        super().__init__(
            name="MeanReversionSynergy", 
            default_tp_pips=70.0, 
            default_sl_pips=25.0, 
            enable_trailing_stop=True, 
            trail_start_pips=20.0
        )
        self.base_timeframe = "15m"
        self.vision_timeframes = ["15m", "1h", "4h", "1d"]

    def calculate_indicators(self, df):
        # --- 1. Base Timeframe Indicators (15m) ---
        # Choppiness Index
        df['CHOP'] = ta.chop(df['High'], df['Low'], df['Close'])
        
        # ADX: Trend strength
        adx_df = ta.adx(df['High'], df['Low'], df['Close'])
        df['ADX'] = adx_df.iloc[:, 0] 
        
        # Volatility: ATR for stability
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'])
        df['ATR_SMA'] = df['ATR'].rolling(window=24).mean()

        # Entry Triggers: Increased period to 24 for superior noise filtering
        # Bollinger Bands (24, 2.0)
        bb = ta.bbands(df['Close'], length=24, std=2.0)
        df['BBL'] = bb.iloc[:, 0] 
        df['BBM'] = bb.iloc[:, 1] 
        df['BBU'] = bb.iloc[:, 2] 
        
        # Keltner Channel (24, 2.0)
        kc = ta.kc(df['High'], df['Low'], df['Close'], length=24, scalar=2.0)
        df['KCL'] = kc.iloc[:, 0] 
        df['KCU'] = kc.iloc[:, 2] 
        
        # CCI (24)
        df['CCI'] = ta.cci(df['High'], df['Low'], df['Close'], length=24)

        # --- 2. Multi-Timeframe (MTF) Alignment ---
        for tf in ["1h", "4h", "1d"]:
            resampled = df[['High', 'Low', 'Close']].resample(tf).agg({
                'High': 'max', 
                'Low': 'min', 
                'Close': 'last'
            })
            adx_tf_series = ta.adx(resampled['High'], resampled['Low'], resampled['Close'])
            adx_tf_shifted = adx_tf_series.iloc[:, 0].shift(1)
            df[f'ADX_{tf}'] = adx_tf_shifted.reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        # Minimum data requirement
        if len(df) < 30:
            return None

        curr = df.iloc[-1]
        prev = df.iloc[-2]

        # --- 1. MTF Environment Filter (The Alignment) ---
        # Tightened: ADX > 25 on any HTF = Trend danger. No mean reversion.
        mtf_trending = (curr['ADX_1h'] > 25) or (curr['ADX_4h'] > 25) or (curr['ADX_1d'] > 25)
        if mtf_trending:
            return None

        # --- 2. Execution Timeframe Environment Filter (The Shield) ---
        # Hardened: Must be both Choppy AND Low Trend. 
        # This removes the "false range" signals that dilute the PF.
        is_choppy = (curr['CHOP'] > 50.0) and (curr['ADX'] < 20)
        is_vol_stable = curr['ATR'] < (curr['ATR_SMA'] * 1.5)
        
        if not (is_choppy and is_vol_stable):
            return None

        # --- 3. Entry Triggers (The Sniper) ---
        # Long Signal: 
        # 1. Price MUST be below BOTH BBL and KCL (Maximum Overshoot)
        # 2. CCI pivot from below -200 (Maximum Exhaustion)
        if (curr['Close'] < curr['BBL']) and (curr['Close'] < curr['KCL']):
            if (prev['CCI'] < -200) and (curr['CCI'] > prev['CCI']):
                return 'BUY'

        # Short Signal:
        # 1. Price MUST be above BOTH BBU and KCU (Maximum Overshoot)
        # 2. CCI pivot from above 200 (Maximum Exhaustion)
        if (curr['Close'] > curr['BBU']) and (curr['Close'] > curr['KCU']):
            if (prev['CCI'] > 200) and (curr['CCI'] < prev['CCI']):
                return 'SELL'

        return None

MQL5再現コード

//+------------------------------------------------------------------+
//|                                    MeanReversionSynergy_EA.mq5   |
//|                                  Copyright 2026, Quant Engineer  |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

//--- Input Parameters
input int      InpPeriod      = 24;       // Indicator Period
input double   InpBBStd       = 2.0;      // BB Standard Deviation
input double   InpKCScale     = 2.0;      // KC Scalar
input double   InpCCILevel    = 200.0;    // CCI Exhaustion Level
input double   InpTP          = 700;      // Take Profit (points)
input double   InpSL          = 250;      // Stop Loss (points)
input double   InpLot         = 0.1;      // Lot Size
input int      InpTrailing    = 200;      // Trailing Start (points)

//--- Global Handles
int hBB, hKC, hCCI, hADX, hATR;
int hADX_1h, hADX_4h, hADX_1d;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    hBB   = iBands(_Symbol, _Period, InpPeriod, 0, InpBBStd, PRICE_CLOSE);
    hCCI  = iCCI(_Symbol, _Period, InpPeriod, PRICE_TYPICAL);
    hADX  = iADX(_Symbol, _Period, InpPeriod);
    hATR  = iATR(_Symbol, _Period, InpPeriod);
    
    hADX_1h = iADX(_Symbol, PERIOD_H1, InpPeriod);
    hADX_4h = iADX(_Symbol, PERIOD_H4, InpPeriod);
    hADX_1d = iADX(_Symbol, PERIOD_D1, InpPeriod);
    
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom Chop Index calculation                                    |
//+------------------------------------------------------------------+
double CalculateChop(int period)
{
    double high_max = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, period, 1));
    double low_min  = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, period, 1));
    double sum_range = 0;
    for(int i=1; i<=period; i++)
        sum_range += MathAbs(iHigh(_Symbol, _Period, i) - iLow(_Symbol, _Period, i));
    
    if(high_max - low_min == 0) return 0;
    return 100.0 * MathLog10(sum_range / (high_max - low_min)) / MathLog10(period);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!isNewBar()) return;

    double bbL[], bbU[], cci[], adx[], atr[];
    double adx1h[], adx4h[], adx1d[];
    
    CopyBuffer(hBB, 2, 0, 2, bbL); // Lower band
    CopyBuffer(hBB, 0, 0, 2, bbU); // Upper band
    CopyBuffer(hCCI, 0, 0, 2, cci);
    CopyBuffer(hADX, 0, 0, 2, adx);
    CopyBuffer(hATR, 0, 0, 2, atr);
    CopyBuffer(hADX_1h, 0, 0, 1, adx1h);
    CopyBuffer(hADX_4h, 0, 0, 1, adx4h);
    CopyBuffer(hADX_1d, 0, 0, 1, adx1d);

    // Keltner Channel Calculation (Simplified)
    double sma = 0;
    for(int i=1; i<=InpPeriod; i++) sma += iClose(_Symbol, _Period, i);
    sma /= InpPeriod;
    double kc_range = atr[0] * InpKCScale;
    double kcL = sma - kc_range;
    double kcU = sma + kc_range;

    double chop = CalculateChop(InpPeriod);
    double close = iClose(_Symbol, _Period, 0);

    //--- 1. MTF Filter
    if(adx1h[0] > 25 || adx4h[0] > 25 || adx1d[0] > 25) return;

    //--- 2. Environment Filter
    if(!(chop > 50.0 && adx[0] < 20)) return;
    
    double atr_sma = 0;
    for(int i=0; i<24; i++) atr_sma += iATR(_Symbol, _Period, InpPeriod, i);
    atr_sma /= 24;
    if(atr[0] >= atr_sma * 1.5) return;

    //--- 3. Entry Signals
    // Buy
    if(close < bbL[0] && close < kcL)
    {
        if(cci[1] < -InpCCILevel && cci[0] > cci[1])
        {
            TradeOpen(ORDER_TYPE_BUY);
        }
    }
    // Sell
    else if(close > bbU[0] && close > kcU)
    {
        if(cci[1] > InpCCILevel && cci[0] < cci[1])
        {
            TradeOpen(ORDER_TYPE_SELL);
        }
    }
}

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 = InpLot;
    request.type = type;
    request.price = price;
    request.sl = sl;
    request.tp = tp;
    request.deviation = 10;
    request.magic = 123456;
    
    OrderSend(request, result);
}

bool isNewBar()
{
    static datetime last_time = 0;
    datetime lastbar_time = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
    if(last_time != lastbar_time)
    {
        last_time = lastbar_time;
        return true;
    }
    return false;
}