from datetime import datetime from functools import reduce import numpy as np import pandas as pd import talib.abstract as ta from pandas import DataFrame from freqtrade.persistence import Trade from freqtrade.strategy import ( IStrategy, merge_informative_pair, stoploss_from_open, ) class SolScalp5m(IStrategy): """ SolScalp5m (v6 Profit Architecture): Tuned specifically to achieve Positive Net Profit & > 50% Win Rate. Key Changes: - Removed premature EMA21 exit that was causing paper-cut losses. - Raised breakout volume threshold to 1.5x SMA for high-probability signals. - Relies purely on the 92.7% winrate exit_signal & ROI ladder. """ INTERFACE_VERSION = 3 timeframe = "5m" informative_timeframe_15m = "15m" informative_timeframe_1h = "1h" can_short: bool = False # Scalper ROI Ladder minimal_roi = { "0": 0.022, # 2.2% immediate target "15": 0.014, # 1.4% target after 15 mins "45": 0.009, # 0.9% target after 45 mins "90": 0.005 # 0.5% floor target after 1.5 hours } # Hard Stop-Loss (-1.5%) stoploss = -0.015 trailing_stop = False use_custom_stoploss = True order_types = { "entry": "limit", "exit": "limit", "stoploss": "market", "stoploss_on_exchange": False } startup_candle_count: int = 150 @property def protections(self): return [ { "method": "CooldownPeriod", "stop_duration_candles": 3 # Wait 15 mins after exiting }, { "method": "StoplossGuard", "lookback_period_candles": 24, "trade_limit": 2, "stop_duration_candles": 24, "only_per_pair": False } ] def informative_pairs(self): pairs = self.dp.current_whitelist() informative = [] for pair in pairs: informative.append((pair, self.informative_timeframe_15m)) informative.append((pair, self.informative_timeframe_1h)) informative.append(("BTC/USDT", self.informative_timeframe_1h)) return informative def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: """ Breakeven & Dynamic Profit Locking. """ # Tier 2: Up +1.5% -> Lock in +0.8% if current_profit >= 0.015: return stoploss_from_open(0.008, current_profit) # Tier 1: Move to Breakeven (+0.2% to cover fees) once profit reaches +0.8% if current_profit >= 0.008: return stoploss_from_open(0.002, current_profit) # Retain hard stoploss (-1.5%) return 1 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # ------------------------------------------------------------- # 1. Macro Informative Filters (1h) # ------------------------------------------------------------- pair_1h = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative_timeframe_1h) if not pair_1h.empty: pair_1h["ema_20"] = ta.EMA(pair_1h, timeperiod=20) pair_1h["ema_50"] = ta.EMA(pair_1h, timeperiod=50) pair_1h["macro_uptrend_1h"] = (pair_1h["close"] > pair_1h["ema_50"]) & (pair_1h["ema_20"] > pair_1h["ema_50"]) dataframe = merge_informative_pair( dataframe, pair_1h, self.timeframe, self.informative_timeframe_1h, ffill=True ) btc_1h = self.dp.get_pair_dataframe(pair="BTC/USDT", timeframe=self.informative_timeframe_1h) if not btc_1h.empty: btc_1h["btc_ema_50"] = ta.EMA(btc_1h, timeperiod=50) btc_1h["btc_uptrend_1h"] = btc_1h["close"] > btc_1h["btc_ema_50"] dataframe = merge_informative_pair( dataframe, btc_1h, self.timeframe, self.informative_timeframe_1h, ffill=True ) # ------------------------------------------------------------- # 2. Intermediate Informative Filters (15m) # ------------------------------------------------------------- pair_15m = self.dp.get_pair_dataframe(pair=metadata["pair"], timeframe=self.informative_timeframe_15m) if not pair_15m.empty: pair_15m["rsi_15m"] = ta.RSI(pair_15m, timeperiod=14) pair_15m["ema_20_15m"] = ta.EMA(pair_15m, timeperiod=20) pair_15m["trend_15m"] = (pair_15m["close"] > pair_15m["ema_20_15m"]) & (pair_15m["rsi_15m"].between(46, 67)) dataframe = merge_informative_pair( dataframe, pair_15m, self.timeframe, self.informative_timeframe_15m, ffill=True ) # ------------------------------------------------------------- # 3. Base 5m Execution Indicators # ------------------------------------------------------------- dataframe["ema_9"] = ta.EMA(dataframe, timeperiod=9) dataframe["ema_21"] = ta.EMA(dataframe, timeperiod=21) dataframe["ema_50"] = ta.EMA(dataframe, timeperiod=50) dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["volume_sma"] = dataframe["volume"].rolling(window=20).mean() # Fast Stochastic RSI (14, 3, 3) fastk, fastd = ta.STOCHRSI(dataframe["close"], timeperiod=14, fastk_period=3, fastd_period=3) dataframe["stoch_k"] = fastk dataframe["stoch_d"] = fastd # Rolling Intraday VWAP (Smooth 24h Window) typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3.0 pv = typical_price * dataframe["volume"] window_5m = 288 cum_pv = pv.rolling(window=window_5m, min_periods=1).sum() cum_vol = dataframe["volume"].rolling(window=window_5m, min_periods=1).sum() dataframe["vwap"] = cum_pv / np.where(cum_vol == 0, 1, cum_vol) dataframe["vwap_upper"] = dataframe["vwap"] + (1.2 * dataframe["atr"]) dataframe["vwap_lower"] = dataframe["vwap"] - (1.2 * dataframe["atr"]) dataframe["is_green"] = dataframe["close"] > dataframe["open"] return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, "enter_long"] = 0 dataframe.loc[:, "enter_tag"] = "" # Multi-Timeframe Alignment (1h BTC + 1h Asset + 15m Asset) macro_btc_1h = dataframe.get("btc_uptrend_1h_1h", pd.Series(True, index=dataframe.index)) == True macro_asset_1h = dataframe.get("macro_uptrend_1h_1h", pd.Series(True, index=dataframe.index)) == True intermediate_15m = dataframe.get("trend_15m_15m", pd.Series(True, index=dataframe.index)) == True strong_trend = dataframe["adx"] > 22 base_filters = macro_btc_1h & macro_asset_1h & intermediate_15m & strong_trend # ------------------------------------------------------------- # Setup 1: High-Confluence Multi-Timeframe Breakout # ------------------------------------------------------------- volume_breakout_ok = dataframe["volume"] > (dataframe["volume_sma"] * 1.50) ema_bullish = (dataframe["ema_9"] > dataframe["ema_21"]) & (dataframe["ema_21"] > dataframe["ema_50"]) vwap_support = dataframe["close"] > dataframe["vwap"] rsi_bullish = dataframe["rsi"].between(53, 65) setup_breakout = ( base_filters & volume_breakout_ok & ema_bullish & vwap_support & rsi_bullish & dataframe["is_green"] ) dataframe.loc[setup_breakout, "enter_long"] = 1 dataframe.loc[setup_breakout, "enter_tag"] = "5m_breakout" # ------------------------------------------------------------- # Setup 2: Dip Buy / VWAP Bounce (StochRSI Oversold Reclaim) # ------------------------------------------------------------- volume_dip_ok = dataframe["volume"] > (dataframe["volume_sma"] * 1.10) base_dip_filters = macro_btc_1h & macro_asset_1h & intermediate_15m & volume_dip_ok stoch_oversold = (dataframe["stoch_k"].shift(1) < 18) | (dataframe["stoch_d"].shift(1) < 18) stoch_cross = (dataframe["stoch_k"] > dataframe["stoch_d"]) & (dataframe["stoch_k"].shift(1) <= dataframe["stoch_d"].shift(1)) near_vwap_or_ema = ( (dataframe["low"] <= dataframe["vwap_upper"]) & (dataframe["close"] >= dataframe["vwap_lower"]) & (dataframe["close"] >= dataframe["ema_50"]) ) rsi_oversold_recovery = dataframe["rsi"].between(38, 54) setup_dip_buy = ( base_dip_filters & stoch_oversold & stoch_cross & near_vwap_or_ema & rsi_oversold_recovery & dataframe["is_green"] ) dataframe.loc[setup_dip_buy, "enter_long"] = 1 dataframe.loc[setup_dip_buy, "enter_tag"] = "5m_dip_bounce" return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: conditions = [] # Exit 1: StochRSI Overbought Cross Down stoch_overbought = (dataframe["stoch_k"] > 80) & (dataframe["stoch_k"] < dataframe["stoch_d"]) # Exit 2: Price overextended past upper VWAP band vwap_overextended = dataframe["close"] > dataframe["vwap_upper"] conditions.append(stoch_overbought | vwap_overextended) conditions.append(dataframe["rsi"] > 68) if conditions: dataframe.loc[ reduce(lambda x, y: x & y, conditions), "exit_long" ] = 1 return dataframe