💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は困難な道のりです。多くのロジックが、わずか数か月の暴落で破綻します。今回は、10年という長期バックテストを破綻せず生き抜いたロジックを紹介します。成績は爆発的ではありませんが、極めて堅牢な設計です。

私は以前、ボリンジャーバンドを用いた逆張り戦略に没頭していました。しかし、強いトレンドが発生した際の「バンドウォーク」に何度も資金を削られました。だましを排除し、真に勝率の高いレンジ相場だけを抽出したい。そんな執念から、この厳格なフィルター群を構築した経緯があります。

このコードをベースにすれば、MT5のEA開発に費やす数百時間を節約できます。ゼロから構築する手間を省き、自分だけのオリジナル戦略を作るための最高の土台になります。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

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

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

このロジックの最大の弱点は、取引回数の少なさです。10年で61回という数字は、機会損失が非常に大きいことを意味します。フィルターを厳格にしすぎた結果、利益を伸ばすチャンスまで捨ててしまいました。

また、PF1.54という数値は合格点ですが、爆発力に欠けます。低ドローダウンを実現した代償として、資産増加のスピードが緩やかになりました。純粋なレンジ相場以外では、ほぼ機能しない点も技術的な限界と言えます。

🤖
堅牢さを求めすぎて、おとなしすぎる性格のロジックになりましたね。

ロジックの技術的詳細

本ロジックは、3つの時間軸と複数の指標を組み合わせた「環境認識重視」の設計です。

項目 使用指標・条件 設定値・役割
環境認識(下位) ADX 22未満(トレンドの不在を確認)
環境認識(上位) 1時間足ADX 28未満(長期的なトレンドを排除)
平坦判定 BBM(中心線)の傾き 0.0003未満(完全な横ばいを厳選)
エントリー(買) 価格 $\le$ BBL かつ RSI $\le$ 30 過熱感のある売られすぎ水準で反発狙い
エントリー(売) 価格 $\ge$ BBU かつ RSI $\ge$ 70 過熱感のある買われすぎ水準で反落狙い
決済ルール 固定TP/SL + トレーリング TP: 35pips / SL: 15pips / Trail: 15pips

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

このロジックを「化けさせる」には、エントリー条件の緩和と動的な決済ロジックの導入が必要です。例えば、固定のTP/SLではなく、ATR(平均真の範囲)に基づいた可変幅に変更してください。相場のボラティリティに合わせれば、利益効率は向上します。

また、時間帯フィルターの追加を推奨します。東京市場などのレンジになりやすい時間帯に限定し、ロンドン・ニューヨーク市場のトレンド発生時間を避ける工夫です。プロのEAは、こうした「時間軸の環境認識」を独自のフィルターで補っています。

本コードは完璧なシステムではなく、改造して遊ぶための「研究素材」です。この堅牢な土台にあなたのアイデアを加え、最適解を導き出してください。

公開コード:Python (検証用)

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

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

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

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

class VolatilityAdjustedRangeStrategy(BaseStrategy):
    def __init__(self):
        super().__init__(
            name="VolatilityAdjustedRangeStrategy",
            default_tp_pips=35.0,  # ペイオフレシオを向上させ、PFとリカバリーファクターを底上げするため目標利幅を拡大
            default_sl_pips=15.0,  # 損切幅をタイトに設定し、1トレードあたりのリスクを抑制してPFを改善
            enable_trailing_stop=True,
            trail_start_pips=15.0  # 利益を確保しつつ、最大DDを抑制するための最適値に調整
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]
        
        # --- クオンツ・外科調整 (だまし排除と利益効率の最大化) ---
        # 取引回数が過多(1273回)でPFが低いため、フィルターを大幅に厳格化し「高期待値トレード」のみを抽出
        self.ADX_THRESHOLD = 22      # 40 -> 22: 強いトレンド相場での逆張りを完全に排除し、純粋なレンジのみを捕捉
        self.MTF_ADX_THRESHOLD = 28   # 45 -> 28: 上位足のトレンドフィルターを強化し、環境認識の精度を向上
        self.SLOPE_THRESHOLD = 0.0003 # 0.001 -> 0.0003: BBMの傾き判定を極めて厳格にし、横ばい相場を厳選
        self.RSI_LOW = 30            # 40 -> 30: 逆張りエントリーの閾値を深くし、真の売られすぎ水準まで待機
        self.RSI_HIGH = 70           # 60 -> 70: 逆張りエントリーの閾値を高くし、真の買われすぎ水準まで待機

    def calculate_indicators(self, df):
        # --- 下位足 (5分足) インジケーター計算 ---
        # ADX (トレンド強度判定)
        df.ta.adx(length=14, append=True)
        
        # Bollinger Bands (価格の乖離判定)
        # 動的なカラム名によるKeyErrorを完全に排除する堅牢な抽出ロジック
        bb_df = df.ta.bbands(length=20, std=2.0)
        if bb_df is not None:
            df['BBL'] = bb_df.filter(like='BBL').iloc[:, 0]
            df['BBM'] = bb_df.filter(like='BBM').iloc[:, 0]
            df['BBU'] = bb_df.filter(like='BBU').iloc[:, 0]
        
        # RSI (オシレーターによる過熱感判定)
        # 期間を14から10へ短縮し、レンジ内の反転タイミングに対する反応速度を改善
        df.ta.rsi(length=10, append=True)
        
        # ATR (ボラティリティ参照)
        df.ta.atr(length=14, append=True)
        
        # SMAの傾き (BBMを利用)
        # 判定期間を5本に延長し、短期的なノイズを排除して「真の平坦さ」を定義
        df['sma_slope'] = df['BBM'].diff(5)

        # --- 上位足 (1時間足) フィルタリング (ルックアヘッドバイアス排除) ---
        h1_resampled = df[['High', 'Low', 'Close']].resample('1h').agg({
            'High': 'max', 
            'Low': 'min', 
            'Close': 'last'
        })
        
        h1_adx_df = ta.adx(h1_resampled['High'], h1_resampled['Low'], h1_resampled['Close'], length=14)
        
        if h1_adx_df is not None:
            # shift(1)により確定足のみを使用。reindex and ffillで時間軸を完全一致させる
            mtf_adx = h1_adx_df['ADX_14'].shift(1).reindex(df.index, method='ffill')
            df['mtf_adx'] = mtf_adx
        else:
            df['mtf_adx'] = float('nan')

        return df

    def generate_signal(self, df):
        if len(df) < 30:
            return None
            
        last = df.iloc[-1]
        
        try:
            # pandas_taのADXカラム名は通常 'ADX_14'
            adx = last['ADX_14']
            mtf_adx = last['mtf_adx']
            slope = last['sma_slope']
            close = last['Close']
            bbl = last['BBL']
            bbu = last['BBU']
            # RSI期間変更に伴いカラム名を動的に取得(RSI_10)
            rsi_col = [c for c in df.columns if c.startswith('RSI_')][0]
            rsi = last[rsi_col]
        except (KeyError, IndexError):
            return None

        # --- エントリー条件判定 (高精度クオンツ・フィルタ) ---
        # 1. 環境認識: 3つの指標が同時に「極めて低いボラティリティ/トレンド」を示すこと
        # これにより、トレンド発生時の「Bands Walk」による連敗(だまし)を徹底的に排除
        is_strict_range = (
            adx < self.ADX_THRESHOLD and 
            mtf_adx < self.MTF_ADX_THRESHOLD and 
            abs(slope) < self.SLOPE_THRESHOLD
        )

        if not is_strict_range:
            return None

        # 2. エントリータイミング: BBの外側への突き抜け + RSIの極端な過熱感
        # 【買いシグナル】
        if close <= bbl and rsi < self.RSI_LOW:
            return 'BUY'

        # 【売りシグナル】
        if close >= bbu and rsi > self.RSI_HIGH:
            return 'SELL'

        return None

公開コード:MQL5 (MT5実装用)

//+------------------------------------------------------------------+
//|                                  VolatilityAdjustedRangeStrategy |
//|                                  Copyright 2026, Quant Engineer  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link      "https://your-blog-url.com"
#property version   "1.00"
#property strict

// --- 入力パラメータ ---
input int      InpADXPeriod   = 14;          // ADX Period
input double   InpADXThres    = 22.0;        // ADX Threshold (Lower = More Range)
input int      InpMTFADXPeriod = 14;         // H1 ADX Period
input double   InpMTFADXThres  = 28.0;        // H1 ADX Threshold
input double   InpSlopeThres   = 0.0003;     // SMA Slope Threshold
input int      InpRSIPeriod    = 10;         // RSI Period
input int      InpRSI_Low      = 30;         // RSI Buy Level
input int      InpRSI_High     = 70;         // RSI Sell Level
input int      InpBBPeriod     = 20;         // Bollinger Bands Period
input double   InpBBDev        = 2.0;        // Bollinger Bands Deviation
input double   InpTP_Pips      = 35.0;       // Take Profit (Pips)
input double   InpSL_Pips      = 15.0;       // Stop Loss (Pips)
input double   InpTrailStart   = 15.0;       // Trailing Start (Pips)
input double   InpLotSize      = 0.1;        // Lot Size

// --- グローバル変数 ---
int handleADX, handleMTFADX, handleRSI, handleBB;
double bufferADX[], bufferMTFADX[], bufferRSI[], bufferBBU[], bufferBBM[], bufferBBL[];

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handleADX    = iADX(_Symbol, _Period, InpADXPeriod);
   handleMTFADX  = iADX(_Symbol, PERIOD_H1, InpMTFADXPeriod);
   handleRSI    = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
   handleBB     = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBDev, PRICE_CLOSE);

   if(handleADX == INVALID_HANDLE || handleMTFADX == INVALID_HANDLE || 
      handleRSI == INVALID_HANDLE || handleBB == INVALID_HANDLE)
   {
      Print("インジケーターハンドルの作成に失敗しました");
      return(INIT_FAILED);
   }
   
   ArraySetAsSeries(bufferADX, true);
   ArraySetAsSeries(bufferMTFADX, true);
   ArraySetAsSeries(bufferRSI, true);
   ArraySetAsSeries(bufferBBU, true);
   ArraySetAsSeries(bufferBBM, true);
   ArraySetAsSeries(bufferBBL, true);

   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // データのコピー
   if(CopyBuffer(handleADX, 0, 0, 2, bufferADX) < 2) return;
   if(CopyBuffer(handleMTFADX, 0, 0, 2, bufferMTFADX) < 2) return;
   if(CopyBuffer(handleRSI, 0, 0, 2, bufferRSI) < 2) return;
   if(CopyBuffer(handleBB, 0, 0, 6, bufferBBU) < 6) return;
   if(CopyBuffer(handleBB, 1, 0, 6, bufferBBM) < 6) return;
   if(CopyBuffer(handleBB, 2, 0, 6, bufferBBL) < 6) return;

   // 傾きの計算 (BBMの5本前との差)
   double slope = bufferBBM[0] - bufferBBM[5];
   double close = iClose(_Symbol, _Period, 0);

   // 環境認識フィルタ
   bool isStrictRange = (bufferADX[0] < InpADXThres && 
                         bufferMTFADX[1] < InpMTFADXThres && 
                         MathAbs(slope) < InpSlopeThres);

   if(!isStrictRange) return;

   // エントリー判定
   if(PositionsTotal() == 0)
   {
      // BUY: 価格 <= LowerBand かつ RSI <= Low
      if(close <= bufferBBL[0] && bufferRSI[0] <= InpRSI_Low)
      {
         ExecuteTrade(ORDER_TYPE_BUY);
      }
      // SELL: 価格 >= UpperBand かつ RSI >= High
      else if(close >= bufferBBU[0] && bufferRSI[0] >= InpRSI_High)
      {
         ExecuteTrade(ORDER_TYPE_SELL);
      }
   }

   ManageTrailingStop();
}

//+------------------------------------------------------------------+
//| トレード実行関数                                                  |
//+------------------------------------------------------------------+
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;

   if(!OrderSend(request, result)) Print("OrderSend Error: ", GetLastError());
}

//+------------------------------------------------------------------+
//| トレーリングストップ管理                                            |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket))
      {
         if(PositionGetInteger(POSITION_MAGIC) != 123456) continue;
         
         double currentPrice = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
         double currentSL = PositionGetDouble(POSITION_SL);
         
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
         {
            if(currentPrice - openPrice > InpTrailStart * _Point * 10)
            {
               double newSL = currentPrice - InpSL_Pips * _Point * 10;
               if(newSL > currentSL) ModifySL(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
         else
         {
            if(openPrice - currentPrice > InpTrailStart * _Point * 10)
            {
               double newSL = currentPrice + InpSL_Pips * _Point * 10;
               if(newSL < currentSL || currentSL == 0) ModifySL(ticket, newSL, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}

void ModifySL(ulong ticket, double sl, double tp)
{
   MqlTradeRequest request = {};
   MqlTradeResult result = {};
   request.action = TRADE_ACTION_SLTP;
   request.position = ticket;
   request.sl = sl;
   request.tp = tp;
   if(!OrderSend(request, result)) Print("ModifySL Error: ", GetLastError());
}