import logging import numpy as np import pandas as pd import pandas_ta as pta from freqtrade.strategy.interface import IStrategy from freqtrade.strategy import merge_informative_pair from pandas import DataFrame, Series from functools import reduce from freqtrade.persistence import Trade, Order from datetime import datetime, timedelta import time from typing import Optional import warnings log = logging.getLogger(__name__) # log.setLevel(logging.DEBUG) warnings.simplefilter(action="ignore", category=pd.errors.PerformanceWarning) class NostalgiaForInfinityX6(IStrategy): INTERFACE_VERSION = 3 def version(self) -> str: return "v1.0.168" stoploss = -0.99 # Trailing stoploss trailing_stop = False trailing_only_offset_is_reached = True trailing_stop_positive = 0.01 trailing_stop_positive_offset = 0.03 use_custom_stoploss = False # Optimal timeframe for the strategy. timeframe = "5m" info_timeframes = ["15m", "1h", "4h", "1d"] # Run "populate_indicators()" only for new candle. process_only_new_candles = True # These values can be overridden in the "ask_strategy" section in the config. use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = True # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 800 # Long grind mode name long_grind_mode_name = "long_grind" # Shorting short_grind_mode_name = "short_grind" is_futures_mode = False futures_mode_leverage = 3.0 # user specified fees to be used for profit calculations custom_fee_open_rate = None custom_fee_close_rate = None # Position adjust feature position_adjustment_enable = True # Grinding v2 grind_v2_stake_multiplier_spot = 5 grind_v2_stake_multiplier_futures = 5 grind_v2_stake_multiplier_first = 0.15 grind_v2_profit_exit_threshold = 0.25 grinding_v2_max_stake = 1.0 grinding_v2_grind_1_enable = True grinding_v2_grind_1_stakes_spot = [0.05, 0.10] grinding_v2_grind_1_thresholds_spot = [-0.06, -0.12] grinding_v2_grind_1_stakes_futures = [0.05, 0.10] grinding_v2_grind_1_thresholds_futures = [-0.06, -0.12] grinding_v2_grind_1_profit_threshold_spot = 0.05 grinding_v2_grind_1_profit_threshold_futures = 0.05 grinding_v2_grind_1_use_derisk = True grinding_v2_grind_1_derisk_spot = -0.20 grinding_v2_grind_1_derisk_futures = -0.20 grinding_v2_grind_2_enable = True grinding_v2_grind_2_stakes_spot = [0.05, 0.10] grinding_v2_grind_2_thresholds_spot = [-0.06, -0.20] grinding_v2_grind_2_stakes_futures = [0.05, 0.10] grinding_v2_grind_2_thresholds_futures = [-0.06, -0.20] grinding_v2_grind_2_profit_threshold_spot = 0.10 grinding_v2_grind_2_profit_threshold_futures = 0.10 grinding_v2_grind_2_use_derisk = True grinding_v2_grind_2_derisk_spot = -0.28 grinding_v2_grind_2_derisk_futures = -0.28 grinding_v2_grind_3_enable = True grinding_v2_grind_3_stakes_spot = [0.10, 0.20] grinding_v2_grind_3_thresholds_spot = [-0.06, -0.24] grinding_v2_grind_3_stakes_futures = [0.10, 0.20] grinding_v2_grind_3_thresholds_futures = [-0.06, -0.24] grinding_v2_grind_3_profit_threshold_spot = 0.15 grinding_v2_grind_3_profit_threshold_futures = 0.15 grinding_v2_grind_3_use_derisk = True grinding_v2_grind_3_derisk_spot = -0.32 grinding_v2_grind_3_derisk_futures = -0.32 grinding_v2_grind_x_enable = True grinding_v2_grind_x_stakes_spot = [0.15, 0.30] grinding_v2_grind_x_thresholds_spot = [-0.06, -0.24] grinding_v2_grind_x_stakes_futures = [0.15, 0.30] grinding_v2_grind_x_thresholds_futures = [-0.06, -0.24] grinding_v2_grind_x_profit_threshold_spot = 0.20 grinding_v2_grind_x_profit_threshold_futures = 0.20 grinding_v2_grind_x_use_derisk = True grinding_v2_grind_x_derisk_spot = -0.32 grinding_v2_grind_x_derisk_futures = -0.32 def __init__(self, config: dict) -> None: super().__init__(config) self.is_futures_mode = self.config.get("trading_mode", "spot") == "futures" if self.is_futures_mode: self.futures_mode_leverage = self.config.get("leverage", 3.0) self.can_short = True self.exchange_name = self.config.get("exchange", {}).get("name", "").lower() self.stake_currency = self.config.get("stake_currency", "USDT") self.exit_price_side = self.config.get("exit_pricing", {}).get("price_side", "same") # Plot configuration for FreqUI # --------------------------------------------------------------------------------------------- @property def plot_config(self): plot_config = { "main_plot": { "EMA_12": {"color": "LightGreen"}, "EMA_26": {"color": "Yellow"}, } } return plot_config # Calc Total Profit # --------------------------------------------------------------------------------------------- def calc_total_profit( self, trade: "Trade", filled_entries: "Orders", filled_exits: "Orders", exit_rate: float ) -> tuple: """ Calculates the absolute profit for open trades. :param trade: trade object. :param filled_entries: Filled entries list. :param filled_exits: Filled exits list. :param exit_rate: The exit rate. :return tuple: The total profit in stake, ratio, ratio based on current stake, and ratio based on the first entry stake. """ fee_open_rate = trade.fee_open if self.custom_fee_open_rate is None else self.custom_fee_open_rate fee_close_rate = trade.fee_close if self.custom_fee_close_rate is None else self.custom_fee_close_rate total_amount = 0.0 total_stake = 0.0 total_profit = 0.0 current_stake = 0.0 for entry_order in filled_entries: if trade.is_short: entry_stake = entry_order.safe_filled * \ entry_order.safe_price * (1 - fee_open_rate) total_amount += entry_order.safe_filled total_stake += entry_stake total_profit += entry_stake else: entry_stake = entry_order.safe_filled * \ entry_order.safe_price * (1 + fee_open_rate) total_amount += entry_order.safe_filled total_stake += entry_stake total_profit -= entry_stake for exit_order in filled_exits: if trade.is_short: exit_stake = exit_order.safe_filled * \ exit_order.safe_price * (1 + fee_close_rate) total_amount -= exit_order.safe_filled total_profit -= exit_stake else: exit_stake = exit_order.safe_filled * \ exit_order.safe_price * (1 - fee_close_rate) total_amount -= exit_order.safe_filled total_profit += exit_stake if trade.is_short: current_stake = total_amount * exit_rate * (1 + fee_close_rate) total_profit -= current_stake else: current_stake = total_amount * exit_rate * (1 - fee_close_rate) total_profit += current_stake if self.is_futures_mode: total_profit += trade.funding_fees total_profit_ratio = total_profit / total_stake current_profit_ratio = total_profit / current_stake stake_cost = trade.get_custom_data(key="stake_cost") init_profit_ratio = total_profit / (stake_cost if stake_cost else filled_entries[0].cost) return total_profit, total_profit_ratio, current_profit_ratio, init_profit_ratio # Custom Should Exit # --------------------------------------------------------------------------------------------- def should_exit( self, trade: Trade, rate: float, current_time: datetime, *, enter: bool, exit_: bool, low: float | None = None, high: float | None = None, force_stoploss: float = 0, ) -> list: exits = super().should_exit( trade, rate, current_time, enter=enter, exit_=exit_, low=low, high=high, force_stoploss=force_stoploss, ) if not exits: return [] if all(exit_.exit_reason == "liquidation" for exit_ in exits): return [] exits_without_liquidation = [exit_ for exit_ in exits if exit_.exit_reason != "liquidation"] return exits_without_liquidation # Custom Exit # --------------------------------------------------------------------------------------------- def custom_exit( self, pair: str, trade: "Trade", current_time: "datetime", current_rate: float, current_profit: float, **kwargs ): df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if len(df) < 2: return None last_candle = df.iloc[-1].squeeze() previous_candle_1 = df.iloc[-2].squeeze() enter_tag = "empty" if hasattr(trade, "enter_tag") and trade.enter_tag is not None: enter_tag = trade.enter_tag filled_entries = trade.select_filled_orders(trade.entry_side) filled_exits = trade.select_filled_orders(trade.exit_side) profit_stake, profit_ratio, profit_current_stake_ratio, profit_init_ratio = self.calc_total_profit( trade, filled_entries, filled_exits, current_rate ) if not trade.is_short: sell, signal_name = self.long_exit_grind( profit_init_ratio, last_candle, previous_candle_1, ) if sell and (signal_name is not None): return f"{signal_name} ( {enter_tag})" else: # Short trades sell, signal_name = self.short_exit_grind( profit_init_ratio, last_candle, previous_candle_1, ) if sell and (signal_name is not None): return f"{signal_name} ( {enter_tag})" return None # Custom Stake Amount # --------------------------------------------------------------------------------------------- def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: Optional[float], max_stake: float, leverage: float, entry_tag: Optional[str], side: str, **kwargs, ) -> float: stake_multiplier = ( self.grind_v2_stake_multiplier_futures if self.is_futures_mode else self.grind_v2_stake_multiplier_spot ) stake = proposed_stake * stake_multiplier * self.grind_v2_stake_multiplier_first if stake > min_stake: return stake return min_stake # Order filled Callback # --------------------------------------------------------------------------------------------- def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: # Check if it's the first entry and stake_cost doesn't exist if trade.nr_of_successful_entries == 1 and trade.nr_of_successful_exits == 0: if trade.get_custom_data(key="stake_cost") is None: trade.set_custom_data(key="stake_cost", value=order.cost / self.grind_v2_stake_multiplier_first) stake_cost = trade.get_custom_data(key="stake_cost") log.info(f"[{trade.pair}] of trade stake_cost: {stake_cost}") return None # Adjust Trade Position # --------------------------------------------------------------------------------------------- def adjust_trade_position( self, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs, ): if not self.position_adjustment_enable: return None enter_tag = "empty" if hasattr(trade, "enter_tag") and trade.enter_tag is not None: enter_tag = trade.enter_tag enter_tags = enter_tag.split() if not trade.is_short: return self.long_grind_adjust_trade_position_v2( trade, enter_tags, current_time, current_rate, current_profit, min_stake, max_stake, current_entry_rate, current_exit_rate, current_entry_profit, current_exit_profit, ) else: return self.short_grind_adjust_trade_position_v2( trade, enter_tags, current_time, current_rate, current_profit, min_stake, max_stake, current_entry_rate, current_exit_rate, current_entry_profit, current_exit_profit, ) def notification_msg( self, msg_type: str, tag: str, pair: str, rate: float, stake_amount: float, profit_stake: float, profit_ratio: float, grind_profit_stake: float = None, grind_profit_pct: float = None, stake_currency: str = None, coin_amount: float = None, ) -> str: # Headers for different message types headers = { "grinding-entry": f"✅ ​**Grinding entry:** `({tag})`\n", "grinding-exit": f"❎​ ​**Grinding exit:** `({tag})`\n", "grinding-derisk": f"❌​​ ​**Grinding de-risk:** `({tag})`\n", "grinding-stop": f"❌ ​**Grinding stop exit:** `({tag})`\n", "re-entry": f"✅ ​**Re-entry:** `({tag})`\n", "de-risk": f"❌​​ ​**De-risk:** `({tag})`\n", } # Start with the header msg = headers.get(msg_type, None) # Add exchange information msg += f"🏦 **Exchange:** `{self.exchange_name.capitalize()}`\n" # Common fields msg += ( f"🪙 **Pair:** `{pair}`\n" f"〽️ **Rate:** `{rate}`\n" f"💰 **Stake amount:** `{stake_amount:.2f}{'' if stake_currency is None else ' ' + stake_currency}`\n" ) # Add coin amount if available (exit/stop cases) if coin_amount is not None: msg += f"🪙 **Coin amount:** `{coin_amount}`\n" # Profit section profit_pct = profit_ratio * 100 msg += ( f"💵 **Profit (stake):** `{profit_stake:.2f}{'' if stake_currency is None else ' ' + stake_currency}`\n" f"💸 **Profit (percent):** `{profit_pct:.2f}%`\n" ) # Grind profit calculation if grind_profit_stake is not None and grind_profit_stake != 0: msg += f"💶 **Grind profit (stake):** `{grind_profit_stake:.2f}{'' if stake_currency is None else ' ' + stake_currency}`\n" if grind_profit_pct is not None and grind_profit_pct != 0: msg += f"💸 **Grind profit (percent):** `{(grind_profit_pct * 100.0):.2f}%`" return msg # Informative Pairs # --------------------------------------------------------------------------------------------- def informative_pairs(self): # get access to all pairs available in whitelist. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [] for info_timeframe in self.info_timeframes: informative_pairs.extend( [(pair, info_timeframe) for pair in pairs]) return informative_pairs # Informative 1d Timeframe Indicators # --------------------------------------------------------------------------------------------- def informative_1d_indicators(self, metadata: dict, info_timeframe) -> DataFrame: tik = time.perf_counter() assert self.dp, "DataProvider is required for multiple timeframes." # Get the informative pair informative_1d = self.dp.get_pair_dataframe( pair=metadata["pair"], timeframe=info_timeframe) # Indicators # RSI informative_1d["RSI_3"] = pta.rsi(informative_1d["close"], length=3) # ROC informative_1d["ROC_2"] = pta.roc(informative_1d["close"], length=2) informative_1d["ROC_9"] = pta.roc(informative_1d["close"], length=9) # Performance logging # ----------------------------------------------------------------------------------------- tok = time.perf_counter() log.debug( f"[{metadata['pair']}] informative_1d_indicators took: {tok - tik:0.4f} seconds.") return informative_1d # Informative 4h Timeframe Indicators # --------------------------------------------------------------------------------------------- def informative_4h_indicators(self, metadata: dict, info_timeframe) -> DataFrame: tik = time.perf_counter() assert self.dp, "DataProvider is required for multiple timeframes." # Get the informative pair informative_4h = self.dp.get_pair_dataframe( pair=metadata["pair"], timeframe=info_timeframe) # Indicators # RSI informative_4h["RSI_3"] = pta.rsi(informative_4h["close"], length=3) informative_4h["RSI_14"] = pta.rsi(informative_4h["close"], length=14) # ROC informative_4h["ROC_2"] = pta.roc(informative_4h["close"], length=2) informative_4h["ROC_9"] = pta.roc(informative_4h["close"], length=9) # Max highs informative_4h["high_max_12"] = informative_4h["high"].rolling( 12).max() informative_4h["high_max_24"] = informative_4h["high"].rolling( 24).max() # Min lows informative_4h["low_min_12"] = informative_4h["low"].rolling(12).min() informative_4h["low_min_24"] = informative_4h["low"].rolling(24).min() # Performance logging # ----------------------------------------------------------------------------------------- tok = time.perf_counter() log.debug( f"[{metadata['pair']}] informative_4h_indicators took: {tok - tik:0.4f} seconds.") return informative_4h # Informative 1h Timeframe Indicators # --------------------------------------------------------------------------------------------- def informative_1h_indicators(self, metadata: dict, info_timeframe) -> DataFrame: tik = time.perf_counter() assert self.dp, "DataProvider is required for multiple timeframes." # Get the informative pair informative_1h = self.dp.get_pair_dataframe( pair=metadata["pair"], timeframe=info_timeframe) # Indicators # ----------------------------------------------------------------------------------------- # RSI informative_1h["RSI_3"] = pta.rsi(informative_1h["close"], length=3) informative_1h["RSI_14"] = pta.rsi(informative_1h["close"], length=14) # BB 20 - STD2 bbands_20_2 = pta.bbands(informative_1h["close"], length=20) informative_1h["BBB_20_2.0"] = bbands_20_2["BBB_20_2.0"] if isinstance( bbands_20_2, pd.DataFrame) else np.nan # Williams %R informative_1h["WILLR_84"] = pta.willr( informative_1h["high"], informative_1h["low"], informative_1h["close"], length=84 ) # ROC informative_1h["ROC_2"] = pta.roc(informative_1h["close"], length=2) informative_1h["ROC_9"] = pta.roc(informative_1h["close"], length=9) # Max highs informative_1h["high_max_6"] = informative_1h["high"].rolling(6).max() informative_1h["high_max_12"] = informative_1h["high"].rolling( 12).max() # Min lows informative_1h["low_min_6"] = informative_1h["low"].rolling(6).min() informative_1h["low_min_12"] = informative_1h["low"].rolling(12).min() # Performance logging # ----------------------------------------------------------------------------------------- tok = time.perf_counter() log.debug( f"[{metadata['pair']}] informative_1h_indicators took: {tok - tik:0.4f} seconds.") return informative_1h # Informative 15m Timeframe Indicators # --------------------------------------------------------------------------------------------- def informative_15m_indicators(self, metadata: dict, info_timeframe) -> DataFrame: tik = time.perf_counter() assert self.dp, "DataProvider is required for multiple timeframes." # Get the informative pair informative_15m = self.dp.get_pair_dataframe( pair=metadata["pair"], timeframe=info_timeframe) # Indicators # RSI informative_15m["RSI_3"] = pta.rsi(informative_15m["close"], length=3) # AROON aroon_14 = pta.aroon( informative_15m["high"], informative_15m["low"], length=14) informative_15m["AROONU_14"] = aroon_14["AROONU_14"] if isinstance( aroon_14, pd.DataFrame) else np.nan informative_15m["AROOND_14"] = aroon_14["AROOND_14"] if isinstance( aroon_14, pd.DataFrame) else np.nan # Stochastic RSI stochrsi = pta.stochrsi(informative_15m["close"]) informative_15m["STOCHRSIk_14_14_3_3"] = ( stochrsi["STOCHRSIk_14_14_3_3"] if isinstance( stochrsi, pd.DataFrame) else np.nan ) # Performance logging # ----------------------------------------------------------------------------------------- tok = time.perf_counter() log.debug( f"[{metadata['pair']}] informative_15m_indicators took: {tok - tik:0.4f} seconds.") return informative_15m # Coin Pair Base Timeframe Indicators # --------------------------------------------------------------------------------------------- def base_tf_5m_indicators(self, metadata: dict, df: DataFrame) -> DataFrame: tik = time.perf_counter() # RSI df["RSI_3"] = pta.rsi(df["close"], length=3) df["RSI_14"] = pta.rsi(df["close"], length=14) df["RSI_20"] = pta.rsi(df["close"], length=20) # EMA df["EMA_9"] = pta.ema(df["close"], length=9) df["EMA_12"] = pta.ema(df["close"], length=12) df["EMA_16"] = pta.ema(df["close"], length=16) df["EMA_20"] = pta.ema(df["close"], length=20) df["EMA_26"] = pta.ema(df["close"], length=26) df["EMA_100"] = pta.ema(df["close"], length=100, fillna=0.0) # SMA df["SMA_9"] = pta.sma(df["close"], length=9) df["SMA_16"] = pta.sma(df["close"], length=16) df["SMA_21"] = pta.sma(df["close"], length=21) df["SMA_30"] = pta.sma(df["close"], length=30) # BB 20 - STD2 bbands_20_2 = pta.bbands(df["close"], length=20) df["BBL_20_2.0"] = bbands_20_2["BBL_20_2.0"] if isinstance( bbands_20_2, pd.DataFrame) else np.nan df["BBU_20_2.0"] = bbands_20_2["BBU_20_2.0"] if isinstance( bbands_20_2, pd.DataFrame) else np.nan # Williams %R df["WILLR_14"] = pta.willr( df["high"], df["low"], df["close"], length=14) # AROON aroon_14 = pta.aroon(df["high"], df["low"], length=14) df["AROONU_14"] = aroon_14["AROONU_14"] if isinstance( aroon_14, pd.DataFrame) else np.nan df["AROOND_14"] = aroon_14["AROOND_14"] if isinstance( aroon_14, pd.DataFrame) else np.nan # Stochastic RSI stochrsi = pta.stochrsi(df["close"]) df["STOCHRSIk_14_14_3_3"] = stochrsi["STOCHRSIk_14_14_3_3"] if isinstance( stochrsi, pd.DataFrame) else np.nan # Close max df["close_max_12"] = df["close"].rolling(12).max() df["close_max_48"] = df["close"].rolling(48).max() # Close min df["close_min_12"] = df["close"].rolling(12).min() df["close_min_48"] = df["close"].rolling(48).min() # Performance logging # ----------------------------------------------------------------------------------------- tok = time.perf_counter() log.debug( f"[{metadata['pair']}] base_tf_5m_indicators took: {tok - tik:0.4f} seconds.") return df # Coin Pair Indicator Switch Case # --------------------------------------------------------------------------------------------- def info_switcher(self, metadata: dict, info_timeframe) -> DataFrame: if info_timeframe == "1d": return self.informative_1d_indicators(metadata, info_timeframe) elif info_timeframe == "4h": return self.informative_4h_indicators(metadata, info_timeframe) elif info_timeframe == "1h": return self.informative_1h_indicators(metadata, info_timeframe) elif info_timeframe == "15m": return self.informative_15m_indicators(metadata, info_timeframe) else: raise RuntimeError( f"{info_timeframe} not supported as informative timeframe for BTC pair.") # Populate Indicators # --------------------------------------------------------------------------------------------- def populate_indicators(self, df: DataFrame, metadata: dict) -> DataFrame: tik = time.perf_counter() """ --> Indicators on informative timeframes ___________________________________________________________________________________________ """ for info_timeframe in self.info_timeframes: info_indicators = self.info_switcher(metadata, info_timeframe) df = merge_informative_pair( df, info_indicators, self.timeframe, info_timeframe, ffill=True) # Customize what we drop - in case we need to maintain some informative timeframe ohlcv data # Default drop all except base timeframe ohlcv data drop_columns = { "1d": [f"{s}_{info_timeframe}" for s in ["date", "open", "high", "low", "close", "volume"]], "4h": [f"{s}_{info_timeframe}" for s in ["date", "open", "high", "low", "close", "volume"]], "1h": [f"{s}_{info_timeframe}" for s in ["date", "open", "high", "low", "close", "volume"]], "15m": [f"{s}_{info_timeframe}" for s in ["date", "high", "low", "volume"]], }.get(info_timeframe, [f"{s}_{info_timeframe}" for s in ["date", "open", "high", "low", "close", "volume"]]) df.drop(columns=df.columns.intersection( drop_columns), inplace=True) """ --> The indicators for the base timeframe (5m) ___________________________________________________________________________________________ """ df = self.base_tf_5m_indicators(metadata, df) df["RSI_14_1h"] = df["RSI_14_1h"].astype(np.float64).replace( to_replace=[np.nan, None], value=(50.0)) tok = time.perf_counter() log.debug( f"[{metadata['pair']}] Populate indicators took a total of: {tok - tik:0.4f} seconds.") return df # Confirm Trade Exit # --------------------------------------------------------------------------------------------- def confirm_trade_exit( self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, time_in_force: str, exit_reason: str, current_time: datetime, **kwargs, ) -> bool: # Allow force exits if exit_reason != "force_exit": if exit_reason in ["stop_loss", "trailing_stop_loss"]: return False return True # Bot Loop Start # --------------------------------------------------------------------------------------------- def bot_loop_start(self, current_time: datetime, **kwargs) -> None: if self.config["runmode"].value not in ("live", "dry_run"): return super().bot_loop_start(datetime, **kwargs) # Check and set stake_cost for existing trades that don't have it trades = Trade.get_trades_proxy(is_open=True) for trade in trades: stake_cost = trade.get_custom_data(key="stake_cost") if stake_cost is None: filled_entries = trade.select_filled_orders(trade.entry_side) if filled_entries: # Use the first entry order's cost as stake_cost first_entry = filled_entries[0] trade.set_custom_data(key="stake_cost", value=first_entry.cost / self.grind_v2_stake_multiplier_first) log.info(f"[{trade.pair}] Set stake_cost to {first_entry.cost} for existing trade") return super().bot_loop_start(current_time, **kwargs) # Leverage # --------------------------------------------------------------------------------------------- def leverage( self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, **kwargs, ) -> float: return self.futures_mode_leverage # Correct Min Stake # --------------------------------------------------------------------------------------------- def correct_min_stake(self, min_stake: float) -> float: if self.exchange_name == "bybit": if self.is_futures_mode: if min_stake < 5.0 / self.futures_mode_leverage: min_stake = 5.0 / self.futures_mode_leverage return min_stake def is_backtest_mode(self) -> bool: """Check if the current run mode is backtest or hyperopt""" return self.dp.runmode.value in ["backtest", "hyperopt"] # Populate Exit Trend # --------------------------------------------------------------------------------------------- def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame: df.loc[:, "exit_long"] = 0 df.loc[:, "exit_short"] = 0 return df # Populate Entry Trend # --------------------------------------------------------------------------------------------- def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame: long_entry_conditions = [] short_entry_conditions = [] df.loc[:, "enter_tag"] = "" df.loc[:, "enter_long"] = 0 df.loc[:, "enter_short"] = 0 long_entry_conditions.append(df["STOCHRSIk_14_14_3_3"] < 20.0) long_entry_conditions.append(df["WILLR_14"] < -80.0) long_entry_conditions.append(df["AROONU_14"] < 25.0) long_entry_conditions.append(df["close"] < (df["EMA_20"] * 0.978)) long_entry_conditions.append(df["volume"] > 0) short_entry_conditions.append(df["EMA_12"] > df["EMA_26"]) short_entry_conditions.append((df["EMA_12"] - df["EMA_26"]) > (df["open"] * 0.010)) short_entry_conditions.append((df["EMA_12"].shift() - df["EMA_26"].shift()) > (df["open"] / 100.0)) short_entry_conditions.append(df["close"] > (df["BBU_20_2.0"] * 1.002)) # if long_entry_conditions: # df.loc[:, "enter_long"] = reduce( # lambda x, y: x & y, long_entry_conditions) # if short_entry_conditions: # df.loc[:, "enter_short"] = reduce( # lambda x, y: x & y, short_entry_conditions) return df ############################################################################################### # Long Exit Grind # --------------------------------------------------------------------------------------------- def long_exit_grind( self, profit_init_ratio: float, last_candle, previous_candle_1, ) -> tuple: if profit_init_ratio > self.grind_v2_profit_exit_threshold: return True, f"exit_{self.long_grind_mode_name}_g" # Here ends exit signal conditions for long_exit_grind return False, None # Long Grinding Adjust Trade Position v2 # --------------------------------------------------------------------------------------------- def long_grind_adjust_trade_position_v2( self, trade: Trade, enter_tags, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs, ): is_backtest = self.is_backtest_mode() min_stake = self.correct_min_stake(min_stake) df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) if len(df) < 2: return None last_candle = df.iloc[-1].squeeze() previous_candle = df.iloc[-2].squeeze() # we already waiting for an order to get filled if trade.has_open_orders: return None filled_orders = trade.select_filled_orders() filled_entries = trade.select_filled_orders(trade.entry_side) filled_exits = trade.select_filled_orders(trade.exit_side) exit_rate = current_rate if self.dp.runmode.value in ("live", "dry_run"): ticker = self.dp.ticker(trade.pair) if ("bid" in ticker) and ("ask" in ticker): if trade.is_short: if self.exit_price_side in ["ask", "other"]: if ticker["ask"] is not None: exit_rate = ticker["ask"] else: if self.exit_price_side in ["bid", "other"]: if ticker["bid"] is not None: exit_rate = ticker["bid"] profit_stake, profit_ratio, profit_current_stake_ratio, profit_init_ratio = self.calc_total_profit( trade, filled_entries, filled_exits, exit_rate ) current_stake_amount = trade.amount * exit_rate stake_cost = trade.get_custom_data(key="stake_cost") slice_amount = stake_cost if stake_cost else filled_entries[0].cost slice_profit = ( exit_rate - filled_orders[-1].safe_price) / filled_orders[-1].safe_price has_order_tags = False if hasattr(filled_orders[0], "ft_order_tag"): has_order_tags = True fee_open_rate = trade.fee_open if self.custom_fee_open_rate is None else self.custom_fee_open_rate fee_close_rate = trade.fee_close if self.custom_fee_close_rate is None else self.custom_fee_close_rate grind_1_max_sub_grinds = 0 grind_1_stakes = ( self.grinding_v2_grind_1_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_1_stakes_spot.copy() ) grind_1_sub_thresholds = ( self.grinding_v2_grind_1_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_1_thresholds_spot ) if (slice_amount * grind_1_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_1_stakes[0] * trade.leverage for i, _ in enumerate(grind_1_stakes): grind_1_stakes[i] *= multi grind_1_max_sub_grinds = len(grind_1_stakes) grind_1_derisk_grinds = ( self.grinding_v2_grind_1_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_1_derisk_spot ) grind_1_profit_threshold = ( self.grinding_v2_grind_1_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_1_profit_threshold_spot ) grind_2_max_sub_grinds = 0 grind_2_stakes = ( self.grinding_v2_grind_2_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_2_stakes_spot.copy() ) grind_2_sub_thresholds = ( self.grinding_v2_grind_2_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_2_thresholds_spot ) if (slice_amount * grind_2_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_2_stakes[0] * trade.leverage for i, _ in enumerate(grind_2_stakes): grind_2_stakes[i] *= multi grind_2_max_sub_grinds = len(grind_2_stakes) grind_2_derisk_grinds = ( self.grinding_v2_grind_2_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_2_derisk_spot ) grind_2_profit_threshold = ( self.grinding_v2_grind_2_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_2_profit_threshold_spot ) grind_3_max_sub_grinds = 0 grind_3_stakes = ( self.grinding_v2_grind_3_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_3_stakes_spot.copy() ) grind_3_sub_thresholds = ( self.grinding_v2_grind_3_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_3_thresholds_spot ) if (slice_amount * grind_3_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_3_stakes[0] * trade.leverage for i, _ in enumerate(grind_3_stakes): grind_3_stakes[i] *= multi grind_3_max_sub_grinds = len(grind_3_stakes) grind_3_derisk_grinds = ( self.grinding_v2_grind_3_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_3_derisk_spot ) grind_3_profit_threshold = ( self.grinding_v2_grind_3_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_3_profit_threshold_spot ) grind_x_max_sub_grinds = 0 grind_x_stakes = ( self.grinding_v2_grind_x_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_x_stakes_spot.copy() ) grind_x_sub_thresholds = ( self.grinding_v2_grind_x_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_x_thresholds_spot ) if (slice_amount * grind_x_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_x_stakes[0] * trade.leverage for i, _ in enumerate(grind_x_stakes): grind_x_stakes[i] *= multi grind_x_max_sub_grinds = len(grind_x_stakes) grind_x_derisk_grinds = ( self.grinding_v2_grind_x_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_x_derisk_spot ) grind_x_profit_threshold = ( self.grinding_v2_grind_x_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_x_profit_threshold_spot ) grind_1_sub_grind_count = 0 grind_1_total_amount = 0.0 grind_1_total_cost = 0.0 grind_1_current_open_rate = 0.0 grind_1_current_grind_stake = 0.0 grind_1_current_grind_stake_profit = 0.0 grind_1_is_exit_found = False grind_1_found = False grind_1_buy_orders = [] grind_1_orders = [] grind_1_distance_ratio = 0.0 grind_2_sub_grind_count = 0 grind_2_total_amount = 0.0 grind_2_total_cost = 0.0 grind_2_current_open_rate = 0.0 grind_2_current_grind_stake = 0.0 grind_2_current_grind_stake_profit = 0.0 grind_2_is_exit_found = False grind_2_found = False grind_2_buy_orders = [] grind_2_orders = [] grind_2_distance_ratio = 0.0 grind_3_sub_grind_count = 0 grind_3_total_amount = 0.0 grind_3_total_cost = 0.0 grind_3_current_open_rate = 0.0 grind_3_current_grind_stake = 0.0 grind_3_current_grind_stake_profit = 0.0 grind_3_is_exit_found = False grind_3_found = False grind_3_buy_orders = [] grind_3_orders = [] grind_3_distance_ratio = 0.0 grind_x_sub_grind_count = 0 grind_x_total_amount = 0.0 grind_x_total_cost = 0.0 grind_x_current_open_rate = 0.0 grind_x_current_grind_stake = 0.0 grind_x_current_grind_stake_profit = 0.0 grind_x_is_exit_found = False grind_x_found = False grind_x_buy_orders = [] grind_x_orders = [] grind_x_distance_ratio = 0.0 for order in reversed(filled_orders): if order.ft_order_side == "buy": order_tag = "" if has_order_tags: if order.ft_order_tag is not None: order_tag = order.ft_order_tag if not grind_1_is_exit_found and order_tag == "grind_1_entry": grind_1_sub_grind_count += 1 grind_1_total_amount += order.safe_filled grind_1_total_cost += order.safe_filled * order.safe_price grind_1_buy_orders.append(order.id) grind_1_orders.append(order) if not grind_1_found: grind_1_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_1_found = True elif not grind_2_is_exit_found and order_tag == "grind_2_entry": grind_2_sub_grind_count += 1 grind_2_total_amount += order.safe_filled grind_2_total_cost += order.safe_filled * order.safe_price grind_2_buy_orders.append(order.id) grind_2_orders.append(order) if not grind_2_found: grind_2_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_2_found = True elif not grind_3_is_exit_found and order_tag == "grind_3_entry": grind_3_sub_grind_count += 1 grind_3_total_amount += order.safe_filled grind_3_total_cost += order.safe_filled * order.safe_price grind_3_buy_orders.append(order.id) grind_3_orders.append(order) if not grind_3_found: grind_3_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_3_found = True elif not grind_x_is_exit_found and order_tag not in ["grind_1_entry", "grind_2_entry", "grind_3_entry"]: grind_x_sub_grind_count += 1 grind_x_total_amount += order.safe_filled grind_x_total_cost += order.safe_filled * order.safe_price grind_x_buy_orders.append(order.id) grind_x_orders.append(order) if not grind_x_found: grind_x_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_x_found = True elif order.ft_order_side == "sell": order_tag = "" if has_order_tags: if order.ft_order_tag is not None: sell_order_tag = order.ft_order_tag order_mode = sell_order_tag.split(" ", 1) if len(order_mode) > 0: order_tag = order_mode[0] if not grind_1_is_exit_found and order_tag in ["grind_1_exit", "grind_1_derisk"]: grind_1_is_exit_found = True elif not grind_2_is_exit_found and order_tag in ["grind_2_exit", "grind_2_derisk"]: grind_2_is_exit_found = True elif not grind_3_is_exit_found and order_tag in ["grind_3_exit", "grind_3_derisk"]: grind_3_is_exit_found = True elif not grind_x_is_exit_found and order_tag in ["grind_x_exit", "grind_x_derisk", "derisk_v2", "grind_v2_exit"]: grind_x_is_exit_found = True if grind_1_sub_grind_count > 0: grind_1_current_open_rate = grind_1_total_cost / grind_1_total_amount grind_1_current_grind_stake = grind_1_total_amount * \ exit_rate * (1 - trade.fee_close) grind_1_current_grind_stake_profit = grind_1_current_grind_stake - grind_1_total_cost if grind_2_sub_grind_count > 0: grind_2_current_open_rate = grind_2_total_cost / grind_2_total_amount grind_2_current_grind_stake = grind_2_total_amount * \ exit_rate * (1 - trade.fee_close) grind_2_current_grind_stake_profit = grind_2_current_grind_stake - grind_2_total_cost if grind_3_sub_grind_count > 0: grind_3_current_open_rate = grind_3_total_cost / grind_3_total_amount grind_3_current_grind_stake = grind_3_total_amount * \ exit_rate * (1 - trade.fee_close) grind_3_current_grind_stake_profit = grind_3_current_grind_stake - grind_3_total_cost if grind_x_sub_grind_count > 0: grind_x_current_open_rate = grind_x_total_cost / grind_x_total_amount grind_x_current_grind_stake = grind_x_total_amount * \ exit_rate * (1 - trade.fee_close) grind_x_current_grind_stake_profit = grind_x_current_grind_stake - grind_x_total_cost # all grinds num_open_grinds = grind_1_sub_grind_count + grind_2_sub_grind_count + grind_3_sub_grind_count + grind_x_sub_grind_count # not reached the max allowed stake for all grinds is_not_trade_max_stake = current_stake_amount < (slice_amount * self.grinding_v2_max_stake) is_long_extra_checks_entry = ( (current_time - timedelta(minutes=5) > filled_entries[-1].order_filled_utc) and ((current_time - timedelta(hours=2) > filled_orders[-1].order_filled_utc) or (slice_profit < -0.06)) ) is_long_grind_entry = ( self.long_grind_entry_v2( last_candle, previous_candle, slice_profit) or ( (num_open_grinds == 0) and ( (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 20.0) and (last_candle["RSI_3_1h"] > 20.0) and (last_candle["RSI_3_1h"] > 20.0) and (last_candle["AROONU_14"] < 50.0) and (last_candle["AROONU_14_15m"] < 50.0) ) ) or ( self.is_futures_mode and trade.liquidation_price is not None and ( (trade.is_short and current_rate > trade.liquidation_price * 0.90) or (not trade.is_short and current_rate < trade.liquidation_price * 1.10) ) and (slice_profit < -0.03) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 20.0) and (last_candle["AROONU_14"] < 50.0) and (last_candle["AROONU_14_15m"] < 50.0) ) ) # Grinding 1 if ( self.grinding_v2_grind_1_enable and is_long_grind_entry and is_long_extra_checks_entry and (grind_1_sub_grind_count < grind_1_max_sub_grinds) and ( (grind_1_sub_grind_count == 0) or (grind_1_distance_ratio < grind_1_sub_thresholds[grind_1_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_1_stakes[grind_1_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_1_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_1_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_1_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount if grind_1_sub_grind_count > 0: grind_profit = (exit_rate - grind_1_current_open_rate) / \ grind_1_current_open_rate if (grind_profit > (grind_1_profit_threshold + fee_open_rate + fee_close_rate)) and self.long_grind_exit_v2( last_candle, previous_candle, slice_profit ): sell_amount = grind_1_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_1_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_1_total_amount, ) ) log.info( f"Grinding exit (grind_1_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_1_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_1_exit" for grind_entry_id in grind_1_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount if ( self.grinding_v2_grind_1_use_derisk and (grind_1_sub_grind_count > 0) and (((exit_rate - grind_1_current_open_rate) / grind_1_current_open_rate) < grind_1_derisk_grinds) and (grind_1_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_1_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_1_current_open_rate > 0.0: grind_profit = ( ((exit_rate - grind_1_current_open_rate) / grind_1_current_open_rate) if grind_1_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_1_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_1_current_grind_stake_profit, grind_profit_pct=grind_profit, coin_amount=grind_1_total_amount, ) ) log.info( f"Grinding de-risk (grind_1_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_1_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_1_current_grind_stake_profit} {self.stake_currency})" ) order_tag = "grind_1_derisk" for grind_entry_id in grind_1_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 2 if ( self.grinding_v2_grind_2_enable and is_long_grind_entry and is_long_extra_checks_entry and (grind_2_sub_grind_count < grind_2_max_sub_grinds) and ( (grind_2_sub_grind_count == 0) and (grind_1_sub_grind_count > 1) or (grind_2_distance_ratio < grind_2_sub_thresholds[grind_2_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_2_stakes[grind_2_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_2_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_2_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_2_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount if grind_2_sub_grind_count > 0: grind_profit = (exit_rate - grind_2_current_open_rate) / \ grind_2_current_open_rate if (grind_profit > (grind_2_profit_threshold + fee_open_rate + fee_close_rate)) and self.long_grind_exit_v2( last_candle, previous_candle, slice_profit ): sell_amount = grind_2_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_2_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_2_total_amount, ) ) log.info( f"Grinding exit (grind_2_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_2_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_2_exit" for grind_entry_id in grind_2_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount if ( self.grinding_v2_grind_2_use_derisk and (grind_2_sub_grind_count > 0) and (((exit_rate - grind_2_current_open_rate) / grind_2_current_open_rate) < grind_2_derisk_grinds) and (grind_2_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_2_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_2_current_open_rate > 0.0: grind_profit = ( ((exit_rate - grind_2_current_open_rate) / grind_2_current_open_rate) if grind_2_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_2_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_2_current_grind_stake_profit, grind_profit_pct=grind_profit, coin_amount=grind_2_total_amount, ) ) log.info( f"Grinding de-risk (grind_2_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_2_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_2_current_grind_stake_profit} {self.stake_currency})" ) order_tag = "grind_2_derisk" for grind_entry_id in grind_2_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 3 Entry if ( self.grinding_v2_grind_3_enable and is_long_grind_entry and is_long_extra_checks_entry and (grind_3_sub_grind_count < grind_3_max_sub_grinds) and ( (grind_3_sub_grind_count == 0) and (grind_2_sub_grind_count > 1) or (grind_3_distance_ratio < grind_3_sub_thresholds[grind_3_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * grind_3_stakes[grind_3_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_3_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_3_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_3_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount # Grinding 3 Exit if grind_3_sub_grind_count > 0: grind_profit = (exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate if (grind_profit > (grind_3_profit_threshold + fee_open_rate + fee_close_rate)) and self.long_grind_exit_v2( last_candle, previous_candle, slice_profit ): sell_amount = grind_3_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_3_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_3_total_amount, ) ) log.info( f"Grinding exit (grind_3_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_3_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_3_exit" for grind_entry_id in grind_3_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 3 De-risk if ( self.grinding_v2_grind_3_use_derisk and (grind_3_sub_grind_count > 0) and (((exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate) < grind_3_derisk_grinds) and (grind_3_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_3_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_3_current_open_rate > 0.0: grind_profit = ( ((exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate) if grind_3_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_3_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_3_current_grind_stake_profit, grind_profit_pct=grind_profit, coin_amount=grind_3_total_amount, ) ) log.info( f"Grinding de-risk (grind_3_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_3_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_3_current_grind_stake_profit} {self.stake_currency})" ) order_tag = "grind_3_derisk" for grind_entry_id in grind_3_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding X if ( self.grinding_v2_grind_x_enable and is_long_grind_entry and is_long_extra_checks_entry and (grind_x_sub_grind_count < grind_x_max_sub_grinds) and grind_x_sub_grind_count != 0 and (grind_x_distance_ratio < grind_x_sub_thresholds[grind_x_sub_grind_count]) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_x_stakes[grind_x_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_x_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_x_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_x_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount # Grinding X - Exit if grind_x_sub_grind_count > 0: grind_profit = (exit_rate - grind_x_current_open_rate) / \ grind_x_current_open_rate if (grind_profit > (grind_x_profit_threshold + fee_open_rate + fee_close_rate)) and self.long_grind_exit_v2( last_candle, previous_candle, slice_profit ): sell_amount = grind_x_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_x_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_x_total_amount, ) ) log.info( f"Grinding exit (grind_x_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_x_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_x_exit" for grind_entry_id in grind_x_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding X De-risk if ( self.grinding_v2_grind_x_use_derisk and (grind_x_sub_grind_count > 0) and (((exit_rate - grind_x_current_open_rate) / grind_x_current_open_rate) < grind_x_derisk_grinds) and (grind_x_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_x_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_x_current_open_rate > 0.0: grind_profit = ( ((exit_rate - grind_x_current_open_rate) / grind_x_current_open_rate) if grind_x_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_x_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_x_current_grind_stake_profit, grind_profit_pct=grind_profit, coin_amount=grind_x_total_amount, ) ) log.info( f"Grinding de-risk (grind_x_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_x_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}% ({grind_x_current_grind_stake_profit} {self.stake_currency})" ) order_tag = "grind_x_derisk" for grind_entry_id in grind_x_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount return None def long_grind_entry_v2(self, last_candle: Series, previous_candle: Series, slice_profit: float) -> float: if ( (last_candle["enter_long"] == True) or ( (last_candle["RSI_14"] < 46.0) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 15.0) and (last_candle["RSI_3_1h"] > 15.0) and (last_candle["RSI_3_4h"] > 15.0) and (last_candle["ROC_2_1h"] > -10.0) and (last_candle["ROC_2_4h"] > -10.0) and (last_candle["ROC_2_1d"] > -10.0) and (last_candle["ROC_9_1h"] > -25.0) and (last_candle["ROC_9_4h"] > -25.0) and (last_candle["ROC_9_1d"] > -25.0) and (last_candle["AROONU_14"] < 25.0) and (last_candle["close"] > (last_candle["close_max_48"] * 0.90)) and (last_candle["close"] > (last_candle["high_max_6_1h"] * 0.85)) and (last_candle["close"] > (last_candle["high_max_12_1h"] * 0.80)) and (last_candle["close"] < (last_candle["low_min_24_4h"] * 1.20)) and (last_candle["close"] < (last_candle["EMA_16"] * 0.968)) ) or ( (last_candle["RSI_14"] < 36.0) and (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 15.0) and (last_candle["RSI_3_1h"] > 15.0) and (last_candle["RSI_3_4h"] > 15.0) and (last_candle["ROC_2_1h"] > -10.0) and (last_candle["ROC_2_4h"] > -10.0) and (last_candle["ROC_9_1h"] > -10.0) and (last_candle["ROC_9_4h"] > -10.0) and (last_candle["ROC_9_1d"] > -30.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 50.0) and (last_candle["EMA_26"] > last_candle["EMA_12"]) and ((last_candle["EMA_26"] - last_candle["EMA_12"]) > (last_candle["open"] * 0.020)) and ((previous_candle["EMA_26"] - previous_candle["EMA_12"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_14"] < 36.0) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["RSI_3_1d"] > 10.0) and (last_candle["ROC_2_1h"] > -5.0) and (last_candle["ROC_2_4h"] > -5.0) and (last_candle["ROC_2_1d"] > -5.0) and (last_candle["ROC_9_1h"] > -10.0) and (last_candle["ROC_9_4h"] > -10.0) and (last_candle["ROC_9_1d"] > -10.0) and (last_candle["AROONU_14_15m"] < 25.0) and (last_candle["close"] > (last_candle["close_max_48"] * 0.90)) and (last_candle["close"] > (last_candle["high_max_6_1h"] * 0.85)) and (last_candle["close"] > (last_candle["high_max_12_1h"] * 0.80)) and (last_candle["close"] < (last_candle["EMA_12"] * 0.980)) ) or ( (last_candle["RSI_14"] < 36.0) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["RSI_3_1d"] > 10.0) and (last_candle["ROC_2_1h"] > -10.0) and (last_candle["ROC_2_4h"] > -10.0) and (last_candle["ROC_2_1d"] > -10.0) and (last_candle["AROONU_14"] < 25.0) and (last_candle["close"] > (last_candle["close_max_48"] * 0.90)) and (last_candle["close"] > (last_candle["high_max_6_1h"] * 0.85)) and (last_candle["close"] > (last_candle["high_max_12_1h"] * 0.80)) and (last_candle["close"] < (last_candle["EMA_26"] * 0.962)) and (last_candle["close"] < (last_candle["BBL_20_2.0"] * 0.999)) ) or ( (last_candle["RSI_14"] < 35.0) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["ROC_2_1h"] > -10.0) and (last_candle["ROC_2_4h"] > -10.0) and (last_candle["ROC_2_1d"] > -10.0) and (last_candle["ROC_9_1h"] > -10.0) and (last_candle["ROC_9_4h"] > -10.0) and (last_candle["AROONU_14"] < 25.0) and (last_candle["close"] < (last_candle["low_min_12_4h"] * 1.25)) and (last_candle["close"] < (last_candle["EMA_9"] * 0.968)) and (last_candle["close"] < (last_candle["EMA_20"] * 0.980)) ) or ( (last_candle["RSI_14"] > 35.0) and (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3"] < 40.0) and (last_candle["RSI_3_15m"] > 15.0) and (last_candle["ROC_2_1h"] > -5.0) and (last_candle["ROC_2_4h"] > -5.0) and (last_candle["ROC_9_1h"] > -10.0) and (last_candle["ROC_9_4h"] > -10.0) and (last_candle["AROONU_14"] < 25.0) and (last_candle["RSI_20"] < previous_candle["RSI_20"]) and (last_candle["close"] < (last_candle["SMA_16"] * 0.955)) ) or ( (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["ROC_2_1h"] > -5.0) and (last_candle["ROC_2_4h"] > -5.0) and (last_candle["ROC_9_1h"] > -5.0) and (last_candle["ROC_9_4h"] > -5.0) and (last_candle["WILLR_14"] < -50.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 20.0) and (last_candle["WILLR_84_1h"] < -70.0) and (last_candle["close"] < (last_candle["low_min_24_4h"] * 1.30)) and (last_candle["BBB_20_2.0_1h"] > 12.0) and (last_candle["close_max_48"] >= (last_candle["close"] * 1.10)) ) or ( (last_candle["RSI_3"] < 30.0) and (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 5.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["ROC_9_1d"] > -30.0) and (last_candle["EMA_26"] > last_candle["EMA_12"]) and ((last_candle["EMA_26"] - last_candle["EMA_12"]) > (last_candle["open"] * 0.034)) and ((previous_candle["EMA_26"] - previous_candle["EMA_12"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 25.0) and (last_candle["RSI_3_1h"] > 30.0) and (last_candle["close"] < (last_candle["high_max_24_4h"] * 0.90)) and (last_candle["close"] < (last_candle["close_max_48"] * 0.90)) and (last_candle["close"] > (last_candle["close_min_12"] * 1.08)) ) or ( (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 5.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 20.0) and (last_candle["RSI_14"] < (last_candle["RSI_14_1h"] - 45.0)) ) or ( (last_candle["RSI_3"] > 10.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["RSI_3_1d"] > 10.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 20.0) and (last_candle["close"] < (last_candle["SMA_30"] * 0.978)) and (last_candle["close"] < (last_candle["BBL_20_2.0"] * 0.999)) ) or ( (last_candle["RSI_14"] < 36.0) and (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["RSI_3_1d"] > 10.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 30.0) and (last_candle["close"] > (last_candle["close_max_48"] * 0.85)) and (last_candle["close"] > (last_candle["high_max_6_1h"] * 0.80)) and (last_candle["close"] > (last_candle["high_max_12_1h"] * 0.75)) and (last_candle["close"] < (last_candle["low_min_12_4h"] * 1.25)) and (last_candle["EMA_26"] > last_candle["EMA_12"]) and ((last_candle["EMA_26"] - last_candle["EMA_12"]) > (last_candle["open"] * 0.018)) and ((previous_candle["EMA_26"] - previous_candle["EMA_12"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_3"] > 5.0) and (previous_candle["SMA_9"] < previous_candle["SMA_21"]) and (last_candle["SMA_9"] > last_candle["SMA_21"]) and (last_candle["close"] < (last_candle["EMA_100"] * 0.984)) and (last_candle["RSI_3_1h"] > 20.0) and (last_candle["RSI_3_4h"] > 20.0) ) or ( (slice_profit < -0.12) and (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_15m"] > 10.0) and (last_candle["RSI_14"] < 40.0) and (last_candle["AROONU_14"] < 25.0) and (last_candle["AROONU_14_15m"] < 30.0) and (last_candle["STOCHRSIk_14_14_3_3"] < 20.0) and (last_candle["STOCHRSIk_14_14_3_3_15m"] < 30.0) and (last_candle["RSI_14_1h"] < 50.0) and (last_candle["RSI_14_4h"] < 50.0) ) or ( (last_candle["RSI_14"] < 36.0) and (last_candle["RSI_3"] > 5.0) and (last_candle["RSI_3_1h"] > 10.0) and (last_candle["RSI_3_4h"] > 10.0) and (last_candle["close"] < (last_candle["EMA_12"] * 0.999)) and (last_candle["close"] < (last_candle["BBL_20_2.0"] * 0.996)) ) ): return True return False def long_grind_exit_v2(self, last_candle: Series, previous_candle: Series, slice_profit: float) -> float: if ( (last_candle["RSI_3"] > 99.0) or (last_candle["RSI_14"] > 70.0) or (last_candle["WILLR_14"] > -0.1) or (last_candle["STOCHRSIk_14_14_3_3"] > 95.0) or (last_candle["close"] > (last_candle["BBU_20_2.0"] * 1.01)) or ((last_candle["RSI_3"] > 90.0) and (last_candle["RSI_14"] < 50.0)) ): return True return False ############################################################################################### # SHORT EXIT FUNCTIONS STARTS HERE ############################################################################################### # Short Exit Grind # --------------------------------------------------------------------------------------------- def short_exit_grind( self, profit_init_ratio: float, last_candle, previous_candle_1, ) -> tuple: if profit_init_ratio > self.grind_v2_profit_exit_threshold: return True, f"exit_{self.short_grind_mode_name}_g" # Here ends exit signal conditions for short_exit_grind return False, None ############################################################################################### # SHORT GRIND FUNCTIONS STARTS HERE ############################################################################################### # Short Grinding Adjust Trade Position v2 # --------------------------------------------------------------------------------------------- def short_grind_adjust_trade_position_v2( self, trade: Trade, enter_tags, current_time: datetime, current_rate: float, current_profit: float, min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs, ): is_backtest = self.is_backtest_mode() min_stake = self.correct_min_stake(min_stake) df, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) if len(df) < 2: return None last_candle = df.iloc[-1].squeeze() previous_candle = df.iloc[-2].squeeze() # we already waiting for an order to get filled if trade.has_open_orders: return None filled_orders = trade.select_filled_orders() filled_entries = trade.select_filled_orders(trade.entry_side) filled_exits = trade.select_filled_orders(trade.exit_side) exit_rate = current_rate if self.dp.runmode.value in ("live", "dry_run"): ticker = self.dp.ticker(trade.pair) if ("bid" in ticker) and ("ask" in ticker): if trade.is_short: if self.exit_price_side in ["ask", "other"]: if ticker["ask"] is not None: exit_rate = ticker["ask"] else: if self.exit_price_side in ["bid", "other"]: if ticker["bid"] is not None: exit_rate = ticker["bid"] profit_stake, profit_ratio, profit_current_stake_ratio, profit_init_ratio = self.calc_total_profit( trade, filled_entries, filled_exits, exit_rate ) current_stake_amount = trade.amount * exit_rate stake_cost = trade.get_custom_data(key="stake_cost") slice_amount = stake_cost if stake_cost else filled_entries[0].cost slice_profit = ( exit_rate - filled_orders[-1].safe_price) / filled_orders[-1].safe_price has_order_tags = False if hasattr(filled_orders[0], "ft_order_tag"): has_order_tags = True fee_open_rate = trade.fee_open if self.custom_fee_open_rate is None else self.custom_fee_open_rate fee_close_rate = trade.fee_close if self.custom_fee_close_rate is None else self.custom_fee_close_rate grind_1_max_sub_grinds = 0 grind_1_stakes = ( self.grinding_v2_grind_1_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_1_stakes_spot.copy() ) grind_1_sub_thresholds = ( self.grinding_v2_grind_1_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_1_thresholds_spot ) if (slice_amount * grind_1_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_1_stakes[0] * trade.leverage for i, _ in enumerate(grind_1_stakes): grind_1_stakes[i] *= multi grind_1_max_sub_grinds = len(grind_1_stakes) grind_1_derisk_grinds = ( self.grinding_v2_grind_1_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_1_derisk_spot ) grind_1_profit_threshold = ( self.grinding_v2_grind_1_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_1_profit_threshold_spot ) grind_2_max_sub_grinds = 0 grind_2_stakes = ( self.grinding_v2_grind_2_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_2_stakes_spot.copy() ) grind_2_sub_thresholds = ( self.grinding_v2_grind_2_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_2_thresholds_spot ) if (slice_amount * grind_2_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_2_stakes[0] * trade.leverage for i, _ in enumerate(grind_2_stakes): grind_2_stakes[i] *= multi grind_2_max_sub_grinds = len(grind_2_stakes) grind_2_derisk_grinds = ( self.grinding_v2_grind_2_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_2_derisk_spot ) grind_2_profit_threshold = ( self.grinding_v2_grind_2_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_2_profit_threshold_spot ) grind_3_max_sub_grinds = 0 grind_3_stakes = ( self.grinding_v2_grind_3_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_3_stakes_spot.copy() ) grind_3_sub_thresholds = ( self.grinding_v2_grind_3_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_3_thresholds_spot ) if (slice_amount * grind_3_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_3_stakes[0] * trade.leverage for i, _ in enumerate(grind_3_stakes): grind_3_stakes[i] *= multi grind_3_max_sub_grinds = len(grind_3_stakes) grind_3_derisk_grinds = ( self.grinding_v2_grind_3_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_3_derisk_spot ) grind_3_profit_threshold = ( self.grinding_v2_grind_3_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_3_profit_threshold_spot ) grind_x_max_sub_grinds = 0 grind_x_stakes = ( self.grinding_v2_grind_x_stakes_futures.copy() if self.is_futures_mode else self.grinding_v2_grind_x_stakes_spot.copy() ) grind_x_sub_thresholds = ( self.grinding_v2_grind_x_thresholds_futures if self.is_futures_mode else self.grinding_v2_grind_x_thresholds_spot ) if (slice_amount * grind_x_stakes[0] / (trade.leverage if self.is_futures_mode else 1.0)) < min_stake: multi = min_stake / slice_amount / \ grind_x_stakes[0] * trade.leverage for i, _ in enumerate(grind_x_stakes): grind_x_stakes[i] *= multi grind_x_max_sub_grinds = len(grind_x_stakes) grind_x_derisk_grinds = ( self.grinding_v2_grind_x_derisk_futures if self.is_futures_mode else self.grinding_v2_grind_x_derisk_spot ) grind_x_profit_threshold = ( self.grinding_v2_grind_x_profit_threshold_futures if self.is_futures_mode else self.grinding_v2_grind_x_profit_threshold_spot ) grind_1_sub_grind_count = 0 grind_1_total_amount = 0.0 grind_1_total_cost = 0.0 grind_1_current_open_rate = 0.0 grind_1_current_grind_stake = 0.0 grind_1_current_grind_stake_profit = 0.0 grind_1_is_exit_found = False grind_1_found = False grind_1_buy_orders = [] grind_1_orders = [] grind_1_distance_ratio = 0.0 grind_2_sub_grind_count = 0 grind_2_total_amount = 0.0 grind_2_total_cost = 0.0 grind_2_current_open_rate = 0.0 grind_2_current_grind_stake = 0.0 grind_2_current_grind_stake_profit = 0.0 grind_2_is_exit_found = False grind_2_found = False grind_2_buy_orders = [] grind_2_orders = [] grind_2_distance_ratio = 0.0 grind_3_sub_grind_count = 0 grind_3_total_amount = 0.0 grind_3_total_cost = 0.0 grind_3_current_open_rate = 0.0 grind_3_current_grind_stake = 0.0 grind_3_current_grind_stake_profit = 0.0 grind_3_is_exit_found = False grind_3_found = False grind_3_buy_orders = [] grind_3_orders = [] grind_3_distance_ratio = 0.0 grind_x_sub_grind_count = 0 grind_x_total_amount = 0.0 grind_x_total_cost = 0.0 grind_x_current_open_rate = 0.0 grind_x_current_grind_stake = 0.0 grind_x_current_grind_stake_profit = 0.0 grind_x_is_exit_found = False grind_x_found = False grind_x_buy_orders = [] grind_x_orders = [] grind_x_distance_ratio = 0.0 for order in reversed(filled_orders): if order.ft_order_side == "sell": order_tag = "" if has_order_tags: if order.ft_order_tag is not None: order_tag = order.ft_order_tag if not grind_1_is_exit_found and order_tag == "grind_1_entry": grind_1_sub_grind_count += 1 grind_1_total_amount += order.safe_filled grind_1_total_cost += order.safe_filled * order.safe_price grind_1_buy_orders.append(order.id) grind_1_orders.append(order) if not grind_1_found: grind_1_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_1_found = True elif not grind_2_is_exit_found and order_tag == "grind_2_entry": grind_2_sub_grind_count += 1 grind_2_total_amount += order.safe_filled grind_2_total_cost += order.safe_filled * order.safe_price grind_2_buy_orders.append(order.id) grind_2_orders.append(order) if not grind_2_found: grind_2_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_2_found = True elif not grind_3_is_exit_found and order_tag == "grind_3_entry": grind_3_sub_grind_count += 1 grind_3_total_amount += order.safe_filled grind_3_total_cost += order.safe_filled * order.safe_price grind_3_buy_orders.append(order.id) grind_3_orders.append(order) if not grind_3_found: grind_3_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_3_found = True elif not grind_x_is_exit_found and order_tag not in ["grind_1_entry", "grind_2_entry", "grind_3_entry"]: grind_x_sub_grind_count += 1 grind_x_total_amount += order.safe_filled grind_x_total_cost += order.safe_filled * order.safe_price grind_x_buy_orders.append(order.id) grind_x_orders.append(order) if not grind_x_found: grind_x_distance_ratio = ( exit_rate - order.safe_price) / order.safe_price grind_x_found = True elif order.ft_order_side == "buy": order_tag = "" if has_order_tags: if order.ft_order_tag is not None: sell_order_tag = order.ft_order_tag order_mode = sell_order_tag.split(" ", 1) if len(order_mode) > 0: order_tag = order_mode[0] if not grind_1_is_exit_found and order_tag in ["grind_1_exit", "grind_1_derisk"]: grind_1_is_exit_found = True elif not grind_2_is_exit_found and order_tag in ["grind_2_exit", "grind_2_derisk"]: grind_2_is_exit_found = True elif not grind_3_is_exit_found and order_tag in ["grind_3_exit", "grind_3_derisk"]: grind_3_is_exit_found = True elif not grind_x_is_exit_found and order_tag in ["grind_x_exit", "grind_x_derisk", "derisk_v2", "grind_v2_exit"]: grind_x_is_exit_found = True if grind_1_sub_grind_count > 0: grind_1_current_open_rate = grind_1_total_cost / grind_1_total_amount grind_1_current_grind_stake = grind_1_total_amount * \ exit_rate * (1 - trade.fee_close) grind_1_current_grind_stake_profit = grind_1_current_grind_stake - grind_1_total_cost if grind_2_sub_grind_count > 0: grind_2_current_open_rate = grind_2_total_cost / grind_2_total_amount grind_2_current_grind_stake = grind_2_total_amount * \ exit_rate * (1 - trade.fee_close) grind_2_current_grind_stake_profit = grind_2_current_grind_stake - grind_2_total_cost if grind_3_sub_grind_count > 0: grind_3_current_open_rate = grind_3_total_cost / grind_3_total_amount grind_3_current_grind_stake = grind_3_total_amount * \ exit_rate * (1 - trade.fee_close) grind_3_current_grind_stake_profit = grind_3_current_grind_stake - grind_3_total_cost if grind_x_sub_grind_count > 0: grind_x_current_open_rate = grind_x_total_cost / grind_x_total_amount grind_x_current_grind_stake = grind_x_total_amount * \ exit_rate * (1 - trade.fee_close) grind_x_current_grind_stake_profit = grind_x_current_grind_stake - grind_x_total_cost # all grinds num_open_grinds = grind_1_sub_grind_count + grind_2_sub_grind_count + grind_3_sub_grind_count + grind_x_sub_grind_count # not reached the max allowed stake for all grinds is_not_trade_max_stake = current_stake_amount < ( slice_amount * self.grinding_v2_max_stake) is_short_extra_checks_entry = ( (current_time - timedelta(minutes=5) > filled_entries[-1].order_filled_utc) and ((current_time - timedelta(hours=2) > filled_orders[-1].order_filled_utc) or (slice_profit > 0.06)) ) is_short_grind_entry = ( self.short_grind_entry_v2( last_candle, previous_candle, slice_profit) or ( (num_open_grinds == 0) and ( (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 80.0) and (last_candle["RSI_3_1h"] < 80.0) and (last_candle["AROOND_14"] < 50.0) and (last_candle["AROOND_14_15m"] < 50.0) ) ) or ( self.is_futures_mode and trade.liquidation_price is not None and ( (trade.is_short and current_rate > trade.liquidation_price * 0.90) or (not trade.is_short and current_rate < trade.liquidation_price * 1.10) ) and (slice_profit > 0.03) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 80.0) and (last_candle["AROOND_14"] < 50.0) and (last_candle["AROOND_14_15m"] < 50.0) ) ) # Grinding 1 if ( self.grinding_v2_grind_1_enable and is_short_grind_entry and is_short_extra_checks_entry and (grind_1_sub_grind_count < grind_1_max_sub_grinds) and ( (grind_1_sub_grind_count == 0) or (-grind_1_distance_ratio < grind_1_sub_thresholds[grind_1_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_1_stakes[grind_1_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_1_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_1_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_1_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount if grind_1_sub_grind_count > 0: grind_profit = - \ (exit_rate - grind_1_current_open_rate) / \ grind_1_current_open_rate if ( grind_profit > (grind_1_profit_threshold + fee_open_rate + fee_close_rate) ) and self.short_grind_exit_v2(last_candle, previous_candle, slice_profit): sell_amount = grind_1_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_1_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_1_total_amount, ) ) log.info( f"Grinding exit (grind_1_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} |" f" Stake amount: {sell_amount} | Coin amount: {grind_1_total_amount} | " f"Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | " f"Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_1_exit" for grind_entry_id in grind_1_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount if ( self.grinding_v2_grind_1_use_derisk and (grind_1_sub_grind_count > 0) and ((-(exit_rate - grind_1_current_open_rate) / grind_1_current_open_rate) < grind_1_derisk_grinds) and (grind_1_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_1_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_1_current_open_rate > 0.0: grind_profit = ( -((exit_rate - grind_1_current_open_rate) / grind_1_current_open_rate) if grind_1_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_1_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_1_total_amount, ) ) log.info( f"Grinding de-risk (grind_1_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} |" f" Stake amount: {sell_amount} | Coin amount: {grind_1_total_amount} | " f"Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}%" ) order_tag = "grind_1_derisk" for grind_entry_id in grind_1_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 2 if ( self.grinding_v2_grind_2_enable and is_short_grind_entry and is_short_extra_checks_entry and (grind_2_sub_grind_count < grind_2_max_sub_grinds) and ( (grind_2_sub_grind_count == 0) and (grind_1_sub_grind_count > 1) or (-grind_2_distance_ratio < grind_2_sub_thresholds[grind_2_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_2_stakes[grind_2_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_2_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_2_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_2_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount if grind_2_sub_grind_count > 0: grind_profit = - \ (exit_rate - grind_2_current_open_rate) / \ grind_2_current_open_rate if ( grind_profit > (grind_2_profit_threshold + fee_open_rate + fee_close_rate) ) and self.short_grind_exit_v2(last_candle, previous_candle, slice_profit): sell_amount = grind_2_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_2_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_2_total_amount, ) ) log.info( f"Grinding exit (grind_2_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} |" f" Stake amount: {sell_amount} | Coin amount: {grind_2_total_amount} |" f" Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% |" f" Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_2_exit" for grind_entry_id in grind_2_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount if ( self.grinding_v2_grind_2_use_derisk and (grind_2_sub_grind_count > 0) and ((-(exit_rate - grind_2_current_open_rate) / grind_2_current_open_rate) < grind_2_derisk_grinds) and (grind_2_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_2_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_2_current_open_rate > 0.0: grind_profit = ( -((exit_rate - grind_2_current_open_rate) / grind_2_current_open_rate) if grind_2_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_2_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_2_total_amount, ) ) log.info( f"Grinding de-risk (grind_2_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_2_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}%" ) order_tag = "grind_2_derisk" for grind_entry_id in grind_2_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 3 Entry if ( self.grinding_v2_grind_3_enable and is_short_grind_entry and is_short_extra_checks_entry and (grind_3_sub_grind_count < grind_3_max_sub_grinds) and ( (grind_3_sub_grind_count == 0) and (grind_2_sub_grind_count > 1) or (-grind_3_distance_ratio < grind_3_sub_thresholds[grind_3_sub_grind_count]) ) and is_not_trade_max_stake ): buy_amount = slice_amount * grind_3_stakes[grind_3_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_3_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_3_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_3_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount # Grinding 3 Exit if grind_3_sub_grind_count > 0: grind_profit = - (exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate if ( grind_profit > (grind_3_profit_threshold + fee_open_rate + fee_close_rate) ) and self.short_grind_exit_v2(last_candle, previous_candle, slice_profit): sell_amount = grind_3_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_3_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_3_total_amount, ) ) log.info( f"Grinding exit (grind_3_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} |" f" Stake amount: {sell_amount} | Coin amount: {grind_3_total_amount} |" f" Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% |" f" Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_3_exit" for grind_entry_id in grind_3_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding 3 De-risk if ( self.grinding_v2_grind_3_use_derisk and (grind_3_sub_grind_count > 0) and ((-(exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate) < grind_3_derisk_grinds) and (grind_3_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_3_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_3_current_open_rate > 0.0: grind_profit = ( -((exit_rate - grind_3_current_open_rate) / grind_3_current_open_rate) if grind_3_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_3_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_3_total_amount, ) ) log.info( f"Grinding de-risk (grind_3_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_3_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}%" ) order_tag = "grind_3_derisk" for grind_entry_id in grind_3_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding X - Entry (for entries not matching grind_1, 2, 3) if ( self.grinding_v2_grind_x_enable and is_short_grind_entry and is_short_extra_checks_entry and (grind_x_sub_grind_count < grind_x_max_sub_grinds) and grind_x_sub_grind_count != 0 and (-grind_x_distance_ratio < grind_x_sub_thresholds[grind_x_sub_grind_count]) and is_not_trade_max_stake ): buy_amount = slice_amount * \ grind_x_stakes[grind_x_sub_grind_count] / trade.leverage if buy_amount < (min_stake * 1.5): buy_amount = min_stake * 1.5 if buy_amount > max_stake: return None self.dp.send_msg( self.notification_msg( "grinding-entry", tag="grind_x_entry", pair=trade.pair, rate=current_rate, stake_amount=buy_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, ) ) log.info( f"Grinding entry (grind_x_entry) [{current_time}] [{trade.pair}] | Rate: {current_rate} | Stake amount: {buy_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}%" ) order_tag = "grind_x_entry" if has_order_tags: return buy_amount, order_tag else: return buy_amount # Grinding X - Exit if grind_x_sub_grind_count > 0: grind_profit = - \ (exit_rate - grind_x_current_open_rate) / \ grind_x_current_open_rate if ( grind_profit > (grind_x_profit_threshold + fee_open_rate + fee_close_rate) ) and self.short_grind_exit_v2(last_candle, previous_candle, slice_profit): sell_amount = grind_x_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: self.dp.send_msg( self.notification_msg( "grinding-exit", tag="grind_x_exit", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_x_total_amount, ) ) log.info( f"Grinding exit (grind_x_exit) [{current_time}] [{trade.pair}] | Rate: {exit_rate} |" f" Stake amount: {sell_amount} | Coin amount: {grind_x_total_amount} | " f"Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | " f"Grind profit: {(grind_profit * 100.0):.2f}% ({grind_profit * sell_amount * trade.leverage} {self.stake_currency})" ) order_tag = "grind_x_exit" for grind_entry_id in grind_x_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount # Grinding X De-risk if ( self.grinding_v2_grind_x_use_derisk and (grind_x_sub_grind_count > 0) and ((-(exit_rate - grind_x_current_open_rate) / grind_x_current_open_rate) < grind_x_derisk_grinds) and (grind_x_orders[-1].order_date_utc.replace(tzinfo=None) >= datetime(2025, 8, 3) or is_backtest) ): sell_amount = grind_x_total_amount * exit_rate / trade.leverage if ((current_stake_amount / trade.leverage) - sell_amount) < (min_stake * 1.55): sell_amount = (trade.amount * exit_rate / trade.leverage) - (min_stake * 1.55) ft_sell_amount = sell_amount * trade.leverage * \ (trade.stake_amount / trade.amount) / exit_rate if sell_amount > min_stake and ft_sell_amount > min_stake: grind_profit = 0.0 if grind_x_current_open_rate > 0.0: grind_profit = ( -((exit_rate - grind_x_current_open_rate) / grind_x_current_open_rate) if grind_x_is_exit_found else profit_ratio ) self.dp.send_msg( self.notification_msg( "grinding-derisk", tag="grind_x_derisk", pair=trade.pair, rate=exit_rate, stake_amount=sell_amount, profit_stake=profit_stake, profit_ratio=profit_ratio, stake_currency=self.stake_currency, grind_profit_stake=grind_profit * sell_amount * trade.leverage, grind_profit_pct=grind_profit, coin_amount=grind_x_total_amount, ) ) log.info( f"Grinding de-risk (grind_x_derisk) [{current_time}] [{trade.pair}] | Rate: {exit_rate} | Stake amount: {sell_amount} | Coin amount: {grind_x_total_amount} | Profit (stake): {profit_stake} | Profit: {(profit_ratio * 100.0):.2f}% | Grind profit: {(grind_profit * 100.0):.2f}%" ) order_tag = "grind_x_derisk" for grind_entry_id in grind_x_buy_orders: order_tag += " " + str(grind_entry_id) if has_order_tags: return -ft_sell_amount, order_tag else: return -ft_sell_amount return None def short_grind_entry_v2(self, last_candle: Series, previous_candle: Series, slice_profit: float) -> float: if ( (last_candle["enter_short"] == True) or ( (last_candle["RSI_14"] > 54.0) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 85.0) and (last_candle["RSI_3_1h"] < 85.0) and (last_candle["RSI_3_4h"] < 85.0) and (last_candle["ROC_2_1h"] < 10.0) and (last_candle["ROC_2_4h"] < 10.0) and (last_candle["ROC_2_1d"] < 10.0) and (last_candle["ROC_9_1h"] < 25.0) and (last_candle["ROC_9_4h"] < 25.0) and (last_candle["ROC_9_1d"] < 25.0) and (last_candle["AROOND_14"] < 25.0) and (last_candle["close"] < (last_candle["close_min_48"] * 1.10)) and (last_candle["close"] < (last_candle["low_min_6_1h"] * 1.18)) and (last_candle["close"] < (last_candle["low_min_12_1h"] * 1.25)) and (last_candle["close"] > (last_candle["high_max_24_4h"] * 0.85)) and (last_candle["close"] > (last_candle["EMA_16"] * 1.032)) ) or ( (last_candle["RSI_14"] > 64.0) and (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 85.0) and (last_candle["RSI_3_1h"] < 85.0) and (last_candle["RSI_3_4h"] < 85.0) and (last_candle["ROC_2_1h"] < 10.0) and (last_candle["ROC_2_4h"] < 10.0) and (last_candle["ROC_9_1h"] < 10.0) and (last_candle["ROC_9_4h"] < 10.0) and (last_candle["ROC_9_1d"] < 30.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 50.0) and (last_candle["EMA_12"] > last_candle["EMA_26"]) and ((last_candle["EMA_12"] - last_candle["EMA_26"]) > (last_candle["open"] * 0.020)) and ((previous_candle["EMA_12"] - previous_candle["EMA_26"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_14"] > 64.0) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["RSI_3_1d"] < 90.0) and (last_candle["ROC_2_1h"] < 5.0) and (last_candle["ROC_2_4h"] < 5.0) and (last_candle["ROC_2_1d"] < 5.0) and (last_candle["ROC_9_1h"] < 10.0) and (last_candle["ROC_9_4h"] < 10.0) and (last_candle["ROC_9_1d"] < 10.0) and (last_candle["AROOND_14_15m"] < 25.0) and (last_candle["close"] < (last_candle["close_min_48"] * 1.10)) and (last_candle["close"] < (last_candle["low_min_6_1h"] * 1.18)) and (last_candle["close"] < (last_candle["low_min_12_1h"] * 1.25)) and (last_candle["close"] > (last_candle["EMA_12"] * 1.020)) ) or ( (last_candle["RSI_14"] > 64.0) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["RSI_3_1d"] < 90.0) and (last_candle["ROC_2_1h"] < 10.0) and (last_candle["ROC_2_4h"] < 10.0) and (last_candle["ROC_2_1d"] < 10.0) and (last_candle["AROOND_14"] < 25.0) and (last_candle["close"] < (last_candle["close_min_48"] * 1.10)) and (last_candle["close"] < (last_candle["low_min_6_1h"] * 1.18)) and (last_candle["close"] < (last_candle["low_min_12_1h"] * 1.25)) and (last_candle["close"] > (last_candle["EMA_26"] * 1.038)) and (last_candle["close"] > (last_candle["BBU_20_2.0"] * 1.0)) ) or ( (last_candle["RSI_14"] > 65.0) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["ROC_2_1h"] < 10.0) and (last_candle["ROC_2_4h"] < 10.0) and (last_candle["ROC_2_1d"] < 10.0) and (last_candle["ROC_9_1h"] < 10.0) and (last_candle["ROC_9_4h"] < 10.0) and (last_candle["AROOND_14"] < 25.0) and (last_candle["close"] > (last_candle["high_max_12_4h"] * 0.80)) and (last_candle["close"] > (last_candle["EMA_9"] * 1.032)) and (last_candle["close"] > (last_candle["EMA_20"] * 1.020)) ) or ( (last_candle["RSI_14"] > 65.0) and (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3"] > 60.0) and (last_candle["RSI_3_15m"] < 85.0) and (last_candle["ROC_2_1h"] < 5.0) and (last_candle["ROC_2_4h"] < 5.0) and (last_candle["ROC_9_1h"] < 10.0) and (last_candle["ROC_9_4h"] < 10.0) and (last_candle["AROOND_14"] < 25.0) and (last_candle["RSI_20"] > previous_candle["RSI_20"]) and (last_candle["close"] > (last_candle["SMA_16"] * 1.045)) ) or ( (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["ROC_2_1h"] < 5.0) and (last_candle["ROC_2_4h"] < 5.0) and (last_candle["ROC_9_1h"] < 5.0) and (last_candle["ROC_9_4h"] < 5.0) and (last_candle["WILLR_14"] > -50.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 80.0) and (last_candle["WILLR_84_1h"] > -30.0) and (last_candle["close"] < (last_candle["high_max_24_4h"] * 0.77)) and (last_candle["BBB_20_2.0_1h"] > 12.0) and (last_candle["close_min_48"] <= (last_candle["close"] * 0.90)) ) or ( (last_candle["RSI_3"] > 70.0) and (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 95.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["ROC_9_1d"] < 30.0) and (last_candle["EMA_12"] > last_candle["EMA_26"]) and ((last_candle["EMA_12"] - last_candle["EMA_26"]) > (last_candle["open"] * 0.034)) and ((previous_candle["EMA_12"] - previous_candle["EMA_26"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 75.0) and (last_candle["RSI_3_1h"] < 70.0) and (last_candle["close"] > (last_candle["low_min_24_4h"] * 1.10)) and (last_candle["close"] > (last_candle["close_min_48"] * 1.10)) and (last_candle["close"] < (last_candle["close_max_12"] * 0.92)) ) or ( (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 95.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 80.0) and (last_candle["RSI_14"] > (last_candle["RSI_14_1h"] + 45.0)) ) or ( (last_candle["RSI_3"] < 90.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["RSI_3_1d"] < 90.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 80.0) and (last_candle["close"] > (last_candle["SMA_30"] * 1.022)) and (last_candle["close"] > (last_candle["BBU_20_2.0"] * 1.0)) ) or ( (last_candle["RSI_14"] > 64.0) and (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["RSI_3_1d"] < 90.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 70.0) and (last_candle["close"] < (last_candle["close_min_48"] * 1.15)) and (last_candle["close"] < (last_candle["low_min_6_1h"] * 1.20)) and (last_candle["close"] < (last_candle["low_min_12_1h"] * 1.33)) and (last_candle["close"] > (last_candle["high_max_12_4h"] * 0.75)) and (last_candle["EMA_12"] > last_candle["EMA_26"]) and ((last_candle["EMA_12"] - last_candle["EMA_26"]) > (last_candle["open"] * 0.018)) and ((previous_candle["EMA_12"] - previous_candle["EMA_26"]) > (last_candle["open"] / 100.0)) ) or ( (last_candle["RSI_3"] < 95.0) and (previous_candle["SMA_9"] > previous_candle["SMA_21"]) and (last_candle["SMA_9"] < last_candle["SMA_21"]) and (last_candle["close"] > (last_candle["EMA_100"] * 1.016)) and (last_candle["RSI_3_1h"] < 80.0) and (last_candle["RSI_3_4h"] < 80.0) ) or ( (slice_profit > 0.12) and (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_15m"] < 90.0) and (last_candle["RSI_14"] > 60.0) and (last_candle["AROOND_14"] < 25.0) and (last_candle["AROOND_14_15m"] < 30.0) and (last_candle["STOCHRSIk_14_14_3_3"] > 80.0) and (last_candle["STOCHRSIk_14_14_3_3_15m"] > 70.0) and (last_candle["RSI_14_1h"] > 50.0) and (last_candle["RSI_14_4h"] > 50.0) ) or ( (last_candle["RSI_14"] > 64.0) and (last_candle["RSI_3"] < 95.0) and (last_candle["RSI_3_1h"] < 90.0) and (last_candle["RSI_3_4h"] < 90.0) and (last_candle["close"] > (last_candle["EMA_12"] * 1.001)) and (last_candle["close"] > (last_candle["BBL_20_2.0"] * 1.004)) ) ): return True return False def short_grind_exit_v2(self, last_candle: Series, previous_candle: Series, slice_profit: float) -> float: if ( (last_candle["RSI_3"] < 1.0) or (last_candle["RSI_14"] < 30.0) or (last_candle["WILLR_14"] < -99.9) or (last_candle["STOCHRSIk_14_14_3_3"] < 5.0) or (last_candle["close"] < (last_candle["BBL_20_2.0"] * 0.99)) or ((last_candle["RSI_3"] < 10.0) and (last_candle["RSI_14"] > 50.0)) ): return True return False