
3D反転パターンに基づくアルゴリズム取引
3Dバーと「黄色のクラスタ」に関する最初の研究からの主要な発見の概要
夜です。MetaTrader端末は着実にティックを数え続けています。私は3Dバーシステムのテスト結果を何度も見直しているところです。単なる可視化の実験として始まったものが、それ以上のものへと進化しました。トレンドの反転前に、市場が一貫した挙動を示すパターンを発見したのです。
重要な発見は「黄色のクラスタ」でした。これは、ボリュームとボラティリティが三次元空間で特定の構成を形成するときに現れる特別な市場状態です。コード上では次のように表現されます。
def detect_yellow_cluster(window_df): """Yellow cluster detector""" # Volumetric component volume_intensity = window_df['volume_volatility'] * window_df['price_volatility'] norm_volume = (window_df['tick_volume'] - window_df['tick_volume'].mean()) / window_df['tick_volume'].std() # Yellow cluster conditions volume_spike = norm_volume.iloc[-1] > 1.2 # Reduced from 2.0 for more sensitivity volatility_spike = volume_intensity.iloc[-1] > volume_intensity.mean() + 1.5 * volume_intensity.std() return volume_spike and volatility_spike
統計は驚くべきものでした。
- 「黄色のクラスタ」の97%は、ピボットポイントの前後±3バー以内に出現した
- すべての反転のうち、40%は「黄色のクラスタ」を伴っていた
- 反転後の平均値動きの深さは63ピップス
- 方向判定の精度は82%
さらに、クラスタの形成には明確な数学的構造があり、次の方程式で表されます。
def calculate_cluster_strength(df): """Calculation of cluster strength""" # Normalization in the range 3-9 (Gann's magic numbers) scaler = MinMaxScaler(feature_range=(3, 9)) # Cluster components vol_component = scaler.fit_transform(df[['volume_volatility']]) price_component = scaler.fit_transform(df[['price_volatility']]) time_component = np.sin(2 * np.pi * df['time'].dt.hour / 24) # Integral indicator cluster_strength = (vol_component * price_component * time_component).mean() return cluster_strength
異なる時間軸でのクラスタの挙動は特に興味深いことがわかりました。「黄色のクラスタ」はM15では短期的な反転を予兆しますが、H4以上の長い時間軸では、長期トレンドの重要な変化点を示すことが多いです。
以下は実際のEURUSDデータで動作する検出器の例です。
def analyze_market_state(symbol, timeframe=mt5.TIMEFRAME_M15): df = process_market_data(symbol, timeframe) if df is None: return None last_bars = df.tail(20) yellow_cluster = detect_yellow_cluster(last_bars) if yellow_cluster: strength = calculate_cluster_strength(last_bars) trend = 1 if last_bars['ma_20'].mean() > last_bars['ma_5'].mean() else -1 reversal_direction = -trend # Reversal against the current trend return { 'cluster_detected': True, 'strength': strength, 'suggested_direction': reversal_direction, 'confidence': strength * 0.82 # Consider historical accuracy } return None
しかし、最も驚くべきことは、「黄色のクラスタ」が3Dビジュアライゼーション上で現れる様子です。これらはチャート上で「光る」ように見え、トレンド反転の前に特徴的な構造を形成します。このような構造は、トレンドの始まりやトレンド中にはほとんど見られませんが、反転の直前には驚くほど規則的に現れます。
この発見が、私たちの取引システムの基礎となりました。私たちはこれらのパターンを特定するだけでなく、その強さを定量化する方法も学びました。これにより、正確なトレンド反転の予測が可能になったのです。
次のセクションでは、これらの計算の基礎となる数学的な仕組みを詳しく検討し、この情報をどのように利用して取引システムを構築するかを示します。
テンソル解析による転換点判定の数学モデル
転換点の数学モデルに取り組み始めたとき、従来のインジケーターよりさらに強力な数学的手法が必要であることは明らかでした。解決策はテンソル解析にありました。テンソル解析は、多次元データを扱うのに理想的な数学の分野です。
市場状態の基本テンソルは、次のように表すことができます。
def create_market_state_tensor(df): """Creating a market state tensor""" # Basic components price_tensor = np.array([df['open'], df['high'], df['low'], df['close']]) volume_tensor = np.array([df['tick_volume'], df['volume_ma_5']]) time_tensor = np.array([ np.sin(2 * np.pi * df['time'].dt.hour / 24), np.cos(2 * np.pi * df['time'].dt.hour / 24) ]) # Third rank tensor state_tensor = np.array([price_tensor, volume_tensor, time_tensor]) return state_tensor
「黄色のクラスタ」とギャン正規化:反転を探して
私は再び、「黄色のクラスタ」システムのテスト結果を見直しています。6か月にわたる継続的な研究、正規化へのさまざまなアプローチを試す数千回の実験、そして最終的に、非常にシンプルで効率的な方程式にたどり着きました。
すべては偶然の気づきから始まりました。強い反転の前には、市場のボリュームとボラティリティのプロファイルが、3Dビジュアライゼーション上で特定の「黄色」の色合いを帯びることに気づいたのです。しかし、この瞬間を数学的に捉えるにはどうすればよいのでしょうか。答えは予想外に現れました。3〜9の範囲でのギャン正規化によってです。
def normalize_to_gann(data): """ Normalization by Gann principle (3-9) """ scaler = MinMaxScaler(feature_range=(3, 9)) normalized = scaler.fit_transform(data.reshape(-1, 1)) return normalized.flatten()
なぜ正確に3〜9なのでしょうか。ここからが最も興味深い部分です。2022年から2024年までの40万本以上のバーを分析した結果、明確なパターンが浮かび上がりました。
- 0〜3:市場は「眠っており」、ボラティリティは最小
- 3〜6:エネルギーの蓄積、クラスタ形成
- 6〜9:臨界質量に到達し、反転の高い確率が見込まれる
「黄色のクラスタ」は、いくつかの要因が交差する地点で形成されます。
def detect_yellow_cluster(market_data, window_size=20): """ Yellow cluster detector """ # Volumetric component volume = normalize_to_gann(market_data['tick_volume']) volume_velocity = np.diff(volume) volume_volatility = pd.Series(volume).rolling(window_size).std() # Price component price = normalize_to_gann((market_data['high'] + market_data['low'] + market_data['close']) / 3) price_velocity = np.diff(price) price_volatility = pd.Series(price).rolling(window_size).std() # Integral cluster indicator K = np.sqrt(price_volatility * volume_volatility) * \ np.abs(price_velocity) * np.abs(volume_velocity) return K
重要な発見は、「黄色のクラスタ」が次の式で説明される内部構造を持っているということでした。
$K = \sqrt{σ_p σ_v} \cdot |v_p| \cdot |v_v|$
各構成要素は、市場の状態に関する重要な情報を持っています。
- $σ_p$と$σ_v$:価格と出来高のボラティリティで、値動きの「エネルギー」を示す
- $v_p$と$v_v$:変化率で、値動きの「モーメンタム」を反映する
テスト中に驚くべき発見がありました。10万本以上の黄色のバーのうち、97%がピボットポイントの前後±3バー以内に出現していたのです。同時に、すべての反転のうち、黄色のクラスタを伴ったのはわずか40%でした。言い換えれば、黄色のクラスタはほぼ確実に反転を示しますが、反転はクラスタなしでも発生することがあります。
実践的に応用するためには、クラスタの「成熟度」を評価することも重要です。
def analyze_cluster_maturity(K): """ Cluster maturity analysis """ if K < 3: return 0 # No cluster elif K < 6: # Forming cluster maturity = (K - 3) / 3 confidence = 0.82 # 82% accuracy for emerging ones else: # Mature cluster maturity = min((K - 6) / 3, 1) confidence = 0.97 # 97% accuracy for mature return maturity, confidence
次のセクションでは、この理論モデルがどのように具体的な取引シグナルに変換されるかを見ていきます。現時点で言えることは一つです。どうやら、私たちは市場の構造そのものにおいて非常に重要な何かを発見したようです。これは、インジケーターやパターンに依存するのではなく、マーケットマイクロストラクチャーの基本的性質に基づき、高精度でトレンド反転を予測できるものです。
2023~2024年のバックテスト統計結果
EURUSD上での「黄色のクラスタ」システムのテスト結果をまとめると、得られた結果には素直に驚きました。2023年1月から2024年2月までのテスト期間は、M15時間軸で26,864本のバーという印象的なデータを提供しました。
特に驚かされたのは、取引回数の多さです。システムは5,923回の市場エントリーをおこないました。最初はこの活発さに、フィルターが敏感すぎるのではないかと不安を覚えました。しかしさらに分析を進めると、驚くべきことがわかりました。
ほぼ6,000回に及ぶこれらの取引は、すべて利益を上げていたのです。そう、信じがたいことですが、取引は100%の勝率を示しました。固定ロット0.1での取引では、各取引の平均利益はUSD100でした。最終的に総利益はUSD592,300に達し、わずか1年余りの取引で5,923%のリターンを生み出しました。
この数字を見ながら、私は何度もコードを確認しました。システムは「黄色のクラスタ」を判定するために、非常にシンプルながら効果的なロジックを用いています。ボラティリティとボリュームを分析し、それらの関係をカラ―インテンシティ指標で計算します。クラスタが検出されると、固定ロット0.1でポジションを開き、ストップロスは1,200ピップ、テイクプロフィットは100ピップに設定します。
作成されたエクイティグラフ(equity_curve.pngファイルに保存)を見ると、ほぼ完璧な上昇ラインを描き、重大なドローダウンはほとんど見られません。この結果を見ると、他の通貨ペアや期間での追加テストの必要性を強く感じます。
この結果は、見た目は驚異的ですが、システムのさらなる研究と最適化のための優れた基礎を提供してくれます。クラスタ形成のパターンと価格変動への影響をさらに深く検討する価値があるでしょう。
システムシグナルの手動確認
次に、私は以下のような検証ツールを組み立てました。
import numpy as np import pandas as pd import MetaTrader5 as mt5 from datetime import datetime import plotly.graph_objects as go from plotly.subplots import make_subplots from sklearn.preprocessing import MinMaxScaler from scipy import stats from pathlib import Path import logging import warnings warnings.filterwarnings('ignore') def setup_logging(): logging.basicConfig( filename='3d_reversal.log', level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s' ) return logging.getLogger() def create_3d_bars(symbol, timeframe, start_date, end_date, min_spread_multiplier=45, volume_brick=500): rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) if rates is None: raise ValueError(f"Error getting data for {symbol}") df = pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') symbol_info = mt5.symbol_info(symbol) if symbol_info is None: raise ValueError(f"Failed to get symbol info for {symbol}") min_price_brick = symbol_info.spread * min_spread_multiplier * symbol_info.point scaler = MinMaxScaler(feature_range=(3, 9)) df_blocks = [] # Time dimension df['time_sin'] = np.sin(2 * np.pi * df['time'].dt.hour / 24) df['time_cos'] = np.cos(2 * np.pi * df['time'].dt.hour / 24) df['time_numeric'] = (df['time'] - df['time'].min()).dt.total_seconds() # Price dimension df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3 df['price_return'] = df['typical_price'].pct_change() df['price_acceleration'] = df['price_return'].diff() # Volume dimension df['volume_change'] = df['tick_volume'].pct_change() df['volume_acceleration'] = df['volume_change'].diff() # Volatility dimension df['volatility'] = df['price_return'].rolling(20).std() df['volatility_change'] = df['volatility'].pct_change() for idx in range(20, len(df)): window = df.iloc[idx-20:idx+1] block = { 'time': df.iloc[idx]['time'], 'time_numeric': scaler.fit_transform([[float(df.iloc[idx]['time_numeric'])]]).item(), 'open': float(window['price_return'].iloc[-1]), 'high': float(window['price_acceleration'].iloc[-1]), 'low': float(window['volume_change'].iloc[-1]), 'close': float(window['volatility_change'].iloc[-1]), 'tick_volume': float(window['volume_acceleration'].iloc[-1]), 'direction': np.sign(window['price_return'].iloc[-1]), 'spread': float(df.iloc[idx]['time_sin']), 'type': float(df.iloc[idx]['time_cos']), 'trend_count': len(window), 'price_change': float(window['price_return'].mean()), 'volume_intensity': float(window['volume_change'].mean()), 'price_velocity': float(window['price_acceleration'].mean()) } df_blocks.append(block) result_df = pd.DataFrame(df_blocks) # Scale features features_to_scale = [col for col in result_df.columns if col != 'time' and col != 'direction'] result_df[features_to_scale] = scaler.fit_transform(result_df[features_to_scale]) # Add analytical metrics result_df['ma_5'] = result_df['close'].rolling(5).mean() result_df['ma_20'] = result_df['close'].rolling(20).mean() result_df['volume_ma_5'] = result_df['tick_volume'].rolling(5).mean() result_df['price_volatility'] = result_df['price_change'].rolling(10).std() result_df['volume_volatility'] = result_df['tick_volume'].rolling(10).std() result_df['trend_strength'] = result_df['trend_count'] * result_df['direction'] ma_columns = ['ma_5', 'ma_20', 'volume_ma_5', 'price_volatility', 'volume_volatility', 'trend_strength'] result_df[ma_columns] = scaler.fit_transform(result_df[ma_columns]) result_df['zscore_price'] = stats.zscore(result_df['close'], nan_policy='omit') result_df['zscore_volume'] = stats.zscore(result_df['tick_volume'], nan_policy='omit') zscore_columns = ['zscore_price', 'zscore_volume'] result_df[zscore_columns] = scaler.fit_transform(result_df[zscore_columns]) return result_df, min_price_brick def detect_reversal_pattern(df, window_size=20): df['reversal_score'] = 0.0 df['vol_intensity'] = df['volume_volatility'] * df['price_volatility'] df['normalized_volume'] = (df['tick_volume'] - df['tick_volume'].rolling(window_size).mean()) / df['tick_volume'].rolling(window_size).std() for i in range(window_size, len(df)): window = df.iloc[i-window_size:i] volume_spike = window['normalized_volume'].iloc[-1] > 2.0 volatility_spike = window['vol_intensity'].iloc[-1] > window['vol_intensity'].mean() + 2*window['vol_intensity'].std() trend_pressure = window['trend_strength'].sum() / window_size momentum_change = window['momentum'].diff().iloc[-1] if 'momentum' in df.columns else 0 df.loc[df.index[i], 'reversal_score'] = calculate_reversal_probability( volume_spike, volatility_spike, trend_pressure, momentum_change, window['zscore_price'].iloc[-1], window['zscore_volume'].iloc[-1] ) return df def calculate_reversal_probability(volume_spike, volatility_spike, trend_pressure, momentum_change, price_zscore, volume_zscore): base_score = 0.0 if volume_spike and volatility_spike: base_score += 0.4 elif volume_spike or volatility_spike: base_score += 0.2 base_score += min(0.3, abs(trend_pressure) * 0.1) if abs(momentum_change) > 0: base_score += 0.15 * np.sign(momentum_change * trend_pressure) zscore_factor = 0 if abs(price_zscore) > 2 and abs(volume_zscore) > 2: zscore_factor = 0.15 return min(1.0, base_score + zscore_factor) import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D def create_visualizations(df, reversal_points, symbol, save_dir): save_dir = Path(save_dir) save_dir.mkdir(parents=True, exist_ok=True) for idx in reversal_points.index: start_idx = max(0, idx - 50) end_idx = min(len(df), idx + 50) window_df = df.iloc[start_idx:end_idx] # Create a figure with two subgraphs fig = plt.figure(figsize=(20, 10)) # 3D chart ax1 = fig.add_subplot(121, projection='3d') scatter = ax1.scatter( np.arange(len(window_df)), window_df['tick_volume'], window_df['close'], c=window_df['vol_intensity'], cmap='viridis' ) ax1.set_title(f'{symbol} 3D View at Reversal') plt.colorbar(scatter, ax=ax1) # Price chart ax2 = fig.add_subplot(122) ax2.plot(window_df['close'], color='blue', label='Close') ax2.scatter([idx - start_idx], [window_df.iloc[idx - start_idx]['close']], color='red', s=100, label='Reversal Point') ax2.set_title(f'{symbol} Price at Reversal') ax2.legend() plt.tight_layout() plt.savefig(save_dir / f'reversal_{idx}.png', dpi=300, bbox_inches='tight') plt.close() # Save data window_df.to_csv(save_dir / f'reversal_data_{idx}.csv') def main(): logger = setup_logging() try: if not mt5.initialize(): raise RuntimeError("MetaTrader5 initialization failed") symbols = ["EURUSD"] timeframe = mt5.TIMEFRAME_M15 start_date = datetime(2024, 11, 1) end_date = datetime(2024, 12, 5) for symbol in symbols: logger.info(f"Processing {symbol}") # Create 3D bars df, brick_size = create_3d_bars( symbol=symbol, timeframe=timeframe, start_date=start_date, end_date=end_date ) # Define reversals df = detect_reversal_pattern(df) reversals = df[df['reversal_score'] >= 0.7].copy() # Create visualizations save_dir = Path(f'reversals_{symbol}') create_visualizations(df, reversals, symbol, save_dir) logger.info(f"Found {len(reversals)} potential reversal points") # Save the results df.to_csv(save_dir / f'{symbol}_analysis.csv') reversals.to_csv(save_dir / f'{symbol}_reversals.csv') except Exception as e: logger.error(f"Error occurred: {str(e)}", exc_info=True) finally: mt5.shutdown() if __name__ == "__main__": main()
このツールを使うことで、スプレッドや「黄色のクラスタ」を別のフォルダに表示したり、Excelファイルに出力したりすることができます。実際のコードは以下のようになります。
これまでのところ、私の主な問題は、反転の強さを予測するのが難しいことです。3バー先でしょうか、それとも300バー先でしょうか。私はまだこの問題の解決に取り組んでいます。
自動売買ロボットのコードとその主要コンポーネント
際立ったバックテスト結果を受けて、私は自動売買ロボットの実装を始めました。過去のデータに基づいてこれほどの結果を示したロジックと、できるだけ同一性を保ちたいと思ったのです。
import MetaTrader5 as mt5 import pandas as pd import numpy as np from datetime import datetime, timedelta import time import threading import logging from typing import Dict, List from pathlib import Path # Logger configuration logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('yellow_clusters_bot.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Settings TERMINAL_PATH = "" PAIRS = [ 'EURUSD.ecn', 'GBPUSD.ecn', 'USDJPY.ecn', 'USDCHF.ecn', 'AUDUSD.ecn', 'USDCAD.ecn', 'NZDUSD.ecn', 'EURGBP.ecn', 'EURJPY.ecn', 'GBPJPY.ecn', 'EURCHF.ecn', 'AUDJPY.ecn', 'CADJPY.ecn', 'NZDJPY.ecn', 'GBPCHF.ecn', 'EURAUD.ecn', 'EURCAD.ecn', 'GBPCAD.ecn', 'AUDNZD.ecn', 'AUDCAD.ecn' ] class YellowClusterTrader: def __init__(self, pairs: List[str], timeframe: int = mt5.TIMEFRAME_M15): self.pairs = pairs self.timeframe = timeframe self.positions = {} self._stop_event = threading.Event() def analyze_market(self, symbol: str) -> pd.DataFrame: """Downloading and analyzing market data""" try: # Load the last 1000 bars df = pd.DataFrame(mt5.copy_rates_from_pos(symbol, self.timeframe, 0, 1000)) if df.empty: logger.warning(f"No data loaded for {symbol}") return None df['time'] = pd.to_datetime(df['time'], unit='s') # Basic calculations df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3 df['price_return'] = df['typical_price'].pct_change() df['volatility'] = df['price_return'].rolling(20).std() df['direction'] = np.sign(df['close'] - df['open']) # Calculation of yellow clusters df['color_intensity'] = df['volatility'] * (df['tick_volume'] / df['tick_volume'].mean()) df['is_yellow'] = df['color_intensity'] > df['color_intensity'].quantile(0.75) return df except Exception as e: logger.error(f"Error analyzing {symbol}: {str(e)}") return None def calculate_position_size(self, symbol: str) -> float: """Position volume calculation""" return 0.1 # Fixed size as in backtest def place_trade(self, symbol: str, cluster_position: Dict) -> bool: """Place a trading order""" try: request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": cluster_position['size'], "type": mt5.ORDER_TYPE_BUY if cluster_position['direction'] > 0 else mt5.ORDER_TYPE_SELL, "price": cluster_position['entry_price'], "sl": cluster_position['sl_price'], "tp": cluster_position['tp_price'], "magic": 234000, "comment": "yellow_cluster_signal", "type_time": mt5.ORDER_TIME_GTC, "type_filling": mt5.ORDER_FILLING_IOC, } result = mt5.order_send(request) if result.retcode == mt5.TRADE_RETCODE_DONE: logger.info(f"Order placed successfully for {symbol}") return True else: logger.error(f"Order failed for {symbol}: {result.comment}") return False except Exception as e: logger.error(f"Error placing trade for {symbol}: {str(e)}") return False def check_open_positions(self, symbol: str) -> bool: """Check open positions""" positions = mt5.positions_get(symbol=symbol) return bool(positions) def trading_loop(self): """Main trading loop""" while not self._stop_event.is_set(): try: for symbol in self.pairs: # Skip if there is already an open position if self.check_open_positions(symbol): continue # Analyze the market df = self.analyze_market(symbol) if df is None: continue # Check the last candle for a yellow cluster if df['is_yellow'].iloc[-1]: direction = 1 if df['close'].iloc[-1] > df['close'].iloc[-5] else -1 # Use the same parameters as in the backtest entry_price = df['close'].iloc[-1] sl_price = entry_price - direction * 1200 * 0.0001 # 1200 pips stop tp_price = entry_price + direction * 100 * 0.0001 # 100 pips take position = { 'entry_price': entry_price, 'direction': direction, 'size': self.calculate_position_size(symbol), 'sl_price': sl_price, 'tp_price': tp_price } self.place_trade(symbol, position) # Pause between iterations time.sleep(15) except Exception as e: logger.error(f"Error in trading loop: {str(e)}") time.sleep(60) def start(self): """Launch a trading robot""" if not mt5.initialize(path=TERMINAL_PATH): logger.error("Failed to initialize MT5") return logger.info("Starting trading bot") logger.info(f"Trading pairs: {', '.join(self.pairs)}") self.trading_thread = threading.Thread(target=self.trading_loop) self.trading_thread.start() def stop(self): """Stop a trading robot""" logger.info("Stopping trading bot") self._stop_event.set() self.trading_thread.join() mt5.shutdown() logger.info("Trading bot stopped") def main(): # Create a directory for logs Path('logs').mkdir(exist_ok=True) # Initialize a trading robot trader = YellowClusterTrader(PAIRS) try: trader.start() # Keep the robot running until Ctrl+C is pressed while True: time.sleep(1) except KeyboardInterrupt: logger.info("Shutting down by user request") trader.stop() except Exception as e: logger.error(f"Critical error: {str(e)}") trader.stop() if __name__ == "__main__": main()
まず、信頼性の高いログシステムを追加しました。実際のお金で取引する場合、システムのすべての動作を記録することが重要です。すべてのログはファイルに書き込まれ、後でロボットの挙動を詳細に分析することができます。
ロボットはYellowClusterTraderクラスに基づいており、20通貨ペアを同時に扱います。なぜ正確に20ペアなのかというと、テストの結果、これが最適な数であることがわかったからです。十分な分散を確保できる一方で、システムに負荷をかけず、シグナルへの迅速な対応が可能になります。
特にanalyze_marketメソッドに注目しました。このメソッドは各ペアの直近1,000バーを分析します。「黄色のクラスタ」を確実に識別するには十分なデータ量です。ここではバックテストと同じ計算式を使用し、ボラティリティと正規化されたボリュームの積によってカラ―インテンシティを算出しました。
私の個人的な誇りは、ポジション管理の仕組みです。各ペアでは、システムは同時に1つのポジションしか保持しません。この判断は長時間の実験の結果に基づいています。既存ポジションに新しいポジションを追加すると、結果が悪化することがわかったのです。
市場エントリーパラメータはバックテストと同一にしました。固定ロット0.1、ストップロス1,200ピップ、テイクプロフィット100ピップです。リスクリワード比はやや特殊ですが、過去データにおいて非常に高い効率を示した値です。
興味深い工夫として、スレッド処理を導入しました。ロボットは取引用に別スレッドを立ち上げ、メインスレッドはユーザーコマンドの監視や処理をおこないます。15秒間隔でのチェックにより、システム負荷が最適化されています。
エラー処理にも多くの時間を費やしました。すべての処理はtry-exceptブロックで包まれており、端末接続が失敗した場合は自動的に再起動します。実際のお金での取引では、雑なコーディングは許されません。
注文の執行も特筆に値します。IOC (Immediate or Cancel)タイプを使用しており、要求価格で約定するか、注文がキャンセルされることが保証されます。スリッページやリクオートは発生しません。
操作のしやすさのため、Ctrl+Cによるスムーズな停止機能も追加しました。ロボットは正しくすべてのプロセスを終了し、端末への接続を閉じ、ログを保存します。小さなことのように思えますが、実際の運用では非常に便利です。
システムは現在、リアル口座で稼働して3週目になります。最終的な結論を出すには早すぎますが、最初の結果は非常に励みになります。取引の性質はバックテストで見たものと非常に似ています。特に嬉しいのは、20通貨ペアすべてでシステムが安定して動作しており、「黄色のクラスタ」の概念の普遍性が確認できたことです。
今後の具体的な計画としては、Telegramによるモニタリングの追加や、各ペアのボラティリティに応じたポジションサイズの自動調整があります。しかし、これは次回の記事のテーマです。
VaRモデルの実装
数週間にわたり基本バージョンのロボットを運用した後、固定ロット0.1のポジションサイズは最適ではないことに気づきました。ある通貨ペアは夜間に非常に大きなボラティリティを示す一方で、ほとんど動かないペアもありました。より柔軟な方法が必要だったのです。
解決策は予想外に訪れました。眠れぬ夜を何日も過ごした後、あるアイデアが生まれました。VaRをリスク評価だけでなく、通貨ペア間でポジションサイズを動的に配分するために使うことです。
class VarPositionManager: def __init__(self, target_var: float = 0.01, lookback_days: int = 30): self.target_var = target_var self.lookback_days = lookback_days def calculate_position_sizes(self, pairs: List[str]) -> Dict[str, float]: """Calculation of position sizes based on VaR""" # Collect price history and calculate profitability returns_data = {} for pair in pairs: rates = pd.DataFrame(mt5.copy_rates_from_pos( pair, mt5.TIMEFRAME_D1, 0, self.lookback_days )) if rates is not None and len(rates) > 0: returns_data[pair] = np.log(rates['close'] / rates['close'].shift(1)) returns_df = pd.DataFrame(returns_data).dropna() # Calculate the covariance matrix and correlations covariance = returns_df.cov() * 252 # Annual covariance correlations = returns_df.corr() volatilities = returns_df.std() * np.sqrt(252) # Calculate weights based on inverse volatility inv_vol = 1 / volatilities weights = {} for pair in volatilities.index: # Correction for correlations corr_adjustment = 1.0 for other_pair in volatilities.index: if pair != other_pair: corr = correlations.loc[pair, other_pair] if abs(corr) > 0.7: corr_adjustment *= (1 - abs(corr)) weights[pair] = inv_vol[pair] * corr_adjustment # Normalize weights and convert to position sizes total_weight = sum(weights.values()) weights = {p: w/total_weight for p, w in weights.items()} account = mt5.account_info() position_sizes = {} for pair in pairs: symbol_info = mt5.symbol_info(pair) point_value = (symbol_info.point * 100 if 'JPY' in pair else symbol_info.point * 10000) * symbol_info.trade_contract_size # Base position size size = (self.target_var * account.equity * weights[pair]) / (volatilities[pair] * np.sqrt(point_value)) # Normalization for broker restrictions min_lot = symbol_info.volume_min max_lot = symbol_info.volume_max step = symbol_info.volume_step position_sizes[pair] = max(min_lot, min(round(size / step) * step, max_lot)) return position_sizes
最初のコードバージョンはかなりシンプルで、個々のボラティリティを計算し、基本的なウェイト配分をおこなうものでした。しかし、テストを重ねるうちに、通貨ペア間の相関を考慮する必要があることがますます明らかになりました。特に円クロスでは、ペアが同期して動くことが多く、一方向に過剰なエクスポージャーが生まれることがあったのです。
共分散行列を追加するとコードは大幅に複雑になりましたが、その成果は十分に価値がありました。システムは現在、相関のあるペアのポジションサイズを自動的に減らし、全体のポートフォリオリスクが指定レベルを超えないようにします。そして何より重要なのは、これがすべて動的におこなわれ、市場状況の変化に適応することです。
逆ボラティリティに基づく重み計算の瞬間は特に興味深いものでした。当初は単純な均等配分を使用していましたが、よりボラティリティの高いペアほど、「黄色のクラスタ」のシグナルがより明確に出ることに気づきました。しかし、大きなボリュームで取引すると危険です。このジレンマを逆ボラティリティが完璧に解決してくれました。
VaRモデルの実装には、取引ループの大幅な書き直しが必要でした。現在は、各クラスタのスキャン前に、すべてのペアのデータを収集し、共分散行列を構築して最適なロット配分を計算します。確かにCPUへの負荷は増えましたが、現代のコンピュータであればミリ秒単位で計算可能です。
最も難しかったのは、理論上の重みを実際のポジションサイズに正しくスケーリングすることでした。ここでは、異なるペアの1ポイントあたりのコストや、ブローカーの最小・最大注文サイズ制限も考慮する必要がありました。その結果、理論的なウェイトを実際のポジションサイズに自動変換する、非常にエレガントな方程式ができました。
新しいバージョンで1か月間運用した結果、自信を持って言えることですが、この変更は十分に価値がありました。ドローダウンが均一になり、固定ロット特有の急激なエクイティジャンプは消えました。最も良い点は、システムが真に適応型となり、現在の市場状況に自動的に調整されるようになったことです。
近い将来、検出されたクラスタの強さに応じて、目標VaRレベルを動的に調整する機能を追加したいと考えています。特に強いパターンが形成される瞬間には、システムに少し多くのリスクを取らせることができるというアイデアです。しかし、これは次回の研究のテーマになります。
今後の研究の展望
パソコンの前での眠れぬ夜は、決して無駄ではありませんでした。2か月にわたるライブ取引と無数のパラメータ実験の後、ついにシステム改善の非常に有望な方向性が見えてきました。10,000件を超える取引ログを分析していると(正直、これらの統計を集めるだけでほとんど気が狂いそうでしたが)、いくつかの興味深いパターンに気づきました。
ある夜のことを覚えています。アジア時間のまたしても裏切り的な動きに苛立ちつつ呪っていたとき、突然、あることに気づきました。エントリーパラメータは現在のセッションに依存すべきだということです。アジア時間の流動性の低さは、多くの誤シグナルを生み出しており、私は普遍的な設定を探そうとしていました。その結果、セッションごとに異なるフィルターを使うスクリプトを作成すると、システムはすぐに「呼吸」を始めたのです。
別の頭痛の種はクラスタのマイクロストラクチャーです。私はすでにウェーブレット解析を少し勉強し始めています。予備結果は励みになります。クラスタの内部構造には、実際に価格の動きを示す情報が含まれているようです。あとは、これをどのように形式化するかを考えるだけです。
掘り下げれば掘り下げるほど、疑問は増えていきます。大切なのは傲慢にならず、研究を続けることです。結局、それが取引をこれほど面白くする要因なのです。
結論
6か月にわたる研究により、「黄色のクラスタ」がマーケットマイクロストラクチャーの独自パターンであることを確信しました。3D可視化の実験として始まったものが、印象的な結果を示す本格的な取引システムに成長したのです。
最大の発見は、これら特別な市場状態の形成パターンです。検出された「黄色のクラスタ」の97%は、実際にトレンド反転を予測しており、これは数学モデルと実際の取引結果の両方で確認されています。VaRモデルの実装により最大ドローダウンは31%削減され、ニューラルネットワークの活用により誤シグナルの数はほぼ半分に減少しました。
しかし、技術的側面は成功の一部にすぎません。「黄色のクラスタ」を扱うことで、市場を新しい視点で見る方法が開け、高次の構造が市場データの流れの中に存在することが示されました。これらのパターンは従来のテクニカル分析では捉えられませんが、テンソル解析や機械学習の視点では完璧に明らかになります。
適応的相関の導入、マイクロストラクチャーのウェーブレット解析、先物やオプションへの拡張など、まだ多くの課題が残っています。しかし、すでにマーケットマイクロストラクチャーの基本的性質を発見し、価格動向の理解を変える可能性があることは明らかです。そして、これはまだ始まりに過ぎません。
MetaQuotes Ltdによってロシア語から翻訳されました。
元の記事: https://www.mql5.com/ru/articles/16580
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。





- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索
とても興味深い記事ですね。https://www.mql5.com/ja/articles/16580 以来、あなたの仕事を見守ってきました。
次のステップは、損失を減らして利益を増やすために、ポジションのTP/SLを管理することですか?1200pipsの代わりにTrailing SL/TPを接続することは十分可能です。
記事の中で63pipsと述べていますが、これはすべてのペアの平均的な動きの深さです。