深夜2時。またAIが「完璧なロジックを生成しました!」みたいな顔してゴミを量産してる。 今回もまた、教科書通りの「それっぽい」手法を盛り込んだだけの、実戦完全無視なボツEAが爆誕した。
今回のターゲットは**「NYオープン時間帯の構造的ブレイクアウト」**。 Pythonコードを眺める限り、AIの思考回路はこうだ。 「NY時間はボラが出る(常識) → EMA200でトレンド方向を固定する(教科書1ページ目) → 直近のレンジを抜けた方向にエントリーする(初心者向け手法) → でも大陽線・大陰線は『クライマックス』だから避ける(ここが致命的な勘違い)」
……いや、NY時間のボラティリティを狙っておいて、強い動きを「クライマックス」として排除するってどういう脳内回路だよ。正気か?
絶望のバックテスト結果
- PF: 0.86(はい、勝ち少ない。手数料とスプレッドで死ぬパターン)
- 最大ドローダウン: 0.1%(一見低く見えるが、単にリスクを取りすぎていないか、取引回数に対して期待値が低すぎて資金がじりじりと削られただけの結果)
- 不合格理由: 直近2年間のPFが1.2を余裕で下回った。
技術的な「破綻」ポイント:なぜ負けるのか
1. 「構造(Structure)」の定義が幼稚園レベル
lookback_period = 5。5分足の5本分、つまり「直近25分間の高値・安値」を構造と呼んでいる。
おい、それは「構造」じゃなくてただの「ノイズ」だ。相場の構造(Structure)を語るなら、最低でも1時間足や4時間足の意識されるレベルを追え。25分のレンジブレイクなんて、スプレッドの変動レベルで簡単にダマシに合う。
2. 自殺志願のような「クライマックス・フィルタ」
body_size < (curr['atr'] * 2.5)
ここがこのコードの最大のギャグ。NY時間のブレイクアウト戦略において、最も利益が出るのは「強い方向感が出たとき」だ。それを「実体が大きすぎるから天井/底かも」という妄想でフィルタリングしている。
つまり、**「本当に強いトレンドが出たときだけエントリーを回避し、迷いのある弱い動きのときだけエントリーする」**という、負けるためだけの完璧なロジックが完成している。天才か。
3. 固定TP/SLの思考停止 TP 30 / SL 15 という固定ピプス設定。 ボラティリティが激しく変動するNY時間に、ATRを計算しておきながら利確・損切りにそれを反映させない。これじゃあ、ボラが大きい日は一瞬で狩られ、ボラが小さい日は利確まで届かずに戻ってくる。
結論:このEAは**「トレンドの初動を逃し、ノイズに飛び乗り、正解の波だけを拒絶する」**という、トレードにおける禁忌を全て詰め込んだ芸術作品である。
敗北のコード(Python)
from strategies.base import BaseStrategy
import pandas as pd
import numpy as np
class NYStructureBreak(BaseStrategy):
"""
NYオープン時間帯(21:00-23:00 JST)に特化した構造的ブレイクアウト戦略。
EMA200で方向性を固定し、直近の価格構造(レンジ)を終値でブレイクした瞬間を狙う。
"""
def __init__(self):
# PF改善のためリスクリワード比を 2:1 に設定 (TP 30 / SL 15)
super().__init__(name="NY_Structure_Break", default_tp_pips=30.0, default_sl_pips=15.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
# パラメータ設定
self.ema_long_period = 200 # 1時間足相当の環境認識
self.atr_period = 14 # ボラティリティ判定
self.lookback_period = 5 # 構造(レンジ)判定のための参照期間
def calculate_indicators(self, df):
"""
テクニカル指標の計算
"""
# 1. 大局的なトレンド判定 (EMA200)
df['ema_long'] = df['Close'].ewm(span=self.ema_long_period, adjust=False).mean()
# 2. ATR (Average True Range) の計算
high_low = df['High'] - df['Low']
high_close = (df['High'] - df['Close'].shift()).abs()
low_close = (df['Low'] - df['Close'].shift()).abs()
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
df['atr'] = tr.rolling(window=self.atr_period).mean()
return df
def generate_signal(self, df):
"""
エントリーシグナルの生成
"""
if len(df) < self.ema_long_period:
return None
curr = df.iloc[-1]
# --- 時間帯判定 (JST 21:00 - 23:00 => UTC 12:00 - 14:00) ---
current_hour = curr.name.hour
if not (12 <= current_hour < 14):
return None
# --- 層1: MTF環境認識 (大局的な方向性の固定) ---
is_bullish_env = curr['Close'] > curr['ema_long']
is_bearish_env = curr['Close'] < curr['ema_long']
# --- 層2: 構造的ブレイクアウト (Price Action) ---
# 直近のレンジ(最高値・最安値)を計算 (現在の足を除いた直近n本)
recent_df = df.iloc[-(self.lookback_period + 1):-1]
local_high = recent_df['High'].max()
local_low = recent_df['Low'].min()
# --- 層3: ボラティリティ・フィルタ (クライマックス回避) ---
# 確定足の実体が大きすぎると「天井/底」の可能性が高いため、ATRで制限
body_size = abs(curr['Close'] - curr['Open'])
is_not_climax = body_size < (curr['atr'] * 2.5)
# 買いシグナル:
# 1. 大局的に強気トレンドであること
# 2. 終値が直近の最高値を明確に上抜けたこと (Structure Break)
# 3. 陽線であり、かつクライマックス(過剰な伸び)ではないこと
if is_bullish_env and is_not_climax:
if curr['Close'] > local_high and curr['Close'] > curr['Open']:
return 'BUY'
# 売りシグナル:
# 1. 大局的に弱気トレンドであること
# 2. 終値が直近の最安値を明確に下抜けたこと (Structure Break)
# 3. 陰線であり、かつクライマックス(過剰な伸び)ではないこと
if is_bearish_env and is_not_climax:
if curr['Close'] < local_low and curr['Close'] < curr['Open']:
return 'SELL'
return None
オマケ:MQL5変換(反面教師用)
「この絶望感をMT5で体感したい」という物好きな方のために、ロジックを忠実に移植してやった。そのまま動かすと、あなたの口座残高がゆっくりと、しかし確実に溶けていく快感を味わえるはずだ。
//+------------------------------------------------------------------+
//| NY_Structure_Break.mq5 |
//| Copyright 2026, Otaku Engineer |
//| https://example.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Otaku Engineer"
#property link "https://example.com"
#property version "1.00"
#property strict
input int InpEMAPeriod = 200; // EMA Period
input int InpATRPeriod = 14; // ATR Period
input int InpLookback = 5; // Lookback Period
input double InpTP = 300; // Take Profit (points)
input double InpSL = 150; // Stop Loss (points)
int handleEMA, handleATR;
int OnInit() {
handleEMA = iMA(_Symbol, _Period, InpEMAPeriod, 0, MODE_EMA, PRICE_CLOSE);
handleATR = iATR(_Symbol, _Period, InpATRPeriod);
return(INIT_SUCCEEDED);
}
void OnTick() {
MqlDateTime dt;
TimeCurrent(dt);
// UTC 12:00 - 14:00 (JST 21:00 - 23:00)
if(dt.hour < 12 || dt.hour >= 14) return;
double ema[], atr[], close[], open[], high[], low[];
ArraySetAsSeries(ema, true);
ArraySetAsSeries(atr, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(open, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
if(CopyBuffer(handleEMA, 0, 0, 2, ema) < 2) return;
if(CopyBuffer(handleATR, 0, 0, 2, atr) < 2) return;
if(CopyClose(_Symbol, _Period, 0, InpLookback + 1, close) < InpLookback + 1) return;
if(CopyOpen(_Symbol, _Period, 0, 1, open) < 1) return;
if(CopyHigh(_Symbol, _Period, 0, InpLookback + 1, high) < InpLookback + 1) return;
if(CopyLow(_Symbol, _Period, 0, InpLookback + 1, low) < InpLookback + 1) return;
double localHigh = high[1];
double localLow = low[1];
for(int i=1; i <= InpLookback; i++) {
if(high[i] > localHigh) localHigh = high[i];
if(low[i] < localLow) localLow = low[i];
}
double bodySize = MathAbs(close[0] - open[0]);
bool isNotClimax = bodySize < (atr[0] * 2.5);
bool isBullishEnv = close[0] > ema[0];
bool isBearishEnv = close[0] < ema[0];
if(isBullishEnv && isNotClimax && close[0] > localHigh && close[0] > open[0]) {
TradeBuy();
} else if(isBearishEnv && isNotClimax && close[0] < localLow && close[0] < open[0]) {
TradeSell();
}
}
void TradeBuy() {
MqlTradeRequest request={0};
MqlTradeResult result={0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = request.price - InpSL * _Point;
request.tp = request.price + InpTP * _Point;
OrderSend(request, result);
}
void TradeSell() {
MqlTradeRequest request={0};
MqlTradeResult result={0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
request.sl = request.price + InpSL * _Point;
request.tp = request.price - InpTP * _Point;
OrderSend(request, result);
}