直近2年間の激しいインフレと金利変動を勝ち抜いたロジックを公開します。 AIが導き出したこの戦略は、徹底したリスク管理とボラティリティの選別に基づいています。

これが直近2年間を生き抜いた資産推移の証拠です。(※本ロジックは EURUSDの5分足 専用です)
| 項目 | 実績値 |
|---|---|
| プロフィットファクター (PF) | 1.28 |
| 勝率 | 42.3% |
| 総取引数 | 26 回 |
| 純利益 (0.1ロット/1万通貨時) | $68.56 |
| 最大ドローダウン | 1.83% |
なぜこのロジックは生き残れたのか
結論から述べます。 相場の「溜め」と「解放」を数値化し、トレンドの方向性を完全に一致させたからです。
理由は3点あります。 1つ目は、ボリンジャーバンドとケルトナーチャネルを用いたスクイーズ判定で、無益なエントリーを排除したことです。 2つ目は、カウフマン効率比(ER)により、ノイズの多い相場を回避したことです。 3つ目は、3つの時間足でトレンド方向を同期させ、期待値の高い局面のみに絞ったことです。
具体例を挙げます。 ニューヨーク市場の開始直後、価格が狭いレンジに収束した後に急激に放たれる瞬間を捉えます。 このとき、1時間足と15分足が同じ方向を向いていれば、高い確率でトレンドが継続します。
結論として、感情を排除した厳格なフィルタリングが、最大ドローダウン1.83%という驚異的な安定感を実現しました。
戦略の技術的詳細
本ロジックが採用しているテクニカル指標とエントリー条件を整理します。
| 指標 | 役割 | 条件 |
|---|---|---|
| BB & KC | ボラティリティ判定 | BB幅 < KC幅 (スクイーズ) からの脱出 |
| ER (効率比) | トレンドの質 | 0.3以上 (効率的なトレンドが発生中) |
| ADX | トレンド強度 | 20以上 (明確なトレンドが存在する) |
| MTF SMA | 環境認識 | 1時間足・15分足のSMAが価格と同方向 |
| RSI | モメンタム | 50を基準に方向性を判定 |
| 時間制限 | 取引時間帯 | 21:00 〜 23:00 (JST) |
公開Pythonコード
AIが生成したバックテスト合格済みのソースコードです。
🔍 プロはどうやってダマシを回避しているのか?
今回のAIロジックも優秀ですが、長年ランキング上位に居続けるプロのEAは、さらに複雑な環境認識や、独自のボラティリティフィルターを何層も実装しています。
自作EAの成績が行き詰まった時は、実際にリアル口座で長期間利益を出し続けている「本物の市販EA」の挙動(エントリーと決済のタイミング)を観察することが、一番の勉強になります。
from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np
class KineticVolatilityStrategy(BaseStrategy):
"""
Kinetic Volatility Compression Strategy (Robust Version)
- Concept: Volatility Squeeze (BB inside KC) + Efficiency Ratio + MTF Alignment
- Fix: Robust column handling to prevent KeyError from pandas_ta
- Target: New York Trend Window (21:00 - 23:00 JST)
"""
def __init__(self):
# トレーリングストップを有効にし、利益を最大化させる設定
super().__init__(
name="KineticVolatilityStrategy",
default_tp_pips=60.0,
default_sl_pips=20.0,
enable_trailing_stop=True,
trail_start_pips=15.0
)
self.base_timeframe = "5m"
self.vision_timeframes = ["5min", "15min", "1h"]
def calculate_indicators(self, df):
# --- 1. ボラティリティ・スクイーズの計算 (BB vs KC) ---
# pandas_taの戻り値(DataFrame)から位置指定で取得し、固定名のカラムに代入してKeyErrorを防止
bb = df.ta.bbands(length=20, std=2.0)
df['BBU'] = bb.iloc[:, 0] # Upper Band
df['BBL'] = bb.iloc[:, 2] # Lower Band
kc = df.ta.kc(length=20, scalar=2.0)
df['KCU'] = kc.iloc[:, 0] # Upper Channel
df['KCL'] = kc.iloc[:, 2] # Lower Channel
bb_width = df['BBU'] - df['BBL']
kc_width = df['KCU'] - df['KCL']
# Squeeze: BBがKCの内側にある状態 (1=Squeeze, 0=Release)
df['Squeeze'] = (bb_width < kc_width).astype(int)
# Squeeze Release: 前の足がSqueezeで、今の足がReleaseした瞬間
df['Squeeze_Release'] = (df['Squeeze'].shift(1) == 1) & (df['Squeeze'] == 0)
# --- 2. カウフマン効率比 (Efficiency Ratio - ER) ---
change = (df['Close'] - df['Close'].shift(10)).abs()
volatility = df['Close'].diff().abs().rolling(window=10).sum()
df['ER'] = change / volatility
# --- 3. トレンド強度フィルター (ADX & ATR) ---
adx_df = df.ta.adx(length=14)
df['ADX'] = adx_df.iloc[:, 0] # ADX_14
df.ta.atr(length=14, append=True)
# ATRカラム名は通常 'ATRr_14' になるが、generate_signalではADXを優先利用
# --- 4. マルチタイムフレーム (MTF) 分析 ---
# ルックアヘッドバイアス排除のため、resample後に必ず .shift(1)
# 15分足のトレンド (SMA20)
df_15m = df['Close'].resample('15min').last().shift(1)
sma_15m = df_15m.rolling(window=20).mean()
df['MTF_15m_Trend'] = sma_15m.reindex(df.index, method='ffill')
df['MTF_15m_Bull'] = (df['Close'] > df['MTF_15m_Trend']).astype(int)
df['MTF_15m_Bear'] = (df['Close'] < df['MTF_15m_Trend']).astype(int)
# 1時間足のトレンド (SMA50)
df_1h = df['Close'].resample('1h').last().shift(1)
sma_1h = df_1h.rolling(window=50).mean()
df['MTF_1h_Trend'] = sma_1h.reindex(df.index, method='ffill')
df['MTF_1h_Bull'] = (df['Close'] > df['MTF_1h_Trend']).astype(int)
df['MTF_1h_Bear'] = (df['Close'] < df['MTF_1h_Trend']).astype(int)
# --- 5. モメンタムフィルター (RSI) ---
# RSIはSeriesで返るためそのまま代入可能
df['RSI'] = df.ta.rsi(length=14)
return df
def generate_signal(self, df):
# 最新の行を取得
last = df.iloc[-1]
# --- エントリー時間帯の限定 (NYトレンド: 21:00 - 23:00 JST) ---
current_hour = df.index[-1].hour
if not (21 <= current_hour <= 23):
return None
# --- 相場環境フィルター ---
# 1. ボラティリティが十分にあるか (ADX > 20)
if last['ADX'] < 20:
return None
# 2. 効率的なトレンドが出ているか (ER > 0.3)
if last['ER'] < 0.3:
return None
# --- エントリーロジック ---
# 条件A: スクイーズからの解放 (エネルギーの爆発)
is_release = last['Squeeze_Release']
# 条件B: MTFの方向性一致 (1h Trend & 15m Trend)
is_bullish_mtf = (last['MTF_1h_Bull'] == 1) and (last['MTF_15m_Bull'] == 1)
is_bearish_mtf = (last['MTF_1h_Bear'] == 1) and (last['MTF_15m_Bear'] == 1)
# 条件C: モメンタムの方向性 (RSI)
is_rsi_bull = last['RSI'] > 50
is_rsi_bear = last['RSI'] < 50
# --- シグナル判定 ---
# BUY: スクイーズ解放 + MTF上昇 + RSI上昇
if is_release and is_bullish_mtf and is_rsi_bull:
return 'BUY'
# SELL: スクイーズ解放 + MTF下降 + RSI下降
if is_release and is_bearish_mtf and is_rsi_bear:
return 'SELL'
return None
MT5用 MQL5コードへの翻訳
Pythonのロジックを忠実に再現したMQL5コードです。MT5のメタエディターに貼り付けてコンパイルしてください。
//+------------------------------------------------------------------+
//| KineticVolatilityStrategy.mq5|
//| Copyright 2026, AI Researcher |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, AI Researcher"
#property link ""
#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 InpKCPeriod = 20; // Keltner Channel Period
input double InpKCScalar = 2.0; // Keltner Channel Scalar
input int InpADXPeriod = 14; // ADX Period
input int InpRSIPeriod = 14; // RSI Period
input int InpERPeriod = 10; // Efficiency Ratio Period
input double InpERMin = 0.3; // Minimum ER
input double InpADXMin = 20.0; // Minimum ADX
input int InpStartHour = 21; // Start Hour (JST)
input int InpEndHour = 23; // End Hour (JST)
input double InpTP_Pips = 60.0; // Take Profit (Pips)
input double InpSL_Pips = 20.0; // Stop Loss (Pips)
input double InpTrailStart = 15.0; // Trailing Start (Pips)
input double InpLotSize = 0.1; // Lot Size
// グローバル変数
CTrade trade;
int handleBB, handleKC_MA, handleKC_ATR, handleADX, handleRSI, handleMA15, handleMA60;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
handleBB = iBands(_Symbol, _Period, InpBBPeriod, 0, InpBBStdDev, PRICE_CLOSE);
handleKC_MA = iMA(_Symbol, _Period, InpKCPeriod, 0, MODE_SMA, PRICE_CLOSE);
handleKC_ATR = iATR(_Symbol, _Period, InpKCPeriod);
handleADX = iADX(_Symbol, _Period, InpADXPeriod);
handleRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
handleMA15 = iMA(_Symbol, PERIOD_M15, 20, 0, MODE_SMA, PRICE_CLOSE);
handleMA60 = iMA(_Symbol, PERIOD_H1, 50, 0, MODE_SMA, PRICE_CLOSE);
if(handleBB == INVALID_HANDLE || handleADX == INVALID_HANDLE) return INIT_FAILED;
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
// 時間フィルター
if(dt.hour < InpStartHour || dt.hour > InpEndHour) return;
// 指標値の取得
double bbu[], bbl[], kc_ma[], kc_atr[], adx[], rsi[], ma15[], ma60[];
ArraySetAsSeries(bbu, true); ArraySetAsSeries(bbl, true);
ArraySetAsSeries(kc_ma, true); ArraySetAsSeries(kc_atr, true);
ArraySetAsSeries(adx, true); ArraySetAsSeries(rsi, true);
ArraySetAsSeries(ma15, true); ArraySetAsSeries(ma60, true);
if(CopyBuffer(handleBB, 1, 0, 2, bbu) <= 0) return;
if(CopyBuffer(handleBB, 2, 0, 2, bbl) <= 0) return;
if(CopyBuffer(handleKC_MA, 0, 0, 2, kc_ma) <= 0) return;
if(CopyBuffer(handleKC_ATR, 0, 0, 2, kc_atr) <= 0) return;
if(CopyBuffer(handleADX, 0, 0, 2, adx) <= 0) return;
if(CopyBuffer(handleRSI, 0, 0, 2, rsi) <= 0) return;
if(CopyBuffer(handleMA15, 0, 0, 2, ma15) <= 0) return;
if(CopyBuffer(handleMA60, 0, 0, 2, ma60) <= 0) return;
double close = iClose(_Symbol, _Period, 0);
// ERの計算
double diff_10 = MathAbs(close - iClose(_Symbol, _Period, InpERPeriod));
double sum_vol = 0;
for(int i=0; i<InpERPeriod; i++) sum_vol += MathAbs(iClose(_Symbol, _Period, i) - iClose(_Symbol, _Period, i+1));
double er = (sum_vol != 0) ? diff_10 / sum_vol : 0;
// Squeeze判定 (BB vs KC)
double bb_width = bbu[0] - bbl[0];
double kc_width = (kc_atr[0] * InpKCScalar) * 2;
bool is_squeeze = (bb_width < kc_width);
double prev_bbu[], prev_bbl[], prev_kc_atr[];
ArraySetAsSeries(prev_bbu, true); ArraySetAsSeries(prev_bbl, true); ArraySetAsSeries(prev_kc_atr, true);
CopyBuffer(handleBB, 1, 1, 1, prev_bbu);
CopyBuffer(handleBB, 2, 1, 1, prev_bbl);
CopyBuffer(handleKC_ATR, 0, 1, 1, prev_kc_atr);
bool prev_squeeze = ((prev_bbu[0] - prev_bbl[0]) < (prev_kc_atr[0] * InpKCScalar * 2));
bool squeeze_release = (prev_squeeze && !is_squeeze);
// 環境フィルター
if(adx[0] < InpADXMin || er < InpERMin) return;
// ポジション確認
if(PositionsTotal() > 0) {
ManageTrailingStop();
return;
}
// エントリー条件
bool bull_mtf = (close > ma15[0] && close > ma60[0]);
bool bear_mtf = (close < ma15[0] && close < ma60[0]);
if(squeeze_release && bull_mtf && rsi[0] > 50) {
double sl = close - InpSL_Pips * _Point * 10;
double tp = close + InpTP_Pips * _Point * 10;
trade.Buy(InpLotSize, _Symbol, close, sl, tp);
}
else if(squeeze_release && bear_mtf && rsi[0] < 50) {
double sl = close + InpSL_Pips * _Point * 10;
double tp = close - InpTP_Pips * _Point * 10;
trade.Sell(InpLotSize, _Symbol, close, sl, tp);
}
}
void ManageTrailingStop()
{
for(int i=PositionsTotal()-1; i>=0; i--) {
ulong ticket = PositionGetTicket(i);
if(PositionSelectByTicket(ticket)) {
double open_price = PositionGetDouble(POSITION_PRICE_OPEN);
double current_price = PositionGetDouble(POSITION_PRICE_CURRENT);
double stop_loss = PositionGetDouble(POSITION_SL);
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
if(current_price - open_price > InpTrailStart * _Point * 10) {
double new_sl = current_price - InpTrailStart * _Point * 10;
if(new_sl > stop_loss) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
} else {
if(open_price - current_price > InpTrailStart * _Point * 10) {
double new_sl = current_price + InpTrailStart * _Point * 10;
if(new_sl < stop_loss || stop_loss == 0) trade.PositionModify(ticket, new_sl, PositionGetDouble(POSITION_TP));
}
}
}
}
}