from freqtrade.strategy import IStrategy, IntParameter from pandas import DataFrame import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib import pandas as pd import numpy as np from datetime import datetime, timedelta from functools import reduce from typing import Optional class BaseStrategy(IStrategy): # Common strategy parameters INTERFACE_VERSION = 3 # Max open trades - only one trade at a time max_open_trades = 1 # Minimal ROI designed for the strategy ADJUST THIS minimal_roi = { # 1 ATR } # Optimal stoploss designed for the strategy ADJUST THIS stoploss = -0.05 # 5% stop loss # Trailing stoploss trailing_stop = True trailing_stop_positive = 0.01 trailing_stop_positive_offset = 0.02 trailing_only_offset_is_reached = True # Position sizing position_size = 0.1 # 10% of account per trade # Loss tracking consecutive_losses = 0 last_trade_time = None trading_paused = False pause_until = None def __init__(self, config: dict) -> None: super().__init__(config) self.loss_counter = 0 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # MACD I dont think ill need macds macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdhist'] = macd['macdhist'] # RSI dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) # EMAs dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200) # ATR dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) return dataframe def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: return self.position_size * self.wallets.get_total_stake_amount() def custom_stoploss(self, pair: str, trade, current_time, current_rate, current_profit, **kwargs): # Fallback ATR in case metadata is missing atr = trade.open_trade_metadata.get('atr_at_entry', 0.01) # Calculate stoploss as distance from entry stop_loss_price = trade.open_rate - atr stop_loss_relative = (current_rate - stop_loss_price) / current_rate # If profit > 1.5x ATR, tighten stop to lock in profit if current_profit > (1.5 * (atr / trade.open_rate)): return 0.01 # Tight trailing stop return max(0.01, stop_loss_relative) # Prevent SL from being too tight def open_trade(self, trade, **kwargs): # Capture ATR at trade entry trade.update_open_trade_metadata({'atr_at_entry': self.dataframe['atr'].iloc[-1]}) return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: Optional[str], side: str, **kwargs) -> bool: """ Called before a trade entry is executed. Used to prevent trades during pause periods. """ # Check if we're in a trading pause if self.trading_paused: if current_time >= self.pause_until: self.trading_paused = False self.consecutive_losses = 0 print(f"Trading resumed after pause period") else: print(f"Trade entry blocked - trading paused until {self.pause_until}") return False # Block the trade entry return True # Allow the trade entry class MomentumStrategy(BaseStrategy): """ Strategy for timeframes <= 30 minutes using MACD and EMA200 """ timeframe = '30m' # Default timeframe changed from '5m' to '30m' def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # Check if we're in a trading pause if self.trading_paused: # Use current time from dataframe instead of datetime.now() current_time = pd.to_datetime(dataframe['date'].iloc[-1]) if len(dataframe) > 0 else datetime.now() if current_time >= self.pause_until: self.trading_paused = False self.consecutive_losses = 0 else: dataframe['buy'] = 0 return dataframe # MACD cross conditions - MACD line (faster) crosses above signal line (slower) macd_cross = ( (dataframe['macd'] > dataframe['macdsignal']) & (dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1)) ) # MACD crosses above zero line (momentum turning positive) macd_above_zero = ( (dataframe['macd'] > 0) & (dataframe['macd'].shift(1) <= 0) ) # Price above 200 EMA for longs above_ema200 = dataframe['close'] > dataframe['ema200'] # Combine conditions: MACD cross above zero line AND price above 200 EMA buy_condition = macd_above_zero & above_ema200 conditions.append(buy_condition) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'buy'] = 1 return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # MACD cross down - MACD line crosses below signal line macd_cross_down = ( (dataframe['macd'] < dataframe['macdsignal']) & (dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1)) ) # Price below 200 EMA below_ema200 = dataframe['close'] < dataframe['ema200'] # Sell on MACD cross down OR price below 200 EMA conditions.append(macd_cross_down | below_ema200) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'sell'] = 1 return dataframe def custom_stoploss(self, pair: str, trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic: - Use ATR-based static stoploss until price moves 2x ATR above entry - Once price is 2x ATR above entry, activate trailing stop """ dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe is None or len(dataframe) == 0: return self.stoploss # fallback to default # Find the candle at or just before the trade open time entry_time = trade.open_date_utc entry_idx = dataframe[dataframe['date'] <= entry_time].index.max() if pd.isna(entry_idx): return self.stoploss entry_atr = dataframe.loc[entry_idx, 'atr'] entry_price = trade.open_rate atr_multiple = 2.0 trigger_price = entry_price + atr_multiple * entry_atr # If price has not reached trigger, use static ATR stop if current_rate < trigger_price: return (entry_price - entry_atr) / current_rate - 1 # If price has reached trigger, use trailing stop (e.g. 1x ATR below high) else: # Find the highest price since entry high_since_entry = dataframe.loc[entry_idx:, 'high'].max() trailing_sl = high_since_entry - entry_atr # If current price is above trailing stop, set stoploss to trailing if current_rate > trailing_sl: return (trailing_sl / current_rate) - 1 else: # If price is below trailing stop, fallback to default stoploss return self.stoploss def open_trade(self, trade, **kwargs): """ Capture ATR at trade entry for stoploss calculation """ # Get the current dataframe for this pair dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) # Capture ATR at the time of entry (last row) atr_at_entry = dataframe['atr'].iloc[-1] if len(dataframe) > 0 else 0.01 # Store ATR in trade metadata trade.update_open_trade_metadata({'atr_at_entry': atr_at_entry}) return None def confirm_trade_exit(self, pair: str, trade, order_type: str, amount: float, rate: float, time_in_force: str, sell_reason: str, current_time: datetime, **kwargs) -> bool: """ Called before a trade exit is executed. Used for loss tracking and trading pause logic. """ # Calculate profit before exit current_profit = trade.calc_profit_ratio(rate) # Update consecutive losses counter if current_profit < 0: self.consecutive_losses += 1 self.loss_counter += 1 else: self.consecutive_losses = 0 self.loss_counter = 0 # Check for trading pause conditions - pause after 3 consecutive losses if self.consecutive_losses >= 3: self.trading_paused = True self.pause_until = current_time + timedelta(days=2) print(f"Trading paused for 2 days after {self.consecutive_losses} consecutive losses") return True def custom_exit(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs): # This method is now only for custom exit logic, not loss tracking return None class DipBuyStrategy(BaseStrategy): """ Strategy for timeframes => 30 minutes using RSI and EMA touches """ timeframe = '30m' # Base timeframe def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # Check if we're in a trading pause if self.trading_paused: if datetime.now() >= self.pause_until: self.trading_paused = False self.consecutive_losses = 0 else: dataframe['buy'] = 0 return dataframe # RSI conditions rsi_oversold = dataframe['rsi'] < 30 # EMA touch conditions ema_touch = ( (dataframe['close'] > dataframe['ema50']) & (dataframe['close'] > dataframe['ema200']) ) conditions.append((rsi_oversold | ema_touch)) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'buy'] = 1 return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # RSI overbought rsi_overbought = dataframe['rsi'] > 70 # EMA touch conditions ema_touch_down = ( (dataframe['close'] < dataframe['ema50']) | (dataframe['close'] < dataframe['ema200']) ) conditions.append(rsi_overbought | ema_touch_down) if conditions: dataframe.loc[ reduce(lambda x, y: x | y, conditions), 'sell'] = 1 return dataframe def custom_exit(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs): # Update consecutive losses counter if current_profit < 0: self.consecutive_losses += 1 self.loss_counter += 1 else: self.consecutive_losses = 0 self.loss_counter = 0 # Check for trading pause conditions if self.consecutive_losses >= 3: self.trading_paused = True self.pause_until = current_time + timedelta(days=3) return None