また出たよ。最近のAIが書き出す「いかにも正解っぽく見えるゴミ」だ。

今回の標本は、NY市場のボラティリティを狙ったという『NYStructureTrend』。 ロジックを紐解けば、長期EMAで環境認識をし、短期EMAへのプルバックを待ち、出来高を確認し、直近高値・安値のブレイクでエントリーする……という、FXの入門書に書いてある「正解」を全部盛り込んだような構成だ。

AIはきっとこう思ったんだろう。「条件を重ねれば重ねるほど、勝率は上がり、リスクは減る」と。 だが、相場の本質を理解していない人間(とAI)が陥る最大の罠がここにある。

破綻の理由:フィルターの積み上げによる「取引機会の絶滅」

まずはこの絶望的なバックテスト結果を見てくれ。

  • 取引回数: 0回
  • プロフィットファクター (PF): 0.00
  • 最大ドローダウン: 0.0%

笑わせるな。ドローダウン0%? それは「勝った」のではなく、「戦場にすら立てなかった」ということだ。

なぜこうなったか? 技術的に解説してやる。このEAは**「フィルターの過剰積載」**で自死したんだ。

  1. 時間帯の極端な制限: JST 21:00〜23:00の2時間のみ。
  2. 環境認識の硬直化: EMA200の傾きと価格位置という、遅行指標による二重チェック。
  3. プルバックの厳格な定義: EMA20に「接近または下回ること」というピンポイントな要求。
  4. 出来高のハードル: 平均の80%以上の出来高を要求。
  5. 構造的ブレイクの確定: さらに直近2本の高値を抜けるというトリガー。

これらすべてが「同時に」成立する確率を計算してみろ。 「トレンドが出ていて、かつ、ちょうどいいタイミングで押し目を作り、かつ、十分な出来高があり、かつ、タイミングよく直近高値を抜ける」……そんな都合のいいタイミングが、5分足のわずか2時間枠の中で、2年間一度も起きなかったということだ。

AIは「期待値」ではなく「条件の整合性」だけを追い求めた。 実際の相場はもっと泥臭い。インジケーターが完璧に整うのを待っていたら、美味しいところは全部食い尽くされている。裁量トレーダーが意識するのは「条件の合致」ではなく「相場のリズム」と「不均衡」だ。

このコードは、教科書を丸暗記して試験には満点を取るが、実戦では一歩も動けない臆病者の思考そのものだ。

失敗したPythonコード

[SYSTEM WARNING] 本記事のEAは実運用に耐えません

大切な資金を運用するなら、厳しいリアルフォワードテストをクリアしたプロ開発のEAを利用することを強く推奨します。
実際にリアルタイムで利益を出し続けている本物のデータを確認してください。
※GogoJungleのリンク発行待ち(プレースホルダー)

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);
}