""" Signal Interpreter for Neural Network Trading System Converts model predictions into actionable trading signals with enhanced profitability filters """ import numpy as np import logging from collections import deque import time logger = logging.getLogger('NN.utils.signal_interpreter') class SignalInterpreter: """ Enhanced signal interpreter for short-term high-leverage trading Converts model predictions to trading signals with adaptive filters """ def __init__(self, config=None): """ Initialize signal interpreter with configuration parameters Args: config (dict): Configuration dictionary with parameters """ self.config = config or {} # Signal thresholds - higher thresholds for high-leverage trading self.buy_threshold = self.config.get('buy_threshold', 0.65) self.sell_threshold = self.config.get('sell_threshold', 0.65) self.hold_threshold = self.config.get('hold_threshold', 0.75) # Adaptive parameters self.confidence_multiplier = self.config.get('confidence_multiplier', 1.0) self.signal_history = deque(maxlen=20) # Store recent signals for pattern recognition self.price_history = deque(maxlen=20) # Store recent prices for trend analysis # Performance tracking self.trade_count = 0 self.profitable_trades = 0 self.unprofitable_trades = 0 self.avg_profit_per_trade = 0 self.last_trade_time = None self.last_trade_price = None self.current_position = None # None = no position, 'long' = buy, 'short' = sell # Filters for better signal quality self.trend_filter_enabled = self.config.get('trend_filter_enabled', True) self.volume_filter_enabled = self.config.get('volume_filter_enabled', True) self.oscillation_filter_enabled = self.config.get('oscillation_filter_enabled', True) # Sensitivity parameters self.min_price_movement = self.config.get('min_price_movement', 0.0005) # 0.05% minimum expected movement self.hold_cooldown = self.config.get('hold_cooldown', 3) # Minimum periods to wait after a HOLD self.consecutive_signals_required = self.config.get('consecutive_signals_required', 2) # State tracking self.consecutive_buy_signals = 0 self.consecutive_sell_signals = 0 self.consecutive_hold_signals = 0 self.periods_since_last_trade = 0 logger.info("Signal interpreter initialized with enhanced filters for short-term trading") def interpret_signal(self, action_probs, price_prediction=None, market_data=None): """ Interpret model predictions to generate trading signal Args: action_probs (ndarray): Model action probabilities [SELL, HOLD, BUY] price_prediction (float): Predicted price change (optional) market_data (dict): Additional market data for filtering (optional) Returns: dict: Trading signal with action and metadata """ # Extract probabilities sell_prob, hold_prob, buy_prob = action_probs # Apply confidence multiplier - amplifies the signal when model is confident adjusted_buy_prob = min(buy_prob * self.confidence_multiplier, 1.0) adjusted_sell_prob = min(sell_prob * self.confidence_multiplier, 1.0) # Incorporate price prediction if available if price_prediction is not None: # Strengthen buy signal if price is predicted to rise if price_prediction > self.min_price_movement: adjusted_buy_prob *= (1.0 + price_prediction * 5) adjusted_sell_prob *= (1.0 - price_prediction * 2) # Strengthen sell signal if price is predicted to fall elif price_prediction < -self.min_price_movement: adjusted_sell_prob *= (1.0 + abs(price_prediction) * 5) adjusted_buy_prob *= (1.0 - abs(price_prediction) * 2) # Track consecutive signals to reduce false signals raw_signal = self._get_raw_signal(adjusted_buy_prob, adjusted_sell_prob, hold_prob) # Update consecutive signal counters if raw_signal == 'BUY': self.consecutive_buy_signals += 1 self.consecutive_sell_signals = 0 self.consecutive_hold_signals = 0 elif raw_signal == 'SELL': self.consecutive_buy_signals = 0 self.consecutive_sell_signals += 1 self.consecutive_hold_signals = 0 else: # HOLD self.consecutive_buy_signals = 0 self.consecutive_sell_signals = 0 self.consecutive_hold_signals += 1 # Apply trend filter if enabled and market data available if self.trend_filter_enabled and market_data and 'trend' in market_data: raw_signal = self._apply_trend_filter(raw_signal, market_data['trend']) # Apply volume filter if enabled and market data available if self.volume_filter_enabled and market_data and 'volume' in market_data: raw_signal = self._apply_volume_filter(raw_signal, market_data['volume']) # Apply oscillation filter to prevent excessive trading if self.oscillation_filter_enabled: raw_signal = self._apply_oscillation_filter(raw_signal) # Create final signal with confidence metrics and metadata signal = { 'action': raw_signal, 'timestamp': time.time(), 'confidence': self._calculate_confidence(adjusted_buy_prob, adjusted_sell_prob, hold_prob), 'price_prediction': price_prediction if price_prediction is not None else 0.0, 'consecutive_signals': max(self.consecutive_buy_signals, self.consecutive_sell_signals), 'periods_since_last_trade': self.periods_since_last_trade } # Update signal history self.signal_history.append(signal) self.periods_since_last_trade += 1 # Track trade if action taken if signal['action'] in ['BUY', 'SELL']: self._track_trade(signal, market_data) return signal def _get_raw_signal(self, buy_prob, sell_prob, hold_prob): """ Get raw signal based on adjusted probabilities Args: buy_prob (float): Buy probability sell_prob (float): Sell probability hold_prob (float): Hold probability Returns: str: Raw signal ('BUY', 'SELL', or 'HOLD') """ # Require higher consecutive signals for high-leverage actions if buy_prob > self.buy_threshold and self.consecutive_buy_signals >= self.consecutive_signals_required: return 'BUY' elif sell_prob > self.sell_threshold and self.consecutive_sell_signals >= self.consecutive_signals_required: return 'SELL' elif hold_prob > self.hold_threshold: return 'HOLD' elif buy_prob > sell_prob: # If close to threshold but not quite there, still prefer action over hold if buy_prob > self.buy_threshold * 0.8: return 'BUY' else: return 'HOLD' elif sell_prob > buy_prob: # If close to threshold but not quite there, still prefer action over hold if sell_prob > self.sell_threshold * 0.8: return 'SELL' else: return 'HOLD' else: return 'HOLD' def _apply_trend_filter(self, raw_signal, trend): """ Apply trend filter to align signals with overall market trend Args: raw_signal (str): Raw signal trend (str or float): Market trend indicator Returns: str: Filtered signal """ # Skip if fresh signal doesn't match trend if isinstance(trend, str): if raw_signal == 'BUY' and trend == 'downtrend': return 'HOLD' elif raw_signal == 'SELL' and trend == 'uptrend': return 'HOLD' elif isinstance(trend, (int, float)): # Trend as numerical value (positive = uptrend, negative = downtrend) if raw_signal == 'BUY' and trend < -0.2: return 'HOLD' elif raw_signal == 'SELL' and trend > 0.2: return 'HOLD' return raw_signal def _apply_volume_filter(self, raw_signal, volume): """ Apply volume filter to ensure sufficient liquidity for trade Args: raw_signal (str): Raw signal volume (dict): Volume data Returns: str: Filtered signal """ # Skip trading when volume is too low if volume.get('is_low', False) and raw_signal in ['BUY', 'SELL']: return 'HOLD' # Reduce sensitivity during volume spikes to avoid getting caught in volatility if volume.get('is_spike', False): # For short-term trading, a spike could be an opportunity if it confirms our signal if volume.get('direction', 0) > 0 and raw_signal == 'BUY': # Volume spike in buy direction - strengthen buy signal return raw_signal elif volume.get('direction', 0) < 0 and raw_signal == 'SELL': # Volume spike in sell direction - strengthen sell signal return raw_signal else: # Volume spike against our signal - be cautious return 'HOLD' return raw_signal def _apply_oscillation_filter(self, raw_signal): """ Apply oscillation filter to prevent excessive trading Returns: str: Filtered signal """ # Implement a cooldown period after HOLD signals if self.consecutive_hold_signals < self.hold_cooldown: # Check if we're switching positions too quickly if len(self.signal_history) >= 2: last_action = self.signal_history[-1]['action'] if last_action in ['BUY', 'SELL'] and raw_signal != last_action and raw_signal != 'HOLD': # We're trying to reverse position immediately after taking one # For high-leverage trading, this could be allowed if signal is very strong if raw_signal == 'BUY' and self.consecutive_buy_signals >= self.consecutive_signals_required * 1.5: # Extra strong buy signal - allow reversal return raw_signal elif raw_signal == 'SELL' and self.consecutive_sell_signals >= self.consecutive_signals_required * 1.5: # Extra strong sell signal - allow reversal return raw_signal else: # Not strong enough to justify immediate reversal return 'HOLD' # Check for oscillation patterns over time if len(self.signal_history) >= 4: # Look for alternating BUY/SELL pattern which indicates indecision actions = [s['action'] for s in list(self.signal_history)[-4:]] if actions.count('BUY') >= 2 and actions.count('SELL') >= 2: # Oscillating pattern detected, force a HOLD return 'HOLD' return raw_signal def _calculate_confidence(self, buy_prob, sell_prob, hold_prob): """ Calculate confidence score for the signal Args: buy_prob (float): Buy probability sell_prob (float): Sell probability hold_prob (float): Hold probability Returns: float: Confidence score (0.0-1.0) """ # Maximum probability indicates confidence level max_prob = max(buy_prob, sell_prob, hold_prob) # Calculate the gap between highest and second highest probability sorted_probs = sorted([buy_prob, sell_prob, hold_prob], reverse=True) prob_gap = sorted_probs[0] - sorted_probs[1] # Combine both factors - higher max and larger gap mean more confidence confidence = (max_prob * 0.7) + (prob_gap * 0.3) # Scale to ensure output is between 0 and 1 return min(max(confidence, 0.0), 1.0) def _track_trade(self, signal, market_data): """ Track trade for performance monitoring Args: signal (dict): Trading signal market_data (dict): Market data including price """ self.trade_count += 1 self.periods_since_last_trade = 0 # Update position state if signal['action'] == 'BUY': self.current_position = 'long' elif signal['action'] == 'SELL': self.current_position = 'short' # Store trade time and price if available current_time = time.time() current_price = market_data.get('price', None) if market_data else None # Record profitability if we have both current and previous trade data if self.last_trade_time and self.last_trade_price and current_price: # Calculate holding period holding_period = current_time - self.last_trade_time # Calculate profit/loss based on position if self.current_position == 'long' and signal['action'] == 'SELL': # Closing a long position profit_pct = (current_price - self.last_trade_price) / self.last_trade_price # Update trade statistics if profit_pct > 0: self.profitable_trades += 1 else: self.unprofitable_trades += 1 # Update average profit total_trades = self.profitable_trades + self.unprofitable_trades self.avg_profit_per_trade = ((self.avg_profit_per_trade * (total_trades - 1)) + profit_pct) / total_trades logger.info(f"Closed LONG position with {profit_pct:.4%} profit after {holding_period:.1f}s") elif self.current_position == 'short' and signal['action'] == 'BUY': # Closing a short position profit_pct = (self.last_trade_price - current_price) / self.last_trade_price # Update trade statistics if profit_pct > 0: self.profitable_trades += 1 else: self.unprofitable_trades += 1 # Update average profit total_trades = self.profitable_trades + self.unprofitable_trades self.avg_profit_per_trade = ((self.avg_profit_per_trade * (total_trades - 1)) + profit_pct) / total_trades logger.info(f"Closed SHORT position with {profit_pct:.4%} profit after {holding_period:.1f}s") # Update last trade info self.last_trade_time = current_time self.last_trade_price = current_price def get_performance_stats(self): """ Get trading performance statistics Returns: dict: Performance statistics """ total_trades = self.profitable_trades + self.unprofitable_trades win_rate = self.profitable_trades / total_trades if total_trades > 0 else 0 return { 'total_trades': self.trade_count, 'profitable_trades': self.profitable_trades, 'unprofitable_trades': self.unprofitable_trades, 'win_rate': win_rate, 'avg_profit_per_trade': self.avg_profit_per_trade } def reset(self): """Reset all trading statistics and state""" self.signal_history.clear() self.price_history.clear() self.trade_count = 0 self.profitable_trades = 0 self.unprofitable_trades = 0 self.avg_profit_per_trade = 0 self.last_trade_time = None self.last_trade_price = None self.current_position = None self.consecutive_buy_signals = 0 self.consecutive_sell_signals = 0 self.consecutive_hold_signals = 0 self.periods_since_last_trade = 0 logger.info("Signal interpreter reset")