"""SentimentEventTrader — News-driven extreme sentiment strategy. LEARNING PHASE: Conviction/risk gates DISABLED. Trades extreme sentiment spikes detected by the NLP pipeline (FinBERT/VADER). Long on sentiment > 0.7, short on sentiment < -0.7. Gate: RSI must not already be extended in the direction of the trade. """ import logging import talib.abstract as ta from freqtrade.strategy import DecimalParameter, IntParameter, IStrategy from pandas import DataFrame logger = logging.getLogger(__name__) LEARNING_PHASE = True # Sentiment signal access try: import importlib HAS_SENTIMENT = ( importlib.util.find_spec("common.sentiment.scorer") is not None and importlib.util.find_spec("common.sentiment.signal") is not None ) except Exception: HAS_SENTIMENT = False class SentimentEventTrader(IStrategy): """Trades extreme sentiment events from NLP pipeline.""" INTERFACE_VERSION = 3 timeframe = "1h" can_short = False # Kraken spot only — short signals ignored until futures exchange added startup_candle_count = 50 stoploss = -0.12 use_custom_stoploss = True minimal_roi = { "0": 0.04, "60": 0.025, "240": 0.015, "480": 0.005, } # Hyperopt parameters buy_sentiment_threshold = DecimalParameter(0.5, 0.9, default=0.7, space="buy") sell_sentiment_threshold = DecimalParameter(-0.9, -0.5, default=-0.7, space="sell") buy_rsi_max = IntParameter(55, 75, default=65, space="buy") sell_rsi_min = IntParameter(25, 45, default=35, space="sell") atr_multiplier = DecimalParameter(1.0, 3.0, default=2.0, space="buy") # Cached sentiment scores per pair _sentiment_scores: dict[str, float] = {} def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14) dataframe["adx"] = ta.ADX(dataframe, timeperiod=14) dataframe["atr"] = ta.ATR(dataframe, timeperiod=14) dataframe["ema_20"] = ta.EMA(dataframe, timeperiod=20) dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=20) dataframe["volume_ratio"] = dataframe["volume"] / dataframe["volume_sma"] # Add sentiment score column from cached values pair = metadata.get("pair", "") sentiment = self._sentiment_scores.get(pair, 0.0) dataframe["sentiment_score"] = sentiment return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # LEARNING PHASE: Sentiment pipeline not yet connected to Freqtrade. # Use technical proxy: RSI mean-reversion + volume surge as sentiment # stand-in. Real sentiment scoring will be wired in Week 2 when # conviction pipeline is re-enabled in logging mode. has_sentiment = dataframe["sentiment_score"] > self.buy_sentiment_threshold.value # Technical fallback: RSI oversold bounce + volume surge (proxy for # capitulation → recovery pattern that sentiment would normally catch) technical_proxy = ( (dataframe["rsi"] < 35) & (dataframe["rsi"].shift(1) < dataframe["rsi"]) # RSI turning up & (dataframe["volume_ratio"] > 1.3) ) # Long: sentiment signal OR technical proxy, plus basic filters dataframe.loc[ ( (has_sentiment | technical_proxy) & (dataframe["rsi"] < self.buy_rsi_max.value) & (dataframe["rsi"] > 20) & (dataframe["volume"] > 0) ), "enter_long", ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Exit long: sentiment reverses or RSI overbought dataframe.loc[ ( (dataframe["sentiment_score"] < 0) | (dataframe["rsi"] > 75) ), "exit_long", ] = 1 # Exit short: sentiment reverses or RSI oversold dataframe.loc[ ( (dataframe["sentiment_score"] > 0) | (dataframe["rsi"] < 25) ), "exit_short", ] = 1 return dataframe def bot_loop_start(self, **kwargs) -> None: # Refresh sentiment scores for active pairs if HAS_SENTIMENT and hasattr(self, "dp") and self.dp is not None: try: pairs = self.dp.current_whitelist() for pair in pairs[:10]: signal = self._get_cached_signal(pair) if signal is not None: self._sentiment_scores[pair] = signal.get("sentiment_score", 0.0) except Exception as e: logger.warning("Sentiment refresh failed: %s", e) def _get_cached_signal(self, pair: str) -> dict | None: """Get cached conviction signal with sentiment data.""" return None def custom_leverage(self, pair: str, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs) -> float: return min(3.0, max_leverage) def confirm_trade_entry(self, pair, order_type, amount, rate, time_in_force, current_time, entry_tag, side, **kwargs) -> bool: logger.info("ENTRY SIGNAL %s: %s @ %.6f (sentiment, no gates)", pair, side, rate) return True def custom_stake_amount(self, current_time, current_rate, proposed_stake, min_stake, max_stake, leverage, entry_tag, side, **kwargs) -> float: return proposed_stake def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs) -> float: atr = self.dp.get_pair_dataframe(pair, self.timeframe)["atr"].iloc[-1] regime_mult = 1.0 atr_stop = (atr / current_rate) * self.atr_multiplier.value * regime_mult stop = -atr_stop if current_profit > 0.02: stop = max(stop, -0.025) if current_profit > 0.04: stop = max(stop, -0.015) return stop def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs): return None