💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は孤独な戦いです。多くの人が期待して作成したロジックが、わずか数ヶ月の相場変動で破綻する光景を何度も見てきました。今回は、10年という全期間のバックテストを破綻せずに生き抜いたロジックを共有します。ただし、成績は決して爆発的ではありません。むしろ、慎重すぎて利益が伸び悩んだという側面を持つ誠実なコードです。

もともと私は、ロンドン時間からニューヨーク時間へ移行する際の「セッションブレイクアウト」に注目していました。しかし、多くの手法が抱える「だまし」に何度も翻弄されました。強いトレンドだと思ってエントリーした直後に反転する。この絶望感を解消したかったのが開発の原点です。そこで、トレンドの強度とキャンドルの質、そして上位足の方向性を極限まで厳格に組み合わせる手法を模索し続けました。

🤖
完璧を求めすぎて、エントリー回数が激減したときは正直焦りましたね。

このコードをベースにすれば、MT5のEAをゼロから開発する数百時間を節約できます。検証済みのフィルタリング構造は、あなただけのオリジナルEAを作るための最高の土台になるはずです。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.33
勝率 54.1%
総取引数 196 回
純利益 (0.1ロット/1万通貨時) ¥58130.91
最大ドローダウン 3.85%
リカバリーファクター 1.51
期待利得 (Expected Payoff) 0.61

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

このロジックの最大の弱点は、取引回数の少なさです。10年で196回という数字は、年間平均に直すと極めて少ない水準にあります。これは、ADXや実体比率などのフィルターを厳しくしすぎた結果です。

また、純利益も運用額に対して控えめな数値に留まっています。リスクを徹底的に排除したため、大きなトレンドを掴みきれず、利益を逃した局面が散見されます。効率的に資産を増やすという視点では、まだ改善の余地がある未完成の原石といえます。

ロジックの技術的詳細

本ロジックは、時間帯による流動性の変化と、マルチタイムフレーム分析を組み合わせています。

項目 設定内容・条件
対象通貨・時間足 USDJPY / 5分足
環境認識 (上位足) 1時間足のEMA200より価格が上か下かで方向性を固定
エントリー時間 16:00 ~ 18:00 (サーバー時間)
レンジ基準 14:00 ~ 16:00 の高値・安値をブレイクアウト
トレンド強度 ADX(14)が30以上であること
ボラティリティ ATR(14)が過去20本の平均の1.3倍以上であること
キャンドル質 実体比率が60%以上の強い足であること
モメンタム RSI(14)が60以上(買い) または 40以下(売り)
決済ルール TP: 60pips / SL: 20pips / トレーリング開始: 20pips
単なる数値の組み合わせではなく、「方向性→強度→質→タイミング」という論理的なステップを踏んでいる点が、10年のサバイバルを可能にした要因です。

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

この無料コードをベースに改造するなら、まずは「エントリー条件の緩和」を検討してください。例えば、ADXの閾値を25に下げるだけで、取引回数と利益が増える可能性があります。

また、固定のTP/SLではなく、直近の安値・高値に合わせたダイナミックな決済ロジックを導入すべきです。プロの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 SessionLiquidityBreakout(BaseStrategy):
    """
    Session Liquidity Breakout Strategy (Quantum-Precision Tuning v4)
    
    【クオンツ外科手術:PFおよびリカバリーファクターの劇的改善】
    1. ペイオフレシオの強制底上げ: 
       - TPを60.0に拡大し、SLを20.0に維持することでRR比を 1:3 へ設計変更。
       - トレーリングストップ開始位置を20.0に引き上げ、早期確定による利益の機会損失を防止。
    2. 「だまし」の徹底排除 (False Sign Filter):
       - ADX閾値を30へ引き上げ、極めて強いトレンド発生時のみにエントリーを限定。
       - キャンドル実体比率(Body Ratio)を0.6へ厳格化し、迷いのない突き抜けのみを採用。
       - RSIの判定基準を 60/40 に変更し、モメンタムの確実性を担保。
    3. トレンド同期の深化:
       - 1H EMA200(大方向) + 5min EMA50(中方向) + 5min Close(即時方向) の完全な整列を必須条件化。
    4. 執行時間の最適化:
       - 最もボラティリティと方向性が一致しやすい 16:00 - 18:00 に時間を凝縮し、期待値の低い時間帯をカット。
    """
    def __init__(self):
        # 【クオンツ調整】
        # リカバリーファクター3.0以上を目指し、1回の勝ち幅を最大化する設定
        super().__init__(
            name="SessionLiquidityBreakout", 
            default_tp_pips=60.0,           # 45 -> 60 (ペイオフレシオの向上)
            default_sl_pips=20.0,           # MaxDD抑制のためタイトに維持
            enable_trailing_stop=True, 
            trail_start_pips=20.0           # 15 -> 20 (利益を伸ばし、純利を最大化)
        )
        self.base_timeframe = "5min"
        self.vision_timeframes = ["5min", "15min", "1h"]

    def calculate_indicators(self, df):
        # --- 1. 上位足 (1h) の計算とルックアヘッドバイアス排除 ---
        df_1h_raw = df.resample('1h').agg({
            'Open': 'first', 
            'High': 'max', 
            'Low': 'min', 
            'Close': 'last'
        })
        
        df_1h_raw['HTF_EMA200'] = ta.ema(df_1h_raw['Close'], length=200)
        
        df_1h_shifted = df_1h_raw[['Close', 'HTF_EMA200']].shift(1)
        df_htf = df_1h_shifted.reindex(df.index, method='ffill')
        
        df['HTF_Close'] = df_htf['Close']
        df['HTF_EMA200'] = df_htf['HTF_EMA200']

        # --- 2. 下位足 (5min) の指標計算 ---
        # トレンド強度: 閾値を30に設定し、低ボラティリティ時の往復ビンタを排除
        adx_df = df.ta.adx(length=14)
        df['ADX_14'] = adx_df['ADX_14'] if adx_df is not None else np.nan
        
        # 短期トレンド方向: EMA50
        df['LTF_EMA50'] = ta.ema(df['Close'], length=50)
        
        # モメンタム: RSI (強気/弱気の境界線を明確化)
        df['RSI_14'] = ta.rsi(df['Close'], length=14)
        
        # ボラティリティ: 閾値を1.3倍へ引き上げ、爆発的な動きに限定
        df['ATR_14'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['ATR_MA20'] = df['ATR_14'].rolling(window=20).mean()

        # キャンドル実体比率: 実体比率0.6以上 = 強い方向性を持つ足
        df['Body_Ratio'] = (df['Close'] - df['Open']).abs() / (df['High'] - df['Low'] + 1e-9)

        # --- 3. セッションレンジ (14:00 - 16:00) の算出 ---
        df['date'] = df.index.date
        df['hour'] = df.index.hour
        
        is_range_period = (df['hour'] >= 14) & (df['hour'] < 16)
        
        df['temp_high'] = np.where(is_range_period, df['High'], np.nan)
        df['temp_low'] = np.where(is_range_period, df['Low'], np.nan)
        
        df['range_high'] = df.groupby('date')['temp_high'].transform('max')
        df['range_low'] = df.groupby('date')['temp_low'].transform('min')
        
        # 16:00以降のみ参照可能
        df['range_high'] = np.where(df['hour'] >= 16, df['range_high'], np.nan)
        df['range_low'] = np.where(df['hour'] >= 16, df['range_low'], np.nan)
        
        df.drop(columns=['date', 'hour', 'temp_high', 'temp_low'], inplace=True)
        
        return df

    def generate_signal(self, df):
        row = df.iloc[-1]
        
        # 【クオンツ調整】執行時間を 16:00 - 18:00 に限定し、最高効率の窓のみを狙う
        current_hour = df.index[-1].hour
        if not (16 <= current_hour < 18):
            return None

        if pd.isna(row['range_high']) or pd.isna(row['HTF_EMA200']) or pd.isna(row['ADX_14']):
            return None

        # --- 判定ロジック (超厳格フィルタリング) ---
        # 1. トレンドの完全同期 (1H EMA200 < 5m EMA50 < Close)
        htf_bullish = row['HTF_Close'] > row['HTF_EMA200']
        ltf_bullish = row['Close'] > row['LTF_EMA50']
        
        htf_bearish = row['HTF_Close'] < row['HTF_EMA200']
        ltf_bearish = row['Close'] < row['LTF_EMA50']
        
        # 2. トレンド強度とボラティリティの厳格化 (ADX 30+, ATR 1.3x)
        strength_ok = row['ADX_14'] > 30
        volatility_ok = row['ATR_14'] > (row['ATR_MA20'] * 1.3)
        
        # 3. キャンドルクオリティ (実体比率 60% 以上)
        quality_ok = row['Body_Ratio'] > 0.6
        
        # 4. RSIモメンタム確定 (60以上で強気 / 40以下で弱気)
        rsi_buy_ok = row['RSI_14'] > 60
        rsi_sell_ok = row['RSI_14'] < 40
        
        # 5. セッションブレイクアウト条件
        breakout_high = row['Close'] > row['range_high']
        breakout_low = row['Close'] < row['range_low']

        # --- シグナル確定 ---
        if breakout_high and htf_bullish and ltf_bullish and strength_ok and volatility_ok and quality_ok and rsi_buy_ok:
            return 'BUY'
        
        if breakout_low and htf_bearish and ltf_bearish and strength_ok and volatility_ok and quality_ok and rsi_sell_ok:
            return 'SELL'
            
        return None

MQL5実装コード (.mq5)

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

#include <Trade\Trade.mqh>

//--- 入力パラメータ
input double InpLotSize = 0.1;          // ロットサイズ
input int    InpTP_Pips = 600;          // 利確 (points)
input int    InpSL_Pips = 200;          // 損切 (points)
input int    InpTrailStart = 200;       // トレーリング開始 (points)
input int    InpADX_Threshold = 30;     // ADX閾値
input double InpBodyRatio = 0.6;        // キャンドル実体比率
input int    InpRSI_Upper = 60;         // RSI強気境界
input int    InpRSI_Lower = 40;         // RSI弱気境界

//--- グローバル変数
CTrade trade;
int handle_ema200_1h, handle_ema50_5m, handle_adx, handle_rsi, handle_atr;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handle_ema200_1h = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
    handle_ema50_5m  = iMA(_Symbol, PERIOD_M5, 50, 0, MODE_EMA, PRICE_CLOSE);
    handle_adx       = iADX(_Symbol, PERIOD_M5, 14);
    handle_rsi       = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
    handle_atr       = iATR(_Symbol, PERIOD_M5, 14);
    
    if(handle_ema200_1h == INVALID_HANDLE || handle_ema50_5m == INVALID_HANDLE) return(INIT_FAILED);
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    MqlDateTime dt;
    TimeCurrent(dt);
    
    // 執行時間の限定 (16:00 - 18:00)
    if(dt.hour < 16 || dt.hour >= 18) return;

    // ポジション確認
    if(PositionsTotal() > 0) 
    {
        ApplyTrailingStop();
        return;
    }

    // 指標値の取得
    double ema200_1h[], ema50_5m[], adx[], rsi[], atr[];
    ArraySetAsSeries(ema200_1h, true);
    ArraySetAsSeries(ema50_5m, true);
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(rsi, true);
    ArraySetAsSeries(atr, true);
    
    CopyBuffer(handle_ema200_1h, 0, 1, 1, ema200_1h);
    CopyBuffer(handle_ema50_5m, 0, 0, 1, ema50_5m);
    CopyBuffer(handle_adx, 0, 0, 1, adx);
    CopyBuffer(handle_rsi, 0, 0, 1, rsi);
    CopyBuffer(handle_atr, 0, 0, 20, atr);

    // セッションレンジ (14:00 - 16:00) の算出
    double range_high = -1, range_low = 999999;
    MqlRates rates[];
    ArraySetAsSeries(rates, true);
    int copied = CopyRates(_Symbol, PERIOD_M5, 0, 500, rates);
    
    for(int i=0; i<copied; i++)
    {
        MqlDateTime rdt;
        TimeToStruct(rates[i].time, rdt);
        if(rdt.day != dt.day) break;
        if(rdt.hour >= 14 && rdt.hour < 16)
        {
            if(rates[i].high > range_high) range_high = rates[i].high;
            if(rates[i].low < range_low) range_low = rates[i].low;
        }
    }

    // キャンドル実体比率の計算
    double body = MathAbs(rates[0].close - rates[0].open);
    double range = rates[0].high - rates[0].low + 0.00001;
    double body_ratio = body / range;

    // ATR平均の計算 (過去20本)
    double atr_sum = 0;
    for(int i=0; i<20; i++) atr_sum += atr[i];
    double atr_ma20 = atr_sum / 20;

    // エントリー判定
    double close = rates[0].close;
    bool strength_ok = adx[0] > InpADX_Threshold;
    bool vol_ok = atr[0] > (atr_ma20 * 1.3);
    bool quality_ok = body_ratio > InpBodyRatio;

    // BUY条件
    if(close > range_high && close > ema200_1h[0] && close > ema50_5m[0] && 
       strength_ok && vol_ok && quality_ok && rsi[0] > InpRSI_Upper)
    {
        double sl = close - InpSL_Pips * _Point;
        double tp = close + InpTP_Pips * _Point;
        trade.Buy(InpLotSize, _Symbol, close, sl, tp);
    }
    // SELL条件
    else if(close < range_low && close < ema200_1h[0] && close < ema50_5m[0] && 
             strength_ok && vol_ok && quality_ok && rsi[0] < InpRSI_Lower)
    {
        double sl = close + InpSL_Pips * _Point;
        double tp = close - InpTP_Pips * _Point;
        trade.Sell(InpLotSize, _Symbol, close, sl, tp);
    }
}

//+------------------------------------------------------------------+
//| トレーリングストップ関数                                            |
//+------------------------------------------------------------------+
void ApplyTrailingStop()
{
    for(int i=PositionsTotal()-1; i>=0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket))
        {
            double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
            double current_price = PositionGetDouble(POSITION_PRICE_CURRENT);
            double stop_loss = PositionGetDouble(POSITION_SL);
            
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
            {
                if(current_price - open_price > InpTrailStart * _Point)
                {
                    double new_sl = current_price - InpSL_Pips * _Point;
                    if(new_sl > stop_loss) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
                }
            }
            else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
            {
                if(open_price - current_price > InpTrailStart * _Point)
                {
                    double new_sl = current_price + InpSL_Pips * _Point;
                    if(new_sl < stop_loss || stop_loss == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}