- Introduced `config_utils.py` for loading and managing strategy configurations, including functions for loading templates, generating dropdown options, and retrieving parameter schemas and default values. - Added JSON templates for EMA Crossover, MACD, and RSI strategies, defining their parameters and validation rules to enhance modularity and maintainability. - Implemented `StrategyManager` in `manager.py` for managing user-defined strategies with file-based storage, supporting easy sharing and portability. - Updated `__init__.py` to include new components and ensure proper module exports. - Enhanced error handling and logging practices across the new modules for improved reliability. These changes establish a robust foundation for strategy management and configuration, aligning with project goals for modularity, performance, and maintainability.
179 lines
7.0 KiB
Python
179 lines
7.0 KiB
Python
"""
|
|
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 |