導入:AIが夢見た「完璧なブレイクアウト」
今回のボツEAは、AIが「ボラティリティの凝縮から拡散への転換点」を捉えようとして設計した、いわゆるボラティリティ・スクイーズ戦略です。
Pythonコードから読み取れるAIの意図は以下の通りです。
- 長期的な方向性の確認: 線形回帰の傾き(Slope)と効率比(Efficiency Ratio)を用いて、「ノイズではない明確なトレンド」があることを確認する。
- エネルギーの蓄積を検知: 短期ボラティリティと長期ボラティリティの比率を算出し、相場が「スクイーズ(凝縮)」状態にあることを定義する。
- 構造的なブレイクアウト: スクイーズ期間中の高値・安値を更新し、かつ「強すぎず弱すぎない」適度なモメンタム(Impulse Intensity)を伴って抜けた瞬間にエントリーする。
理論だけを見れば、非常に「クオンツらしく」知的なアプローチに見えます。しかし、結果は無残なものでした。
破綻の解説:知的な装いをした「ただのゴミ」
このEAが敗北した理由は、単なるコードのバグだけではありません。行動経済学的な観点から見れば、これは**「コントロールの錯覚(Illusion of Control)」**に陥った典型的な事例です。
1. 致命的な実装ミス(技術的破綻)
まず、このEAはバックテストにすら到達していません。
Execution Error: 'numpy.float64' object has no attribute 'shift'
という、あまりにも初歩的なエラーで自爆しています。generate_signal関数内で、単一の行(Series)であるlast['squeeze_high']に対して.shift(1)を呼び出そうとしています。.shift()は pandas の Series や DataFrame のメソッドであり、数値(float64)に対しては使えません。
AIは「過去のデータを参照する」という概念をコードに盛り込みましたが、「今、自分が扱っている変数が『配列』なのか『単一の値』なのか」という型定義の意識が完全に欠落していたのです。
2. 過剰適合(Overfitting)の罠
仮にバグを修正したとしても、このロジックが現実の相場で通用する可能性は極めて低いです。
vol_ratio < 0.85er_l > 0.250.7 < impulse_rel < 3.0pds > 0.6
これらの数値(しきい値)はどこから来たのでしょうか?根拠のない「魔法の数字」を並べてフィルターを重ねる行為は、過去データに無理やり合わせた**カーブフィッティング(過剰適合)**そのものです。フィルターを増やせば増やすほど、バックテストの数字は綺麗になりますが、未知の相場(アウトオブサンプル)では全く機能しません。
3. 「ダマシ」への無知
ボラティリティ・スクイーズ後のブレイクアウトは、機関投資家にとって絶好の「流動性狩り(ストップ狩り)」の標的です。AIは「適度なインパルス(Impulse)」でダマシを回避できると考えていたようですが、現実の市場では、最も「正しそうに見えるブレイク」こそが、逆方向に突き抜ける罠になることが多々あります。
このEAは、数学的な美しさに酔いしれ、相場の本質である**「不確実性と騙し合い」**という心理的側面を完全に無視した、机上の空論に過ぎなかったと言えるでしょう。
Pythonコードの公開(失敗作)
import pandas as pd
import numpy as np
from strategies.base import BaseStrategy
class VolatilitySqueezeBreakout(BaseStrategy):
"""
Quants Strategy: Volatility Squeeze & Structural Breakout.
Focuses on the transition from extreme volatility compression
to a structural breakout of the squeeze range.
"""
def __init__(self):
# Optimized for high PF and consistent trade frequency
super().__init__(name="VolSqueezeBreakout", default_tp_pips=20.0, default_sl_pips=15.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
# Mathematical Windows
self.window_l = 24 # L-Term: Structural Bias (2h equivalent)
self.window_m = 6 # M-Term: Squeeze Detection
self.vol_short = 5 # Short-term volatility for ratio
self.vol_long = 20 # Long-term volatility for baseline
def calculate_indicators(self, df):
"""
Computes structural vector, volatility ratios, and squeeze ranges.
"""
df = df.copy()
# 1. L-Term: Structural Vector (Linear Regression Slope)
def get_slope(series, window):
def calc_slope(y):
x = np.arange(len(y))
return np.polyfit(x, y, 1)[0]
return series.rolling(window=window).apply(calc_slope, raw=True)
df['slope_l'] = get_slope(df['Close'], self.window_l)
# L-Term Efficiency Ratio (ER) to ensure a clean trend
def get_er(series, window):
net = (series.diff(window)).abs()
vol = series.diff().abs().rolling(window=window).sum()
return net / vol.replace(0, np.nan)
df['er_l'] = get_er(df['Close'], self.window_l)
# 2. M-Term: Volatility Ratio & Squeeze
# Using standard deviation of returns as a proxy for volatility
df['returns'] = df['Close'].pct_change()
df['vol_s'] = df['returns'].rolling(window=self.vol_short).std()
df['vol_l'] = df['returns'].rolling(window=self.vol_long).std()
# Volatility Ratio: Short-term Vol / Long-term Vol
df['vol_ratio'] = df['vol_s'] / df['vol_l'].replace(0, np.nan)
# Identify the 'Squeeze' state (Ratio is low)
df['is_squeezed'] = df['vol_ratio'] < 0.85
# 3. S-Term: Structural Range & Breakout
# Track the high/low during the squeeze period
# We use a rolling window to find the boundary of the most recent squeeze
df['squeeze_high'] = df['High'].rolling(window=self.window_m).max()
df['squeeze_low'] = df['Low'].rolling(window=self.window_m).min()
# Impulse Intensity: Body relative to volatility
df['tr'] = df['High'] - df['Low']
df['vol_ma'] = df['tr'].rolling(window=self.vol_long).mean()
df['body'] = (df['Close'] - df['Open']).abs()
df['impulse_rel'] = df['body'] / df['vol_ma'].replace(0, np.nan)
# Closing position (PDS)
df['pds'] = (df['Close'] - df['Low']) / df['tr'].replace(0, np.nan)
return df
def generate_signal(self, df):
"""
Triggers a trade when a structural vector is aligned and
price breaks out of a volatility squeeze with moderate intensity.
"""
if len(df) < self.window_l + self.vol_long:
return None
last = df.iloc[-1]
prev = df.iloc[-2]
# --- 1. L-Term: Structural Alignment ---
# Ensure the market is in an efficient trend (not noise)
is_ordered = last['er_l'] > 0.25
bull_bias = (last['slope_l'] > 0) and is_ordered
bear_bias = (last['slope_l'] < 0) and is_ordered
# --- 2. M-Term: Squeeze-to-Release Transition ---
# Previous state was squeezed, current state is expanding
is_releasing = prev['is_squeezed'] and (last['vol_ratio'] > 1.0)
# --- 3. S-Term: Structural Breakout ---
# Price must close outside the range of the recent squeeze
# and avoid 'Climax' candles (Impulse < 3.0) to prevent buying the top
is_moderate_impulse = 0.7 < last['impulse_rel'] < 3.0
bull_breakout = (last['Close'] > last['squeeze_high'].shift(1)) and (last['pds'] > 0.6)
bear_breakout = (last['Close'] < last['squeeze_low'].shift(1)) and (last['pds'] < 0.4)
# --- Final Synchronization ---
# BUY: Bull Bias + Vol Release + Structural Breakout + Moderate Impulse
if (bull_bias and
is_releasing and
bull_breakout and
is_moderate_impulse):
return 'BUY'
# SELL: Bear Bias + Vol Release + Structural Breakout + Moderate Impulse
if (bear_bias and
is_releasing and
bear_breakout and
is_moderate_impulse):
return 'SELL'
return None
オマケ(MQL5変換)
「こんな過剰適合まみれのロジックを、あえてMT5で動かして絶望してみたい」という物好きな方向けに、ロジックをMQL5に移植しました。反面教師としてご利用ください。
//+------------------------------------------------------------------+
//| VolSqueezeBreakout_Failure.mq5|
//| Copyright 2026, Behavioral Econ |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Behavioral Econ"
#property link "https://www.mql5.com"
#property version "1.00"
#property strict
// Parameters
input int window_l = 24; // Structural Bias Window
input int window_m = 6; // Squeeze Detection Window
input int vol_short = 5; // Short-term Volatility
input int vol_long = 20; // Long-term Volatility
input double tp_pips = 20.0;
input double sl_pips = 15.0;
// Global Variables
int handle_std_short, handle_std_long;
int OnInit() {
return(INIT_SUCCEEDED);
}
// Simple Linear Regression Slope
double GetSlope(int period, int shift) {
double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
for(int i = shift; i < shift + period; i++) {
double y = iClose(_Symbol, _Period, i);
sumX += i;
sumY += y;
sumXY += i * y;
sumX2 += i * i;
}
return (period * sumXY - sumX * sumY) / (period * sumX2 - sumX * sumX);
}
// Efficiency Ratio
double GetER(int period, int shift) {
double net = MathAbs(iClose(_Symbol, _Period, shift) - iClose(_Symbol, _Period, shift + period));
double vol = 0;
for(int i = shift; i < shift + period; i++) {
vol += MathAbs(iClose(_Symbol, _Period, i) - iClose(_Symbol, _Period, i + 1));
}
return (vol == 0) ? 0 : net / vol;
}
void OnTick() {
if(Bars(_Symbol, _Period) < window_l + vol_long) return;
// 1. L-Term: Structural Alignment
double slope = GetSlope(window_l, 1);
double er = GetER(window_l, 1);
bool bull_bias = (slope < 0) && (er > 0.25); // MQL5 index is reverse
bool bear_bias = (slope > 0) && (er > 0.25);
// 2. M-Term: Volatility Squeeze
double std_s = iStdDev(_Symbol, _Period, vol_short, 0, MODE_SMA, PRICE_CLOSE, 1);
double std_l = iStdDev(_Symbol, _Period, vol_long, 0, MODE_SMA, PRICE_CLOSE, 1);
double vol_ratio = (std_l == 0) ? 0 : std_s / std_l;
double prev_std_s = iStdDev(_Symbol, _Period, vol_short, 0, MODE_SMA, PRICE_CLOSE, 2);
double prev_std_l = iStdDev(_Symbol, _Period, vol_long, 0, MODE_SMA, PRICE_CLOSE, 2);
double prev_vol_ratio = (prev_std_l == 0) ? 0 : prev_std_s / prev_std_l;
bool is_releasing = (prev_vol_ratio < 0.85) && (vol_ratio > 1.0);
// 3. S-Term: Breakout
double squeeze_high = iHigh(_Symbol, _Period, iHighest(_Symbol, _Period, MODE_HIGH, window_m, 2));
double squeeze_low = iLow(_Symbol, _Period, iLowest(_Symbol, _Period, MODE_LOW, window_m, 2));
double close = iClose(_Symbol, _Period, 1);
double high = iHigh(_Symbol, _Period, 1);
double low = iLow(_Symbol, _Period, 1);
double open = iOpen(_Symbol, _Period, 1);
double tr = high - low;
double body = MathAbs(close - open);
// Simplified Vol MA for impulse
double vol_ma = 0;
for(int i=1; i<=vol_long; i++) vol_ma += (iHigh(_Symbol, _Period, i) - iLow(_Symbol, _Period, i));
vol_ma /= vol_long;
double impulse_rel = (vol_ma == 0) ? 0 : body / vol_ma;
double pds = (tr == 0) ? 0 : (close - low) / tr;
bool is_moderate_impulse = (impulse_rel > 0.7 && impulse_rel < 3.0);
bool bull_breakout = (close > squeeze_high && pds > 0.6);
bool bear_breakout = (close < squeeze_low && pds < 0.4);
// Execution
if(bull_bias && is_releasing && bull_breakout && is_moderate_impulse) {
// OrderSend(BUY...);
}
if(bear_bias && is_releasing && bear_breakout && is_moderate_impulse) {
// OrderSend(SELL...);
}
}