💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は困難な道です。多くの人が最適化の罠に落ち、実運用で破綻します。今回は、10年という全期間のバックテストを破綻せず生き抜いたロジックを紹介します。劇的な爆益はありません。しかし、堅実な生存能力を持った研究素材としての価値があります。

もともとは、ブレイクアウト手法の「だまし」に絶望したことが開発のきっかけでした。多くのインジケーターを組み合わせても、レンジ相場では資産を削られます。そこで私は、時間帯とボラティリティを極限まで絞り込む手法を模索しました。試行錯誤を繰り返し、ようやく生き残れる条件に辿り着いた経緯があります。

🤖
「勝率を追い求めるのではなく、負けない時間帯を定義することに集中しました」

このコードを手に入れることで、MT5のEA開発に費やす数百時間を節約できます。ゼロからロジックを組む手間は不要です。堅牢な土台があるため、ここから独自のフィルターを加えるだけで、あなただけのオリジナルEAを構築できるはずです。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

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

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

このロジックの最大の弱点は、取引回数の少なさです。10年で140回という数字は、年平均に直すとわずか14回となります。チャンスを絞りすぎた結果、資金効率が極めて低い状態です。

また、PF1.54という数値は合格点ですが、爆発力に欠けます。ボラティリティの急拡大を待つため、長い停滞期が発生します。精神的な忍耐力が求められる設計であり、短期的に利益を求める方には不向きな仕様と言えます。

ロジックの技術的詳細

本ロジックは、ロンドン市場開始時のモメンタムを狙い撃つスナイパー形式です。

要素 設定内容 役割
ベース時間足 5分足 エントリータイミングの判定
環境認識足 1時間足 (EMA200) 長期トレンドの方向性一致を確認
時間帯制限 16:00 〜 18:30 ボラティリティ最大局面への凝縮
ボラティリティ ATR(14) > ATR_MA(100) * 1.5 異常な値動きが発生しているか判定
トレンド強度 ADX(14) > 25 且つ 上昇中 勢いが加速していることを確認
モメンタム RSI(9) > 60 または < 40 明確な方向への圧力があるか判定
ブレイク判定 ドンチアンチャネル(20) 直近高値・安値の明確な突破を検知

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

このコードは「未完成の原石」です。さらなる進化のためには、以下の改善案が考えられます。

  • 時間帯の拡張: ニューヨーク市場の主要時間帯にも適用できるか検証すること。
  • ボラティリティ閾値の動的化: 固定の1.5倍ではなく、相場状況に合わせて変動させること。
  • 出口戦略の精緻化: 現在の固定TP/SLではなく、ATRに基づいた可変幅を採用すること。
プロの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 LondonOpenMomentumBreakout(BaseStrategy):
    def __init__(self):
        # 【クオンツ外科処置:利益効率の極大化】
        # PF 1.05 → 1.20+ および リカバリーファクターの劇的改善のため、
        # リスクリワード比を 4:1 (80/20) へ拡大し、期待値を数学的に底上げする。
        # トレーリングストップの開始点を引き上げ、利益を最大限に伸ばす設計に変更。
        super().__init__(
            name="LondonOpenMomentumBreakout", 
            default_tp_pips=80.0, 
            default_sl_pips=20.0, 
            enable_trailing_stop=True, 
            trail_start_pips=30.0
        )
        # 【絶対準拠】時間足定義
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        """
        だまし(False Signal)を徹底的に排除するための高精度フィルタリング演算
        """
        # --- 1. 上位足(1h)の環境認識(トレンドの整合性を厳格化) ---
        h1_close_series = df['Close'].resample('1h').last()
        # 長期トレンドの基準としてEMA200を維持しつつ、方向性を確定させる
        h1_ema_series = ta.ema(h1_close_series, length=200)
        
        df['ema200_1h'] = h1_ema_series.shift(1).reindex(df.index, method='ffill')
        df['close_1h'] = h1_close_series.shift(1).reindex(df.index, method='ffill')

        # --- 2. 5分足のテクニカル計算 ---
        # ボラティリティ (ATR)
        df.ta.atr(length=14, append=True) # ATRr_14
        
        # トレンド強度 (ADX)
        df.ta.adx(length=14, append=True) # ADX_14
        
        # モメンタム (RSI) - 周期を14から9へ短縮し、直近の反応速度を向上
        df.ta.rsi(length=9, append=True) # RSI_9
        
        # ドンチアンチャネル - 期間を20に最適化し、ブレイクの鮮度を重視
        df['DCH_20'] = df['High'].rolling(window=20).max()
        df['DCL_20'] = df['Low'].rolling(window=20).min()
        
        # ボラティリティ・ベースライン (ATRの長期平均)
        # 期間を100に設定し、真に異常なボラティリティ拡大のみを抽出
        df['atr_ma_long'] = df['ATRr_14'].rolling(window=100).mean()

        return df

    def generate_signal(self, df):
        """
        リカバリーファクター 3.0 突破を目指すスナイパー形式のエントリー判定
        """
        if len(df) < 200:
            return None

        last = df.iloc[-1]
        prev = df.iloc[-2]
        prev2 = df.iloc[-3]

        # --- A. 時間帯フィルター (最盛期への凝縮) ---
        # ロンドンオープン直後の最もボラティリティが高い 16:00 - 17:30 に限定
        # 時間帯を絞ることで、低効率なレンジ相場での取引回数を削減しPFを向上させる
        current_hour = df.index[-1].hour
        current_minute = df.index[-1].minute
        if not (16 <= current_hour < 18):
            if current_hour == 18 and current_minute < 30:
                pass # 18:30まで許容
            else:
                return None

        # --- B. 高精度フィルター (だまし排除の外科処置) ---
        # 1. ボラティリティの爆発判定: ATRが長期平均の 1.5倍 を超えていること
        volatility_ok = (last['ATRr_14'] > last['atr_ma_long'] * 1.5)
        
        # 2. トレンド強度の「上昇」判定: ADX > 25 かつ ADXが前本より上昇していること
        # (単なる数値ではなく、勢いが加速している局面のみを抽出)
        trend_strong = (last['ADX_14'] > 25) and (last['ADX_14'] > prev['ADX_14'])
        
        # 3. RSIによるモメンタムの明確化: 
        # Long: RSI > 60 (強い上昇圧力) / Short: RSI < 40 (強い下降圧力)
        rsi_long_ok = (last['RSI_9'] > 60)
        rsi_short_ok = (last['RSI_9'] < 40)
        
        if not (volatility_ok and trend_strong):
            return None

        # --- C. エントリーロジック (厳格なブレイクアウト判定) ---
        # ロング条件: 
        # 1. 1hトレンドが強気 (Close > EMA200)
        # 2. 5m終値が前本のドンチアン上限を明確に超え、かつ前本が陽線であること
        if (last['close_1h'] > last['ema200_1h']) and \
           (last['Close'] > prev['DCH_20']) and \
           (prev['Close'] > prev['Open']) and \
           rsi_long_ok:
            return 'BUY'

        # ショート条件: 
        # 1. 1hトレンドが弱気 (Close < EMA200)
        # 2. 5m終値が前本のドンチアン下限を明確に下回り、かつ前本が陰線であること
        elif (last['close_1h'] < last['ema200_1h']) and \
             (last['Close'] < prev['DCL_20']) and \
             (prev['Close'] < prev['Open']) and \
             rsi_short_ok:
            return 'SELL'

        return None

MQL5実装コード

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

// 入力パラメータ
input double   InpLotSize = 0.1;          // ロットサイズ
input int      InpTP_Pips = 800;          // 利確 (Points)
input int      InpSL_Pips = 200;          // 損切 (Points)
input int      InpTrailStart = 300;       // トレーリング開始 (Points)
input int      InpDonchianPeriod = 20;    // ドンチアン期間
input int      InpATRPeriod = 14;         // ATR期間
input int      InpATRMAPeriod = 100;      // ATR平均期間
input int      InpRSIPeriod = 9;          // RSI期間
input int      InpADXPeriod = 14;         // ADX期間

// ハンドル
int handle_h1_ema, handle_m5_atr, handle_m5_adx, handle_m5_rsi;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handle_h1_ema = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
    handle_m5_atr = iATR(_Symbol, PERIOD_M5, InpATRPeriod);
    handle_m5_adx = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
    handle_m5_rsi = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);

    if(handle_h1_ema == INVALID_HANDLE || handle_m5_atr == INVALID_HANDLE || 
       handle_m5_adx == INVALID_HANDLE || handle_m5_rsi == INVALID_HANDLE) 
    {
        Print("ハンドルの作成に失敗しました");
        return(INIT_FAILED);
    }
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 新しい足の確定時のみ判定
    static datetime last_bar;
    datetime current_bar = iTime(_Symbol, PERIOD_M5, 0);
    if(last_bar == current_bar) return;
    last_bar = current_bar;

    // 時間帯フィルター (16:00 - 18:30)
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    bool time_ok = (dt.hour >= 16 && dt.hour < 18) || (dt.hour == 18 && dt.min < 30);
    if(!time_ok) return;

    // インジケーター値の取得
    double ema_h1[], atr_m5[], adx_m5[], rsi_m5[], close_h1[];
    CopyBuffer(handle_h1_ema, 0, 1, 1, ema_h1);
    CopyBuffer(handle_m5_atr, 0, 0, InpATRMAPeriod + 1, atr_m5);
    CopyBuffer(handle_m5_adx, 0, 0, 2, adx_m5);
    CopyBuffer(handle_m5_rsi, 0, 1, 1, rsi_m5);
    CopyClose(_Symbol, PERIOD_H1, 1, 1, close_h1);

    // ATRの長期平均計算
    double atr_sum = 0;
    for(int i=0; i<InpATRMAPeriod; i++) atr_sum += atr_m5[i];
    double atr_ma = atr_sum / InpATRMAPeriod;

    // ドンチアンチャネル計算
    double high_20 = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, InpDonchianPeriod, 1));
    double low_20 = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, InpDonchianPeriod, 1));

    // 条件判定
    bool vol_ok = (atr_m5[0] > atr_ma * 1.5);
    bool trend_strong = (adx_m5[0] > 25 && adx_m5[0] > adx_m5[1]);
    
    if(!vol_ok || !trend_strong) return;

    double close_m5 = iClose(_Symbol, PERIOD_M5, 1);
    double open_m5 = iOpen(_Symbol, PERIOD_M5, 1);

    // ロングエントリー
    if(close_h1[0] > ema_h1[0] && close_m5 > high_20 && close_m5 > open_m5 && rsi_m5[0] > 60)
    {
        TradeExecute(ORDER_TYPE_BUY);
    }
    // ショートエントリー
    else if(close_h1[0] < ema_h1[0] && close_m5 < low_20 && close_m5 < open_m5 && rsi_m5[0] < 40)
    {
        TradeExecute(ORDER_TYPE_SELL);
    }

    ManageTrailingStop();
}

// 取引実行関数
void TradeExecute(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;
    request.type_filling = ORDER_FILLING_IOC;

    OrderSend(request, result);
}

// トレーリングストップ管理
void ManageTrailingStop()
{
    for(int i = PositionsTotal() - 1; i >= 0; i--)
    {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket))
        {
            if(PositionGetInteger(POSITION_MAGIC) != 123456) continue;
            
            double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
            double current_price = PositionGetDouble(POSITION_PRICE_CURRENT);
            double current_sl = 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 > current_sl) ModifySL(ticket, new_sl);
                }
            }
            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 < current_sl || current_sl == 0) ModifySL(ticket, new_sl);
                }
            }
        }
    }
}

void ModifySL(ulong ticket, double new_sl)
{
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    request.action = TRADE_ACTION_SLTP;
    request.position = ticket;
    request.sl = new_sl;
    OrderSend(request, result);
}