💡 本ロジックの対象環境
対象通貨ペア:GBPUSD(ポンドドル) / 使用時間足:5分足専用
今回は、10年のバックテストを破綻せずに生き抜いたものの、成績が伸び悩んだロジックをご紹介します。

バックテスト資産推移

これが過酷な全期間10年の市場変動を破綻せず生き抜いた資産推移グラフ(長期検証証拠)です。

項目 実績値
プロフィットファクター (PF) 1.38
勝率 45.7%
総取引数 35 回
純利益 (0.1ロット/1万通貨時) $92.83
最大ドローダウン 1.35%

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

このロジックの最大の問題は、取引回数の少なさです。 10年間で35回という数値は、機会損失が極めて大きいことを意味します。 PF1.38という数値は安定していますが、収益力は不足しています。

原因は、フィルターを重ねすぎたことです。 上位足のトレンド判定とZ-Scoreの乖離率を同時に求めています。 さらに時間帯制限まで加えたため、エントリー条件が厳しくなりすぎました。 結果として、相場の大きな波を何度も逃しています。

🤖
破綻しない安心感はありますが、これでは資産を増やすのに時間がかかりすぎますね。

ロジックの技術的詳細

本ロジックは、均衡点からの統計的乖離を狙う逆張り的なアプローチに、トレンドフィルターを掛け合わせた構成です。

項目 設定・条件 内容
ベース時間足 5分足 GBPUSD専用の短期スキャルピング
トレンド判定 1時間足基準線 価格が基準線より上なら買い、下なら売り
エントリー指標 Z-Score 基準線から$\pm$2.0$\sigma$以上の乖離を検知
ボラティリティ ATR / ATR_MA ATRが平均の1.2倍未満の時のみエントリー
反転トリガー RSI $&$ 直近高安値 RSIの方向転換と直近足のブレイクで確定
取引時間帯 JST 16:00 - 24:00 流動性の高い欧州・NY市場序盤に限定
決済戦略 トレーリングストップ 10pips利益到達後にストップを切り上げ

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

このロジックを実用的なレベルへ引き上げるには、フィルターの緩和が必要です。 例えば、Z-Scoreの閾値を2.0から1.5へ下げること。 これにより、取引回数を増やして収益機会を広げられます。

また、ADXを用いたトレンド強度判定の導入を推奨します。 強いトレンド発生時はZ-Scoreの逆張りを禁止する制御を加えること。 プロのEAは、こうした「環境認識による切り替え」でエッジを強めています。

このコードは、極めて低いドローダウンを実現した「守りの設計図」です。ここに独自の攻めのロジックを足すことで、化ける可能性があります。

公開コード(Python)

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

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

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

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

class ElasticEquilibriumBreakout(BaseStrategy):
    def __init__(self):
        # 【ルール4】トレーリングストップ有効化、利確損切の伸長防衛
        super().__init__(
            name="ElasticEquilibriumBreakout", 
            default_tp_pips=50.0, 
            default_sl_pips=20.0, 
            enable_trailing_stop=True, 
            trail_start_pips=10.0
        )
        # 【ルール5】ベース時間足の定義
        self.base_timeframe = "5m"
        # 【ルール6】画像認識AI用時間足の定義
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        """
        高速ベクトル演算によるテクニカル指標の計算。ループ処理は一切禁止。
        """
        # --- 1. 5分足の基本指標 (ベクトル演算) ---
        # 基準線 (Kijun-sen): (期間内最高値 + 期間内最安値) / 2
        df['Kijun'] = (df['High'].rolling(window=26).max() + df['Low'].rolling(window=26).min()) / 2
        
        # ATRおよびボラティリティ・フィルター
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['ATR_MA'] = df['ATR'].rolling(window=100).mean()
        
        # RSI
        df['RSI'] = ta.rsi(df['Close'], length=14)
        
        # ADX (pandas_taのADXはDataFrameを返すため、特定のカラムを抽出)
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['ADX'] = adx_df['ADX_14']
        
        # Z-Score (均衡点からの統計的乖離率)
        df['Z_Score'] = (df['Close'] - df['Kijun']) / df['ATR']

        # --- 2. 上位足 (1h) MTFフィルター ---
        # 【ルール7】ルックアヘッドバイアスの完全排除: resample -> shift(1) -> reindex
        # 1時間足の基準線を計算
        h1_high = df['High'].resample('1h').max().shift(1)
        h1_low = df['Low'].resample('1h').min().shift(1)
        h1_close = df['Close'].resample('1h').last().shift(1)
        
        h1_kijun = (h1_high + h1_low) / 2
        
        # 5分足のインデックスに再マッピング(前方の値を維持)
        df['H1_Kijun'] = h1_kijun.reindex(df.index, method='ffill')
        df['H1_Close'] = h1_close.reindex(df.index, method='ffill')
        
        # 上位足トレンド判定 (価格が基準線の上か下か)
        df['MTF_Trend'] = 'bull'
        df.loc[df['H1_Close'] < df['H1_Kijun'], 'MTF_Trend'] = 'bear'

        return df

    def generate_signal(self, df):
        """
        最新の行に基づいたシグナル生成。
        """
        if len(df) < 100:
            return None

        # 最新行と1本前の行を取得
        curr = df.iloc[-1]
        prev = df.iloc[-2]

        # 【戦略仕様】時間帯フィルター (JST 16:00 - 24:00)
        # サーバー時間(UTC)を想定し、必要に応じて調整されるが、ここではインデックスのhourを使用
        current_hour = curr.name.hour
        is_trading_window = 16 <= current_hour <= 24

        # --- 買いシグナル条件 ---
        # 1. 上位足が強気
        # 2. 取引時間帯内
        # 3. 均衡点から下方に過伸展 (Z-Score < -2.0)
        # 4. ボラティリティが暴走していない (ATR < ATR_MA * 1.2)
        # 5. RSIの切り上がり (ダイバージェンスの萌芽)
        # 6. 直近高値の更新 (反転のトリガー)
        long_condition = (
            (curr['MTF_Trend'] == 'bull') and
            (is_trading_window) and
            (curr['Z_Score'] < -2.0) and
            (curr['ATR'] < curr['ATR_MA'] * 1.2) and
            (curr['RSI'] > prev['RSI']) and
            (curr['Close'] > prev['High'])
        )

        # --- 売りシグナル条件 ---
        # 1. 上位足が弱気
        # 2. 取引時間帯内
        # 3. 均衡点から上方に過伸展 (Z-Score > 2.0)
        # 4. ボラティリティが暴走していない (ATR < ATR_MA * 1.2)
        # 5. RSIの切り下がり (ダイバージェンスの萌芽)
        # 6. 直近安値の更新 (反転のトリガー)
        short_condition = (
            (curr['MTF_Trend'] == 'bear') and
            (is_trading_window) and
            (curr['Z_Score'] > 2.0) and
            (curr['ATR'] < curr['ATR_MA'] * 1.2) and
            (curr['RSI'] < prev['RSI']) and
            (curr['Close'] < prev['Low'])
        )

        if long_condition:
            return 'BUY'
        if short_condition:
            return 'SELL'
            
        return None

MT5用コード(MQL5)

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

#include <Trade\Trade.mqh>

// 入力パラメータ
input int      InpKijunPeriod = 26;       // Kijun-sen Period
input int      InpATRPeriod   = 14;       // ATR Period
input int      InpATRMAPeriod = 100;      // ATR MA Period
input int      InpRSIPeriod   = 14;       // RSI Period
input double   InpZScoreLevel = 2.0;      // Z-Score Threshold
input double   InpTP_Pips     = 50.0;     // Take Profit (Pips)
input double   InpSL_Pips     = 20.0;     // Stop Loss (Pips)
input double   InpTrailStart  = 10.0;     // Trailing Start (Pips)
input double   InpLotSize     = 0.1;      // Lot Size

// グローバル変数
CTrade trade;
int handleRSI, handleATR;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
    handleATR = iATR(_Symbol, _Period, InpATRPeriod);
    
    if(handleRSI == INVALID_HANDLE || handleATR == INVALID_HANDLE)
        return(INIT_FAILED);
        
    return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    if(!IsNewBar()) return;

    // 指標値の取得
    double rsi[], atr[], close[], high[], low[];
    ArraySetAsSeries(rsi, true);
    ArraySetAsSeries(atr, true);
    ArraySetAsSeries(close, true);
    ArraySetAsSeries(high, true);
    ArraySetAsSeries(low, true);

    if(CopyBuffer(handleRSI, 0, 0, 3, rsi) < 3) return;
    if(CopyBuffer(handleATR, 0, 0, 101, atr) < 101) return;
    if(CopyClose(_Symbol, _Period, 0, 3, close) < 3) return;
    if(CopyHigh(_Symbol, _Period, 0, 3, high) < 3) return;
    if(CopyLow(_Symbol, _Period, 0, 3, low) < 3) return;

    // 基準線の計算 (5分足)
    double highest = high[ArrayMaximum(high, 1, InpKijunPeriod)];
    double lowest = low[ArrayMinimum(low, 1, InpKijunPeriod)];
    double kijun = (highest + lowest) / 2.0;

    // ATR MAの計算
    double atr_sum = 0;
    for(int i=1; i<=InpATRMAPeriod; i++) atr_sum += atr[i];
    double atr_ma = atr_sum / InpATRMAPeriod;

    // Z-Score計算
    double z_score = (close[1] - kijun) / atr[1];

    // 上位足(1時間足)基準線判定
    double h1_high = iHigh(_Symbol, PERIOD_H1, 1);
    double h1_low = iLow(_Symbol, PERIOD_H1, 1);
    double h1_close = iClose(_Symbol, PERIOD_H1, 1);
    double h1_kijun = (iHigh(_Symbol, PERIOD_H1, iHighest(_Symbol, PERIOD_H1, MODE_HIGH, InpKijunPeriod, 1)) + 
                       iLow(_Symbol, PERIOD_H1, iLowest(_Symbol, PERIOD_H1, MODE_LOW, InpKijunPeriod, 1))) / 2.0;
    
    string mtf_trend = (h1_close > h1_kijun) ? "bull" : "bear";

    // 時間帯フィルター (UTC 16:00 - 24:00)
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    bool is_window = (dt.hour >= 16 && dt.hour <= 23);

    // エントリー判定
    if(PositionsTotal() == 0 && is_window)
    {
        // Long: 上位足強気 & 過剰売られ & ボラ安定 & RSI切り上がり & 直近高値更新
        if(mtf_trend == "bull" && z_score < -InpZScoreLevel && atr[1] < atr_ma * 1.2 
           && rsi[1] > rsi[2] && close[1] > high[2])
        {
            double sl = close[1] - InpSL_Pips * _Point * 10;
            double tp = close[1] + InpTP_Pips * _Point * 10;
            trade.Buy(InpLotSize, _Symbol, close[1], sl, tp);
        }
        // Short: 上位足弱気 & 過剰買われ & ボラ安定 & RSI切り下がり & 直近安値更新
        else if(mtf_trend == "bear" && z_score > InpZScoreLevel && atr[1] < atr_ma * 1.2 
                && rsi[1] < rsi[2] && close[1] < low[2])
        {
            double sl = close[1] + InpSL_Pips * _Point * 10;
            double tp = close[1] - InpTP_Pips * _Point * 10;
            trade.Sell(InpLotSize, _Symbol, close[1], sl, tp);
        }
    }
    
    ManageTrailingStop();
}

//+------------------------------------------------------------------+
//| トレーリングストップ管理                                           |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
    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 * 10)
                {
                    double new_sl = current_price - InpTrailStart * _Point * 10;
                    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 * 10)
                {
                    double new_sl = current_price + InpTrailStart * _Point * 10;
                    if(new_sl < stop_loss || stop_loss == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
                }
            }
        }
    }
}

bool IsNewBar()
{
    static datetime last_time = 0;
    datetime lastbar_time = (datetime)SeriesInfoInteger(_Symbol, _Period, SERIES_LASTBAR_DATE);
    if(last_time != lastbar_time)
    {
        last_time = lastbar_time;
        return true;
    }
    return false;
}