Vasily.onl fd5a59fc39 4.0 - 1.0 Implement strategy engine foundation with modular components
- Introduced a new `strategies` package containing the core structure for trading strategies, including `BaseStrategy`, `StrategyFactory`, and various strategy implementations (EMA, RSI, MACD).
- Added utility functions for signal detection and validation in `strategies/utils.py`, enhancing modularity and maintainability.
- Updated `pyproject.toml` to include the new `strategies` package in the build configuration.
- Implemented comprehensive unit tests for the strategy foundation components, ensuring reliability and adherence to project standards.

These changes establish a solid foundation for the strategy engine, aligning with project goals for modularity, performance, and maintainability.
2025-06-12 14:41:16 +08:00

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