深夜2時。またAIが「完璧な聖杯」を生成したと抜かしていたので、バックテストにぶち込んでみた。 結果? ゴミ。完敗。大爆死。
今回の被害者は NYVolatilityValue と名付けられた、いかにも「分かってる感」を出した戦略だ。
Pythonコードを見る限り、AIの意図はこうだ。
「NY時間のボラティリティを利用し、長期EMAでトレンドを掴み、短期EMAでの押し目(戻り)を確認してから再加速でエントリーする。リスクリワードは1:2.5で効率よく稼ぐぜ!」
……いやいや、2026年にもなってまだそんな「FX初心者向けYouTube動画」みたいなロジックを本気で組んだのか?
破綻の解説:なぜこのコードは「ただの寄付」に終わったか
まず結果を見てくれ。 プロフィットファクター (PF): 1.05
これ、実質的に「手数料とスプレッドで全部持っていかれた」ってことだ。ほぼランダムウォーク。 最大ドローダウン 0.1% という数字だけ見れば「安全」に見えるかもしれないが、これは単にエントリー回数が少なすぎて、運良く大火傷を免れただけ。期待値が死んでいる。
技術的なダメ出しポイント:
-
固定pipsの絶望感: TP 50pips / SL 20pips という固定値。NY時間のボラティリティを狙うと言いながら、ボラティリティに応じて変動しない固定SLを置くという矛盾。NY時間のノイズ一本で20pipsなんて余裕で吹き飛ぶ。AI、お前はATRを計算したのに、なぜそれをSLに反映させなかった? 宝の持ち腐れもいいところだ。
-
「EMA20を上抜けたら買い」というお花畑ロジック:
close_prev <= ema_f_prev * 1.0005で調整を確認し、close_curr > ema_f_currでエントリー。これ、典型的な「ダマシ」の量産機だ。相場がレンジに移行した瞬間、EMAを上下に跨ぐたびにエントリーしてはSLに引っかかる地獄ループが見える。 -
ATRフィルターが形骸化している:
if atr == 0 or pd.isna(atr): return None……は? これを「ボラティリティフィルター」と呼ぶのか? 0じゃなければOKって、それはフィルターじゃなくて単なる「生存確認」だろ。最低限の閾値を設定しろ。
結論として、このEAは**「教科書に書いてあることをそのまま実装したが、相場のダイナミズム(ノイズと流動性)を完全に無視した」**ため、手数料を支払うためだけの機械と化したわけだ。
失敗したPythonコード
from strategies.base import BaseStrategy
import pandas as pd
import pandas_ta as ta
class NYVolatilityValue(BaseStrategy):
"""
NYオープン時間帯 (21:00-23:00 JST) に特化したボラティリティ・バリュー戦略。
長期トレンド方向への回帰(EMA20)を確認し、ATRでボラティリティを、
直近高値/安値の更新で再加速を判定してエントリーする。
"""
def __init__(self):
# リスクリワード比を 1:2.5 に設定 (TP 50pips / SL 20pips)
# NY時間の強いトレンド特性を活かし、1回の勝ちで2.5回分の負けをカバーする。
super().__init__(name="NY_Volatility_Value", default_tp_pips=50.0, default_sl_pips=20.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
# パラメータ設定
self.fast_ema_len = 20 # 短期バリューエリア
self.slow_ema_len = 100 # 長期環境認識 (MTF: 約8時間相当)
self.atr_len = 14 # ボラティリティ判定期間
def calculate_indicators(self, df):
"""
テクニカル指標の計算。
"""
# EMAによる方向性判定
df['ema_fast'] = ta.ema(df['Close'], length=self.fast_ema_len)
df['ema_slow'] = ta.ema(df['Close'], length=self.slow_ema_len)
# ATRによるボラティリティ算出
df['atr'] = ta.atr(df['High'], df['Low'], df['Close'], length=self.atr_len)
return df
def generate_signal(self, df):
"""
シグナル生成ロジック。
【BUY条件】
1. 時間帯が 21:00〜23:00 JST であること
2. 価格が長期EMA100より上にあり、上昇トレンドであること
3. ATRが一定水準以上にあり、相場に十分なボラティリティがあること
4. 直前の足の終値が短期EMA20付近まで調整していること
5. 最新の終値が短期EMA20を上抜け、かつ直前の足の高値を更新して再加速すること
"""
if len(df) < self.slow_ema_len:
return None
# 最新の2本分を取得
curr = df.iloc[-1]
prev = df.iloc[-2]
# 時間判定 (JST)
current_time = curr.name if hasattr(curr.name, 'hour') else df.index[-1]
hour = current_time.hour
if not (21 <= hour < 23):
return None
# 指標の抽出
close_curr = curr['Close']
close_prev = prev['Close']
high_prev = prev['High']
low_prev = prev['Low']
ema_f_curr = curr['ema_fast']
ema_f_prev = prev['ema_fast']
ema_s_curr = curr['ema_slow']
atr = curr['atr']
# ボラティリティフィルター: ATRが0であることや極端に低いことを排除
if atr == 0 or pd.isna(atr):
return None
# --- BUY シグナル ---
# 1. 環境認識: 長期上昇トレンド
if close_curr > ema_s_curr:
# 2. 調整の確認: 前回の終値がEMA20以下、または非常に近い
if close_prev <= ema_f_prev * 1.0005:
# 3. 再加速の確定: EMA20を上抜け、かつ前回の高値を更新
if close_curr > ema_f_curr and close_curr > high_prev:
return 'BUY'
# --- SELL シグナル ---
# 1. 環境認識: 長期下降トレンド
if close_curr < ema_s_curr:
# 2. 調整の確認: 前回の終値がEMA20以上、または非常に近い
if close_prev >= ema_f_prev * 0.9995:
# 3. 再加速の確定: EMA20を下抜け、かつ前回の安値を更新
if close_curr < ema_f_curr and close_curr < low_prev:
return 'SELL'
return None
オマケ:MQL5変換(反面教師用)
「このゴミのようなロジックが、実際のMT5でどう惨敗するかを身をもって体験したい」というマゾヒストな方のために、MQL5に書き換えておいた。どうぞ、資金を溶かす快感に浸ってくれ。
//+------------------------------------------------------------------+
//| NY_Volatility_Value.mq5 |
//| Copyright 2026, OtakuEngineer |
//| https://www.example.com |
//+------------------------------------------------------------------+
#property strict
input int FastEMALen = 20;
input int SlowEMALen = 100;
input int ATRLen = 14;
input double TP_Pips = 50.0;
input double SL_Pips = 20.0;
int handleFastEMA, handleSlowEMA, handleATR;
int OnInit() {
handleFastEMA = iMA(_Symbol, _Period, FastEMALen, 0, MODE_EMA, PRICE_CLOSE);
handleSlowEMA = iMA(_Symbol, _Period, SlowEMALen, 0, MODE_EMA, PRICE_CLOSE);
handleATR = iATR(_Symbol, _Period, ATRLen);
return(INIT_SUCCEEDED);
}
void OnTick() {
MqlDateTime dt;
TimeCurrent(dt);
// 時間判定 (JST 21:00-23:00) -> サーバー時間への変換は適宜必要
if(!(dt.hour >= 21 && dt.hour < 23)) return;
double emaFast[], emaSlow[], atr[];
MqlRates rates[];
ArraySetAsSeries(emaFast, true);
ArraySetAsSeries(emaSlow, true);
ArraySetAsSeries(atr, true);
ArraySetAsSeries(rates, true);
if(CopyBuffer(handleFastEMA, 0, 0, 2, emaFast) < 2) return;
if(CopyBuffer(handleSlowEMA, 0, 0, 2, emaSlow) < 2) return;
if(CopyBuffer(handleATR, 0, 0, 1, atr) < 1) return;
if(CopyRates(_Symbol, _Period, 0, 2, rates) < 2) return;
if(atr[0] <= 0) return;
double closeCurr = rates[0].close;
double closePrev = rates[1].close;
double highPrev = rates[1].high;
double lowPrev = rates[1].low;
// BUY Signal
if(closeCurr > emaSlow[0]) {
if(closePrev <= emaFast[1] * 1.0005) {
if(closeCurr > emaFast[0] && closeCurr > highPrev) {
TradeOrder(ORDER_TYPE_BUY);
}
}
}
// SELL Signal
else if(closeCurr < emaSlow[0]) {
if(closePrev >= emaFast[1] * 0.9995) {
if(closeCurr < emaFast[0] && closeCurr < lowPrev) {
TradeOrder(ORDER_TYPE_SELL);
}
}
}
}
void TradeOrder(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);
}