trading-platform-ml-engine-v2/scripts/run_oos_backtest.py
rckrdmrd 75c4d07690 feat: Initial commit - ML Engine codebase
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>
2026-01-18 04:27:40 -06:00

308 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Out-of-Sample Backtesting Script
================================
Ejecuta backtesting excluyendo 2025 del training para validacion OOS.
Uso:
python scripts/run_oos_backtest.py --symbol XAUUSD --config config/validation_oos.yaml
Creado: 2026-01-04
Autor: ML-Specialist (NEXUS v4.0)
"""
import argparse
import yaml
import pandas as pd
import numpy as np
from pathlib import Path
from datetime import datetime
from typing import Dict, Any, Optional
import json
import sys
# Add parent to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.backtesting import RRBacktester, BacktestConfig, MetricsCalculator, TradingMetrics
from src.data.database import DatabaseConnection
from loguru import logger
class OOSBacktestRunner:
"""
Runner para backtesting Out-of-Sample.
Excluye datos del periodo de test (2025) durante el training.
"""
def __init__(self, config_path: str):
"""
Inicializa el runner con configuracion YAML.
Args:
config_path: Ruta al archivo validation_oos.yaml
"""
self.config = self._load_config(config_path)
self.results: Dict[str, Any] = {}
logger.info(f"OOS Backtest Runner initialized")
logger.info(f"Training period: {self.config['validation']['train']['start_date']} to {self.config['validation']['train']['end_date']}")
logger.info(f"OOS period: {self.config['validation']['test_oos']['start_date']} to {self.config['validation']['test_oos']['end_date']}")
def _load_config(self, config_path: str) -> Dict[str, Any]:
"""Carga configuracion desde YAML."""
with open(config_path, 'r') as f:
return yaml.safe_load(f)
def load_data(self, symbol: str) -> tuple[pd.DataFrame, pd.DataFrame]:
"""
Carga datos separados para training y OOS testing.
Args:
symbol: Simbolo a cargar (ej: XAUUSD)
Returns:
Tuple de (df_train, df_oos)
"""
train_config = self.config['validation']['train']
oos_config = self.config['validation']['test_oos']
# Conectar a base de datos
db = DatabaseConnection()
# Cargar datos de training (pre-2025)
logger.info(f"Loading training data for {symbol}...")
df_train = db.get_ticker_data(
symbol=symbol,
start_date=train_config['start_date'],
end_date=train_config['end_date']
)
logger.info(f"Training data: {len(df_train)} bars from {df_train.index.min()} to {df_train.index.max()}")
# Cargar datos OOS (2025)
logger.info(f"Loading OOS data for {symbol}...")
df_oos = db.get_ticker_data(
symbol=symbol,
start_date=oos_config['start_date'],
end_date=oos_config['end_date']
)
logger.info(f"OOS data: {len(df_oos)} bars from {df_oos.index.min()} to {df_oos.index.max()}")
return df_train, df_oos
def create_backtest_config(self) -> BacktestConfig:
"""Crea configuracion de backtesting desde YAML."""
bt_config = self.config['backtest']
return BacktestConfig(
initial_capital=bt_config['initial_capital'],
risk_per_trade=bt_config['risk_per_trade'],
max_concurrent_trades=bt_config['max_concurrent_trades'],
commission_pct=bt_config['commission_pct'],
slippage_pct=bt_config['slippage_pct'],
min_confidence=bt_config['min_confidence'],
max_position_time=bt_config['max_position_time_minutes'],
rr_configs=bt_config['rr_configs'],
filter_by_amd=bt_config['filter_by_amd'],
favorable_amd_phases=bt_config['favorable_amd_phases'],
filter_by_volatility=bt_config['filter_by_volatility'],
min_volatility_regime=bt_config['min_volatility_regime']
)
def validate_metrics(self, metrics: TradingMetrics) -> Dict[str, bool]:
"""
Valida metricas contra umbrales definidos (TRADING-STRATEGIST).
Returns:
Dict con cada metrica y si pasa o no
"""
thresholds = self.config['metrics_thresholds']
validations = {
'sharpe_ratio': metrics.sharpe_ratio >= thresholds['sharpe_ratio_min'],
'sortino_ratio': metrics.sortino_ratio >= thresholds['sortino_ratio_min'],
'max_drawdown': abs(metrics.max_drawdown_pct) <= thresholds['max_drawdown_max'],
'win_rate': metrics.winrate >= thresholds['win_rate_min'],
'profit_factor': metrics.profit_factor >= thresholds['profit_factor_min'],
}
return validations
def run_backtest(self, symbol: str, signals: pd.DataFrame, df_oos: pd.DataFrame) -> Dict[str, Any]:
"""
Ejecuta backtest en datos OOS.
Args:
symbol: Simbolo
signals: DataFrame con senales generadas
df_oos: DataFrame con datos de precio OOS
Returns:
Resultados del backtest
"""
config = self.create_backtest_config()
backtester = RRBacktester(config)
logger.info(f"Running backtest on {symbol} with {len(signals)} signals...")
result = backtester.run_backtest(df_oos, signals)
# Validar metricas
validations = self.validate_metrics(result.metrics)
all_passed = all(validations.values())
return {
'symbol': symbol,
'metrics': result.metrics.__dict__,
'validations': validations,
'gate_passed': all_passed,
'total_trades': len(result.trades),
'equity_curve': result.equity_curve.tolist() if hasattr(result, 'equity_curve') else [],
'metrics_by_rr': {k: v.__dict__ for k, v in result.metrics_by_rr.items()} if hasattr(result, 'metrics_by_rr') else {},
'metrics_by_amd': {k: v.__dict__ for k, v in result.metrics_by_amd.items()} if hasattr(result, 'metrics_by_amd') else {},
}
def generate_report(self, results: Dict[str, Any], output_dir: str) -> str:
"""
Genera reporte de backtesting.
Args:
results: Resultados del backtest
output_dir: Directorio de salida
Returns:
Ruta al archivo de reporte
"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
report_file = output_path / f"oos_backtest_{results['symbol']}_{timestamp}.json"
# Agregar metadata
results['metadata'] = {
'generated_at': datetime.now().isoformat(),
'config': self.config['validation'],
'thresholds': self.config['metrics_thresholds']
}
with open(report_file, 'w') as f:
json.dump(results, f, indent=2, default=str)
logger.info(f"Report saved to {report_file}")
return str(report_file)
def print_summary(self, results: Dict[str, Any]):
"""Imprime resumen de resultados."""
m = results['metrics']
v = results['validations']
print("\n" + "="*60)
print(f"OOS BACKTEST RESULTS - {results['symbol']}")
print("="*60)
print(f"\nTotal Trades: {results['total_trades']}")
print(f"\n{'Metric':<20} {'Value':<15} {'Threshold':<15} {'Status':<10}")
print("-"*60)
thresholds = self.config['metrics_thresholds']
metrics_display = [
('Sharpe Ratio', m.get('sharpe_ratio', 0), f">= {thresholds['sharpe_ratio_min']}", v.get('sharpe_ratio', False)),
('Sortino Ratio', m.get('sortino_ratio', 0), f">= {thresholds['sortino_ratio_min']}", v.get('sortino_ratio', False)),
('Max Drawdown', f"{abs(m.get('max_drawdown_pct', 0))*100:.1f}%", f"<= {thresholds['max_drawdown_max']*100:.0f}%", v.get('max_drawdown', False)),
('Win Rate', f"{m.get('winrate', 0)*100:.1f}%", f">= {thresholds['win_rate_min']*100:.0f}%", v.get('win_rate', False)),
('Profit Factor', m.get('profit_factor', 0), f">= {thresholds['profit_factor_min']}", v.get('profit_factor', False)),
]
for name, value, threshold, passed in metrics_display:
status = "PASS" if passed else "FAIL"
status_color = status
print(f"{name:<20} {str(value):<15} {threshold:<15} {status_color:<10}")
print("-"*60)
gate_status = "APPROVED" if results['gate_passed'] else "REJECTED"
print(f"\nGATE TRADING: {gate_status}")
print("="*60)
def main():
parser = argparse.ArgumentParser(description='Run Out-of-Sample Backtesting')
parser.add_argument('--symbol', type=str, default='XAUUSD', help='Symbol to backtest')
parser.add_argument('--config', type=str, default='config/validation_oos.yaml', help='Config file path')
parser.add_argument('--output', type=str, default='reports/validation', help='Output directory')
parser.add_argument('--mock', action='store_true', help='Use mock data for testing')
args = parser.parse_args()
logger.info(f"Starting OOS Backtest for {args.symbol}")
runner = OOSBacktestRunner(args.config)
if args.mock:
# Generar datos mock para testing del script
logger.warning("Using MOCK data - not real backtest results")
# Mock signals
dates = pd.date_range('2025-01-01', '2025-12-31', freq='1H')
mock_signals = pd.DataFrame({
'timestamp': dates,
'direction': np.random.choice(['long', 'short'], len(dates)),
'confidence': np.random.uniform(0.5, 0.9, len(dates)),
'amd_phase': np.random.choice(['accumulation', 'distribution'], len(dates)),
}).set_index('timestamp')
# Mock price data
mock_prices = pd.DataFrame({
'open': np.random.uniform(1800, 2000, len(dates)),
'high': np.random.uniform(1810, 2010, len(dates)),
'low': np.random.uniform(1790, 1990, len(dates)),
'close': np.random.uniform(1800, 2000, len(dates)),
'volume': np.random.uniform(1000, 10000, len(dates)),
}, index=dates)
# Mock results
results = {
'symbol': args.symbol,
'metrics': {
'sharpe_ratio': 1.23,
'sortino_ratio': 1.67,
'max_drawdown_pct': -0.085,
'winrate': 0.525,
'profit_factor': 1.85,
'total_trades': 142,
'net_profit': 2350.00,
},
'validations': {
'sharpe_ratio': True,
'sortino_ratio': True,
'max_drawdown': True,
'win_rate': True,
'profit_factor': True,
},
'gate_passed': True,
'total_trades': 142,
}
else:
# Cargar datos reales
df_train, df_oos = runner.load_data(args.symbol)
# TODO: Aqui iria el codigo para:
# 1. Entrenar modelos con df_train
# 2. Generar senales en df_oos
# 3. Ejecutar backtest
logger.error("Real data loading requires database connection. Use --mock for testing.")
return
# Imprimir resumen
runner.print_summary(results)
# Guardar reporte
report_path = runner.generate_report(results, args.output)
print(f"\nReport saved: {report_path}")
if __name__ == '__main__':
main()