Vasily.onl d34da789ec 4.0 - 2.0 Implement strategy configuration utilities and templates
- 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.
2025-06-12 15:17:35 +08:00

184 lines
7.4 KiB
Python

"""
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