対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
私は以前、ボリンジャーバンドを用いた逆張り戦略に没頭していました。しかし、強いトレンドが発生した際の「バンドウォーク」に何度も資金を削られました。だましを排除し、真に勝率の高いレンジ相場だけを抽出したい。そんな執念から、この厳格なフィルター群を構築した経緯があります。
このコードをベースにすれば、MT5のEA開発に費やす数百時間を節約できます。ゼロから構築する手間を省き、自分だけのオリジナル戦略を作るための最高の土台になります。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.54 |
| 勝率 | 55.7% |
| 総取引数 | 61 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥22209.40 |
| 最大ドローダウン | 0.97% |
| リカバリーファクター | 2.28 |
| 期待利得 (Expected Payoff) | 0.68 |
今回の検証結果の「限界」と「ダメ出し」
このロジックの最大の弱点は、取引回数の少なさです。10年で61回という数字は、機会損失が非常に大きいことを意味します。フィルターを厳格にしすぎた結果、利益を伸ばすチャンスまで捨ててしまいました。
また、PF1.54という数値は合格点ですが、爆発力に欠けます。低ドローダウンを実現した代償として、資産増加のスピードが緩やかになりました。純粋なレンジ相場以外では、ほぼ機能しない点も技術的な限界と言えます。
ロジックの技術的詳細
本ロジックは、3つの時間軸と複数の指標を組み合わせた「環境認識重視」の設計です。
| 項目 | 使用指標・条件 | 設定値・役割 |
|---|---|---|
| 環境認識(下位) | ADX | 22未満(トレンドの不在を確認) |
| 環境認識(上位) | 1時間足ADX | 28未満(長期的なトレンドを排除) |
| 平坦判定 | BBM(中心線)の傾き | 0.0003未満(完全な横ばいを厳選) |
| エントリー(買) | 価格 $\le$ BBL かつ RSI $\le$ 30 | 過熱感のある売られすぎ水準で反発狙い |
| エントリー(売) | 価格 $\ge$ BBU かつ RSI $\ge$ 70 | 過熱感のある買われすぎ水準で反落狙い |
| 決済ルール | 固定TP/SL + トレーリング | TP: 35pips / SL: 15pips / Trail: 15pips |
どう改善すべきか(次なる展望)
このロジックを「化けさせる」には、エントリー条件の緩和と動的な決済ロジックの導入が必要です。例えば、固定の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 VolatilityAdjustedRangeStrategy(BaseStrategy):
def __init__(self):
super().__init__(
name="VolatilityAdjustedRangeStrategy",
default_tp_pips=35.0, # ペイオフレシオを向上させ、PFとリカバリーファクターを底上げするため目標利幅を拡大
default_sl_pips=15.0, # 損切幅をタイトに設定し、1トレードあたりのリスクを抑制してPFを改善
enable_trailing_stop=True,
trail_start_pips=15.0 # 利益を確保しつつ、最大DDを抑制するための最適値に調整
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15min", "1h"]
# --- クオンツ・外科調整 (だまし排除と利益効率の最大化) ---
# 取引回数が過多(1273回)でPFが低いため、フィルターを大幅に厳格化し「高期待値トレード」のみを抽出
self.ADX_THRESHOLD = 22 # 40 -> 22: 強いトレンド相場での逆張りを完全に排除し、純粋なレンジのみを捕捉
self.MTF_ADX_THRESHOLD = 28 # 45 -> 28: 上位足のトレンドフィルターを強化し、環境認識の精度を向上
self.SLOPE_THRESHOLD = 0.0003 # 0.001 -> 0.0003: BBMの傾き判定を極めて厳格にし、横ばい相場を厳選
self.RSI_LOW = 30 # 40 -> 30: 逆張りエントリーの閾値を深くし、真の売られすぎ水準まで待機
self.RSI_HIGH = 70 # 60 -> 70: 逆張りエントリーの閾値を高くし、真の買われすぎ水準まで待機
def calculate_indicators(self, df):
# --- 下位足 (5分足) インジケーター計算 ---
# ADX (トレンド強度判定)
df.ta.adx(length=14, append=True)
# Bollinger Bands (価格の乖離判定)
# 動的なカラム名によるKeyErrorを完全に排除する堅牢な抽出ロジック
bb_df = df.ta.bbands(length=20, std=2.0)
if bb_df is not None:
df['BBL'] = bb_df.filter(like='BBL').iloc[:, 0]
df['BBM'] = bb_df.filter(like='BBM').iloc[:, 0]
df['BBU'] = bb_df.filter(like='BBU').iloc[:, 0]
# RSI (オシレーターによる過熱感判定)
# 期間を14から10へ短縮し、レンジ内の反転タイミングに対する反応速度を改善
df.ta.rsi(length=10, append=True)
# ATR (ボラティリティ参照)
df.ta.atr(length=14, append=True)
# SMAの傾き (BBMを利用)
# 判定期間を5本に延長し、短期的なノイズを排除して「真の平坦さ」を定義
df['sma_slope'] = df['BBM'].diff(5)
# --- 上位足 (1時間足) フィルタリング (ルックアヘッドバイアス排除) ---
h1_resampled = df[['High', 'Low', 'Close']].resample('1h').agg({
'High': 'max',
'Low': 'min',
'Close': 'last'
})
h1_adx_df = ta.adx(h1_resampled['High'], h1_resampled['Low'], h1_resampled['Close'], length=14)
if h1_adx_df is not None:
# shift(1)により確定足のみを使用。reindex and ffillで時間軸を完全一致させる
mtf_adx = h1_adx_df['ADX_14'].shift(1).reindex(df.index, method='ffill')
df['mtf_adx'] = mtf_adx
else:
df['mtf_adx'] = float('nan')
return df
def generate_signal(self, df):
if len(df) < 30:
return None
last = df.iloc[-1]
try:
# pandas_taのADXカラム名は通常 'ADX_14'
adx = last['ADX_14']
mtf_adx = last['mtf_adx']
slope = last['sma_slope']
close = last['Close']
bbl = last['BBL']
bbu = last['BBU']
# RSI期間変更に伴いカラム名を動的に取得(RSI_10)
rsi_col = [c for c in df.columns if c.startswith('RSI_')][0]
rsi = last[rsi_col]
except (KeyError, IndexError):
return None
# --- エントリー条件判定 (高精度クオンツ・フィルタ) ---
# 1. 環境認識: 3つの指標が同時に「極めて低いボラティリティ/トレンド」を示すこと
# これにより、トレンド発生時の「Bands Walk」による連敗(だまし)を徹底的に排除
is_strict_range = (
adx < self.ADX_THRESHOLD and
mtf_adx < self.MTF_ADX_THRESHOLD and
abs(slope) < self.SLOPE_THRESHOLD
)
if not is_strict_range:
return None
# 2. エントリータイミング: BBの外側への突き抜け + RSIの極端な過熱感
# 【買いシグナル】
if close <= bbl and rsi < self.RSI_LOW:
return 'BUY'
# 【売りシグナル】
if close >= bbu and rsi > self.RSI_HIGH:
return 'SELL'
return None
公開コード:MQL5 (MT5実装用)
//+------------------------------------------------------------------+
//| VolatilityAdjustedRangeStrategy |
//| Copyright 2026, Quant Engineer |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link "https://your-blog-url.com"
#property version "1.00"
#property strict
// --- 入力パラメータ ---
input int InpADXPeriod = 14; // ADX Period
input double InpADXThres = 22.0; // ADX Threshold (Lower = More Range)
input int InpMTFADXPeriod = 14; // H1 ADX Period
input double InpMTFADXThres = 28.0; // H1 ADX Threshold
input double InpSlopeThres = 0.0003; // SMA Slope Threshold
input int InpRSIPeriod = 10; // RSI Period
input int InpRSI_Low = 30; // RSI Buy Level
input int InpRSI_High = 70; // RSI Sell Level
input int InpBBPeriod = 20; // Bollinger Bands Period
input double InpBBDev = 2.0; // Bollinger Bands Deviation
input double InpTP_Pips = 35.0; // Take Profit (Pips)
input double InpSL_Pips = 15.0; // Stop Loss (Pips)
input double InpTrailStart = 15.0; // Trailing Start (Pips)
input double InpLotSize = 0.1; // Lot Size
// --- グローバル変数 ---
int handleADX, handleMTFADX, handleRSI, handleBB;
double bufferADX[], bufferMTFADX[], bufferRSI[], bufferBBU[], bufferBBM[], bufferBBL[];
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleADX = iADX(_Symbol, _Period, InpADXPeriod);
handleMTFADX = iADX(_Symbol, PERIOD_H1, InpMTFADXPeriod);
handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
handleBB = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBDev, PRICE_CLOSE);
if(handleADX == INVALID_HANDLE || handleMTFADX == INVALID_HANDLE ||
handleRSI == INVALID_HANDLE || handleBB == INVALID_HANDLE)
{
Print("インジケーターハンドルの作成に失敗しました");
return(INIT_FAILED);
}
ArraySetAsSeries(bufferADX, true);
ArraySetAsSeries(bufferMTFADX, true);
ArraySetAsSeries(bufferRSI, true);
ArraySetAsSeries(bufferBBU, true);
ArraySetAsSeries(bufferBBM, true);
ArraySetAsSeries(bufferBBL, true);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// データのコピー
if(CopyBuffer(handleADX, 0, 0, 2, bufferADX) < 2) return;
if(CopyBuffer(handleMTFADX, 0, 0, 2, bufferMTFADX) < 2) return;
if(CopyBuffer(handleRSI, 0, 0, 2, bufferRSI) < 2) return;
if(CopyBuffer(handleBB, 0, 0, 6, bufferBBU) < 6) return;
if(CopyBuffer(handleBB, 1, 0, 6, bufferBBM) < 6) return;
if(CopyBuffer(handleBB, 2, 0, 6, bufferBBL) < 6) return;
// 傾きの計算 (BBMの5本前との差)
double slope = bufferBBM[0] - bufferBBM[5];
double close = iClose(_Symbol, _Period, 0);
// 環境認識フィルタ
bool isStrictRange = (bufferADX[0] < InpADXThres &&
bufferMTFADX[1] < InpMTFADXThres &&
MathAbs(slope) < InpSlopeThres);
if(!isStrictRange) return;
// エントリー判定
if(PositionsTotal() == 0)
{
// BUY: 価格 <= LowerBand かつ RSI <= Low
if(close <= bufferBBL[0] && bufferRSI[0] <= InpRSI_Low)
{
ExecuteTrade(ORDER_TYPE_BUY);
}
// SELL: 価格 >= UpperBand かつ RSI >= High
else if(close >= bufferBBU[0] && bufferRSI[0] >= InpRSI_High)
{
ExecuteTrade(ORDER_TYPE_SELL);
}
}
ManageTrailingStop();
}
//+------------------------------------------------------------------+
//| トレード実行関数 |
//+------------------------------------------------------------------+
void ExecuteTrade(ENUM_ORDER_TYPE type)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
double price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
double sl = (type == ORDER_TYPE_BUY) ? price - InpSL_Pips * _Point * 10 : price + InpSL_Pips * _Point * 10;
double tp = (type == ORDER_TYPE_BUY) ? price + InpTP_Pips * _Point * 10 : price - InpTP_Pips * _Point * 10;
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = InpLotSize;
request.type = type;
request.price = price;
request.sl = sl;
request.tp = tp;
request.deviation = 10;
request.magic = 123456;
request.type_filling = ORDER_FILLING_IOC;
if(!OrderSend(request, result)) Print("OrderSend Error: ", GetLastError());
}
//+------------------------------------------------------------------+
//| トレーリングストップ管理 |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket))
{
if(PositionGetInteger(POSITION_MAGIC) != 123456) continue;
double currentPrice = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double currentSL = PositionGetDouble(POSITION_SL);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
if(currentPrice - openPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice - InpSL_Pips * _Point * 10;
if(newSL > currentSL) ModifySL(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
else
{
if(openPrice - currentPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice + InpSL_Pips * _Point * 10;
if(newSL < currentSL || currentSL == 0) ModifySL(ticket, newSL, PositionGetDouble(POSITION_TP));
}
}
}
}
}
void ModifySL(ulong ticket, double sl, double tp)
{
MqlTradeRequest request = {};
MqlTradeResult result = {};
request.action = TRADE_ACTION_SLTP;
request.position = ticket;
request.sl = sl;
request.tp = tp;
if(!OrderSend(request, result)) Print("ModifySL Error: ", GetLastError());
}