またAIが「それっぽい」名前の戦略を書き出して、見事に相場に焼かれた。今回の被害者は AdaptiveResonanceStrategy。
名前からして意識が高い。「適応的共鳴戦略」か。聞くだけで量子力学か何かを使いそうな雰囲気だが、中身を解剖してみると、結局は「ローソク足の形状に統計的な化粧を施しただけの単純な順張り」だった。
導入:AIが描いた「高尚な」ロジック
このEAの意図をPythonコードから読み解くと、AIは以下のような「理論的アプローチ」を試みたようだ。
- 物理的圧力(Pressure)の定義:
(Open+Close)/2と(High+Low)/2の差分を取り、価格の重心がどちらに寄っているかを「圧力」と定義。 - 多時間軸(MTF)の同期: 1時間足で方向性を決め、15分足で同調を確認し、5分足でトリガーを引くという、教科書通りの階層的フィルター。
- 純度(Purity)によるノイズ除去: 実体(Body)がヒゲに対してどれだけ大きいかを計算し、それをZ-Scoreで正規化することで「統計的に有意なインパルス」だけを抽出。
- ボラティリティ・レジームの制御: 適度なボラティリティがあるときのみエントリーし、端に寄りすぎた価格(Over-extension)を排除。
一見すると、リスク管理とフィルタリングが徹底された緻密な設計に見える。だが、クオンツの視点から見れば、これは典型的な**「過剰適合への憧憬」と「統計学の誤用」**の塊だ。
破綻の解説:なぜPF 1.02という「死」に至ったか
結果は悲惨だ。PF 1.02。最大ドローダウン 0.0%(おそらく取引回数が少なすぎて、あるいは極端に保守的な設定で、資産曲線がほぼ横ばいだったのだろう)。
なぜこのロジックは通用しなかったのか。技術的な問題点を列挙する。
1. Z-Scoreへの盲信(正規分布の罠)
AIはこのコードの中で、あらゆる指標を calculate_zscore で正規化している。Z-Scoreはデータが正規分布に従っていることを前提とした指標だ。しかし、相場のリターンや価格変動(特に5分足のような短期足)は、ファットテイル(尖度が高い)な分布を持つ。
正規分布を前提に「0.8σを超えたら有意なインパルス」と判定しても、実際にはそれは単なる「ノイズの塊」に過ぎない。統計的な「有意性」を演出したが、相場における「意味」を抽出できていない。
2. 「圧力」という名の擬似指標
pressure の計算式を見てほしい。これは単にローソク足の中央値がどこにあるかを見ているだけだ。これを「物理的圧力」と呼ぶのは、単なる言葉遊びに過ぎない。結局のところ、これは「ヒゲが長いか短いか」を別の言い方で表現しているだけであり、新しいエッジ(優位性)を何一つ提供していない。
3. 低い期待値とスプレッドの壁
TP 20pips / SL 15pips という設定。PF 1.02 という数値は、手数料とスプレッドを考慮すれば、実質的にはマイナスである。 AIは「閾値を1.5から0.8へ緩和し、機会損失を削減した」とコメントしているが、これは典型的な**「負けトレードを増やして取引回数を稼ぐ」**という愚策だ。エッジのない戦略において、取引回数を増やすことは、破産への速度を上げることに等しい。
4. 静的な閾値の限界
0.6 < vol_ratio < 3.0 や 0.05 < close_pos < 0.95 といったハードコードされた数値。相場のボラティリティは動的に変化する。ある時期には適切だった数値が、直近2年間(レポートにあるつまずいた期間)には全く適合しなかったのだろう。
結論として、このEAは**「統計学的な用語を散りばめて、単純なインジケーターの組み合わせを高度に見せかけただけのハリボテ」**であったと言える。
失敗したPythonコード
AIが自信満々に書き出した、その「ゴミ」をここに晒しておく。
import pandas as pd
import numpy as np
from strategies.base import BaseStrategy
class AdaptiveResonanceStrategy(BaseStrategy):
"""
Adaptive Resonance Strategy:
統計的な圧力共振(Pressure Resonance)をベースにしつつ、
閾値を適応的に緩和させることで、取引回数と期待値を両立させた戦略。
"""
def __init__(self):
# 親クラスの初期化
super().__init__(name="AdaptiveResonance", default_tp_pips=20.0, default_sl_pips=15.0)
# 執行時間足
self.base_timeframe = "5m"
# AI視認用時間足
self.vision_timeframes = ["5m", "15m", "1h"]
def calculate_indicators(self, df):
"""
価格の物理的圧力と純度を計算し、統計的に正規化する。
"""
# --- 1. 物理的圧力 (Pressure) ---
df['pressure'] = ((df['Open'] + df['Close']) / 2) - ((df['High'] + df['Low']) / 2)
# --- 2. 統計的正規化 (Z-Score) ---
def calculate_zscore(series, window):
return (series - series.rolling(window=window).mean()) / (series.rolling(window=window).std() + 1e-9)
# MTF圧力のZ-Score化
df['z_p5'] = calculate_zscore(df['pressure'], 20)
df['z_p15'] = calculate_zscore(df['pressure'].rolling(window=3).mean(), 20)
df['z_p60'] = calculate_zscore(df['pressure'].rolling(window=12).mean(), 20)
# --- 3. 方向性の純度 (Purity) ---
df['body_size'] = (df['Close'] - df['Open']).abs()
df['total_range'] = df['High'] - df['Low']
df['purity'] = df['body_size'] / (df['total_range'] + 1e-9)
# 純度のZ-Score
df['z_purity'] = calculate_zscore(df['purity'], 20)
# --- 4. ボラティリティ・レジーム ---
df['vol_ma'] = df['total_range'].rolling(window=20).mean()
df['vol_ratio'] = df['total_range'] / (df['vol_ma'] + 1e-9)
# --- 5. 価格確定位置 ---
df['close_pos'] = (df['Close'] - df['Low']) / (df['total_range'] + 1e-9)
return df
def generate_signal(self, df):
"""
上位足の方向性に沿い, 下位足で統計的に有意なインパルスが発生した時にエントリー。
"""
if len(df) < 40:
return None
last = df.iloc[-1]
# --- エントリー条件の構築 ---
# 1. Hierarchical Bias (階層的トレンド方向)
# 1時間足が方向性を決め(Z > 0)、15分足がそれに同調していること
bullish_bias = (last['z_p60'] > 0) and (last['z_p15'] > -0.5)
bearish_bias = (last['z_p60'] < 0) and (last['z_p15'] < 0.5)
# 2. Adaptive Impulse (適応的インパルス)
# 純度のZ-Scoreが一定水準を超え、かつ物理的な圧力が方向性と一致していること
# 閾値を1.5から0.8へ緩和し、機会損失を削減
bullish_impulse = (last['z_purity'] > 0.8) and (last['z_p5'] > 0.5) and (last['Close'] > last['Open'])
bearish_impulse = (last['z_purity'] > 0.8) and (last['z_p5'] < -0.5) and (last['Close'] < last['Open'])
# 3. Volatility Filter (ボラティリティの適正化)
# 低すぎず、高すぎないボラティリティ圏内であること
regime_ok = 0.6 < last['vol_ratio'] < 3.0
# 4. Over-extension Guard (緩やかなオーバーシュート排除)
# 終値がレンジの極端な端(5%以下または95%以上)にないこと
# 強いトレンド時は許容するように範囲を拡大
not_extreme = 0.05 < last['close_pos'] < 0.95
# --- 最終判定 ---
if bullish_bias and bullish_impulse and regime_ok and not_extreme:
return 'BUY'
if bearish_bias and bearish_impulse and regime_ok and not_extreme:
return 'SELL'
return None
オマケ:反面教師としてのMQL5実装
「理論だけは立派なロジックを実際に動かして、どう絶望するかを体験したい」という物好きな方のために、上記のロジックをMQL5に移植した。
MT5のバックテストで、スプレッドを現実的な数値(例えば1.5〜3.0 pips)に設定して走らせてみてほしい。PF 1.02という数字が、いかに「奇跡的な上振れ」だったかに気づくはずだ。
//+------------------------------------------------------------------+
//| AdaptiveResonance_Fail.mq5 |
//| Copyright 2026, Cynical Quant |
//| https://example.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Cynical Quant"
#property link "https://example.com"
#property version "1.00"
#property strict
input double InpTP = 200; // Take Profit (points)
input double InpSL = 150; // Stop Loss (points)
input int InpWindow = 20; // Z-Score Window
// Z-Score計算用関数
double CalculateZScore(const double &data[], int period, int shift) {
if(ArraySize(data) < period + shift) return 0;
double sum = 0;
for(int i = 0; i < period; i++) sum += data[shift + i];
double mean = sum / period;
double sq_sum = 0;
for(int i = 0; i < period; i++) sq_sum += MathPow(data[shift + i] - mean, 2);
double std_dev = MathSqrt(sq_sum / period);
return (std_dev == 0) ? 0 : (data[shift] - mean) / std_dev;
}
void OnTick() {
MqlRates rates5[], rates15[], rates60[];
ArraySetAsSeries(rates5, true);
ArraySetAsSeries(rates15, true);
ArraySetAsSeries(rates60, true);
if(CopyRates(_Symbol, PERIOD_M5, 0, 100, rates5) < 100) return;
if(CopyRates(_Symbol, PERIOD_M15, 0, 100, rates15) < 100) return;
if(CopyRates(_Symbol, PERIOD_H1, 0, 100, rates60) < 100) return;
double pressure5[], purity5[], range5[];
ArrayResize(pressure5, 100);
ArrayResize(purity5, 100);
ArrayResize(range5, 100);
for(int i = 0; i < 100; i++) {
pressure5[i] = ((rates5[i].open + rates5[i].close) / 2.0) - ((rates5[i].high + rates5[i].low) / 2.0);
range5[i] = rates5[i].high - rates5[i].low;
purity5[i] = (range5[i] == 0) ? 0 : MathAbs(rates5[i].close - rates5[i].open) / range5[i];
}
double z_p5 = CalculateZScore(pressure5, InpWindow, 0);
double z_purity = CalculateZScore(purity5, InpWindow, 0);
// 簡略化のためMTF圧力は現在の足ベースで近似計算
double pressure15 = ((rates15[0].open + rates15[0].close) / 2.0) - ((rates15[0].high + rates15[0].low) / 2.0);
double pressure60 = ((rates60[0].open + rates60[0].close) / 2.0) - ((rates60[0].high + rates60[0].low) / 2.0);
// 擬似的なMTF Z-Score (実装簡略化のため単一値比較)
bool bullish_bias = (pressure60 > 0) && (pressure15 > -0.0001);
bool bearish_bias = (pressure60 < 0) && (pressure15 < 0.0001);
bool bullish_impulse = (z_purity > 0.8) && (z_p5 > 0.5) && (rates5[0].close > rates5[0].open);
bool bearish_impulse = (z_purity > 0.8) && (z_p5 < -0.5) && (rates5[0].close < rates5[0].open);
double vol_ma = 0;
for(int i=0; i<InpWindow; i++) vol_ma += range5[i];
vol_ma /= InpWindow;
double vol_ratio = (vol_ma == 0) ? 0 : range5[0] / vol_ma;
bool regime_ok = (vol_ratio > 0.6 && vol_ratio < 3.0);
double close_pos = (range5[0] == 0) ? 0 : (rates5[0].close - rates5[0].low) / range5[0];
bool not_extreme = (close_pos > 0.05 && close_pos < 0.95);
if(bullish_bias && bullish_impulse && regime_ok && not_extreme) {
// BUY Order Logic here
} else if(bearish_bias && bearish_impulse && regime_ok && not_extreme) {
// SELL Order Logic here
}
}