English
preview
MetaTrader 5機械学習の設計図(第6回):実務で使えるキャッシュシステムの設計

MetaTrader 5機械学習の設計図(第6回):実務で使えるキャッシュシステムの設計

MetaTrader 5トレーディングシステム |
18 1
Patrick Murimi Njoroge
Patrick Murimi Njoroge


はじめに

本連載の前回の記事では、金融機械学習のための堅牢なパイプラインを構築しました。先読みバイアスへの耐性を確保し、トリプルバリアトレンドスキャンなどの高度なラベリング手法を実装しました。しかし、戦略やMLモデル(たとえば逐次ブートストラップされたランダムフォレスト)が複雑化するにつれ、計算ボトルネックが発生し、高速な反復を妨げる課題に直面します。

たとえば、あなたが有望な平均回帰型戦略を構築したとします。バックテストではシャープレシオ1.8、市場環境にかかわらず安定した利益、そしてクリーンなエクイティカーブが確認できました。ハイパーパラメータ最適化、異なる期間の過去データのテスト、ウォークフォワード分析での検証の準備は整いました。

しかし現実は厳しいです。  

1つのパラメータ組み合わせを計算するのに6分かかります。50通り試したい場合、5時間待つ必要があります。特徴量を変更すればさらに5時間。新しいインジケーターを追加すれば、さらに時間がかかります。

問題は単なる時間の浪費ではありません。機会の損失です。 計算を待っている間は反復作業も新しいアイデアのテストもできません。自分のエッジを改善する余地がなくなり、開発の速度が止まってしまいます。

これは私が初期の取引戦略で痛感した問題です。週末丸々バックテストに費やして、月曜の朝に単純なコードミスに気づき、再び待ち、フラストレーションが積み重なります。

もっと良い方法があるはずです。

本記事では、インテリジェントキャッシュを使ってこのボトルネックを解消する方法を紹介します。読み終わるころには次のことが可能になります。

  • 戦略の最適化時間を「時間単位」から「分単位」に短縮する
  • 以前なら5時間かかっていた作業を50以上のパラメータ組み合わせでテストする
  • すべてを再計算せずに特徴量やモデルを反復改善する

それでは、今あなたが直面している問題から解決していきましょう。


計算パイプラインの可視化

キャッシュを実装する前に、機械学習パイプラインの各処理が実際にどれくらい時間を消費しているかを理解することが重要です。下の図は、ティックデータの読み込みから最終的なバックテストまでのフルパスを示しており、キャッシュなしで各ステップにかかる秒数を表しています。

キャッシュなし

次に、AFMLキャッシュを有効にした場合の同じ処理シーケンスを見てみましょう。ほとんどのステップは再計算されず、瞬時に読み込まれることに注目してください。下の図から、速度の違いは一目でわかります。

AFMLキャッシュあり


なぜ汎用キャッシュは金融機械学習では通用しないのか

一見簡単そうな方法がなぜ通用しないのかを見ていきましょう。

標準のlru_cacheを使えば簡単に解決できそうに思えるかもしれません。しかし、Pythonの組み込みキャッシュは、PandasのSeriesやDataFrameのような金融データ構造では正しく機能しません。以下はその簡単な例です。

# This is USELESS for financial data. Don't do this.
from functools import lru_cache

@lru_cache(maxsize=128)
def compute_rsi(prices, period=14):
    delta = prices.diff()
    gain = delta.where(delta > 0, 0)
    loss = -delta.where(delta < 0, 0)
    
    avg_gain = gain.ewm(alpha=1/period, adjust=False).mean()
    avg_loss = loss.ewm(alpha=1/period, adjust=False).mean()
    
    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

compute_rsi(bb_df.close, period=14)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[25], line 18
     15     rsi = 100 - (100 / (1 + rs))
     16     return rsi
---> 18 compute_rsi(bb_df.close, period=14)

TypeError: unhashable type: 'Series'

Pythonにはfunctools.lru_cacheがありますが、金融機械学習には根本的に不十分です。理由は以下の通りです。

  1. メモリ上のみの保存:Pythonを終了するとキャッシュが消える
  2. セッション間の持続性なし:再起動ごとにすべてを再実行する必要がある
  3. 自動無効化なし:コード変更後も古いキャッシュが残る
  4. NumPy/Pandasの扱いが不十分:配列のハッシュ化で問題が発生
  5. 分散キャッシュなし:複数プロセス間でキャッシュを共有できない
  6. 金融データ認識なし:タイムスタンプや先読みバイアスを理解できない


第I部:アーキテクチャの基礎

コア設計原則


私たちのAFMLキャッシュシステムは、3つの基本的な柱に基づいて構築されています。

AFMLキャッシュアーキテクチャ

各コンポーネントが存在する理由と、それらがどのように連携しているかを理解しましょう。

課題1:永続ストレージ

まず最初に、最も明白な制約から見ていきましょう。通常のキャッシュはRAM上にしか存在せず、Pythonを再起動するとすべての結果が消えます。以下は「普通の方法」がどのように機能し、なぜ現実のMLパイプラインでは役に立たないかの簡単な例です。

# Traditional approach - memory only
from functools import lru_cache

@lru_cache(maxsize=128)
def compute_features(data):
    # Expensive computation
    return features  # Lost on restart!

AFMLではこの問題をシンプルに解決しています。キャッシュをディスク上に移動するのです。Joblibは計算結果をPythonの実行間で保持するため、再起動後でも以前に計算したデータを瞬時に取得できます。実装例を以下に示します。

# AFML approach - persistent
from joblib import Memory
from appdirs import user_cache_dir


memory = Memory(location=user_cache_dir("afml"), verbose=0)

@memory.cache
def compute_features(data):
    # Expensive computation
    return features  # Saved to disk automatically!

課題2:複雑な金融データ構造のハッシュ化

さらに大きな問題があります。金融データは標準的な方法ではハッシュ化できません。Pandas DataFrame、NumPy配列、日時などはすべて「ハッシュ化不可」であり、通常のキャッシュでは動作しません。以下はその失敗例です。

# This fails!
import pandas as pd
from functools import lru_cache

@lru_cache
def bad_cache_example(df: pd.DataFrame):
    return df.mean()

df = pd.DataFrame({'price': [100, 101, 102]})
bad_cache_example(df)  # TypeError: unhashable type: 'DataFrame'

AFMLでは、独自のキー生成器を作成することでキャッシュの正しい動作を保証しています。DataFrameの構造、データ型、列、インデックス、そして最も重要な時間範囲まで理解できるようになっています。カスタムハッシュ化の実装例は以下の通りです。

# afml/cache/robust_cache_keys.py

class CacheKeyGenerator:
    """Generate collision-resistant cache keys for ML data structures."""
    
    @staticmethod
    def _hash_dataframe(df: pd.DataFrame, name: str) -> str:
        """
        Hash DataFrame with attention to:
        1. Shape and structure
        2. Column names and types
        3. Index (especially DatetimeIndex)
        4. Actual data content
        """
        parts = [
            f"shape_{df.shape}",
            f"cols_{hashlib.md5(str(tuple(df.columns)).encode()).hexdigest()[:8]}",
            f"dtypes_{hashlib.md5(str(tuple(df.dtypes)).encode()).hexdigest()[:8]}",
        ]
        
        # Special handling for DatetimeIndex (critical for financial data!)
        if isinstance(df.index, pd.DatetimeIndex):
            # Hash: start date, end date, and length
            # This catches both data changes AND temporal shifts
            parts.append(f"idx_dt_{df.index[0]}_{df.index[-1]}_{len(df.index)}")
        else:
            idx_hash = hashlib.md5(str(tuple(df.index)).encode()).hexdigest()[:8]
            parts.append(f"idx_{idx_hash}")
        
        # For large DataFrames, sample for performance
        if df.size > 10000:
            # Sample ~100 rows evenly distributed
            sample_rows = df.iloc[::max(1, len(df) // 100)]
            content_hash = hashlib.md5(sample_rows.values.tobytes()).hexdigest()[:8]
        else:
            # Hash full content for small DataFrames
            content_hash = hashlib.md5(df.values.tobytes()).hexdigest()[:8]
        
        parts.append(f"data_{content_hash}")
        
        return f"{name}_df_{'_'.join(parts)}"

標準PythonキャッシュとAFMLキャッシュを比較すると、通常のキャッシュが先読みバイアスを引き起こすのに対し、AFMLはこれを回避できることがわかります。以下は、標準的なアプローチでは先読みバイアスが発生するのに対し、AFMLでは発生しない理由を示した図です。

DataFrameハッシュ比較

課題3:コード変更時の自動キャッシュ無効化

完璧なキャッシュであっても、コードを編集して古い結果が残っている場合は意味がありません。AFMLは関数の変更を自動的に追跡し、古くなった部分だけをキャッシュからクリアします。実装方法を見てみましょう。

AFMLは関数のソースコードハッシュとファイルの最終更新日時を保存します。何かが変更された場合、その関数に対応するキャッシュは自動的にリセットされます。内部的な仕組みは以下の通りです。

# afml/cache/selective_cleaner.py
class FunctionTracker:
    """
    Tracks function signatures and source code hashes.
    Automatically detects when functions change.
    """
    
    def track_function(self, func) -> bool:
        """
        Returns True if function has changed since last tracking.
        """
        func_name = f"{func.__module__}.{func.__qualname__}"
        
        # Get current function metadata
        current_hash = self._get_function_hash(func)      # Hash source code
        current_mtime = self._get_file_mtime(func)        # File modification time
        
        # Compare with stored metadata
        stored = self.tracked_functions.get(func_name, {})
        stored_hash = stored.get("hash")
        stored_mtime = stored.get("mtime")
        
        # Function changed if EITHER hash or mtime differs
        has_changed = (
            current_hash != stored_hash or 
            current_mtime != stored_mtime or 
            stored_hash is None  # New function
        )
        
        if has_changed:
            # Update tracking data
            self.tracked_functions[func_name] = {
                "hash": current_hash,
                "mtime": current_mtime,
                "module": func.__module__,
            }
            self._save_tracking_data()
            
        return has_changed
    
    def _get_function_hash(self, func) -> Optional[str]:
        """Hash function source code."""
        try:
            source = inspect.getsource(func)
            return hashlib.md5(source.encode()).hexdigest()
        except (OSError, TypeError):
            return None

実際のシナリオを考えてみましょう。関数に最初はエラーがあり、それを修正した場合でも、AFMLは変更を自動で検知し、該当部分のキャッシュだけをクリアします。

スマートキャッシュ可能:自動無効化フロー


第II部:高機能キャッシュ可能デコレーター

すべてのAFML機能は、ひとつの強力なファクトリーデコレーターに統合されています。このデコレーターは、通常キャッシュ、テンポラリキャッシュ、データ追跡キャッシュ、コード追跡キャッシュなど、必要なキャッシュタイプを生成します。以下はファクトリーのソースコードです。

# afml/cache/robust_cache_keys.py

def create_robust_cacheable(
    track_data_access: bool = False,
    dataset_name: Optional[str] = None,
    purpose: Optional[str] = None,
    use_time_awareness: bool = False,
):
    """
    Factory function to create robust cacheable decorators.
    This is where all the magic comes together.
    """
    from functools import wraps
    from . import cache_stats, memory
    from .cache_monitoring import get_cache_monitor
    
    def decorator(func):
        func_name = f"{func.__module__}.{func.__qualname__}"
        cached_func = memory.cache(func)  # Use joblib for persistence
        seen_signatures = set()           # Track cache hits/misses
        monitor = get_cache_monitor()     # Performance monitoring
        
        @wraps(func)
        def wrapper(*args, **kwargs):
            # Step 1: Generate cache key
            try:
                if use_time_awareness:
                    cache_key = TimeSeriesCacheKey.generate_key_with_time_range(
                        func, args, kwargs
                    )
                else:
                    cache_key = CacheKeyGenerator.generate_key(func, args, kwargs)
                
                # Step 2: Track hit/miss
                if cache_key in seen_signatures:
                    cache_stats.record_hit(func_name)
                    is_hit = True
                else:
                    cache_stats.record_miss(func_name)
                    seen_signatures.add(cache_key)
                    is_hit = False
                    
            except Exception as e:
                logger.warning(f"Cache key generation failed: {e}")
                cache_stats.record_miss(func_name)
                cache_key = None
                is_hit = False
            
            # Step 3: Track data access if requested (prevent look-ahead bias)
            if track_data_access:
                try:
                    from .data_access_tracker import get_data_tracker
                    _track_dataframe_access(
                        get_data_tracker(), args, kwargs, dataset_name, purpose
                    )
                except Exception as e:
                    logger.warning(f"Data tracking failed: {e}")
            
            # Step 4: Track access time (for monitoring)
            monitor.track_access(func_name)
            
            # Step 5: Execute function with timing
            start_time = time.time()
            try:
                result = cached_func(*args, **kwargs)
                
                # Track computation time for misses
                if not is_hit:
                    computation_time = time.time() - start_time
                    monitor.track_computation_time(func_name, computation_time)
                
                return result
                
            except (EOFError, pickle.PickleError, OSError) as e:
                # Handle cache corruption gracefully
                logger.warning(f"Cache corruption: {type(e).__name__} - recomputing")
                
                # Clear corrupted cache
                if cache_key is not None:
                    _clear_corrupted_cache(cached_func, cache_key)
                
                # Execute function directly
                return func(*args, **kwargs)
                
        wrapper._afml_cacheable = True
        return wrapper
    
    return decorator

# Standard decorators
robust_cacheable = create_robust_cacheable(use_time_awareness=False)
time_aware_cacheable = create_robust_cacheable(use_time_awareness=True)

# Data tracking decorators
data_tracking_cacheable = lambda dataset_name, purpose: create_robust_cacheable(
    track_data_access=True, dataset_name=dataset_name, purpose=purpose, use_time_awareness=False
)

time_aware_data_tracking_cacheable = lambda dataset_name, purpose: create_robust_cacheable(
    track_data_access=True, dataset_name=dataset_name, purpose=purpose, use_time_awareness=True
)



第III部:進化したキャッシュパターン

パターン1:ウォークフォワード分析向けの時間認識キャッシュ

ウォークフォワード検証では、キャッシュが異なる期間を区別することが非常に重要です。これをおこなわないと、キャッシュキー衝突や誤った結果が簡単に発生します。下の図は実際の動作を示しています。

Time Series Data (2024):
─────────────────────────────────────────────────────────────────────
Jan  Feb  Mar  Apr  May  Jun  Jul  Aug  Sep  Oct  Nov  Dec
├────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┼────┤
│    │    │    │    │    │    │    │    │    │    │    │    │
▼────▼────▼────▼────▼────▼────▼────▼────▼────▼────▼────▼────▼

Walk-Forward Splits:
─────────────────────────────────────────────────────────────────────

Split 1:
        Train Period: Jan-Mar              Test Period: Apr
        ├─────────────────────────────┤    ├───────┤

Split 2:
            Train Period: Feb-Apr              Test Period: May
            ├─────────────────────────────┤    ├───────┤

Split 3:
                Train Period: Mar-May              Test Period: Jun
                ├─────────────────────────────┤    ├───────┤

次の例は、通常キャッシュを異なる期間で使うと同じハッシュが生成され、モデルが誤ったデータで学習してしまうケースを示しています。その結果、モデルは誤ったデータで学習され、先読みバイアスが発生します。

@robust_cacheable  # Generic caching
def train_model(data, params):
    return model

Split 1: data = data['2024-01':'2024-03']
         Cache Key: hash(data.values)
         └──> "a7f3e8d92c1b..."        ◄──┐
                                          │
Split 2: data = data['2024-02':'2024-04'] │
         Cache Key: hash(data.values)     │
         └──> "a7f3e8d92c1b..."        ◄──┘ COLLISION! 
              └──> Cache hit on DIFFERENT time period
              └──> Model trained on wrong data!

時間認識キャッシュでは、キー生成時に時間範囲を追加することでこの問題を完全に回避します。キャッシュは各期間ごとに正しく区別され、モデルはその期間専用に学習されます。

@time_aware_cacheable  # Includes temporal info
def train_model(data, params):
    return model

Split 1: data = data['2024-01':'2024-03']
         Cache Key: hash(data.values + "2024-01_2024-03")
         └──> "a7f3_time_2024-01_2024-03"
         
Split 2: data = data['2024-02':'2024-04']
         Cache Key: hash(data.values + "2024-02_2024-04")
         └──> "b9e4_time_2024-02_2024-04"  ← Different!
              └──> Cache miss (correct)
              └──> Train new model for this period ✓

Split 3: data = data['2024-03':'2024-05']
         Cache Key: hash(data.values + "2024-03_2024-05")
         └──> "c7d1_time_2024-03_2024-05"  ← Different!
              └──> Cache miss (correct)
              └──> Train new model for this period ✓

以下は、キャッシュキーにタイムスタンプを追加するクラスの実装です。

class TimeSeriesCacheKey(CacheKeyGenerator):
    """Extended cache key generator with time-series awareness."""
    
    @staticmethod
    def generate_key_with_time_range(
        func, 
        args: tuple, 
        kwargs: dict, 
        time_range: Tuple[pd.Timestamp, pd.Timestamp] = None
    ) -> str:
        """
        Generate cache key that includes time range information.
        Critical for preventing temporal data leakage.
        """
        base_key = CacheKeyGenerator.generate_key(func, args, kwargs)
        
        if time_range is None:
            # Try to extract time range from data
            time_range = TimeSeriesCacheKey._extract_time_range(args, kwargs)
        
        if time_range:
            start, end = time_range
            time_hash = f"time_{start}_{end}"
            return f"{base_key}_{time_hash}"
        
        return base_key

# Usage in walk-forward validation
@time_aware_cacheable
def train_on_period(data: pd.DataFrame, params: dict) -> Model:
    """
    Train model on specific time period.
    Cache is automatically keyed by time range!
    """
    model = RandomForestClassifier(**params)
    model.fit(data.drop('target', axis=1), data['target'])
    return model

# Walk-forward loop
for train_start, train_end, test_start, test_end in walk_forward_splits:
    # Each period is cached independently
    train_data = data.loc[train_start:train_end]
    model = train_on_period(train_data, params)  # Cached per period
    
    test_data = data.loc[test_start:test_end]
    predictions = model.predict(test_data)

パターン2:Sklearn推定器向けクロスバリデーションキャッシュ

クロスバリデーションのキャッシュは別の課題です。Sklearn分類器は内部状態を持ち、直接ハッシュ化できません。AFMLではモデルの種類とパラメータだけをハッシュ化することで、キャッシュを安全に適用します。以下が、その主要関数です。

# afml/cache/cv_cache.py

def _hash_classifier(clf: BaseEstimator) -> str:
    """
    Generate stable hash for sklearn classifier.
    
    KEY INSIGHT: Hash the type + parameters, NOT the trained state!
    """
    try:
        clf_type = type(clf).__name__
        params = clf.get_params(deep=True)
        
        # Filter out non-serializable params
        serializable_params = {}
        for k, v in params.items():
            try:
                json.dumps(v)  # Test if JSON serializable
                serializable_params[k] = v
            except (TypeError, ValueError):
                # Use type name for non-serializable params
                serializable_params[k] = f"<{type(v).__name__}>"
        
        # Create stable hash
        param_str = json.dumps(serializable_params, sort_keys=True)
        combined = f"{clf_type}_{param_str}"
        return hashlib.md5(combined.encode()).hexdigest()[:12]
        
    except Exception as e:
        logger.debug(f"Failed to hash classifier: {e}")
        return f"clf_{type(clf).__name__}_{id(clf)}"

次に、クロスバリデーション結果をキャッシュするためのデコレーターの例を示します。これを使うことで、ハイパーパラメータ選択時の計算時間を大幅に節約できます。

@cv_cacheable
def ml_cross_val_score(
    classifier: BaseEstimator,
    X: pd.DataFrame,
    y: pd.Series,
    cv_gen,  # PurgedKFold, TimeSeriesSplit, etc.
    sample_weight_train: Optional[np.ndarray] = None,
    scoring: str = 'neg_log_loss'
) -> np.ndarray:
    """
    Cross-validation with proper caching.
    
    Caches based on:
    - Classifier type and parameters (not trained state)
    - Data content (X, y)
    - CV generator configuration
    - Sample weights
    """
    scores = []
    
    for train_idx, test_idx in cv_gen.split(X):
        X_train, X_test = X.iloc[train_idx], X.iloc[test_idx]
        y_train, y_test = y.iloc[train_idx], y.iloc[test_idx]
        
        # Train fresh model (not from cache)
        model = clone(classifier)
        
        if sample_weight_train is not None:
            model.fit(X_train, y_train, 
                     sample_weight=sample_weight_train[train_idx])
        else:
            model.fit(X_train, y_train)
        
        score = model.score(X_test, y_test)
        scores.append(score)
    
    return np.array(scores)

パターン3:テストデータ汚染の防止

ML開発において、最も見落としやすいミスの一つは、誤ってテストデータを学習に使用してしまうことです。AFMLはデータセットへのすべてのアクセスを監視し、漏洩があれば通知します。

AFMLは各データアクセスを記録し、期間、用途(学習、テスト、検証など)、呼び出し元を含む情報を保存します。これらのログに基づき、潜在的なデータリークに関するレポートを生成します。

# afml/cache/data_access_tracker.py

class DataAccessTracker:
    """
    Track every data access to detect test set contamination.
    
    This is CRITICAL for preventing data snooping bias.
    """
    
    def log_access(
        self,
        dataset_name: str,
        start_date: pd.Timestamp,
        end_date: pd.Timestamp,
        purpose: str,  # 'train', 'test', 'validate', 'optimize'
        data_shape: Optional[Tuple[int, int]] = None,
    ):
        """Log a dataset access with full temporal metadata."""
        entry = {
            "timestamp": datetime.now().isoformat(),
            "dataset": dataset_name,
            "start_date": str(start_date),
            "end_date": str(end_date),
            "purpose": purpose,
            "data_shape": str(data_shape) if data_shape else None,
            "caller": self._get_caller_info(),  # Stack trace
        }
        
        self.access_log.append(entry)
        logger.debug(
            f"Logged access: {dataset_name} [{start_date} to {end_date}] "
            f"for {purpose}"
        )
# Usage with caching decorator
@time_aware_data_tracking_cacheable(dataset_name="eur_usd_2024", purpose="test")
def evaluate_on_test_set(test_data: pd.DataFrame, model) -> dict:
    """
    Evaluate model on test set.
    Access is logged automatically!
    """
    predictions = model.predict(test_data)
    metrics = calculate_metrics(predictions, test_data['target'])
    return metrics

モデル開発プロセス中にデータ汚染がないか確認するために、次のようにprint_contamination_report()を呼び出します。

# Later, check for contamination
from afml.cache import print_contamination_report

print_contamination_report()

データ汚染レポート


第IV部:パフォーマンス監視と最適化

キキャッシュ健全性モニタリング


AFMLにはキャッシュの健全性を監視する機能も組み込まれており、次の項目を追跡できます。

  • どの関数が最も頻繁にキャッシュされているか
  • どこでキャッシュミスが発生しているか
  • キャッシュサイズがどれくらいか
  • 不審な状況が発生していないか
以下はレポートのサンプルです。

from afml.cache import print_cache_health


print_cache_health()

キャッシュ健全性レポート


第V部:既存プロジェクトへの統合

システムを使い始めるには、モジュールをインポートして目的のデコレーターを使用するだけです。複雑な設定は必要ありません。実際のプロジェクトでは次のように利用できます。

以下のコードをターミナルで実行してリポジトリをクローンするか、cache.zipをダウンロードしてください。

git clone https://github.com/pnjoroge54/Machine-Learning-Blueprint.git
cd Machine-Learning-Blueprint/afml/cache
その後、キャッシュモジュールを自分のパッケージにコピーします。
my_package/
├── cache/
│   ├── __init__.py
│   ├── backtest_cache.py
│   ├── cache_monitoring.py
│   ├── cv_cache.py
│   ├── data_access_tracker.py
│   ├── robust_cache_keys.py
│   ├── selective_cleaner.py
│   └── mql5_bridge.py

このキャッシュシステムを自分のプロジェクトに実装する詳細については、user_guide.pyを参照してください。


結論:研究の速度から実行スピードへ

AFMLキャッシュシステムは、研究段階と本番運用を分ける2つの重要なボトルネック、すなわち反復速度と実行レイテンシーを解決することで、金融ML開発を根本的に変えます。

研究速度:アルファ生成の基盤

研究フェーズでは、このキャッシュアーキテクチャにより前例のない探索が可能になります。

  • 迅速な特徴量エンジニアリング:RSI期間やボラティリティ計算、複数のテクニカル指標の組み合わせを数十パターン試しても、ベースとなる変換を再計算する必要はありません。システムが中間結果をインテリジェントにキャッシュするため、計算待ちではなく特徴量選択の反復に集中できます。

  • 徹底した戦略検証:PurgedKFoldクロスバリデーションを数百のパラメータ組み合わせで実行できます。各データフォール度の特徴量とラベルはキャッシュされたまま保持されるため、従来手法で問題となる時間方向のデータリークを防ぎつつ、大規模なバックテストが可能になります。

  • マルチ時間足分析:1分足、5分足、1時間足など複数の時間足にまたがる複雑な特徴量の相互作用を検証できます。時間認識キャッシュにより、各期間の計算は独立して保持され、再現性も確保されます。

  • ラベリング戦略の最適化:Triple-Barrier、Trend-Scanning、Meta-Labelingといった手法を比較する際も、高コストなバーサンプリング処理を毎回再計算する必要はありません。

完全なパイプライン:研究から実行まで

このキャッシュアーキテクチャにより、実験的な研究から本番トレーディングへの移行がシームレスになります。

  1. 研究フェーズ:Pythonの豊富なエコシステムとAFMLキャッシュを利用して高速に実験と検証をおこないます。
  2. モデルのエクスポート:検証済みモデルをONNX形式に変換し、依存関係なしでデプロイできるようにします。
  3. 特徴量パイプライン移行:重要な特徴量計算をMQL5にネイティブ実装し、並列キャッシュを活用します。
  4. 本番環境への展開:研究の整合性を保ったまま、マイクロ秒レベルのレイテンシーで戦略を実行します。

データの整合性:目に見えない利点

パフォーマンスだけでなく、このシステムはデータアクセス追跡によって本番MLシステムを破壊する微妙なデータ汚染も防ぎます。完全な監査ログにより、誤ってテストデータで最適化してしまうことを防ぎます。また時間認識機能により、ウォークフォワード検証の整合性も保証されます。

今後の展望:最後のフロンティア

次回の記事では、このパイプラインをさらに完成させます。

  1. ONNXモデルの展開:scikit-learnやカスタムモデルをONNXへエクスポートし、Python依存なしでMQL5上で実行します。
  2. MQL5推論エンジン:マイクロ秒単位で動作する超低レイテンシー予測システムを構築します。
  3. ハイブリッド特徴量管理:研究段階ではPythonで複雑な特徴量を計算し、本番環境ではMQL5へ移行するインテリジェントな仕組みを設計します。
  4. リアルタイムモデル監視:MQL5の実行環境内でドリフト検出とパフォーマンス追跡を実装します。

最終的に得られるのは、Pythonの柔軟性で研究し、MQL5のパフォーマンスでデプロイし、ライフサイクル全体を通じて科学的厳密性を維持できる完全なエコシステムです。これは単に計算を高速化する話ではありません。高度なML戦略が実際の市場で機能するためのフレームワークを構築するということです。


コードリポジトリ

この記事のすべてのコードは、Machine-Learning-Blueprintリポジトリで公開されています。ターミナルで次のコードを実行してクローンできます。

git clone https://github.com/pnjoroge54/Machine-Learning-Blueprint.git
cd Machine-Learning-Blueprint/afml/cache


モジュールファイル

モジュールファイル 目的 主な特徴 用途
__init__.py 中央初期化および調整モジュール • すべてのキャッシュサブシステムを初期化
• NumbaとJoblibのキャッシュ設定
• すべてのキャッシュ機能に対する統一APIを提供 • 便利な関数をエクスポート
• キャッシュディレクトリを構成
任意のキャッシュ機能にアクセスする際にインポートします。システム全体の単一エントリーポイントです。
robust_cache_keys.py 金融データ向け高度キャッシュキー生成 • NumPy配列を正しくハッシュ化
• DatetimeIndex付きPandas DataFrameに対応
• 時系列認識型キー生成
• Sklearn推定器のハッシュ化
• 時間方向のデータリークを防止
金融時系列データ、DataFrame、MLモデルを処理する関数で使用します。
selective_cleaner.py インテリジェントなキャッシュ無効化システム • 関数ソースコード変更の追跡
• コード変更時の自動キャッシュクリア
• モジュール単位の選択的無効化
• サイズおよび経過時間ベースのクリーンアップ
• smart_cacheableデコレーター
開発中の反復作業で古いキャッシュ問題を防ぐために使用します。研究開発では必須です。
data_access_tracker.py テストデータ汚染の防止 • タイムスタンプ付きでデータセットアクセスを記録
• 学習/テスト/検証用途を追跡
• 汚染レポート生成
• データスヌーピングバイアス検出
• 監査ログ提供
研究の信頼性確保に重要です。モデル開発中のすべてのデータアクセス追跡に使用します。
cv_cache.py クロスバリデーション専用キャッシュ • CV結果を効率的にキャッシュ
• sklearn推定器に対応
• PurgedKFoldおよびカスタムCV対応
• 推定器パラメータと状態を分離
• 高速CV反復
高コストなクロスバリデーション実験を実行する際に使用します。ハイパーパラメータ最適化を大幅に高速化します。
backtest_cache.py バックテストワークフローの最適化 • バックテスト実行全体をキャッシュ
• ウォークフォワード分析対応
• パラメータ最適化追跡
• 取引単位キャッシュ
• 結果比較ツール
戦略開発で重要です。バックテスト結果をキャッシュし、パラメータバリエーション比較を効率化します。
cache_monitoring.py パフォーマンス分析と診断 • 関数ごとのヒット率追跡
• 計算時間測定
• キャッシュサイズ監視
• 健全性レポートと改善提案
• 効率分析
キャッシュ性能を把握し、最適化ポイントを特定する際に使用します。
mlflow_integration.py 実験追跡統合 • キャッシュとMLflowの統合
• 実験ログ自動記録
• モデルバージョン管理
• メトリック追跡
• 結果比較
本格的な研究環境で実験を追跡しながらキャッシュを活用する場合に使用します。
mql5_bridge.py Python-MQL5通信ブリッジ • MQL5からPythonスクリプトを起動
• ファイルベース信号による言語間実行
• リアルタイムモデル推論対応
• MLパイプラインとMetaTrader 5を統合
PythonベースMLモデルをMQL5取引環境へデプロイする際に使用します。
startup_script.py 環境ブートストラップとキャッシュセットアップ • キャッシュディレクトリとログ初期化
• 環境変数読み込み
• 再現可能な起動環境を保証
• MLflowや監視設定を起動可能
MLまたは取引ワークフローの開始時に使用し、再現性のある環境を確保します。
PythonBridgeEA.mq5 Python連携用MQL5ブリッジEA • MQL5とPythonの橋渡し
• チャートイベントとファイルシグナルを使用
• MetaTraderからPythonスクリプトを起動
• Python出力と取引ロジックを同期
MetaTrader5のチャートにアタッチしてPython通信を有効化します。リアルタイムML統合に必須です。


MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20302

添付されたファイル |
PythonBridgeEA.mq5 (21.22 KB)
user_guide.py (22.37 KB)
cache.zip (45.84 KB)
最後のコメント | ディスカッションに移動 (1)
Kgothatso Nkuna
Kgothatso Nkuna | 27 11月 2025 において 08:45
FX 自動売買ロボット mt5を作りたい
EAのサンプル EAのサンプル
一般的なMACDを使ったEAを例として、MQL4開発の原則を紹介します。
MQL5での取引戦略の自動化(第41回):ローソク足レンジ理論(CRT)-蓄積・操作・分配(AMD) MQL5での取引戦略の自動化(第41回):ローソク足レンジ理論(CRT)-蓄積・操作・分配(AMD)
MQL5で動作するローソク足レンジ理論(CRT)取引システムを開発します。このシステムは、指定した時間足での蓄積のレンジを特定し、操作の深さフィルタリングを用いてブレイクを検知し、分配フェーズにおける反転確認を経てエントリーをおこないます。また、リスクリワード比に基づく動的または静的なストップロスとテイクプロフィット設定、任意のトレーリングストップ、方向ごとのポジション上限設定などによるリスク管理にも対応しています。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
ケンドールのタウ係数と距離相関を用いたVGTの市場ポジショニング分析コード ケンドールのタウ係数と距離相関を用いたVGTの市場ポジショニング分析コード
この記事では、相補的なインジケーターのペアをどのように活用して、過去5年間のVanguard Information Technology Index Fund ETF (VGT)を分析できるかを検討します。具体的には、ケンドールのタウ係数と距離相関という2つの指標を用い、VGTの取引に適したインジケーターの組み合わせだけでなく、それら2つのインジケーターに対するシグナルパターンの最適なペアリングも選定することを目指します。