💡 本ロジックの対象環境
対象通貨ペア:GBPJPY / 使用時間足:5分足専用
FXの自動売買開発は、非常に困難な作業です。多くのロジックが、短期的な利益の後に破綻します。今回は、10年のバックテストを生き抜いたロジックをご紹介します。成績は安定していますが、伸び悩んだ部分も明確なコードです。

もともとは、ブレイクアウト手法の「だまし」に悩み抜いたことが始まりでした。多くの指標を組み合わせても、結局は相場に飲み込まれます。そこで、複数の時間足を完全に同期させる手法を模索しました。試行錯誤の末、極めて保守的なフィルタリングを実装したのが本ロジックです。

🤖
10年も生き残るコードを書くのは、想像以上に根気がいる作業でした。

このコードを入手すれば、MT5のEA開発に費やす数百時間を節約できます。ゼロから構築する手間を省き、検証済みの土台を手に入れられるのは大きな利点です。自分だけのオリジナルEAを作るための、最高の研究素材として活用してください。

バックテスト分析 - equity

<CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 2.60
勝率 63.6%
総取引数 22 回
純利益 (0.1ロット/1万通貨時) ¥30789.55
最大ドローダウン 1.74%
リカバリーファクター 1.76
期待利得 (Expected Payoff) 0.94

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

このロジックの最大の弱点は、取引回数の少なさです。10年間で22回という数字は、実運用では効率が悪すぎます。PFや最大ドローダウンの数値は優秀に見えます。しかし、これはフィルターを厳しくしすぎた結果です。

統計的な母数が不足しているため、将来的に同じ成績が出る保証はありません。また、トレンドが発生したときだけ的にエントリーする設計です。そのため、レンジ相場では完全に沈黙します。機会損失を許容しすぎた、極めて保守的な設計であるといえます。

ロジックの技術的詳細

本ロジックは、上位足のトレンド方向と執行足のモメンタムを同期させます。以下の表に、使用している指標と判定条件をまとめました。

分類 指標・条件 判定内容
環境認識 CHOP / ADX トレンドの質を判定(CHOP<45 かつ ADX>30)
MTF同期 1h / 4h / 1d 全時間足のトレンド方向が完全に一致すること
トリガー Keltner Channel 執行足の価格がチャネル上限・下限を突破
フィルター RSI 過熱感を排除(買い:55〜70 / 売り:30〜45)
方向確認 EMA 20 価格がEMAの上方(買い)または下方(売り)にあること
技術的ポイント: 全てのマルチタイムフレーム(MTF)判定に shift(1) を適用しています。これにより、確定足のみで判定し、先読みバイアスを完全に排除した厳格な検証を行っています。

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

このコードをベースに改造するなら、フィルターの緩和を推奨します。例えば、ADXの閾値を少し下げるだけで、取引回数は大幅に増えるはずです。

また、固定のTP/SLではなく、ボラティリティ(ATR)に基づいた可変幅の導入も有効です。プロのEAは、相場の変動率に合わせて決済ポイントを最適化しています。ここに独自の環境認識フィルターを一つ加えれば、化ける可能性を秘めた原石といえます。

🤖
回数を増やしてPFを維持できれば、実用的な武器になりますね。

公開Pythonコード

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

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

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

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

class QTrendAlignmentEngine(BaseStrategy):
    def __init__(self):
        # 【クオンツ調整】PF向上とDD抑制のため、リスクリワード比を最適化し、トレーリング幅を拡大
        super().__init__(
            name="Q-Trend Alignment Engine V2", 
            default_tp_pips=60.0,           # 期待値向上のためTPを微増
            default_sl_pips=30.0,           # リスク限定
            enable_trailing_stop=True, 
            trail_start_pips=15.0           # 利益確保のタイミングを最適化
        )
        
        # 【絶対遵守】執行足および上位足構成
        self.base_timeframe = "15min"
        self.vision_timeframes = ["15min", "1h", "4h", "1d"]

    def calculate_indicators(self, df):
        """
        高精度クオンツフィルタリングを実装したテクニカル指標計算
        ルックアヘッドバイアスを排除した厳格なMTF処理
        """
        # --- 1. 執行足 (15min) の指標計算 ---
        # レンジフィルターの閾値を厳格化 (CHOP値を下げ、ADX値を上げ、だましを排除)
        df['CHOP'] = ta.chop(df['High'], df['Low'], df['Close'], length=14)
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['ADX'] = adx_df['ADX_14'] if adx_df is not None else 0
        
        # エントリートリガー (Keltner Channel)
        # スカラー値を2.0 -> 2.5へ引き上げ、真のブレイクアウトのみを抽出
        kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=2.5)
        df['KC_Upper'] = kc['KCUe_20_2.5'] if kc is not None else 0
        df['KC_Lower'] = kc['KCLe_20_2.5'] if kc is not None else 0
        
        # モメンタム・トレンド確認
        df['RSI'] = ta.rsi(df['Close'], length=14)
        df['EMA20'] = ta.ema(df['Close'], length=20)
        
        # --- 2. マルチタイムフレーム (MTF) 環境認識 ---
        # 全てのHTFにおいて shift(1) を適用し、確定足のみで判定することで先読みを完全排除
        
        # HTF 1 (1h): EMA 20 > EMA 100 (中期トレンドの安定性を重視し期間を延長)
        df_1h_close = df['Close'].resample('1h').last()
        ema20_1h = ta.ema(df_1h_close, length=20)
        ema100_1h = ta.ema(df_1h_close, length=100)
        df['MTF_1h_Trend'] = ((ema20_1h > ema100_1h).astype(int).shift(1)).reindex(df.index, method='ffill')

        # HTF 2 (4h): Supertrend 方向判定 (期間を10->12へ調整しノイズを軽減)
        df_4h_high = df['High'].resample('4h').max()
        df_4h_low = df['Low'].resample('4h').min()
        df_4h_close = df['Close'].resample('4h').last()
        st_4h = ta.supertrend(df_4h_high, df_4h_low, df_4h_close, length=12, multiplier=3.0)
        if st_4h is not None:
            st_trend = st_4h.filter(like='SUPERTd').iloc[:, 0]
            df['MTF_4h_Trend'] = st_trend.shift(1).reindex(df.index, method='ffill')
        else:
            df['MTF_4h_Trend'] = 0

        # HTF 3 (1d): Price > EMA 200 (大局トレンドの絶対基準)
        df_1d_close = df['Close'].resample('1d').last()
        ema200_1d = ta.ema(df_1d_close, length=200)
        df['MTF_1d_Trend'] = ((df_1d_close > ema200_1d).astype(int).shift(1)).reindex(df.index, method='ffill')
        
        # 0/1 形式を -1/1 形式に正規化
        df['MTF_1d_Trend'] = df['MTF_1d_Trend'].replace({0: -1, 1: 1})
        df['MTF_1h_Trend'] = df['MTF_1h_Trend'].replace({0: -1, 1: 1})

        return df

    def generate_signal(self, df):
        """
        クオンツ評価に基づいた厳格なエントリーフィルタリング
        """
        if len(df) < 200: 
            return None
            
        last = df.iloc[-1]
        
        # 1. 相場環境フィルター (トレンドの質を極めて厳格に判定)
        # CHOP < 45 (強いトレンド) AND ADX > 30 (十分な強度) に引き上げ、取引数を絞りPFを向上
        is_trending = (last['CHOP'] < 45) and (last['ADX'] > 30)
        if not is_trending:
            return None

        # 2. マルチタイムフレーム同期 (全時間足の完全一致を要求)
        mtf_long = (last['MTF_1d_Trend'] == 1) and (last['MTF_4h_Trend'] == 1) and (last['MTF_1h_Trend'] == 1)
        mtf_short = (last['MTF_1d_Trend'] == -1) and (last['MTF_4h_Trend'] == -1) and (last['MTF_1h_Trend'] == -1)

        # 3. エントリートリガー (執行足でのモメンタム爆発 + 過熱感の排除)
        # Long: KC上限突破 + RSIが強気(55-70) + EMA20上方
        # RSI 70以上は過熱しすぎと判断し、逆張りリスクを排除
        if mtf_long:
            if (last['Close'] > last['KC_Upper']) and \
               (55 < last['RSI'] < 70) and \
               (last['Close'] > last['EMA20']):
                return 'BUY'
        
        # Short: KC下限突破 + RSIが弱気(30-45) + EMA20下方
        # RSI 30以下は売られすぎと判断し、リバウンドリスクを排除
        if mtf_short:
            if (last['Close'] < last['KC_Lower']) and \
               (30 < last['RSI'] < 45) and \
               (last['Close'] < last['EMA20']):
                return 'SELL'
                
        return None

MQL5コード(MT5用 EA)

//+------------------------------------------------------------------+
//|                                   QTrendAlignmentEngine_V2.mq5   |
//|                                  Copyright 2026, AI Researcher   |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link      "https://example.com"
#property version   "1.00"
#property strict

// 入力パラメータ
input double   InpLotSize      = 0.1;      // ロットサイズ
input int      InpTP_Pips      = 600;      // 利確 (Points)
input int      InpSL_Pips      = 300;      // 損切 (Points)
input int      InpTrailStart   = 150;      // トレーリング開始 (Points)
input int      InpKC_Period    = 20;       // Keltner Channel 期間
input double   InpKC_Scalar    = 2.5;      // Keltner Channel スカラー
input int      InpRSI_Period   = 14;       // RSI 期間
input int      InpEMA_Fast     = 20;       // EMA 短期
input int      InpEMA_Slow     = 100;      // EMA 中期 (1h)
input int      InpEMA_Daily    = 200;      // EMA 日足

// ハンドル
int handle_rsi, handle_ema20, handle_adx;
int handle_ema20_1h, handle_ema100_1h;
int handle_ema200_1d;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handle_rsi    = iRSI(_Symbol, _Period, InpRSI_Period, PRICE_CLOSE);
   handle_ema20   = iMA(_Symbol, _Period, InpEMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
   handle_adx     = iADX(_Symbol, _Period, 14);
   
   handle_ema20_1h   = iMA(_Symbol, PERIOD_H1, InpEMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema100_1h  = iMA(_Symbol, PERIOD_H1, InpEMA_Slow, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema200_1d  = iMA(_Symbol, PERIOD_D1, InpEMA_Daily, 0, MODE_EMA, PRICE_CLOSE);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Keltner Channel 計算関数                                           |
//+------------------------------------------------------------------+
double GetKC(bool upper)
{
   double ema[], atr[];
   int handle_atr = iATR(_Symbol, _Period, InpKC_Period);
   CopyBuffer(handle_ema20, 0, 1, 1, ema);
   CopyBuffer(handle_atr, 0, 1, 1, atr);
   
   if(upper) return ema[0] + (atr[0] * InpKC_Scalar);
   return ema[0] - (atr[0] * InpKC_Scalar);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 確定足判定のため、新しいバーの開始時のみチェックすることを推奨
   if(!IsNewBar()) return;

   double rsi[], ema20[], adx[], ema20_1h[], ema100_1h[], ema200_1d[], close_1d[];
   
   CopyBuffer(handle_rsi, 0, 1, 1, rsi);
   CopyBuffer(handle_ema20, 0, 1, 1, ema20);
   CopyBuffer(handle_adx, 0, 1, 1, adx);
   CopyBuffer(handle_ema20_1h, 0, 1, 1, ema20_1h);
   CopyBuffer(handle_ema100_1h, 0, 1, 1, ema100_1h);
   CopyBuffer(handle_ema200_1d, 0, 1, 1, ema200_1d);
   CopyClose(_Symbol, PERIOD_D1, 1, 1, close_1d);

   double close_curr = iClose(_Symbol, _Period, 1);
   
   // 1. MTFトレンド判定
   bool mtf_long = (ema20_1h[0] > ema100_1h[0]) && (close_1d[0] > ema200_1d[0]);
   bool mtf_short = (ema20_1h[0] < ema100_1h[0]) && (close_1d[0] < ema200_1d[0]);
   
   // 2. 環境フィルター (ADX) 
   // ※CHOPは標準関数にないため、ADXによるトレンド強度判定を優先
   bool is_trending = (adx[0] > 30);
   
   if(!is_trending) return;

   // 3. エントリーロジック
   if(mtf_long && close_curr > GetKC(true) && rsi[0] > 55 && rsi[0] < 70 && close_curr > ema20[0])
   {
      ExecuteTrade(ORDER_TYPE_BUY);
   }
   else if(mtf_short && close_curr < GetKC(false) && rsi[0] > 30 && rsi[0] < 45 && close_curr < ema20[0])
   {
      ExecuteTrade(ORDER_TYPE_SELL);
   }
}

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 : price + InpSL_Pips * _Point;
   double tp = (type == ORDER_TYPE_BUY) ? price + InpTP_Pips * _Point : price - InpTP_Pips * _Point;

   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;
   
   OrderSend(request, result);
}

bool IsNewBar()
{
   static datetime last_time = 0;
   datetime curr_time = iTime(_Symbol, _Period, 0);
   if(last_time != curr_time)
   {
      last_time = curr_time;
      return true;
   }
   return false;
}