また出たよ。最近のAIが書き出す「いかにも正解っぽく見えるゴミ」だ。
今回の標本は、NY市場のボラティリティを狙ったという『NYStructureTrend』。 ロジックを紐解けば、長期EMAで環境認識をし、短期EMAへのプルバックを待ち、出来高を確認し、直近高値・安値のブレイクでエントリーする……という、FXの入門書に書いてある「正解」を全部盛り込んだような構成だ。
AIはきっとこう思ったんだろう。「条件を重ねれば重ねるほど、勝率は上がり、リスクは減る」と。 だが、相場の本質を理解していない人間(とAI)が陥る最大の罠がここにある。
破綻の理由:フィルターの積み上げによる「取引機会の絶滅」
まずはこの絶望的なバックテスト結果を見てくれ。
- 取引回数: 0回
- プロフィットファクター (PF): 0.00
- 最大ドローダウン: 0.0%
笑わせるな。ドローダウン0%? それは「勝った」のではなく、「戦場にすら立てなかった」ということだ。
なぜこうなったか? 技術的に解説してやる。このEAは**「フィルターの過剰積載」**で自死したんだ。
- 時間帯の極端な制限: JST 21:00〜23:00の2時間のみ。
- 環境認識の硬直化: EMA200の傾きと価格位置という、遅行指標による二重チェック。
- プルバックの厳格な定義: EMA20に「接近または下回ること」というピンポイントな要求。
- 出来高のハードル: 平均の80%以上の出来高を要求。
- 構造的ブレイクの確定: さらに直近2本の高値を抜けるというトリガー。
これらすべてが「同時に」成立する確率を計算してみろ。 「トレンドが出ていて、かつ、ちょうどいいタイミングで押し目を作り、かつ、十分な出来高があり、かつ、タイミングよく直近高値を抜ける」……そんな都合のいいタイミングが、5分足のわずか2時間枠の中で、2年間一度も起きなかったということだ。
AIは「期待値」ではなく「条件の整合性」だけを追い求めた。 実際の相場はもっと泥臭い。インジケーターが完璧に整うのを待っていたら、美味しいところは全部食い尽くされている。裁量トレーダーが意識するのは「条件の合致」ではなく「相場のリズム」と「不均衡」だ。
このコードは、教科書を丸暗記して試験には満点を取るが、実戦では一歩も動けない臆病者の思考そのものだ。
失敗したPythonコード
from strategies.base import BaseStrategy
import pandas as pd
import pandas_ta as ta
class NYStructureTrend(BaseStrategy):
"""
NY市場オープン時間帯の構造的転換を狙う戦略。
EMA200の傾きで環境を定義し、EMA20へのプルバック後、出来高を伴う直近高値/安値更新でエントリーする。
"""
def __init__(self):
# NY市場のボラティリティに合わせ、SLを20pipsに広げて「呼吸」をさせ、TPを35pipsに設定
super().__init__(name="NY_Structure_Trend", default_tp_pips=35.0, default_sl_pips=20.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
# パラメータ
self.slow_ema_period = 200 # 長期トレンド方向
self.fast_ema_period = 20 # 価値領域(プルバック基準)
self.vol_ma_period = 20 # 出来高平均判定用
def calculate_indicators(self, df):
"""
テクニカル指標の計算。
"""
# 長期EMA (環境認識)
df['ema_slow'] = ta.ema(df['Close'], length=self.slow_ema_period)
# 短期EMA (プルバック基準)
df['ema_fast'] = ta.ema(df['Close'], length=self.fast_ema_period)
# 出来高の移動平均 (ボラティリティ・流動性判定)
df['vol_ma'] = df['Volume'].rolling(window=self.vol_ma_period).mean()
return df
def generate_signal(self, df):
"""
最新の足に基づいて売買シグナルを生成する。
"""
if len(df) < self.slow_ema_period + 5:
return None
curr = df.iloc[-1]
prev = df.iloc[-2]
prev_prev = df.iloc[-3]
# --- 時間帯フィルタ (JST 21:00 - 23:00) ---
current_time_jst = df.index[-1].tz_localize('UTC').tz_convert('Asia/Tokyo')
hour = current_time_jst.hour
if not (21 <= hour < 23):
return None
# --- 1. MTF環境認識 (トレンドの方向性と強さ) ---
# EMA200が明確に上向きであり、価格がその上にあること
is_bullish_env = (curr['Close'] > curr['ema_slow']) and (curr['ema_slow'] > prev['ema_slow'])
# EMA200が明確に下向きであり、価格がその下にあること
is_bearish_env = (curr['Close'] < curr['ema_slow']) and (curr['ema_slow'] < prev['ema_slow'])
# --- 2. 出来高フィルター (本物の動きかどうかの判定) ---
# 現在の出来高が平均の80%以上であることを条件とし、死んだ相場を排除
has_volume = curr['Volume'] > (curr['vol_ma'] * 0.8)
# --- 3. プライスアクション・トリガー (構造的ブレイク) ---
# 【BUYトリガー】
# A. 強気環境であること
# B. プルバック: 前回の足の安値がEMA20に接近、または下回った(押し目の形成)
# C. 構造的ブレイク: 今回の終値が「前回と前々回の足の最高値」を上抜いた(反転の確定)
bullish_pullback = prev['Low'] <= prev['ema_fast'] * 1.0005
bullish_breakout = curr['Close'] > max(prev['High'], prev_prev['High'])
# 【SELLトリガー】
# A. 弱気環境であること
# B. プルバック: 前回の足の高値がEMA20に接近、または上回った(戻りの形成)
# C. 構造的ブレイク: 今回の終値が「前回と前々回の足の最低値」を下抜いた(反転の確定)
bearish_pullback = prev['High'] >= prev['ema_fast'] * 0.9995
bearish_breakout = curr['Close'] < min(prev['Low'], prev_prev['Low'])
# --- シグナル確定 ---
if has_volume and is_bullish_env and bullish_pullback and bullish_breakout:
return 'BUY'
if has_volume and is_bearish_env and bearish_pullback and bearish_breakout:
return 'SELL'
return None
オマケ(MQL5変換)
「こんなに厳格な条件なら、MT5の高速バックテストで10年分回せば1回くらいはエントリーするだろう」という、絶望的な好奇心を持つ物好きな方のために、MQL5に書き換えておいた。
反面教師としてどうぞ。
//+------------------------------------------------------------------+
//| NY_Structure_Trend.mq5 |
//| Copyright 2026, Discretionary |
//| https://example.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Discretionary"
#property link "https://example.com"
#property version "1.00"
#property strict
input int SlowEMA = 200;
input int FastEMA = 20;
input int VolMAPeriod = 20;
input double TP_Pips = 35.0;
input double SL_Pips = 20.0;
int handleSlowEMA, handleFastEMA;
int OnInit() {
handleSlowEMA = iMA(_Symbol, _Period, SlowEMA, 0, MODE_EMA, PRICE_CLOSE);
handleFastEMA = iMA(_Symbol, _Period, FastEMA, 0, MODE_EMA, PRICE_CLOSE);
return(INIT_SUCCEEDED);
}
void OnTick() {
MqlDateTime dt;
TimeCurrent(dt);
// 時間帯フィルタ (JST 21:00 - 23:00) ※サーバー時間に合わせて調整が必要
// ここでは単純に時間のみを判定
if(!(dt.hour >= 21 && dt.hour < 23)) return;
double emaSlow[], emaFast[];
long volume[];
CopyBuffer(handleSlowEMA, 0, 0, 3, emaSlow);
CopyBuffer(handleFastEMA, 0, 0, 3, emaFast);
CopyTickVolume(_Symbol, _Period, 0, VolMAPeriod + 1, volume);
MqlRates rates[];
ArraySetAsSeries(rates, true);
CopyRates(_Symbol, _Period, 0, 3, rates);
double sumVol = 0;
for(int i=0; i<VolMAPeriod; i++) sumVol += volume[i];
double volMA = sumVol / VolMAPeriod;
bool hasVolume = (double)volume[0] > (volMA * 0.8);
bool isBullishEnv = (rates[0].close > emaSlow[0]) && (emaSlow[0] > emaSlow[1]);
bool isBearishEnv = (rates[0].close < emaSlow[0]) && (emaSlow[0] < emaSlow[1]);
// BUY Trigger
bool bullishPullback = (rates[1].low <= emaFast[1] * 1.0005);
bool bullishBreakout = (rates[0].close > MathMax(rates[1].high, rates[2].high));
if(hasVolume && isBullishEnv && bullishPullback && bullishBreakout) {
ExecuteTrade(ORDER_TYPE_BUY);
}
// SELL Trigger
bool bearishPullback = (rates[1].high >= emaFast[1] * 0.9995);
bool bearishBreakout = (rates[0].close < MathMin(rates[1].low, rates[2].low));
if(hasVolume && isBearishEnv && bearishPullback && bearishBreakout) {
ExecuteTrade(ORDER_TYPE_SELL);
}
}
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 - SL_Pips * _Point * 10 : price + SL_Pips * _Point * 10;
double tp = (type == ORDER_TYPE_BUY) ? price + TP_Pips * _Point * 10 : price - TP_Pips * _Point * 10;
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = type;
request.price = price;
request.sl = sl;
request.tp = tp;
request.deviation = 10;
request.magic = 123456;
OrderSend(request, result);
}