new strategies

This commit is contained in:
2025-10-25 21:51:25 +02:00
parent 76a858a7df
commit 541a71d2a6
6 changed files with 291 additions and 52 deletions

View File

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

View 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

View File

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