対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
もともと私は、ブレイクアウト手法の「だまし」に絶望していました。ボリンジャーバンドを抜けた瞬間に反転する。そんな経験を何度も繰り返しました。そこで、「ボラティリティが極限まで凝縮された状態」を定義し、エネルギーが爆発した瞬間だけを狙う戦略を考えました。試行錯誤の末にたどり着いたのが、このスクイーズ戦略です。
このコードを手に入れることで、あなたが得られるメリットは明確です。MT5のEA開発に必要な基礎構造を、ゼロから構築する数百時間を節約できます。長期的に破綻しないためのフィルター設計は、非常に価値のある土台となるはずです。自分だけのオリジナルEAを作るための「研究素材」として活用してください。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.25 |
| 勝率 | 53.9% |
| 総取引数 | 102 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥16644.48 |
| 最大ドローダウン | 1.88% |
| リカバリーファクター | 0.88 |
| 期待利得 (Expected Payoff) | 0.57 |
今回の検証結果の「限界」と「ダメ出し」
このロジックの最大の弱点は、取引回数の少なさと収益性の低さです。10年で102回という回数は、年平均に直すとわずか10回程度になります。チャンスを待ちすぎるあまり、多くの利益機会を逃しています。
PF 1.25という数字も、プロの視点から見れば平凡な成績です。これは、カーブフィッティングを徹底的に排除し、汎用性を重視した結果といえます。また、トレンドが緩やかな相場では全く機能せず、機会損失が激しい点も明確な課題です。
ロジックの技術的詳細
本ロジックは、マルチタイムフレーム分析とボラティリティの凝縮を組み合わせた設計です。
| 要素 | 設定・条件 | 役割 |
|---|---|---|
| 長期トレンド | 1時間足 EMA 200 | 全体的な方向性の決定 |
| 中期トレンド | 15分足 EMA 50 + 傾き判定 | トレンドの勢いを確認 |
| 凝縮判定 | BB(20, 2.2) $\subset$ KC(20, 1.2) | ボラティリティのスクイーズを検知 |
| 勢い判定 | ADX > 20 + RSI (50-70 / 30-50) | ブレイクアウトの正当性を確認 |
| 時間フィルター | 14時 $\sim$ 翌4時 | 流動性の高い時間帯に限定 |
| 出口戦略 | TP: 50pips / SL: 15pips | リスクリワード 3.33:1 の追求 |
どう改善すべきか(次なる展望)
このロジックを「化けさせる」には、環境認識の精度を上げることです。現在は単純なEMAの傾きで判定していますが、ここに上位足のサポレジラインの概念を加えれば、勝率はさらに向上するでしょう。
また、固定の利確・損切りではなく、ATRに基づいたダイナミックな決済ロジックへの変更を推奨します。相場のボラティリティに合わせて幅を変えることで、期待利得の底上げが期待できます。この無料コードをベースに、ぜひあなた独自のフィルターを組み合わせてみてください。
Pythonコードの公開
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
class VSMBStrategy(BaseStrategy):
def __init__(self):
super().__init__(
name="Volatility Squeeze & Momentum Breakout",
default_tp_pips=50.0, # PF向上と勝率確保のため、現実的な利確目標へ調整
default_sl_pips=15.0, # リスクリワード 3.33:1 を維持し、リカバリーファクターを追求
enable_trailing_stop=True,
trail_start_pips=15.0 # 利益確定の確実性を高め、ドローダウンを抑制
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
def calculate_indicators(self, df):
# --- MTF Trend & Slope Filter ---
# 1h EMA 200 (長期トレンド)
h1_close = df['Close'].resample('1h').last().shift(1)
h1_ema = ta.ema(h1_close, length=200)
df['htf_ema200'] = h1_ema.reindex(df.index, method='ffill')
# 15m EMA 50 (中期トレンド + 傾き判定)
m15_close = df['Close'].resample('15min').last().shift(1)
m15_ema = ta.ema(m15_close, length=50)
df['mtf_ema50'] = m15_ema.reindex(df.index, method='ffill')
# 傾きを判定し、トレンドの「勢い」を確認してだましを排除
df['mtf_ema_slope_bull'] = df['mtf_ema50'] > df['mtf_ema50'].shift(3)
df['mtf_ema_slope_bear'] = df['mtf_ema50'] < df['mtf_ema50'].shift(3)
df['htf_trend_bull'] = (df['Close'] > df['htf_ema200']) & (df['Close'] > df['mtf_ema50']) & df['mtf_ema_slope_bull']
df['htf_trend_bear'] = (df['Close'] < df['htf_ema200']) & (df['Close'] < df['mtf_ema50']) & df['mtf_ema_slope_bear']
# --- Volatility Indicators (5m) ---
# BB期間を20に回帰させつつ、標準偏差を2.2へ引き上げ、より精度の高いブレイクアウトを検知
bb = ta.bbands(df['Close'], length=20, std=2.2)
df['bb_lower'] = bb.iloc[:, 0]
df['bb_upper'] = bb.iloc[:, 2]
# Keltner Channels (スカラーを1.2へタイト化し、より「凝縮された」スクイーズを定義)
kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=1.2)
df['kc_lower'] = kc.iloc[:, 0]
df['kc_upper'] = kc.iloc[:, 2]
# ATR & Volatility Baseline
df['atr'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
df['atr_sma'] = df['atr'].rolling(window=60).mean()
df['volatility_ok'] = df['atr'] / df['atr_sma'] > 0.8
# ADX (Trend Strength)
adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
df['adx'] = adx_df.iloc[:, 0]
# --- Momentum Filter (RSI) ---
# RSIを単なる上限ではなく、トレンド方向への「勢い」の証明として利用
df['rsi'] = ta.rsi(df['Close'], length=14)
# --- Squeeze Logic ---
df['is_squeezed'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
# 凝縮期間を3本に設定し、エネルギー充填を確認
df['squeeze_confirmed'] = df['is_squeezed'].rolling(window=3).sum() >= 3
# --- Time Filter ---
df['hour'] = df.index.hour
df['time_ok'] = (df['hour'] >= 14) | (df['hour'] <= 4)
return df
def generate_signal(self, df):
if len(df) < 200:
return None
row = df.iloc[-1]
prev_row = df.iloc[-2]
# Common conditions
is_volatility_ok = row['volatility_ok']
is_trend_strong = row['adx'] > 20 # 20に緩和しつつ、EMA傾きで質を担保
is_time_ok = row['time_ok']
was_squeezed = prev_row['squeeze_confirmed']
# BREAKOUT Quality Check: ブレイクアウト時の足が平均的なボラティリティを超えているか
# (Volume不使用ルールの代替として、ローソク足のサイズで勢いを判定)
candle_range = row['High'] - row['Low']
is_explosive = candle_range > (row['atr'] * 0.8)
# Long Entry: HTF Bull + Slope OK + Vol OK + ADX OK + Time OK + Prev Squeeze + BB Breakout + RSI Momentum
if (row['htf_trend_bull'] and is_volatility_ok and is_trend_strong and
is_time_ok and was_squeezed and is_explosive and
row['Close'] > row['bb_upper'] and
row['Close'] > row.get('Open', 0) and
50 < row['rsi'] < 70): # RSI 50-70の「強い上昇トレンド」圏内
return 'BUY'
# Short Entry: HTF Bear + Slope OK + Vol OK + ADX OK + Time OK + Prev Squeeze + BB Breakout + RSI Momentum
if (row['htf_trend_bear'] and is_volatility_ok and is_trend_strong and
is_time_ok and was_squeezed and is_explosive and
row['Close'] < row['bb_lower'] and
row['Close'] < row.get('Open', float('inf')) and
30 < row['rsi'] < 50): # RSI 30-50の「強い下落トレンド」圏内
return 'SELL'
return None
MQL5コードへの翻訳
//+------------------------------------------------------------------+
//| VolatilitySqueezeMomentum.mq5 |
//| Copyright 2026, EA Research Lab |
//| https://example.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, EA Research Lab"
#property link "https://example.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input Parameters
input double InpLotSize = 0.1; // Lot Size
input int InpTP = 500; // Take Profit (points)
input int InpSL = 150; // Stop Loss (points)
input int InpTrailingStart = 150; // Trailing Start (points)
input int InpBBPeriod = 20; // BB Period
input double InpBBStdDev = 2.2; // BB StdDev
input int InpKCPeriod = 20; // KC Period
input double InpKCScalar = 1.2; // KC Scalar
input int InpEMA_Long = 200; // H1 EMA Period
input int InpEMA_Mid = 50; // M15 EMA Period
//--- Global Handles
int hBB, hKC, hATR, hADX, hRSI, hEMA_H1, hEMA_M15;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
hBB = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
hKC = iCustom(_Symbol, PERIOD_M5, "KeltnerChannel", InpKCPeriod, InpKCScalar); // Assuming custom KC indicator
hATR = iATR(_Symbol, PERIOD_M5, 14);
hADX = iADX(_Symbol, PERIOD_M5, 14);
hRSI = iRSI(_Symbol, PERIOD_M5, 14, PRICE_CLOSE);
hEMA_H1 = iMA(_Symbol, PERIOD_H1, InpEMA_Long, 0, MODE_EMA, PRICE_CLOSE);
hEMA_M15 = iMA(_Symbol, PERIOD_M15, InpEMA_Mid, 0, MODE_EMA, PRICE_CLOSE);
if(hBB == INVALID_HANDLE || hEMA_H1 == INVALID_HANDLE || hEMA_M15 == INVALID_HANDLE) return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(!IsNewBar()) return;
double close[], open[], high[], low[];
CopyClose(_Symbol, PERIOD_M5, 0, 3, close);
CopyOpen(_Symbol, PERIOD_M5, 0, 3, open);
CopyHigh(_Symbol, PERIOD_M5, 0, 3, high);
CopyLow(_Symbol, PERIOD_M5, 0, 3, low);
double bb_up[], bb_low[], kc_up[], kc_low[], atr[], adx[], rsi[];
CopyBuffer(hBB, 1, 0, 3, bb_up);
CopyBuffer(hBB, 2, 0, 3, bb_low);
// Simplified KC retrieval (assuming typical buffer indices)
CopyBuffer(hKC, 1, 0, 3, kc_up);
CopyBuffer(hKC, 2, 0, 3, kc_low);
CopyBuffer(hATR, 0, 0, 60, atr);
CopyBuffer(hADX, 0, 0, 3, adx);
CopyBuffer(hRSI, 0, 0, 3, rsi);
double ema_h1[], ema_m15[];
CopyBuffer(hEMA_H1, 0, 0, 1, ema_h1);
CopyBuffer(hEMA_M15, 0, 0, 4, ema_m15);
//--- Technical Logic
bool mtf_slope_bull = ema_m15[0] > ema_m15[3];
bool mtf_slope_bear = ema_m15[0] < ema_m15[3];
bool htf_trend_bull = (close[0] > ema_h1[0] && close[0] > ema_m15[0] && mtf_slope_bull);
bool htf_trend_bear = (close[0] < ema_h1[0] && close[0] < ema_m15[0] && mtf_slope_bear);
bool is_squeezed = (bb_up[0] < kc_up[0] && bb_low[0] > kc_low[0]);
bool was_squeezed = (bb_up[1] < kc_up[1] && bb_low[1] > kc_low[1]); // Simplified to 1 bar for example
double atr_sma = 0;
for(int i=0; i<60; i++) atr_sma += atr[i];
atr_sma /= 60;
bool volatility_ok = (atr[0] / atr_sma) > 0.8;
bool is_explosive = (high[0] - low[0]) > (atr[0] * 0.8);
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
bool time_ok = (dt.hour >= 14 || dt.hour <= 4);
//--- Entry Signal
if(htf_trend_bull && volatility_ok && adx[0] > 20 && time_ok && was_squeezed && is_explosive && close[0] > bb_up[0] && close[0] > open[0] && rsi[0] > 50 && rsi[0] < 70)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point;
trade.Buy(InpLotSize, _Symbol, 0, sl, tp);
}
else if(htf_trend_bear && volatility_ok && adx[0] > 20 && time_ok && was_squeezed && is_explosive && close[0] < bb_low[0] && close[0] < open[0] && rsi[0] > 30 && rsi[0] < 50)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point;
trade.Sell(InpLotSize, _Symbol, 0, sl, tp);
}
ManageTrailingStop();
}
void ManageTrailingStop()
{
for(int i=PositionsTotal()-1; i>=0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
if(SymbolInfoDouble(_Symbol, SYMBOL_BID) - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailingStart * _Point)
{
double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailingStart * _Point;
if(new_sl > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
if(PositionGetDouble(POSITION_PRICE_OPEN) - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > InpTrailingStart * _Point)
{
double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTrailingStart * _Point;
if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
}
}
}
bool IsNewBar()
{
static datetime last_time = 0;
datetime curr_time = iTime(_Symbol, PERIOD_M5, 0);
if(last_time != curr_time) { last_time = curr_time; return true; }
return false;
}