対象通貨ペア:USDJPY(ドル円) / 使用時間足:5分足専用
もともと、私はブレイクアウト手法のダマシに悩まされていました。トレンドを追うほど、往復ビンタに合う日々が続いたからです。そこで視点を変え、価格の「定常性」に着目しました。価格が一定の範囲に戻る性質を、数学的に捉えたいと考えたのが始まりです。フィルターを厳しくしすぎた時期もあり、取引回数が激減して絶望しました。試行錯誤を繰り返し、ようやく実用的なバランスに到達しました。
<CHART_IMAGE_EQUITY> <CHART_IMAGE_HEATMAP> <CHART_IMAGE_HOURLY>
これが過酷な全期間10年の市場変動を破綻せず生き抜いた分析ダッシュボードです。
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.32 |
| 勝率 | 47.4% |
| 総取引数 | 215 回 |
| 純利益 (0.1ロット/1万通貨時) | ¥78607.66 |
| 最大ドローダウン | 2.49% |
| リカバリーファクター | 3.16 |
| 期待利得 (Expected Payoff) | 0.70 |
今回の検証結果の「限界」と「ダメ出し」
このロジックは、安定感こそ抜群ですが、爆発力に欠けます。PF1.32という数値は、プロの視点では「平凡」な成績です。勝率が50%を下回るため、精神的な負荷がかかる局面があります。
最大の弱点は、取引回数の少なさです。10年で215回という頻度は、年平均でわずか21回しか取引していません。これは、定常性判定フィルターを厳しく設定しすぎた結果です。強いトレンド相場を完全に排除したため、大きな利益を取り逃しています。安全性を追求しすぎて、機会損失を招いた形となりました。
ロジックの技術的詳細
本ロジックは、ADXとボリンジャーバンドを組み合わせた平均回帰戦略です。
| 項目 | 設定・条件 |
|---|---|
| ベース時間足 | 5分足 |
| トレンドフィルター | ADX(14) < 30 かつ 上位足ADX(1h) < 35 |
| 定常性判定 | BBミドルラインの傾き(Slope) < 0.15 |
| ボラティリティ制限 | ATR(14)/ATR(100) が 0.5 〜 2.0 の範囲内 |
| 買いエントリー | Low < BBL かつ RSI(14) < 35 かつ Close > BBL |
| 売りエントリー | High > BBU かつ RSI(14) > 65 かつ Close < BBU |
| 決済ルール | TP: 40pips / SL: 30pips / トレーリングストップあり |
どう改善すべきか(次なる展望)
このコードをベースに、さらに利益を伸ばす余地は十分にあります。まずは、定常性判定の閾値を動的に変更する仕組みを導入すべきです。相場のボラティリティに合わせて、Slopeの制限値を変動させます。これで、取引回数を適切に増やせるはずです。
また、上位足の環境認識に、価格帯別出来高や重要レジサポラインを加えたいところです。単純な指標だけでなく、壁となる価格帯を認識させれば、勝率は向上します。プロの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 ASMRStrategy(BaseStrategy):
"""
Adaptive Stationary Mean-Reversion (ASMR) Strategy - Recovery Version
- 【改善点】取引回数不足を解消するため、定常性判定フィルターを「厳格」から「適正」へ緩和。
- 【改善点】RSI閾値を30/70 → 35/65へ拡大し、エントリー機会を増加。
- 【改善点】時間帯フィルターを撤廃し、インジケーターによる定常性判定のみで取引を制御。
- 【改善点】ペイオフレシオ向上のため、TP/SL比率を再設計。
"""
def __init__(self):
# PFおよびリカバリーファクター向上のため、損切を適正化し、利確を期待値に基づき調整
super().__init__(
name="ASMR_Strategy_Recovery",
default_tp_pips=40.0,
default_sl_pips=30.0,
enable_trailing_stop=True,
trail_start_pips=15.0
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15min", "1h"]
def calculate_indicators(self, df):
# --- 1. 基本インジケーター計算 (5m) ---
# ADX: トレンド強度 (14期間)
adx_df = df.ta.adx(length=14)
if adx_df is not None:
df['ADX'] = adx_df.iloc[:, 0] # インデックスによるアクセスでカラム名エラーを完全に排除
# Bollinger Bands: 価格境界 (20期間, 2.0標準偏差)
bb_df = df.ta.bbands(length=20, std=2.0)
if bb_df is not None:
df['BBL'] = bb_df.iloc[:, 0]
df['BBM'] = bb_df.iloc[:, 1]
df['BBU'] = bb_df.iloc[:, 2]
# RSI: 過熱感 (14期間)
df['RSI'] = df.ta.rsi(length=14)
# ATR: ボラティリティ
df['ATR_14'] = df.ta.atr(length=14)
df['ATR_100'] = df.ta.atr(length=100)
# BB-Midline Slope (正規化傾き)
# 閾値を緩和するため、分母に微小値を加え安定化
df['BB_Slope'] = df['BBM'].diff(3) / (df['ATR_14'] + 1e-9)
# ATR Ratio (ボラティリティの相対的安定性)
df['Vol_Ratio'] = df['ATR_14'] / (df['ATR_100'] + 1e-9)
# --- 2. MTFフィルター実装 (1時間足) ---
h1_close = df['Close'].resample('1h').last().shift(1)
h1_high = df['High'].resample('1h').max().shift(1)
h1_low = df['Low'].resample('1h').min().shift(1)
df_h1 = pd.DataFrame({'High': h1_high, 'Low': h1_low, 'Close': h1_close}, index=h1_close.index)
adx_h1_df = df_h1.ta.adx(length=14)
if adx_h1_df is not None:
df['ADX_1h'] = adx_h1_df.iloc[:, 0].reindex(df.index, method='ffill')
else:
df['ADX_1h'] = np.nan
return df
def generate_signal(self, df):
if len(df) < 100:
return None
last = df.iloc[-1]
# --- 【外科的調整】フィルター条件の緩和による取引回数の確保 ---
# 1. ADX(5m) < 30: レンジ判定を緩め、緩やかなトレンド局面も許容 (取引数UP)
# 2. ADX(1h) < 35: 上位足のトレンド制限を緩和
# 3. BB_Slope < 0.15: 完全に水平でなくとも、一定の範囲内で回帰を狙う (取引数UP)
# 4. Vol_Ratio (0.5 ~ 2.0): ボラティリティ変動への耐性を拡大
# 5. 時間帯フィルターの撤廃: インジケーターによる判定を優先し、24時間チャンスを捕捉
is_stationary = (
(last['ADX'] < 30) and
(last['ADX_1h'] < 35) and
(abs(last['BB_Slope']) < 0.15) and
(0.5 < last['Vol_Ratio'] < 2.0)
)
if not is_stationary:
return None
# --- 【外科的調整】エントリートリガーの感度向上 ---
# RSI閾値を 30/70 -> 35/65 に変更し、エントリー頻度を向上させつつ逆張り優位性を維持
# 買いトリガー: LowがBBLを割り込み、RSIが35以下、かつ終値がBBLより上で確定(回帰)
if (last['Low'] < last['BBL']) and \
(last['RSI'] < 35) and \
(last['Close'] > last['BBL']):
return 'BUY'
# 売りトリガー: HighがBBUを突き抜け、RSIが65以上、かつ終値がBBUより下で確定(回帰)
if (last['High'] > last['BBU']) and \
(last['RSI'] > 65) and \
(last['Close'] < last['BBU']):
return 'SELL'
return None
MQL5コードへの翻訳
//+------------------------------------------------------------------+
//| ASMR_Strategy_Recovery.mq5 |
//| Copyright 2026, Quant Engineer |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Quant Engineer"
#property link "https://www.mql5.com"
#property version "1.00"
#property strict
#include <Trade\Trade.mqh>
//--- 入力パラメータ
input int InpADXPeriod = 14; // ADX Period
input int InpBBPeriod = 20; // Bollinger Bands Period
input double InpBBStdDev = 2.0; // Bollinger Bands StdDev
input int InpRSIPeriod = 14; // RSI Period
input int InpATRPeriod = 14; // ATR Period
input int InpATRLong = 100; // ATR Long Period
input double InpTP_Pips = 40.0; // Take Profit (Pips)
input double InpSL_Pips = 30.0; // Stop Loss (Pips)
input double InpTrailStart = 15.0; // Trailing Start (Pips)
input double InpLotSize = 0.1; // Lot Size
//--- ハンドル
int handleADX, handleBB, handleRSI, handleATR14, handleATR100, handleADX_H1;
CTrade trade;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleADX = iADX(_Symbol, PERIOD_M5, InpADXPeriod);
handleBB = iBands(_Symbol, PERIOD_M5, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleRSI = iRSI(_Symbol, PERIOD_M5, InpRSIPeriod, PRICE_CLOSE);
handleATR14 = iATR(_Symbol, PERIOD_M5, InpATRPeriod);
handleATR100 = iATR(_Symbol, PERIOD_M5, InpATRLong);
handleADX_H1 = iADX(_Symbol, PERIOD_H1, InpADXPeriod);
if(handleADX == INVALID_HANDLE || handleBB == INVALID_HANDLE ||
handleRSI == INVALID_HANDLE || handleADX_H1 == INVALID_HANDLE)
return(INIT_FAILED);
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
// 新しい足の確定時に判定を行うため、ここでは簡易的に最新値で判定
// 実際には新足判定ロジックを推奨
double adx[], bbUpper[], bbLower[], bbMid[], rsi[], atr14[], atr100[], adxH1[];
ArraySetAsSeries(adx, true);
ArraySetAsSeries(bbUpper, true);
ArraySetAsSeries(bbLower, true);
ArraySetAsSeries(bbMid, true);
ArraySetAsSeries(rsi, true);
ArraySetAsSeries(atr14, true);
ArraySetAsSeries(atr100, true);
ArraySetAsSeries(adxH1, true);
if(CopyBuffer(handleADX, 0, 0, 3, adx) < 3) return;
if(CopyBuffer(handleBB, 1, 0, 3, bbUpper) < 3) return;
if(CopyBuffer(handleBB, 2, 0, 3, bbLower) < 3) return;
if(CopyBuffer(handleBB, 0, 0, 3, bbMid) < 3) return;
if(CopyBuffer(handleRSI, 0, 0, 3, rsi) < 3) return;
if(CopyBuffer(handleATR14, 0, 0, 3, atr14) < 3) return;
if(CopyBuffer(handleATR100, 0, 0, 3, atr100) < 3) return;
if(CopyBuffer(handleADX_H1, 0, 0, 3, adxH1) < 3) return;
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, PERIOD_M5, 0, 4, rates) < 4) return;
// --- 定常性判定 (Stationarity) ---
double slope = (bbMid[0] - bbMid[3]) / (atr14[0] + 1e-9);
double volRatio = atr14[0] / (atr100[0] + 1e-9);
bool isStationary = (adx[0] < 30) &&
(adxH1[0] < 35) &&
(MathAbs(slope) < 0.15) &&
(volRatio > 0.5 && volRatio < 2.0);
if(!isStationary) return;
// --- ポジション確認 ---
bool hasPosition = PositionSelect(_Symbol);
if(!hasPosition)
{
// 買いトリガー
if(rates[0].low < bbLower[0] && rsi[0] < 35 && rates[0].close > bbLower[0])
{
double sl = rates[0].close - InpSL_Pips * _Point * 10;
double tp = rates[0].close + InpTP_Pips * _Point * 10;
trade.Buy(InpLotSize, _Symbol, rates[0].close, sl, tp, "ASMR Buy");
}
// 売りトリガー
else if(rates[0].high > bbUpper[0] && rsi[0] > 65 && rates[0].close < bbUpper[0])
{
double sl = rates[0].close + InpSL_Pips * _Point * 10;
double tp = rates[0].close - InpTP_Pips * _Point * 10;
trade.Sell(InpLotSize, _Symbol, rates[0].close, sl, tp, "ASMR Sell");
}
}
else
{
// トレーリングストップ実装
ManageTrailingStop();
}
}
//+------------------------------------------------------------------+
//| Trailing Stop Logic |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
if(!PositionSelect(_Symbol)) return;
double currentPrice = (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double stopLoss = PositionGetDouble(POSITION_SL);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
{
if(currentPrice - openPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice - InpTrailStart * _Point * 10;
if(newSL > stopLoss) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
}
}
else
{
if(openPrice - currentPrice > InpTrailStart * _Point * 10)
{
double newSL = currentPrice + InpTrailStart * _Point * 10;
if(newSL < stopLoss || stopLoss == 0) trade.PositionModify(_Symbol, newSL, PositionGetDouble(POSITION_TP));
}
}
}