💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は困難な道のりです。多くのロジックが数ヶ月の運用で破綻します。今回は、10年という長い期間を生き抜いたロジックを公開します。成績は爆発的ではありません。しかし、市場の激変に耐え抜いた強靭さを持っています。

私自身、かつては単純なブレイクアウト手法に依存していました。しかし、相場の「だまし」に何度も資金を削られました。そこで、大口投資家が仕掛ける「ストップ狩り」の動きに着目しました。リクイディティ(流動性)の回収を待ってからエントリーする設計に苦心した記憶があります。

🤖
完璧を求めすぎると、カーブフィッティングの罠にハマる。あえて「ほどほどの成績」で耐える設計にしたのが正解でした。

このコードをベースにすれば、MT5のEA開発にかかる数百時間を節約できます。ゼロから構築するよりも効率的です。自分だけのオリジナルEAを作るための最高の土台として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

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

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

このロジックの最大の弱点は、取引数の少なさです。10年で75回という回数は、機会損失が非常に多いことを意味します。フィルターを厳しくしすぎた結果です。

また、PF1.47という数字は安定していますが、資産を急激に増やす力はありません。トレンドが発生しないレンジ相場では、全くエントリーしません。効率的に稼ぐには、ボラティリティの低い局面での対応策が不足しています。

ロジックの技術的詳細

本ロジックは、上位足のトレンド方向へ、短期的な流動性の回収を確認してエントリーします。

項目 設定内容 役割
時間足 5分足(USDJPY専用) 短期的な構造変化の検知
上位足フィルター H1 EMA(20) > EMA(100) 且つ 傾き正 長期的な方向性の合致
ボラティリティ ATR(10) > ATR_SMA(20) 相場の活性化を確認
トレンド強度 ADX(14) > 30 且つ 3本連続上昇 強いトレンドの発生を判定
リクイディティ 15時台の高値・安値の更新後の回帰 ストップ狩り後の反転を検知
トリガー 直近3本の高値/安値を終値でブレイク エントリータイミングの確定
決済 TP 70pips / SL 20pips リスクリワード 1:3.5 の設計
ここがポイント 単なるブレイクアウトではなく、「一度突き抜けてから戻ってきた」ことを条件にしています。これにより、大口の罠を回避し、本物のトレンドに乗る確率を高めています。

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

このロジックをさらに進化させるには、時間帯の最適化が不可欠です。現在はロンドンオープンのみに絞っています。ニューヨーク市場の特性に合わせたフィルターを追加すれば、取引数は向上します。

また、固定のTP/SLではなく、ATRに基づいた可変決済の導入を推奨します。相場のボラティリティに応じて利確幅を変えることで、PFの向上が見込めます。プロのEAは、こうした環境認識による動的な調整を組み込んでいます。

公開Pythonコード

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

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

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

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

class SessionLiquidityShiftStrategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ外科手術】PF 1.09 -> 1.20+ および RF 1.24 -> 3.00+ を目指す
        # 期待値(Payoff)を劇的に向上させるため、RR比を 1:3.5 に再設計
        # SLを20pipsにタイト化し、TPを70pipsへ拡大。
        # トレーリングストップの開始を20pips(SL分)に設定し、建値への移行を早めてDDを抑制
        super().__init__(
            name="SessionLiquidityShift_Quants_v3", 
            default_tp_pips=70.0, 
            default_sl_pips=20.0, 
            enable_trailing_stop=True, 
            trail_start_pips=20.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        だまし(False Sign)を排除するための高精度フィルタリング実装
        """
        # --- 1. 上位足(1h)トレンドフィルターの厳格化 ---
        h1_close_series = df['Close'].resample('1h').last().shift(1)
        
        h1_df = pd.DataFrame({'Close': h1_close_series})
        # トレンドの方向性だけでなく、「乖離の拡大(モメンタム)」を確認
        h1_ema_fast = ta.ema(h1_df['Close'], length=20)
        h1_ema_slow = ta.ema(h1_df['Close'], length=100)
        h1_slope = h1_ema_fast.diff(1)
        
        df['h1_close'] = h1_close_series.reindex(df.index, method='ffill')
        df['h1_ema_fast'] = h1_ema_fast.reindex(df.index, method='ffill')
        df['h1_ema_slow'] = h1_ema_slow.reindex(df.index, method='ffill')
        df['h1_slope'] = h1_slope.reindex(df.index, method='ffill')

        # --- 2. ボラティリティ・トレンド強度フィルター ---
        # ATRの期間を短縮して直近のボラティリティ変化に敏感に反応させる
        df.ta.atr(length=10, append=True)
        atr_col = 'ATRr_10' 
        df['atr_sma'] = df[atr_col].rolling(window=20).mean()
        
        # ADX閾値を30に維持しつつ、ADXが「上昇トレンドにあること」を厳格に判定
        df.ta.adx(length=14, append=True)
        adx_col = 'ADX_14'
        # 直近3本のADXがすべて上昇していることを条件に加える(だまし排除)
        df['adx_strong_rising'] = (df[adx_col] > df[adx_col].shift(1)) & \
                                 (df[adx_col].shift(1) > df[adx_col].shift(2))

        # --- 3. セッションリクイディティ・ロジックの再構築 ---
        # プレレンジ定義 (15:00 - 16:00)
        is_pre_open = (df.index.hour == 15)
        df['pre_high'] = df['High'].where(is_pre_open).groupby(df.index.date).transform('max')
        df['pre_low'] = df['Low'].where(is_pre_open).groupby(df.index.date).transform('min')

        # 【重要】単なる下抜け(dipped)ではなく、「下抜けてから戻した(Reclaim)」ことを検知
        # これにより、単なるトレンド崩壊ではなく「ストップ狩り後の反転」を特定する
        df['grab_long'] = (df['Low'] < df['pre_low']) & (df['Close'] > df['pre_low'])
        df['grab_short'] = (df['High'] > df['pre_high']) & (df['Close'] < df['pre_high'])
        
        # 過去12本(1時間)以内にリクイディティ・グラブが発生したかを判定
        df['has_grabbed_long'] = df['grab_long'].rolling(window=12).max() > 0
        df['has_grabbed_short'] = df['grab_short'].rolling(window=12).max() > 0

        # --- 4. エントリー条件のベクトル判定 ---
        # 時間帯: ロンドンオープン (16:00 - 19:00)
        df['session_ok'] = (df.index.hour >= 16) & (df.index.hour <= 19)
        
        # 環境: ボラティリティ拡大 且つ ADXが強く上昇中
        df['regime_ok'] = (df[atr_col] > df['atr_sma']) & (df[adx_col] > 30) & (df['adx_strong_rising'])
        
        # MTF同期: H1でEMA20 > EMA100 且つ 傾きが正
        df['trend_long'] = (df['h1_ema_fast'] > df['h1_ema_slow']) & (df['h1_slope'] > 0)
        df['trend_short'] = (df['h1_ema_fast'] < df['h1_ema_slow']) & (df['h1_slope'] < 0)

        # トリガー: 直近3本の高値/安値を終値で明確にブレイク (ノイズ除去)
        df['trigger_long'] = (df['Close'] > df['High'].shift(1).rolling(window=3).max())
        df['trigger_short'] = (df['Close'] < df['Low'].shift(1).rolling(window=3).min())

        return df

    def generate_signal(self, df):
        """
        外科的に調整されたフィルタリング条件に基づくシグナル生成
        """
        if len(df) < 20:
            return None
        
        last = df.iloc[-1]
        
        # ロング: セッション内 & 強トレンド & H1整合性 & 下値リクイディティ回収済 & 構造変化
        if (last['session_ok'] and 
            last['regime_ok'] and 
            last['trend_long'] and 
            last['has_grabbed_long'] and 
            last['trigger_long']):
            return 'BUY'
        
        # ショート: セッション内 & 強トレンド & H1整合性 & 上値リクイディティ回収済 & 構造変化
        if (last['session_ok'] and 
            last['regime_ok'] and 
            last['trend_short'] and 
            last['has_grabbed_short'] and 
            last['trigger_short']):
            return 'SELL'
            
        return None

MT5用 MQL5実装コード

//+------------------------------------------------------------------+
//|                                  SessionLiquidityShift_Quants.mq5|
//|                                  Copyright 2026, System Trader   |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, System Trader"
#property link      "https://your-blog-link.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

// 入力パラメータ
input int      InpEMA_Fast = 20;          // H1 EMA Fast
input int      InpEMA_Slow = 100;         // H1 EMA Slow
input int      InpADX_Period = 14;        // ADX Period
input int      InpATR_Period = 10;        // ATR Period
input double   InpTP_Pips = 70.0;         // Take Profit (Pips)
input double   InpSL_Pips = 20.0;         // Stop Loss (Pips)
input double   InpTrailStart = 20.0;      // Trailing Start (Pips)
input double   InpLotSize = 0.1;          // Lot Size

// グローバル変数
int      handle_ema_fast, handle_ema_slow, handle_adx, handle_atr;
CTrade   trade;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handle_ema_fast = iMA(_Symbol, PERIOD_H1, InpEMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema_slow = iMA(_Symbol, PERIOD_H1, InpEMA_Slow, 0, MODE_EMA, PRICE_CLOSE);
   handle_adx      = iADX(_Symbol, PERIOD_CURRENT, InpADX_Period);
   handle_atr      = iATR(_Symbol, PERIOD_CURRENT, InpATR_Period);
   
   if(handle_ema_fast == INVALID_HANDLE || handle_ema_slow == INVALID_HANDLE || 
      handle_adx == INVALID_HANDLE || handle_atr == INVALID_HANDLE) return(INIT_FAILED);
      
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
   // 新しい足の確定時に判定
   if(!isNewBar()) return;

   MqlDateTime dt;
   TimeCurrent(dt);
   
   // 1. セッション判定 (16:00 - 19:00)
   if(dt.hour < 16 || dt.hour > 19) return;

   // 2. H1トレンド判定
   double ema_f[2], ema_s[2];
   CopyBuffer(handle_ema_fast, 0, 1, 2, ema_f);
   CopyBuffer(handle_ema_slow, 0, 1, 2, ema_s);
   bool trend_long = (ema_f[0] > ema_s[0]) && (ema_f[0] > ema_f[1]);
   bool trend_short = (ema_f[0] < ema_s[0]) && (ema_f[0] < ema_f[1]);

   // 3. ボラティリティ・ADX判定
   double adx[3], atr[21];
   CopyBuffer(handle_adx, 0, 1, 3, adx);
   CopyBuffer(handle_atr, 0, 1, 21, atr);
   
   double atr_sma = 0;
   for(int i=0; i<20; i++) atr_sma += atr[i];
   atr_sma /= 20.0;
   
   bool regime_ok = (atr[0] > atr_sma) && (adx[0] > 30) && (adx[0] > adx[1]) && (adx[1] > adx[2]);
   if(!regime_ok) return;

   // 4. リクイディティ判定 (15時台のレンジ)
   double pre_high = -1, pre_low = 999999;
   MqlRates rates[];
   int copied = CopyRates(_Symbol, PERIOD_M5, 0, 288, rates); // 過去12時間分
   
   for(int i=0; i<copied; i++) {
      MqlDateTime r_dt;
      TimeToStruct(rates[i].time, r_dt);
      if(r_dt.day == dt.day && r_dt.hour == 15) {
         if(rates[i].high > pre_high) pre_high = rates[i].high;
         if(rates[i].low < pre_low) pre_low = rates[i].low;
      }
   }

   // リクイディティ・グラブ確認 (過去12本分)
   bool has_grabbed_long = false;
   bool has_grabbed_short = false;
   for(int i=1; i<=12; i++) {
      if(rates[copied-1-i].low < pre_low && rates[copied-1-i].close > pre_low) has_grabbed_long = true;
      if(rates[copied-1-i].high > pre_high && rates[copied-1-i].close < pre_high) has_grabbed_short = true;
   }

   // 5. トリガー判定 (直近3本の高値安値ブレイク)
   double highest_3 = MathMax(rates[copied-2].high, MathMax(rates[copied-3].high, rates[copied-4].high));
   double lowest_3 = MathMin(rates[copied-2].low, MathMin(rates[copied-3].low, rates[copied-4].low));
   
   double close_now = rates[copied-1].close;

   // エントリー実行
   if(trend_long && has_grabbed_long && close_now > highest_3) {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL_Pips * _Point * 10;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP_Pips * _Point * 10;
      trade.Buy(InpLotSize, _Symbol, 0, sl, tp);
   }
   else if(trend_short && has_grabbed_short && close_now < lowest_3) {
      double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL_Pips * _Point * 10;
      double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP_Pips * _Point * 10;
      trade.Sell(InpLotSize, _Symbol, 0, sl, tp);
   }
   
   ApplyTrailingStop();
}

// トレーリングストップ関数
void ApplyTrailingStop() {
   for(int i=PositionsTotal()-1; i>=0; i--) {
      if(PositionGetSymbol(i) == _Symbol) {
         double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
         double current_sl = PositionGetDouble(POSITION_SL);
         double current_bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
         double current_ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
         
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
            if(current_bid - open_price > InpTrailStart * _Point * 10) {
               double new_sl = current_bid - InpSL_Pips * _Point * 10;
               if(new_sl > current_sl) trade.PositionModify(PositionGetTicket(i), new_sl, PositionGetDouble(POSITION_TP));
            }
         } else {
            if(open_price - current_ask > InpTrailStart * _Point * 10) {
               double new_sl = current_ask + InpSL_Pips * _Point * 10;
               if(new_sl < current_sl || current_sl == 0) trade.PositionModify(PositionGetTicket(i), new_sl, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}

bool isNewBar() {
   static datetime last_time = 0;
   datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
   if(last_time != current_time) {
      last_time = current_time;
      return true;
   }
   return false;
}