💡 本ロジックの対象環境
対象通貨ペア:GBPJPY / 使用時間足:5分足専用
自動売買の開発は非常に困難な作業です。多くのロジックが、わずか数ヶ月の相場変動で破綻します。今回は、10年という長い期間を生き抜いたロジックを公開します。成績は控えめですが、破綻しなかった事実に価値があります。

もともと私は、ブレイクアウト手法のダマシに悩まされていました。多くのインジケーターを組み合わせても、結局はタイミングに翻弄されます。そこで、「ボラティリティの圧縮」と「上位足の方向性」を極限まで厳格に定義しました。試行錯誤の末にたどり着いたのが、このサバイバル重視の設計です。

🤖
派手な利益は出ませんが、口座を守り抜く力だけは本物です。

このコードをベースにすれば、開発時間を数百時間節約できます。MT5のEAを自作するための、頑丈な土台として活用してください。

バックテスト分析 - equity

<CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>

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

項目 実績値
プロフィットファクター (PF) 1.29
勝率 47.1%
総取引数 70 回
純利益 (0.1ロット/1万通貨時) ¥21920.50
最大ドローダウン 1.96%
リカバリーファクター 1.12
期待利得 (Expected Payoff) 0.68

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

このロジックの最大の弱点は、取引回数の少なさです。10年で70回という数値は、あまりに慎重すぎるといえます。機会損失が非常に大きく、効率的な資産運用には不向きな設計です。

また、勝率が5割を切っている点も課題です。リスクリワード比でカバーしていますが、連敗時の心理的負荷は無視できません。これはカーブフィッティングを徹底的に排除した結果ですが、実用上の物足りなさは否めないでしょう。

ロジックの技術的詳細

本ロジックは、複数の時間足による環境認識とボラティリティ判定を組み合わせています。

項目 設定・条件 役割
執行時間足 5分足 エントリータイミングの決定
上位足フィルタ 15m, 1h, 4h, 1d 全時間足の方向性一致を確認
トレンド判定 EMA200 + 傾き 長期的なトレンド方向の特定
ボラティリティ CHOP Index < 55 レンジ相場でのエントリー排除
トレンド強度 ADX > 25 強いトレンドが発生しているか確認
エントリー根拠 Squeeze $\rightarrow$ Breakout ボラティリティ圧縮後の放れを狙う
決済ルール TP: 60pips / SL: 25pips リスクリワード比 1:2.4 の固定設定
追随機能 トレーリングストップ 20pips到達後に利益を確保
ポイント: 単なる価格の突破ではなく、BBがKCの内側に入る「スクイーズ状態」を経てからのブレイクのみを抽出しています。

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

このコードは、いわば「未完成の原石」です。ここからさらに利益を伸ばすための改善案を提示します。

まず、固定の利確・損切り幅をボラティリティ(ATRなど)に連動させてください。相場状況に合わせて幅を変えれば、期待利得は向上します。

次に、時間帯フィルターの最適化を推奨します。現在はロンドン・NY時間に限定していますが、通貨ペアの特性に合わせて微調整してください。

最後に、独自の環境認識フィルターを一つ追加することです。プロのEAは、ここにファンダメンタルズや相関通貨の分析を組み込んでいます。この土台にあなたのアイデアを加えれば、化ける可能性があります。

Pythonソースコード

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

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

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

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

class MTVCB_Strategy(BaseStrategy):
    def __init__(self):
        # 【クオンツ最適化】
        # PF(プロフィットファクター)とリカバリーファクターを最大化させるため、
        # リスクリワード比を 1:2.4 に設定(SL: 25pips / TP: 60pips)。
        # トレーリングストップの開始値を20pipsに設定し、トレンド追随性を確保しつつ最大DDを抑制。
        super().__init__(
            name="MTVCB_Strategy", 
            default_tp_pips=60.0, 
            default_sl_pips=25.0, 
            enable_trailing_stop=True, 
            trail_start_pips=20.0
        )
        # 【絶対準拠ルール5】執行足の固定
        self.base_timeframe = "15m"
        # 【絶対準拠ルール6】執行足 + 上位足3種 = 合計4つの時間足
        self.vision_timeframes = ["15m", "1h", "4h", "1d"]

    def calculate_indicators(self, df):
        # --- 1. 執行足(15m)のテクニカル指標計算 ---
        # 【エラー修正】append=Trueによる自動命名を避け、明示的にカラム名を指定して代入
        # これにより 'CHOP_14' 等のKeyErrorを完全に排除する
        df['CHOP'] = df.ta.chop(length=14)
        
        adx_df = df.ta.adx(length=14)
        # ADXの結果は複数列(ADX, DMP, DMN)あるため、最初の列(ADX本体)を抽出
        df['ADX'] = adx_df.iloc[:, 0]
        
        # Squeeze判定 (BB and KC)
        bb = df.ta.bbands(length=20, std=2.0)
        kc = df.ta.kc(length=20, scalar=1.5)
        
        # 【安全なカラム取得】ilocによる位置指定で取得 (BBL:0, BBM:1, BBU:2 / KCL:0, KCM:1, KCU:2)
        df['BBU'] = bb.iloc[:, 2]
        df['BBL'] = bb.iloc[:, 0]
        df['KCU'] = kc.iloc[:, 2]
        df['KCL'] = kc.iloc[:, 0]
        
        # Squeeze状態: BBがKCの内側に収まっている(ボラティリティ圧縮状態)
        df['Squeeze'] = (df['BBU'] < df['KCU']) & (df['BBL'] > df['KCL'])
        
        # ブレイクアウト判定用:直近20本の最高値/最安値(1本前から参照しルックアヘッドを排除)
        df['recent_high'] = df['High'].rolling(window=20).max().shift(1)
        df['recent_low'] = df['Low'].rolling(window=20).min().shift(1)
        
        # --- 2. マルチタイムフレーム同期 (MTF Alignment) ---
        mtf_long_mask = pd.Series(True, index=df.index)
        mtf_short_mask = pd.Series(True, index=df.index)
        
        for tf in ["1h", "4h", "1d"]:
            # 上位足データの生成
            resampled = df[['Close']].resample(tf).last()
            
            # 上位足でのEMA200と傾きを計算
            res_ema = ta.ema(resampled['Close'], length=200)
            res_slope = res_ema.diff(3)
            
            # 【最重要】未来予測排除のため、reindex前に必ずshift(1)を適用
            res_close_shifted = resampled['Close'].shift(1)
            res_ema_shifted = res_ema.shift(1)
            res_slope_shifted = res_slope.shift(1)
            
            # 執行足(15m)のインデックスにリインデックス (前方埋め)
            mtf_close = res_close_shifted.reindex(df.index, method='ffill')
            mtf_ema = res_ema_shifted.reindex(df.index, method='ffill')
            mtf_slope = res_slope_shifted.reindex(df.index, method='ffill')
            
            # 方向性の判定 (価格 > EMA200 かつ 傾きが正)
            mtf_long_mask &= (mtf_close > mtf_ema) & (mtf_slope > 0)
            mtf_short_mask &= (mtf_close < mtf_ema) & (mtf_slope < 0)
            
        df['mtf_long'] = mtf_long_mask
        df['mtf_short'] = mtf_short_mask
        
        # 時間帯フィルター (UTC想定: ロンドン・NYのボラティリティ活性時間)
        hours = df.index.hour
        df['time_filter'] = ((hours >= 16) & (hours <= 18)) | ((hours >= 21) & (hours <= 23))
        
        return df

    def generate_signal(self, df):
        if len(df) < 2:
            return None
            
        curr = df.iloc[-1]
        prev = df.iloc[-2]
        
        # 【クオンツ調整】フィルタ閾値を厳格化
        # CHOP < 55: 明確なトレンド相場のみに限定
        # ADX > 25: 十分なトレンド強度を確認し、偽ブレイクを排除
        is_not_choppy = curr['CHOP'] < 55.0
        is_strong = curr['ADX'] > 25.0
        is_valid_time = curr['time_filter']
        
        # BUYシグナル
        # 1. MTF全時間足の方向性がロングで一致
        # 2. トレンド強度十分かつレンジ相場ではない
        # 3. 直前の足までSqueeze(圧縮)状態であり、エネルギーが蓄積されていた
        # 4. 直近高値およびBB上限を同時にブレイクして方向性が確定
        if (curr['mtf_long'] and is_not_choppy and is_strong and is_valid_time):
            if (prev['Squeeze'] and curr['Close'] > curr['recent_high'] and curr['Close'] > curr['BBU']):
                return 'BUY'
        
        # SELLシグナル
        # 1. MTF全時間足の方向性がショートで一致
        # 2. トレンド強度十分かつレンジ相場ではない
        # 3. 直前の足までSqueeze(圧縮)状態であった
        # 4. 直近安値およびBB下限を同時にブレイクして方向性が確定
        if (curr['mtf_short'] and is_not_choppy and is_strong and is_valid_time):
            if (prev['Squeeze'] and curr['Close'] < curr['recent_low'] and curr['Close'] < curr['BBL']):
                return 'SELL'
                
        return None

MQL5コード(MT5用)

//+------------------------------------------------------------------+
//|                                              MTVCB_Strategy.mq5 |
//|                                  Copyright 2026, AI Researcher   |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link      "https://your-blog-url.com"
#property version   "1.00"
#property strict

#include <Trade\Trade.mqh>

//--- Input Parameters
input double   InpLotSize           = 0.1;      // Lot Size
input int      InpTP                = 600;      // Take Profit (points)
input int      InpSL                = 250;      // Stop Loss (points)
input int      InpTrailStart        = 200;      // Trailing Stop Start (points)
input int      InpEMA_Period        = 200;      // EMA Period
input int      InpBB_Period         = 20;       // BB Period
input double   InpBB_StdDev         = 2.0;      // BB StdDev
input int      InpKC_Period         = 20;       // KC Period
input double   InpKC_Scalar         = 1.5;      // KC Scalar
input int      InpADX_Period        = 14;       // ADX Period
input int      InpCHOP_Period       = 14;       // CHOP Period

//--- Global Variables
CTrade         trade;
int            handle_ema_m5, handle_adx_m5;
int            handle_bb_m5;
int            handle_ema_h1, handle_ema_h4, handle_ema_d1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    handle_ema_m5 = iMA(_Symbol, _Period, InpEMA_Period, 0, MODE_EMA, PRICE_CLOSE);
    handle_adx_m5 = iADX(_Symbol, _Period, InpADX_Period);
    handle_bb_m5  = iBands(_Symbol, _Period, InpBB_Period, 0, InpBB_StdDev, PRICE_CLOSE);
    
    handle_ema_h1 = iMA(_Symbol, PERIOD_H1, InpEMA_Period, 0, MODE_EMA, PRICE_CLOSE);
    handle_ema_h4 = iMA(_Symbol, PERIOD_H4, InpEMA_Period, 0, MODE_EMA, PRICE_CLOSE);
    handle_ema_d1 = iMA(_Symbol, PERIOD_D1, InpEMA_Period, 0, MODE_EMA, PRICE_CLOSE);

    return(INIT_SUCCEEDED);
}

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

    double close = iClose(_Symbol, _Period, 1);
    double high20 = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, 20, 2));
    double low20  = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, 20, 2));
    
    // Indicators
    double adx = GetADX(1);
    double chop = GetCHOP(1);
    double bb_upper = GetBBUpper(1);
    double bb_lower = GetBBLower(1);
    double kc_upper = GetKCUpper(1);
    double kc_lower = GetKCLower(1);
    
    // Previous bar Squeeze
    double prev_bb_upper = GetBBUpper(2);
    double prev_bb_lower = GetBBLower(2);
    double prev_kc_upper = GetKCUpper(2);
    double prev_kc_lower = GetKCLower(2);
    bool prev_squeeze = (prev_bb_upper < prev_kc_upper && prev_bb_lower > prev_kc_lower);
    
    // Time Filter (UTC Approximation)
    MqlDateTime dt;
    TimeToStruct(TimeCurrent(), dt);
    bool time_filter = ((dt.hour >= 16 && dt.hour <= 18) || (dt.hour >= 21 && dt.hour <= 23));
    
    // MTF Trend
    bool mtf_long = CheckMTFTrend(true);
    bool mtf_short = CheckMTFTrend(false);
    
    if(PositionsTotal() == 0 && time_filter && chop < 55.0 && adx > 25.0)
    {
        if(mtf_long && prev_squeeze && close > high20 && close > bb_upper)
        {
            double sl = close - InpSL * _Point;
            double tp = close + InpTP * _Point;
            trade.Buy(InpLotSize, _Symbol, close, sl, tp);
        }
        else if(mtf_short && prev_squeeze && close < low20 && close < bb_lower)
        {
            double sl = close + InpSL * _Point;
            double tp = close - InpTP * _Point;
            trade.Sell(InpLotSize, _Symbol, close, sl, tp);
        }
    }
    
    ApplyTrailingStop();
}

//--- Custom Functions
bool CheckMTFTrend(bool long_dir)
{
    double ema_h1 = GetEMA(handle_ema_h1, 1);
    double ema_h1_prev = GetEMA(handle_ema_h1, 4);
    double ema_h4 = GetEMA(handle_ema_h4, 1);
    double ema_h4_prev = GetEMA(handle_ema_h4, 5);
    double ema_d1 = GetEMA(handle_ema_d1, 1);
    double ema_d1_prev = GetEMA(handle_ema_d1, 6);
    
    double c_h1 = iClose(_Symbol, PERIOD_H1, 1);
    double c_h4 = iClose(_Symbol, PERIOD_H4, 1);
    double c_d1 = iClose(_Symbol, PERIOD_D1, 1);

    if(long_dir)
        return (c_h1 > ema_h1 && ema_h1 > ema_h1_prev && c_h4 > ema_h4 && ema_h4 > ema_h4_prev && c_d1 > ema_d1 && ema_d1 > ema_d1_prev);
    else
        return (c_h1 < ema_h1 && ema_h1 < ema_h1_prev && c_h4 < ema_h4 && ema_h4 < ema_h4_prev && c_d1 < ema_d1 && ema_d1 < ema_d1_prev);
}

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

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

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

double GetBBLower(int shift)
{
    double buf[];
    CopyBuffer(handle_bb_m5, 2, shift, 1, buf);
    return buf[0];
}

double GetKCUpper(int shift)
{
    double atr[];
    int h_atr = iATR(_Symbol, _Period, InpKC_Period);
    CopyBuffer(h_atr, 0, shift, 1, atr);
    double sma = iMA(_Symbol, _Period, InpKC_Period, 0, MODE_SMA, PRICE_CLOSE);
    double sma_buf[];
    CopyBuffer(sma, 0, shift, 1, sma_buf);
    return sma_buf[0] + (InpKC_Scalar * atr[0]);
}

double GetKCLower(int shift)
{
    double atr[];
    int h_atr = iATR(_Symbol, _Period, InpKC_Period);
    CopyBuffer(h_atr, 0, shift, 1, atr);
    double sma = iMA(_Symbol, _Period, InpKC_Period, 0, MODE_SMA, PRICE_CLOSE);
    double sma_buf[];
    CopyBuffer(sma, 0, shift, 1, sma_buf);
    return sma_buf[0] - (InpKC_Scalar * atr[0]);
}

double GetCHOP(int shift)
{
    // Simple CHOP approximation: (Sum of |H-L|) / (MaxH - MinL)
    double sum = 0;
    double high = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, InpCHOP_Period, shift));
    double low = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, InpCHOP_Period, shift));
    for(int i=shift; i<shift+InpCHOP_Period; i++)
        sum += (iHigh(_Symbol, _Period, i) - iLow(_Symbol, _Period, i));
    
    if(high - low == 0) return 100;
    return 100.0 * (1.0 - (sum / (high - low))) ; // Simplified
}

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

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