""" Strategy Management System This module provides functionality to manage user-defined strategies with file-based storage. Each strategy is saved as a separate JSON file for portability and easy sharing. """ import json import os import uuid from datetime import datetime, timezone from pathlib import Path from typing import Dict, List, Optional, Any, Tuple from dataclasses import dataclass, asdict from enum import Enum import importlib from utils.logger import get_logger # Initialize logger logger = get_logger() # Base directory for strategies STRATEGIES_DIR = Path("config/strategies") USER_STRATEGIES_DIR = STRATEGIES_DIR / "user_strategies" TEMPLATES_DIR = STRATEGIES_DIR / "templates" class StrategyType(str, Enum): """Supported strategy types.""" EMA_CROSSOVER = "ema_crossover" RSI = "rsi" MACD = "macd" class StrategyCategory(str, Enum): """Strategy categories.""" TREND_FOLLOWING = "trend_following" MOMENTUM = "momentum" MEAN_REVERSION = "mean_reversion" SCALPING = "scalping" SWING_TRADING = "swing_trading" @dataclass class StrategyConfig: """Strategy configuration data.""" id: str name: str description: str strategy_type: str # StrategyType category: str # StrategyCategory parameters: Dict[str, Any] timeframes: List[str] enabled: bool = True created_date: str = "" modified_date: str = "" def __post_init__(self): """Initialize timestamps if not provided.""" current_time = datetime.now(timezone.utc).isoformat() if not self.created_date: self.created_date = current_time if not self.modified_date: self.modified_date = current_time def to_dict(self) -> Dict[str, Any]: """Convert to dictionary for JSON serialization.""" return { 'id': self.id, 'name': self.name, 'description': self.description, 'strategy_type': self.strategy_type, 'category': self.category, 'parameters': self.parameters, 'timeframes': self.timeframes, 'enabled': self.enabled, 'created_date': self.created_date, 'modified_date': self.modified_date } @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'StrategyConfig': """Create StrategyConfig from dictionary.""" return cls( id=data['id'], name=data['name'], description=data.get('description', ''), strategy_type=data['strategy_type'], category=data.get('category', 'trend_following'), parameters=data.get('parameters', {}), timeframes=data.get('timeframes', []), enabled=data.get('enabled', True), created_date=data.get('created_date', ''), modified_date=data.get('modified_date', '') ) class StrategyManager: """Manager for user-defined strategies with file-based storage.""" def __init__(self): """Initialize the strategy manager.""" self.logger = logger self._ensure_directories() def _ensure_directories(self): """Ensure strategy directories exist.""" try: USER_STRATEGIES_DIR.mkdir(parents=True, exist_ok=True) TEMPLATES_DIR.mkdir(parents=True, exist_ok=True) self.logger.debug("Strategy manager: Strategy directories created/verified") except Exception as e: self.logger.error(f"Strategy manager: Error creating strategy directories: {e}") def _get_strategy_file_path(self, strategy_id: str) -> Path: """Get file path for a strategy.""" return USER_STRATEGIES_DIR / f"{strategy_id}.json" def _get_template_file_path(self, strategy_type: str) -> Path: """Get file path for a strategy template.""" return TEMPLATES_DIR / f"{strategy_type}_template.json" def save_strategy(self, strategy: StrategyConfig) -> bool: """ Save a strategy to file. Args: strategy: StrategyConfig instance to save Returns: True if saved successfully, False otherwise """ try: # Update modified date strategy.modified_date = datetime.now(timezone.utc).isoformat() file_path = self._get_strategy_file_path(strategy.id) with open(file_path, 'w', encoding='utf-8') as f: json.dump(strategy.to_dict(), f, indent=2, ensure_ascii=False) self.logger.info(f"Strategy manager: Saved strategy: {strategy.name} ({strategy.id})") return True except Exception as e: self.logger.error(f"Strategy manager: Error saving strategy {strategy.id}: {e}") return False def load_strategy(self, strategy_id: str) -> Optional[StrategyConfig]: """ Load a strategy from file. Args: strategy_id: ID of the strategy to load Returns: StrategyConfig instance or None if not found/error """ try: file_path = self._get_strategy_file_path(strategy_id) if not file_path.exists(): self.logger.warning(f"Strategy manager: Strategy file not found: {strategy_id}") return None with open(file_path, 'r', encoding='utf-8') as f: data = json.load(f) strategy = StrategyConfig.from_dict(data) self.logger.debug(f"Strategy manager: Loaded strategy: {strategy.name} ({strategy.id})") return strategy except Exception as e: self.logger.error(f"Strategy manager: Error loading strategy {strategy_id}: {e}") return None def list_strategies(self, enabled_only: bool = False) -> List[StrategyConfig]: """ List all user strategies. Args: enabled_only: If True, only return enabled strategies Returns: List of StrategyConfig instances """ strategies = [] try: if not USER_STRATEGIES_DIR.exists(): return strategies for file_path in USER_STRATEGIES_DIR.glob("*.json"): strategy = self.load_strategy(file_path.stem) if strategy: if not enabled_only or strategy.enabled: strategies.append(strategy) # Sort by name strategies.sort(key=lambda s: s.name.lower()) except Exception as e: self.logger.error(f"Strategy manager: Error listing strategies: {e}") return strategies def delete_strategy(self, strategy_id: str) -> bool: """ Delete a strategy file. Args: strategy_id: ID of the strategy to delete Returns: True if deleted successfully, False otherwise """ try: file_path = self._get_strategy_file_path(strategy_id) if file_path.exists(): file_path.unlink() self.logger.info(f"Strategy manager: Deleted strategy: {strategy_id}") return True else: self.logger.warning(f"Strategy manager: Strategy file not found for deletion: {strategy_id}") return False except Exception as e: self.logger.error(f"Strategy manager: Error deleting strategy {strategy_id}: {e}") return False def create_strategy(self, name: str, strategy_type: str, parameters: Dict[str, Any], description: str = "", category: str = None, timeframes: List[str] = None) -> Optional[StrategyConfig]: """ Create a new strategy with validation. Args: name: Strategy name strategy_type: Type of strategy (must be valid StrategyType) parameters: Strategy parameters description: Optional description category: Strategy category (defaults based on type) timeframes: Supported timeframes (defaults to common ones) Returns: StrategyConfig instance if created successfully, None otherwise """ try: # Validate strategy type if strategy_type not in [t.value for t in StrategyType]: self.logger.error(f"Strategy manager: Invalid strategy type: {strategy_type}") return None # Validate parameters against template if not self._validate_parameters(strategy_type, parameters): self.logger.error(f"Strategy manager: Invalid parameters for strategy type: {strategy_type}") return None # Set defaults if category is None: category = self._get_default_category(strategy_type) if timeframes is None: timeframes = self._get_default_timeframes(strategy_type) # Create strategy strategy = StrategyConfig( id=str(uuid.uuid4()), name=name, description=description, strategy_type=strategy_type, category=category, parameters=parameters, timeframes=timeframes, enabled=True ) # Save strategy if self.save_strategy(strategy): self.logger.info(f"Strategy manager: Created strategy: {name}") return strategy else: return None except Exception as e: self.logger.error(f"Strategy manager: Error creating strategy: {e}") return None def update_strategy(self, strategy_id: str, **updates) -> bool: """ Update an existing strategy. Args: strategy_id: ID of strategy to update **updates: Fields to update Returns: True if updated successfully, False otherwise """ try: strategy = self.load_strategy(strategy_id) if not strategy: return False # Update fields for field, value in updates.items(): if hasattr(strategy, field): setattr(strategy, field, value) # Validate parameters if they were updated if 'parameters' in updates: if not self._validate_parameters(strategy.strategy_type, strategy.parameters): self.logger.error(f"Strategy manager: Invalid parameters for update") return False return self.save_strategy(strategy) except Exception as e: self.logger.error(f"Strategy manager: Error updating strategy {strategy_id}: {e}") return False def get_strategies_by_category(self, category: str) -> List[StrategyConfig]: """Get strategies filtered by category.""" return [s for s in self.list_strategies() if s.category == category] def get_available_strategy_types(self) -> List[str]: """Get list of available strategy types.""" return [t.value for t in StrategyType] def _get_default_category(self, strategy_type: str) -> str: """Get default category for a strategy type.""" category_mapping = { StrategyType.EMA_CROSSOVER.value: StrategyCategory.TREND_FOLLOWING.value, StrategyType.RSI.value: StrategyCategory.MOMENTUM.value, StrategyType.MACD.value: StrategyCategory.TREND_FOLLOWING.value, } return category_mapping.get(strategy_type, StrategyCategory.TREND_FOLLOWING.value) def _get_default_timeframes(self, strategy_type: str) -> List[str]: """Get default timeframes for a strategy type.""" timeframe_mapping = { StrategyType.EMA_CROSSOVER.value: ["1h", "4h", "1d"], StrategyType.RSI.value: ["15m", "1h", "4h", "1d"], StrategyType.MACD.value: ["1h", "4h", "1d"], } return timeframe_mapping.get(strategy_type, ["1h", "4h", "1d"]) def _validate_parameters(self, strategy_type: str, parameters: Dict[str, Any]) -> bool: """Validate strategy parameters against template.""" try: # Import here to avoid circular dependency from config.strategies.config_utils import validate_strategy_parameters is_valid, errors = validate_strategy_parameters(strategy_type, parameters) if not is_valid: for error in errors: self.logger.error(f"Strategy manager: Parameter validation error: {error}") return is_valid except ImportError: self.logger.warning("Strategy manager: Could not import validation function, skipping parameter validation") return True except Exception as e: self.logger.error(f"Strategy manager: Error validating parameters: {e}") return False def get_template(self, strategy_type: str) -> Optional[Dict[str, Any]]: """ Load strategy template for the given type. Args: strategy_type: Strategy type to get template for Returns: Template dictionary or None if not found """ try: file_path = self._get_template_file_path(strategy_type) if not file_path.exists(): self.logger.warning(f"Strategy manager: Template not found: {strategy_type}") return None with open(file_path, 'r', encoding='utf-8') as f: template = json.load(f) return template except Exception as e: self.logger.error(f"Strategy manager: Error loading template {strategy_type}: {e}") return None # Global strategy manager instance _strategy_manager = None def get_strategy_manager() -> StrategyManager: """Get global strategy manager instance (singleton pattern).""" global _strategy_manager if _strategy_manager is None: _strategy_manager = StrategyManager() return _strategy_manager