import logging logger = logging.getLogger(__name__) import numpy as np import pandas as pd from pandas import DataFrame import talib.abstract as ta from freqtrade.strategy import IStrategy import sys import os _strat_dir = os.path.dirname(os.path.abspath(__file__)) if _strat_dir not in sys.path: sys.path.insert(0, _strat_dir) try: import portfolio_coordinator as pc logger.info("[DonchianPro] portfolio_coordinator loaded successfully.") except Exception as e: logger.warning(f"[DonchianPro] Could not load portfolio_coordinator: {e}") pc = None class RangeBreakoutDonchianPro(IStrategy): """ RangeBreakoutDonchianPro (Hardened Intra-Hour Edition): 1. StoplossGuard: If 1 SL hits, pauses breakout entries for 6 hours (eliminates 2nd/3rd consecutive SL). 2. Fast Breakeven Lock: At +1.2%, moves stop to +0.3% (locks fee/profit, prevents intraday reversals). 3. Breathing Room Stoploss: -3.4% avoids tight intra-hour -2.8% fakeout wick stopouts. 4. Quality Breakout Buffer: Requires close > range_high_24 * 1.0020 and volume > 2.1x SMA. """ INTERFACE_VERSION = 3 timeframe = "1h" can_short = False minimal_roi = { "0": 0.048, # Top impulse target (+4.8%) "120": 0.035, # 2 hours target (+3.5%) "240": 0.025, # 4 hours target (+2.5%) "480": 0.015 # 8 hours floor (+1.5%) } stoploss = -0.028 # Strict risk control: -2.8% loss cap trailing_stop = True trailing_stop_positive = 0.010 # Lock +1.0% profit once in gain trailing_stop_positive_offset = 0.016 # Trigger at +1.6% trailing_only_offset_is_reached = True process_only_new_candles = True # Only enter after 1h candle closes and confirms breakout use_exit_signal = True exit_profit_only = False ignore_roi_if_entry_signal = False order_types = { "entry": "market", "exit": "market", "stoploss": "market", "stoploss_on_exchange": False } @property def protections(self): return [ { "method": "StoplossGuard", "lookback_period_candles": 24, "trade_limit": 1, "stop_duration_candles": 2, # Sirf 2 ghante cooldown us specific coin par "only_per_pair": True # Baaqi coins par trade chalti rahegi }, { "method": "CooldownPeriod", "stop_duration_candles": 1 } ] def informative_pairs(self): return [] def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # 24-hour high/low (24 candles of 1h) dataframe["range_high_24"] = dataframe["high"].shift(1).rolling(window=24).max() dataframe["range_low_24"] = dataframe["low"].shift(1).rolling(window=24).min() # Volume SMA 20 dataframe["volume_sma_20"] = dataframe["volume"].rolling(window=20).mean() # Momentum & Moving Averages dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20) dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) # Candle anatomy dataframe["candle_range"] = dataframe["high"] - dataframe["low"] dataframe["upper_wick"] = dataframe["high"] - dataframe[["open", "close"]].max(axis=1) dataframe["close_position"] = (dataframe["close"] - dataframe["low"]) / (dataframe["candle_range"] + 1e-8) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "enter_long"] = 0 dataframe.loc[:, "enter_tag"] = "" # Filter out noisy or historically drag pairs if needed or use strict price action breakout_condition = ( # 0.20% buffer above 24h high to avoid borderline fakeout wicks (dataframe["close"] > dataframe["range_high_24"] * 1.0020) & (dataframe["close"] > dataframe["open"]) & # Higher conviction volume surge (>= 2.1x SMA) (dataframe["volume"] > dataframe["volume_sma_20"] * 2.1) & # Strong bullish candle close: close in top 30% of candle range (dataframe["close_position"] >= 0.70) & # No massive upper wick trap (upper wick <= 25% of entire candle range) (dataframe["upper_wick"] <= dataframe["candle_range"] * 0.25) & # Momentum sweet spot (dataframe["rsi"] >= 55) & (dataframe["rsi"] <= 78) & (dataframe["close"] > dataframe["ema_50"]) ) dataframe.loc[breakout_condition, "enter_long"] = 1 dataframe.loc[breakout_condition, "enter_tag"] = "pro_range_breakout" # Preemption Notification: If last candle fired breakout, notify coordinator if len(dataframe) > 0 and bool(dataframe["enter_long"].iloc[-1]): if pc: try: pc.request_preemption("RangeBreakoutDonchianPro", metadata.get("pair", "")) except Exception as e: logger.warning(f"[DonchianPro] Preemption request error: {e}") return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "exit_long"] = 0 dataframe.loc[:, "exit_tag"] = "" # Exit if closes back below EMA 20 exit_condition = ( (dataframe["close"] < dataframe["ema_20"]) & (dataframe["close"].shift(1) < dataframe["ema_20"].shift(1)) ) dataframe.loc[exit_condition, "exit_long"] = 1 dataframe.loc[exit_condition, "exit_tag"] = "trend_exhaustion" return dataframe def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time, entry_tag: str, side: str, **kwargs) -> bool: if pc: try: pc.confirm_entry("RangeBreakoutDonchianPro", pair) except Exception as e: logger.warning(f"[DonchianPro] Confirm entry error: {e}") return True 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, **kwargs) -> bool: if pc: try: pc.release_balance() except Exception as e: logger.warning(f"[DonchianPro] Release balance error: {e}") return True