gogo2/NN/utils/signal_interpreter.py
Dobromir Popov 73c5ecb0d2 enhancements
2025-04-01 13:46:53 +03:00

391 lines
16 KiB
Python

"""
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 - lower thresholds to increase trade frequency
self.buy_threshold = self.config.get('buy_threshold', 0.35)
self.sell_threshold = self.config.get('sell_threshold', 0.35)
self.hold_threshold = self.config.get('hold_threshold', 0.60)
# 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', False) # Disable trend filter by default
self.volume_filter_enabled = self.config.get('volume_filter_enabled', False) # Disable volume filter by default
self.oscillation_filter_enabled = self.config.get('oscillation_filter_enabled', False) # Disable oscillation filter by default
# Sensitivity parameters
self.min_price_movement = self.config.get('min_price_movement', 0.0001) # Lower price movement threshold
self.hold_cooldown = self.config.get('hold_cooldown', 1) # Shorter hold cooldown
self.consecutive_signals_required = self.config.get('consecutive_signals_required', 1) # Require only one signal
# 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")