trading-platform-ml-engine-v2/scripts/llm_strategy_backtester.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

1083 lines
36 KiB
Python

#!/usr/bin/env python3
"""
LLM Strategy Backtester with Risk Management
=============================================
Sistema completo de backtesting que:
1. Usa predicciones de los modelos ML (high/low)
2. Genera informes para el agente LLM
3. Implementa gestión de riesgo (cuenta 1000 USD)
4. Backtestea estrategias del agente
5. Genera informe final de operaciones
Author: ML-Specialist + LLM-Agent (NEXUS v4.0)
Version: 1.0.0
Created: 2026-01-05
"""
import sys
import os
from pathlib import Path
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, field
from enum import Enum
import json
import numpy as np
import pandas as pd
from loguru import logger
import joblib
# Add src to path
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from config.reduced_features import generate_reduced_features, get_feature_columns_without_ohlcv
from models.volatility_attention import compute_attention_weights, VolatilityAttentionConfig
# ==============================================================================
# Enums and Data Classes
# ==============================================================================
class TradeDirection(Enum):
LONG = "LONG"
SHORT = "SHORT"
HOLD = "HOLD"
class TradeStatus(Enum):
OPEN = "OPEN"
CLOSED_TP = "CLOSED_TP"
CLOSED_SL = "CLOSED_SL"
CLOSED_SIGNAL = "CLOSED_SIGNAL"
CLOSED_TIMEOUT = "CLOSED_TIMEOUT"
@dataclass
class RiskConfig:
"""Risk management configuration for 1000 USD account"""
initial_capital: float = 1000.0
max_risk_per_trade: float = 0.02 # 2% per trade
max_daily_loss: float = 0.05 # 5% daily max loss
max_drawdown: float = 0.15 # 15% max drawdown
max_positions: int = 2 # Max concurrent positions
min_rr_ratio: float = 1.5 # Minimum Risk:Reward ratio
# Position sizing
leverage: float = 1.0 # No leverage by default
commission_pct: float = 0.0002 # 0.02% commission per trade
@dataclass
class Trade:
"""Represents a single trade"""
id: str
symbol: str
direction: TradeDirection
entry_price: float
stop_loss: float
take_profit: float
size: float
entry_time: datetime
exit_time: Optional[datetime] = None
exit_price: Optional[float] = None
status: TradeStatus = TradeStatus.OPEN
pnl: float = 0.0
pnl_pct: float = 0.0
confidence: float = 0.0
predicted_high: float = 0.0
predicted_low: float = 0.0
attention_weight: float = 1.0
notes: str = ""
def calculate_pnl(self, exit_price: float) -> float:
"""Calculate PnL for the trade"""
if self.direction == TradeDirection.LONG:
self.pnl = (exit_price - self.entry_price) * self.size
else:
self.pnl = (self.entry_price - exit_price) * self.size
self.pnl_pct = self.pnl / (self.entry_price * self.size) * 100
return self.pnl
@dataclass
class EquityPoint:
"""Point in equity curve"""
timestamp: datetime
equity: float
balance: float
drawdown: float
open_positions: int
@dataclass
class BacktestResult:
"""Results from backtesting"""
symbol: str
timeframe: str
start_date: str
end_date: str
initial_capital: float
final_capital: float
total_return: float
total_return_pct: float
total_trades: int
winning_trades: int
losing_trades: int
win_rate: float
profit_factor: float
max_drawdown: float
max_drawdown_pct: float
sharpe_ratio: float
avg_winner: float
avg_loser: float
largest_winner: float
largest_loser: float
avg_trade_duration: float
trades: List[Trade] = field(default_factory=list)
equity_curve: List[EquityPoint] = field(default_factory=list)
# ==============================================================================
# Risk Manager
# ==============================================================================
class RiskManager:
"""Manages risk for the trading account"""
def __init__(self, config: RiskConfig):
self.config = config
self.current_equity = config.initial_capital
self.daily_pnl = 0.0
self.peak_equity = config.initial_capital
self.current_drawdown = 0.0
self.open_positions: List[Trade] = []
self.daily_trades = 0
self.consecutive_losses = 0
def can_open_trade(self) -> Tuple[bool, str]:
"""Check if we can open a new trade"""
# Check max positions
if len(self.open_positions) >= self.config.max_positions:
return False, "Max positions reached"
# Check daily loss limit
daily_loss_pct = abs(self.daily_pnl) / self.config.initial_capital
if self.daily_pnl < 0 and daily_loss_pct >= self.config.max_daily_loss:
return False, f"Daily loss limit reached ({daily_loss_pct:.1%})"
# Check max drawdown
if self.current_drawdown >= self.config.max_drawdown:
return False, f"Max drawdown reached ({self.current_drawdown:.1%})"
# Check consecutive losses (circuit breaker)
if self.consecutive_losses >= 5:
return False, "Circuit breaker: 5 consecutive losses"
return True, "OK"
def calculate_position_size(
self,
entry_price: float,
stop_loss: float,
symbol: str
) -> float:
"""Calculate position size based on risk"""
risk_amount = self.current_equity * self.config.max_risk_per_trade
sl_distance = abs(entry_price - stop_loss)
if sl_distance == 0:
return 0
# Position size in units
position_size = risk_amount / sl_distance
# Apply leverage
position_size *= self.config.leverage
# Symbol-specific adjustments
if 'USD' in symbol and 'XAU' not in symbol and 'BTC' not in symbol:
# Forex: lot size
position_size = round(position_size * 10000) / 10000 # 0.01 lots
return position_size
def update_equity(self, pnl: float):
"""Update equity after trade close"""
self.current_equity += pnl
self.daily_pnl += pnl
# Update peak and drawdown
if self.current_equity > self.peak_equity:
self.peak_equity = self.current_equity
self.current_drawdown = (self.peak_equity - self.current_equity) / self.peak_equity
# Update consecutive losses
if pnl < 0:
self.consecutive_losses += 1
else:
self.consecutive_losses = 0
def reset_daily(self):
"""Reset daily counters"""
self.daily_pnl = 0.0
self.daily_trades = 0
# ==============================================================================
# Signal Generator (Using ML Predictions)
# ==============================================================================
class MLSignalGenerator:
"""Generates trading signals from ML model predictions"""
def __init__(self, model_dir: str = 'models/reduced_features_models'):
self.model_dir = Path(model_dir)
self.models = {}
self.load_models()
def load_models(self):
"""Load all available models"""
if not self.model_dir.exists():
logger.warning(f"Model directory not found: {self.model_dir}")
return
for model_file in self.model_dir.glob("*.joblib"):
if model_file.name != 'metadata.joblib':
key = model_file.stem
self.models[key] = joblib.load(model_file)
logger.info(f"Loaded model: {key}")
def get_prediction(
self,
features: pd.DataFrame,
symbol: str,
timeframe: str,
horizon: int = 3
) -> Dict[str, np.ndarray]:
"""Get predictions for a symbol/timeframe"""
key_high = f"{symbol}_{timeframe}_high_h{horizon}"
key_low = f"{symbol}_{timeframe}_low_h{horizon}"
feature_cols = get_feature_columns_without_ohlcv()
available_cols = [c for c in feature_cols if c in features.columns]
X = features[available_cols].values
predictions = {}
if key_high in self.models:
predictions['high'] = self.models[key_high].predict(X)
if key_low in self.models:
predictions['low'] = self.models[key_low].predict(X)
return predictions
def generate_signal(
self,
df: pd.DataFrame,
predictions: Dict[str, np.ndarray],
attention_weights: np.ndarray,
idx: int,
min_confidence: float = 0.6,
use_directional_filters: bool = True
) -> Tuple[TradeDirection, float, float, float, float]:
"""
Generate trading signal based on predictions WITH directional filters.
Based on backtest analysis showing 100% of winning trades were SHORT,
we implement strict filters for LONG entries and prioritize SHORT.
Returns:
Tuple of (direction, entry, stop_loss, take_profit, confidence)
"""
if 'high' not in predictions or 'low' not in predictions:
return TradeDirection.HOLD, 0, 0, 0, 0
pred_high = predictions['high'][idx]
pred_low = predictions['low'][idx]
attention = attention_weights[idx] if idx < len(attention_weights) else 1.0
current_price = df['close'].iloc[idx]
atr = df['ATR'].iloc[idx] if 'ATR' in df.columns else abs(df['high'].iloc[idx] - df['low'].iloc[idx])
# Get technical indicators for directional filters
rsi = df['RSI'].iloc[idx] if 'RSI' in df.columns else 50
sar = df['SAR'].iloc[idx] if 'SAR' in df.columns else current_price
cmf = df['CMF'].iloc[idx] if 'CMF' in df.columns else 0
mfi = df['MFI'].iloc[idx] if 'MFI' in df.columns else 50
# Calculate asymmetry ratio
asymmetry = pred_high / (pred_low + 1e-10)
# Directional confirmation scores
short_confirms = 0
long_confirms = 0
if use_directional_filters:
# SHORT confirmations
if rsi > 55: # RSI elevated
short_confirms += 1
if sar > current_price: # SAR bearish
short_confirms += 1
if cmf < 0: # Money flow negative
short_confirms += 1
if mfi > 55: # MFI elevated (selling pressure)
short_confirms += 1
# LONG confirmations (stricter requirements)
if rsi < 35: # RSI oversold
long_confirms += 1
if sar < current_price: # SAR bullish
long_confirms += 1
if cmf > 0.1: # Strong positive money flow
long_confirms += 1
if mfi < 35: # MFI oversold
long_confirms += 1
# Determine direction based on asymmetry AND directional filters
direction = TradeDirection.HOLD
confidence = 0
entry = 0
stop_loss = 0
take_profit = 0
# SHORT BIAS FIRST (based on backtest: 100% winners were SHORT)
if asymmetry < 0.85 and attention > 0.7:
if not use_directional_filters or short_confirms >= 2:
direction = TradeDirection.SHORT
entry = current_price
# Tighter stop loss using ATR
stop_loss = current_price + atr * 1.5
# Take profit based on prediction with ATR buffer
take_profit = current_price - pred_low * 0.8
# Confidence boosted by confirmations
base_conf = min(2 / asymmetry, 1.0) * min(attention, 1.5) / 1.5
conf_boost = 1.0 + (short_confirms * 0.1) if use_directional_filters else 1.0
confidence = min(base_conf * conf_boost, 1.0)
# LONG BIAS - Much stricter requirements
elif asymmetry > 1.4 and attention > 1.0: # Higher thresholds for LONG
if not use_directional_filters or long_confirms >= 3: # Need 3+ confirmations
direction = TradeDirection.LONG
entry = current_price
stop_loss = current_price - atr * 1.5
take_profit = current_price + pred_high * 0.8
base_conf = min(asymmetry / 2, 1.0) * min(attention, 1.5) / 1.5
conf_boost = 1.0 + (long_confirms * 0.1) if use_directional_filters else 1.0
confidence = min(base_conf * conf_boost, 1.0)
if direction == TradeDirection.HOLD:
return TradeDirection.HOLD, 0, 0, 0, 0
# Different minimum confidence thresholds by direction
min_conf_short = min_confidence
min_conf_long = min_confidence + 0.15 # Higher bar for LONG
required_conf = min_conf_long if direction == TradeDirection.LONG else min_conf_short
if confidence < required_conf:
return TradeDirection.HOLD, 0, 0, 0, 0
return direction, entry, stop_loss, take_profit, confidence
# ==============================================================================
# Backtester Engine
# ==============================================================================
class LLMStrategyBacktester:
"""
Backtester that simulates LLM-guided trading with ML predictions.
"""
def __init__(
self,
risk_config: RiskConfig = None,
model_dir: str = 'models/reduced_features_models'
):
self.risk_config = risk_config or RiskConfig()
self.risk_manager = RiskManager(self.risk_config)
self.signal_generator = MLSignalGenerator(model_dir)
self.trades: List[Trade] = []
self.equity_curve: List[EquityPoint] = []
def run_backtest(
self,
df: pd.DataFrame,
symbol: str,
timeframe: str,
start_date: str = None,
end_date: str = None,
min_confidence: float = 0.6,
horizon: int = 3
) -> BacktestResult:
"""
Run backtest on historical data.
Args:
df: OHLCV DataFrame
symbol: Trading symbol
timeframe: Timeframe
start_date: Start date for backtest
end_date: End date for backtest
min_confidence: Minimum signal confidence
horizon: Prediction horizon in bars
Returns:
BacktestResult with all metrics
"""
logger.info(f"\n{'='*60}")
logger.info(f"Starting Backtest: {symbol} {timeframe}")
logger.info(f"Capital: ${self.risk_config.initial_capital:,.2f}")
logger.info(f"{'='*60}")
# Generate features
features = generate_reduced_features(df)
# Get predictions
predictions = self.signal_generator.get_prediction(
features, symbol, timeframe, horizon
)
if not predictions:
logger.error("No predictions available")
return self._create_empty_result(symbol, timeframe)
# Compute attention weights
config = VolatilityAttentionConfig(factor_window=100, w_max=3.0)
attention_weights = compute_attention_weights(df, config)
# Reset state
self.trades = []
self.equity_curve = []
self.risk_manager = RiskManager(self.risk_config)
open_trades: Dict[str, Trade] = {}
trade_id = 0
# Main backtest loop
for i in range(horizon, len(df) - horizon):
current_time = df.index[i]
current_price = df['close'].iloc[i]
high_price = df['high'].iloc[i]
low_price = df['low'].iloc[i]
# Check and close existing trades
trades_to_close = []
for tid, trade in open_trades.items():
closed, exit_price, status = self._check_trade_exit(
trade, high_price, low_price, current_price, i, df, horizon
)
if closed:
trade.exit_price = exit_price
trade.exit_time = current_time
trade.status = status
trade.calculate_pnl(exit_price)
# Update risk manager
self.risk_manager.update_equity(trade.pnl)
self.risk_manager.open_positions.remove(trade)
trades_to_close.append(tid)
self.trades.append(trade)
logger.debug(f"Closed trade {tid}: {trade.status.value}, PnL: ${trade.pnl:.2f}")
for tid in trades_to_close:
del open_trades[tid]
# Generate new signal
direction, entry, sl, tp, confidence = self.signal_generator.generate_signal(
features, predictions, attention_weights, i, min_confidence
)
# Check if we can open a trade
if direction != TradeDirection.HOLD:
can_trade, reason = self.risk_manager.can_open_trade()
if can_trade:
# Calculate position size
position_size = self.risk_manager.calculate_position_size(
entry, sl, symbol
)
if position_size > 0:
# Calculate R:R ratio
risk = abs(entry - sl)
reward = abs(tp - entry)
rr_ratio = reward / risk if risk > 0 else 0
if rr_ratio >= self.risk_config.min_rr_ratio:
trade_id += 1
trade = Trade(
id=f"T{trade_id:04d}",
symbol=symbol,
direction=direction,
entry_price=entry,
stop_loss=sl,
take_profit=tp,
size=position_size,
entry_time=current_time,
confidence=confidence,
predicted_high=predictions['high'][i],
predicted_low=predictions['low'][i],
attention_weight=attention_weights[i]
)
open_trades[trade.id] = trade
self.risk_manager.open_positions.append(trade)
logger.debug(f"Opened trade {trade.id}: {direction.value} @ {entry:.2f}, "
f"SL: {sl:.2f}, TP: {tp:.2f}, Conf: {confidence:.2f}")
# Record equity point
unrealized_pnl = sum(
self._calculate_unrealized_pnl(t, current_price)
for t in open_trades.values()
)
equity = self.risk_manager.current_equity + unrealized_pnl
self.equity_curve.append(EquityPoint(
timestamp=current_time,
equity=equity,
balance=self.risk_manager.current_equity,
drawdown=self.risk_manager.current_drawdown,
open_positions=len(open_trades)
))
# Reset daily counters at day change
if i > 0 and df.index[i].date() != df.index[i-1].date():
self.risk_manager.reset_daily()
# Close any remaining trades at end
final_price = df['close'].iloc[-1]
for trade in open_trades.values():
trade.exit_price = final_price
trade.exit_time = df.index[-1]
trade.status = TradeStatus.CLOSED_TIMEOUT
trade.calculate_pnl(final_price)
self.risk_manager.update_equity(trade.pnl)
self.trades.append(trade)
# Calculate final metrics
return self._calculate_metrics(symbol, timeframe, df)
def _check_trade_exit(
self,
trade: Trade,
high: float,
low: float,
close: float,
bar_idx: int,
df: pd.DataFrame,
horizon: int
) -> Tuple[bool, float, TradeStatus]:
"""Check if trade should be closed"""
if trade.direction == TradeDirection.LONG:
# Check stop loss
if low <= trade.stop_loss:
return True, trade.stop_loss, TradeStatus.CLOSED_SL
# Check take profit
if high >= trade.take_profit:
return True, trade.take_profit, TradeStatus.CLOSED_TP
else: # SHORT
# Check stop loss
if high >= trade.stop_loss:
return True, trade.stop_loss, TradeStatus.CLOSED_SL
# Check take profit
if low <= trade.take_profit:
return True, trade.take_profit, TradeStatus.CLOSED_TP
# Check timeout (after horizon bars)
bars_open = bar_idx - df.index.get_loc(trade.entry_time)
if bars_open >= horizon * 2:
return True, close, TradeStatus.CLOSED_TIMEOUT
return False, 0, TradeStatus.OPEN
def _calculate_unrealized_pnl(self, trade: Trade, current_price: float) -> float:
"""Calculate unrealized PnL for open trade"""
if trade.direction == TradeDirection.LONG:
return (current_price - trade.entry_price) * trade.size
else:
return (trade.entry_price - current_price) * trade.size
def _calculate_metrics(
self,
symbol: str,
timeframe: str,
df: pd.DataFrame
) -> BacktestResult:
"""Calculate all backtest metrics"""
if not self.trades:
return self._create_empty_result(symbol, timeframe)
# Basic stats
total_trades = len(self.trades)
winning_trades = [t for t in self.trades if t.pnl > 0]
losing_trades = [t for t in self.trades if t.pnl < 0]
win_count = len(winning_trades)
loss_count = len(losing_trades)
win_rate = win_count / total_trades if total_trades > 0 else 0
# PnL stats
total_profit = sum(t.pnl for t in winning_trades)
total_loss = abs(sum(t.pnl for t in losing_trades))
profit_factor = total_profit / total_loss if total_loss > 0 else float('inf')
avg_winner = total_profit / win_count if win_count > 0 else 0
avg_loser = total_loss / loss_count if loss_count > 0 else 0
largest_winner = max((t.pnl for t in winning_trades), default=0)
largest_loser = min((t.pnl for t in losing_trades), default=0)
# Capital stats
final_capital = self.risk_manager.current_equity
total_return = final_capital - self.risk_config.initial_capital
total_return_pct = total_return / self.risk_config.initial_capital * 100
# Drawdown
equity_values = [e.equity for e in self.equity_curve]
if equity_values:
peak = equity_values[0]
max_dd = 0
for eq in equity_values:
if eq > peak:
peak = eq
dd = (peak - eq) / peak
max_dd = max(max_dd, dd)
else:
max_dd = 0
# Sharpe ratio (simplified)
if len(self.trades) > 1:
returns = [t.pnl_pct for t in self.trades]
avg_return = np.mean(returns)
std_return = np.std(returns)
sharpe = avg_return / std_return if std_return > 0 else 0
else:
sharpe = 0
# Average trade duration
durations = []
for t in self.trades:
if t.exit_time and t.entry_time:
duration = (t.exit_time - t.entry_time).total_seconds() / 3600
durations.append(duration)
avg_duration = np.mean(durations) if durations else 0
return BacktestResult(
symbol=symbol,
timeframe=timeframe,
start_date=str(df.index[0]),
end_date=str(df.index[-1]),
initial_capital=self.risk_config.initial_capital,
final_capital=final_capital,
total_return=total_return,
total_return_pct=total_return_pct,
total_trades=total_trades,
winning_trades=win_count,
losing_trades=loss_count,
win_rate=win_rate,
profit_factor=profit_factor,
max_drawdown=max_dd * self.risk_config.initial_capital,
max_drawdown_pct=max_dd * 100,
sharpe_ratio=sharpe,
avg_winner=avg_winner,
avg_loser=avg_loser,
largest_winner=largest_winner,
largest_loser=largest_loser,
avg_trade_duration=avg_duration,
trades=self.trades,
equity_curve=self.equity_curve
)
def _create_empty_result(self, symbol: str, timeframe: str) -> BacktestResult:
"""Create empty result when no trades"""
return BacktestResult(
symbol=symbol,
timeframe=timeframe,
start_date="",
end_date="",
initial_capital=self.risk_config.initial_capital,
final_capital=self.risk_config.initial_capital,
total_return=0,
total_return_pct=0,
total_trades=0,
winning_trades=0,
losing_trades=0,
win_rate=0,
profit_factor=0,
max_drawdown=0,
max_drawdown_pct=0,
sharpe_ratio=0,
avg_winner=0,
avg_loser=0,
largest_winner=0,
largest_loser=0,
avg_trade_duration=0
)
# ==============================================================================
# Report Generator for LLM
# ==============================================================================
class LLMReportGenerator:
"""Generates reports formatted for LLM analysis"""
@staticmethod
def generate_prediction_report(
results: List[BacktestResult]
) -> str:
"""Generate comprehensive report for LLM to analyze"""
report = """# INFORME DE PREDICCIONES ML PARA ESTRATEGIA DE TRADING
## Resumen Ejecutivo
Este informe contiene los resultados del backtesting de los modelos ML
para los 3 activos principales. El objetivo es que el agente LLM analice
estos datos y genere una estrategia optimizada.
## Configuración del Backtest
- **Capital Inicial:** $1,000.00 USD
- **Riesgo por Operación:** 2%
- **Máximo Drawdown Permitido:** 15%
- **Posiciones Simultáneas:** Máximo 2
- **Ratio Riesgo:Beneficio Mínimo:** 1.5:1
---
## Resultados por Activo
"""
for result in results:
report += f"""
### {result.symbol} - {result.timeframe}
| Métrica | Valor |
|---------|-------|
| Capital Final | ${result.final_capital:,.2f} |
| Retorno Total | {result.total_return_pct:+.2f}% |
| Total Trades | {result.total_trades} |
| Trades Ganadores | {result.winning_trades} |
| Trades Perdedores | {result.losing_trades} |
| Win Rate | {result.win_rate:.1%} |
| Profit Factor | {result.profit_factor:.2f} |
| Max Drawdown | {result.max_drawdown_pct:.1f}% |
| Sharpe Ratio | {result.sharpe_ratio:.2f} |
| Promedio Ganador | ${result.avg_winner:.2f} |
| Promedio Perdedor | ${result.avg_loser:.2f} |
| Mayor Ganancia | ${result.largest_winner:.2f} |
| Mayor Pérdida | ${result.largest_loser:.2f} |
| Duración Promedio | {result.avg_trade_duration:.1f} horas |
"""
# Summary statistics
total_trades = sum(r.total_trades for r in results)
total_winners = sum(r.winning_trades for r in results)
overall_win_rate = total_winners / total_trades if total_trades > 0 else 0
combined_return = sum(r.total_return for r in results)
combined_return_pct = combined_return / 1000 * 100 # Assuming single 1000 USD
report += f"""
---
## Resumen Consolidado
| Métrica | Valor |
|---------|-------|
| Total Operaciones | {total_trades} |
| Win Rate Global | {overall_win_rate:.1%} |
| Retorno Combinado | ${combined_return:,.2f} ({combined_return_pct:+.2f}%) |
---
## Análisis por Activo
"""
# Rank assets by performance
ranked = sorted(results, key=lambda x: x.total_return_pct, reverse=True)
report += "### Ranking de Activos (por Retorno)\n\n"
for i, r in enumerate(ranked, 1):
status = "OPERAR" if r.total_return_pct > 0 and r.win_rate > 0.4 else "PRECAUCION" if r.total_return_pct > -5 else "EVITAR"
report += f"{i}. **{r.symbol}**: {r.total_return_pct:+.2f}% - {status}\n"
report += """
---
## Recomendaciones para el Agente LLM
Basándose en estos resultados, el agente LLM debe:
1. **Priorizar activos rentables** en las decisiones de trading
2. **Ajustar tamaño de posición** según el win rate histórico
3. **Aplicar gestión de riesgo estricta** especialmente en activos con alto drawdown
4. **Considerar la volatilidad** (attention weights) en las decisiones
---
## Datos para Fine-Tuning
Los siguientes patrones fueron exitosos:
"""
for r in results:
if r.trades:
winning = [t for t in r.trades if t.pnl > 0]
if winning:
avg_confidence = np.mean([t.confidence for t in winning])
avg_attention = np.mean([t.attention_weight for t in winning])
report += f"""
### {r.symbol} - Patrones Exitosos
- Confianza promedio en ganadores: {avg_confidence:.2f}
- Attention weight promedio: {avg_attention:.2f}
- Direcciones ganadoras: {sum(1 for t in winning if t.direction == TradeDirection.LONG)} LONG, {sum(1 for t in winning if t.direction == TradeDirection.SHORT)} SHORT
"""
return report
@staticmethod
def generate_trade_log(results: List[BacktestResult]) -> str:
"""Generate detailed trade log"""
log = "# LOG DETALLADO DE OPERACIONES\n\n"
for result in results:
log += f"## {result.symbol} - {result.timeframe}\n\n"
log += "| ID | Dirección | Entrada | SL | TP | Salida | PnL | Estado | Confianza |\n"
log += "|-----|-----------|---------|-----|-----|--------|-----|--------|----------|\n"
for trade in result.trades[:50]: # Limit to 50 trades per symbol
exit_price_str = f"{trade.exit_price:.4f}" if trade.exit_price else "N/A"
log += f"| {trade.id} | {trade.direction.value} | {trade.entry_price:.4f} | "
log += f"{trade.stop_loss:.4f} | {trade.take_profit:.4f} | "
log += f"{exit_price_str} | "
log += f"${trade.pnl:+.2f} | {trade.status.value} | {trade.confidence:.2f} |\n"
log += "\n"
return log
@staticmethod
def save_reports(
results: List[BacktestResult],
output_dir: str = 'reports'
):
"""Save all reports to files"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Save prediction report
report = LLMReportGenerator.generate_prediction_report(results)
report_file = output_path / f"prediction_report_{timestamp}.md"
with open(report_file, 'w') as f:
f.write(report)
logger.info(f"Saved prediction report to {report_file}")
# Save trade log
trade_log = LLMReportGenerator.generate_trade_log(results)
log_file = output_path / f"trade_log_{timestamp}.md"
with open(log_file, 'w') as f:
f.write(trade_log)
logger.info(f"Saved trade log to {log_file}")
# Save JSON results
results_dict = []
for r in results:
results_dict.append({
'symbol': r.symbol,
'timeframe': r.timeframe,
'start_date': r.start_date,
'end_date': r.end_date,
'initial_capital': r.initial_capital,
'final_capital': r.final_capital,
'total_return': r.total_return,
'total_return_pct': r.total_return_pct,
'total_trades': r.total_trades,
'winning_trades': r.winning_trades,
'losing_trades': r.losing_trades,
'win_rate': r.win_rate,
'profit_factor': r.profit_factor,
'max_drawdown_pct': r.max_drawdown_pct,
'sharpe_ratio': r.sharpe_ratio,
'avg_winner': r.avg_winner,
'avg_loser': r.avg_loser
})
json_file = output_path / f"backtest_results_{timestamp}.json"
with open(json_file, 'w') as f:
json.dump(results_dict, f, indent=2, default=str)
logger.info(f"Saved JSON results to {json_file}")
return report_file, log_file, json_file
# ==============================================================================
# Main Execution
# ==============================================================================
def load_data_for_backtest(
symbol: str,
start_date: str = '2025-01-01',
end_date: str = '2025-01-31',
timeframe: str = '15m'
) -> pd.DataFrame:
"""Load data for backtesting"""
try:
sys.path.insert(0, str(Path(__file__).parent.parent / 'src'))
from data.database import MySQLConnection
db = MySQLConnection('config/database.yaml')
# Get DB prefix
prefixes = {'XAUUSD': 'C:', 'EURUSD': 'C:', 'BTCUSD': 'X:'}
db_symbol = f"{prefixes.get(symbol, 'C:')}{symbol}"
query = """
SELECT date_agg as time, open, high, low, close, volume
FROM tickers_agg_data
WHERE ticker = :symbol
AND date_agg >= :start_date
AND date_agg <= :end_date
ORDER BY date_agg ASC
"""
df = db.execute_query(query, {
'symbol': db_symbol,
'start_date': start_date,
'end_date': end_date
})
if df.empty:
logger.warning(f"No data for {symbol}")
return df
df['time'] = pd.to_datetime(df['time'])
df.set_index('time', inplace=True)
# Resample if needed
if timeframe != '5m':
tf_map = {'15m': '15min', '30m': '30min', '1H': '1H'}
offset = tf_map.get(timeframe, timeframe)
df = df.resample(offset).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
logger.info(f"Loaded {len(df)} records for {symbol} {timeframe}")
return df
except Exception as e:
logger.error(f"Failed to load data: {e}")
return pd.DataFrame()
def run_full_backtest():
"""Run full backtest for all symbols"""
logger.info("=" * 60)
logger.info("LLM STRATEGY BACKTESTER")
logger.info("Account: $1,000 USD")
logger.info("=" * 60)
# Configuration
risk_config = RiskConfig(
initial_capital=1000.0,
max_risk_per_trade=0.02,
max_daily_loss=0.05,
max_drawdown=0.15,
max_positions=2,
min_rr_ratio=1.5
)
backtester = LLMStrategyBacktester(risk_config)
symbols = ['XAUUSD', 'EURUSD', 'BTCUSD']
timeframes = ['5m', '15m']
all_results = []
for symbol in symbols:
for timeframe in timeframes:
logger.info(f"\nBacktesting {symbol} {timeframe}...")
# Load data
df = load_data_for_backtest(
symbol,
start_date='2025-01-01',
end_date='2025-01-31',
timeframe=timeframe
)
if df.empty:
logger.warning(f"Skipping {symbol} {timeframe} - no data")
continue
# Run backtest
result = backtester.run_backtest(
df, symbol, timeframe,
min_confidence=0.5,
horizon=3
)
all_results.append(result)
logger.info(f" Trades: {result.total_trades}")
logger.info(f" Return: {result.total_return_pct:+.2f}%")
logger.info(f" Win Rate: {result.win_rate:.1%}")
# Generate reports
logger.info("\nGenerating reports...")
report_file, log_file, json_file = LLMReportGenerator.save_reports(all_results)
# Print summary
print("\n" + "=" * 60)
print("BACKTEST SUMMARY")
print("=" * 60)
for r in all_results:
print(f"\n{r.symbol} {r.timeframe}:")
print(f" Capital: ${r.initial_capital:,.2f} -> ${r.final_capital:,.2f}")
print(f" Return: {r.total_return_pct:+.2f}%")
print(f" Trades: {r.total_trades} (Win: {r.winning_trades}, Loss: {r.losing_trades})")
print(f" Win Rate: {r.win_rate:.1%}")
print(f" Max Drawdown: {r.max_drawdown_pct:.1f}%")
print("\n" + "=" * 60)
print("Reports saved to:")
print(f" - {report_file}")
print(f" - {log_file}")
print(f" - {json_file}")
print("=" * 60)
return all_results
if __name__ == "__main__":
# Setup logging
logger.remove()
logger.add(sys.stderr, level="INFO", format="{time:HH:mm:ss} | {level} | {message}")
# Run backtest
results = run_full_backtest()