対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用

これが過酷な全期間8年半の市場変動を破綻せず生き抜いた資産推移グラフ(長期検証証拠)です。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.20 |
| 勝率 | 44.0% |
| 総取引数 | 50 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥10977.34 |
| 最大ドローダウン | 1.91% |
今回の検証結果の「限界」と「ダメ出し」
このロジックの最大の弱点は、取引回数の少なさです。 8年半で50回という数字は、実戦では機会損失が大きすぎます。 PF 1.20という数値も、運用効率としては低いです。
要因は、フィルターを掛けすぎたことにあります。 ADXによるレンジ判定と時間帯制限が、多くのチャンスを排除しました。 結果として、破綻は免れたものの、利益を伸ばす力に欠けています。 堅実さを追求しすぎて、収益性が犠牲になった形です。
ロジックの技術的詳細
本ロジックは、ボラティリティが安定したレンジ相場での「平均回帰」を狙います。 以下の条件をすべて満たしたときにエントリーします。
| 項目 | 条件・設定内容 |
|---|---|
| 時間帯制限 | UTC 0時から9時までのみ動作 |
| 環境認識(レンジ) | 5分足ADX < 20 かつ 1時間足ADX < 25 |
| ボラティリティ制限 | BB Widthが移動平均の1.1倍未満であること |
| 買い条件 | 終値 $\le$ BB下限 かつ StochRSIが20%以下でGC かつ 陽線/下ヒゲ |
| 売り条件 | 終値 $\ge$ BB上限 かつ StochRSIが80%以上でDC かつ 陰線/上ヒゲ |
| 決済ルール | TP 30pips / SL 30pips / トレーリングストップ有効 |
どう改善すべきか(次なる展望)
このコードをベースに改造するなら、フィルターの緩和を推奨します。 例えば、1時間足のADX制限を少し緩めるだけでも、取引数は増えるはずです。 また、固定のTP/SLではなく、ATRに基づいた可変幅の導入も有効です。
まずはこのコードをMT5に移植し、ご自身の分析手法を組み合わせてみてください。
Pythonソースコード
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np
class EquilibriumMeanReversion(BaseStrategy):
def __init__(self):
# 【クオンツ調剤】PF(プロフィットファクター)とリカバリーファクターの最大化
# 平均回帰戦略の特性上, 勝率を高めつつ, 最大DDを抑制するためにTP/SLを最適化
# TPを30pipsに設定し, 期待値の高い「中心線への回帰」を確実に獲る設計に変更
super().__init__(
name="Equilibrium Mean Reversion Optimized",
default_tp_pips=30.0,
default_sl_pips=30.0,
enable_trailing_stop=True,
trail_start_pips=10.0
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
def calculate_indicators(self, df):
# --- 5分足インジケーター ---
# ADX: トレンド強度判定
df.ta.adx(length=14, append=True)
# Bollinger Bands: 境界判定
# 【重要】KeyError 'BBU_20_2.0' を完全に排除するため, 動的カラム抽出を採用
bb = df.ta.bbands(length=20, std=2.0)
# pandas_taの命名規則に関わらず, BBU/BBM/BBLを含むカラムを正確に抽出してリネーム
df['BBU'] = bb.filter(like='BBU').iloc[:, 0]
df['BBM'] = bb.filter(like='BBM').iloc[:, 0]
df['BBL'] = bb.filter(like='BBL').iloc[:, 0]
# StochRSI: モメンタム反転判定
df.ta.stochrsi(length=14, rsi_length=14, k=3, d=3, append=True)
# ATR: ボラティリティ指標 (Volume不使用ルール準拠)
df.ta.atr(length=14, append=True)
# BB Width 計算: ボラティリティの収縮・拡大を定量化
df['bb_width'] = (df['BBU'] - df['BBL']) / df['BBM']
df['bb_width_ma'] = df['bb_width'].rolling(window=20).mean()
# --- MTF実装 (1h ADX) ---
# ルックアヘッドバイアス排除のため, リサンプル後にshift(1)を適用
df_h1_ohlc = df[['Open', 'High', 'Low', 'Close']].resample('1h').agg({
'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'
})
h1_adx_res = ta.adx(df_h1_ohlc['High'], df_h1_ohlc['Low'], df_h1_ohlc['Close'], length=14)
if h1_adx_res is not None:
# 確定した1時間足のADXのみを使用
h1_adx_series = h1_adx_res['ADX_14']
df['h1_adx'] = h1_adx_series.shift(1).reindex(df.index, method='ffill')
else:
df['h1_adx'] = np.nan
return df
def generate_signal(self, df):
# インジケーター計算期間を考慮し, 十分なデータがあるか確認
if len(df) < 20:
return None
curr = df.iloc[-1]
prev = df.iloc[-2]
# --- A. 相場環境フィルター (The Guard) ---
# 時間帯フィルター: ボラティリティが安定し, レンジを形成しやすい時間帯 (UTC 0-9)
hour = curr.name.hour if hasattr(curr.name, 'hour') else pd.Timestamp(curr.name).hour
if not (0 <= hour <= 9):
return None
# 【クオンツ調整】レンジ判定の厳格化
# 5分足ADX < 20 かつ 1時間足ADX < 25 の時のみエントリーを許可し, トレンド相場でのDDを防止
is_range = (curr['ADX_14'] < 20) and (curr['h1_adx'] < 25)
if not is_range:
return None
# BB Width安定判定: 急激なボラティリティ拡大(スクイーズからのブレイク)を回避
is_stable = curr['bb_width'] < (curr['bb_width_ma'] * 1.1)
if not is_stable:
return None
# --- B. エントリー・ロジック (The Trigger) ---
# pandas_ta StochRSI カラム名の動的取得
k_col = 'STOCHRSIk_14_14_3_3'
d_col = 'STOCHRSId_14_14_3_3'
if k_col not in curr:
return None
k = curr[k_col]
d = curr[d_col]
prev_k = prev[k_col]
prev_d = prev[d_col]
# Long Entry:
# 1. 終値がBB下限(BBL)以下で反発の兆候
# 2. StochRSIが20%以下の過売域でゴールデンクロス
# 3. 陽線確定 または 下ヒゲ発生
is_bullish = (curr['Close'] > curr['Open']) or (curr['Low'] < min(curr['Open'], curr['Close']))
if (curr['Close'] <= curr['BBL']) and \
(k < 20) and (prev_k <= prev_d) and (k > d) and \
is_bullish:
return 'BUY'
# Short Entry:
# 1. 終値がBB上限(BBU)以上で反落の兆候
# 2. StochRSIが80%以上の過買域でデッドクロス
# 3. 陰線確定 または 上ヒゲ発生
is_bearish = (curr['Close'] < curr['Open']) or (curr['High'] > max(curr['Open'], curr['Close']))
if (curr['Close'] >= curr['BBU']) and \
(k > 80) and (prev_k >= prev_d) and (k < d) and \
is_bearish:
return 'SELL'
return None
MQL5コード(MT5用)
//+------------------------------------------------------------------+
//| EquilibriumMeanReversion.mq5 |
//| Copyright 2026, System Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, System Trader"
#property link "https://your-blog-url.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- Input Parameters
input int InpADXPeriod = 14; // ADX Period
input int InpBBPeriod = 20; // Bollinger Bands Period
input double InpBBStdDev = 2.0; // Bollinger Bands StdDev
input int InpStochRSIPeriod = 14; // StochRSI Period
input double InpTP = 300; // Take Profit (points)
input double InpSL = 300; // Stop Loss (points)
input double InpTrailStart = 100; // Trailing Start (points)
//--- Global Variables
int handleADX5m, handleADX1h, handleBB, handleRSI;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleADX5m = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
handleADX1h = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
handleBB = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleRSI = iRSI(_Symbol, PERIOD_M5, InpStochRSIPeriod, PRICE_CLOSE);
if(handleADX5m == INVALID_HANDLE || handleADX1h == INVALID_HANDLE ||
handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE)
return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// Time Filter (UTC 0-9)
MqlDateTime dt;
TimeCurrent(dt);
if(dt.hour < 0 || dt.hour > 9) return;
// Indicator Buffers
double adx5[], adx1h[], bbUpper[], bbLower[], bbMid[], rsi[];
ArraySetAsSeries(adx5, true);
ArraySetAsSeries(adx1h, true);
ArraySetAsSeries(bbUpper, true);
ArraySetAsSeries(bbLower, true);
ArraySetAsSeries(bbMid, true);
ArraySetAsSeries(rsi, true);
if(CopyBuffer(handleADX5m, 0, 0, 2, adx5) < 2) return;
if(CopyBuffer(handleADX1h, 0, 0, 2, adx1h) < 2) return;
if(CopyBuffer(handleBB, 1, 0, 2, bbUpper) < 2) return;
if(CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2) return;
if(CopyBuffer(handleBB, 0, 0, 2, bbMid) < 2) return;
if(CopyBuffer(handleRSI, 0, 0, 20, rsi) < 20) return;
// StochRSI Calculation (Simplified)
double stochK = calculateStochK(rsi, 0);
double prevStochK = calculateStochK(rsi, 1);
double stochD = (stochK + prevStochK) / 2.0; // Simple D
// Range Filter
if(adx5[0] >= 20 || adx1h[0] >= 25) return;
// BB Width Stability
double bbWidth = (bbUpper[0] - bbLower[0]) / bbMid[0];
// Note: For simplicity, BB Width MA is omitted in this translation,
// but could be added with a simple rolling buffer.
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, PERIOD_M5, 0, 2, rates) < 2) return;
// Entry Logic
bool isBullish = (rates[0].close > rates[0].open) || (rates[0].low < MathMin(rates[0].open, rates[0].close));
bool isBearish = (rates[0].close < rates[0].open) || (rates[0].high > MathMax(rates[0].open, rates[0].close));
if(rates[0].close <= bbLower[0] && stochK < 20 && prevStochK <= stochD && stochK > stochD && isBullish)
{
if(!PositionSelect(_Symbol))
trade.Buy(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_ASK),
SymbolInfoDouble(_Symbol, SYMBOL_ASK) - InpSL * _Point,
SymbolInfoDouble(_Symbol, SYMBOL_ASK) + InpTP * _Point);
}
else if(rates[0].close >= bbUpper[0] && stochK > 80 && prevStochK >= stochD && stochK < stochD && isBearish)
{
if(!PositionSelect(_Symbol))
trade.Sell(0.1, _Symbol, SymbolInfoDouble(_Symbol, SYMBOL_BID),
SymbolInfoDouble(_Symbol, SYMBOL_BID) + InpSL * _Point,
SymbolInfoDouble(_Symbol, SYMBOL_BID) - InpTP * _Point);
}
// Trailing Stop
manageTrailingStop();
}
double calculateStochK(double &rsi_buf[], int shift)
{
double min_rsi = rsi_buf[shift];
double max_rsi = rsi_buf[shift];
for(int i=shift; i<shift+14; i++)
{
if(rsi_buf[i] < min_rsi) min_rsi = rsi_buf[i];
if(rsi_buf[i] > max_rsi) max_rsi = rsi_buf[i];
}
if(max_rsi == min_rsi) return 50.0;
return ((rsi_buf[shift] - min_rsi) / (max_rsi - min_rsi)) * 100.0;
}
void manageTrailingStop()
{
if(!PositionSelect(_Symbol)) return;
double price = PositionGetDouble(POSITION_PRICE_OPEN);
double current_sl = PositionGetDouble(POSITION_SL);
double current_price = PositionGetDouble(POSITION_PRICE_CURRENT);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
if(current_price - price > InpTrailStart * _Point)
{
double new_sl = current_price - InpTrailStart * _Point;
if(new_sl > current_sl) trade.PositionModify(_Symbol, new_sl, PositionGetDouble(POSITION_TP));
}
}
else
{
if(price - current_price > InpTrailStart * _Point)
{
double new_sl = current_price + InpTrailStart * _Point;
if(new_sl < current_sl || current_sl == 0) trade.PositionModify(_Symbol, new_sl, PositionGetDouble(POSITION_TP));
}
}
}