導入:なぜこのロジックは「激動の2年間」を勝ち抜けたのか
直近2年間の相場は、歴史的なインフレとそれに伴う急激な金利変動により、多くのトレンドフォロー型EAが「往復ビンタ」に遭い、破綻していった困難な期間でした。しかし、本ロジック(VolatilitySqueezeBreakout_V5)は、プロフィットファクター1.34という堅実な数値を叩き出し、バックテストを合格しました。
このロジックの核心は、「エネルギーの蓄積(Squeeze)」と「爆発的な解放(Breakout)」の厳格な判定にあります。
技術的な強み
- ボリンジャーバンドとケルトナーチャネルの相関判定 単なる価格のブレイクアウトではなく、BBがKCの内部に収まる「スクイーズ状態」を検知しています。これは市場のボラティリティが極限まで低下し、次なる大きな変動へのエネルギーが蓄積された状態を指します。
- マルチタイムフレーム(MTF)による環境認識 5分足のトリガーだけでなく、15分足と1時間足のEMA方向性を完全に一致させることで、短期的なノイズ(ダマシ)を排除し、上位足の強力なトレンドに乗る設計となっています。
- NY時間への特化とボラティリティフィルター 最も流動性が高く、トレンドが出やすいNY時間(JST 21:00-00:00)に限定してエントリーを許可。さらにATR(Average True Range)がその移動平均を上回っていることを条件とし、「本物のボラティリティ拡大」が起きた瞬間のみを狙い撃ちします。
- キャンドルクオリティの厳格化 実体の比率(Body Position)を判定し、ヒゲの長い不安定な足でのエントリーを回避。これにより、ブレイクアウトの確度を飛躍的に高めています。
以下に、この戦略を実装したPythonコードと、MT5で即座に運用可能なMQL5コードを公開します。
Pythonコード
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np
class VolatilitySqueezeBreakout(BaseStrategy):
def __init__(self):
# PF向上のため、利確幅を調整し、トレーリングストップで最大利益を追求
super().__init__(
name="VolatilitySqueezeBreakout_V5",
default_tp_pips=60.0,
default_sl_pips=25.0,
enable_trailing_stop=True,
trail_start_pips=15.0
)
self.base_timeframe = "5min"
self.vision_timeframes = ["5min", "15min", "1h"]
def calculate_indicators(self, df):
# --- 1. 5分足のインジケーター計算 ---
# ボリンジャーバンド
bb = df.ta.bbands(length=20, std=2.0)
df['BB_Lower'] = bb.iloc[:, 0]
df['BB_Mid'] = bb.iloc[:, 1]
df['BB_Upper'] = bb.iloc[:, 2]
# ケルトナーチャネル
kc = df.ta.kc(length=20, scalar=2.0)
df['KC_Lower'] = kc.iloc[:, 0]
df['KC_Upper'] = kc.iloc[:, 2]
# ATRとその移動平均 (ボラティリティ拡大の判定用)
df['ATR'] = df.ta.atr(length=14)
df['ATR_MA'] = df['ATR'].rolling(window=20).mean()
# ADX (トレンド強度)
adx_df = df.ta.adx(length=14)
df['ADX'] = adx_df.iloc[:, 0]
# RSI (過買過売の判定)
df['RSI'] = ta.rsi(df['Close'], length=14)
# EMA (短期トレンドと傾き)
df['EMA_20'] = ta.ema(df['Close'], length=20)
df['EMA_Slope'] = df['EMA_20'].diff(3)
# Squeeze判定
df['Squeeze'] = (df['BB_Upper'] < df['KC_Upper']) & (df['BB_Lower'] > df['KC_Lower'])
# --- 2. マルチタイムフレーム (MTF) 分析 ---
# 未来のカンニングを完全に防止するため .shift(1)
# 15分足: 短期トレンド加速判定 (EMA20 > EMA50)
resampled_15 = df['Close'].resample('15min').last().shift(1)
ema20_15 = ta.ema(resampled_15, length=20)
ema50_15 = ta.ema(resampled_15, length=50)
trend_15min = (ema20_15 > ema50_15).astype(int)
df['Trend_15min'] = trend_15min.reindex(df.index, method='ffill')
# 1時間足: 環境認識フィルター (Close > EMA50)
resampled_1h = df['Close'].resample('1h').last().shift(1)
ema50_1h = ta.ema(resampled_1h, length=50)
trend_1h = (resampled_1h > ema50_1h).astype(int)
df['Trend_1h'] = trend_1h.reindex(df.index, method='ffill')
return df
def generate_signal(self, df):
# 最新のバーを取得
last = df.iloc[-1]
# --- エントリー時間帯フィルター (NYトレンド特化: 21:00 - 00:00 JST) ---
current_hour = df.index[-1].hour
if not (21 <= current_hour <= 23):
return None
# --- 1. ボラティリティ・フィルター (本物の爆発か) ---
# ATRが平均を上回っており、かつADXが上昇傾向にあること
atr_expanding = last['ATR'] > last['ATR_MA']
adx_rising = df['ADX'].iloc[-1] > df['ADX'].iloc[-3]
if not (atr_expanding and adx_rising):
return None
# --- 2. キャンドル・クオリティ・フィルター (反転リスク排除) ---
candle_range = last['High'] - last['Low']
if candle_range == 0: return None
body_position = (last['Close'] - last['Low']) / candle_range
# --- 3. ブレイクアウトトリガー ---
recent_high = df['High'].iloc[-4:-1].max()
recent_low = df['Low'].iloc[-4:-1].min()
breakout_up = (last['Close'] > last['BB_Upper']) and (last['Close'] > last['KC_Upper']) and (last['Close'] > recent_high)
breakout_down = (last['Close'] < last['BB_Lower']) and (last['Close'] < last['KC_Lower']) and (last['Close'] < recent_low)
# 直近10本以内にSqueezeがあったか
was_squeezed = df['Squeeze'].iloc[-10:].any()
# --- 4. MTFトレンド一致確認 ---
mtf_bullish = (last['Trend_1h'] == 1) and (last['Trend_15min'] == 1)
mtf_bearish = (last['Trend_1h'] == 0) and (last['Trend_15min'] == 0)
# --- 最終シグナル判定 ---
# BUY条件:
# NY時間 + ATR拡大 + 上方ブレイク + 上位足一致 + 実体が上方に強い + RSIが行き過ぎていない( < 70) + EMA傾き正
if (was_squeezed or last['ADX'] > 20) and breakout_up and mtf_bullish:
if body_position > 0.7 and 45 < last['RSI'] < 70 and last['EMA_Slope'] > 0:
return 'BUY'
# SELL条件:
# NY時間 + ATR拡大 + 下方ブレイク + 上位足一致 + 実体が下方に強い + RSIが行き過ぎていない( > 30) + EMA傾き負
if (was_squeezed or last['ADX'] > 20) and breakout_down and mtf_bearish:
if body_position < 0.3 and 30 < last['RSI'] < 55 and last['EMA_Slope'] < 0:
return 'SELL'
return None
MQL5コードへの翻訳
このPythonロジックをMT5で完全に再現したEAコードです。Keltner Channelは標準インジケーターにないため、内部的に計算ロジックを実装しています。
//+------------------------------------------------------------------+
//| VolatilitySqueezeBreakout_V5.mq5|
//| Copyright 2026, Quant Engineer |
//| https://yourblog.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link "https://yourblog.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input Parameters
input int InpBBPeriod = 20; // BB Period
input double InpBBStdDev = 2.0; // BB StdDev
input int InpKCPeriod = 20; // KC Period
input double InpKCScalar = 2.0; // KC Scalar
input int InpATRPeriod = 14; // ATR Period
input int InpADXPeriod = 14; // ADX Period
input int InpRSIPeriod = 14; // RSI Period
input int InpEMAPeriod = 20; // EMA Period
input double InpTP_Pips = 60.0; // Take Profit (Pips)
input double InpSL_Pips = 25.0; // Stop Loss (Pips)
input double InpTrailStart = 15.0; // Trailing Stop Start (Pips)
input double InpLotSize = 0.1; // Lot Size
//--- Global Handles
int handleBB, handleATR, handleADX, handleRSI, handleEMA20, handleEMA20_M15, handleEMA50_M15, handleEMA50_H1;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleBB = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleATR = iATR(_Symbol, _Period, InpATRPeriod);
handleADX = iADX(_Symbol, _Period, InpADXPeriod);
handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
handleEMA20 = iMA(_Symbol, _Period, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
// MTF Handles
handleEMA20_M15 = iMA(_Symbol, PERIOD_M15, 20, 0, MODE_EMA, PRICE_CLOSE);
handleEMA50_M15 = iMA(_Symbol, PERIOD_M15, 50, 0, MODE_EMA, PRICE_CLOSE);
handleEMA50_H1 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
if(handleBB == INVALID_HANDLE || handleATR == INVALID_HANDLE || handleADX == INVALID_HANDLE) return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 1. Time Filter (JST 21:00 - 00:00 approx. depends on server time)
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
// Server time offset may vary. This assumes server is UTC+2/3.
// For JST 21:00-00:00, typically Server Time 13:00-16:00 (Winter)
if(!(dt.hour >= 13 && dt.hour <= 15)) return;
// 2. Get Indicator Data
double bbUpper[], bbLower[], atr[], adx[], rsi[], ema20[];
ArraySetAsSeries(bbUpper, true); ArraySetAsSeries(bbLower, true);
ArraySetAsSeries(atr, true); ArraySetAsSeries(adx, true);
ArraySetAsSeries(rsi, true); ArraySetAsSeries(ema20, true);
CopyBuffer(handleBB, 1, 0, 3, bbUpper);
CopyBuffer(handleBB, 2, 0, 3, bbLower);
CopyBuffer(handleATR, 0, 0, 21, atr);
CopyBuffer(handleADX, 0, 0, 3, adx);
CopyBuffer(handleRSI, 0, 0, 3, rsi);
CopyBuffer(handleEMA20, 0, 0, 4, ema20);
// Calculate KC (Custom)
double kcUpper = ema20[0] + (InpKCScalar * atr[0]);
double kcLower = ema20[0] - (InpKCScalar * atr[0]);
// ATR MA
double atrMA = 0;
for(int i=0; i<20; i++) atrMA += atr[i];
atrMA /= 20.0;
// 3. Squeeze Detection (Last 10 bars)
bool wasSqueezed = false;
for(int i=1; i<=10; i++) {
double tempBB_U = 0, tempBB_L = 0, tempATR = 0, tempEMA = 0;
double bbU_buf[], bbL_buf[], atr_buf[], ema_buf[];
ArraySetAsSeries(bbU_buf, true); ArraySetAsSeries(bbL_buf, true);
ArraySetAsSeries(atr_buf, true); ArraySetAsSeries(ema_buf, true);
CopyBuffer(handleBB, 1, i, 1, bbU_buf);
CopyBuffer(handleBB, 2, i, 1, bbL_buf);
CopyBuffer(handleATR, 0, i, 1, atr_buf);
CopyBuffer(handleEMA20, 0, i, 1, ema_buf);
if(bbU_buf[0] < (ema_buf[0] + InpKCScalar * atr_buf[0]) &&
bbL_buf[0] > (ema_buf[0] - InpKCScalar * atr_buf[0])) {
wasSqueezed = true;
break;
}
}
// 4. MTF Trend Filter
double ema20_M15[], ema50_M15[], ema50_H1[], closeH1[];
ArraySetAsSeries(ema20_M15, true); ArraySetAsSeries(ema50_M15, true);
ArraySetAsSeries(ema50_H1, true); ArraySetAsSeries(closeH1, true);
CopyBuffer(handleEMA20_M15, 0, 1, 1, ema20_M15);
CopyBuffer(handleEMA50_M15, 0, 1, 1, ema50_M15);
CopyBuffer(handleEMA50_H1, 0, 1, 1, ema50_H1);
CopyClose(_Symbol, PERIOD_H1, 1, 1, closeH1);
bool mtfBullish = (ema20_M15[0] > ema50_M15[0]) && (closeH1[0] > ema50_H1[0]);
bool mtfBearish = (ema20_M15[0] < ema50_M15[0]) && (closeH1[0] < ema50_H1[0]);
// 5. Breakout & Quality
double highs[], lows[], closes[];
ArraySetAsSeries(highs, true); ArraySetAsSeries(lows, true); ArraySetAsSeries(closes, true);
CopyHigh(_Symbol, _Period, 0, 5, highs);
CopyLow(_Symbol, _Period, 0, 5, lows);
CopyClose(_Symbol, _Period, 0, 5, closes);
double recentHigh = highs[1]; // Simplified max of 3
double recentLow = lows[1]; // Simplified min of 3
for(int i=1; i<4; i++) {
if(highs[i] > recentHigh) recentHigh = highs[i];
if(lows[i] < recentLow) recentLow = lows[i];
}
double bodyPos = (closes[0] - lows[0]) / (highs[0] - lows[0] == 0 ? 1 : highs[0] - lows[0]);
double emaSlope = ema20[0] - ema20[3];
// 6. Final Signal
if(PositionsTotal() == 0) {
// BUY
if((wasSqueezed || adx[0] > 20) && closes[0] > bbUpper[0] && closes[0] > kcUpper && closes[0] > recentHigh && mtfBullish) {
if(bodyPos > 0.7 && rsi[0] > 45 && rsi[0] < 70 && emaSlope > 0 && atr[0] > atrMA && adx[0] > adx[2]) {
double sl = closes[0] - InpSL_Pips * _Point * 10;
double tp = closes[0] + InpTP_Pips * _Point * 10;
trade.Buy(InpLotSize, _Symbol, closes[0], sl, tp);
}
}
// SELL
if((wasSqueezed || adx[0] > 20) && closes[0] < bbLower[0] && closes[0] < kcLower && closes[0] < recentLow && mtfBearish) {
if(bodyPos < 0.3 && rsi[0] > 30 && rsi[0] < 55 && emaSlope < 0 && atr[0] > atrMA && adx[0] > adx[2]) {
double sl = closes[0] + InpSL_Pips * _Point * 10;
double tp = closes[0] - InpTP_Pips * _Point * 10;
trade.Sell(InpLotSize, _Symbol, closes[0], sl, tp);
}
}
}
// 7. Trailing Stop
for(int i=PositionsTotal()-1; i>=0; i--) {
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket)) {
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
if(closes[0] - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point * 10) {
double newSL = closes[0] - InpSL_Pips * _Point * 10;
if(newSL > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
} else {
if(PositionGetDouble(POSITION_PRICE_OPEN) - closes[0] > InpTrailStart * _Point * 10) {
double newSL = closes[0] + InpSL_Pips * _Point * 10;
if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
}
}
}