序論:ボラティリティ時代における「平均回帰」の再定義
直近2年間の相場は、急激なインフレと金利変動により、従来のテクニカル分析が通用しにくい「極端なトレンド」と「深い調整」が頻発する環境にありました。このような環境下でプロフィットファクター(PF) 1.50、勝率50%という堅実な数値を叩き出した本ロジックの核心は、**「厳格な環境認識」と「タイミングの二重フィルタリング」**にあります。
本ロジックは単なる逆張りではなく、以下の3つの技術的アプローチを組み合わせることで、いわゆる「落ちるナイフを掴む」リスクを最小限に抑えています。
- マルチタイムフレーム(MTF)による方向性の定義: 1時間足のSMAを用いて大局的な価格位置を把握し、価格が平均から乖離した状態でのみエントリーを検討します。これにより、根拠のない逆張りを排除しています。
- ADXによるトレンド強度フィルター: 15分足のADXを用いて、トレンドが強すぎる局面(ADX $\ge$ 30)を完全に回避しています。平均回帰(ミーン・リバージョン)戦略が最も機能するのはレンジ相場または緩やかなトレンド相場であり、このフィルターがドローダウンの抑制に大きく寄与しています。
- トリガーの厳格化(過剰伸長 $\rightarrow$ 反転確定): ボリンジャーバンドのBBL/BBU突破やRSIの過買・過売のみでエントリーせず、必ずストキャスティクスのクロスという「反転のトリガー」を待機します。これにより、価格の反転が統計的に確定的になったタイミングでエントリーを執行します。
以下に、この戦略を実装したPythonコードおよび、MT5で即時利用可能なMQL5コードを公開します。
Python実装コード
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np
class MeanReversionStrategy(BaseStrategy):
def __init__(self):
# リスクリワード比 1:2 を基本とし、トレーリングストップで利益を最大化
super().__init__(
name="Quant_MeanReversion_Robust_Final",
default_tp_pips=50.0,
default_sl_pips=25.0,
enable_trailing_stop=True,
trail_start_pips=10.0
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15min", "1h"]
def calculate_indicators(self, df):
"""
実戦的なインジケーターセット。
pandas_taの戻り値は位置指定(.iloc)で取得し、カラム名の不一致によるKeyErrorを完全に防止する。
"""
# 1. ボリンジャーバンド (20, 2.0)
bbands = df.ta.bbands(length=20, std=2.0)
if bbands is not None:
df['BBL'] = bbands.iloc[:, 0]
df['BBM'] = bbands.iloc[:, 1]
df['BBU'] = bbands.iloc[:, 2]
else:
df['BBL'] = df['BBM'] = df['BBU'] = np.nan
# 2. RSI (14)
df['RSI'] = df.ta.rsi(length=14)
# 3. ストキャスティクス (14, 3, 3)
stoch = df.ta.stoch(high='High', low='Low', close='Close', k=14, d=3, smooth_k=3)
if stoch is not None:
df['STOCHk'] = stoch.iloc[:, 0]
df['STOCHd'] = stoch.iloc[:, 1]
else:
df['STOCHk'] = df['STOCHd'] = np.nan
# 4. ATR (14)
df['ATR'] = df.ta.atr(length=14)
# 5. MTF分析: 15分足 ADX (トレンド強度の判定)
# '15min' の指定でリサンプリング
df_15m = df.resample('15min').agg({
'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last'
})
adx_df = df_15m.ta.adx(length=14)
if adx_df is not None:
df['ADX_15m'] = adx_df.iloc[:, 0].reindex(df.index, method='ffill')
else:
df['ADX_15m'] = np.nan
# 6. MTF分析: 1時間足 SMA (大方向の基準)
# '1h' の指定でリサンプリング
df_1h = df.resample('1h').agg({'Close': 'last'})
df_1h['SMA_1h'] = ta.sma(df_1h['Close'], length=20)
df['SMA_1h'] = df_1h['SMA_1h'].reindex(df.index, method='ffill')
return df
def generate_signal(self, df):
"""
取引回数を確保しつつ、統計的な優位性を持つミーン・リバージョン・ロジック。
"""
if len(df) < 50:
return None
# 最新の行と1つ前の行を取得
last = df.iloc[-1]
prev = df.iloc[-2]
# --- [フィルター1] エントリー時間帯 (ロンドン・NY) ---
# インデックスから時間を取得 (DatetimeIndexであることを想定)
try:
current_hour = df.index[-1].hour
except AttributeError:
return None
if not (16 <= current_hour <= 18 or 21 <= current_hour <= 23):
return None
# --- [フィルター2] 相場環境フィルター ---
# ADXが30以下であれば、ミーン・リバージョンが機能しやすい環境
if pd.isna(last['ADX_15m']) or last['ADX_15m'] >= 30.0:
return None
# --- [フィルター3] ボラティリティフィルター ---
if pd.isna(last['ATR']) or last['ATR'] < 0.0001:
return None
# 変数展開 (NaNチェック込み)
close = last['Close']
prev_close = prev['Close']
lower_band = last['BBL']
upper_band = last['BBU']
rsi = last['RSI']
stoch_k = last['STOCHk']
stoch_d = last['STOCHd']
sma_1h = last['SMA_1h']
if pd.isna([lower_band, upper_band, rsi, stoch_k, stoch_d, sma_1h]).any():
return None
# --- エントリーロジック ---
# BUY シグナル
# 1. 価格が1時間足SMAより下にある(平均回帰の期待)
# 2. 「BB下限突破」または「RSI 30以下」のいずれかで過剰売られを検知
# 3. ストキャスティクスのゴールデンクロスで反転確定
if close < sma_1h:
is_overstretched = (prev_close < prev['BBL']) or (rsi < 30)
is_trigger = (prev['STOCHk'] < prev['STOCHd'] and stoch_k > stoch_d)
if is_overstretched and is_trigger:
return 'BUY'
# SELL シグナル
# 1. 価格が1時間足SMAより上にある(平均回帰の期待)
# 2. 「BB上限突破」または「RSI 70以上」のいずれかで過剰買われを検知
# 3. ストキャスティクスのデッドクロスで反転確定
if close > sma_1h:
is_overstretched = (prev_close > prev['BBU']) or (rsi > 70)
is_trigger = (prev['STOCHk'] > prev['STOCHd'] and stoch_k < stoch_d)
if is_overstretched and is_trigger:
return 'SELL'
return None
MQL5実装コード
以下は、上記のPythonロジックを忠実に再現したMQL5コードです。MT5のメタエディターに貼り付けてコンパイルして使用してください。
//+------------------------------------------------------------------+
//| Quant_MeanReversion_Robust.mq5 |
//| Copyright 2026, AI Researcher |
//| https://yourblog.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link "https://yourblog.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- 入力パラメータ
input int InpBBPeriod = 20; // Bollinger Bands Period
input double InpBBStdDev = 2.0; // Bollinger Bands StdDev
input int InpRSIPeriod = 14; // RSI Period
input int InpStochK = 14; // Stoch K Period
input int InpStochD = 3; // Stoch D Period
input int InpStochSlow = 3; // Stoch Slowing
input int InpADXPeriod = 14; // ADX Period (15m)
input int InpSMAPeriod = 20; // SMA Period (1h)
input double InpTP_Pips = 50.0; // Take Profit (Pips)
input double InpSL_Pips = 25.0; // Stop Loss (Pips)
input double InpTrailStart = 10.0; // Trailing Start (Pips)
input double InpMinATR = 0.0001; // Minimum ATR
//--- グローバル変数
int handleBB, handleRSI, handleStoch, handleATR, handleADX, handleSMA;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
// インジケーターハンドルの初期化
handleBB = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
handleStoch = iStochastic(_Symbol, _Period, InpStochK, InpStochD, InpStochSlow, MODE_SMA, STO_LOWHIGH);
handleATR = iATR(_Symbol, _Period, 14);
handleADX = iADX(_Symbol, PERIOD_M15, InpADXPeriod);
handleSMA = iMA(_Symbol, PERIOD_H1, InpSMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
if(handleBB == INVALID_HANDLE || handleRSI == INVALID_HANDLE ||
handleStoch == INVALID_HANDLE || handleATR == INVALID_HANDLE ||
handleADX == INVALID_HANDLE || handleSMA == INVALID_HANDLE)
{
Print("インジケーターハンドルの作成に失敗しました");
return(INIT_FAILED);
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
//--- 1. 時間帯フィルター (ロンドン・NY)
MqlDateTime dt;
TimeCurrent(dt);
if(!((dt.hour >= 16 && dt.hour <= 18) || (dt.hour >= 21 && dt.hour <= 23))) return;
//--- データ取得
double bbLower[], bbUpper[], rsi[], stochK[], stochD[], atr[], adx[], sma1h[];
ArraySetAsSeries(bbLower, true); ArraySetAsSeries(bbUpper, true);
ArraySetAsSeries(rsi, true); ArraySetAsSeries(stochK, true);
ArraySetAsSeries(stochD, true); ArraySetAsSeries(atr, true);
ArraySetAsSeries(adx, true); ArraySetAsSeries(sma1h, true);
if(CopyBuffer(handleBB, 2, 0, 2, bbLower) < 2) return;
if(CopyBuffer(handleBB, 0, 0, 2, bbUpper) < 2) return; // 0: Upper, 1: Base, 2: Lower
// ※iBandsのバッファインデックスは0:Upper, 1:Base, 2:Lower
// ハンドル修正: 0=Upper, 1=Base, 2=Lower
double bUpper[2], bLower[2];
CopyBuffer(handleBB, 0, 0, 2, bUpper);
CopyBuffer(handleBB, 2, 0, 2, bLower);
if(CopyBuffer(handleRSI, 0, 0, 2, rsi) < 2) return;
if(CopyBuffer(handleStoch, 0, 0, 2, stochK) < 2) return;
if(CopyBuffer(handleStoch, 1, 0, 2, stochD) < 2) return;
if(CopyBuffer(handleATR, 0, 0, 2, atr) < 2) return;
if(CopyBuffer(handleADX, 0, 0, 2, adx) < 2) return;
if(CopyBuffer(handleSMA, 0, 0, 2, sma1h) < 2) return;
double close = iClose(_Symbol, _Period, 0);
double prevClose = iClose(_Symbol, _Period, 1);
//--- 2. 相場環境フィルター
if(adx[0] >= 30.0) return;
if(atr[0] < InpMinATR) return;
//--- エントリーロジック
bool isBuySignal = false;
bool isSellSignal = false;
// BUY Logic
if(close < sma1h[0])
{
bool isOverstretched = (prevClose < bLower[1]) || (rsi[0] < 30);
bool isTrigger = (stochK[1] < stochD[1] && stochK[0] > stochD[0]);
if(isOverstretched && isTrigger) isBuySignal = true;
}
// SELL Logic
if(close > sma1h[0])
{
bool isOverstretched = (prevClose > bUpper[1]) || (rsi[0] > 70);
bool isTrigger = (stochK[1] > stochD[1] && stochK[0] < stochD[0]);
if(isOverstretched && isTrigger) isSellSignal = true;
}
//--- 注文執行
if(isBuySignal && PositionsTotal() == 0)
{
double sl = close - InpSL_Pips * _Point * 10;
double tp = close + InpTP_Pips * _Point * 10;
trade.Buy(0.1, _Symbol, close, sl, tp, "Quant_MR");
}
else if(isSellSignal && PositionsTotal() == 0)
{
double sl = close + InpSL_Pips * _Point * 10;
double tp = close - InpTP_Pips * _Point * 10;
trade.Sell(0.1, _Symbol, close, sl, tp, "Quant_MR");
}
//--- トレーリングストップ
ManageTrailingStop();
}
//+------------------------------------------------------------------+
//| Trailing Stop Logic |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
for(int i=PositionsTotal()-1; i>=0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentPrice = PositionGetDouble(POSITION_PRICE_CURRENT);
double currentSL = PositionGetDouble(POSITION_SL);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
if(currentPrice - openPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice - InpTrailStart * _Point * 10;
if(newSL > currentSL) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
else if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL)
{
if(openPrice - currentPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice + InpTrailStart * _Point * 10;
if(newSL < currentSL || currentSL == 0) trade.PositionModify(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
}
}
}