Hierarchical ML Pipeline for trading predictions:
- Level 0: Attention Models (volatility/flow classification)
- Level 1: Base Models (XGBoost per symbol/timeframe)
- Level 2: Metamodels (XGBoost Stacking + Neural Gating)
Key components:
- src/pipelines/hierarchical_pipeline.py - Main prediction pipeline
- src/models/ - All ML model classes
- src/training/ - Training utilities
- src/api/ - FastAPI endpoints
- scripts/ - Training and evaluation scripts
- config/ - YAML configurations
Note: Trained models (*.joblib, *.pt) are gitignored.
Regenerate with training scripts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
286 lines
11 KiB
Python
286 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Train Neural Gating Metamodel and compare with XGBoost Stacking.
|
|
|
|
This script:
|
|
1. Loads the same training data used for XGBoost metamodels
|
|
2. Trains Neural Gating version
|
|
3. Compares performance metrics
|
|
|
|
Usage:
|
|
python scripts/train_neural_gating.py --symbols XAUUSD,EURUSD
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
import argparse
|
|
import numpy as np
|
|
import pandas as pd
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from loguru import logger
|
|
import joblib
|
|
|
|
# Configure logging
|
|
logger.remove()
|
|
logger.add(sys.stdout, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Train Neural Gating Metamodel')
|
|
parser.add_argument('--symbols', type=str, default='XAUUSD,EURUSD',
|
|
help='Comma-separated list of symbols')
|
|
parser.add_argument('--output-dir', type=str, default='models/metamodels_neural',
|
|
help='Output directory for trained models')
|
|
parser.add_argument('--epochs', type=int, default=100,
|
|
help='Max training epochs')
|
|
parser.add_argument('--compare', action='store_true',
|
|
help='Compare with XGBoost metamodel')
|
|
args = parser.parse_args()
|
|
|
|
symbols = [s.strip() for s in args.symbols.split(',')]
|
|
output_dir = Path(args.output_dir)
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
logger.info("=" * 60)
|
|
logger.info("NEURAL GATING METAMODEL TRAINING")
|
|
logger.info("=" * 60)
|
|
logger.info(f"Symbols: {symbols}")
|
|
logger.info(f"Output: {output_dir}")
|
|
|
|
# Check PyTorch availability
|
|
try:
|
|
import torch
|
|
logger.info(f"PyTorch version: {torch.__version__}")
|
|
logger.info(f"CUDA available: {torch.cuda.is_available()}")
|
|
except ImportError:
|
|
logger.error("PyTorch not installed! Run: pip install torch")
|
|
return
|
|
|
|
from src.models.neural_gating_metamodel import (
|
|
NeuralGatingMetamodelWrapper,
|
|
NeuralGatingConfig
|
|
)
|
|
|
|
# Load trainer metadata from XGBoost training
|
|
trainer_metadata_path = Path('models/metamodels/trainer_metadata.joblib')
|
|
|
|
if not trainer_metadata_path.exists():
|
|
logger.error("Trainer metadata not found! Train XGBoost metamodels first.")
|
|
return
|
|
|
|
trainer_metadata = joblib.load(trainer_metadata_path)
|
|
logger.info(f"Loaded trainer metadata from {trainer_metadata_path}")
|
|
|
|
results = {}
|
|
|
|
for symbol in symbols:
|
|
logger.info(f"\n{'='*60}")
|
|
logger.info(f"Training Neural Gating for {symbol}")
|
|
logger.info(f"{'='*60}")
|
|
|
|
# Load training data from metamodel trainer output
|
|
data_path = Path(f'models/metamodels/{symbol}/training_data.joblib')
|
|
|
|
if not data_path.exists():
|
|
# Need to regenerate training data using metamodel trainer
|
|
logger.warning(f"Training data not found for {symbol}, generating via MetamodelTrainer...")
|
|
|
|
from src.training.metamodel_trainer import MetamodelTrainer, MetamodelTrainerConfig
|
|
from src.data.database import MySQLConnection
|
|
|
|
# Load data from database
|
|
db = MySQLConnection()
|
|
|
|
# Map symbol to ticker (database uses C: prefix for forex, X: for crypto)
|
|
ticker_map = {
|
|
'XAUUSD': 'C:XAUUSD',
|
|
'EURUSD': 'C:EURUSD',
|
|
'BTCUSD': 'X:BTCUSD',
|
|
'GBPUSD': 'C:GBPUSD',
|
|
'USDJPY': 'C:USDJPY'
|
|
}
|
|
ticker = ticker_map.get(symbol, f'C:{symbol}')
|
|
|
|
# Load 5m and 15m data for OOS period
|
|
query = f"""
|
|
SELECT date_agg as timestamp, open, high, low, close, volume
|
|
FROM tickers_agg_data
|
|
WHERE ticker = '{ticker}'
|
|
AND date_agg >= '2024-01-01' AND date_agg <= '2024-08-31'
|
|
ORDER BY date_agg ASC
|
|
"""
|
|
|
|
import pandas as pd
|
|
df_raw = pd.read_sql(query, db.engine)
|
|
|
|
if len(df_raw) < 5000:
|
|
logger.error(f"Insufficient data for {symbol}: {len(df_raw)} rows")
|
|
continue
|
|
|
|
df_raw['timestamp'] = pd.to_datetime(df_raw['timestamp'])
|
|
df_raw.set_index('timestamp', inplace=True)
|
|
|
|
# Resample to 5m and 15m
|
|
agg_dict = {
|
|
'open': 'first',
|
|
'high': 'max',
|
|
'low': 'min',
|
|
'close': 'last',
|
|
'volume': 'sum'
|
|
}
|
|
|
|
df_5m = df_raw.resample('5min').agg(agg_dict).dropna()
|
|
df_15m = df_raw.resample('15min').agg(agg_dict).dropna()
|
|
|
|
logger.info(f"Loaded data: 5m={len(df_5m)}, 15m={len(df_15m)}")
|
|
|
|
# Use MetamodelTrainer to generate features
|
|
trainer_config = MetamodelTrainerConfig(
|
|
symbols=[symbol],
|
|
attention_model_path='models/attention',
|
|
base_model_path='models/symbol_timeframe_models',
|
|
output_path=str(output_dir)
|
|
)
|
|
|
|
trainer = MetamodelTrainer(trainer_config)
|
|
|
|
if not trainer.load_models():
|
|
logger.error("Failed to load models")
|
|
continue
|
|
|
|
result = trainer.train_single(df_5m.reset_index(), df_15m.reset_index(), symbol)
|
|
|
|
if result.get('status') == 'failed':
|
|
logger.error(f"Training data generation failed: {result.get('reason')}")
|
|
continue
|
|
|
|
# Get meta_features and targets from the trained model data
|
|
# We need to extract these from the trainer
|
|
logger.warning("Using XGBoost-generated data - Neural Gating uses same features")
|
|
|
|
# Load the just-trained XGBoost data
|
|
xgb_meta_path = Path(str(output_dir)) / symbol / 'training_data.joblib'
|
|
if not xgb_meta_path.exists():
|
|
logger.error(f"Training data not created at {xgb_meta_path}")
|
|
continue
|
|
|
|
data = joblib.load(xgb_meta_path)
|
|
meta_features = data['meta_features']
|
|
target_high = data['target_high']
|
|
target_low = data['target_low']
|
|
|
|
else:
|
|
data = joblib.load(data_path)
|
|
meta_features = data['meta_features']
|
|
target_high = data['target_high']
|
|
target_low = data['target_low']
|
|
|
|
logger.info(f"Training samples: {len(meta_features)}")
|
|
|
|
# Configure neural gating
|
|
config = NeuralGatingConfig(
|
|
epochs=args.epochs,
|
|
early_stopping_patience=15,
|
|
learning_rate=0.001,
|
|
batch_size=256,
|
|
gating_hidden_dims=[32, 16],
|
|
residual_hidden_dims=[64, 32],
|
|
confidence_hidden_dims=[32, 16],
|
|
dropout=0.2
|
|
)
|
|
|
|
# Train
|
|
model = NeuralGatingMetamodelWrapper(symbol, config)
|
|
model.fit(meta_features, target_high, target_low)
|
|
|
|
# Save
|
|
model_path = output_dir / symbol
|
|
model.save(str(model_path))
|
|
|
|
results[symbol] = model.get_training_summary()
|
|
|
|
# Compare with XGBoost if requested
|
|
if args.compare:
|
|
xgb_path = Path(f'models/metamodels/{symbol}')
|
|
if xgb_path.exists():
|
|
from src.models.asset_metamodel import AssetMetamodel
|
|
xgb_model = AssetMetamodel.load(str(xgb_path))
|
|
xgb_summary = xgb_model.get_training_summary()
|
|
|
|
logger.info(f"\n{'-'*40}")
|
|
logger.info(f"COMPARISON: Neural Gating vs XGBoost Stacking")
|
|
logger.info(f"{'-'*40}")
|
|
|
|
neural_metrics = results[symbol]['metrics']
|
|
xgb_metrics = xgb_summary['metrics']
|
|
|
|
logger.info(f"{'Metric':<25} {'Neural':<15} {'XGBoost':<15} {'Winner':<10}")
|
|
logger.info("-" * 65)
|
|
|
|
# MAE comparison (lower is better)
|
|
neural_mae = (neural_metrics['mae_high'] + neural_metrics['mae_low']) / 2
|
|
xgb_mae = (xgb_metrics['mae_high'] + xgb_metrics['mae_low']) / 2
|
|
winner = "Neural" if neural_mae < xgb_mae else "XGBoost"
|
|
logger.info(f"{'MAE (avg)':<25} {neural_mae:<15.4f} {xgb_mae:<15.4f} {winner:<10}")
|
|
|
|
# R2 comparison (higher is better)
|
|
neural_r2 = (neural_metrics['r2_high'] + neural_metrics['r2_low']) / 2
|
|
xgb_r2 = (xgb_metrics['r2_high'] + xgb_metrics['r2_low']) / 2
|
|
winner = "Neural" if neural_r2 > xgb_r2 else "XGBoost"
|
|
logger.info(f"{'R2 (avg)':<25} {neural_r2:<15.4f} {xgb_r2:<15.4f} {winner:<10}")
|
|
|
|
# Confidence accuracy
|
|
neural_conf = neural_metrics['conf_accuracy']
|
|
xgb_conf = xgb_metrics['confidence_accuracy']
|
|
winner = "Neural" if neural_conf > xgb_conf else "XGBoost"
|
|
logger.info(f"{'Confidence Accuracy':<25} {neural_conf:<15.4f} {xgb_conf:<15.4f} {winner:<10}")
|
|
|
|
# Improvement over simple average
|
|
neural_imp = (neural_metrics['improvement_high'] + neural_metrics['improvement_low']) / 2
|
|
xgb_imp = xgb_metrics['improvement_over_avg']
|
|
winner = "Neural" if neural_imp > xgb_imp else "XGBoost"
|
|
logger.info(f"{'Improvement over avg':<25} {neural_imp:<15.1f}% {xgb_imp:<15.1f}% {winner:<10}")
|
|
|
|
# Save training report
|
|
report_path = output_dir / f'training_report_{datetime.now().strftime("%Y%m%d_%H%M%S")}.md'
|
|
|
|
with open(report_path, 'w') as f:
|
|
f.write("# Neural Gating Metamodel Training Report\n\n")
|
|
f.write(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
|
|
f.write("## Summary\n\n")
|
|
f.write("| Symbol | MAE High | MAE Low | R2 High | R2 Low | Alpha High | Alpha Low |\n")
|
|
f.write("|--------|----------|---------|---------|--------|------------|----------|\n")
|
|
|
|
for symbol, summary in results.items():
|
|
m = summary['metrics']
|
|
f.write(f"| {symbol} | {m['mae_high']:.4f} | {m['mae_low']:.4f} | ")
|
|
f.write(f"{m['r2_high']:.4f} | {m['r2_low']:.4f} | ")
|
|
f.write(f"{m['alpha_high_mean']:.3f} | {m['alpha_low_mean']:.3f} |\n")
|
|
|
|
f.write("\n## Gating Analysis\n\n")
|
|
f.write("The alpha values show the learned weight for 5m predictions:\n")
|
|
f.write("- Alpha = 0.5: Equal weight to 5m and 15m\n")
|
|
f.write("- Alpha > 0.5: More weight to 5m (faster timeframe)\n")
|
|
f.write("- Alpha < 0.5: More weight to 15m (slower timeframe)\n\n")
|
|
|
|
for symbol, summary in results.items():
|
|
m = summary['metrics']
|
|
f.write(f"### {symbol}\n")
|
|
f.write(f"- Alpha HIGH mean: {m['alpha_high_mean']:.3f}\n")
|
|
f.write(f"- Alpha LOW mean: {m['alpha_low_mean']:.3f}\n")
|
|
f.write(f"- Interpretation: Model weights {'5m more' if m['alpha_high_mean'] > 0.5 else '15m more'} for HIGH, ")
|
|
f.write(f"{'5m more' if m['alpha_low_mean'] > 0.5 else '15m more'} for LOW\n\n")
|
|
|
|
logger.info(f"\nReport saved to: {report_path}")
|
|
logger.info("\n" + "=" * 60)
|
|
logger.info("NEURAL GATING TRAINING COMPLETE")
|
|
logger.info("=" * 60)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|