201 lines
5.9 KiB
Python
201 lines
5.9 KiB
Python
"""
|
|
Backtesting Service.
|
|
|
|
This module provides the service layer for backtesting operations,
|
|
integrating with the database to run backtesting on historical matches.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from typing import Dict, List, Any, Optional
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models.match import Match
|
|
from app.models.energy_score import EnergyScore
|
|
from app.ml.backtesting import (
|
|
run_backtesting_batch,
|
|
export_to_json,
|
|
export_to_csv,
|
|
export_to_html,
|
|
filter_matches_by_league,
|
|
filter_matches_by_period
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BacktestingService:
|
|
"""Service for running backtesting on historical match data."""
|
|
|
|
def __init__(self, db: Session):
|
|
"""
|
|
Initialize backtesting service.
|
|
|
|
Args:
|
|
db: SQLAlchemy database session
|
|
"""
|
|
self.db = db
|
|
|
|
def get_historical_matches(
|
|
self,
|
|
leagues: Optional[List[str]] = None,
|
|
start_date: Optional[datetime] = None,
|
|
end_date: Optional[datetime] = None
|
|
) -> List[Dict[str, Any]]:
|
|
"""
|
|
Retrieve historical matches with energy scores and actual results.
|
|
|
|
Args:
|
|
leagues: Optional list of leagues to filter by
|
|
start_date: Optional start date for filtering
|
|
end_date: Optional end date for filtering
|
|
|
|
Returns:
|
|
List of match dictionaries with energy scores and actual winners
|
|
|
|
Raises:
|
|
ValueError: If no historical matches found
|
|
"""
|
|
logger.info("Fetching historical matches from database")
|
|
|
|
# Query matches that have actual_winner set (completed matches)
|
|
query = self.db.query(Match).filter(
|
|
Match.actual_winner.isnot(None)
|
|
)
|
|
|
|
# Apply filters
|
|
if leagues:
|
|
query = query.filter(Match.league.in_(leagues))
|
|
|
|
if start_date:
|
|
query = query.filter(Match.date >= start_date)
|
|
|
|
if end_date:
|
|
query = query.filter(Match.date <= end_date)
|
|
|
|
matches = query.all()
|
|
|
|
if not matches:
|
|
raise ValueError(
|
|
"No historical matches found. Please populate database with "
|
|
"historical match data and actual winners before running backtesting."
|
|
)
|
|
|
|
logger.info(f"Found {len(matches)} historical matches")
|
|
|
|
# Convert to list of dictionaries with energy scores
|
|
match_data = []
|
|
for match in matches:
|
|
# Get energy scores for this match
|
|
home_energy_score = self.db.query(EnergyScore).filter(
|
|
EnergyScore.match_id == match.id
|
|
).first()
|
|
|
|
if not home_energy_score:
|
|
logger.warning(f"No energy score found for match {match.id}, skipping")
|
|
continue
|
|
|
|
match_dict = {
|
|
'match_id': match.id,
|
|
'home_team': match.home_team,
|
|
'away_team': match.away_team,
|
|
'date': match.date,
|
|
'league': match.league,
|
|
'home_energy': home_energy_score.home_energy,
|
|
'away_energy': home_energy_score.away_energy,
|
|
'actual_winner': match.actual_winner
|
|
}
|
|
|
|
match_data.append(match_dict)
|
|
|
|
logger.info(f"Processed {len(match_data)} matches with energy scores")
|
|
|
|
return match_data
|
|
|
|
def run_backtesting(
|
|
self,
|
|
leagues: Optional[List[str]] = None,
|
|
start_date: Optional[datetime] = None,
|
|
end_date: Optional[datetime] = None
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Run backtesting on historical matches with optional filters.
|
|
|
|
Args:
|
|
leagues: Optional list of leagues to filter by
|
|
start_date: Optional start date for filtering
|
|
end_date: Optional end date for filtering
|
|
|
|
Returns:
|
|
Dictionary containing backtesting results
|
|
|
|
Raises:
|
|
ValueError: If no matches found or matches lack required data
|
|
"""
|
|
logger.info("Starting backtesting process")
|
|
|
|
# Get historical matches
|
|
matches = self.get_historical_matches(
|
|
leagues=leagues,
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
# Apply filters
|
|
if leagues:
|
|
matches = filter_matches_by_league(matches, leagues)
|
|
|
|
if start_date or end_date:
|
|
matches = filter_matches_by_period(matches, start_date, end_date)
|
|
|
|
if not matches:
|
|
raise ValueError("No matches match the specified filters")
|
|
|
|
logger.info(f"Running backtesting on {len(matches)} matches")
|
|
|
|
# Run backtesting
|
|
result = run_backtesting_batch(matches)
|
|
|
|
# Log results
|
|
logger.info(
|
|
f"Backtesting complete: {result['total_matches']} matches, "
|
|
f"{result['correct_predictions']} correct, "
|
|
f"{result['accuracy']:.2f}% accuracy, "
|
|
f"status: {result['status']}"
|
|
)
|
|
|
|
return result
|
|
|
|
def export_results(
|
|
self,
|
|
backtesting_result: Dict[str, Any],
|
|
format: str = 'json'
|
|
) -> str:
|
|
"""
|
|
Export backtesting results in specified format.
|
|
|
|
Args:
|
|
backtesting_result: Result from run_backtesting
|
|
format: Export format ('json', 'csv', or 'html')
|
|
|
|
Returns:
|
|
Formatted string in specified format
|
|
|
|
Raises:
|
|
ValueError: If format is not supported
|
|
"""
|
|
logger.info(f"Exporting backtesting results as {format}")
|
|
|
|
if format == 'json':
|
|
return export_to_json(backtesting_result)
|
|
elif format == 'csv':
|
|
return export_to_csv(backtesting_result)
|
|
elif format == 'html':
|
|
return export_to_html(backtesting_result)
|
|
else:
|
|
raise ValueError(
|
|
f"Unsupported export format: {format}. "
|
|
"Supported formats are: json, csv, html"
|
|
)
|