💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
FXの自動売買開発は困難です。多くのロジックが数ヶ月で破綻します。最適化の罠に陥り、バックテストだけが右肩上がりになるケースも少なくありません。今回は、10年という長期検証を破綻せず生き抜いたロジックを公開します。成績は控えめですが、極めて高い生存能力を持つコードです。

もともと私は、ブレイクアウト手法のダマシに悩み続けていました。トレンドを追いかけるほど、資金を削られる経験をしました。そこで「あえてトレンドが出ない場所」だけを狙う戦略を考えました。ボラティリティが低い時間帯にのみ、心地よく反発を拾う。そんな地味なアプローチを追求し、このロジックに辿り着きました。

🤖
派手な利益は出ませんが、夜も安心して眠れる設計を目指しました。

このコードをベースにすれば、MT5のEA開発にかかる数百時間を節約できます。ロジックの組み方やMTF(マルチタイムフレーム)分析の実装方法は、そのまま実戦で使えます。自分だけのオリジナルEAを作るための、最高の土台として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.21
勝率 46.2%
総取引数 13 回
純利益 (0.1ロット/1万通貨時) ¥2625.11
最大ドローダウン 0.89%
リカバリーファクター 0.29
期待利得 (Expected Payoff) 0.65

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

このロジックの最大の弱点は、取引回数の少なさです。10年で13回という数字は、実運用では機会損失が大きすぎます。PF1.21という数値も、攻めの姿勢が足りない証拠と言えるでしょう。

原因は、フィルターを厳しくしすぎたことにあります。ADXやCHOP、さらには上位足の状況まで重ね合わせたため、エントリー条件が極めて限定的になりました。カーブフィッティングを避けた結果ですが、収益性は犠牲になっています。

「生き残る能力」は証明されましたが、「稼ぐ能力」はまだ不十分な未完成の原石です。

ロジックの技術的詳細

本ロジックは、低ボラティリティ環境での平均回帰(ミーンリバージョン)を狙います。

指標 設定・条件 役割
ADX (14) 23未満 トレンドの不在を確認
CHOP (14) 60超え 相場の停滞(レンジ)を検知
ボリンジャーバンド 20期間 / 2σ 価格の境界線の判定
ドンチアンチャネル 20期間 直近高値・安値の把握
RSI (7) 30以下 $\rightarrow$ 上抜け / 70以上 $\rightarrow$ 下抜け 短期的な反転タイミングの検知
MTFフィルター 15分/1時間ADX < 28、4時間SMA傾斜 $\approx$ 0 上位足のトレンド不在を同期
時間フィルター 東京市場 (0時〜6時) 低ボラティリティ時間帯の限定

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

このコードをベースに改造するなら、まずは「時間フィルター」の緩和を勧めます。東京時間以外でも、同様の低ボラティリティ状態が発生する時間帯があるはずです。

また、固定のTP/SLではなく、ATRに基づいた可変幅の導入も有効でしょう。相場の変動幅に合わせれば、期待利得を向上させられます。プロのEAは、ここに独自の「環境認識」を加え、トレンド相場への移行をより早く察知して切り替えています。

🤖
フィルターを一つ外すだけで、取引回数が劇的に増えて化ける可能性がありますね。

公開コード (Python)

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

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

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

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

class EquilibriumHunter(BaseStrategy):
    """
    Strategy: The Equilibrium Hunter (Optimized v2)
    Concept: Mean Reversion in Low-Volatility Regime
    Fix: Resolved 'Series object has no attribute ta' by using functional pandas_ta calls.
    Goal: Maximize PF and Recovery Factor by tightening regime filters.
    """
    def __init__(self):
        # Optimization: Refined TP/SL for stability. 
        # TP 35 / SL 25 provides a balanced reward-to-risk ratio for range trading.
        super().__init__(
            name="EquilibriumHunter", 
            default_tp_pips=35.0, 
            default_sl_pips=25.0, 
            enable_trailing_stop=True, 
            trail_start_pips=12.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h", "4h"]

    def calculate_indicators(self, df):
        # --- Local Timeframe (5m) Indicators ---
        # Use functional ta calls to avoid accessor issues
        df['ADX'] = ta.adx(df['High'], df['Low'], df['Close'], length=14)['ADX_14']
        df['ADX_slope'] = df['ADX'].diff(1)
        df['CHOP'] = ta.chop(df['High'], df['Low'], df['Close'], length=14)

        # Bollinger Bands: Access by index to avoid 'BBL_20_2.0' KeyError
        bb = ta.bbands(df['Close'], length=20, std=2.0)
        df['BBL'] = bb.iloc[:, 0]
        df['BBM'] = bb.iloc[:, 1]
        df['BBU'] = bb.iloc[:, 2]
        
        df['BBW'] = (df['BBU'] - df['BBL']) / df['BBM']
        df['BBW_avg'] = df['BBW'].rolling(window=20).mean()

        # RSI for fast reversal detection
        df['RSI'] = ta.rsi(df['Close'], length=7)

        # Donchian Channel: Access by index
        dc = ta.donchian(df['High'], df['Low'], length=20)
        df['DCL'] = dc.iloc[:, 0]
        df['DCU'] = dc.iloc[:, 1]
        
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)

        # --- Multi-Timeframe (MTF) Logic ---
        # Rule: Shift(1) BEFORE reindex to eliminate Look-ahead Bias
        
        # 15min Filter
        close_15m = df['Close'].resample('15min').last().shift(1)
        high_15m = df['High'].resample('15min').max().shift(1)
        low_15m = df['Low'].resample('15min').min().shift(1)
        # To use ADX on resampled data, create a temporary DataFrame
        df_15m = pd.DataFrame({'High': high_15m, 'Low': low_15m, 'Close': close_15m})
        adx_15m = ta.adx(df_15m['High'], df_15m['Low'], df_15m['Close'], length=14)['ADX_14']
        df['MTF_ADX_15m'] = adx_15m.reindex(df.index, method='ffill')

        # 1h Filter
        close_1h = df['Close'].resample('1h').last().shift(1)
        high_1h = df['High'].resample('1h').max().shift(1)
        low_1h = df['Low'].resample('1h').min().shift(1)
        df_1h = pd.DataFrame({'High': high_1h, 'Low': low_1h, 'Close': close_1h})
        adx_1h = ta.adx(df_1h['High'], df_1h['Low'], df_1h['Close'], length=14)['ADX_14']
        df['MTF_ADX_1h'] = adx_1h.reindex(df.index, method='ffill')

        # 4h Filter: Use ta.sma() functional call instead of .ta.sma() on Series
        close_4h = df['Close'].resample('4h').last().shift(1)
        sma_4h = ta.sma(close_4h, length=20)
        slope_4h = sma_4h.diff(1)
        df['MTF_SMA_Slope_4h'] = slope_4h.reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        if len(df) < 2:
            return None
            
        curr = df.iloc[-1]
        prev = df.iloc[-2]

        # 1. Regime Detection (5m)
        # Tightened: ADX < 23 and CHOP > 60 for higher probability range signals.
        is_local_range = (
            (curr['ADX'] < 23) and 
            (curr['CHOP'] > 60) and 
            (curr['BBW'] <= curr['BBW_avg'] * 1.1)
        )

        # 2. MTF Sync
        # Filter: Ensure multiple timeframes are not trending.
        is_mtf_range = (
            (curr['MTF_ADX_15m'] < 28) and 
            (curr['MTF_ADX_1h'] < 28) and 
            (abs(curr['MTF_SMA_Slope_4h']) < (curr['Close'] * 0.00012))
        )

        # 3. Time Filter: Tokyo Session (Low volatility hours)
        is_tokyo_time = (0 <= df.index[-1].hour < 6)

        # 4. Entry Logic (Long)
        # Touches lower band/channel and RSI recovers from oversold.
        long_condition = (
            is_local_range and 
            is_mtf_range and 
            is_tokyo_time and 
            (curr['Low'] <= curr['DCL'] or curr['Low'] <= curr['BBL']) and 
            (prev['RSI'] < 30 and curr['RSI'] > 30)
        )

        # 5. Entry Logic (Short)
        # Touches upper band/channel and RSI recovers from overbought.
        short_condition = (
            is_local_range and 
            is_mtf_range and 
            is_tokyo_time and 
            (curr['High'] >= curr['DCU'] or curr['High'] >= curr['BBU']) and 
            (prev['RSI'] > 70 and curr['RSI'] < 70)
        )

        # 6. Emergency Exit / Filter: Stop entries if a trend starts emerging.
        if curr['ADX'] > 28 or (curr['ADX_slope'] > 2.5):
            return None

        if long_condition:
            return 'BUY'
        elif short_condition:
            return 'SELL'
        
        return None

MT5用 MQL5コード (.mq5)

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

// Input parameters
input double   InpTP = 350;              // Take Profit (points)
input double   InpSL = 250;              // Stop Loss (points)
input double   InpTrailStart = 120;      // Trailing Start (points)
input int      InpADXPeriod = 14;        // ADX Period
input int      InpRSIPeriod = 7;         // RSI Period
input int      InpBBPeriod = 20;         // BB Period
input double   InpBBStdDev = 2.0;        // BB StdDev

// Handles
int handleADX_5m, handleRSI_5m, handleBB_5m, handleADX_15m, handleADX_1h;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handleADX_5m = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
   handleRSI_5m = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
   handleBB_5m = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
   handleADX_15m = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
   handleADX_1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
   
   if(handleADX_5m == INVALID_HANDLE || handleRSI_5m == INVALID_HANDLE) return(INIT_FAILED);
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Custom Chop Index Calculation                                    |
//+------------------------------------------------------------------+
double CalculateCHOP(int period)
{
   double sumTR = 0;
   double highestHigh = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, period, 1));
   double lowestLow = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, period, 1));
   
   for(int i=1; i<=period; i++) {
      double tr = MathMax(iHigh(_Symbol, PERIOD_M5, i) - iLow(_Symbol, PERIOD_M5, i), 
                 MathMax(MathAbs(iHigh(_Symbol, PERIOD_M5, i) - iClose(_Symbol, PERIOD_M5, i+1)), 
                         MathAbs(iLow(_Symbol, PERIOD_M5, i) - iClose(_Symbol, PERIOD_M5, i+1))));
      sumTR += tr;
   }
   
   double range = highestHigh - lowestLow;
   if(range == 0) return 0;
   return 100.0 * (1.0 - (sumTR / (range * period))); // Simplified Chop logic
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   MqlTradeRequest request;
   MqlTradeResult result;
   ZeroMemory(request);

   // Time Filter: Tokyo Session (0-6)
   MqlDateTime dt;
   TimeToStruct(TimeCurrent(), dt);
   if(dt.hour < 0 || dt.hour >= 6) return;

   // Indicators Data
   double adx[2], rsi[2], bbl[1], bbu[1], bbm[1], adx15[1], adx1h[1];
   CopyBuffer(handleADX_5m, 0, 0, 2, adx);
   CopyBuffer(handleRSI_5m, 0, 0, 2, rsi);
   CopyBuffer(handleBB_5m, 1, 0, 1, bbu);
   CopyBuffer(handleBB_5m, 2, 0, 1, bbl);
   CopyBuffer(handleBB_5m, 0, 0, 1, bbm);
   CopyBuffer(handleADX_15m, 0, 0, 1, adx15);
   CopyBuffer(handleADX_1h, 0, 0, 1, adx1h);

   double chop = CalculateCHOP(14);
   double low = iLow(_Symbol, PERIOD_M5, 0);
   double high = iHigh(_Symbol, PERIOD_M5, 0);
   double dcl = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, 20, 1));
   double dcu = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, 20, 1));

   // 1. Regime Detection
   bool isLocalRange = (adx[0] < 23 && chop > 60);
   
   // 2. MTF Sync
   bool isMTFRange = (adx15[0] < 28 && adx1h[0] < 28);

   // Entry Logic
   if(isLocalRange && isMTFRange && PositionsTotal() == 0)
   {
      // LONG
      if((low <= dcl || low <= bbl[0]) && (rsi[1] < 30 && rsi[0] > 30))
      {
         request.action = TRADE_ACTION_DEAL;
         request.symbol = _Symbol;
         request.volume = 0.1;
         request.type = ORDER_TYPE_BUY;
         request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         request.sl = request.price - InpSL * _Point;
         request.tp = request.price + InpTP * _Point;
         OrderSend(request, result);
      }
      // SHORT
      else if((high >= dcu || high >= bbu[0]) && (rsi[1] > 70 && rsi[0] < 70))
      {
         request.action = TRADE_ACTION_DEAL;
         request.symbol = _Symbol;
         request.volume = 0.1;
         request.type = ORDER_TYPE_SELL;
         request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         request.sl = request.price + InpSL * _Point;
         request.tp = request.price - InpTP * _Point;
         OrderSend(request, result);
      }
   }
   
   // Simple Trailing Stop
   for(int i=PositionsTotal()-1; i>=0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            if(SymbolInfoDouble(_Symbol, SYMBOL_BID) - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point)
            {
               double newSL = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailStart * _Point;
               if(newSL > PositionGetDouble(POSITION_SL))
               {
                  MqlTradeRequest mod; MqlTradeResult res; ZeroMemory(mod);
                  mod.action = TRADE_ACTION_SLTP; mod.position = ticket; mod.sl = newSL;
                  OrderSend(mod, res);
               }
            }
         }
      }
   }
}