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.
This commit is contained in:
18
strategies/implementations/__init__.py
Normal file
18
strategies/implementations/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Strategy implementations package.
|
||||
|
||||
This package contains individual implementations of trading strategies,
|
||||
each in its own module for better maintainability and separation of concerns.
|
||||
"""
|
||||
|
||||
from .ema_crossover import EMAStrategy
|
||||
from .rsi import RSIStrategy
|
||||
from .macd import MACDStrategy
|
||||
# from .macd import MACDIndicator
|
||||
|
||||
__all__ = [
|
||||
'EMAStrategy',
|
||||
'RSIStrategy',
|
||||
'MACDStrategy',
|
||||
# 'MACDIndicator'
|
||||
]
|
||||
185
strategies/implementations/ema_crossover.py
Normal file
185
strategies/implementations/ema_crossover.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
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
|
||||
180
strategies/implementations/macd.py
Normal file
180
strategies/implementations/macd.py
Normal file
@@ -0,0 +1,180 @@
|
||||
"""
|
||||
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
|
||||
168
strategies/implementations/rsi.py
Normal file
168
strategies/implementations/rsi.py
Normal file
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
Relative Strength Index (RSI) Strategy Implementation
|
||||
|
||||
This module implements an RSI-based momentum trading strategy.
|
||||
It extends the BaseStrategy and generates buy/sell signals based on
|
||||
RSI crossing overbought/oversold thresholds.
|
||||
"""
|
||||
|
||||
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_threshold_signals_vectorized
|
||||
|
||||
|
||||
class RSIStrategy(BaseStrategy):
|
||||
"""
|
||||
RSI Strategy.
|
||||
|
||||
Generates buy/sell signals when RSI crosses overbought/oversold thresholds.
|
||||
"""
|
||||
|
||||
def __init__(self, logger=None):
|
||||
super().__init__(logger)
|
||||
self.strategy_name = "rsi"
|
||||
|
||||
def get_required_indicators(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Defines the indicators required by the RSI strategy.
|
||||
It needs one RSI indicator.
|
||||
"""
|
||||
# Default period for RSI, can be overridden by strategy config
|
||||
return [
|
||||
{'type': 'rsi', 'period': 14, 'price_column': 'close'}
|
||||
]
|
||||
|
||||
def calculate(self, df: pd.DataFrame, indicators_data: Dict[str, pd.DataFrame], **kwargs) -> List[StrategyResult]:
|
||||
"""
|
||||
Calculate RSI strategy signals.
|
||||
|
||||
Args:
|
||||
df: DataFrame with OHLCV data.
|
||||
indicators_data: Dictionary of pre-calculated indicator DataFrames.
|
||||
Expected key: 'rsi_period_14'.
|
||||
**kwargs: Additional strategy parameters (e.g., period, overbought, oversold, price_column).
|
||||
|
||||
Returns:
|
||||
List of StrategyResult objects, each containing generated signals.
|
||||
"""
|
||||
# Extract parameters from kwargs or use defaults
|
||||
period = kwargs.get('period', 14)
|
||||
overbought = kwargs.get('overbought', 70)
|
||||
oversold = kwargs.get('oversold', 30)
|
||||
price_column = kwargs.get('price_column', 'close')
|
||||
|
||||
# Generate indicator key using shared utility function
|
||||
rsi_key = create_indicator_key({'type': 'rsi', 'period': period})
|
||||
|
||||
# Validate that the main DataFrame has enough data for strategy calculation
|
||||
if not self.validate_dataframe(df, period):
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.strategy_name}: Insufficient main DataFrame for calculation.")
|
||||
return []
|
||||
|
||||
# Validate that the required RSI indicator data is present and sufficient
|
||||
required_indicators = [
|
||||
{'type': 'rsi', 'period': period}
|
||||
]
|
||||
if not self.validate_indicators_data(indicators_data, required_indicators):
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.strategy_name}: Missing or insufficient RSI indicator data.")
|
||||
return []
|
||||
|
||||
rsi_df = indicators_data.get(rsi_key)
|
||||
|
||||
if rsi_df is None or rsi_df.empty:
|
||||
if self.logger:
|
||||
self.logger.warning(f"{self.strategy_name}: RSI 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']],
|
||||
rsi_df[['rsi']],
|
||||
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
|
||||
buy_signals, sell_signals = detect_threshold_signals_vectorized(
|
||||
merged_df, 'rsi', overbought, oversold
|
||||
)
|
||||
|
||||
results: List[StrategyResult] = []
|
||||
strategy_metadata = {
|
||||
'period': period,
|
||||
'overbought': overbought,
|
||||
'oversold': oversold
|
||||
}
|
||||
|
||||
# Process buy signals (RSI crosses above oversold threshold)
|
||||
buy_indices = merged_df[buy_signals].index
|
||||
for timestamp in buy_indices:
|
||||
row = merged_df.loc[timestamp]
|
||||
|
||||
# Skip if RSI value is NaN
|
||||
if pd.isna(row['rsi']):
|
||||
continue
|
||||
|
||||
signal = StrategySignal(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
signal_type=SignalType.BUY,
|
||||
price=float(row[price_column]),
|
||||
confidence=0.7,
|
||||
metadata={'rsi_cross': 'oversold_to_buy', **strategy_metadata}
|
||||
)
|
||||
|
||||
results.append(StrategyResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
strategy_name=self.strategy_name,
|
||||
signals=[signal],
|
||||
indicators_used={'rsi': float(row['rsi'])},
|
||||
metadata=strategy_metadata
|
||||
))
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.strategy_name}: BUY signal at {timestamp} for {row['symbol']} (RSI: {row['rsi']:.2f})")
|
||||
|
||||
# Process sell signals (RSI crosses below overbought threshold)
|
||||
sell_indices = merged_df[sell_signals].index
|
||||
for timestamp in sell_indices:
|
||||
row = merged_df.loc[timestamp]
|
||||
|
||||
# Skip if RSI value is NaN
|
||||
if pd.isna(row['rsi']):
|
||||
continue
|
||||
|
||||
signal = StrategySignal(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
signal_type=SignalType.SELL,
|
||||
price=float(row[price_column]),
|
||||
confidence=0.7,
|
||||
metadata={'rsi_cross': 'overbought_to_sell', **strategy_metadata}
|
||||
)
|
||||
|
||||
results.append(StrategyResult(
|
||||
timestamp=timestamp,
|
||||
symbol=row['symbol'],
|
||||
timeframe=row['timeframe'],
|
||||
strategy_name=self.strategy_name,
|
||||
signals=[signal],
|
||||
indicators_used={'rsi': float(row['rsi'])},
|
||||
metadata=strategy_metadata
|
||||
))
|
||||
|
||||
if self.logger:
|
||||
self.logger.info(f"{self.strategy_name}: SELL signal at {timestamp} for {row['symbol']} (RSI: {row['rsi']:.2f})")
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user