ロジック解説:ボラティリティの「蓄積」と「放出」の定量化
本ロジックは、直近2年間のインフレ加速および金利変動に伴う激しいボラティリティ相場において、バックテストでPF1.50という安定した数値を記録したブレイクアウト戦略です。
多くのブレイクアウト戦略が「ダマシ」に遭い、ドローダウンを拡大させる中で、本ロジックが生き残った理由は、エントリー条件におけるフィルターの多層構造にあります。
技術的な強み
-
ボラティリティのV字底打ち判定 (BB Width) 単に価格がバンドを抜けたことを見るのではなく、ボリンジャーバンド幅(BB Width)が過去50期間の平均を下回った状態から上昇に転じた瞬間(vol_release)を捉えています。これにより、「エネルギーが蓄積された後の放出」という相場の本質的なサイクルを定量的に判定しています。
-
ADXによるモメンタムの加速確認 トレンドの有無を示すADXが20以上であることに加え、「ADX自体が上昇していること」を条件に加えています。これにより、トレンドの終焉局面でのエントリーを避け、加速局面のみに限定しています。
-
厳格なMTF(マルチタイムフレーム)フィルター 5分足の執行足だけでなく、15分足(EMA20)と1時間足(EMA50)の方向性が完全に一致している場合のみエントリーを許可します。上位足のトレンドに逆らわない設計が、勝率50%という数値ながら高いPFを実現させています。
-
時間帯の限定(ロンドン市場初動) JST 16:30〜19:00という、世界的に流動性が急増するロンドン市場のオープン時間帯に限定することで、方向感のないアジア時間のノイズを完全に排除しています。
Pythonコードの公開
本ロジックのベースとなったPythonコードです。pandas_ta を用いたテクニカル分析と、厳格な条件分岐が実装されています。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
class VolatilityBreakoutPro(BaseStrategy):
"""
High-Precision Volatility Breakout Strategy.
Focus: London Session Energy Release with Momentum Filter.
Logic: BB Width V-Bottom -> ADX Rising -> MTF Alignment -> Band Walk Start.
"""
def __init__(self):
# トレーリングストップで利益を最大化
super().__init__(
name="VolatilityBreakoutPro_v4",
default_tp_pips=50.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. 短期足(5min)のインジケーター計算 ---
# ボリンジャーバンド
bb = ta.bbands(df['Close'], length=20, std=2.0)
df['BBL'] = bb.iloc[:, 0]
df['BBM'] = bb.iloc[:, 1]
df['BBU'] = bb.iloc[:, 2]
# BB Width とそのトレンド(ボラティリティの底打ち判定用)
df['BBW'] = (df['BBU'] - df['BBL']) / df['BBM']
df['BBW_avg'] = df['BBW'].rolling(window=50).mean()
df['BBW_rising'] = df['BBW'] > df['BBW'].shift(1)
# ADX (トレンド強度と方向)
adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
df['ADX'] = adx_df['ADX_14']
df['ADX_rising'] = df['ADX'] > df['ADX'].shift(1)
# --- 2. マルチタイムフレーム (MTF) 分析 ---
# 1時間足:長期方向性 (EMA 50)
df_1h_close = df['Close'].resample('1h').last().ffill()
ema_1h = ta.ema(df_1h_close, length=50)
df['EMA_1h'] = ema_1h.reindex(df.index, method='ffill')
# 15分足:中期方向性 (EMA 20)
df_15m_close = df['Close'].resample('15min').last().ffill()
ema_15m = ta.ema(df_15m_close, length=20)
df['EMA_15m'] = ema_15m.reindex(df.index, method='ffill')
# --- 3. 相場環境の定義 ---
# 1. ボラティリティの蓄積から放出への転換
# BBWが平均以下であり、かつ上昇し始めた瞬間
df['vol_release'] = (df['BBW'] < df['BBW_avg'] * 1.1) & df['BBW_rising']
# 2. バンドの傾き(バンドウォークの兆候)
df['BBU_rising'] = df['BBU'] > df['BBU'].shift(1)
df['BBL_falling'] = df['BBL'] < df['BBL'].shift(1)
# 3. ブレイクアウト判定(BBU突破 + 直近高値更新)
df['break_high'] = (df['Close'] > df['BBU']) & (df['Close'] > df['High'].shift(1).rolling(3).max())
df['break_low'] = (df['Close'] < df['BBL']) & (df['Close'] < df['Low'].shift(1).rolling(3).min())
# --- 4. 上位足トレンドフィルター ---
df['trend_filter'] = 0
# 1h-EMA50 と 15m-EMA20 の両方で方向が一致していること
df.loc[(df['Close'] > df['EMA_1h']) & (df['Close'] > df['EMA_15m']), 'trend_filter'] = 1
df.loc[(df['Close'] < df['EMA_1h']) & (df['Close'] < df['EMA_15m']), 'trend_filter'] = -1
return df
def generate_signal(self, df):
if len(df) < 2:
return None
last = df.iloc[-1]
prev = df.iloc[-2]
# --- エントリー時間帯フィルター (ロンドン初動のノイズを除いた 16:30 - 19:00 JST) ---
current_hour = df.index[-1].hour
current_minute = df.index[-1].minute
# 16:30以降、19:00まで
if not ((current_hour == 16 and current_minute >= 30) or (17 <= current_hour < 19)):
return None
# --- 相場環境フィルター ---
# ADXが20以上であり、かつ上昇していること(トレンド加速中)
if last['ADX'] < 20 or not last['ADX_rising']:
return None
# --- エントリーロジック ---
# BUY条件:
# 1. MTFトレンド一致 (1h & 15m Up)
# 2. ボラティリティが低水準から上昇に転じた (vol_release)
# 3. 価格がBBUを突破し、BBU自体が上昇している (Band Walk)
if (last['trend_filter'] == 1 and
(last['vol_release'] or prev['vol_release']) and
last['break_high'] and
last['BBU_rising']):
return 'BUY'
# SELL条件:
# 1. MTFトレンド一致 (1h & 15m Down)
# 2. ボラティリティが低水準から上昇に転じた (vol_release)
# 3. 価格がBBLを突破し、BBL自体が下降している (Band Walk)
if (last['trend_filter'] == -1 and
(last['vol_release'] or prev['vol_release']) and
last['break_low'] and
last['BBL_falling']):
return 'SELL'
return None
MQL5コードへの翻訳(MT5用EA)
上記のPythonロジックをMetaTrader 5 (MT5) で動作するように忠実に再現したEAコードです。
//+------------------------------------------------------------------+
//| VolatilityBreakoutPro.mq5 |
//| Copyright 2026, AI Researcher |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link "https://your-blog-link.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input Parameters
input int InpBBPereiod = 20; // Bollinger Bands Period
input double InpBBDev = 2.0; // Bollinger Bands Deviation
input int InpADXPeriod = 14; // ADX Period
input int InpEMA1h = 50; // 1h EMA Period
input int InpEMA15m = 20; // 15m EMA Period
input double InpTP = 500; // Take Profit (points)
input double InpSL = 250; // Stop Loss (points)
input double InpTrailStart = 150; // Trailing Start (points)
//--- Global Handles
int hBands, hADX, hEMA1h, hEMA15m;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
hBands = iBands(_Symbol, PERIOD_M5, InpBBPereiod, 0, InpBBDev, PRICE_CLOSE);
hADX = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
hEMA1h = iMA(_Symbol, PERIOD_H1, InpEMA1h, 0, MODE_EMA, PRICE_CLOSE);
hEMA15m = iMA(_Symbol, PERIOD_M15, InpEMA15m, 0, MODE_EMA, PRICE_CLOSE);
if(hBands == INVALID_HANDLE || hADX == INVALID_HANDLE || hEMA1h == INVALID_HANDLE || hEMA15m == INVALID_HANDLE)
return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check Time Filter (JST 16:30 - 19:00 -> Adjust to Server Time)
MqlDateTime dt;
TimeCurrent(dt);
// Server time offset depends on broker, assuming GMT+2/3 common for MT5
// This is a simplified hour check. Please adjust offset based on your broker.
if(!( (dt.hour == 10 && dt.min >= 30) || (dt.hour >= 11 && dt.hour < 13) ))
return;
// Buffers
double bUpper[], bLower[], bMiddle[], adx[], ema1h[], ema15m[];
ArraySetAsSeries(bUpper, true);
ArraySetAsSeries(bLower, true);
ArraySetAsSeries(bMiddle, true);
ArraySetAsSeries(adx, true);
ArraySetAsSeries(ema1h, true);
ArraySetAsSeries(ema15m, true);
if(CopyBuffer(hBands, 1, 0, 51, bUpper) < 0) return;
if(CopyBuffer(hBands, 2, 0, 51, bLower) < 0) return;
if(CopyBuffer(hBands, 0, 0, 51, bMiddle) < 0) return;
if(CopyBuffer(hADX, 0, 0, 2, adx) < 0) return;
if(CopyBuffer(hEMA1h, 0, 0, 1, ema1h) < 0) return;
if(CopyBuffer(hEMA15m, 0, 0, 1, ema15m) < 0) return;
double close = iClose(_Symbol, PERIOD_M5, 0);
double high1 = iHigh(_Symbol, PERIOD_M5, 1);
double high2 = iHigh(_Symbol, PERIOD_M5, 2);
double high3 = iHigh(_Symbol, PERIOD_M5, 3);
double low1 = iLow(_Symbol, PERIOD_M5, 1);
double low2 = iLow(_Symbol, PERIOD_M5, 2);
double low3 = iLow(_Symbol, PERIOD_M5, 3);
// BB Width Calculation
double bbw0 = (bUpper[0] - bLower[0]) / bMiddle[0];
double bbw1 = (bUpper[1] - bLower[1]) / bMiddle[1];
double sumBBW = 0;
for(int i=0; i<50; i++) sumBBW += (bUpper[i] - bLower[i]) / bMiddle[i];
double bbwAvg = sumBBW / 50.0;
bool volRelease0 = (bbw0 < bbwAvg * 1.1) && (bbw0 > bbw1);
bool volRelease1 = (bbw1 < bbwAvg * 1.1) && (bbw1 > (bUpper[2]-bLower[2])/bMiddle[2]);
// Trend Filter
int trend = 0;
if(close > ema1h[0] && close > ema15m[0]) trend = 1;
else if(close < ema1h[0] && close < ema15m[0]) trend = -1;
// ADX Filter
bool adxCondition = (adx[0] >= 20) && (adx[0] > adx[1]);
// Entry Logic
if(PositionsTotal() == 0 && adxCondition)
{
// BUY
if(trend == 1 && (volRelease0 || volRelease1) && close > bUpper[0] && close > MathMax(high1, MathMax(high2, high3)) && bUpper[0] > bUpper[1])
{
double sl = close - InpSL * _Point;
double tp = close + InpTP * _Point;
trade.Buy(0.1, _Symbol, close, sl, tp);
}
// SELL
if(trend == -1 && (volRelease0 || volRelease1) && close < bLower[0] && close < MathMin(low1, MathMin(low2, low3)) && bLower[0] < bLower[1])
{
double sl = close + InpSL * _Point;
double tp = close - InpTP * _Point;
trade.Sell(0.1, _Symbol, close, sl, tp);
}
}
// 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(close - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point)
{
double newSL = close - InpSL * _Point;
if(newSL > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
if(PositionGetDouble(POSITION_PRICE_OPEN) - close > InpTrailStart * _Point)
{
double newSL = close + InpSL * _Point;
if(newSL < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
}
}
}