またAIが「完璧なロジック」を自称してゴミを量産した。
今回の被験体は、NYオープンという相場のボラティリティが最大化する時間帯を狙った「レンジブレイクアウト戦略」だ。Pythonコードを見る限り、AIの意図は明確だった。
- 環境認識: EMA50とEMA200のパーフェクトオーダーでトレンド方向を固定。
- トリガー: 直近1時間の高値・安値を終値でブレイク。
- フィルター: EMA200からの乖離を制限し、「高値掴み・安値掴み」を防止。
- 時間限定: NYオープン(JST 21-23時)という、最も「動きがある」時間帯に限定。
一見すると、リスク管理とトレンドフォローを兼ね備えた堅実な戦略に見える。だが、結果は惨敗。PF 0.84という、時間をかけて資金を溶かす典型的な「負けパターン」に陥った。
なぜこのロジックは「ゴミ」なのか
結論から言えば、「遅行指標によるトレンド判定」と「短期レンジブレイク」の相性が最悪だからだ。
1. 遅行性のダブルパンチ
EMA50とEMA200のパーフェクトオーダーを確認してエントリーする。この時点で、5分足レベルのトレンドは既に中盤から終盤に差し掛かっていることが多い。そこにさらに「直近1時間の高値ブレイク」という条件を加えるため、エントリータイミングは常に「トレンドの天井・底」付近になる。
2. NYオープンの「ダマシ」を正解だと思い込んでいる
NYオープン時間帯は、方向感を決める前の激しい上下振幅(ウィップソー)が頻発する。12本(1時間)程度の狭いレンジをブレイクしたからといって、それがトレンドの継続を意味することは少ない。むしろ、AIが「完璧なトレンドだ」と判定して飛び乗った瞬間が、大口の利益確定による反転ポイントになるという皮肉な結果を招いている。
3. 最大ドローダウン 0.1% の正体
一見、最大DD 0.1%という数字は驚異的な安定感に見える。だが、クオンツ的に見ればこれは「リスクを取っていない」のではなく、**「損切り(SL 15pips)が浅すぎて、方向性が出る前に速攻で切られている」**だけのことだ。PF 0.84という数値が示す通り、勝ちを伸ばせず、負けを頻発させているだけ。安定して負けているだけである。
AIは「教科書に書いてある条件」を組み合わせれば勝てると信じているようだが、相場は足し算で解決するほど単純ではない。
失敗したPythonコード
from strategies.base import BaseStrategy
import pandas as pd
import pandas_ta as ta
class NYRangeBreakout(BaseStrategy):
"""
NYオープン時間帯に特化したMTFレンジブレイクアウト戦略。
EMA 50/200による強力なトレンドフィルターを掛け、
直近1時間(12本分)の最高値/最安値を終値でブレイクした瞬間のみにエントリーを絞る。
"""
def __init__(self):
# 勝率と利益のバランスを最適化した設定
super().__init__(name="NYRangeBreakout", default_tp_pips=25.0, default_sl_pips=15.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
def calculate_indicators(self, df):
"""
MTFトレンド判定およびブレイクアウト判定用の指標を計算。
"""
# 中期トレンドフィルター (15m相当)
df['ema_mid'] = ta.ema(df['Close'], length=50)
# 長期トレンドフィルター (1h相当)
df['ema_long'] = ta.ema(df['Close'], length=200)
# 直近12本(1時間分)の最高値と最安値を計算 (ブレイクアウト判定用)
# shift(1)することで、現在の足を含めない「直近の壁」を定義する
df['range_high'] = df['High'].rolling(window=12).max().shift(1)
df['range_low'] = df['Low'].rolling(window=12).min().shift(1)
return df
def generate_signal(self, df):
"""
NYオープン時間帯に、MTFトレンドが一致し、かつ1時間レンジをブレイクした時にシグナルを出す。
"""
if len(df) < 200:
return None
# 最新の足を取得
current_bar = df.iloc[-1]
# --- 時間帯判定 (JST 21:00 - 23:00) ---
# JST 21:00 = UTC 12:00 / JST 23:00 = UTC 14:00
current_hour = current_bar.name.hour
if not (12 <= current_hour < 14):
return None
# --- 変数抽出 ---
close = current_bar['Close']
ema_m = current_bar['ema_mid']
ema_l = current_bar['ema_long']
range_h = current_bar['range_high']
range_l = current_bar['range_low']
# --- エントリーロジック ---
# BUY:
# 1. MTF環境認識: 価格 > EMA50 > EMA200 (完璧な上昇トレンドの並び)
# 2. ブレイク判定: 終値が直近12本(1時間)の最高値を明確に上抜けた
if (close > ema_m) and (ema_m > ema_l):
if close > range_h:
# 乖離フィルター: 長期EMAから離れすぎている場合は飛び乗りと判断
if close < ema_l * 1.01:
return 'BUY'
# SELL:
# 1. MTF環境認識: 価格 < EMA50 < EMA200 (完璧な下降トレンドの並び)
# 2. ブレイク判定: 終値が直近12本(1時間)の最安値を明確に下抜けた
if (close < ema_m) and (ema_m < ema_l):
if close < range_l:
# 乖離フィルター
if close > ema_l * 0.99:
return 'SELL'
return None
オマケ:MQL5への変換
「この絶望感をMT5で体感したい」という物好きな方のために、上記ロジックをMQL5に移植した。検証環境でぜひ、資金がじわじわと削られていく快感を味わってほしい。
//+------------------------------------------------------------------+
//| NYRangeBreakout.mq5 |
//| Copyright 2026, Cynical Quant |
//+------------------------------------------------------------------+
#property strict
input int EMA_Mid_Period = 50;
input int EMA_Long_Period = 200;
input int Range_Window = 12;
input double TP_Pips = 25.0;
input double SL_Pips = 15.0;
input int Start_Hour = 12; // UTC
input int End_Hour = 14; // UTC
int handle_ema_mid, handle_ema_long;
int OnInit() {
handle_ema_mid = iMA(_Symbol, _Period, EMA_Mid_Period, 0, MODE_EMA, PRICE_CLOSE);
handle_ema_long = iMA(_Symbol, _Period, EMA_Long_Period, 0, MODE_EMA, PRICE_CLOSE);
return(INIT_SUCCEEDED);
}
void OnTick() {
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
if(dt.hour < Start_Hour || dt.hour >= End_Hour) return;
double ema_mid[], ema_long[], close[], high[], low[];
ArraySetAsSeries(ema_mid, true);
ArraySetAsSeries(ema_long, true);
ArraySetAsSeries(close, true);
ArraySetAsSeries(high, true);
ArraySetAsSeries(low, true);
if(CopyBuffer(handle_ema_mid, 0, 0, 2, ema_mid) < 2) return;
if(CopyBuffer(handle_ema_long, 0, 0, 2, ema_long) < 2) return;
if(CopyClose(_Symbol, _Period, 0, 2, close) < 2) return;
if(CopyHigh(_Symbol, _Period, 0, Range_Window + 1, high) < Range_Window + 1) return;
if(CopyLow(_Symbol, _Period, 0, Range_Window + 1, low) < Range_Window + 1) return;
double range_high = high[1];
double range_low = low[1];
for(int i=1; i<=Range_Window; i++) {
if(high[i] > range_high) range_high = high[i];
if(low[i] < range_low) range_low = low[i];
}
double price = close[0];
// BUY Logic
if(price > ema_mid[0] && ema_mid[0] > ema_long[0]) {
if(price > range_high && price < ema_long[0] * 1.01) {
ExecuteTrade(ORDER_TYPE_BUY);
}
}
// SELL Logic
else if(price < ema_mid[0] && ema_mid[0] < ema_long[0]) {
if(price < range_low && price > ema_long[0] * 0.99) {
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.magic = 123456;
OrderSend(request, result);
}