4.0 - 3.0 Implement strategy analysis tables and repository for backtesting
- Added `StrategyRun` and `StrategySignal` models to track strategy execution sessions and generated signals, respectively, ensuring a clear separation from live trading data. - Introduced `StrategyRepository` for managing database operations related to strategy runs and signals, including methods for creating, updating, and retrieving strategy data. - Updated `DatabaseOperations` to integrate the new repository, enhancing the overall architecture and maintaining consistency with existing database access patterns. - Enhanced documentation to reflect the new database schema and repository functionalities, ensuring clarity for future development and usage. These changes establish a robust foundation for strategy analysis and backtesting, aligning with project goals for modularity, performance, and maintainability.
This commit is contained in:
parent
d34da789ec
commit
f09864d61b
@ -241,6 +241,72 @@ class BotPerformance(Base):
|
||||
return f"<BotPerformance(Bot {self.bot_id} - Value: {self.total_value} - Win Rate: {self.win_rate:.2f}%)>"
|
||||
|
||||
|
||||
class StrategyRun(Base):
|
||||
"""Strategy Execution Sessions - For tracking strategy backtesting and analysis runs"""
|
||||
__tablename__ = 'strategy_runs'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
strategy_name = Column(String(100), nullable=False)
|
||||
symbol = Column(String(20), nullable=False)
|
||||
timeframe = Column(String(5), nullable=False)
|
||||
start_time = Column(DateTime(timezone=True), nullable=False)
|
||||
end_time = Column(DateTime(timezone=True))
|
||||
status = Column(String(20), nullable=False, default='running') # running, completed, failed
|
||||
config = Column(JSONB) # Strategy configuration parameters
|
||||
run_metadata = Column(JSONB) # Run metadata (backtesting params, etc.)
|
||||
total_signals = Column(Integer, default=0)
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
strategy_signals = relationship("StrategySignal", back_populates="strategy_run", cascade="all, delete-orphan")
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("status IN ('running', 'completed', 'failed')", name='chk_strategy_run_status'),
|
||||
Index('idx_strategy_runs_strategy_time', 'strategy_name', 'start_time'),
|
||||
Index('idx_strategy_runs_symbol', 'symbol'),
|
||||
Index('idx_strategy_runs_status', 'status'),
|
||||
Index('idx_strategy_runs_timeframe', 'timeframe'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StrategyRun({self.strategy_name} - {self.symbol} - {self.status})>"
|
||||
|
||||
|
||||
class StrategySignal(Base):
|
||||
"""Strategy Analysis Signals - Generated by strategies for analysis and backtesting (separate from bot signals)"""
|
||||
__tablename__ = 'strategy_signals'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
run_id = Column(Integer, ForeignKey('strategy_runs.id', ondelete='CASCADE'), nullable=False)
|
||||
strategy_name = Column(String(100), nullable=False)
|
||||
strategy_config = Column(JSONB) # Strategy configuration used for this signal
|
||||
symbol = Column(String(20), nullable=False)
|
||||
timeframe = Column(String(5), nullable=False)
|
||||
timestamp = Column(DateTime(timezone=True), nullable=False)
|
||||
signal_type = Column(String(20), nullable=False) # buy, sell, hold, entry_long, exit_long, etc.
|
||||
price = Column(DECIMAL(18, 8))
|
||||
confidence = Column(DECIMAL(5, 4)) # 0.0000 to 1.0000
|
||||
signal_metadata = Column(JSONB) # Additional signal metadata (indicator values, etc.)
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
|
||||
# Relationships
|
||||
strategy_run = relationship("StrategyRun", back_populates="strategy_signals")
|
||||
|
||||
__table_args__ = (
|
||||
CheckConstraint("signal_type IN ('buy', 'sell', 'hold', 'entry_long', 'exit_long', 'entry_short', 'exit_short', 'stop_loss', 'take_profit')", name='chk_strategy_signal_type'),
|
||||
CheckConstraint("confidence >= 0 AND confidence <= 1", name='chk_strategy_confidence'),
|
||||
Index('idx_strategy_signals_strategy_time', 'strategy_name', 'timestamp'),
|
||||
Index('idx_strategy_signals_run_time', 'run_id', 'timestamp'),
|
||||
Index('idx_strategy_signals_symbol_timeframe', 'symbol', 'timeframe'),
|
||||
Index('idx_strategy_signals_type', 'signal_type'),
|
||||
Index('idx_strategy_signals_timestamp', 'timestamp'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StrategySignal({self.strategy_name} - {self.signal_type} - {self.price} - {self.confidence})>"
|
||||
|
||||
|
||||
# Reference tables for system configuration
|
||||
class SupportedTimeframe(Base):
|
||||
"""Supported timeframes configuration"""
|
||||
@ -277,6 +343,8 @@ def get_model_by_table_name(table_name: str):
|
||||
'signals': Signal,
|
||||
'trades': Trade,
|
||||
'bot_performance': BotPerformance,
|
||||
'strategy_runs': StrategyRun,
|
||||
'strategy_signals': StrategySignal,
|
||||
'supported_timeframes': SupportedTimeframe,
|
||||
'supported_exchanges': SupportedExchange,
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ from .repositories import (
|
||||
BotRepository,
|
||||
MarketDataRepository,
|
||||
RawTradeRepository,
|
||||
StrategyRepository,
|
||||
DatabaseOperationError,
|
||||
)
|
||||
|
||||
@ -33,6 +34,7 @@ class DatabaseOperations:
|
||||
self.bots = BotRepository(logger)
|
||||
self.market_data = MarketDataRepository(logger)
|
||||
self.raw_trades = RawTradeRepository(logger)
|
||||
self.strategies = StrategyRepository(logger)
|
||||
|
||||
def health_check(self) -> bool:
|
||||
"""
|
||||
@ -64,7 +66,8 @@ class DatabaseOperations:
|
||||
'repositories': {
|
||||
'bots': 'BotRepository',
|
||||
'market_data': 'MarketDataRepository',
|
||||
'raw_trades': 'RawTradeRepository'
|
||||
'raw_trades': 'RawTradeRepository',
|
||||
'strategies': 'StrategyRepository'
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,6 +76,8 @@ class DatabaseOperations:
|
||||
stats['bot_count'] = session.execute(text("SELECT COUNT(*) FROM bots")).scalar_one()
|
||||
stats['candle_count'] = session.execute(text("SELECT COUNT(*) FROM market_data")).scalar_one()
|
||||
stats['raw_trade_count'] = session.execute(text("SELECT COUNT(*) FROM raw_trades")).scalar_one()
|
||||
stats['strategy_runs_count'] = session.execute(text("SELECT COUNT(*) FROM strategy_runs")).scalar_one()
|
||||
stats['strategy_signals_count'] = session.execute(text("SELECT COUNT(*) FROM strategy_signals")).scalar_one()
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@ from .base_repository import BaseRepository, DatabaseOperationError
|
||||
from .bot_repository import BotRepository
|
||||
from .market_data_repository import MarketDataRepository
|
||||
from .raw_trade_repository import RawTradeRepository
|
||||
from .strategy_repository import StrategyRepository
|
||||
|
||||
__all__ = [
|
||||
"BaseRepository",
|
||||
@ -12,4 +13,5 @@ __all__ = [
|
||||
"BotRepository",
|
||||
"MarketDataRepository",
|
||||
"RawTradeRepository",
|
||||
"StrategyRepository",
|
||||
]
|
||||
252
database/repositories/strategy_repository.py
Normal file
252
database/repositories/strategy_repository.py
Normal file
@ -0,0 +1,252 @@
|
||||
"""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 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}")
|
||||
@ -65,12 +65,13 @@ class BotManager:
|
||||
```
|
||||
|
||||
### 4. Database
|
||||
**Responsibility**: Data persistence and storage
|
||||
**Responsibility**: Data persistence and storage for market data, bot configurations, trading signals, and strategy analysis results.
|
||||
**Implementation**: `database/`
|
||||
**Key Features**:
|
||||
- PostgreSQL with TimescaleDB extension for time-series data
|
||||
- SQLAlchemy for ORM and schema management
|
||||
- Alembic for database migrations
|
||||
- Dedicated tables for strategy backtesting (e.g., `strategy_signals`, `strategy_runs`) separate from live trading `signals`.
|
||||
- See [Database Operations Documentation (`modules/database_operations.md`)](./modules/database_operations.md) for details.
|
||||
|
||||
### 5. Backtesting Engine
|
||||
|
||||
@ -27,23 +27,23 @@ The Database Operations module (`database/operations.py`) provides a clean, cent
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DatabaseOperations │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Health Check & Stats │ │
|
||||
│ │ • Connection health monitoring │ │
|
||||
│ │ • Database statistics │ │
|
||||
│ │ • Performance metrics │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
|
||||
│ │MarketDataRepo │ │RawTradeRepo │ │ BotRepo │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ • upsert_candle │ │ • insert_data │ │ • add │ │
|
||||
│ │ • get_candles │ │ • get_trades │ │ • get_by_id │ │
|
||||
│ │ • get_latest │ │ • raw_websocket │ │ • update/delete│ │
|
||||
│ └─────────────────┘ └─────────────────┘ └──────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌───────────────────────────────────────────────────────────────────┐
|
||||
│ DatabaseOperations │
|
||||
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||
│ │ Health Check & Stats │ │
|
||||
│ │ • Connection health monitoring │ │
|
||||
│ │ • Database statistics │ │
|
||||
│ │ • Performance metrics │ │
|
||||
│ └───────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ ┌───────────────────┐ │
|
||||
│ │MarketDataRepo │ │RawTradeRepo │ │ BotRepo │ │ StrategyRepo │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ • upsert_candle │ │ • insert_data │ │ • add │ │ • create_run │ │
|
||||
│ │ • get_candles │ │ • get_trades │ │ • get_by_id │ │ • store_signals │ │
|
||||
│ │ • get_latest │ │ • raw_websocket │ │ • update/delete│ │ • get_signals/stats │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └──────────────┘ └───────────────────┘ │
|
||||
└───────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────┐
|
||||
│ BaseRepository │
|
||||
@ -152,6 +152,8 @@ stats = db.get_stats()
|
||||
print(f"Bots: {stats['bot_count']:,}")
|
||||
print(f"Candles: {stats['candle_count']:,}")
|
||||
print(f"Raw trades: {stats['raw_trade_count']:,}")
|
||||
print(f"Strategy runs: {stats['strategy_runs_count']:,}")
|
||||
print(f"Strategy signals: {stats['strategy_signals_count']:,}")
|
||||
print(f"Health: {stats['healthy']}")
|
||||
```
|
||||
|
||||
@ -369,6 +371,166 @@ print(f"Raw Trades Table Size: {raw_stats.get('table_size')}")
|
||||
print(f"Total Raw Records: {raw_stats.get('total_records')}")
|
||||
```
|
||||
|
||||
### StrategyRepository
|
||||
|
||||
Repository for `strategy_signals` and `strategy_runs` table operations.
|
||||
|
||||
#### Methods
|
||||
|
||||
##### `create_strategy_run(run_data: Dict[str, Any]) -> StrategyRun`
|
||||
|
||||
Create a new strategy run session.
|
||||
|
||||
**Parameters:**
|
||||
- `run_data`: Dictionary containing run information (`strategy_name`, `symbol`, `timeframe`, `start_time`, `status`, `config`, `run_metadata`)
|
||||
|
||||
**Returns:** The newly created `StrategyRun` object.
|
||||
|
||||
```python
|
||||
from datetime import datetime, timezone
|
||||
|
||||
run_data = {
|
||||
"strategy_name": "EMA Crossover Backtest",
|
||||
"symbol": "BTC-USDT",
|
||||
"timeframe": "1h",
|
||||
"start_time": datetime(2023, 1, 1, tzinfo=timezone.utc),
|
||||
"status": "running",
|
||||
"config": {"fast_ema": 12, "slow_ema": 26},
|
||||
"run_metadata": {"backtest_period": "2023-Q1"}
|
||||
}
|
||||
new_run = db.strategies.create_strategy_run(run_data)
|
||||
print(f"Created strategy run with ID: {new_run.id}")
|
||||
```
|
||||
|
||||
##### `get_strategy_run_by_id(run_id: int) -> Optional[StrategyRun]`
|
||||
|
||||
Get a strategy run by its ID.
|
||||
|
||||
```python
|
||||
run = db.strategies.get_strategy_run_by_id(1)
|
||||
if run:
|
||||
print(f"Found strategy run: {run.strategy_name} ({run.status})")
|
||||
```
|
||||
|
||||
##### `update_strategy_run(run_id: int, update_data: Dict[str, Any]) -> Optional[StrategyRun]`
|
||||
|
||||
Update a strategy run's information.
|
||||
|
||||
```python
|
||||
update_payload = {"status": "completed", "total_signals": 150}
|
||||
updated_run = db.strategies.update_strategy_run(1, update_payload)
|
||||
if updated_run:
|
||||
print(f"Strategy run status updated to: {updated_run.status}")
|
||||
```
|
||||
|
||||
##### `complete_strategy_run(run_id: int, total_signals: int) -> bool`
|
||||
|
||||
Mark a strategy run as completed.
|
||||
|
||||
**Parameters:**
|
||||
- `run_id`: The ID of the strategy run to complete.
|
||||
- `total_signals`: The total number of signals generated during the run.
|
||||
|
||||
**Returns:** `True` if successful, `False` otherwise.
|
||||
|
||||
```python
|
||||
success = db.strategies.complete_strategy_run(1, 150)
|
||||
if success:
|
||||
print("Strategy run marked as completed.")
|
||||
```
|
||||
|
||||
##### `store_strategy_signals(run_id: int, strategy_results: List[StrategyResult]) -> int`
|
||||
|
||||
Store multiple strategy signals from strategy results.
|
||||
|
||||
**Parameters:**
|
||||
- `run_id`: The strategy run ID these signals belong to.
|
||||
- `strategy_results`: List of `StrategyResult` objects containing signals.
|
||||
|
||||
**Returns:** Number of signals stored.
|
||||
|
||||
```python
|
||||
from strategies.data_types import StrategyResult, StrategySignal, SignalType
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
signal1 = StrategySignal(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
symbol="BTC-USDT",
|
||||
timeframe="1h",
|
||||
signal_type=SignalType.BUY,
|
||||
price=Decimal("50000.0"),
|
||||
confidence=Decimal("0.9")
|
||||
)
|
||||
result1 = StrategyResult(
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
symbol="BTC-USDT",
|
||||
timeframe="1h",
|
||||
strategy_name="EMA Crossover",
|
||||
signals=[signal1],
|
||||
indicators_used={"ema_fast": 49900.0, "ema_slow": 50100.0}
|
||||
)
|
||||
|
||||
signals_stored = db.strategies.store_strategy_signals(new_run.id, [result1])
|
||||
print(f"Stored {signals_stored} signals.")
|
||||
```
|
||||
|
||||
##### `get_strategy_signals(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.
|
||||
|
||||
**Parameters:**
|
||||
- `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 (e.g., "buy", "sell", "entry_long").
|
||||
- `limit`: Maximum number of signals to return.
|
||||
|
||||
**Returns:** List of `StrategySignal` objects.
|
||||
|
||||
```python
|
||||
signals = db.strategies.get_strategy_signals(
|
||||
run_id=new_run.id,
|
||||
signal_type="buy",
|
||||
limit=10
|
||||
)
|
||||
for signal in signals:
|
||||
print(f"Signal: {signal.signal_type} at {signal.price} on {signal.timestamp}")
|
||||
```
|
||||
|
||||
##### `get_strategy_signal_stats(run_id: Optional[int] = None) -> Dict[str, Any]`
|
||||
|
||||
Get statistics about strategy signals.
|
||||
|
||||
**Parameters:**
|
||||
- `run_id`: Optional run ID to get stats for a specific run.
|
||||
|
||||
**Returns:** Dictionary with statistics like total signals, counts by type, average confidence.
|
||||
|
||||
```python
|
||||
signal_stats = db.strategies.get_strategy_signal_stats(run_id=new_run.id)
|
||||
print(f"Total Signals: {signal_stats['total_signals']}")
|
||||
print(f"Signal Counts: {signal_stats['signal_counts']}")
|
||||
print(f"Average Confidence: {signal_stats['average_confidence']:.2f}")
|
||||
```
|
||||
|
||||
##### `cleanup_old_strategy_data(days_to_keep: int = 30) -> Dict[str, int]`
|
||||
|
||||
Clean up old strategy signals and runs.
|
||||
|
||||
**Parameters:**
|
||||
- `days_to_keep`: Number of days to retain completed strategy runs and their signals.
|
||||
|
||||
**Returns:** Dictionary with counts of deleted runs and signals.
|
||||
|
||||
```python
|
||||
deleted_info = db.strategies.cleanup_old_strategy_data(days_to_keep=60)
|
||||
print(f"Deleted {deleted_info['deleted_runs']} old strategy runs.")
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The database operations module includes comprehensive error handling with custom exceptions.
|
||||
|
||||
@ -63,6 +63,16 @@
|
||||
- **Reasoning**: Ensures consistency with the `BaseStrategy` abstract class, which now requires `strategy_name` during initialization, providing a clear identifier for each strategy instance.
|
||||
- **Impact**: All strategy implementations now correctly initialize their `strategy_name` via the base class, standardizing strategy identification across the engine.
|
||||
|
||||
### 5. Database Schema Design for Strategy Analysis
|
||||
- **Decision**: Created separate `strategy_signals` and `strategy_runs` tables distinct from the existing `signals` table used for bot operations.
|
||||
- **Reasoning**: Maintains clear separation between live bot trading signals and strategy analysis/backtesting data, avoiding conflicts and ensuring data integrity for different use cases.
|
||||
- **Impact**: Strategy analysis can be performed independently without affecting live trading operations, with dedicated tables optimized for analytical queries and data retention policies.
|
||||
|
||||
### 6. Repository Pattern Integration
|
||||
- **Decision**: Implemented `StrategyRepository` following the established `BaseRepository` pattern and integrated it into the centralized `DatabaseOperations` class.
|
||||
- **Reasoning**: Maintains consistency with existing database access patterns, ensures proper session management, and provides a clean API for strategy data operations.
|
||||
- **Impact**: All strategy database operations follow the same patterns as other modules, with proper error handling, logging, and transaction management.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1.0 Core Strategy Foundation Setup
|
||||
@ -88,16 +98,16 @@
|
||||
- [x] 2.8 Implement strategy parameter validation and default value handling
|
||||
- [x] 2.9 Add configuration export/import functionality for strategy sharing
|
||||
|
||||
- [ ] 3.0 Database Schema and Repository Layer
|
||||
- [ ] 3.1 Create new `strategy_signals` table migration (separate from existing `signals` table for bot operations)
|
||||
- [ ] 3.2 Design `strategy_signals` table with fields: strategy_name, strategy_config, symbol, timeframe, timestamp, signal_type, price, confidence, metadata, run_id
|
||||
- [ ] 3.3 Create `strategy_runs` table to track strategy execution sessions for backtesting and analysis
|
||||
- [ ] 3.4 Implement `StrategyRepository` class in `database/repositories/strategy_repository.py` following repository pattern
|
||||
- [ ] 3.5 Add strategy repository methods for signal storage, retrieval, and batch operations
|
||||
- [ ] 3.6 Update `database/operations.py` to include strategy operations access
|
||||
- [ ] 3.7 Create database indexes for optimal strategy signal queries (strategy_name, symbol, timeframe, timestamp)
|
||||
- [ ] 3.8 Add data retention policies for strategy signals (configurable cleanup of old analysis data)
|
||||
- [ ] 3.9 Implement strategy signal aggregation queries for performance analysis
|
||||
- [x] 3.0 Database Schema and Repository Layer
|
||||
- [x] 3.1 Create new `strategy_signals` table migration (separate from existing `signals` table for bot operations)
|
||||
- [x] 3.2 Design `strategy_signals` table with fields: strategy_name, strategy_config, symbol, timeframe, timestamp, signal_type, price, confidence, signal_metadata, run_id
|
||||
- [x] 3.3 Create `strategy_runs` table to track strategy execution sessions for backtesting and analysis
|
||||
- [x] 3.4 Implement `StrategyRepository` class in `database/repositories/strategy_repository.py` following repository pattern
|
||||
- [x] 3.5 Add strategy repository methods for signal storage, retrieval, and batch operations
|
||||
- [x] 3.6 Update `database/operations.py` to include strategy operations access
|
||||
- [x] 3.7 Create database indexes for optimal strategy signal queries (strategy_name, symbol, timeframe, timestamp)
|
||||
- [x] 3.8 Add data retention policies for strategy signals (configurable cleanup of old analysis data)
|
||||
- [x] 3.9 Implement strategy signal aggregation queries for performance analysis
|
||||
|
||||
- [ ] 4.0 Strategy Data Integration
|
||||
- [ ] 4.1 Create `StrategyDataIntegrator` class in new `strategies/data_integration.py` module
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user