Timeframe
5m
Direction
Long & Short
Stoploss
-99.0%
Trailing Stop
No
ROI
N/A
Interface Version
3
Startup Candles
N/A
Indicators
9
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
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