またAIが「完璧な聖杯」を夢見て、壮大なゴミを生成してくれた。
今回の被験体は、名付けて**「Symmetry-Based Vacuum Expansion (SVE)」**。 名前だけ見れば、ヘッジファンドのクオンツが深夜のホワイトボードで書き殴ったような、いかにも「凄そうな」雰囲気を出している。
ロジックの意図をPythonコードから読み解くと、AIは「相場のエネルギー蓄積(Coiling)からの爆発的解放(Vacuum Expansion)」を捉えようとしたらしい。具体的には、ローソク足の「実体比率(Symmetry)」で方向性を確認し、「価格変化率のZスコア(Velocity Z-Score)」で統計的な異常値を検知し、さらに「中央値からの乖離(Tension)」で高値掴みを回避するという、三段構えのフィルターを構築している。
なるほど、理屈は立っている。だが、相場は数式通りに動くほど親切ではない。
惨敗の分析:なぜこのロジックは「ゴミ」なのか
バックテストの結果を見てみよう。 プロフィットファクター (PF): 0.92。
結論から言えば、手数料とスプレッドを考慮すれば、このEAを動かすことは「電子的な寄付」に等しい。最大ドローダウンが0.1%という数値だけを見れば「安全」に見えるかもしれないが、これは単にリスクを取りすぎていない(あるいは取引機会を絞りすぎている)だけであり、期待値が1を割っている以上、運用すればするほど口座残高は緩やかに、しかし確実に死に至る。
技術的な破綻理由は主に3点だ。
1. 「Zスコア > 2.0」という遅効性の罠
AIはZスコアを用いて「統計的な異常値(Vacuum)」をトリガーにした。しかし、Zスコアが2.0を超えるということは、すでに価格が急激に動いた後であることを意味する。 つまり、このEAは「爆発の瞬間」ではなく、「爆発しきった後の末端」でエントリーしている。典型的な「追っかけ買い・売り」だ。
2. 「対称性(Symmetry)」という名のノイズ
実体とヒゲの比率で方向性を判定しているが、5分足レベルでのこの数値は極めてノイズが多い。強いトレンドが出ている時こそヒゲが出ることも多く、このフィルターがあるせいで、真のトレンド発生時にエントリーを拒否し、レンジ内の小さなボラティリティに反応するという本末転倒な挙動を繰り返していたと推測される。
3. 絶望的なTP/SL比
TP 20pips / SL 15pips。 統計的アノマリーを狙う戦略でありながら、利確幅が極めて狭い。Zスコアで捉えた「爆発」に乗るなら、本来はトレンドを伸ばすべきだが、このEAは小刻みに利確しようとした。結果として、たまに発生する大きな逆行(ダマシ)で利益をすべて吐き出し、PF 0.92という絶望的な数字に落ち着いたわけだ。
要するに、「凝った指標を組み合わせれば勝てる」というAI特有の過剰エンジニアリングの典型例である。
失敗したPythonコード
AIが自信満々に書き出した、美しくも役に立たないコードがこちらだ。
import pandas as pd
import numpy as np
from strategies.base import BaseStrategy
class SVEStrategy(BaseStrategy):
"""
Symmetry-Based Vacuum Expansion (SVE) Strategy
This strategy identifies high-probability impulse moves by detecting
the transition from a 'Coiling' state to a 'Vacuum Expansion' state,
synchronized across multiple timeframes using Body-to-Range Symmetry
and Velocity Z-Scores.
"""
def __init__(self):
# Optimized TP/SL to capture the initial impulse and exit before mean reversion
super().__init__(name="SVE_Vacuum_Expansion_Quant", default_tp_pips=20.0, default_sl_pips=15.0)
self.base_timeframe = "5m"
self.vision_timeframes = ["5m", "15m", "1h"]
# Window settings
self.macro_window = 12 # 1h equivalent
self.meso_window = 3 # 15m equivalent
self.stat_window = 20 # Window for Z-score and volatility
self.tension_limit = 0.5 # Maximum allowed tension to prevent chasing
def calculate_indicators(self, df):
"""
Compute Symmetry Conviction, Velocity Z-Score, and Macro Tension.
"""
# --- 1. Symmetry Conviction (Body-to-Range Ratio) ---
df['range'] = df['High'] - df['Low']
df['body'] = df['Close'] - df['Open']
df['symmetry'] = df['body'] / (df['range'] + 1e-9)
# MTF Symmetry: Average conviction over meso and macro windows
df['symmetry_meso'] = df['symmetry'].rolling(window=self.meso_window).mean()
df['symmetry_macro'] = df['symmetry'].rolling(window=self.macro_window).mean()
# --- 2. Velocity Z-Score (The Vacuum Trigger) ---
# Calculate the raw velocity (rate of change)
df['velocity'] = df['Close'].diff(1)
df['vel_mean'] = df['velocity'].rolling(window=self.stat_window).mean()
df['vel_std'] = df['velocity'].rolling(window=self.stat_window).std()
df['vel_zscore'] = (df['velocity'] - df['vel_mean']) / (df['vel_std'] + 1e-9)
# --- 3. Macro Tension & Equilibrium ---
# Use median of midpoints for a robust equilibrium
df['midpoint'] = (df['High'] + df['Low']) / 2
df['macro_median'] = df['midpoint'].rolling(window=self.macro_window).median()
# Macro Range for tension normalization
df['macro_max'] = df['High'].rolling(window=self.macro_window).max()
df['macro_min'] = df['Low'].rolling(window=self.macro_window).min()
df['macro_range'] = df['macro_max'] - df['macro_min']
# Tension: Distance from equilibrium relative to macro range
df['tension'] = (df['Close'] - df['macro_median']).abs() / (df['macro_range'] + 1e-9)
# --- 4. Macro Directional Vector ---
df['macro_vector'] = df['Close'].diff(self.macro_window)
return df
def generate_signal(self, df):
"""
Generate signals based on the synchronicity of Tension, Symmetry, and Vacuum Velocity.
"""
if len(df) < self.macro_window + 5:
return None
last = df.iloc[-1]
prev = df.iloc[-2]
# --- Entry Conditions: The Triple Convergence ---
# 1. Tension Filter: Only enter when price is relatively close to equilibrium
# This prevents "buying the top" or "selling the bottom"
is_low_tension = last['tension'] < self.tension_limit
# 2. Symmetry Synchronization: All timeframes must show conviction in the same direction
# Bullish Symmetry: Current, Meso, and Macro are all positively biased
symmetry_bullish = (
last['symmetry'] > 0.3 and
last['symmetry_meso'] > 0.1 and
last['symmetry_macro'] > 0.05
)
# Bearish Symmetry: Current, Meso, and Macro are all negatively biased
symmetry_bearish = (
last['symmetry'] < -0.3 and
last['symmetry_meso'] < -0.1 and
last['symmetry_macro'] < -0.05
)
# 3. Vacuum Trigger: Velocity Z-score must spike (Statistical anomaly)
# We look for a significant jump in velocity relative to the recent past
vacuum_bullish = last['vel_zscore'] > 2.0
vacuum_bearish = last['vel_zscore'] < -2.0
# 4. Macro Alignment: Trade in the direction of the 1h vector
macro_bullish = last['macro_vector'] > 0
macro_bearish = last['macro_vector'] < 0
# --- Final Execution Logic ---
# BUY: Macro UP + Low Tension + Symmetry BULLISH + Vacuum BULLISH
if macro_bullish and is_low_tension and symmetry_bullish and vacuum_bullish:
# Final confirmation: Candle must be bullish and close near the high
if last['Close'] > last['Open']:
return 'BUY'
# SELL: Macro DOWN + Low Tension + Symmetry BEARISH + Vacuum BEARISH
if macro_bearish and is_low_tension and symmetry_bearish and vacuum_bearish:
# Final confirmation: Candle must be bearish and close near the low
if last['Close'] < last['Open']:
return 'SELL'
return None
オマケ:反面教師としてのMQL5変換
「こんなゴミのようなロジックを、あえてMT5で動かして絶望を味わいたい」という物好きな方のために、MQL5に移植しておいた。 統計的な計算(Zスコアや中央値)をMQL5で実装するのは少々手間だが、その手間こそが「この戦略に価値がないこと」を再認識させる良い修行になるだろう。
//+------------------------------------------------------------------+
//| SVE_Failure_Demo.mq5|
//| Copyright 2026, Cynical Quant |
//| https://your-blog.com|
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Cynical Quant"
#property link "https://your-blog.com"
#property version "1.00"
#property strict
// Parameters
input double InpTP = 200; // Take Profit (points)
input double InpSL = 150; // Stop Loss (points)
input int InpMacroWindow = 12; // Macro Window
input int InpMesoWindow = 3; // Meso Window
input int InpStatWindow = 20; // Stat Window for Z-Score
input double InpTensionLimit = 0.5; // Max Tension
// Global variables
int handle_symbol;
//+------------------------------------------------------------------+
//| Expert initialization function |
//+------------------------------------------------------------------+
int OnInit() {
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Expert tick function |
//+------------------------------------------------------------------+
void OnTick() {
if(Bars(_Symbol, _Period) < InpMacroWindow + 5) return;
MqlRates rates[];
ArraySetAsSeries(rates, true);
if(CopyRates(_Symbol, _Period, 0, InpMacroWindow + InpStatWindow, rates) < 0) return;
// 1. Symmetry Calculation
double symmetry = 0;
double range = rates[0].high - rates[0].low;
if(range > 0) symmetry = (rates[0].close - rates[0].open) / range;
double sum_meso = 0;
for(int i=0; i<InpMesoWindow; i++) {
double r = rates[i].high - rates[i].low;
sum_meso += (r > 0) ? (rates[i].close - rates[i].open) / r : 0;
}
double symmetry_meso = sum_meso / InpMesoWindow;
double sum_macro = 0;
for(int i=0; i<InpMacroWindow; i++) {
double r = rates[i].high - rates[i].low;
sum_macro += (r > 0) ? (rates[i].close - rates[i].open) / r : 0;
}
double symmetry_macro = sum_macro / InpMacroWindow;
// 2. Velocity Z-Score
double velocities[];
ArrayResize(velocities, InpStatWindow);
double sum_vel = 0;
for(int i=0; i<InpStatWindow; i++) {
velocities[i] = rates[i].close - rates[i+1].close;
sum_vel += velocities[i];
}
double mean_vel = sum_vel / InpStatWindow;
double sum_sq_diff = 0;
for(int i=0; i<InpStatWindow; i++) {
sum_sq_diff += MathPow(velocities[i] - mean_vel, 2);
}
double std_vel = MathSqrt(sum_sq_diff / InpStatWindow);
double vel_zscore = (velocities[0] - mean_vel) / (std_vel + 1e-9);
// 3. Macro Tension
double midpoints[];
ArrayResize(midpoints, InpMacroWindow);
for(int i=0; i<InpMacroWindow; i++) midpoints[i] = (rates[i].high + rates[i].low) / 2.0;
ArraySort(midpoints);
double macro_median = midpoints[InpMacroWindow / 2];
double macro_max = rates[0].high;
double macro_min = rates[0].low;
for(int i=1; i<InpMacroWindow; i++) {
if(rates[i].high > macro_max) macro_max = rates[i].high;
if(rates[i].low < macro_min) macro_min = rates[i].low;
}
double macro_range = macro_max - macro_min;
double tension = MathAbs(rates[0].close - macro_median) / (macro_range + 1e-9);
// 4. Macro Vector
double macro_vector = rates[0].close - rates[InpMacroWindow].close;
// Execution Logic
bool is_low_tension = tension < InpTensionLimit;
bool symmetry_bullish = (symmetry > 0.3 && symmetry_meso > 0.1 && symmetry_macro > 0.05);
bool symmetry_bearish = (symmetry < -0.3 && symmetry_meso < -0.1 && symmetry_macro < -0.05);
bool vacuum_bullish = (vel_zscore > 2.0);
bool vacuum_bearish = (vel_zscore < -2.0);
bool macro_bullish = (macro_vector > 0);
bool macro_bearish = (macro_vector < 0);
if(macro_bullish && is_low_tension && symmetry_bullish && vacuum_bullish && rates[0].close > rates[0].open) {
TradeBuy();
} else if(macro_bearish && is_low_tension && symmetry_bearish && vacuum_bearish && rates[0].close < rates[0].open) {
TradeSell();
}
}
void TradeBuy() {
MqlTradeRequest request={0};
MqlTradeResult result={0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_BUY;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
request.sl = request.price - InpSL * _Point;
request.tp = request.price + InpTP * _Point;
OrderSend(request, result);
}
void TradeSell() {
MqlTradeRequest request={0};
MqlTradeResult result={0};
request.action = TRADE_ACTION_DEAL;
request.symbol = _Symbol;
request.volume = 0.1;
request.type = ORDER_TYPE_SELL;
request.price = SymbolInfoDouble(_Symbol, SYMBOL_BID);
request.sl = request.price + InpSL * _Point;
request.tp = request.price - InpTP * _Point;
OrderSend(request, result);
}