TCPDashboard/database/repositories/strategy_repository.py
Vasily.onl 8c23489ff0 4.0 - 4.0 Implement real-time strategy execution and data integration features
- Added `realtime_execution.py` for real-time strategy execution, enabling live signal generation and integration with the dashboard's chart refresh cycle.
- Introduced `data_integration.py` to manage market data orchestration, caching, and technical indicator calculations for strategy signal generation.
- Implemented `validation.py` for comprehensive validation and quality assessment of strategy-generated signals, ensuring reliability and consistency.
- Developed `batch_processing.py` to facilitate efficient backtesting of multiple strategies across large datasets with memory management and performance optimization.
- Updated `__init__.py` files to include new modules and ensure proper exports, enhancing modularity and maintainability.
- Enhanced unit tests for the new features, ensuring robust functionality and adherence to project standards.

These changes establish a solid foundation for real-time strategy execution and data integration, aligning with project goals for modularity, performance, and maintainability.
2025-06-12 18:29:39 +08:00

289 lines
12 KiB
Python

"""Repository for strategy_signals and strategy_runs table operations."""
from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List
from decimal import Decimal
from sqlalchemy import desc, and_, func
from sqlalchemy.orm import joinedload
from ..models import StrategySignal, StrategyRun
from strategies.data_types import StrategySignal as StrategySignalDataType, StrategyResult
from .base_repository import BaseRepository, DatabaseOperationError
class StrategyRepository(BaseRepository):
"""Repository for strategy_signals and strategy_runs table operations."""
# Strategy Run Operations
def create_strategy_run(self, run_data: Dict[str, Any]) -> StrategyRun:
"""
Create a new strategy run session.
Args:
run_data: Dictionary containing run information (strategy_name, symbol, timeframe, etc.)
Returns:
The newly created StrategyRun object
"""
try:
with self.get_session() as session:
new_run = StrategyRun(**run_data)
session.add(new_run)
session.commit()
session.refresh(new_run)
self.log_info(f"Created strategy run: {new_run.strategy_name} for {new_run.symbol}")
return new_run
except Exception as e:
self.log_error(f"Error creating strategy run: {e}")
raise DatabaseOperationError(f"Failed to create strategy run: {e}")
def get_strategy_run_by_id(self, run_id: int) -> Optional[StrategyRun]:
"""Get a strategy run by its ID."""
try:
with self.get_session() as session:
return session.query(StrategyRun).filter(StrategyRun.id == run_id).first()
except Exception as e:
self.log_error(f"Error getting strategy run by ID {run_id}: {e}")
raise DatabaseOperationError(f"Failed to get strategy run by ID: {e}")
def update_strategy_run(self, run_id: int, update_data: Dict[str, Any]) -> Optional[StrategyRun]:
"""Update a strategy run's information."""
try:
with self.get_session() as session:
strategy_run = session.query(StrategyRun).filter(StrategyRun.id == run_id).first()
if strategy_run:
for key, value in update_data.items():
setattr(strategy_run, key, value)
session.commit()
session.refresh(strategy_run)
self.log_info(f"Updated strategy run {run_id}")
return strategy_run
return None
except Exception as e:
self.log_error(f"Error updating strategy run {run_id}: {e}")
raise DatabaseOperationError(f"Failed to update strategy run: {e}")
def complete_strategy_run(self, run_id: int, total_signals: int) -> bool:
"""Mark a strategy run as completed."""
try:
update_data = {
'status': 'completed',
'end_time': datetime.now(datetime.timezone.utc),
'total_signals': total_signals
}
result = self.update_strategy_run(run_id, update_data)
return result is not None
except Exception as e:
self.log_error(f"Error completing strategy run {run_id}: {e}")
return False
# Strategy Signal Operations
def store_strategy_signals(self, run_id: int, strategy_results: List[StrategyResult]) -> int:
"""
Store multiple strategy signals from strategy results.
Args:
run_id: The strategy run ID these signals belong to
strategy_results: List of StrategyResult objects containing signals
Returns:
Number of signals stored
"""
try:
signals_stored = 0
with self.get_session() as session:
for result in strategy_results:
for signal in result.signals:
strategy_signal = StrategySignal(
run_id=run_id,
strategy_name=result.strategy_name,
strategy_config=None, # Could be populated from StrategyRun.config
symbol=signal.symbol,
timeframe=signal.timeframe,
timestamp=signal.timestamp,
signal_type=signal.signal_type.value,
price=Decimal(str(signal.price)),
confidence=Decimal(str(signal.confidence)),
signal_metadata={
'indicators_used': result.indicators_used,
'metadata': signal.metadata or {}
}
)
session.add(strategy_signal)
signals_stored += 1
session.commit()
self.log_info(f"Stored {signals_stored} strategy signals for run {run_id}")
return signals_stored
except Exception as e:
self.log_error(f"Error storing strategy signals for run {run_id}: {e}")
raise DatabaseOperationError(f"Failed to store strategy signals: {e}")
def get_strategy_signals(
self,
run_id: Optional[int] = None,
strategy_name: Optional[str] = None,
symbol: Optional[str] = None,
timeframe: Optional[str] = None,
start_time: Optional[datetime] = None,
end_time: Optional[datetime] = None,
signal_type: Optional[str] = None,
limit: Optional[int] = None
) -> List[StrategySignal]:
"""
Retrieve strategy signals with flexible filtering.
Args:
run_id: Filter by strategy run ID
strategy_name: Filter by strategy name
symbol: Filter by trading symbol
timeframe: Filter by timeframe
start_time: Filter signals after this time
end_time: Filter signals before this time
signal_type: Filter by signal type
limit: Maximum number of signals to return
Returns:
List of StrategySignal objects
"""
try:
with self.get_session() as session:
query = session.query(StrategySignal)
# Apply filters
if run_id is not None:
query = query.filter(StrategySignal.run_id == run_id)
if strategy_name:
query = query.filter(StrategySignal.strategy_name == strategy_name)
if symbol:
query = query.filter(StrategySignal.symbol == symbol)
if timeframe:
query = query.filter(StrategySignal.timeframe == timeframe)
if start_time:
query = query.filter(StrategySignal.timestamp >= start_time)
if end_time:
query = query.filter(StrategySignal.timestamp <= end_time)
if signal_type:
query = query.filter(StrategySignal.signal_type == signal_type)
# Order by timestamp descending
query = query.order_by(desc(StrategySignal.timestamp))
# Apply limit
if limit:
query = query.limit(limit)
return query.all()
except Exception as e:
self.log_error(f"Error retrieving strategy signals: {e}")
raise DatabaseOperationError(f"Failed to retrieve strategy signals: {e}")
def store_signals_batch(self, signal_data_list: List[Dict[str, Any]]) -> int:
"""
Store a batch of real-time strategy signals.
Args:
signal_data_list: List of signal data dictionaries
Returns:
Number of signals stored
"""
try:
signals_stored = 0
with self.get_session() as session:
for signal_data in signal_data_list:
strategy_signal = StrategySignal(
run_id=None, # Real-time signals don't have a run_id
strategy_name=signal_data.get('strategy_name'),
strategy_config=signal_data.get('strategy_config'),
symbol=signal_data.get('symbol'),
timeframe=signal_data.get('timeframe'),
timestamp=signal_data.get('timestamp'),
signal_type=signal_data.get('signal_type', 'HOLD'),
price=Decimal(str(signal_data.get('price'))) if signal_data.get('price') else None,
confidence=Decimal(str(signal_data.get('confidence', 0.0))),
signal_metadata=signal_data.get('signal_metadata', {})
)
session.add(strategy_signal)
signals_stored += 1
session.commit()
self.log_info(f"Stored batch of {signals_stored} real-time strategy signals")
return signals_stored
except Exception as e:
self.log_error(f"Error storing signals batch: {e}")
raise DatabaseOperationError(f"Failed to store signals batch: {e}")
def get_strategy_signal_stats(self, run_id: Optional[int] = None) -> Dict[str, Any]:
"""Get statistics about strategy signals."""
try:
with self.get_session() as session:
query = session.query(StrategySignal)
if run_id is not None:
query = query.filter(StrategySignal.run_id == run_id)
# Get basic counts by signal type
signal_counts = session.query(
StrategySignal.signal_type,
func.count(StrategySignal.id).label('count')
).group_by(StrategySignal.signal_type)
if run_id is not None:
signal_counts = signal_counts.filter(StrategySignal.run_id == run_id)
counts_dict = {signal_type: count for signal_type, count in signal_counts.all()}
# Get total signals
total_signals = query.count()
# Get average confidence
avg_confidence = session.query(func.avg(StrategySignal.confidence)).scalar()
return {
'total_signals': total_signals,
'signal_counts': counts_dict,
'average_confidence': float(avg_confidence) if avg_confidence else 0.0,
'run_id': run_id
}
except Exception as e:
self.log_error(f"Error getting strategy signal stats: {e}")
raise DatabaseOperationError(f"Failed to get strategy signal stats: {e}")
# Data Retention and Cleanup
def cleanup_old_strategy_data(self, days_to_keep: int = 30) -> Dict[str, int]:
"""
Clean up old strategy signals and runs to prevent table bloat.
Args:
days_to_keep: Number of days to retain data
Returns:
Dictionary with counts of deleted records
"""
try:
cutoff_date = datetime.now(datetime.timezone.utc) - timedelta(days=days_to_keep)
with self.get_session() as session:
# Delete old strategy runs (and their signals via CASCADE)
deleted_runs = session.query(StrategyRun).filter(
StrategyRun.created_at < cutoff_date,
StrategyRun.status == 'completed' # Only delete completed runs
).delete(synchronize_session=False)
session.commit()
self.log_info(f"Cleaned up {deleted_runs} old strategy runs and their signals")
return {
'deleted_runs': deleted_runs,
'cutoff_date': cutoff_date.isoformat()
}
except Exception as e:
self.log_error(f"Error cleaning up old strategy data: {e}")
raise DatabaseOperationError(f"Failed to cleanup old strategy data: {e}")