""" EMA Crossover Strategy Implementation This module implements an Exponential Moving Average (EMA) Crossover trading strategy. It extends the BaseStrategy and generates buy/sell signals based on the crossover of a fast EMA and a slow EMA. """ import pandas as pd from typing import List, Dict, Any from ..base import BaseStrategy from ..data_types import StrategyResult, StrategySignal, SignalType from ..utils import create_indicator_key, detect_crossover_signals_vectorized class EMAStrategy(BaseStrategy): """ EMA Crossover Strategy. Generates buy/sell signals when a fast EMA crosses above or below a slow EMA. """ def __init__(self, strategy_name: str, logger=None): super().__init__(strategy_name, logger) def get_required_indicators(self) -> List[Dict[str, Any]]: """ Defines the indicators required by the EMA Crossover strategy. It needs two EMA indicators: a fast one and a slow one. """ # Default periods for EMA crossover, can be overridden by strategy config return [ {'type': 'ema', 'period': 12, 'price_column': 'close'}, {'type': 'ema', 'period': 26, 'price_column': 'close'} ] def calculate(self, df: pd.DataFrame, indicators_data: Dict[str, pd.DataFrame], **kwargs) -> List[StrategyResult]: """ Calculate EMA Crossover strategy signals. Args: df: DataFrame with OHLCV data. indicators_data: Dictionary of pre-calculated indicator DataFrames. Expected keys: 'ema_period_12', 'ema_period_26'. **kwargs: Additional strategy parameters (e.g., fast_period, slow_period, price_column). Returns: List of StrategyResult objects, each containing generated signals. """ # Extract EMA periods from kwargs or use defaults fast_period = kwargs.get('fast_period', 12) slow_period = kwargs.get('slow_period', 26) price_column = kwargs.get('price_column', 'close') # Generate indicator keys using shared utility function fast_ema_key = create_indicator_key({'type': 'ema', 'period': fast_period}) slow_ema_key = create_indicator_key({'type': 'ema', 'period': slow_period}) # Validate that the main DataFrame has enough data for strategy calculation (not just indicators) if not self.validate_dataframe(df, max(fast_period, slow_period)): if self.logger: self.logger.warning(f"{self.strategy_name}: Insufficient main DataFrame for calculation.") return [] # Validate that the required indicators are present and have sufficient data required_indicators = [ {'type': 'ema', 'period': fast_period}, {'type': 'ema', 'period': slow_period} ] if not self.validate_indicators_data(indicators_data, required_indicators): if self.logger: self.logger.warning(f"{self.strategy_name}: Missing or insufficient indicator data.") return [] fast_ema_df = indicators_data.get(fast_ema_key) slow_ema_df = indicators_data.get(slow_ema_key) if fast_ema_df is None or slow_ema_df is None or fast_ema_df.empty or slow_ema_df.empty: if self.logger: self.logger.warning(f"{self.strategy_name}: EMA indicator DataFrames are not found or empty.") return [] # Merge all necessary data into a single DataFrame for easier processing # Ensure alignment by index (timestamp) merged_df = pd.merge(df[[price_column, 'symbol', 'timeframe']], fast_ema_df[['ema']], left_index=True, right_index=True, how='inner', suffixes= ('', '_fast')) merged_df = pd.merge(merged_df, slow_ema_df[['ema']], left_index=True, right_index=True, how='inner', suffixes= ('', '_slow')) # Rename columns to their logical names after merge merged_df.rename(columns={'ema': 'ema_fast', 'ema_slow': 'ema_slow'}, inplace=True) if merged_df.empty: if self.logger: self.logger.warning(f"{self.strategy_name}: Merged DataFrame is empty after indicator alignment. Check data ranges.") return [] # Use vectorized signal detection for better performance bullish_crossover, bearish_crossover = detect_crossover_signals_vectorized( merged_df, 'ema_fast', 'ema_slow' ) results: List[StrategyResult] = [] strategy_metadata = { 'fast_period': fast_period, 'slow_period': slow_period } # Process bullish crossover signals bullish_indices = merged_df[bullish_crossover].index for timestamp in bullish_indices: row = merged_df.loc[timestamp] # Skip if any EMA values are NaN if pd.isna(row['ema_fast']) or pd.isna(row['ema_slow']): continue signal = StrategySignal( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], signal_type=SignalType.BUY, price=float(row[price_column]), confidence=0.8, metadata={'crossover_type': 'bullish', **strategy_metadata} ) results.append(StrategyResult( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], strategy_name=self.strategy_name, signals=[signal], indicators_used={ 'ema_fast': float(row['ema_fast']), 'ema_slow': float(row['ema_slow']) }, metadata=strategy_metadata )) if self.logger: self.logger.info(f"{self.strategy_name}: BUY signal at {timestamp} for {row['symbol']}") # Process bearish crossover signals bearish_indices = merged_df[bearish_crossover].index for timestamp in bearish_indices: row = merged_df.loc[timestamp] # Skip if any EMA values are NaN if pd.isna(row['ema_fast']) or pd.isna(row['ema_slow']): continue signal = StrategySignal( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], signal_type=SignalType.SELL, price=float(row[price_column]), confidence=0.8, metadata={'crossover_type': 'bearish', **strategy_metadata} ) results.append(StrategyResult( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], strategy_name=self.strategy_name, signals=[signal], indicators_used={ 'ema_fast': float(row['ema_fast']), 'ema_slow': float(row['ema_slow']) }, metadata=strategy_metadata )) if self.logger: self.logger.info(f"{self.strategy_name}: SELL signal at {timestamp} for {row['symbol']}") return results