185 lines
7.4 KiB
Python
Raw Normal View History

"""
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, logger=None):
super().__init__(logger)
self.strategy_name = "ema_crossover"
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