from __future__ import annotations import logging from datetime import datetime from typing import Any import numpy as np import talib.abstract as ta from pandas import DataFrame from freqtrade.persistence import Trade from freqtrade.strategy import ( BooleanParameter, CategoricalParameter, DecimalParameter, IStrategy, IntParameter, merge_informative_pair, stoploss_from_absolute, ) logger = logging.getLogger(__name__) class _KhoaAnnualGrowthV122RiskTunePFFrozenV145(IStrategy): """Standalone V122RiskTune. Includes V117/V118/RiskManaged logic in this file. This file intentionally imports no previous KhoaAnnualGrowthVxx strategy. """ """V107 plus a macro-bull overheat guard. The guard is intentionally dynamic: it blocks full-size macro-bull entries only when BTC is already overheated and the traded pair is also stretched. This targets weak blow-off entries without hard-coding dates or years. """ INTERFACE_VERSION = 3 timeframe = "1h" # Binance returns at most 1000 OHLCV rows per request. 999 plus the # currently open candle stays within one request while retaining ample # warmup for the longest 720-candle rolling window. startup_candle_count = 999 can_short = False stoploss = -0.150 minimal_roi = {"0": 0.447, "351": 0.139, "707": 0.078, "1620": 0.0} trailing_stop = True use_custom_stoploss = True trailing_stop_positive = 0.149 trailing_stop_positive_offset = 0.163 trailing_only_offset_is_reached = False process_only_new_candles = True use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False execution_guard_enabled = True execution_guard_max_spread = 0.0030 execution_guard_max_rate_deviation = 0.0040 execution_guard_max_book_impact = 0.0040 execution_guard_book_depth = 5 execution_guard_max_candle_age_minutes = 180 execution_guard_max_red_candle_drop = 0.120 execution_guard_max_panic_range = 0.180 buy_btc_ema50_mult = DecimalParameter(0.950, 1.020, default=0.997, decimals=3, space="buy", optimize=False) buy_btc_ema200_mult = DecimalParameter(0.980, 1.080, default=0.996, decimals=3, space="buy", optimize=False) buy_btc_slope_floor = DecimalParameter(-0.010, 0.010, default=0.003, decimals=3, space="buy", optimize=False) buy_btc_rsi_min = IntParameter(45, 58, default=52, space="buy", optimize=False) buy_btc_extension_mult = DecimalParameter(1.030, 1.140, default=1.131, decimals=3, space="buy", optimize=False) buy_btc_extension_rsi = IntParameter(52, 62, default=62, space="buy", optimize=False) buy_btc_change3_floor = DecimalParameter(0.035, 0.120, default=0.094, decimals=3, space="buy", optimize=False) buy_p4h_ema200_mult = DecimalParameter(0.920, 1.020, default=1.007, decimals=3, space="buy", optimize=False) buy_p4h_ema100_mult = DecimalParameter(0.940, 1.020, default=1.014, decimals=3, space="buy", optimize=False) buy_p4h_slope_floor = DecimalParameter(-0.015, 0.010, default=0.009, decimals=3, space="buy", optimize=False) buy_p4h_rsi_min = IntParameter(46, 58, default=54, space="buy", optimize=False) buy_p4h_adx_min = IntParameter(12, 26, default=18, space="buy", optimize=False) buy_local_ema200_mult = DecimalParameter(0.950, 1.030, default=0.958, decimals=3, space="buy", optimize=False) buy_local_slope_floor = DecimalParameter(-0.015, 0.010, default=0.006, decimals=3, space="buy", optimize=False) buy_local_rsi_min = IntParameter(45, 58, default=50, space="buy", optimize=False) buy_local_rsi_max = IntParameter(66, 82, default=73, space="buy", optimize=False) buy_local_adx_min = IntParameter(10, 24, default=13, space="buy", optimize=False) buy_atr_min = DecimalParameter(0.003, 0.012, default=0.004, decimals=3, space="buy", optimize=False) buy_atr_max = DecimalParameter(0.045, 0.100, default=0.057, decimals=3, space="buy", optimize=False) buy_pullback_ema_buffer = DecimalParameter(1.000, 1.025, default=1.023, decimals=3, space="buy", optimize=False) buy_pullback_volume_min = DecimalParameter(0.55, 1.25, default=1.19, decimals=2, space="buy", optimize=False) buy_retracement_max = DecimalParameter(0.080, 0.260, default=0.113, decimals=3, space="buy", optimize=False) buy_body_ratio_min = DecimalParameter(0.120, 0.420, default=0.17, decimals=2, space="buy", optimize=False) buy_opportunistic_enabled = BooleanParameter(default=False, space="buy", optimize=False) buy_breakout_enabled = BooleanParameter(default=False, space="buy", optimize=False) buy_breakout_window = CategoricalParameter([72, 96, 168], default=168, space="buy", optimize=False) buy_breakout_volume_min = DecimalParameter(0.70, 1.80, default=1.51, decimals=2, space="buy", optimize=False) buy_breakout_rsi_min = IntParameter(50, 64, default=57, space="buy", optimize=False) buy_breakout_rsi_max = IntParameter(68, 86, default=75, space="buy", optimize=False) buy_breakout_close_location = DecimalParameter(0.52, 0.85, default=0.52, decimals=2, space="buy", optimize=False) buy_macro_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_macro_rsi_min = IntParameter(48, 62, default=62, space="buy", optimize=False) buy_macro_rsi_max = IntParameter(68, 88, default=82, space="buy", optimize=False) buy_macro_adx_min = IntParameter(14, 30, default=16, space="buy", optimize=False) buy_macro_volume_min = DecimalParameter(0.45, 1.20, default=0.51, decimals=2, space="buy", optimize=False) buy_macro_overheat_guard_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_macro_overheat_btc_rsi = IntParameter(74, 88, default=80, space="buy", optimize=False) buy_macro_overheat_btc_change3 = DecimalParameter(0.015, 0.100, default=0.025, decimals=3, space="buy", optimize=False) buy_macro_overheat_p4h_rsi = IntParameter(64, 82, default=70, space="buy", optimize=False) buy_macro_overheat_p4h_change3 = DecimalParameter(0.005, 0.080, default=0.02, decimals=3, space="buy", optimize=False) buy_macro_overheat_close_location = DecimalParameter(0.25, 0.70, default=1.0, decimals=2, space="buy", optimize=False) buy_reentry_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_reentry_dump24 = DecimalParameter(0.035, 0.120, default=0.064, decimals=3, space="buy", optimize=False) buy_reentry_dump72 = DecimalParameter(0.060, 0.200, default=0.082, decimals=3, space="buy", optimize=False) buy_reentry_low_buffer = DecimalParameter(1.010, 1.080, default=1.023, decimals=3, space="buy", optimize=False) buy_reentry_max_from_low = DecimalParameter(1.050, 1.180, default=1.085, decimals=3, space="buy", optimize=False) buy_reentry_volume_ratio = DecimalParameter(0.800, 2.500, default=1.81, decimals=2, space="buy", optimize=False) buy_reentry_rsi7 = IntParameter(30, 48, default=45, space="buy", optimize=False) buy_reentry_rsi14 = IntParameter(36, 55, default=51, space="buy", optimize=False) buy_reentry_btc_change3_floor = DecimalParameter(0.080, 0.240, default=0.189, decimals=3, space="buy", optimize=False) buy_reentry_btc_ema200_floor = DecimalParameter(0.650, 0.940, default=0.768, decimals=3, space="buy", optimize=False) buy_reentry_stake_pct = DecimalParameter(0.050, 1.000, default=0.92, decimals=2, space="buy", optimize=False) buy_reentry_full_btc_slope_min = DecimalParameter(-0.020, 0.015, default=-0.004, decimals=3, space="buy", optimize=False) buy_reentry_full_p4h_slope_min = DecimalParameter(-0.020, 0.015, default=-0.002, decimals=3, space="buy", optimize=False) buy_reentry_weak_enabled = BooleanParameter(default=False, space="buy", optimize=False) buy_reentry_weak_stake_pct = DecimalParameter(0.050, 0.700, default=0.18, decimals=2, space="buy", optimize=False) buy_bottom_enabled = BooleanParameter(default=False, space="buy", optimize=False) buy_bottom_bb_std = DecimalParameter(1.70, 2.80, default=2.15, decimals=2, space="buy", optimize=False) buy_bottom_rsi7_max = IntParameter(16, 36, default=29, space="buy", optimize=False) buy_bottom_rsi14_max = IntParameter(24, 44, default=35, space="buy", optimize=False) buy_bottom_reclaim_rsi7 = IntParameter(25, 48, default=36, space="buy", optimize=False) buy_bottom_volume_mult = DecimalParameter(0.75, 2.80, default=1.2, decimals=2, space="buy", optimize=False) buy_bottom_pinbar_ratio = DecimalParameter(0.35, 0.75, default=0.52, decimals=2, space="buy", optimize=False) buy_bottom_max_from_low = DecimalParameter(1.025, 1.140, default=1.075, decimals=3, space="buy", optimize=False) buy_bottom_btc_change3_floor = DecimalParameter(0.080, 0.260, default=0.16, decimals=3, space="buy", optimize=False) buy_bottom_btc_ema200_floor = DecimalParameter(0.620, 0.920, default=0.74, decimals=3, space="buy", optimize=False) buy_bottom_stake_pct = DecimalParameter(0.050, 0.450, default=0.18, decimals=2, space="buy", optimize=False) buy_rebound_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_rebound_btc_change3_floor = DecimalParameter(0.060, 0.180, default=0.115, decimals=3, space="buy", optimize=False) buy_rebound_btc_ema200_floor = DecimalParameter(0.650, 0.880, default=0.75, decimals=3, space="buy", optimize=False) buy_rebound_p4h_rsi_min = IntParameter(52, 70, default=58, space="buy", optimize=False) buy_rebound_p4h_change3_min = DecimalParameter(0.000, 0.060, default=0.018, decimals=3, space="buy", optimize=False) buy_rebound_rsi_min = IntParameter(52, 68, default=58, space="buy", optimize=False) buy_rebound_rsi_max = IntParameter(70, 86, default=82, space="buy", optimize=False) buy_rebound_volume_min = DecimalParameter(0.70, 2.40, default=1.15, decimals=2, space="buy", optimize=False) buy_rebound_close_location = DecimalParameter(0.48, 0.78, default=0.57, decimals=2, space="buy", optimize=False) buy_rebound_max_from_low = DecimalParameter(1.050, 1.220, default=1.15, decimals=3, space="buy", optimize=False) buy_rebound_stake_pct = DecimalParameter(0.150, 1.000, default=1.0, decimals=2, space="buy", optimize=False) buy_rebound_full_btc_slope_min = DecimalParameter(-0.020, 0.015, default=-0.002, decimals=3, space="buy", optimize=False) buy_rebound_full_btc_rsi_min = IntParameter(38, 62, default=48, space="buy", optimize=False) buy_rebound_full_p4h_slope_min = DecimalParameter(-0.020, 0.015, default=0.002, decimals=3, space="buy", optimize=False) buy_rebound_weak_enabled = BooleanParameter(default=False, space="buy", optimize=False) buy_rebound_weak_stake_pct = DecimalParameter(0.050, 0.800, default=0.3, decimals=2, space="buy", optimize=False) buy_medium_pullback_enabled = BooleanParameter(default=False, space="buy", optimize=False) sell_macro_btc_change3_floor = DecimalParameter(0.050, 0.140, default=0.08, decimals=3, space="sell", optimize=False) sell_macro_btc_ema200_mult = DecimalParameter(0.940, 1.020, default=0.98, decimals=3, space="sell", optimize=False) sell_macro_p4h_ema100_mult = DecimalParameter(0.970, 1.030, default=1.0, decimals=3, space="sell", optimize=False) sell_macro_p4h_ema50_ema100_mult = DecimalParameter(0.960, 1.020, default=0.99, decimals=3, space="sell", optimize=False) sell_macro_p4h_slope_floor = DecimalParameter(-0.005, 0.006, default=0.0, decimals=3, space="sell", optimize=False) sell_soft_p4h_ema50_mult = DecimalParameter(0.980, 1.020, default=1.0, decimals=3, space="sell", optimize=False) sell_soft_p4h_slope_max = DecimalParameter(-0.015, 0.003, default=0.0, decimals=3, space="sell", optimize=False) sell_soft_ema100_mult = DecimalParameter(0.980, 1.020, default=1.0, decimals=3, space="sell", optimize=False) sell_soft_rsi_max = IntParameter(38, 52, default=45, space="sell", optimize=False) sell_hard_btc_change3 = DecimalParameter(0.080, 0.180, default=0.12, decimals=3, space="sell", optimize=False) sell_hard_p4h_ema200_mult = DecimalParameter(0.940, 1.010, default=0.97, decimals=3, space="sell", optimize=False) sell_hard_rsi_max = IntParameter(30, 45, default=38, space="sell", optimize=False) sell_macro_loss_guard_enabled = BooleanParameter(default=True, space="sell", optimize=False) sell_macro_loss_guard_profit = DecimalParameter(-0.100, -0.035, default=-0.045, decimals=3, space="sell", optimize=False) sell_macro_loss_guard_btc_change3 = DecimalParameter(0.025, 0.100, default=0.025, decimals=3, space="sell", optimize=False) sell_macro_loss_guard_p4h_ema50_mult = DecimalParameter(0.970, 1.005, default=1.005, decimals=3, space="sell", optimize=False) sell_macro_loss_guard_rsi_max = IntParameter(38, 55, default=55, space="sell", optimize=False) @property def protections(self) -> list[dict[str, Any]]: return [ {"method": "CooldownPeriod", "stop_duration_candles": 4}, { "method": "StoplossGuard", "lookback_period_candles": 96, "trade_limit": 3, "stop_duration_candles": 48, "only_per_pair": True, }, ] def informative_pairs(self) -> list[tuple[str, str]]: pairs = self.dp.current_whitelist() if self.dp else [] informative = [] if self.timeframe == "4h" else [(pair, "4h") for pair in pairs] informative.append(("BTC/USDT", "1d")) return sorted(set(informative)) @staticmethod def _safe_div(numerator, denominator): return numerator / denominator.replace(0, np.nan) @staticmethod def _timeframe_minutes(timeframe: str) -> int: unit = timeframe[-1] amount = int(timeframe[:-1]) if unit == "m": return amount if unit == "h": return amount * 60 if unit == "d": return amount * 1440 return 60 def _candles_for_hours(self, hours: float, minimum: int = 1) -> int: minutes = self._timeframe_minutes(self.timeframe) return max(minimum, int(round(hours * 60 / minutes))) def _execution_guard_active(self) -> bool: if not self.execution_guard_enabled or not self.dp: return False runmode = getattr(getattr(self.dp, "runmode", None), "value", "") return runmode in {"dry_run", "live"} @staticmethod def _estimate_book_impact(asks: list, amount: float) -> float | None: if not asks or amount <= 0: return None remaining = float(amount) cost = 0.0 filled = 0.0 best_ask = float(asks[0][0]) for price, size in asks: take = min(remaining, float(size)) cost += take * float(price) filled += take remaining -= take if remaining <= 0: break if filled <= 0 or remaining > 0: return None average_price = cost / filled return max(0.0, (average_price - best_ask) / best_ask) def _latest_candle_guard_ok(self, pair: str, current_time: datetime, entry_tag: str | None) -> bool: try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe is None or dataframe.empty: return True candle = dataframe.iloc[-1] last_date = candle.get("date") if hasattr(last_date, "to_pydatetime"): last_date = last_date.to_pydatetime() if isinstance(last_date, datetime): check_time = current_time if last_date.tzinfo is None and check_time.tzinfo is not None: last_date = last_date.replace(tzinfo=check_time.tzinfo) if last_date.tzinfo is not None and check_time.tzinfo is None: check_time = check_time.replace(tzinfo=last_date.tzinfo) age_minutes = abs((check_time - last_date).total_seconds()) / 60 if age_minutes > self.execution_guard_max_candle_age_minutes: logger.warning( "entry blocked by stale candle guard pair=%s age_minutes=%.1f tag=%s", pair, age_minutes, entry_tag, ) return False open_rate = float(candle.get("open", 0) or 0) high = float(candle.get("high", 0) or 0) low = float(candle.get("low", 0) or 0) close = float(candle.get("close", 0) or 0) volume = float(candle.get("volume", 0) or 0) if min(open_rate, high, low, close) <= 0 or volume < 0 or high < max(open_rate, close, low) or low > min(open_rate, close, high): logger.warning("entry blocked by OHLCV sanity guard pair=%s tag=%s", pair, entry_tag) return False candle_range = (high - low) / close if close > 0 else 0.0 red_drop = (open_rate - close) / open_rate if open_rate > 0 and close < open_rate else 0.0 close_location = (close - low) / (high - low) if high > low else 0.5 if candle_range > self.execution_guard_max_panic_range and close_location < 0.25: logger.warning( "entry blocked by panic range guard pair=%s range=%.4f close_location=%.2f tag=%s", pair, candle_range, close_location, entry_tag, ) return False if red_drop > self.execution_guard_max_red_candle_drop and close_location < 0.45: logger.warning( "entry blocked by bleeding candle guard pair=%s red_drop=%.4f close_location=%.2f tag=%s", pair, red_drop, close_location, entry_tag, ) return False except Exception as exc: logger.warning("latest candle guard skipped pair=%s tag=%s error=%s", pair, entry_tag, exc) return True def confirm_trade_entry( self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: str | None, side: str, **kwargs: Any, ) -> bool: if side != "long" or not self._execution_guard_active(): return True try: if not self._latest_candle_guard_ok(pair, current_time, entry_tag): return False orderbook = self.dp.orderbook(pair, self.execution_guard_book_depth) bids = orderbook.get("bids") or [] asks = orderbook.get("asks") or [] if not bids or not asks: return True best_bid = float(bids[0][0]) best_ask = float(asks[0][0]) mid = (best_bid + best_ask) / 2 spread = (best_ask - best_bid) / mid if mid > 0 else 0.0 rate_deviation = abs(best_ask - float(rate)) / float(rate) if rate else 0.0 impact = self._estimate_book_impact(asks, amount) if spread > self.execution_guard_max_spread: logger.warning("entry blocked by spread guard pair=%s spread=%.4f tag=%s", pair, spread, entry_tag) return False if rate_deviation > self.execution_guard_max_rate_deviation: logger.warning( "entry blocked by rate guard pair=%s deviation=%.4f rate=%.8f ask=%.8f tag=%s", pair, rate_deviation, rate, best_ask, entry_tag, ) return False if impact is not None and impact > self.execution_guard_max_book_impact: logger.warning("entry blocked by impact guard pair=%s impact=%.4f tag=%s", pair, impact, entry_tag) return False except Exception as exc: logger.warning("entry execution guard skipped pair=%s tag=%s error=%s", pair, entry_tag, exc) return True def _v117_custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: stake = proposed_stake if entry_tag == "post_capitulation_reclaim": stake = proposed_stake * float(self.buy_reentry_stake_pct.value) elif entry_tag == "post_capitulation_reclaim_weak": stake = proposed_stake * float(self.buy_reentry_weak_stake_pct.value) elif entry_tag == "bottom_reclaim": stake = proposed_stake * float(self.buy_bottom_stake_pct.value) elif entry_tag == "macro_bear_rebound": stake = proposed_stake * float(self.buy_rebound_stake_pct.value) elif entry_tag == "macro_bear_rebound_weak": stake = proposed_stake * float(self.buy_rebound_weak_stake_pct.value) stake = min(float(stake), float(max_stake)) if min_stake is not None and stake > 0: stake = max(stake, float(min_stake)) return stake def _v117_custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs: Any, ) -> float | None: if trade.enter_tag in { "post_capitulation_reclaim", "post_capitulation_reclaim_weak", "bottom_reclaim", "macro_bear_rebound", "macro_bear_rebound_weak", }: if current_profit > 0.110: return 0.035 if current_profit > 0.055: return 0.010 try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] atr = float(candle.get("atr", trade.open_rate * 0.025)) recent_low = float(candle.get("panic_low_recent", trade.open_rate * 0.90)) stop_price = max(recent_low - 0.5 * atr, trade.open_rate * 0.90) stop_price = min(stop_price, trade.open_rate * 0.985) return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return 0.10 if trade.enter_tag in {"trend_strong_pullback", "bull_continuation_breakout"} and current_profit > 0.12: return 0.040 if trade.enter_tag == "trend_medium_pullback" and current_profit > 0.09: return 0.030 return None def _v117_custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> str | bool | None: if trade.enter_tag != "macro_bull_hold" or not bool(self.sell_macro_loss_guard_enabled.value): return None if current_profit > float(self.sell_macro_loss_guard_profit.value): return None try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] btc_break = float(candle.get("btc_change3_1d", 0.0)) < -float(self.sell_macro_loss_guard_btc_change3.value) p4h_break = float(candle.get("close_4h", 0.0)) < ( float(candle.get("p4h_ema50_4h", 0.0)) * float(self.sell_macro_loss_guard_p4h_ema50_mult.value) ) local_break = ( float(candle.get("close", 0.0)) < float(candle.get("ema100", 0.0)) and float(candle.get("rsi", 100.0)) < int(self.sell_macro_loss_guard_rsi_max.value) and float(candle.get("macd", 0.0)) < float(candle.get("macdsignal", 0.0)) ) if btc_break or (p4h_break and local_break): return "macro_loss_guard" except (AttributeError, IndexError, KeyError, TypeError, ValueError): return None return None def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: ema_slope_window = self._candles_for_hours(8, minimum=2) atr_mean_window = self._candles_for_hours(168, minimum=24) bottom_recent_window = self._candles_for_hours(48, minimum=8) return_24h_window = self._candles_for_hours(24, minimum=2) return_72h_window = self._candles_for_hours(72, minimum=4) rolling_7d_window = self._candles_for_hours(24 * 7, minimum=24) rolling_30d_window = self._candles_for_hours(24 * 30, minimum=120) panic_recent_window = self._candles_for_hours(72, minimum=8) cap_recent_window = self._candles_for_hours(24, minimum=4) range_high_window = self._candles_for_hours(12, minimum=3) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=7) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) for period in (8, 20, 50, 100, 200): dataframe[f"ema{period}"] = ta.EMA(dataframe, timeperiod=period) dataframe["ema50_slope"] = dataframe["ema50"] - dataframe["ema50"].shift(ema_slope_window) dataframe["ema50_slope_pct"] = self._safe_div(dataframe["ema50"], dataframe["ema50"].shift(ema_slope_window)) - 1.0 macd = ta.MACD(dataframe) dataframe["macd"] = macd["macd"] dataframe["macdsignal"] = macd["macdsignal"] dataframe["macdhist"] = macd["macdhist"] dataframe["macdhist_rising_2"] = ( (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["macdhist"].shift(1) > dataframe["macdhist"].shift(2)) ) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["atr_pct"] = self._safe_div(dataframe["atr"], dataframe["close"]) dataframe["atr_pct_mean_168"] = dataframe["atr_pct"].rolling(atr_mean_window).mean() dataframe["atr_spike"] = dataframe["atr_pct"] > dataframe["atr_pct_mean_168"] * 1.35 dataframe["volume_mean"] = dataframe["volume"].rolling(30).mean() dataframe["volume_ratio"] = self._safe_div(dataframe["volume"], dataframe["volume_mean"]) dataframe["range"] = dataframe["high"] - dataframe["low"] dataframe["body"] = (dataframe["close"] - dataframe["open"]).abs() dataframe["body_ratio"] = self._safe_div(dataframe["body"], dataframe["range"]) dataframe["close_location"] = self._safe_div(dataframe["close"] - dataframe["low"], dataframe["range"]) dataframe["lower_wick_ratio"] = self._safe_div(dataframe[["open", "close"]].min(axis=1) - dataframe["low"], dataframe["range"]) bbands = ta.BBANDS( dataframe, timeperiod=20, nbdevup=float(self.buy_bottom_bb_std.value), nbdevdn=float(self.buy_bottom_bb_std.value), matype=0, ) dataframe["bb_lower"] = bbands["lowerband"] dataframe["bb_mid"] = bbands["middleband"] dataframe["bb_upper"] = bbands["upperband"] dataframe["bb_width"] = self._safe_div(dataframe["bb_upper"] - dataframe["bb_lower"], dataframe["bb_mid"]) dataframe["bottom_penetration"] = (dataframe["close"] < dataframe["bb_lower"]) | (dataframe["low"] < dataframe["bb_lower"]) dataframe["bottom_oversold"] = ( (dataframe["rsi7"] < int(self.buy_bottom_rsi7_max.value)) | (dataframe["rsi"] < int(self.buy_bottom_rsi14_max.value)) ) dataframe["bottom_capitulation"] = ( (dataframe["volume_ratio"] > float(self.buy_bottom_volume_mult.value)) | dataframe["atr_spike"] | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value)) ) dataframe["bottom_absorbed"] = ( (dataframe["close"] > dataframe["open"]) | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value)) | (dataframe["close_location"] > 0.50) ) dataframe["bottom_seed"] = ( dataframe["bottom_penetration"] & dataframe["bottom_oversold"] & dataframe["bottom_capitulation"] & dataframe["bottom_absorbed"] ) dataframe["bottom_seed_recent"] = dataframe["bottom_seed"].rolling(bottom_recent_window).max() > 0 dataframe["high72"] = dataframe["high"].rolling(72).max().shift(1) dataframe["high96"] = dataframe["high"].rolling(96).max().shift(1) dataframe["high168"] = dataframe["high"].rolling(168).max().shift(1) dataframe["low96"] = dataframe["low"].rolling(96).min().shift(1) dataframe["retracement96"] = self._safe_div(dataframe["high96"] - dataframe["close"], dataframe["high96"] - dataframe["low96"]) dataframe["low5"] = dataframe["low"].rolling(5).min() dataframe["return_24h"] = dataframe["close"].pct_change(return_24h_window) dataframe["return_72h"] = dataframe["close"].pct_change(return_72h_window) dataframe["rolling_low_7d"] = dataframe["low"].rolling(rolling_7d_window).min() dataframe["rolling_low_30d"] = dataframe["low"].rolling(rolling_30d_window).min() dataframe["panic_low_recent"] = dataframe["low"].rolling(panic_recent_window).min() dataframe["range_high_12"] = dataframe["high"].rolling(range_high_window).max().shift(1) dataframe["big_dump_candle"] = ( (dataframe["close"] / dataframe["open"] - 1.0 < -0.035) | ((dataframe["range"] > dataframe["atr"] * 2.3) & (dataframe["close_location"] < 0.35)) ) dataframe["panic_seed"] = ( (dataframe["return_24h"] < -float(self.buy_reentry_dump24.value)) | (dataframe["return_72h"] < -float(self.buy_reentry_dump72.value)) | dataframe["big_dump_candle"] ) dataframe["panic_recent"] = dataframe["panic_seed"].rolling(panic_recent_window).max() > 0 dataframe["low_zone_recent"] = ( dataframe["panic_low_recent"] <= dataframe["rolling_low_30d"] * float(self.buy_reentry_low_buffer.value) ) | ( dataframe["panic_low_recent"] <= dataframe["rolling_low_7d"] * float(self.buy_reentry_low_buffer.value) ) dataframe["capitulation_recent"] = ( ( (dataframe["volume_ratio"] > float(self.buy_reentry_volume_ratio.value)) | dataframe["atr_spike"] | (dataframe["lower_wick_ratio"] > 0.45) ) .rolling(cap_recent_window) .max() > 0 ) if self.dp: if self.timeframe == "4h": dataframe["close_4h"] = dataframe["close"] for period in (20, 50, 100, 200): dataframe[f"p4h_ema{period}_4h"] = ta.EMA(dataframe, timeperiod=period) dataframe["p4h_rsi_4h"] = ta.RSI(dataframe, timeperiod=14) dataframe["p4h_adx_4h"] = ta.ADX(dataframe, timeperiod=14) dataframe["p4h_ema50_slope_4h"] = dataframe["p4h_ema50_4h"] - dataframe["p4h_ema50_4h"].shift(4) dataframe["p4h_ema50_slope_pct_4h"] = self._safe_div( dataframe["p4h_ema50_4h"], dataframe["p4h_ema50_4h"].shift(4) ) - 1.0 dataframe["p4h_change3_4h"] = dataframe["close"] / dataframe["close"].shift(3) - 1 else: pair_4h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe="4h") for period in (20, 50, 100, 200): pair_4h[f"p4h_ema{period}"] = ta.EMA(pair_4h, timeperiod=period) pair_4h["p4h_rsi"] = ta.RSI(pair_4h, timeperiod=14) pair_4h["p4h_adx"] = ta.ADX(pair_4h, timeperiod=14) pair_4h["p4h_ema50_slope"] = pair_4h["p4h_ema50"] - pair_4h["p4h_ema50"].shift(4) pair_4h["p4h_ema50_slope_pct"] = self._safe_div(pair_4h["p4h_ema50"], pair_4h["p4h_ema50"].shift(4)) - 1.0 pair_4h["p4h_change3"] = pair_4h["close"] / pair_4h["close"].shift(3) - 1 dataframe = merge_informative_pair(dataframe, pair_4h, self.timeframe, "4h", ffill=True) btc_1d = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe="1d") btc_1d["btc_ema50"] = ta.EMA(btc_1d, timeperiod=50) btc_1d["btc_ema100"] = ta.EMA(btc_1d, timeperiod=100) btc_1d["btc_ema200"] = ta.EMA(btc_1d, timeperiod=200) btc_1d["btc_rsi"] = ta.RSI(btc_1d, timeperiod=14) btc_1d["btc_ema50_slope"] = btc_1d["btc_ema50"] - btc_1d["btc_ema50"].shift(3) btc_1d["btc_ema50_slope_pct"] = self._safe_div(btc_1d["btc_ema50"], btc_1d["btc_ema50"].shift(3)) - 1.0 btc_1d["btc_change3"] = btc_1d["close"] / btc_1d["close"].shift(3) - 1 dataframe = merge_informative_pair(dataframe, btc_1d, self.timeframe, "1d", ffill=True) for column in dataframe.columns: if column != "date": try: dataframe[column] = dataframe[column].ffill().fillna(0) except AttributeError: pass ema200 = dataframe["btc_ema200_1d"] ema100 = dataframe["btc_ema100_1d"] ema50 = dataframe["btc_ema50_1d"] dataframe["btc_slow_reference_1d"] = ema200.where( ema200.gt(0), ema100.where(ema100.gt(0), ema50) ) return dataframe def _v117_populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: market_core = ( (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * float(self.buy_btc_ema50_mult.value)) & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_ema200_mult.value)) & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value)) ) market_extension = ( (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_extension_mult.value)) & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_extension_rsi.value)) ) market_ok = (market_core | market_extension) & (dataframe["btc_change3_1d"] > -float(self.buy_btc_change3_floor.value)) trend_4h = ( (dataframe["close_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema200_mult.value)) & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"]) & (dataframe["p4h_ema100_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema100_mult.value)) & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_p4h_slope_floor.value)) & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value)) & (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value)) & (dataframe["p4h_change3_4h"] > -float(self.buy_btc_change3_floor.value)) ) local_ok = ( (dataframe["close"] > dataframe["ema200"] * float(self.buy_local_ema200_mult.value)) & (dataframe["ema50"] > dataframe["ema100"] * 0.990) & (dataframe["ema50_slope_pct"] > float(self.buy_local_slope_floor.value)) & (dataframe["rsi"] > int(self.buy_local_rsi_min.value)) & (dataframe["rsi"] < int(self.buy_local_rsi_max.value)) & (dataframe["adx"] > int(self.buy_local_adx_min.value)) & (dataframe["atr_pct"] > float(self.buy_atr_min.value)) & (dataframe["atr_pct"] < float(self.buy_atr_max.value)) & (dataframe["volume"] > 0) ) pullback = ( (dataframe["low5"] <= dataframe["ema20"] * float(self.buy_pullback_ema_buffer.value)) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["ema20"] > dataframe["ema50"]) & (dataframe["close"] > dataframe["open"]) & (dataframe["rsi"] > dataframe["rsi"].shift(1)) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["volume_ratio"] > float(self.buy_pullback_volume_min.value)) ) medium_regime = ( (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value) + 7) & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 4) & (dataframe["adx"] > int(self.buy_local_adx_min.value) + 4) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value) + 3) & (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * 1.010) ) strong_regime = ( (dataframe["p4h_adx_4h"] > int(self.buy_p4h_adx_min.value) + 10) & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 6) & (dataframe["adx"] > int(self.buy_local_adx_min.value) + 7) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value) + 5) & (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * 1.020) ) opportunistic_regime = ( (dataframe["p4h_ema100_4h"] > dataframe["p4h_ema200_4h"] * float(self.buy_p4h_ema100_mult.value)) & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * 1.020) & (dataframe["p4h_rsi_4h"] > int(self.buy_p4h_rsi_min.value) + 4) & (dataframe["volume_ratio"] > 1.0) & (dataframe["retracement96"] < float(self.buy_retracement_max.value)) & (dataframe["body_ratio"] > float(self.buy_body_ratio_min.value)) ) macro_overheat_guard = ( bool(self.buy_macro_overheat_guard_enabled.value) & (dataframe["btc_rsi_1d"] > int(self.buy_macro_overheat_btc_rsi.value)) & (dataframe["btc_change3_1d"] > float(self.buy_macro_overheat_btc_change3.value)) & (dataframe["p4h_rsi_4h"] > int(self.buy_macro_overheat_p4h_rsi.value)) & (dataframe["p4h_change3_4h"] > float(self.buy_macro_overheat_p4h_change3.value)) & (dataframe["close_location"] < float(self.buy_macro_overheat_close_location.value)) ) trend_base = market_ok & trend_4h & local_ok & ~macro_overheat_guard dataframe.loc[bool(self.buy_opportunistic_enabled.value) & trend_base & pullback & opportunistic_regime, ["enter_long", "enter_tag"]] = ( 1, "trend_opportunistic_pullback", ) dataframe.loc[bool(self.buy_medium_pullback_enabled.value) & trend_base & pullback & medium_regime, ["enter_long", "enter_tag"]] = ( 1, "trend_medium_pullback", ) dataframe.loc[trend_base & pullback & strong_regime, ["enter_long", "enter_tag"]] = ( 1, "trend_strong_pullback", ) breakout_high = dataframe[f"high{int(self.buy_breakout_window.value)}"] breakout_route = ( bool(self.buy_breakout_enabled.value) & trend_base & (dataframe["close"] > breakout_high) & (dataframe["ema20"] > dataframe["ema50"]) & (dataframe["ema50"] > dataframe["ema100"] * 0.990) & (dataframe["volume_ratio"] > float(self.buy_breakout_volume_min.value)) & (dataframe["rsi"] > int(self.buy_breakout_rsi_min.value)) & (dataframe["rsi"] < int(self.buy_breakout_rsi_max.value)) & dataframe["macdhist_rising_2"] & (dataframe["close_location"] > float(self.buy_breakout_close_location.value)) & (dataframe["volume"] > 0) ) dataframe.loc[breakout_route, ["enter_long", "enter_tag"]] = (1, "bull_continuation_breakout") structured_entry = ( (trend_base & pullback & (opportunistic_regime | (bool(self.buy_medium_pullback_enabled.value) & medium_regime) | strong_regime)) | breakout_route ) macro_bull_hold = ( bool(self.buy_macro_enabled.value) & market_ok & trend_4h & (dataframe["close"] > dataframe["ema50"]) & (dataframe["ema20"] > dataframe["ema50"]) & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"]) & (dataframe["p4h_rsi_4h"] > int(self.buy_macro_rsi_min.value)) & (dataframe["p4h_rsi_4h"] < int(self.buy_macro_rsi_max.value)) & (dataframe["p4h_adx_4h"] > int(self.buy_macro_adx_min.value)) & (dataframe["rsi"] > int(self.buy_local_rsi_min.value)) & (dataframe["volume_ratio"] > float(self.buy_macro_volume_min.value)) & (dataframe["volume"] > 0) & ~structured_entry & ~macro_overheat_guard ) dataframe.loc[macro_bull_hold, ["enter_long", "enter_tag"]] = (1, "macro_bull_hold") btc_not_deep_crash = ( (dataframe["btc_change3_1d"] > -float(self.buy_reentry_btc_change3_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_reentry_btc_ema200_floor.value)) ) reclaim = ( ( (dataframe["close"] > dataframe["ema20"]) & (dataframe["ema8"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"] * 0.985) & ((dataframe["close"].shift(1) <= dataframe["ema20"].shift(1)) | (dataframe["close"] > dataframe["range_high_12"])) ) | ((dataframe["close"] > dataframe["ema50"]) & (dataframe["rsi"] > 45)) ) recovery_momentum = ( ((dataframe["rsi7"] > int(self.buy_reentry_rsi7.value)) | (dataframe["rsi"] > int(self.buy_reentry_rsi14.value))) & dataframe["macdhist_rising_2"] & (dataframe["close"] > dataframe["open"]) & (dataframe["close"] > dataframe["close"].shift(1)) & (dataframe["close_location"] > 0.58) & (dataframe["volume_ratio"] > float(self.buy_reentry_volume_ratio.value)) ) rebound_btc_guard = ( (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value)) ) rebound_setup = ( dataframe["panic_recent"] | (dataframe["low_zone_recent"] & (dataframe["close"] > dataframe["range_high_12"])) | ( (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value)) & (dataframe["close"] > dataframe["range_high_12"]) ) ) macro_bear_rebound = ( bool(self.buy_rebound_enabled.value) & ~market_ok & rebound_setup & dataframe["capitulation_recent"] & reclaim & rebound_btc_guard & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_rebound_max_from_low.value)) & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985) & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value)) & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value)) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"] * 0.990) & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"])) & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value)) & (dataframe["rsi"] < int(self.buy_rebound_rsi_max.value)) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["close"] > dataframe["close"].shift(1)) & (dataframe["close_location"] > float(self.buy_rebound_close_location.value)) & (dataframe["volume_ratio"] > float(self.buy_rebound_volume_min.value)) & (dataframe["volume"] > 0) & ~structured_entry & ~macro_bull_hold ) rebound_full_quality = ( (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_rebound_full_btc_slope_min.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_rebound_full_btc_rsi_min.value)) & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_rebound_full_p4h_slope_min.value)) ) macro_bear_rebound_full = macro_bear_rebound & rebound_full_quality macro_bear_rebound_weak = macro_bear_rebound & ~rebound_full_quality if bool(self.buy_rebound_weak_enabled.value): dataframe.loc[macro_bear_rebound_weak, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound_weak") dataframe.loc[macro_bear_rebound_full, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound") post_capitulation_reclaim = ( bool(self.buy_reentry_enabled.value) & dataframe["panic_recent"] & dataframe["low_zone_recent"] & dataframe["capitulation_recent"] & reclaim & recovery_momentum & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_reentry_max_from_low.value)) & ((dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.980) & (dataframe["p4h_change3_4h"] > -0.060)) & btc_not_deep_crash & (dataframe["volume"] > 0) & ~macro_bear_rebound ) reentry_full_quality = ( (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_reentry_full_btc_slope_min.value)) & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_reentry_full_p4h_slope_min.value)) ) post_capitulation_reclaim_full = post_capitulation_reclaim & reentry_full_quality post_capitulation_reclaim_weak = post_capitulation_reclaim & ~reentry_full_quality if bool(self.buy_reentry_weak_enabled.value): dataframe.loc[post_capitulation_reclaim_weak, ["enter_long", "enter_tag"]] = (1, "post_capitulation_reclaim_weak") dataframe.loc[post_capitulation_reclaim_full, ["enter_long", "enter_tag"]] = (1, "post_capitulation_reclaim") bottom_btc_guard = ( (dataframe["btc_change3_1d"] > -float(self.buy_bottom_btc_change3_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_bottom_btc_ema200_floor.value)) ) bottom_reclaim = ( bool(self.buy_bottom_enabled.value) & dataframe["bottom_seed_recent"] & ~structured_entry & ~post_capitulation_reclaim & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_bottom_max_from_low.value)) & (dataframe["close"] > dataframe["ema8"]) & ((dataframe["close"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"])) & (dataframe["rsi7"] > int(self.buy_bottom_reclaim_rsi7.value)) & (dataframe["rsi7"] > dataframe["rsi7"].shift(1)) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & ( (dataframe["close"] > dataframe["open"]) | (dataframe["lower_wick_ratio"] > float(self.buy_bottom_pinbar_ratio.value)) ) & (dataframe["close_location"] > 0.45) & (dataframe["volume_ratio"] > 0.45) & bottom_btc_guard & (dataframe["volume"] > 0) ) dataframe.loc[bottom_reclaim, ["enter_long", "enter_tag"]] = (1, "bottom_reclaim") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: macro_support = ( (dataframe["btc_change3_1d"] > -float(self.sell_macro_btc_change3_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.sell_macro_btc_ema200_mult.value)) & (dataframe["close_4h"] > dataframe["p4h_ema100_4h"] * float(self.sell_macro_p4h_ema100_mult.value)) & (dataframe["p4h_ema50_4h"] > dataframe["p4h_ema100_4h"] * float(self.sell_macro_p4h_ema50_ema100_mult.value)) & (dataframe["p4h_ema50_slope_4h"] > float(self.sell_macro_p4h_slope_floor.value)) ) soft_break = ( ( (dataframe["close_4h"] < dataframe["p4h_ema50_4h"] * float(self.sell_soft_p4h_ema50_mult.value)) & (dataframe["p4h_ema50_slope_4h"] < float(self.sell_soft_p4h_slope_max.value)) ) | ( (dataframe["close"] < dataframe["ema100"] * float(self.sell_soft_ema100_mult.value)) & (dataframe["rsi"] < int(self.sell_soft_rsi_max.value)) & (dataframe["macd"] < dataframe["macdsignal"]) ) ) hard_break = ( (dataframe["btc_change3_1d"] < -float(self.sell_hard_btc_change3.value)) | (dataframe["close_4h"] < dataframe["p4h_ema200_4h"] * float(self.sell_hard_p4h_ema200_mult.value)) | ((dataframe["close"] < dataframe["ema200"]) & (dataframe["rsi"] < int(self.sell_hard_rsi_max.value))) ) rebound_exit_guard = ( bool(self.buy_rebound_enabled.value) & (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value)) & (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value)) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"] * 0.990) & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"])) & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value)) & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value)) & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value)) & (dataframe["rsi"] < int(self.buy_rebound_rsi_max.value)) & (dataframe["volume_ratio"] > float(self.buy_rebound_volume_min.value)) & (dataframe["volume"] > 0) ) trend_break = ((soft_break & ~macro_support) | hard_break) & (dataframe["volume"] > 0) & ~rebound_exit_guard dataframe.loc[trend_break, ["exit_long", "exit_tag"]] = (1, "v79_trend_break") return dataframe """V117 plus controlled weak-rebound scouting. V117 preserved drawdown by requiring full macro quality after a panic reclaim. That made it miss valid V-shaped rebounds while BTC/4h slope was still repairing. V118 keeps the V117 core, but allows small scout entries on weak rebound/reentry tags and exits them faster if the reclaim fails. """ INTERFACE_VERSION = 3 buy_scout_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_scout_btc_rsi_min = IntParameter(30, 50, default=35, space="buy", optimize=False) buy_scout_btc_change3_min = DecimalParameter(-0.010, 0.030, default=0.005, decimals=3, space="buy", optimize=False) buy_scout_btc_slope_min = DecimalParameter(-0.020, 0.005, default=-0.015, decimals=3, space="buy", optimize=False) buy_scout_p4h_slope_min = DecimalParameter(-0.002, 0.004, default=0.001, decimals=3, space="buy", optimize=False) buy_scout_volume_min = DecimalParameter(1.10, 2.60, default=1.5, decimals=2, space="buy", optimize=False) buy_scout_close_location = DecimalParameter(0.55, 0.85, default=0.65, decimals=2, space="buy", optimize=False) buy_scout_rsi_max = IntParameter(74, 86, default=82, space="buy", optimize=False) buy_reentry_confirm_guard_enabled = BooleanParameter(default=True, space="buy", optimize=False) buy_reentry_confirm_p4h_change_min = DecimalParameter(-0.005, 0.020, default=0.0, decimals=3, space="buy", optimize=False) buy_reentry_confirm_p4h_rsi_min = IntParameter(52, 64, default=58, space="buy", optimize=False) buy_reentry_confirm_p4h_strong_change = DecimalParameter(0.005, 0.040, default=0.018, decimals=3, space="buy", optimize=False) def _v118_custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: if entry_tag == "macro_bear_rebound_scout": stake = proposed_stake * float(self.buy_rebound_weak_stake_pct.value) stake = min(float(stake), float(max_stake)) if min_stake is not None and stake > 0: stake = max(stake, float(min_stake)) return stake return self._v117_custom_stake_amount( pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs, ) def _v118_custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs: Any, ) -> float | None: if trade.enter_tag == "macro_bear_rebound_scout": if current_profit > 0.100: return 0.030 if current_profit > 0.050: return 0.010 try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] atr = float(candle.get("atr", trade.open_rate * 0.025)) recent_low = float(candle.get("panic_low_recent", trade.open_rate * 0.90)) stop_price = max(recent_low - 0.35 * atr, trade.open_rate * 0.925) stop_price = min(stop_price, trade.open_rate * 0.985) return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return 0.075 return self._v117_custom_stoploss(pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs) def _v118_custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> str | bool | None: parent_exit = self._v117_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs) if parent_exit: return parent_exit if trade.enter_tag not in {"macro_bear_rebound_weak", "post_capitulation_reclaim_weak", "macro_bear_rebound_scout"}: return None try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] close = float(candle.get("close", current_rate)) ema20 = float(candle.get("ema20", current_rate)) ema50 = float(candle.get("ema50", current_rate)) macd = float(candle.get("macd", 0.0)) macdsignal = float(candle.get("macdsignal", 0.0)) rsi = float(candle.get("rsi", 50.0)) p4h_change3 = float(candle.get("p4h_change3_4h", 0.0)) btc_change3 = float(candle.get("btc_change3_1d", 0.0)) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return None age_hours = (current_time - trade.open_date_utc).total_seconds() / 3600.0 reclaim_failed = close < ema20 and macd < macdsignal weak_macro_rollover = btc_change3 < -0.045 or p4h_change3 < -0.035 if current_profit < -0.030 and (reclaim_failed or weak_macro_rollover): return "weak_rebound_failed" if age_hours >= 24 and current_profit < 0.010 and close < ema50 and rsi < 52: return "weak_rebound_time_stop" if current_profit > 0.075 and close < ema20 and macd < macdsignal: return "weak_rebound_profit_guard" return None def _v118_populate_entry_trend(self, dataframe, metadata: dict): dataframe = self._v117_populate_entry_trend(dataframe, metadata) if bool(self.buy_reentry_confirm_guard_enabled.value): post_reclaim_tag = dataframe["enter_tag"].eq("post_capitulation_reclaim") reentry_confirmed = ( (dataframe["p4h_change3_4h"] > float(self.buy_reentry_confirm_p4h_change_min.value)) & ( (dataframe["close"] > dataframe["range_high_12"]) | ( (dataframe["p4h_rsi_4h"] > int(self.buy_reentry_confirm_p4h_rsi_min.value)) & ( dataframe["p4h_change3_4h"] > float(self.buy_reentry_confirm_p4h_strong_change.value) ) ) ) ) dataframe.loc[post_reclaim_tag & ~reentry_confirmed, ["enter_long", "enter_tag"]] = (0, "") market_core = ( (dataframe["close_1d"] > dataframe["btc_ema50_1d"] * float(self.buy_btc_ema50_mult.value)) & (dataframe["btc_ema50_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_ema200_mult.value)) & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_rsi_min.value)) ) market_extension = ( (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_btc_extension_mult.value)) & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_btc_slope_floor.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_btc_extension_rsi.value)) ) market_ok = (market_core | market_extension) & ( dataframe["btc_change3_1d"] > -float(self.buy_btc_change3_floor.value) ) rebound_setup = ( dataframe["panic_recent"] | (dataframe["low_zone_recent"] & (dataframe["close"] > dataframe["range_high_12"])) | ( (dataframe["close"] <= dataframe["rolling_low_30d"] * float(self.buy_rebound_max_from_low.value)) & (dataframe["close"] > dataframe["range_high_12"]) ) ) reclaim = ( ( (dataframe["close"] > dataframe["ema20"]) & (dataframe["ema8"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"] * 0.985) & ( (dataframe["close"].shift(1) <= dataframe["ema20"].shift(1)) | (dataframe["close"] > dataframe["range_high_12"]) ) ) | ((dataframe["close"] > dataframe["ema50"]) & (dataframe["rsi"] > 45)) ) scout_rebound = ( bool(self.buy_scout_enabled.value) & ~market_ok & rebound_setup & dataframe["capitulation_recent"] & reclaim & (dataframe["btc_change3_1d"] > -float(self.buy_rebound_btc_change3_floor.value)) & (dataframe["btc_change3_1d"] > float(self.buy_scout_btc_change3_min.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self.buy_rebound_btc_ema200_floor.value)) & (dataframe["btc_rsi_1d"] > int(self.buy_scout_btc_rsi_min.value)) & (dataframe["btc_ema50_slope_pct_1d"] > float(self.buy_scout_btc_slope_min.value)) & (dataframe["close"] <= dataframe["panic_low_recent"] * float(self.buy_rebound_max_from_low.value)) & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985) & (dataframe["p4h_rsi_4h"] > int(self.buy_rebound_p4h_rsi_min.value)) & (dataframe["p4h_change3_4h"] > float(self.buy_rebound_p4h_change3_min.value)) & (dataframe["p4h_ema50_slope_pct_4h"] > float(self.buy_scout_p4h_slope_min.value)) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"] * 0.990) & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"])) & (dataframe["rsi"] > int(self.buy_rebound_rsi_min.value)) & (dataframe["rsi"] < int(self.buy_scout_rsi_max.value)) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["close"] > dataframe["close"].shift(1)) & (dataframe["close_location"] > float(self.buy_scout_close_location.value)) & (dataframe["volume_ratio"] > float(self.buy_scout_volume_min.value)) & (dataframe["volume"] > 0) & (dataframe.get("enter_long", 0) != 1) ) dataframe.loc[scout_rebound, ["enter_long", "enter_tag"]] = (1, "macro_bear_rebound_scout") return dataframe """V118 entries with dynamic stake sizing. This keeps V118's signal engine intact and changes only capital allocation. The goal is to reduce exposure during weak macro / high-volatility moments, while allowing near-full stake on the strongest V118 setups. """ INTERFACE_VERSION = 3 rm_enabled = BooleanParameter(default=True, space="buy", optimize=True) rm_base_stake_cap = DecimalParameter(0.55, 1.00, default=1.0, decimals=2, space="buy", optimize=True) rm_min_stake_mult = DecimalParameter(0.20, 1.00, default=0.9, decimals=2, space="buy", optimize=True) rm_macro_mult = DecimalParameter(0.70, 1.05, default=1.0, decimals=2, space="buy", optimize=True) rm_trend_mult = DecimalParameter(0.80, 1.10, default=1.05, decimals=2, space="buy", optimize=True) rm_rebound_mult = DecimalParameter(0.55, 1.05, default=0.97, decimals=2, space="buy", optimize=True) rm_reentry_mult = DecimalParameter(0.50, 1.00, default=0.94, decimals=2, space="buy", optimize=True) rm_scout_mult = DecimalParameter(0.55, 1.10, default=1.0, decimals=2, space="buy", optimize=True) rm_atr_soft = DecimalParameter(0.030, 0.105, default=0.085, decimals=3, space="buy", optimize=True) rm_atr_hard = DecimalParameter(0.050, 0.120, default=0.12, decimals=3, space="buy", optimize=True) rm_atr_soft_mult = DecimalParameter(0.55, 1.00, default=0.98, decimals=2, space="buy", optimize=True) rm_atr_hard_mult = DecimalParameter(0.30, 1.00, default=0.92, decimals=2, space="buy", optimize=True) rm_btc_weak_change3 = DecimalParameter(-0.080, 0.010, default=-0.08, decimals=3, space="buy", optimize=True) rm_btc_weak_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True) rm_p4h_weak_change3 = DecimalParameter(-0.060, 0.010, default=-0.06, decimals=3, space="buy", optimize=True) rm_p4h_weak_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True) rm_ema20_ext_soft = DecimalParameter(1.040, 1.200, default=1.16, decimals=3, space="buy", optimize=True) rm_ema20_ext_hard = DecimalParameter(1.080, 1.300, default=1.24, decimals=3, space="buy", optimize=True) rm_extension_soft_mult = DecimalParameter(0.55, 1.00, default=0.98, decimals=2, space="buy", optimize=True) rm_extension_hard_mult = DecimalParameter(0.30, 1.00, default=0.93, decimals=2, space="buy", optimize=True) rm_hot_rsi = IntParameter(72, 88, default=88, space="buy", optimize=True) rm_hot_rsi_mult = DecimalParameter(0.45, 0.95, default=0.95, decimals=2, space="buy", optimize=True) rm_volume_floor = DecimalParameter(0.30, 0.85, default=0.4, decimals=2, space="buy", optimize=True) rm_volume_floor_mult = DecimalParameter(0.55, 1.00, default=0.9, decimals=2, space="buy", optimize=True) rm_strong_boost = DecimalParameter(1.00, 1.18, default=1.08, decimals=2, space="buy", optimize=True) rm_strong_btc_change3 = DecimalParameter(0.010, 0.080, default=0.025, decimals=3, space="buy", optimize=True) rm_strong_p4h_change3 = DecimalParameter(0.008, 0.050, default=0.02, decimals=3, space="buy", optimize=True) rm_strong_p4h_rsi = IntParameter(58, 76, default=66, space="buy", optimize=True) rm_dd_start = DecimalParameter(0.060, 0.220, default=0.22, decimals=3, space="buy", optimize=True) rm_dd_hard = DecimalParameter(0.120, 0.320, default=0.3, decimals=3, space="buy", optimize=True) rm_dd_hard_mult = DecimalParameter(0.35, 0.90, default=0.9, decimals=2, space="buy", optimize=True) rm_recent_loss_mult = DecimalParameter(0.50, 0.95, default=0.94, decimals=2, space="buy", optimize=True) rm_early_exit_enabled = BooleanParameter(default=False, space="sell", optimize=True) rm_macro_fail_profit = DecimalParameter(-0.085, -0.025, default=-0.05, decimals=3, space="sell", optimize=True) rm_reclaim_fail_profit = DecimalParameter(-0.060, -0.012, default=-0.026, decimals=3, space="sell", optimize=True) rm_shock_profit = DecimalParameter(-0.060, -0.012, default=-0.025, decimals=3, space="sell", optimize=True) rm_profit_guard_level = DecimalParameter(0.030, 0.090, default=0.055, decimals=3, space="sell", optimize=True) rm_profit_guard_pullback = DecimalParameter(0.012, 0.045, default=0.025, decimals=3, space="sell", optimize=True) def _latest_candle(self, pair: str, current_time: datetime) -> dict[str, Any]: if not self.dp: return {} dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe is None or dataframe.empty: return {} if "date" in dataframe.columns: sliced = dataframe.loc[dataframe["date"] <= current_time] if not sliced.empty: return sliced.iloc[-1].to_dict() return dataframe.iloc[-1].to_dict() @staticmethod def _as_float(candle: dict[str, Any], key: str, default: float) -> float: try: value = float(candle.get(key, default)) if value != value: return default return value except (TypeError, ValueError): return default def _wallet_dd_multiplier(self) -> float: try: total = float(self.wallets.get_total_stake_amount()) except (AttributeError, TypeError, ValueError): return 1.0 if total <= 0: return 1.0 peak = float(getattr(self, "_rm_peak_balance", 0.0)) peak = max(peak, total) setattr(self, "_rm_peak_balance", peak) drawdown = max(0.0, (peak - total) / max(peak, 1e-9)) start = float(self.rm_dd_start.value) hard = float(self.rm_dd_hard.value) if drawdown <= start: return 1.0 if drawdown >= hard: return float(self.rm_dd_hard_mult.value) span = max(hard - start, 1e-9) pressure = (drawdown - start) / span hard_mult = float(self.rm_dd_hard_mult.value) return 1.0 - pressure * (1.0 - hard_mult) def _recent_trade_multiplier(self, pair: str, current_time: datetime) -> float: try: trades = Trade.get_trades_proxy(pair=pair, is_open=False) except (AttributeError, TypeError): return 1.0 closed: list[Trade] = [] for trade in trades: close_date = getattr(trade, "close_date_utc", None) if close_date is not None and close_date < current_time: closed.append(trade) if len(closed) < 2: return 1.0 closed.sort(key=lambda item: item.close_date_utc) recent = closed[-4:] losses = 0 profit_sum = 0.0 for trade in recent: profit_ratio = float(getattr(trade, "close_profit", 0.0) or 0.0) profit_sum += profit_ratio if profit_ratio < 0: losses += 1 if losses >= 2 or profit_sum < -0.03: return float(self.rm_recent_loss_mult.value) return 1.0 def _rm_tag_multiplier(self, entry_tag: str | None) -> float: tag = entry_tag or "" if tag == "macro_bull_hold": return float(self.rm_macro_mult.value) if tag in {"trend_strong_pullback", "trend_medium_pullback", "trend_opportunistic_pullback"}: return float(self.rm_trend_mult.value) if tag == "macro_bear_rebound": return float(self.rm_rebound_mult.value) if tag in {"post_capitulation_reclaim", "bottom_reclaim"}: return float(self.rm_reentry_mult.value) if tag in {"macro_bear_rebound_scout", "macro_bear_rebound_weak", "post_capitulation_reclaim_weak"}: return float(self.rm_scout_mult.value) return 1.0 def _market_multiplier(self, candle: dict[str, Any]) -> float: close = self._as_float(candle, "close", 0.0) ema20 = self._as_float(candle, "ema20", close) rsi = self._as_float(candle, "rsi", 50.0) atr_pct = self._as_float(candle, "atr_pct", 0.0) volume_ratio = self._as_float(candle, "volume_ratio", 1.0) btc_change3 = self._as_float(candle, "btc_change3_1d", 0.0) p4h_change3 = self._as_float(candle, "p4h_change3_4h", 0.0) p4h_rsi = self._as_float(candle, "p4h_rsi_4h", 50.0) mult = float(self.rm_base_stake_cap.value) if atr_pct > float(self.rm_atr_hard.value): mult *= float(self.rm_atr_hard_mult.value) elif atr_pct > float(self.rm_atr_soft.value): mult *= float(self.rm_atr_soft_mult.value) if btc_change3 < float(self.rm_btc_weak_change3.value): mult *= float(self.rm_btc_weak_mult.value) if p4h_change3 < float(self.rm_p4h_weak_change3.value): mult *= float(self.rm_p4h_weak_mult.value) if close > 0 and ema20 > 0: extension = close / ema20 if extension > float(self.rm_ema20_ext_hard.value): mult *= float(self.rm_extension_hard_mult.value) elif extension > float(self.rm_ema20_ext_soft.value): mult *= float(self.rm_extension_soft_mult.value) if rsi > int(self.rm_hot_rsi.value): mult *= float(self.rm_hot_rsi_mult.value) if volume_ratio < float(self.rm_volume_floor.value): mult *= float(self.rm_volume_floor_mult.value) strong = ( btc_change3 > float(self.rm_strong_btc_change3.value) and p4h_change3 > float(self.rm_strong_p4h_change3.value) and p4h_rsi > int(self.rm_strong_p4h_rsi.value) and atr_pct <= float(self.rm_atr_soft.value) ) if strong: mult *= float(self.rm_strong_boost.value) return mult def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: base_stake = self._v118_custom_stake_amount( pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs, ) if not bool(self.rm_enabled.value): if entry_tag == "v122_late_reclaim": late_stake = min( float(base_stake) * float(self.v122_late_stake_mult.value), float(max_stake), ) if min_stake is not None and late_stake > 0: late_stake = max(late_stake, float(min_stake)) return late_stake return base_stake candle = self._latest_candle(pair, current_time) multiplier = ( self._tag_multiplier(entry_tag) * self._market_multiplier(candle) * self._wallet_dd_multiplier() * self._recent_trade_multiplier(pair, current_time) ) multiplier = max(float(self.rm_min_stake_mult.value), min(multiplier, 1.10)) stake = min(float(base_stake) * multiplier, float(max_stake)) if min_stake is not None and stake > 0: stake = max(stake, float(min_stake)) return stake def _rm_custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> str | bool | None: if bool(self.rm_early_exit_enabled.value): try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] prev = dataframe.iloc[-2] if len(dataframe) > 1 else candle close = float(candle.get("close", current_rate)) ema20 = float(candle.get("ema20", current_rate)) ema50 = float(candle.get("ema50", current_rate)) rsi = float(candle.get("rsi", 50.0)) macd = float(candle.get("macd", 0.0)) macdsignal = float(candle.get("macdsignal", 0.0)) macdhist = float(candle.get("macdhist", 0.0)) prev_macdhist = float(prev.get("macdhist", macdhist)) close_location = float(candle.get("close_location", 0.5)) btc_change3 = float(candle.get("btc_change3_1d", 0.0)) p4h_change3 = float(candle.get("p4h_change3_4h", 0.0)) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return self._v118_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs) tag = trade.enter_tag or "" weak_intraday = close < ema20 and macd < macdsignal and macdhist < prev_macdhist weak_structure = close < ema50 and rsi < 52 and close_location < 0.55 macro_shock = btc_change3 < -0.055 or p4h_change3 < -0.040 reclaim_tag = tag in {"macro_bear_rebound", "macro_bear_rebound_scout", "post_capitulation_reclaim", "bottom_reclaim"} if current_profit < float(self.rm_shock_profit.value) and macro_shock and close < ema20: return "rm_market_shock_exit" if tag == "macro_bull_hold" and current_profit < float(self.rm_macro_fail_profit.value): if weak_intraday and weak_structure: return "rm_macro_fail_exit" if reclaim_tag and current_profit < float(self.rm_reclaim_fail_profit.value): if weak_intraday or (weak_structure and p4h_change3 < -0.015): return "rm_reclaim_fail_exit" if current_profit > float(self.rm_profit_guard_level.value): pullback = (trade.max_rate - current_rate) / max(trade.max_rate, 1e-9) if pullback > float(self.rm_profit_guard_pullback.value) and weak_intraday: return "rm_profit_guard_exit" return self._v118_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs) def leverage( self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: return 1.0 """Risk-aware V118 research branch. V122 keeps V118's proven core intact, then adds two live-risk controls: light capital throttling for weak / stretched conditions, and a small late-reclaim entry so the bot can participate after a confirmed recovery without trying to catch the bottom. """ INTERFACE_VERSION = 3 rm_min_stake_mult = DecimalParameter(0.35, 0.98, default=0.9, decimals=2, space="buy", optimize=True) rm_rebound_mult = DecimalParameter(0.60, 1.05, default=0.97, decimals=2, space="buy", optimize=True) rm_reentry_mult = DecimalParameter(0.55, 1.05, default=0.94, decimals=2, space="buy", optimize=True) rm_scout_mult = DecimalParameter(0.60, 1.08, default=1.0, decimals=2, space="buy", optimize=True) rm_atr_soft = DecimalParameter(0.040, 0.110, default=0.085, decimals=3, space="buy", optimize=True) rm_atr_hard = DecimalParameter(0.060, 0.160, default=0.12, decimals=3, space="buy", optimize=True) rm_atr_soft_mult = DecimalParameter(0.65, 1.00, default=0.98, decimals=2, space="buy", optimize=True) rm_atr_hard_mult = DecimalParameter(0.45, 0.98, default=0.92, decimals=2, space="buy", optimize=True) rm_btc_weak_change3 = DecimalParameter(-0.120, -0.005, default=-0.08, decimals=3, space="buy", optimize=True) rm_btc_weak_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True) rm_p4h_weak_change3 = DecimalParameter(-0.100, -0.005, default=-0.06, decimals=3, space="buy", optimize=True) rm_p4h_weak_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True) rm_ema20_ext_soft = DecimalParameter(1.060, 1.200, default=1.16, decimals=3, space="buy", optimize=True) rm_ema20_ext_hard = DecimalParameter(1.110, 1.300, default=1.24, decimals=3, space="buy", optimize=True) rm_extension_soft_mult = DecimalParameter(0.65, 1.00, default=0.98, decimals=2, space="buy", optimize=True) rm_extension_hard_mult = DecimalParameter(0.45, 0.98, default=0.93, decimals=2, space="buy", optimize=True) rm_hot_rsi = IntParameter(76, 92, default=88, space="buy", optimize=True) rm_hot_rsi_mult = DecimalParameter(0.55, 1.00, default=0.95, decimals=2, space="buy", optimize=True) rm_dd_start = DecimalParameter(0.120, 0.300, default=0.22, decimals=3, space="buy", optimize=True) rm_dd_hard = DecimalParameter(0.180, 0.380, default=0.3, decimals=3, space="buy", optimize=True) rm_dd_hard_mult = DecimalParameter(0.50, 0.98, default=0.9, decimals=2, space="buy", optimize=True) rm_recent_loss_mult = DecimalParameter(0.60, 1.00, default=0.94, decimals=2, space="buy", optimize=True) v122_fee_safety_enabled = BooleanParameter(default=False, space="buy", optimize=True) v122_block_hot_rsi = IntParameter(78, 90, default=84, space="buy", optimize=True) v122_block_extension = DecimalParameter(1.080, 1.200, default=1.14, decimals=3, space="buy", optimize=True) v122_block_volume_floor = DecimalParameter(0.35, 1.10, default=0.7, decimals=2, space="buy", optimize=True) v122_block_close_location = DecimalParameter(0.35, 0.70, default=0.5, decimals=2, space="buy", optimize=True) v122_late_reclaim_enabled = BooleanParameter(default=False, space="buy", optimize=True) v122_late_stake_mult = DecimalParameter(0.10, 0.75, default=0.45, decimals=2, space="buy", optimize=True) v122_late_volume_min = DecimalParameter(0.80, 2.20, default=1.15, decimals=2, space="buy", optimize=True) v122_late_rsi_min = IntParameter(48, 64, default=54, space="buy", optimize=True) v122_late_rsi_max = IntParameter(66, 84, default=76, space="buy", optimize=True) v122_late_max_extension = DecimalParameter(1.030, 1.130, default=1.08, decimals=3, space="buy", optimize=True) v122_late_btc_change_floor = DecimalParameter(-0.090, -0.005, default=-0.045, decimals=3, space="buy", optimize=True) v122_late_p4h_change_floor = DecimalParameter(-0.040, 0.030, default=-0.005, decimals=3, space="buy", optimize=True) v122_late_close_location = DecimalParameter(0.48, 0.78, default=0.58, decimals=2, space="buy", optimize=True) def _tag_multiplier(self, entry_tag: str | None) -> float: if entry_tag == "v122_late_reclaim": return float(self.v122_late_stake_mult.value) return self._rm_tag_multiplier(entry_tag) def custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs: Any, ) -> float | None: if trade.enter_tag == "v122_late_reclaim": if current_profit > 0.080: return 0.020 if current_profit > 0.040: return 0.005 try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] atr = float(candle.get("atr", trade.open_rate * 0.025)) ema50 = float(candle.get("ema50", trade.open_rate * 0.95)) stop_price = max(min(ema50 - 0.6 * atr, trade.open_rate * 0.985), trade.open_rate * 0.925) return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return 0.075 return self._v118_custom_stoploss(pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs) def custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> str | bool | None: parent_exit = self._rm_custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs) if parent_exit: return parent_exit if trade.enter_tag != "v122_late_reclaim": return None try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = dataframe.iloc[-1] close = float(candle.get("close", current_rate)) ema20 = float(candle.get("ema20", current_rate)) ema50 = float(candle.get("ema50", current_rate)) rsi = float(candle.get("rsi", 50.0)) macd = float(candle.get("macd", 0.0)) macdsignal = float(candle.get("macdsignal", 0.0)) btc_change3 = float(candle.get("btc_change3_1d", 0.0)) p4h_change3 = float(candle.get("p4h_change3_4h", 0.0)) except (AttributeError, IndexError, KeyError, TypeError, ValueError): return None age_hours = (current_time - trade.open_date_utc).total_seconds() / 3600.0 if current_profit < -0.028 and (close < ema50 or btc_change3 < -0.055 or p4h_change3 < -0.035): return "v122_late_failed" if age_hours >= 30 and current_profit < 0.008 and close < ema20 and rsi < 55: return "v122_late_time_stop" if current_profit > 0.055 and close < ema20 and macd < macdsignal: return "v122_late_profit_guard" return None def populate_entry_trend(self, dataframe, metadata: dict): dataframe = self._v118_populate_entry_trend(dataframe, metadata) if bool(self.v122_fee_safety_enabled.value): extension = dataframe["close"] / dataframe["ema20"].replace(0, float("nan")) hot_weak_close = ( dataframe["enter_tag"].eq("macro_bull_hold") & (dataframe["rsi"] > int(self.v122_block_hot_rsi.value)) & (extension > float(self.v122_block_extension.value)) & (dataframe["volume_ratio"] < float(self.v122_block_volume_floor.value)) & (dataframe["close_location"] < float(self.v122_block_close_location.value)) ) weak_reclaim_quality = ( dataframe["enter_tag"].isin(["macro_bear_rebound_scout", "post_capitulation_reclaim"]) & ( (dataframe["close_location"] < 0.48) | ( (dataframe["volume_ratio"] < 0.75) & (dataframe["p4h_change3_4h"] < -0.020) ) ) ) dataframe.loc[hot_weak_close | weak_reclaim_quality, ["enter_long", "enter_tag"]] = (0, "") extension = dataframe["close"] / dataframe["ema20"].replace(0, float("nan")) late_reclaim = ( bool(self.v122_late_reclaim_enabled.value) & (dataframe.get("enter_long", 0) != 1) & (dataframe["btc_change3_1d"] > float(self.v122_late_btc_change_floor.value)) & (dataframe["p4h_change3_4h"] > float(self.v122_late_p4h_change_floor.value)) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * 0.82) & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["close"] > dataframe["ema50"]) & ((dataframe["ema8"] > dataframe["ema20"]) | (dataframe["close"] > dataframe["range_high_12"])) & (dataframe["close"] > dataframe["range_high_12"]) & (dataframe["rsi"] > int(self.v122_late_rsi_min.value)) & (dataframe["rsi"] < int(self.v122_late_rsi_max.value)) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["macdhist"].shift(1) > dataframe["macdhist"].shift(2)) & (dataframe["volume_ratio"] > float(self.v122_late_volume_min.value)) & (dataframe["close_location"] > float(self.v122_late_close_location.value)) & (extension < float(self.v122_late_max_extension.value)) & (dataframe["atr_pct"] < float(self.buy_atr_max.value)) & (dataframe["volume"] > 0) ) dataframe.loc[late_reclaim, ["enter_long", "enter_tag"]] = (1, "v122_late_reclaim") return dataframe class _KhoaAnnualGrowthV142FrozenV145(_KhoaAnnualGrowthV122RiskTunePFFrozenV145): """Frozen V122PF plus a causal weak-BTC/weak-volume stake throttle.""" v142_fragile_enabled = BooleanParameter(default=True, space="buy", optimize=False) v142_btc_change3_ceiling = DecimalParameter( -0.050, 0.080, default=0.000, decimals=3, space="buy", optimize=False ) v142_volume_ceiling = DecimalParameter( 0.60, 1.20, default=0.90, decimals=2, space="buy", optimize=False ) v142_fragile_stake_scale = DecimalParameter( 0.10, 0.90, default=0.25, decimals=2, space="buy", optimize=False ) def bot_start(self, **kwargs: Any) -> None: super().bot_start(**kwargs) for name in ( "v142_fragile_enabled", "v142_btc_change3_ceiling", "v142_volume_ceiling", "v142_fragile_stake_scale", ): if name in self.config: getattr(self, name).value = self.config[name] def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: stake = super().custom_stake_amount( pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs, ) if not bool(self.v142_fragile_enabled.value) or entry_tag != "macro_bull_hold": return stake candle = self._latest_candle(pair, current_time) btc_change3 = self._as_float(candle, "btc_change3_1d", 1.0) volume_ratio = self._as_float(candle, "volume_ratio", 10.0) fragile = ( btc_change3 <= float(self.v142_btc_change3_ceiling.value) and volume_ratio <= float(self.v142_volume_ceiling.value) ) if not fragile: return stake adjusted = min( float(stake) * float(self.v142_fragile_stake_scale.value), float(max_stake), ) if min_stake is not None and adjusted > 0: adjusted = max(adjusted, float(min_stake)) return adjusted class _KhoaAnnualGrowthV144FrozenV145(_KhoaAnnualGrowthV142FrozenV145): """V142 with bounded exposure boosts for profitable recovery modules.""" v144_scout_boost = DecimalParameter( 1.00, 2.50, default=2.50, decimals=2, space="buy", optimize=False ) v144_reclaim_boost = DecimalParameter( 1.00, 1.10, default=1.087, decimals=3, space="buy", optimize=False ) def bot_start(self, **kwargs: Any) -> None: super().bot_start(**kwargs) for name in ("v144_scout_boost", "v144_reclaim_boost"): if name in self.config: getattr(self, name).value = self.config[name] def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: stake = super().custom_stake_amount( pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs, ) if entry_tag == "macro_bear_rebound_scout": stake *= float(self.v144_scout_boost.value) elif entry_tag == "post_capitulation_reclaim": stake *= float(self.v144_reclaim_boost.value) stake = min(float(stake), float(max_stake)) if min_stake is not None and stake > 0: stake = max(stake, float(min_stake)) return stake class KhoaAnnualGrowthV157(_KhoaAnnualGrowthV144FrozenV145): use_custom_roi = True """V149 with an independently gated bull breakout research route.""" v145_trend_scale = DecimalParameter( 0.10, 1.00, default=0.50, decimals=2, space="buy", optimize=False ) v145_late_scale = DecimalParameter( 0.10, 1.00, default=0.25, decimals=2, space="buy", optimize=False ) v146_macro_stake_scale = DecimalParameter( 0.40, 1.00, default=1.00, decimals=2, space="buy", optimize=False ) v146_macro_guard_enabled = BooleanParameter(default=False, space="sell", optimize=False) v146_macro_stop = DecimalParameter( 0.06, 0.18, default=0.10, decimals=3, space="sell", optimize=False ) v146_macro_fail_enabled = BooleanParameter(default=False, space="sell", optimize=False) v146_macro_fail_profit = DecimalParameter( -0.060, -0.015, default=-0.030, decimals=3, space="sell", optimize=False ) v146_breakout_enabled = BooleanParameter(default=True, space="buy", optimize=False) v146_disable_late_reclaim = BooleanParameter(default=False, space="buy", optimize=False) v146_btc_bear_only = BooleanParameter(default=True, space="buy", optimize=False) v146_breakout_stake_scale = DecimalParameter( 0.15, 0.75, default=0.35, decimals=2, space="buy", optimize=False ) v146_breakout_stop = DecimalParameter( 0.03, 0.10, default=0.06, decimals=3, space="sell", optimize=False ) v146_breakout_volume_min = DecimalParameter( 1.20, 8.00, default=5.00, decimals=2, space="buy", optimize=False ) v146_breakout_extension_max = DecimalParameter( 1.025, 1.090, default=1.055, decimals=3, space="buy", optimize=False ) v146_base_range_max = DecimalParameter( 0.025, 0.100, default=0.045, decimals=3, space="buy", optimize=False ) v146_prior_return_max = DecimalParameter( 0.010, 0.090, default=0.035, decimals=3, space="buy", optimize=False ) v147_spike_volume_min = DecimalParameter( 2.0, 5.0, default=3.0, decimals=2, space="buy", optimize=False ) v147_spike_scale = DecimalParameter( 0.25, 1.0, default=1.0, decimals=2, space="buy", optimize=False ) v147_btc_drop_floor = DecimalParameter( -0.05, 0.02, default=0.0, decimals=3, space="buy", optimize=False ) v147_btc_drop_scale = DecimalParameter( 0.25, 1.0, default=1.0, decimals=2, space="buy", optimize=False ) v148_require_valid_btc_ema200 = BooleanParameter(default=False, space="buy", optimize=False) v149_require_valid_slow_reference = BooleanParameter(default=True, space="buy", optimize=False) v149_require_ema100_warmup = BooleanParameter(default=False, space="buy", optimize=False) v150_bull_breakout_enabled = BooleanParameter(default=False, space="buy", optimize=False) v150_bull_stake_scale = DecimalParameter( 0.10, 0.40, default=0.20, decimals=2, space="buy", optimize=False ) v150_bull_btc_slow_min = DecimalParameter( 1.00, 1.12, default=1.02, decimals=3, space="buy", optimize=False ) v150_bull_btc_change3_floor = DecimalParameter( -0.08, 0.02, default=-0.03, decimals=3, space="buy", optimize=False ) v151_bull_roi_hold_hours = IntParameter( 0, 168, default=0, space="sell", optimize=False ) def bot_start(self, **kwargs: Any) -> None: super().bot_start(**kwargs) self._v151_original_roi = dict(self.minimal_roi) if int(self.config.get("v151_bull_roi_hold_hours", 0)) > 0: self.minimal_roi = {0: 10.0} for name in ( "v145_trend_scale", "v145_late_scale", "v146_macro_stake_scale", "v146_macro_stop", "v146_macro_guard_enabled", "v146_macro_fail_enabled", "v146_macro_fail_profit", "v146_breakout_enabled", "v146_disable_late_reclaim", "v146_btc_bear_only", "v146_breakout_stake_scale", "v146_breakout_stop", "v146_breakout_volume_min", "v146_breakout_extension_max", "v146_base_range_max", "v146_prior_return_max", "v147_spike_volume_min", "v147_spike_scale", "v147_btc_drop_floor", "v147_btc_drop_scale", "v148_require_valid_btc_ema200", "v149_require_valid_slow_reference", "v149_require_ema100_warmup", "v150_bull_breakout_enabled", "v150_bull_stake_scale", "v150_bull_btc_slow_min", "v150_bull_btc_change3_floor", "v151_bull_roi_hold_hours", ): if name in self.config: getattr(self, name).value = self.config[name] def custom_roi( self, pair: str, trade: Trade, current_time: datetime, trade_duration: int, entry_tag: str | None, side: str, **kwargs: Any, ) -> float | None: hold_hours = int(self._v146_value("v151_bull_roi_hold_hours")) if hold_hours <= 0: return None if entry_tag == "v150_bull_breakout": if trade_duration < 453: return 0.187 if trade_duration < hold_hours * 60: return 0.075 return 0.0 for minute, threshold in sorted( ((int(minute), value) for minute, value in self._v151_original_roi.items()), reverse=True, ): if trade_duration >= minute: return float(threshold) return None def _v146_value(self, name: str) -> Any: return self.config.get(name, getattr(self, name).value) def custom_stake_amount( self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float | None, max_stake: float, leverage: float, entry_tag: str | None, side: str, **kwargs: Any, ) -> float: stake = super().custom_stake_amount( pair, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs, ) if entry_tag == "trend_strong_pullback": stake *= float(self._v146_value("v145_trend_scale")) elif entry_tag == "v122_late_reclaim": stake *= float(self._v146_value("v145_late_scale")) elif entry_tag == "macro_bull_hold": stake *= float(self._v146_value("v146_macro_stake_scale")) spike_scale = float(self._v146_value("v147_spike_scale")) btc_scale = float(self._v146_value("v147_btc_drop_scale")) if spike_scale < 1.0 or btc_scale < 1.0: try: frame, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = frame.iloc[-1] if float(candle["volume_ratio"]) >= float(self._v146_value("v147_spike_volume_min")): stake *= spike_scale if float(candle["btc_change3_1d"]) < float(self._v146_value("v147_btc_drop_floor")): stake *= btc_scale except (AttributeError, IndexError, KeyError, TypeError, ValueError): return 0.0 elif entry_tag == "v146_early_breakout": stake *= float(self._v146_value("v146_breakout_stake_scale")) elif entry_tag == "v150_bull_breakout": stake *= float(self._v146_value("v150_bull_stake_scale")) stake = min(float(stake), float(max_stake)) if min_stake is not None and stake > 0: stake = max(stake, float(min_stake)) return stake def custom_stoploss( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs: Any, ) -> float | None: if trade.enter_tag in {"v146_early_breakout", "v150_bull_breakout"} or ( trade.enter_tag == "macro_bull_hold" and bool(self._v146_value("v146_macro_guard_enabled")) ): risk = ( float(self._v146_value("v146_macro_stop")) if trade.enter_tag == "macro_bull_hold" else float(self._v146_value("v146_breakout_stop")) ) stop_price = trade.open_rate * (1.0 - risk) if current_rate <= stop_price: return 0.001 return stoploss_from_absolute(stop_price, current_rate, is_short=False, leverage=1.0) return super().custom_stoploss( pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs ) def custom_exit( self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs: Any, ) -> str | bool | None: parent_exit = super().custom_exit(pair, trade, current_time, current_rate, current_profit, **kwargs) if parent_exit or trade.enter_tag != "macro_bull_hold": return parent_exit if not bool(self._v146_value("v146_macro_fail_enabled")): return None if current_profit > float(self._v146_value("v146_macro_fail_profit")): return None try: frame, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) candle = frame.iloc[-1] prev = frame.iloc[-2] if ( float(candle["close"]) < float(candle["ema20"]) and float(candle["macdhist"]) < float(prev["macdhist"]) and (float(candle["rsi"]) < 52 or float(candle["p4h_change3_4h"]) < -0.01) ): return "v146_macro_fail" except (AttributeError, IndexError, KeyError, TypeError, ValueError): return None return None def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe = super().populate_entry_trend(dataframe, metadata) if bool(self._v146_value("v146_disable_late_reclaim")): late_reclaim = dataframe["enter_tag"].eq("v122_late_reclaim") dataframe.loc[late_reclaim, ["enter_long", "enter_tag"]] = (0, None) previous_high = dataframe["high"].rolling(24).max().shift(1) previous_low = dataframe["low"].rolling(24).min().shift(1) previous_return = dataframe["close"].shift(1) / dataframe["close"].shift(25) - 1.0 breakout_base = ( dataframe["enter_long"].ne(1) & (dataframe["close"] > previous_high) & (previous_high / previous_low.replace(0, float("nan")) - 1.0 < float(self._v146_value("v146_base_range_max"))) & (previous_return < float(self._v146_value("v146_prior_return_max"))) & (dataframe["close"] > dataframe["open"]) & (dataframe["close"] > dataframe["ema20"]) & (dataframe["ema8"] > dataframe["ema20"]) & (dataframe["close"] / dataframe["ema20"].replace(0, float("nan")) < float(self._v146_value("v146_breakout_extension_max"))) & (dataframe["rsi"] > 55) & (dataframe["rsi"] < 88) & (dataframe["macdhist"] > dataframe["macdhist"].shift(1)) & (dataframe["volume_ratio"] > float(self._v146_value("v146_breakout_volume_min"))) & (dataframe["close_location"] > 0.65) & (dataframe["close_4h"] > dataframe["p4h_ema20_4h"] * 0.985) & (dataframe["p4h_rsi_4h"] > 50) & (dataframe["btc_change3_1d"] > -0.08) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * 0.85) & (dataframe["volume"] > 0) ) bear_breakout = ( bool(self._v146_value("v146_breakout_enabled")) & breakout_base & ((not bool(self._v146_value("v146_btc_bear_only"))) | (dataframe["close_1d"] < dataframe["btc_slow_reference_1d"])) ) dataframe.loc[bear_breakout, ["enter_long", "enter_tag"]] = (1, "v146_early_breakout") bull_breakout = ( bool(self._v146_value("v150_bull_breakout_enabled")) & breakout_base & dataframe["enter_long"].ne(1) & (dataframe["close_1d"] > dataframe["btc_slow_reference_1d"] * float(self._v146_value("v150_bull_btc_slow_min"))) & (dataframe["btc_change3_1d"] > float(self._v146_value("v150_bull_btc_change3_floor"))) & (dataframe["close"].pct_change() < 0.03) ) dataframe.loc[bull_breakout, ["enter_long", "enter_tag"]] = (1, "v150_bull_breakout") if bool(self._v146_value("v148_require_valid_btc_ema200")): btc_ema200_valid = dataframe["btc_ema200_1d"].gt(0) & np.isfinite(dataframe["btc_ema200_1d"]) dataframe.loc[~btc_ema200_valid, ["enter_long", "enter_tag"]] = (0, None) if bool(self._v146_value("v149_require_valid_slow_reference")): reference_valid = dataframe["btc_slow_reference_1d"].gt(0) & np.isfinite(dataframe["btc_slow_reference_1d"]) dataframe.loc[~reference_valid, ["enter_long", "enter_tag"]] = (0, None) if bool(self._v146_value("v149_require_ema100_warmup")): ema100_valid = dataframe["btc_ema100_1d"].gt(0) & np.isfinite(dataframe["btc_ema100_1d"]) dataframe.loc[~ema100_valid, ["enter_long", "enter_tag"]] = (0, None) return dataframe