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

もともと私は、アジアレンジのブレイクアウト手法に傾倒していました。しかし、多くの「だまし」に資金を削られました。なぜ価格は節目を抜けた直後に反転するのか。その答えを求めて、リクイディティ・グラブ(流動性の確保)という概念に辿り着きました。大口投資家が仕掛ける罠を逆手に取るロジックを、試行錯誤して構築したのが本システムです。

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

このコードをベースにすれば、MT5のEA開発にかかる数百時間を節約できます。堅牢なリスク管理と時間フィルターの組み込み方は、最高の土台となるはずです。自分だけのオリジナルEAを作るための研究素材として活用してください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

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

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

この成績を見て、物足りなさを感じる方は多いはずです。PF1.21という数字は、決して高いとは言えません。最大の要因は、エントリー条件を厳格にしすぎたことです。

10年で88回という取引数は、あまりに少なすぎます。ボラティリティとトレンド強度のフィルターを重ねた結果、多くのチャンスを逃しました。また、勝率42%という低さは、リクイディティ・グラブの判定が単純であるためです。相場のノイズを拾い、損切りにかかる局面が散見されました。

完璧を求めてフィルターを増やしすぎると、取引回数が激減し、資金効率が悪化するという典型的な例です。

ロジックの技術的詳細

本ロジックは、マルチタイムフレーム(MTF)分析と時間帯制限を組み合わせた戦略です。

要素 設定・条件 目的
メイン時間足 5分足 精緻なエントリータイミングの計測
トレンド判定 1時間足 EMA200 / EMA50 長期的な方向性の合致を確認
ボラティリティ 15分足 ATR > ATR(10)平均 市場の活性化を確認し、停滞相場を排除
トレンド強度 15分足 ADX > 25 明確な方向感がある時のみに限定
時間フィルター ロンドン時間 (16:00 - 19:00) 流動性が高まる時間帯に集中
エントリー条件 アジアレンジ突破後の反転 + 強力な確定足 大口の罠(Fakeout)を検知して逆張り

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

このロジックを「化けさせる」には、固定的なフィルターから動的な判断への移行が必要です。

例えば、ADXの閾値を固定せず、直近の相場環境に合わせて最適化させる手法があります。また、ドルインデックス(DXY)などの相関通貨ペアをフィルターに加えれば、精度はさらに向上するでしょう。

プロの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 LondonLiquidityGrabStrategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ調整】PF 0.68 -> 1.20+ への改善
        # ペイオフレシオが1.00に低下しているため、期待値を上げるためTPを現実的な範囲に下げ、勝率を底上げ
        # トレーリングストップの開始点を早め、利益確定の確実性を高める
        super().__init__(
            name="LondonLiquidityGrab_V3_Optimized", 
            default_tp_pips=40.0, 
            default_sl_pips=20.0, 
            enable_trailing_stop=True, 
            trail_start_pips=12.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        【絶対準拠ルール 8】完全ベクトル演算による高速計算実装
        """
        # 1. MTFトレンド判定 (1時間足 EMA 200 + EMA 50)
        # ルックアヘッドバイアス排除のため shift(1) を徹底
        df_1h = df[['Close']].resample('1h').last().shift(1)
        df_1h['ema_200'] = ta.ema(df_1h['Close'], length=200)
        df_1h['ema_50'] = ta.ema(df_1h['Close'], length=50)
        df = df.join(df_1h[['ema_200', 'ema_50']].reindex(df.index, method='ffill'))

        # 2. 短期モメンタムフィルター (5分足 EMA 20)
        # 価格が短期EMAより上/下にあることを条件に加え、反転の勢いを確認し「だまし」を排除
        df['ema_20'] = ta.ema(df['Close'], length=20)

        # 3. ボラティリティ・強度判定 (15分足 ATR, ADX)
        resampled_15m = df[['Open', 'High', 'Low', 'Close']].resample('15min').agg({
            'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'
        }).shift(1)
        
        resampled_15m['atr'] = ta.atr(resampled_15m['High'], resampled_15m['Low'], resampled_15m['Close'], length=14)
        # ADX期間を14に回帰させつつ、閾値を25に引き上げ、明確なトレンド時のみに限定
        adx_df = ta.adx(resampled_15m['High'], resampled_15m['Low'], resampled_15m['Close'], length=14)
        resampled_15m['adx'] = adx_df['ADX_14']
        
        df = df.join(resampled_15m[['atr', 'adx']].reindex(df.index, method='ffill'))

        # 4. アジアレンジの算出 (09:00 - 15:00)
        df['date'] = df.index.date
        asian_mask = (df.index.hour >= 9) & (df.index.hour < 15)
        
        asian_highs = df.loc[asian_mask, 'High'].groupby(df['date']).max()
        asian_lows = df.loc[asian_mask, 'Low'].groupby(df['date']).min()
        
        df['asian_high'] = df['date'].map(asian_highs)
        df['asian_low'] = df['date'].map(asian_lows)
        
        # 未来参照排除
        df.loc[df.index.hour < 15, ['asian_high', 'asian_low']] = np.nan
        df['asian_high'] = df['asian_high'].ffill()
        df['asian_low'] = df['asian_low'].ffill()

        # 5. フィルター条件の外科的調整
        # ATRの移動平均期間を短縮(10)し、直近のボラティリティ変化に敏感に反応させる
        df['atr_sma'] = df['atr'].rolling(10).mean()
        df['volatility_ok'] = df['atr'] > df['atr_sma']
        
        # ADX閾値を25に設定し、レンジ相場でのエントリーを厳格に排除
        df['strength_ok'] = df['adx'] > 25
        
        # 1時間足のパーフェクトオーダー条件
        df['trend_up'] = (df['Close'] > df['ema_50']) & (df['ema_50'] > df['ema_200'])
        df['trend_down'] = (df['Close'] < df['ema_50']) & (df['ema_50'] < df['ema_200'])
        
        # ロンドン時間フィルター (16:00 - 19:00に集中させ、効率を最大化)
        df['london_time'] = (df.index.hour >= 16) & (df.index.hour < 19)

        # 6. リクイディティ・グラブ(Fakeout)の検知ロジック強化
        # 単なる価格の戻りではなく、「強い反転キャンドル(実体が下ヒゲ/上ヒゲより大きい)」を条件に追加
        body_size = (df['Close'] - df['Open']).abs()
        candle_range = df['High'] - df['Low']
        strong_reversal = body_size > (candle_range * 0.4) # 実体がレンジの40%以上を占める強い確定足

        # Long: 前足Low < AsianLow AND 今足Close > AsianLow AND 陽線 AND 強力な反転 AND 短期EMA超え
        df['grab_long'] = (df['Low'].shift(1) < df['asian_low']) & \
                          (df['Close'] > df['asian_low']) & \
                          (df['Close'] > df['Open']) & \
                          strong_reversal & \
                          (df['Close'] > df['ema_20'])
        
        # Short: 前足High > AsianHigh AND 今足Close < AsianHigh AND 陰線 AND 強力な反転 AND 短期EMA割れ
        df['grab_short'] = (df['High'].shift(1) > df['asian_high']) & \
                           (df['Close'] < df['asian_high']) & \
                           (df['Close'] < df['Open']) & \
                           strong_reversal & \
                           (df['Close'] < df['ema_20'])

        return df

    def generate_signal(self, df):
        """
        【絶対準拠ルール 3】最新行に基づいたシグナル判定
        """
        if len(df) < 2:
            return None
            
        last = df.iloc[-1]
        
        # 共通フィルター: ロンドン時間 && ボラティリティ拡大 && トレンド強度あり
        if not (last['london_time'] and last['volatility_ok'] and last['strength_ok']):
            return None

        # Longシグナル: 1Hトレンド合致 && 強化版リクイディティ・グラブ発生
        if last['trend_up'] and last['grab_long']:
            return 'BUY'
            
        # Shortシグナル: 1Hトレンド合致 && 強化版リクイディティ・グラブ発生
        if last['trend_down'] and last['grab_short']:
            return 'SELL'
            
        return None

MQL5コード(MT5用)

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

#include <Trade\Trade.mqh>

// 入力パラメータ
input double   InpLotSize = 0.1;              // ロット数
input int      InpTP = 400;                   // 利確 (points)
input int      InpSL = 200;                   // 損切り (points)
input int      InpTrailStart = 120;           // トレーリング開始 (points)
input int      InpEMA_Fast = 50;              // 1H EMA Fast
input int      InpEMA_Slow = 200;             // 1H EMA Slow
input int      InpEMA_Short = 20;             // 5M EMA Short
input int      InpADX_Period = 14;            // ADX Period
input int      InpATR_Period = 14;            // ATR Period

// グローバル変数
CTrade trade;
int handle_ema_fast_1h, handle_ema_slow_1h, handle_ema_short_5m, handle_adx_15m, handle_atr_15m;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   handle_ema_fast_1h = iMA(_Symbol, PERIOD_H1, InpEMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema_slow_1h = iMA(_Symbol, PERIOD_H1, InpEMA_Slow, 0, MODE_EMA, PRICE_CLOSE);
   handle_ema_short_5m = iMA(_Symbol, PERIOD_M5, InpEMA_Short, 0, MODE_EMA, PRICE_CLOSE);
   handle_adx_15m = iADX(_Symbol, PERIOD_M15, InpADX_Period);
   handle_atr_15m = iATR(_Symbol, PERIOD_M15, InpATR_Period);
   
   return(INIT_SUCCEEDED);
}

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

   MqlDateTime dt;
   TimeCurrent(dt);
   
   // ロンドン時間フィルター (16:00 - 19:00)
   if(dt.hour < 16 || dt.hour >= 19) return;

   // 指標値の取得
   double ema_fast_1h = GetMA(handle_ema_fast_1h, 1);
   double ema_slow_1h = GetMA(handle_ema_slow_1h, 1);
   double ema_short_5m = GetMA(handle_ema_short_5m, 1);
   double adx_15m = GetADX(handle_adx_15m, 1);
   double atr_15m = GetATR(handle_atr_15m, 1);
   
   // ATRの10期間平均を算出
   double atr_sum = 0;
   for(int i=1; i<=10; i++) atr_sum += GetATR(handle_atr_15m, i);
   double atr_sma = atr_sum / 10.0;

   // ボラティリティ・強度フィルター
   if(atr_15m <= atr_sma || adx_15m <= 25) return;

   // アジアレンジ算出 (09:00 - 15:00)
   double asian_high = 0, asian_low = 0;
   GetAsianRange(asian_high, asian_low);

   // キャンドルデータの取得
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   CopyRates(_Symbol, PERIOD_M5, 0, 2, rates);

   double close0 = rates[0].close;
   double open0 = rates[0].open;
   double high0 = rates[0].high;
   double low0 = rates[0].low;
   double close1 = rates[1].close;
   double high1 = rates[1].high;
   double low1 = rates[1].low;

   // 強い反転キャンドルの判定 (実体がレンジの40%以上)
   double body_size = MathAbs(close0 - open0);
   double candle_range = high0 - low0;
   bool strong_reversal = (candle_range > 0) ? (body_size > candle_range * 0.4) : false;

   // トレンド判定
   bool trend_up = (close0 > ema_fast_1h && ema_fast_1h > ema_slow_1h);
   bool trend_down = (close0 < ema_fast_1h && ema_fast_1h < ema_slow_1h);

   // エントリーロジック
   // Long: 前足Low < AsianLow AND 今足Close > AsianLow AND 陽線 AND 反転 AND EMA20超え
   if(trend_up && low1 < asian_low && close0 > asian_low && close0 > open0 && strong_reversal && close0 > ema_short_5m)
   {
      if(PositionsTotal() == 0)
         trade.Buy(InpLotSize, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK), 
                   SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point, 
                   SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point);
   }

   // Short: 前足High > AsianHigh AND 今足Close < AsianHigh AND 陰線 AND 反転 AND EMA20割れ
   if(trend_down && high1 > asian_high && close0 < asian_high && close0 < open0 && strong_reversal && close0 < ema_short_5m)
   {
      if(PositionsTotal() == 0)
         trade.Sell(InpLotSize, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID), 
                    SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point, 
                    SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point);
   }
   
   ManageTrailingStop();
}

// --- 補助関数 ---

double GetMA(int handle, int shift) {
   double buf[];
   CopyBuffer(handle, 0, shift, 1, buf);
   return buf[0];
}

double GetADX(int handle, int shift) {
   double buf[];
   CopyBuffer(handle, 0, shift, 1, buf);
   return buf[0];
}

double GetATR(int handle, int shift) {
   double buf[];
   CopyBuffer(handle, 0, shift, 1, buf);
   return buf[0];
}

void GetAsianRange(double &high, double &low) {
   MqlRates rates[];
   ArraySetAsSeries(rates, true);
   CopyRates(_Symbol, PERIOD_M5, 0, 500, rates);
   
   high = 0; low = 999999;
   for(int i=0; i<500; i++) {
      MqlDateTime dt;
      TimeToStruct(rates[i].time, dt);
      if(dt.hour >= 9 && dt.hour < 15) {
         if(rates[i].high > high) high = rates[i].high;
         if(rates[i].low < low) low = rates[i].low;
      }
      if(dt.hour < 9) break; 
   }
}

bool IsNewBar() {
   static datetime last_bar = 0;
   datetime curr_bar = iTime(_Symbol, PERIOD_M5, 0);
   if(last_bar != curr_bar) {
      last_bar = curr_bar;
      return true;
   }
   return false;
}

void ManageTrailingStop() {
   for(int i=PositionsTotal()-1; i>=0; i--) {
      ulong ticket = PositionGetTicket(i);
      if(PositionSelectByTicket(ticket)) {
         if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
            double price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
            if(price - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point) {
               double new_sl = price - InpTrailStart * _Point;
               if(new_sl > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
            }
         } else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) {
            double price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
            if(PositionGetDouble(POSITION_PRICE_OPEN) - price > InpTrailStart * _Point) {
               double new_sl = price + InpTrailStart * _Point;
               if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
            }
         }
      }
   }
}