対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
もともと私は、ブレイクアウト手法のダマシに悩み続けていました。トレンドを追いかけるほど、資金を削られる経験をしました。そこで「あえてトレンドが出ない場所」だけを狙う戦略を考えました。ボラティリティが低い時間帯にのみ、心地よく反発を拾う。そんな地味なアプローチを追求し、このロジックに辿り着きました。
このコードをベースにすれば、MT5のEA開発にかかる数百時間を節約できます。ロジックの組み方やMTF(マルチタイムフレーム)分析の実装方法は、そのまま実戦で使えます。自分だけのオリジナルEAを作るための、最高の土台として活用してください。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.21 |
| 勝率 | 46.2% |
| 総取引数 | 13 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥2625.11 |
| 最大ドローダウン | 0.89% |
| リカバリーファクター | 0.29 |
| 期待利得 (Expected Payoff) | 0.65 |
今回の検証結果の「限界」と「ダメ出し」
このロジックの最大の弱点は、取引回数の少なさです。10年で13回という数字は、実運用では機会損失が大きすぎます。PF1.21という数値も、攻めの姿勢が足りない証拠と言えるでしょう。
原因は、フィルターを厳しくしすぎたことにあります。ADXやCHOP、さらには上位足の状況まで重ね合わせたため、エントリー条件が極めて限定的になりました。カーブフィッティングを避けた結果ですが、収益性は犠牲になっています。
ロジックの技術的詳細
本ロジックは、低ボラティリティ環境での平均回帰(ミーンリバージョン)を狙います。
| 指標 | 設定・条件 | 役割 |
|---|---|---|
| ADX (14) | 23未満 | トレンドの不在を確認 |
| CHOP (14) | 60超え | 相場の停滞(レンジ)を検知 |
| ボリンジャーバンド | 20期間 / 2σ | 価格の境界線の判定 |
| ドンチアンチャネル | 20期間 | 直近高値・安値の把握 |
| RSI (7) | 30以下 $\rightarrow$ 上抜け / 70以上 $\rightarrow$ 下抜け | 短期的な反転タイミングの検知 |
| MTFフィルター | 15分/1時間ADX < 28、4時間SMA傾斜 $\approx$ 0 | 上位足のトレンド不在を同期 |
| 時間フィルター | 東京市場 (0時〜6時) | 低ボラティリティ時間帯の限定 |
どう改善すべきか(次なる展望)
このコードをベースに改造するなら、まずは「時間フィルター」の緩和を勧めます。東京時間以外でも、同様の低ボラティリティ状態が発生する時間帯があるはずです。
また、固定のTP/SLではなく、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 EquilibriumHunter(BaseStrategy):
"""
Strategy: The Equilibrium Hunter (Optimized v2)
Concept: Mean Reversion in Low-Volatility Regime
Fix: Resolved 'Series object has no attribute ta' by using functional pandas_ta calls.
Goal: Maximize PF and Recovery Factor by tightening regime filters.
"""
def __init__(self):
# Optimization: Refined TP/SL for stability.
# TP 35 / SL 25 provides a balanced reward-to-risk ratio for range trading.
super().__init__(
name="EquilibriumHunter",
default_tp_pips=35.0,
default_sl_pips=25.0,
enable_trailing_stop=True,
trail_start_pips=12.0
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15min", "1h", "4h"]
def calculate_indicators(self, df):
# --- Local Timeframe (5m) Indicators ---
# Use functional ta calls to avoid accessor issues
df['ADX'] = ta.adx(df['High'], df['Low'], df['Close'], length=14)['ADX_14']
df['ADX_slope'] = df['ADX'].diff(1)
df['CHOP'] = ta.chop(df['High'], df['Low'], df['Close'], length=14)
# Bollinger Bands: Access by index to avoid 'BBL_20_2.0' KeyError
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]
df['BBW'] = (df['BBU'] - df['BBL']) / df['BBM']
df['BBW_avg'] = df['BBW'].rolling(window=20).mean()
# RSI for fast reversal detection
df['RSI'] = ta.rsi(df['Close'], length=7)
# Donchian Channel: Access by index
dc = ta.donchian(df['High'], df['Low'], length=20)
df['DCL'] = dc.iloc[:, 0]
df['DCU'] = dc.iloc[:, 1]
df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
# --- Multi-Timeframe (MTF) Logic ---
# Rule: Shift(1) BEFORE reindex to eliminate Look-ahead Bias
# 15min Filter
close_15m = df['Close'].resample('15min').last().shift(1)
high_15m = df['High'].resample('15min').max().shift(1)
low_15m = df['Low'].resample('15min').min().shift(1)
# To use ADX on resampled data, create a temporary DataFrame
df_15m = pd.DataFrame({'High': high_15m, 'Low': low_15m, 'Close': close_15m})
adx_15m = ta.adx(df_15m['High'], df_15m['Low'], df_15m['Close'], length=14)['ADX_14']
df['MTF_ADX_15m'] = adx_15m.reindex(df.index, method='ffill')
# 1h Filter
close_1h = df['Close'].resample('1h').last().shift(1)
high_1h = df['High'].resample('1h').max().shift(1)
low_1h = df['Low'].resample('1h').min().shift(1)
df_1h = pd.DataFrame({'High': high_1h, 'Low': low_1h, 'Close': close_1h})
adx_1h = ta.adx(df_1h['High'], df_1h['Low'], df_1h['Close'], length=14)['ADX_14']
df['MTF_ADX_1h'] = adx_1h.reindex(df.index, method='ffill')
# 4h Filter: Use ta.sma() functional call instead of .ta.sma() on Series
close_4h = df['Close'].resample('4h').last().shift(1)
sma_4h = ta.sma(close_4h, length=20)
slope_4h = sma_4h.diff(1)
df['MTF_SMA_Slope_4h'] = slope_4h.reindex(df.index, method='ffill')
return df
def generate_signal(self, df):
if len(df) < 2:
return None
curr = df.iloc[-1]
prev = df.iloc[-2]
# 1. Regime Detection (5m)
# Tightened: ADX < 23 and CHOP > 60 for higher probability range signals.
is_local_range = (
(curr['ADX'] < 23) and
(curr['CHOP'] > 60) and
(curr['BBW'] <= curr['BBW_avg'] * 1.1)
)
# 2. MTF Sync
# Filter: Ensure multiple timeframes are not trending.
is_mtf_range = (
(curr['MTF_ADX_15m'] < 28) and
(curr['MTF_ADX_1h'] < 28) and
(abs(curr['MTF_SMA_Slope_4h']) < (curr['Close'] * 0.00012))
)
# 3. Time Filter: Tokyo Session (Low volatility hours)
is_tokyo_time = (0 <= df.index[-1].hour < 6)
# 4. Entry Logic (Long)
# Touches lower band/channel and RSI recovers from oversold.
long_condition = (
is_local_range and
is_mtf_range and
is_tokyo_time and
(curr['Low'] <= curr['DCL'] or curr['Low'] <= curr['BBL']) and
(prev['RSI'] < 30 and curr['RSI'] > 30)
)
# 5. Entry Logic (Short)
# Touches upper band/channel and RSI recovers from overbought.
short_condition = (
is_local_range and
is_mtf_range and
is_tokyo_time and
(curr['High'] >= curr['DCU'] or curr['High'] >= curr['BBU']) and
(prev['RSI'] > 70 and curr['RSI'] < 70)
)
# 6. Emergency Exit / Filter: Stop entries if a trend starts emerging.
if curr['ADX'] > 28 or (curr['ADX_slope'] > 2.5):
return None
if long_condition:
return 'BUY'
elif short_condition:
return 'SELL'
return None
MT5用 MQL5コード (.mq5)
//+------------------------------------------------------------------+
//| EquilibriumHunter.mq5 |
//| Copyright 2026, Quant Engineer |
//| https://example.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link "https://example.com"
#property version "1.00"
#property strict
// Input parameters
input double InpTP = 350; // Take Profit (points)
input double InpSL = 250; // Stop Loss (points)
input double InpTrailStart = 120; // Trailing Start (points)
input int InpADXPeriod = 14; // ADX Period
input int InpRSIPeriod = 7; // RSI Period
input int InpBBPeriod = 20; // BB Period
input double InpBBStdDev = 2.0; // BB StdDev
// Handles
int handleADX_5m, handleRSI_5m, handleBB_5m, handleADX_15m, handleADX_1h;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleADX_5m = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
handleRSI_5m = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
handleBB_5m = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleADX_15m = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
handleADX_1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
if(handleADX_5m == INVALID_HANDLE || handleRSI_5m == INVALID_HANDLE) return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom Chop Index Calculation |
//+------------------------------------------------------------------+
double CalculateCHOP(int period)
{
double sumTR = 0;
double highestHigh = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, period, 1));
double lowestLow = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, period, 1));
for(int i=1; i<=period; i++) {
double tr = MathMax(iHigh(_Symbol, PERIOD_M5, i) - iLow(_Symbol, PERIOD_M5, i),
MathMax(MathAbs(iHigh(_Symbol, PERIOD_M5, i) - iClose(_Symbol, PERIOD_M5, i+1)),
MathAbs(iLow(_Symbol, PERIOD_M5, i) - iClose(_Symbol, PERIOD_M5, i+1))));
sumTR += tr;
}
double range = highestHigh - lowestLow;
if(range == 0) return 0;
return 100.0 * (1.0 - (sumTR / (range * period))); // Simplified Chop logic
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
// Time Filter: Tokyo Session (0-6)
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.hour < 0 || dt.hour >= 6) return;
// Indicators Data
double adx[2], rsi[2], bbl[1], bbu[1], bbm[1], adx15[1], adx1h[1];
CopyBuffer(handleADX_5m, 0, 0, 2, adx);
CopyBuffer(handleRSI_5m, 0, 0, 2, rsi);
CopyBuffer(handleBB_5m, 1, 0, 1, bbu);
CopyBuffer(handleBB_5m, 2, 0, 1, bbl);
CopyBuffer(handleBB_5m, 0, 0, 1, bbm);
CopyBuffer(handleADX_15m, 0, 0, 1, adx15);
CopyBuffer(handleADX_1h, 0, 0, 1, adx1h);
double chop = CalculateCHOP(14);
double low = iLow(_Symbol, PERIOD_M5, 0);
double high = iHigh(_Symbol, PERIOD_M5, 0);
double dcl = iLow(_Symbol, PERIOD_M5, iLowest(_Symbol, PERIOD_M5, MODE_LOW, 20, 1));
double dcu = iHigh(_Symbol, PERIOD_M5, iHighest(_Symbol, PERIOD_M5, MODE_HIGH, 20, 1));
// 1. Regime Detection
bool isLocalRange = (adx[0] < 23 && chop > 60);
// 2. MTF Sync
bool isMTFRange = (adx15[0] < 28 && adx1h[0] < 28);
// Entry Logic
if(isLocalRange && isMTFRange && PositionsTotal() == 0)
{
// LONG
if((low <= dcl || low <= bbl[0]) && (rsi[1] < 30 && rsi[0] > 30))
{
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = request.price - InpSL * _Point;
request.tp = request.price + InpTP * _Point;
OrderSend(request, result);
}
// SHORT
else if((high >= dcu || high >= bbu[0]) && (rsi[1] > 70 && rsi[0] < 70))
{
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
request.sl = request.price + InpSL * _Point;
request.tp = request.price - InpTP * _Point;
OrderSend(request, result);
}
}
// Simple 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(SymbolInfoDouble(_Symbol, SYMBOL_BID) - PositionGetDouble(POSITION_PRICE_OPEN) > InpTrailStart * _Point)
{
double newSL = SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTrailStart * _Point;
if(newSL > PositionGetDouble(POSITION_SL))
{
MqlTradeRequest mod; MqlTradeResult res; ZeroMemory(mod);
mod.action = TRADE_ACTION_SLTP; mod.position = ticket; mod.sl = newSL;
OrderSend(mod, res);
}
}
}
}
}
}