対象通貨ペア:GBPUSD(ポンドドル) / 使用時間足:5分足専用
もともと私はブレイクアウト手法の「だまし」に悩み続けてきました。多くのインジケーターを組み合わせても、レンジ相場で資金を削られる経験を繰り返しました。そこで、単なる突破ではなく「エネルギーの凝縮(スクイーズ)」と「トレンドの完全一致」に注目しました。試行錯誤の結果、生存率を極限まで高めたこのロジックに到達しました。
このコードをベースにすれば、MT5のEAをゼロから開発する数百時間を節約できます。ロジックの組み方やフィルターの掛け方は、実戦的な土台として最適です。自分だけのオリジナルEAを作るための研究素材として活用してください。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.21 |
| 勝率 | 53.0% |
| 総取引数 | 202 回 |
| 純利益 (0.1ロット/1万通貨時) | $595.24 |
| 最大ドローダウン | 4.24% |
| リカバリーファクター | 1.40 |
| 期待利得 (Expected Payoff) | 0.57 |
今回の検証結果の「限界」と「ダメ出し」
このロジックの最大の弱点は、利益の伸び悩みです。PF1.21という数値は、実運用では非常に心許ない水準といえます。これは、エントリー条件を厳選しすぎたためです。
低期待値のトレードを排除した結果、取引回数が激減しました。その結果、大きなトレンドを逃す場面が多々あります。また、固定の利確・損切り幅に依存しているため、相場のボラティリティ変動に柔軟に対応できていません。
ロジックの技術的詳細
本ロジックは、上位足の環境認識と執行足のエネルギー凝縮を組み合わせた戦略です。
| 項目 | 設定・条件 | 役割 |
|---|---|---|
| 上位足 (1h) トレンド | EMA200 < EMA50 < 価格 かつ ADX > 30 | 強力なトレンド方向の限定 |
| エネルギー凝縮 | BBがKCの内部に位置する状態が10本中6本以上 | 爆発前の溜め(スクイーズ)を検知 |
| ブレイクアウト | ドンチアン25本高値/安値を更新 | 意識される価格帯の突破 |
| インパルス判定 | 足の実体 > 直近10本の平均実体 | 勢いのある本物の動きを抽出 |
| 執行足 (5m) 指標 | ADX > 25 かつ 上昇傾向 | トレンド発生の確定 |
| 時間帯制限 | 15:00 〜 22:00 (サーバー時間) | 流動性の高い欧州・NY時間に限定 |
どう改善すべきか(次なる展望)
このコードはあくまで「破綻しない土台」です。ここから収益性を高めるには、以下の改善が考えられます。
- ダイナミックな利確設定: 固定pipsではなく、ATR(平均真域)に基づいた利確幅への変更。
- 時間帯の最適化: 通貨ペアごとのボラティリティピークを再分析し、フィルター時間を調整すること。
- マルチタイムフレームの深化: 15分足のトレンド方向を中継させることで、エントリー精度をさらに向上させる手法。
プロのEAは、こうした環境認識を独自のロジックで補完しています。ぜひこのコードを改造して、あなただけの最適解を見つけてください。
Pythonソースコード
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
class VolatilityCompressionExpansionStrategy(BaseStrategy):
def __init__(self):
# 【クオンツ外科処置:利益効率の極大化】
# PF 1.04 → 1.20以上、リカバリーファクター 0.51 → 3.00への飛躍的な改善を目指す。
# Max DDに余裕がある(7.5%)ため、リスクリワード比を3.3:1へ引き上げ、1回あたりの期待利得を最大化。
# 取引回数が多すぎる(837回)ため、エントリー条件を「超厳選」し、低期待値のトレードを徹底排除する。
super().__init__(
name="VolatilityCompressionExpansion_V3",
default_tp_pips=100.0,
default_sl_pips=30.0,
enable_trailing_stop=True,
trail_start_pips=30.0
)
# 【ルール5】ベース時間足の定義
self.base_timeframe = "5m"
# 【ルール6】画像認識AI用時間足の定義
self.vision_timeframes = ["5m", "15min", "1h"]
def calculate_indicators(self, df):
# --- 1. 上位足(1h)の環境認識 (ルックアヘッドバイアス完全排除) ---
h1_df = df.resample('1h').agg({
'Open': 'first',
'High': 'max',
'Low': 'min',
'Close': 'last'
})
# 【処方箋】トレンド判定の重層化:EMA200(長期)+ EMA50(中期)のパーフェクトオーダーを要求
h1_ema200 = ta.ema(h1_df['Close'], length=200)
h1_ema50 = ta.ema(h1_df['Close'], length=50)
# H1 ADX: 閾値を30に引き上げ、「最強のトレンド」のみを抽出
h1_adx_df = ta.adx(h1_df['High'], h1_df['Low'], h1_df['Close'], length=14)
h1_adx = h1_adx_df['ADX_14'] if h1_adx_df is not None else None
# 強気:価格 > EMA50 > EMA200 かつ ADX > 30
h1_bull = (h1_df['Close'] > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_adx > 30)
# 弱気:価格 < EMA50 < EMA200 かつ ADX > 30
h1_bear = (h1_df['Close'] < h1_ema50) & (h1_ema50 < h1_ema200) & (h1_adx > 30)
df['h1_bull'] = h1_bull.shift(1).reindex(df.index, method='ffill')
df['h1_bear'] = h1_bear.shift(1).reindex(df.index, method='ffill')
# --- 2. 執行足(5m)のテクニカル計算 ---
# ボリンジャーバンド (BB) - iloc参照でエラー回避
bb = ta.bbands(df['Close'], length=20, std=2.0)
df['bb_lower'] = bb.iloc[:, 0]
df['bb_upper'] = bb.iloc[:, 2]
# ケルトナーチャネル (KC) - iloc参照でエラー回避
kc = ta.kc(df['High'], df['Low'], df['Close'], length=20, scalar=2.0)
df['kc_lower'] = kc.iloc[:, 0]
df['kc_upper'] = kc.iloc[:, 2]
# Squeeze判定
df['is_squeeze'] = (df['bb_upper'] < df['kc_upper']) & (df['bb_lower'] > df['kc_lower'])
# 【処方箋】だまし排除:Squeeze継続期間を10本中6本以上に厳格化し、エネルギー凝縮を確実にする
df['squeeze_count'] = df['is_squeeze'].rolling(window=10).sum()
# 【処方箋】ドンチアンチャネル期間を25に延長し、より強力なブレイクアウトのみを捕捉
df['donchian_high'] = df['High'].rolling(window=25).max().shift(1)
df['donchian_low'] = df['Low'].rolling(window=25).min().shift(1)
# 執行足 ADX: 閾値を25に引き上げ
adx_m5 = ta.adx(df['High'], df['Low'], df['Close'], length=14)
df['adx_m5'] = adx_m5['ADX_14'] if adx_m5 is not None else 0
# 【処方箋】インパルス判定:ブレイクした足の実体が直近平均以上のサイズであることを要求
df['body_size'] = (df['Close'] - df['Open']).abs()
df['avg_body_size'] = df['body_size'].rolling(window=10).mean()
# 時間帯フィルター (欧州・NYの最活発時間帯に限定)
df['hour'] = df.index.hour
df['time_filter'] = (df['hour'] >= 15) & (df['hour'] <= 22)
return df
def generate_signal(self, df):
# 最新の行と過去の行を取得
last = df.iloc[-1]
prev = df.iloc[-2] if len(df) > 1 else last
# --- 買いシグナル条件 ---
buy_signal = (
(last['h1_bull'] == True) and
(prev['squeeze_count'] >= 6) and
(last['Close'] > last['donchian_high']) and
(last['Close'] > last['bb_upper']) and
(last['body_size'] > last['avg_body_size']) and
(last['adx_m5'] > 25) and
(last['adx_m5'] > prev['adx_m5']) and
(last['time_filter'] == True)
)
# --- 売りシグナル条件 ---
sell_signal = (
(last['h1_bear'] == True) and
(prev['squeeze_count'] >= 6) and
(last['Close'] < last['donchian_low']) and
(last['Close'] < last['bb_lower']) and
(last['body_size'] > last['avg_body_size']) and
(last['adx_m5'] > 25) and
(last['adx_m5'] > prev['adx_m5']) and
(last['time_filter'] == True)
)
if buy_signal:
return 'BUY'
elif sell_signal:
return 'SELL'
return None
MQL5コード(MT5用)
//+------------------------------------------------------------------+
//| VolatilityCompressionExpansion.mq5|
//| Copyright 2026, Quants Engineer |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quants Engineer"
#property link "https://your-blog-url.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input parameters
input int InpTP_Pips = 100; // Take Profit (Pips)
input int InpSL_Pips = 30; // Stop Loss (Pips)
input int InpTrailStart = 30; // Trailing Start (Pips)
input int InpADX_Threshold_H1 = 30; // H1 ADX Threshold
input int InpADX_Threshold_M5 = 25; // M5 ADX Threshold
input int InpSqueezeMinBars = 6; // Min Squeeze Bars in 10
input int InpDonchianPeriod = 25; // Donchian Period
//--- Global variables
CTrade trade;
int handle_ema200_h1, handle_ema50_h1, handle_adx_h1;
int handle_bb_m5, handle_adx_m5;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// H1 Indicators
handle_ema200_h1 = iMA(_Symbol, PERIOD_H1, 200, 0, MODE_EMA, PRICE_CLOSE);
handle_ema50_h1 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_EMA, PRICE_CLOSE);
handle_adx_h1 = iADX(_Symbol, PERIOD_H1, 14);
// M5 Indicators
handle_bb_m5 = iBands(_Symbol, PERIOD_M5, 20, 0, 2.0, PRICE_CLOSE);
handle_adx_m5 = iADX(_Symbol, PERIOD_M5, 14);
if(handle_ema200_h1 == INVALID_HANDLE || handle_adx_m5 == INVALID_HANDLE) return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
if(!isNewBar()) return;
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.hour < 15 || dt.hour > 22) return;
// H1 Data
double ema200_h1[], ema50_h1[], adx_h1[], close_h1[];
CopyBuffer(handle_ema200_h1, 0, 1, 1, ema200_h1);
CopyBuffer(handle_ema50_h1, 0, 1, 1, ema50_h1);
CopyBuffer(handle_adx_h1, 0, 1, 1, adx_h1);
CopyClose(_Symbol, PERIOD_H1, 1, 1, close_h1);
bool h1_bull = (close_h1[0] > ema50_h1[0] && ema50_h1[0] > ema200_h1[0] && adx_h1[0] > InpADX_Threshold_H1);
bool h1_bear = (close_h1[0] < ema50_h1[0] && ema50_h1[0] < ema200_h1[0] && adx_h1[0] > InpADX_Threshold_H1);
// M5 Data
double bb_upper[], bb_lower[], adx_m5[], close_m5[], open_m5[], high_m5[], low_m5[];
CopyBuffer(handle_bb_m5, 1, 1, 1, bb_upper);
CopyBuffer(handle_bb_m5, 2, 1, 1, bb_lower);
CopyBuffer(handle_adx_m5, 0, 1, 2, adx_m5);
CopyClose(_Symbol, PERIOD_M5, 1, 1, close_m5);
CopyOpen(_Symbol, PERIOD_M5, 1, 1, open_m5);
CopyHigh(_Symbol, PERIOD_M5, 1, InpDonchianPeriod, high_m5);
CopyLow(_Symbol, PERIOD_M5, 1, InpDonchianPeriod, low_m5);
// Donchian
double d_high = high_m5[ArrayMaximum(high_m5)];
double d_low = low_m5[ArrayMinimum(low_m5)];
// Squeeze check (BB vs KC Simplified)
int squeeze_count = 0;
for(int i=1; i<=10; i++) {
double b_up[], b_lo[], k_up[], k_lo[];
// Simplified KC calculation for logic representation
// In actual implementation, use iATR and iMA to calculate KC
if(getBBUpper(i) < getKCUpper(i) && getBBLower(i) > getKCLower(i)) squeeze_count++;
}
// Body Size
double body = MathAbs(close_m5[0] - open_m5[0]);
double avg_body = getAvgBody(10);
// Entry Logic
if(h1_bull && squeeze_count >= InpSqueezeMinBars && close_m5[0] > d_high && close_m5[0] > bb_upper[0] && body > avg_body && adx_m5[0] > InpADX_Threshold_M5 && adx_m5[0] > adx_m5[1]) {
double sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpSL_Pips * _Point * 10;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpTP_Pips * _Point * 10;
trade.Buy(0.1, _Symbol, 0, sl, tp);
}
else if(h1_bear && squeeze_count >= InpSqueezeMinBars && close_m5[0] < d_low && close_m5[0] < bb_lower[0] && body > avg_body && adx_m5[0] > InpADX_Threshold_M5 && adx_m5[0] > adx_m5[1]) {
double sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpSL_Pips * _Point * 10;
double tp = SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpTP_Pips * _Point * 10;
trade.Sell(0.1, _Symbol, 0, sl, tp);
}
manageTrailingStop();
}
//--- Helper functions for KC and Body (Abstracted for brevity)
double getBBUpper(int shift) { double b[]; CopyBuffer(handle_bb_m5, 1, shift, 1, b); return b[0]; }
double getBBLower(int shift) { double b[]; CopyBuffer(handle_bb_m5, 2, shift, 1, b); return b[0]; }
double getKCUpper(int shift) {
double ma[]; double atr[];
int h_ma = iMA(_Symbol, PERIOD_M5, 20, 0, MODE_SMA, PRICE_CLOSE);
int h_atr = iATR(_Symbol, PERIOD_M5, 20);
CopyBuffer(h_ma, 0, shift, 1, ma); CopyBuffer(h_atr, 0, shift, 1, atr);
return ma[0] + 2.0 * atr[0];
}
double getKCLower(int shift) {
double ma[]; double atr[];
int h_ma = iMA(_Symbol, PERIOD_M5, 20, 0, MODE_SMA, PRICE_CLOSE);
int h_atr = iATR(_Symbol, PERIOD_M5, 20);
CopyBuffer(h_ma, 0, shift, 1, ma); CopyBuffer(h_atr, 0, shift, 1, atr);
return ma[0] - 2.0 * atr[0];
}
double getAvgBody(int period) {
double sum = 0;
for(int i=1; i<=period; i++) {
double o[], c[];
CopyOpen(_Symbol, PERIOD_M5, i, 1, o); CopyClose(_Symbol, PERIOD_M5, i, 1, c);
sum += MathAbs(c[0] - o[0]);
}
return sum / period;
}
bool isNewBar() {
static datetime last_bar = 0;
datetime curr_bar = iTime(_Symbol, PERIOD_M5, 0);
if(last_bar != curr_bar) { last_bar = curr_bar; return true; }
return false;
}
void manageTrailingStop() {
for(int i=PositionsTotal()-1; i>=0; i--) {
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket)) {
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailStart * _Point * 10;
if(new_sl > PositionGetDouble(POSITION_SL)) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
} else {
double new_sl = SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTrailStart * _Point * 10;
if(new_sl < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
}
}