深夜2時。またAIが「完璧な聖杯」を自称してゴミを生成してくれた。 今回の被害者は NY_Trend_Momentum_v10。名前だけは立派だが、中身はネットに転がっている「FX初心者向け入門書」をそのままコードに書き出したような、救いようのない凡作だ。

そもそも何を作ろうとしたのか?

Pythonコードを見る限り、AIの意図はこうだ。 「ロンドン〜NY時間のボラティリティを活かし、1時間足で方向性を決め、15分足でフィルタリングし、5分足のMACDとEMAでタイミングを計る」

いわゆるマルチタイムフレーム(MTF)分析によるトレンドフォロー戦略。 教科書通り。あまりにも教科書通りすぎて、涙が出る。AIは「複雑すぎるフィルターを排除し、本質的なトレンドに集中する」とか抜かしているが、要するに「工夫することを諦めた」だけだろう。

破綻の理由:なぜこのコードは「ゴミ」なのか

結果を見てみろ。 PF 0.91。 期待値がマイナス。つまり、使い続ければ確実に口座残高が溶ける仕様だ。最大ドローダウン 0.1% という数値に「低リスクで素晴らしい」と錯覚した情弱がいるかもしれないが、これは単に**「まともにポジションを持てていない」か「損切りが早すぎて利益を伸ばせていない」だけ**だ。

技術的な破綻ポイントを指摘してやる。

1. 「擬似MTF」という名の怠慢

このコードで一番笑ったのがここだ。

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

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

👉 プロが実稼働で証明済みの本物のEA(BeeOne_USDJPY)はこちらフォワード5年で収益率100%!安定的に利益を積み上げるシンプルロジックEA! | GogoJungle
df['EMA_15m'] = ta.ema(df['Close'], length=20 * 3)
df['EMA_1h'] = ta.ema(df['Close'], length=200 * 12)

おい、正気か? 5分足のデータに対して期間を3倍、12倍にしたEMAを引けばMTFになると思っているのか? 本物のMTFは、上位足の確定足(Close)を確認して判定するものだ。単に期間を伸ばしたEMAは、ただの「超長期移動平均線」に過ぎない。上位足の構造的トレンドを捉えているつもりだろうが、実際にはただのラグ(遅延)を増大させているだけ。エントリーが遅すぎる。

2. インジケーターの盛りすぎ(ラグの三重奏)

EMA20 + MACD + ADX。 すべてが「遅行指標」だ。 1時間足の方向を確認し、ADXで強度を測り、MACDの反転を待ち、さらにEMA20を上抜けるのを待つ……。 その頃にはもうトレンドの終盤か、あるいは調整局面に入っている。AIは「確度を高めている」つもりだろうが、実際には**「絶好のエントリータイミングをすべて逃し、天井・底で掴まされるロジック」**を構築している。

3. トレーリングストップの絶望的な設定

trail_start_pips=20.0。 5分足のノイズに簡単に狩られる設定だ。特にNY時間のようなボラティリティがある相場で、わずか20ピップスの利益が出た瞬間にストップを上げる? 結果として、たまにトレンドに乗っても、ちょっとした押し目で即座に決済され、TP 100ピップスに到達することなんて万に一つもない。PF 0.91の正体は、この「チキンすぎる決済ロジック」と「遅すぎるエントリー」の合わせ技だ。

結論:AIに「トレンドフォロー」を丸投げすると、ネット上の平均的な(=勝てない)ロジックの集積体が出来上がる。


失敗の証拠:Pythonコード

from strategies.base import BaseStrategy
import pandas_ta as ta
import pandas as pd
import numpy as np

class NYTrendQuantStrategy(BaseStrategy):
    def __init__(self):
        # 最大ドローダウンを極めて低く抑えつつ、利益を伸ばすための設定
        # 損切りを適切に置き、トレーリングストップでトレンドの最大値を追う
        super().__init__(
            name="NY_Trend_Momentum_v10",
            default_tp_pips=100.0, 
            default_sl_pips=30.0, 
            enable_trailing_stop=True, 
            trail_start_pips=20.0
        )
        self.base_timeframe = "5m"
        self.vision_timeframes = ["5m", "15m", "1h"]

    def calculate_indicators(self, df):
        """
        取引回数を確保しつつ、優位性を維持するための指標セット。
        複雑すぎるフィルターを排除し、本質的なトレンド方向とモメンタムに集中する。
        """
        # --- 5分足 (Execution Layer) ---
        # 短期トレンド基準
        df['EMA_20'] = ta.ema(df['Close'], length=20)
        
        # モメンタムの方向性を判定するMACD
        macd = ta.macd(df['Close'])
        df['MACD_hist'] = macd.iloc[:, 1]
        
        # トレンド強度 (ADX) - 閾値を低めに設定して機会を逃さない
        adx_df = ta.adx(df['High'], df['Low'], df['Close'], length=14)
        df['ADX'] = adx_df.iloc[:, 0]
        
        # ボラティリティ (ATR)
        df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], length=14)
        df['ATR_ma'] = df['ATR'].rolling(window=100).mean()

        # --- マルチタイムフレーム (MTF) 分析 ---
        # 15分足相当 (5m * 3)
        df['EMA_15m'] = ta.ema(df['Close'], length=20 * 3)
        # 1時間足相当 (5m * 12)
        df['EMA_1h'] = ta.ema(df['Close'], length=200 * 12)

        return df

    def generate_signal(self, df):
        """
        【ロジック:NY-London モメンタム・シンクロ】
        取引回数が0になることを避けるため、条件を「必須フィルター」と「トリガー」に明確に分離。
        
        1. 時間帯:ボラティリティの高いロンドン〜NY時間 (JST 16:00 - 01:00)
        2. 環境フィルター(必須):
           - 1時間足のEMAより上に価格があるか (大局トレンド)
           - ADXが一定水準 (15以上) であり、相場が完全に停滞していないこと
        3. エントリートリガー(執行):
           - 5分足でMACDヒストグラムが反転(0を上抜け、または上昇開始)
           - かつ、価格がEMA20を上抜けてモメンタムが確定した瞬間
        """
        if len(df) < 100:
            return None

        last = df.iloc[-1]
        prev = df.iloc[-2]
        
        # 1. エントリー時間帯 (JST 16:00 - 01:00)
        current_hour = df.index[-1].hour
        if not (current_hour >= 16 or current_hour <= 1):
            return None

        # 2. 環境フィルター (Market Environment)
        # 大局的な方向性のみを厳格に判定 (1h EMA)
        is_bull_market = last['Close'] > last['EMA_1h']
        is_bear_market = last['Close'] < last['EMA_1h']
        
        # 最低限のトレンド強度 (ADX > 15) でフィルタリング
        if last['ADX'] < 15:
            return None
        
        # 極端な低ボラティリティ相場を排除
        if last['ATR'] < last['ATR_ma'] * 0.6:
            return None

        # 3. エントリートリガー (Execution)
        
        # --- BUY SIGNAL ---
        if is_bull_market:
            # 条件A: MACDヒストグラムが正転したか、または正の値で上昇している
            momentum_up = (prev['MACD_hist'] <= 0 and last['MACD_hist'] > 0) or \
                          (last['MACD_hist'] > prev['MACD_hist'] and last['MACD_hist'] > 0)
            
            # 条件B: 価格がEMA20を明確に上回っていること (短期的な勢いの確認)
            if momentum_up and last['Close'] > last['EMA_20']:
                # さらに15分足の方向性と一致していれば確度が高い
                if last['Close'] > last['EMA_15m']:
                    return 'BUY'

        # --- SELL SIGNAL ---
        if is_bear_market:
            # 条件A: MACDヒストグラムが負転したか、または負の値で下落している
            momentum_down = (prev['MACD_hist'] >= 0 and last['MACD_hist'] < 0) or \
                            (last['MACD_hist'] < prev['MACD_hist'] and last['MACD_hist'] < 0)
            
            # 条件B: 価格がEMA20を明確に下回っていること
            if momentum_down and last['Close'] < last['EMA_20']:
                # さらに15分足の方向性と一致していれば確度が高い
                if last['Close'] < last['EMA_15m']:
                    return 'SELL'

        return None

オマケ:MQL5への変換(反面教師用)

「このゴミのようなロジックが実際にどう動くか、MT5で身をもって体験したい」という物好きな方のために、MQL5に移植しておいた。

//+------------------------------------------------------------------+
//|                                    NY_Trend_Momentum_Trash.mq5   |
//|                                  Copyright 2026, OtakuEngineer   |
//+------------------------------------------------------------------+
#property strict

input double InpTP = 1000;          // Take Profit (points)
input double InpSL = 300;           // Stop Loss (points)
input double InpTrailStart = 200;   // Trailing Stop Start (points)
input int    InpEMA_Fast = 20;      // Short EMA
input int    InpEMA_15m_Fake = 60;  // Fake 15m EMA (20*3)
input int    InpEMA_1h_Fake = 2400; // Fake 1h EMA (200*12)

int handleEMA20, handleEMA15m, handleEMA1h, handleMACD, handleADX, handleATR;

int OnInit() {
    handleEMA20 = iMA(_Symbol, _Period, InpEMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
    handleEMA15m = iMA(_Symbol, _Period, InpEMA_15m_Fake, 0, MODE_EMA, PRICE_CLOSE);
    handleEMA1h = iMA(_Symbol, _Period, InpEMA_1h_Fake, 0, MODE_EMA, PRICE_CLOSE);
    handleMACD = iMACD(_Symbol, _Period, 12, 26, 9, PRICE_CLOSE);
    handleADX = iADX(_Symbol, _Period, 14);
    handleATR = iATR(_Symbol, _Period, 14);
    return(INIT_SUCCEEDED);
}

void OnTick() {
    MqlDateTime dt;
    TimeCurrent(dt);
    if(!(dt.hour >= 16 || dt.hour <= 1)) return;

    double ema20[], ema15m[], ema1h[], macdMain[], macdSig[], adx[], atr[];
    ArraySetAsSeries(ema20, true);
    ArraySetAsSeries(ema15m, true);
    ArraySetAsSeries(ema1h, true);
    ArraySetAsSeries(macdMain, true);
    ArraySetAsSeries(macdSig, true);
    ArraySetAsSeries(adx, true);
    ArraySetAsSeries(atr, true);

    CopyBuffer(handleEMA20, 0, 0, 2, ema20);
    CopyBuffer(handleEMA15m, 0, 0, 2, ema15m);
    CopyBuffer(handleEMA1h, 0, 0, 2, ema1h);
    CopyBuffer(handleMACD, 0, 0, 2, macdMain); // Main line (histogram for MQL5 MACD is main)
    CopyBuffer(handleADX, 0, 0, 2, adx);
    CopyBuffer(handleATR, 0, 0, 100, atr);

    double close = iClose(_Symbol, _Period, 0);
    double atrMa = 0;
    for(int i=0; i<100; i++) atrMa += atr[i];
    atrMa /= 100;

    if(adx[0] < 15 || atr[0] < atrMa * 0.6) return;

    bool isBull = (close > ema1h[0]);
    bool isBear = (close < ema1h[0]);

    // Buy Signal
    if(isBull && ((macdMain[1] <= 0 && macdMain[0] > 0) || (macdMain[0] > macdMain[1] && macdMain[0] > 0))) {
        if(close > ema20[0] && close > ema15m[0]) {
            ExecuteTrade(ORDER_TYPE_BUY);
        }
    }
    // Sell Signal
    if(isBear && ((macdMain[1] >= 0 && macdMain[0] < 0) || (macdMain[0] < macdMain[1] && macdMain[0] < 0))) {
        if(close < ema20[0] && close < ema15m[0]) {
            ExecuteTrade(ORDER_TYPE_SELL);
        }
    }
    
    ManageTrailingStop();
}

void ExecuteTrade(ENUM_ORDER_TYPE type) {
    MqlTradeRequest request = {};
    MqlTradeResult result = {};
    request.action = TRADE_ACTION_DEAL;
    request.symbol = _Symbol;
    request.volume = 0.1;
    request.type = type;
    request.price = (type == ORDER_TYPE_BUY) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
    request.sl = (type == ORDER_TYPE_BUY) ? request.price - InpSL * _Point : request.price + InpSL * _Point;
    request.tp = (type == ORDER_TYPE_BUY) ? request.price + InpTP * _Point : request.price - InpTP * _Point;
    OrderSend(request, result);
}

void ManageTrailingStop() {
    for(int i=PositionsTotal()-1; i>=0; i--) {
        ulong ticket = PositionGetTicket(i);
        if(PositionSelectByTicket(ticket)) {
            double price = PositionGetDouble(POSITION_PRICE_OPEN);
            double current = PositionGetDouble(POSITION_PRICE_CURRENT);
            if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) {
                if(current - price > InpTrailStart * _Point) {
                    MqlTradeRequest req = {}; MqlTradeResult res = {};
                    req.action = TRADE_ACTION_SLTP; req.position = ticket;
                    req.sl = current - InpSL * _Point;
                    OrderSend(req, res);
                }
            } else {
                if(price - current > InpTrailStart * _Point) {
                    MqlTradeRequest req = {}; MqlTradeResult res = {};
                    req.action = TRADE_ACTION_SLTP; req.position = ticket;
                    req.sl = current + InpSL * _Point;
                    OrderSend(req, res);
                }
            }
        }
    }
}