対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
もともと私はブレイクアウト手法の「だまし」に悩んでいました。指標発表時の乱高下で資金を溶かした経験があります。そこで「エネルギーが蓄積された状態」を数値化したいと考えました。ボリンジャーバンドとケルトナーチャネルの相関関係に着目し、試行錯誤を繰り返しました。開発過程では何度もパラメータ調整に迷い、心折れそうになった記憶があります。
このコードを入手すれば、MT5のEA開発にかかる数百時間を節約できます。ゼロからロジックを組む苦労はありません。堅牢な土台があるため、自分好みのフィルターを追加して改造することが可能です。オリジナルEAを完成させるための最高のベースキャンプになります。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.51 |
| 勝率 | 52.9% |
| 総取引数 | 121 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥49641.89 |
| 最大ドローダウン | 2.36% |
| リカバリーファクター | 2.10 |
| 期待利得 (Expected Payoff) | 0.71 |
今回の検証結果の「限界」と「ダメ出し」
このロジックは安定していますが、収益性は低いです。10年で121回という取引数は少なすぎます。年平均に直すと、月に1回程度しかエントリーしません。これは、非常に厳しいフィルターを重ねた結果です。
また、レンジ相場での微益を積み上げる力に欠けます。トレンドが出なければ、ただ時間を消費するだけです。PF 1.51という数字は合格点ですが、資産を爆発的に増やす力はありません。保守的すぎる設計が、機会損失を招いていると言えます。
ロジックの技術的詳細
本ロジックは、低ボラティリティからの解放を狙う戦略です。以下の条件をすべて満たしたときのみエントリーします。
| 項目 | 条件・設定値 | 役割 |
|---|---|---|
| 上位足トレンド | 1時間足 EMA(200, 50, 20) の並び | 環境認識(方向性の固定) |
| スクイーズ判定 | BB(20, 2.0) $\subset$ KC(20, 1.2) | エネルギー蓄積の検知 |
| ブレイクアウト | BB上限/下限 および 20期間高値/安値の突破 | モメンタムの発生確認 |
| トレンド強度 | ADX(14) > 20 | トレンド発生の確信度向上 |
| リスク管理 | SL 20pips / TP 60pips | リスクリワード 1:3 の固定 |
どう改善すべきか(次なる展望)
このロジックをベースに、取引回数を増やす工夫を検討してください。例えば、ADXの閾値を20から15へ下げることで、エントリー機会が増える可能性があります。
また、固定pipsの利確ではなく、ATRを用いた可変利確を導入すべきです。相場のボラティリティに合わせて利確幅を変えれば、収益性は向上します。プロのEAは、ここで独自の環境認識フィルターを加えています。ぜひ、あなた自身のアイデアでこの「原石」を磨き上げてください。
Pythonソースコード
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np
class VSMBStrategy(BaseStrategy):
"""
Volatility Squeeze Momentum Breakout (VSMB) Strategy - Quant Optimized
- Optimization Target: Maximize PF, Recovery Factor, and Trade Count while minimizing MaxDD.
- Logic: BB inside KC (Squeeze) -> Breakout (Expansion) -> Trend Confirmation (H1 EMA & ADX).
- Fix: Resolved KeyError by using positional indexing for pandas_ta outputs.
"""
def __init__(self):
# 【Quant Tuning】
# TP/SLを固定pipsからボラティリティ適応的に設定するためのベース値。
# リカバリーファクター向上のため、リスクリワード比を 1:3.0 に最適化。
super().__init__(
name="VSMB_Strategy_Optimized",
default_tp_pips=60.0,
default_sl_pips=20.0,
enable_trailing_stop=True,
trail_start_pips=15.0
)
# 時間足定義
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15min", "1h"]
def calculate_indicators(self, df):
# --- 1. 上位足(1時間足)トレンドフィルター ---
# ルックアヘッドバイアス排除のため、resample後の計算にshift(1)を適用
h1_resampled = df[['Close', 'High', 'Low']].resample('1h').last()
# EMA 200: 長期トレンド, EMA 50: 中期トレンド, EMA 20: 短期トレンド
h1_ema200 = ta.ema(h1_resampled['Close'], length=200)
h1_ema50 = ta.ema(h1_resampled['Close'], length=50)
h1_ema20 = ta.ema(h1_resampled['Close'], length=20)
# トレンド判定の最適化: EMAのパーフェクトオーダーに近い状態を定義
h1_trend = pd.Series(0, index=h1_resampled.index)
h1_trend.loc[(h1_resampled['Close'] > h1_ema200) & (h1_ema20 > h1_ema50)] = 1
h1_trend.loc[(h1_resampled['Close'] < h1_ema200) & (h1_ema20 < h1_ema50)] = -1
# 執行足(5m)へマージ
df['h1_trend'] = h1_trend.shift(1).reindex(df.index, method='ffill')
# --- 2. 執行足 (5m) インジケーター計算 ---
# 【KeyError対策】カラム名を直接指定せず、ilocによる位置指定または動的取得を行う
# ボリンジャーバンド (20, 2.0)
bb = ta.bbands(df['Close'], length=20, std=2.0)
# pandas_taのbbandsは [BBL, BBM, BBU] の順で出力される
df['bb_lower'] = bb.iloc[:, 0]
df['bb_upper'] = bb.iloc[:, 2]
# ケルトナーチャネル (20, 1.2)
# 【Tuning】scalarを1.5->1.2に下方修正し、スクイーズ判定の感度を向上させ取引数を確保
kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=1.2)
# pandas_taのkcは [KCL, KC_Mid, KCU] の順で出力される
df['kc_lower'] = kc.iloc[:, 0]
df['kc_upper'] = kc.iloc[:, 2]
# ADX (14)
adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
df['adx'] = adx_df['ADX_14']
# ATR (14) - リスク管理用(Volumeの代替)
df['atr'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
# --- 3. 戦略的判定用データ ---
# Squeeze判定: BBがKCの内部にあるか (低ボラティリティ局面の特定)
df['is_squeeze'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
# ドンチアン的な高値・安値ブレイク判定 (20期間)
# 現在足を含めないためshift(1)を徹底
df['recent_high'] = df['High'].rolling(window=20).max().shift(1)
df['recent_low'] = df['Low'].rolling(window=20).min().shift(1)
# ADXの方向性判定: 20以上のトレンド圏で、かつ上昇しているか
df['adx_rising'] = df['adx'] > df['adx'].shift(1)
return df
def generate_signal(self, df):
# 最新の行および1本前の行データを取得
if len(df) < 2:
return None
row = df.iloc[-1]
prev_row = df.iloc[-2]
# --- Long Entry Logic (Optimized) ---
# 1. 上位足トレンドが上昇 (H1 Trend Bull)
# 2. 1本前までスクイーズ状態であった (Energy Accumulation)
# 3. 現在足でスクイーズを解消し、BB UpperがKC Upperを上抜ける (Volatility Breakout)
# 4. 価格がBB Upperおよび直近高値を同時にブレイク (Strong Momentum)
# 5. ADXがトレンド閾値(20)を超えている (Trend Confirmation)
long_condition = (
(row['h1_trend'] == 1) and
(prev_row['is_squeeze'] == True) and
(row['bb_upper'] > row['kc_upper']) and
(row['Close'] > row['bb_upper']) and
(row['Close'] > row['recent_high']) and
(row['adx'] > 20)
)
# --- Short Entry Logic (Optimized) ---
# 1. 上位足トレンドが下降 (H1 Trend Bear)
# 2. 1本前までスクイーズ状態であった (Energy Accumulation)
# 3. 現在足でスクイーズを解消し、BB LowerがKC Lowerを下抜ける (Volatility Breakout)
# 4. 価格がBB Lowerおよび直近安値を同時にブレイク (Strong Momentum)
# 5. ADXがトレンド閾値(20)を超えている (Trend Confirmation)
short_condition = (
(row['h1_trend'] == -1) and
(prev_row['is_squeeze'] == True) and
(row['bb_lower'] < row['kc_lower']) and
(row['Close'] < row['bb_lower']) and
(row['Close'] < row['recent_low']) and
(row['adx'] > 20)
)
if long_condition:
return 'BUY'
elif short_condition:
return 'SELL'
return None
MQL5コード(MT5用)
//+------------------------------------------------------------------+
//| VSMB_Optimized.mq5 |
//| Copyright 2026, AI Researcher |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link "https://www.mql5.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input parameters
input int InpPeriod = 20; // Period for BB and KC
input double InpBBStdDev = 2.0; // Bollinger Bands StdDev
input double InpKCScalar = 1.2; // Keltner Channel Scalar
input int InpADXPeriod = 14; // ADX Period
input int InpADXMin = 20; // Minimum ADX value
input double InpTP = 600; // Take Profit in points
input double InpSL = 200; // Stop Loss in points
input double InpTrailStart = 150; // Trailing Start in points
input double InpLotSize = 0.1; // Trade Lot Size
//--- Global variables
CTrade trade;
int handle_h1_ema200, handle_h1_ema50, handle_h1_ema20;
int handle_bb, handle_adx, handle_atr;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// H1 Timeframe Indicators
handle_h1_ema200 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
handle_h1_ema50 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
handle_h1_ema20 = iMA(_Symbol, PERIOD_H1, 20, 0, MODE_EMA, PRICE_CLOSE);
// M5 Timeframe Indicators
handle_bb = iBands(_Symbol, PERIOD_M5, InpPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handle_adx = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
handle_atr = iATR(_Symbol, PERIOD_M5, InpADXPeriod);
if(handle_h1_ema200 == INVALID_HANDLE || handle_bb == INVALID_HANDLE || handle_adx == INVALID_HANDLE)
return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Check for new bar on M5
static datetime last_bar = 0;
datetime current_bar = iTime(_Symbol, PERIOD_M5, 0);
if(last_bar == current_bar) return;
last_bar = current_bar;
// Data arrays
double ema200[], ema50[], ema20[], close_h1[];
double bb_upper[], bb_lower[], adx[], close_m5[], high_m5[], low_m5[];
ArraySetAsSeries(ema200, true);
ArraySetAsSeries(ema50, true);
ArraySetAsSeries(ema20, true);
ArraySetAsSeries(close_h1, true);
ArraySetAsSeries(bb_upper, true);
ArraySetAsSeries(bb_lower, true);
ArraySetAsSeries(adx, true);
ArraySetAsSeries(close_m5, true);
ArraySetAsSeries(high_m5, true);
ArraySetAsSeries(low_m5, true);
if(CopyBuffer(handle_h1_ema200, 0, 1, 1, ema200) <= 0) return;
if(CopyBuffer(handle_h1_ema50, 0, 1, 1, ema50) <= 0) return;
if(CopyBuffer(handle_h1_ema20, 0, 1, 1, ema20) <= 0) return;
if(CopyClose(_Symbol, PERIOD_H1, 1, 1, close_h1) <= 0) return;
if(CopyBuffer(handle_bb, 1, 0, 2, bb_upper) <= 0) return;
if(CopyBuffer(handle_bb, 2, 0, 2, bb_lower) <= 0) return;
if(CopyBuffer(handle_adx, 0, 0, 2, adx) <= 0) return;
if(CopyClose(_Symbol, PERIOD_M5, 0, 2, close_m5) <= 0) return;
if(CopyHigh(_Symbol, PERIOD_M5, 0, 21, high_m5) <= 0) return;
if(CopyLow(_Symbol, PERIOD_M5, 0, 21, low_m5) <= 0) return;
// Keltner Channel calculation (Manual)
double atr_buffer[];
ArraySetAsSeries(atr_buffer, true);
CopyBuffer(handle_atr, 0, 0, 2, atr_buffer);
double ema_m5[];
int handle_ema_m5 = iMA(_Symbol, PERIOD_M5, InpPeriod, 0, MODE_EMA, PRICE_CLOSE);
ArraySetAsSeries(ema_m5, true);
CopyBuffer(handle_ema_m5, 0, 0, 2, ema_m5);
double kc_upper_curr = ema_m5[0] + (InpKCScalar * atr_buffer[0]);
double kc_lower_curr = ema_m5[0] - (InpKCScalar * atr_buffer[0]);
double kc_upper_prev = ema_m5[1] + (InpKCScalar * atr_buffer[1]);
double kc_lower_prev = ema_m5[1] - (InpKCScalar * atr_buffer[1]);
// Squeeze Detection
bool is_squeeze_prev = (bb_upper[1] < kc_upper_prev) && (bb_lower[1] > kc_lower_prev);
// Recent High/Low (Donchian 20)
double recent_high = high_m5[1];
double recent_low = low_m5[1];
for(int i=1; i<21; i++) {
if(high_m5[i] > recent_high) recent_high = high_m5[i];
if(low_m5[i] < recent_low) recent_low = low_m5[i];
}
// H1 Trend
int h1_trend = 0;
if(close_h1[0] > ema200[0] && ema20[0] > ema50[0]) h1_trend = 1;
if(close_h1[0] < ema200[0] && ema20[0] < ema50[0]) h1_trend = -1;
// Entry Logic
if(PositionsTotal() == 0)
{
// Long
if(h1_trend == 1 && is_squeeze_prev && bb_upper[0] > kc_upper_curr &&
close_m5[0] > bb_upper[0] && close_m5[0] > recent_high && adx[0] > InpADXMin)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP * _Point;
trade.Buy(InpLotSize, _Symbol, 0, sl, tp, "VSMB_Long");
}
// Short
else if(h1_trend == -1 && is_squeeze_prev && bb_lower[0] < kc_lower_curr &&
close_m5[0] < bb_lower[0] && close_m5[0] < recent_low && adx[0] > InpADXMin)
{
double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL * _Point;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP * _Point;
trade.Sell(InpLotSize, _Symbol, 0, sl, tp, "VSMB_Short");
}
}
// Trailing Stop
for(int i=PositionsTotal()-1; i>=0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if(bid - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point)
{
double new_sl = bid - InpSL * _Point;
if(new_sl > PositionGetDouble(POSITION_SL))
trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(PositionGetDouble(POSITION_PRICE_OPEN) - ask > InpTrailStart * _Point)
{
double new_sl = ask + InpSL * _Point;
if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0)
trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
}
}
}