new strategies
This commit is contained in:
@ -6,13 +6,16 @@ import logging
|
||||
from datetime import datetime, timezone
|
||||
import sqlite3
|
||||
|
||||
from logging_utils import setup_logging
|
||||
|
||||
class BaseStrategy(ABC):
|
||||
"""
|
||||
An abstract base class that defines the blueprint for all trading strategies.
|
||||
It provides common functionality like loading data and saving status.
|
||||
It provides common functionality like loading data, saving status, and state management.
|
||||
"""
|
||||
|
||||
def __init__(self, strategy_name: str, params: dict, log_level: str):
|
||||
def __init__(self, strategy_name: str, params: dict):
|
||||
# Note: log_level is not needed here as logging is set up by the process
|
||||
self.strategy_name = strategy_name
|
||||
self.params = params
|
||||
self.coin = params.get("coin", "N/A")
|
||||
@ -20,21 +23,17 @@ class BaseStrategy(ABC):
|
||||
self.db_path = os.path.join("_data", "market_data.db")
|
||||
self.status_file_path = os.path.join("_data", f"strategy_status_{self.strategy_name}.json")
|
||||
|
||||
# --- ADDED: State variables required for status reporting ---
|
||||
self.current_signal = "INIT"
|
||||
self.last_signal_change_utc = None
|
||||
self.signal_price = None
|
||||
|
||||
# This will be set up by the child class after it's initialized
|
||||
# setup_logging(log_level, f"Strategy-{self.strategy_name}")
|
||||
# logging.info(f"Initializing with parameters: {self.params}")
|
||||
logging.info(f"Initializing with parameters: {self.params}")
|
||||
|
||||
def load_data(self) -> pd.DataFrame:
|
||||
"""Loads historical data for the configured coin and timeframe."""
|
||||
table_name = f"{self.coin}_{self.timeframe}"
|
||||
|
||||
# Dynamically determine the number of candles needed based on all possible period parameters
|
||||
periods = [v for k, v in self.params.items() if 'period' in k or '_ma' in k or 'slow' in k]
|
||||
periods = [v for k, v in self.params.items() if 'period' in k or '_ma' in k or 'slow' in k or 'fast' in k]
|
||||
limit = max(periods) + 50 if periods else 500
|
||||
|
||||
try:
|
||||
@ -51,10 +50,30 @@ class BaseStrategy(ABC):
|
||||
|
||||
@abstractmethod
|
||||
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""
|
||||
The core logic of the strategy. Must be implemented by child classes.
|
||||
"""
|
||||
"""The core logic of the strategy. Must be implemented by child classes."""
|
||||
pass
|
||||
|
||||
def calculate_signals_and_state(self, df: pd.DataFrame):
|
||||
"""
|
||||
A wrapper that calls the strategy's signal calculation and then
|
||||
determines the last signal change from the historical data.
|
||||
"""
|
||||
df_with_signals = self.calculate_signals(df)
|
||||
df_with_signals.dropna(inplace=True)
|
||||
if df_with_signals.empty: return
|
||||
|
||||
df_with_signals['position_change'] = df_with_signals['signal'].diff()
|
||||
|
||||
last_signal = df_with_signals['signal'].iloc[-1]
|
||||
if last_signal == 1: self.current_signal = "BUY"
|
||||
elif last_signal == -1: self.current_signal = "SELL"
|
||||
else: self.current_signal = "HOLD"
|
||||
|
||||
last_change_series = df_with_signals[df_with_signals['position_change'] != 0]
|
||||
if not last_change_series.empty:
|
||||
last_change_row = last_change_series.iloc[-1]
|
||||
self.last_signal_change_utc = last_change_row.name.tz_localize('UTC').isoformat()
|
||||
self.signal_price = last_change_row['close']
|
||||
|
||||
def _save_status(self):
|
||||
"""Saves the current strategy state to its JSON file."""
|
||||
|
||||
@ -7,29 +7,23 @@ class MaCrossStrategy(BaseStrategy):
|
||||
A strategy based on a fast Simple Moving Average (SMA) crossing
|
||||
a slow SMA.
|
||||
"""
|
||||
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
# Support multiple naming conventions: some configs use 'fast'/'slow'
|
||||
# while others use 'short_ma'/'long_ma'. Normalize here so both work.
|
||||
fast_ma_period = self.params.get('short_ma') or self.params.get('fast') or 0
|
||||
slow_ma_period = self.params.get('long_ma') or self.params.get('slow') or 0
|
||||
|
||||
# If parameters are missing, return a neutral signal frame.
|
||||
if not fast_ma_period or not slow_ma_period:
|
||||
logging.warning(f"Missing MA period parameters (fast={fast_ma_period}, slow={slow_ma_period}).")
|
||||
df['signal'] = 0
|
||||
return df
|
||||
def __init__(self, strategy_name: str, params: dict, log_level: str):
|
||||
super().__init__(strategy_name, params)
|
||||
self.fast_ma_period = self.params.get('short_ma') or self.params.get('fast') or 0
|
||||
self.slow_ma_period = self.params.get('long_ma') or self.params.get('slow') or 0
|
||||
|
||||
if len(df) < slow_ma_period:
|
||||
logging.warning(f"Not enough data for MA periods {fast_ma_period}/{slow_ma_period}. Need {slow_ma_period}, have {len(df)}.")
|
||||
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
if not self.fast_ma_period or not self.slow_ma_period or len(df) < self.slow_ma_period:
|
||||
logging.warning(f"Not enough data for MA periods.")
|
||||
df['signal'] = 0
|
||||
return df
|
||||
|
||||
df['fast_sma'] = df['close'].rolling(window=fast_ma_period).mean()
|
||||
df['slow_sma'] = df['close'].rolling(window=slow_ma_period).mean()
|
||||
df['fast_sma'] = df['close'].rolling(window=self.fast_ma_period).mean()
|
||||
df['slow_sma'] = df['close'].rolling(window=self.slow_ma_period).mean()
|
||||
|
||||
# Signal is 1 for Golden Cross (fast > slow), -1 for Death Cross
|
||||
df['signal'] = 0
|
||||
df.loc[df['fast_sma'] > df['slow_sma'], 'signal'] = 1
|
||||
df.loc[df['fast_sma'] < df['slow_sma'], 'signal'] = -1
|
||||
|
||||
return df
|
||||
|
||||
|
||||
@ -6,19 +6,21 @@ class SingleSmaStrategy(BaseStrategy):
|
||||
"""
|
||||
A strategy based on the price crossing a single Simple Moving Average (SMA).
|
||||
"""
|
||||
def __init__(self, strategy_name: str, params: dict):
|
||||
super().__init__(strategy_name, params)
|
||||
self.sma_period = self.params.get('sma_period', 0)
|
||||
|
||||
def calculate_signals(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
sma_period = self.params.get('sma_period', 0)
|
||||
|
||||
if not sma_period or len(df) < sma_period:
|
||||
logging.warning(f"Not enough data for SMA period {sma_period}. Need {sma_period}, have {len(df)}.")
|
||||
if not self.sma_period or len(df) < self.sma_period:
|
||||
logging.warning(f"Not enough data for SMA period {self.sma_period}.")
|
||||
df['signal'] = 0
|
||||
return df
|
||||
|
||||
df['sma'] = df['close'].rolling(window=sma_period).mean()
|
||||
df['sma'] = df['close'].rolling(window=self.sma_period).mean()
|
||||
|
||||
# Signal is 1 when price is above SMA, -1 when below
|
||||
df['signal'] = 0
|
||||
df.loc[df['close'] > df['sma'], 'signal'] = 1
|
||||
df.loc[df['close'] < df['sma'], 'signal'] = -1
|
||||
|
||||
return df
|
||||
|
||||
|
||||
Reference in New Issue
Block a user