💡 本ロジックの対象環境
対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
自動売買の開発は、孤独で険しい道のりです。多くの人が「聖杯」を求めて最適化に溺れ、実運用で破綻します。今回は、10年のバックテストを破綻せず生き抜いたロジックを公開します。成績は決して派手ではありません。しかし、過酷な相場を生き残ったという事実に価値があります。

もともと私は、ブレイクアウト手法の「だまし」に悩み続けていました。多くのインジケーターを組み合わせても、結局は相場のノイズに飲み込まれます。そこで、時間軸を分けた環境認識と、ボラティリティの厳選に注力しました。試行錯誤の末に辿り着いたのが、この保守的な設計です。

このコードを入手すれば、MT5のEA開発に費やす数百時間を節約できます。ゼロからロジックを組む必要はありません。本コードを土台にして、あなただけの改良を加えるのが最短ルートです。研究素材として、自由にご活用ください。

<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

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

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

このロジックの最大の弱点は、収益性の低さです。10年で170回という取引数は、あまりに少なすぎます。機会損失が非常に大きく、資金効率は最悪と言えます。

PF 1.21という数字も、プロの視点では物足りません。これは、カーブフィッティングを徹底的に排除した結果です。安全性を重視しすぎたため、利益を伸ばす局面で早すぎる決済が行われています。また、レンジ相場での待機時間が長く、退屈な運用になることは間違いありません。

🤖
稼げないコードだと思ったかもしれません。でも、10年生き残るコードを書くのは、稼ぐコードを書くよりずっと難しいのです。

ロジックの技術的詳細

本ロジックは、上位足のトレンド方向へ、短期的なボラティリティ拡大を狙ってエントリーします。

項目 設定・条件
執行時間足 5分足
環境認識足 1時間足(EMA50, EMA200, ADX)
取引時間制限 UTC 15:00 〜 23:00
ボラティリティ判定 ATR > (ATR_SMA200 * 1.1)
トレンド条件 価格 > EMA50 > EMA200 且つ ADX > 35
エントリートリガー ドンチャンチャネル(40)突破 且つ MACDヒストグラム加速
決済ルール TP: 80pips / SL: 30pips / トレーリングストップあり

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

このロジックを「化けさせる」には、エントリーフィルターの緩和が必要です。現在はADXの閾値を35と高く設定しています。ここを25程度に下げ、代わりに価格帯のサポート・レジスタンスを判定に加えるべきです。

また、固定の利確幅ではなく、ボラティリティに連動した可変TPの導入を推奨します。プロのEAは、相場の変動幅に合わせて決済指値を動かしています。この土台に「市場の周期性」という視点を加えれば、収益性は飛躍的に向上するはずです。

完璧なシステムは存在しません。しかし、破綻しない土台があれば、そこから無限に改善を積み上げることができます。

Pythonソースコード

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

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

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

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

class MTF_VBO_Strategy(BaseStrategy):
    """
    Multi-Timeframe Volatility Breakout (MTF-VBO) Strategy - High-Efficiency Quant Edition.
    Focus: Drastic improvement of Recovery Factor (RF) and Profit Factor (PF) by 
    eliminating low-probability "noise" trades and maximizing trend capture.
    """

    def __init__(self):
        # Optimization for RF and PF: 
        # 1. Increased TP to capture the "fat tail" of breakout moves.
        # 2. Adjusted SL to avoid premature stops in high-volatility environments.
        # 3. Delayed trailing stop start to allow the trade to develop, reducing 'death by a thousand cuts'.
        super().__init__(
            name="MTF-VBO_Quant_Ultra", 
            default_tp_pips=80.0, 
            default_sl_pips=30.0, 
            enable_trailing_stop=True, 
            trail_start_pips=25.0
        )
        self.base_timeframe = "15m" 
        self.vision_timeframes = ["5min", "15min", "1h"]

    def calculate_indicators(self, df):
        """
        Calculates all necessary indicators using vectorized operations.
        Rule 8: No loops. Rule 10: No Volume.
        """
        # --- 1. LTF (Execution Timeframe: 15m) Indicators ---
        # Volatility Filter: ATR and its SMA
        # Increased SMA length to 200 to create a more robust baseline for "abnormal" volatility.
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['ATR_SMA'] = ta.sma(df['ATR'], length=200) 
        
        # Donchian Channel: Increased window to 40 to filter out short-term fakeouts (False Signs).
        # This ensures we only enter on significant breakouts.
        df['DC_Upper'] = df['High'].shift(1).rolling(window=40).max()
        df['DC_Lower'] = df['Low'].shift(1).rolling(window=40).min()
        
        # MACD (12, 26, 9) - Standard momentum confirmation
        macd = ta.macd(df['Close'], fast=12, slow=26, signal=9)
        df = pd.concat([df, macd], axis=1)
        self.macd_hist_col = 'MACDh_12_26_9'

        # --- 2. HTF (Higher Timeframe: 1h) Indicators ---
        # Rule 7: Strict look-ahead bias elimination
        df_h1 = df[['Open', 'High', 'Low', 'Close']].resample('1h').last()
        
        # HTF Trend Filter: Triple Confirmation (Price > EMA50 > EMA200)
        df_h1['HTF_EMA50'] = ta.ema(df_h1['Close'], length=50)
        df_h1['HTF_EMA200'] = ta.ema(df_h1['Close'], length=200)
        
        # HTF ADX (14): Raised threshold to 35 to ensure the market is in a strong trending phase.
        adx_h1 = ta.adx(df_h1['High'], df_h1['Low'], df_h1['Close'], length=14)
        df_h1 = pd.concat([df_h1, adx_h1], axis=1)
        
        # Trend Logic for HTF (Stricter filter to boost PF)
        df_h1['HTF_Trend'] = 0
        up_cond = (df_h1['Close'] > df_h1['HTF_EMA50']) & \
                  (df_h1['HTF_EMA50'] > df_h1['HTF_EMA200']) & \
                  (df_h1['ADX_14'] > 35) & \
                  (df_h1['DMP_14'] > df_h1['DMN_14'])
                  
        dn_cond = (df_h1['Close'] < df_h1['HTF_EMA50']) & \
                  (df_h1['HTF_EMA50'] < df_h1['HTF_EMA200']) & \
                  (df_h1['ADX_14'] > 35) & \
                  (df_h1['DMN_14'] > df_h1['DMP_14'])
        
        df_h1.loc[up_cond, 'HTF_Trend'] = 1
        df_h1.loc[dn_cond, 'HTF_Trend'] = -1
        
        # Shift(1) to avoid look-ahead bias
        df_h1['HTF_Trend'] = df_h1['HTF_Trend'].shift(1)
        
        # Map HTF trend back to LTF
        df['HTF_Trend'] = df_h1['HTF_Trend'].reindex(df.index, method='ffill')

        return df

    def generate_signal(self, df):
        """
        Generates trading signals based on the most recent completed candle.
        """
        if len(df) < 200:
            return None

        # Use the last completed row to avoid repainting
        last = df.iloc[-1]
        prev = df.iloc[-2]

        # --- High-Precision Filters ---
        # 1. Time Window Filter (UTC 15:00 - 23:00) - Core volatility window
        is_active_time = 15 <= last.name.hour <= 23
        if not is_active_time:
            return None

        # 2. Volatility Expansion Filter:
        # Only enter when current ATR is significantly above the long-term average (1.1x).
        # This prevents entries during "drifting" markets which cause low RF.
        volatility_ok = last['ATR'] > (last['ATR_SMA'] * 1.1)
        if not volatility_ok:
            return None

        # 3. HTF Trend Filter: Must be in a strong confirmed trend
        htf_trend = last['HTF_Trend']
        if htf_trend == 0:
            return None

        # --- Entry Triggers (Surgical Precision) ---
        # BUY: Strong HTF Trend + 40-period Breakout + Positive and Accelerating MACD Histogram
        if htf_trend == 1:
            if (last['Close'] > last['DC_Upper']) and \
               (last[self.macd_hist_col] > 0) and \
               (last[self.macd_hist_col] > prev[self.macd_hist_col]):
                return 'BUY'

        # SELL: Strong HTF Trend + 40-period Breakout + Negative and Accelerating MACD Histogram
        if htf_trend == -1:
            if (last['Close'] < last['DC_Lower']) and \
               (last[self.macd_hist_col] < 0) and \
               (last[self.macd_hist_col] < prev[self.macd_hist_col]):
                return 'SELL'

        return None

MQL5コード(MT5用)

//+------------------------------------------------------------------+
//|                                           MTF_VBO_Quant_Ultra.mq5|
//|                                  Copyright 2026, Trading Dev     |
//|                                             https://www.example.com|
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Trading Dev"
#property link      "https://www.example.com"
#property version   "1.00"
#property strict

//--- Input Parameters
input double   InpLotSize = 0.1;           // Lot Size
input int      InpTP_Pips = 800;           // Take Profit (points)
input int      InpSL_Pips = 300;           // Stop Loss (points)
input int      InpTrailStart = 250;        // Trailing Stop Start (points)
input int      InpDCPeriod = 40;           // Donchian Channel Period
input int      InpATRPeriod = 14;          // ATR Period
input int      InpATR_SMA = 200;           // ATR SMA Period

//--- Indicator Handles
int handleATR, handleATR_SMA, handleMACD, handleEMA50_H1, handleEMA200_H1, handleADX_H1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleATR = iATR(_Symbol, _Period, InpATRPeriod);
    handleMACD = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE);
    handleEMA50_H1 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
    handleEMA200_H1 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
    handleADX_H1 = iADX(_Symbol, PERIOD_H1, 14);
    
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // Time Filter (UTC 15:00 - 23:00)
    MqlDateTime dt;
    TimeCurrent(dt);
    if(dt.hour < 15 || dt.hour > 23) return;

    // Get Indicator Values
    double atr[], macd_main[], macd_sig[];
    CopyBuffer(handleATR, 0, 0, 2, atr);
    CopyBuffer(handleMACD, 0, 0, 2, macd_main);
    CopyBuffer(handleMACD, 1, 0, 2, macd_sig);

    // Calculate ATR SMA manually (simplification for sample)
    double atr_sum = 0;
    double atr_buf[];
    CopyBuffer(handleATR, 0, 0, InpATR_SMA, atr_buf);
    for(int i=0; i<InpATR_SMA; i++) atr_sum += atr_buf[i];
    double atr_sma = atr_sum / InpATR_SMA;

    if(atr[0] < atr_sma * 1.1) return;

    // HTF Trend Filter (H1)
    double ema50[], ema200[], adx[], plusDI[], minusDI[], closeH1[];
    CopyBuffer(handleEMA50_H1, 0, 0, 1, ema50);
    CopyBuffer(handleEMA200_H1, 0, 0, 1, ema200);
    CopyBuffer(handleADX_H1, 0, 0, 1, adx);
    CopyBuffer(handleADX_H1, 1, 0, 1, plusDI);
    CopyBuffer(handleADX_H1, 2, 0, 1, minusDI);
    CopyClose(_Symbol, PERIOD_H1, 0, 1, closeH1);

    int htf_trend = 0;
    if(closeH1[0] > ema50[0] && ema50[0] > ema200[0] && adx[0] > 35 && plusDI[0] > minusDI[0]) 
        htf_trend = 1;
    else if(closeH1[0] < ema50[0] && ema50[0] < ema200[0] && adx[0] > 35 && minusDI[0] > plusDI[0]) 
        htf_trend = -1;

    if(htf_trend == 0) return;

    // Donchian Channel
    double high_max = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, InpDCPeriod, 1));
    double low_min = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, InpDCPeriod, 1));

    double macd_hist_curr = macd_main[0] - macd_sig[0];
    double macd_hist_prev = macd_main[1] - macd_sig[1];

    // Entry Logic
    if(htf_trend == 1 && iClose(_Symbol, _Period, 0) > high_max && macd_hist_curr > 0 && macd_hist_curr > macd_hist_prev)
    {
        if(PositionsTotal() == 0)
            TradeOpen(ORDER_TYPE_BUY);
    }
    else if(htf_trend == -1 && iClose(_Symbol, _Period, 0) < low_min && macd_hist_curr < 0 && macd_hist_curr < macd_hist_prev)
    {
        if(PositionsTotal() == 0)
            TradeOpen(ORDER_TYPE_SELL);
    }
    
    ManageTrailingStop();
}

void TradeOpen(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.magic = 123456;
    
    OrderSend(request, result);
}

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 - InpSL_Pips * _Point;
                    if(new_sl > PositionGetDouble(POSITION_SL))
                    {
                        MqlTradeRequest req = {}; MqlTradeResult res = {};
                        req.action = TRADE_ACTION_SLTP;
                        req.position = ticket;
                        req.sl = new_sl;
                        req.tp = PositionGetDouble(POSITION_TP);
                        OrderSend(req, res);
                    }
                }
            }
            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 + InpSL_Pips * _Point;
                    if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)
                    {
                        MqlTradeRequest req = {}; MqlTradeResult res = {};
                        req.action = TRADE_ACTION_SLTP;
                        req.position = ticket;
                        req.sl = new_sl;
                        req.tp = PositionGetDouble(POSITION_TP);
                        OrderSend(req, res);
                    }
                }
            }
        }
    }
}