""" MACD Strategy Implementation This module implements a Moving Average Convergence Divergence (MACD) trading strategy. It extends the BaseStrategy and generates buy/sell signals based on the crossover of the MACD line and its signal line. """ 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 MACDStrategy(BaseStrategy): """ MACD Strategy. Generates buy/sell signals when the MACD line crosses above or below its signal line. """ 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 MACD strategy. It needs one MACD indicator. """ # Default periods for MACD, can be overridden by strategy config return [ {'type': 'macd', 'fast_period': 12, 'slow_period': 26, 'signal_period': 9, 'price_column': 'close'} ] def calculate(self, df: pd.DataFrame, indicators_data: Dict[str, pd.DataFrame], **kwargs) -> List[StrategyResult]: """ Calculate MACD strategy signals. Args: df: DataFrame with OHLCV data. indicators_data: Dictionary of pre-calculated indicator DataFrames. Expected key: 'macd_fast_period_12_slow_period_26_signal_period_9'. **kwargs: Additional strategy parameters (e.g., fast_period, slow_period, signal_period, price_column). Returns: List of StrategyResult objects, each containing generated signals. """ # Extract parameters from kwargs or use defaults fast_period = kwargs.get('fast_period', 12) slow_period = kwargs.get('slow_period', 26) signal_period = kwargs.get('signal_period', 9) price_column = kwargs.get('price_column', 'close') # Generate indicator key using shared utility function macd_key = create_indicator_key({ 'type': 'macd', 'fast_period': fast_period, 'slow_period': slow_period, 'signal_period': signal_period }) # Validate that the main DataFrame has enough data for strategy calculation min_periods = max(slow_period, signal_period) if not self.validate_dataframe(df, min_periods): if self.logger: self.logger.warning(f"{self.strategy_name}: Insufficient main DataFrame for calculation.") return [] # Validate that the required MACD indicator data is present and sufficient required_indicators = [ {'type': 'macd', 'fast_period': fast_period, 'slow_period': slow_period, 'signal_period': signal_period} ] if not self.validate_indicators_data(indicators_data, required_indicators): if self.logger: self.logger.warning(f"{self.strategy_name}: Missing or insufficient MACD indicator data.") return [] macd_df = indicators_data.get(macd_key) if macd_df is None or macd_df.empty: if self.logger: self.logger.warning(f"{self.strategy_name}: MACD indicator DataFrame is not found or empty.") return [] # Merge all necessary data into a single DataFrame for easier processing merged_df = pd.merge(df[[price_column, 'symbol', 'timeframe']], macd_df[['macd', 'signal', 'histogram']], left_index=True, right_index=True, how='inner') 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, 'macd', 'signal' ) results: List[StrategyResult] = [] strategy_metadata = { 'fast_period': fast_period, 'slow_period': slow_period, 'signal_period': signal_period } # Process bullish crossover signals (MACD crosses above Signal) bullish_indices = merged_df[bullish_crossover].index for timestamp in bullish_indices: row = merged_df.loc[timestamp] # Skip if any MACD values are NaN if pd.isna(row['macd']) or pd.isna(row['signal']): continue signal = StrategySignal( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], signal_type=SignalType.BUY, price=float(row[price_column]), confidence=0.9, metadata={'macd_cross': 'bullish', **strategy_metadata} ) results.append(StrategyResult( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], strategy_name=self.strategy_name, signals=[signal], indicators_used={ 'macd': float(row['macd']), 'signal': float(row['signal']) }, metadata=strategy_metadata )) if self.logger: self.logger.info(f"{self.strategy_name}: BUY signal at {timestamp} for {row['symbol']} (MACD: {row['macd']:.2f}, Signal: {row['signal']:.2f})") # Process bearish crossover signals (MACD crosses below Signal) bearish_indices = merged_df[bearish_crossover].index for timestamp in bearish_indices: row = merged_df.loc[timestamp] # Skip if any MACD values are NaN if pd.isna(row['macd']) or pd.isna(row['signal']): continue signal = StrategySignal( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], signal_type=SignalType.SELL, price=float(row[price_column]), confidence=0.9, metadata={'macd_cross': 'bearish', **strategy_metadata} ) results.append(StrategyResult( timestamp=timestamp, symbol=row['symbol'], timeframe=row['timeframe'], strategy_name=self.strategy_name, signals=[signal], indicators_used={ 'macd': float(row['macd']), 'signal': float(row['signal']) }, metadata=strategy_metadata )) if self.logger: self.logger.info(f"{self.strategy_name}: SELL signal at {timestamp} for {row['symbol']} (MACD: {row['macd']:.2f}, Signal: {row['signal']:.2f})") return results