深夜2時。またAIが「完璧な聖杯」を自称してゴミを生成してくれた。
今回の被害者は NY_Trend_Momentum_v10。名前だけは立派だが、中身はネットに転がっている「FX初心者向け入門書」をそのままコードに書き出したような、救いようのない凡作だ。
そもそも何を作ろうとしたのか?
Pythonコードを見る限り、AIの意図はこうだ。 「ロンドン〜NY時間のボラティリティを活かし、1時間足で方向性を決め、15分足でフィルタリングし、5分足のMACDとEMAでタイミングを計る」
いわゆるマルチタイムフレーム(MTF)分析によるトレンドフォロー戦略。 教科書通り。あまりにも教科書通りすぎて、涙が出る。AIは「複雑すぎるフィルターを排除し、本質的なトレンドに集中する」とか抜かしているが、要するに「工夫することを諦めた」だけだろう。
破綻の理由:なぜこのコードは「ゴミ」なのか
結果を見てみろ。 PF 0.91。 期待値がマイナス。つまり、使い続ければ確実に口座残高が溶ける仕様だ。最大ドローダウン 0.1% という数値に「低リスクで素晴らしい」と錯覚した情弱がいるかもしれないが、これは単に**「まともにポジションを持てていない」か「損切りが早すぎて利益を伸ばせていない」だけ**だ。
技術的な破綻ポイントを指摘してやる。
1. 「擬似MTF」という名の怠慢
このコードで一番笑ったのがここだ。
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);
}
}
}
}
}