Files
gogo2/core/nn_decision_fusion.py
2025-06-25 11:42:12 +03:00

277 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Neural Network Decision Fusion System
Central NN that merges all model outputs + market data for final trading decisions
"""
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
from typing import Dict, List, Optional, Any
from dataclasses import dataclass
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
@dataclass
class ModelPrediction:
"""Standardized prediction from any model"""
model_name: str
prediction_type: str # 'price', 'direction', 'action'
value: float # -1 to 1 for direction, actual price for price predictions
confidence: float # 0 to 1
timestamp: datetime
metadata: Optional[Dict[str, Any]] = None
@dataclass
class MarketContext:
"""Current market context for decision fusion"""
symbol: str
current_price: float
price_change_1m: float
price_change_5m: float
volume_ratio: float
volatility: float
timestamp: datetime
@dataclass
class FusionDecision:
"""Final trading decision from fusion NN"""
action: str # 'BUY', 'SELL', 'HOLD'
confidence: float # 0 to 1
expected_return: float # Expected return percentage
risk_score: float # 0 to 1, higher = riskier
position_size: float # Recommended position size
reasoning: str # Human-readable explanation
model_contributions: Dict[str, float] # How much each model contributed
timestamp: datetime
class DecisionFusionNetwork(nn.Module):
"""Small NN that fuses model predictions with market context"""
def __init__(self, input_dim: int = 32, hidden_dim: int = 64):
super().__init__()
self.fusion_layers = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(hidden_dim, hidden_dim // 2),
nn.ReLU(),
nn.Linear(hidden_dim // 2, 16)
)
# Output heads
self.action_head = nn.Linear(16, 3) # BUY, SELL, HOLD
self.confidence_head = nn.Linear(16, 1)
self.return_head = nn.Linear(16, 1)
def forward(self, features: torch.Tensor) -> Dict[str, torch.Tensor]:
"""Forward pass through fusion network"""
fusion_output = self.fusion_layers(features)
action_logits = self.action_head(fusion_output)
action_probs = F.softmax(action_logits, dim=1)
confidence = torch.sigmoid(self.confidence_head(fusion_output))
expected_return = torch.tanh(self.return_head(fusion_output))
return {
'action_probs': action_probs,
'confidence': confidence.squeeze(),
'expected_return': expected_return.squeeze()
}
class NeuralDecisionFusion:
"""Main NN-based decision fusion system"""
def __init__(self, training_mode: bool = True):
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.network = DecisionFusionNetwork().to(self.device)
self.training_mode = training_mode
self.registered_models = {}
self.last_predictions = {}
logger.info(f"🧠 Neural Decision Fusion initialized on {self.device}")
def register_model(self, model_name: str, model_type: str, prediction_format: str):
"""Register a model that will provide predictions"""
self.registered_models[model_name] = {
'type': model_type,
'format': prediction_format,
'prediction_count': 0
}
logger.info(f"Registered NN model: {model_name} ({model_type})")
def add_prediction(self, prediction: ModelPrediction):
"""Add a prediction from a registered model"""
self.last_predictions[prediction.model_name] = prediction
if prediction.model_name in self.registered_models:
self.registered_models[prediction.model_name]['prediction_count'] += 1
logger.debug(f"🔮 {prediction.model_name}: {prediction.value:.3f} "
f"(confidence: {prediction.confidence:.3f})")
def make_decision(self, symbol: str, market_context: MarketContext,
min_confidence: float = 0.25) -> Optional[FusionDecision]:
"""Make NN-driven trading decision"""
try:
if len(self.last_predictions) < 1:
logger.debug("No NN predictions available")
return None
# Prepare features
features = self._prepare_features(market_context)
if features is None:
return None
# Run NN inference
with torch.no_grad():
self.network.eval()
features_tensor = torch.tensor(features, dtype=torch.float32).unsqueeze(0).to(self.device)
outputs = self.network(features_tensor)
action_probs = outputs['action_probs'][0].cpu().numpy()
confidence = outputs['confidence'].cpu().item()
expected_return = outputs['expected_return'].cpu().item()
# Determine action
action_idx = np.argmax(action_probs)
actions = ['BUY', 'SELL', 'HOLD']
action = actions[action_idx]
# Check confidence threshold
if confidence < min_confidence:
action = 'HOLD'
logger.debug(f"Low NN confidence ({confidence:.3f}), defaulting to HOLD")
# Calculate position size
position_size = self._calculate_position_size(confidence, expected_return)
# Generate reasoning
reasoning = self._generate_reasoning(action, confidence, expected_return, action_probs)
# Calculate risk score and model contributions
risk_score = min(1.0, abs(expected_return) * 5 + (1 - confidence) * 0.5)
model_contributions = self._calculate_model_contributions()
decision = FusionDecision(
action=action,
confidence=confidence,
expected_return=expected_return,
risk_score=risk_score,
position_size=position_size,
reasoning=reasoning,
model_contributions=model_contributions,
timestamp=datetime.now()
)
logger.info(f"🧠 NN DECISION: {action} (conf: {confidence:.3f}, "
f"return: {expected_return:.3f}, size: {position_size:.4f})")
return decision
except Exception as e:
logger.error(f"Error in NN decision making: {e}")
return None
def _prepare_features(self, context: MarketContext) -> Optional[np.ndarray]:
"""Prepare feature vector for NN"""
try:
features = np.zeros(32)
# Model predictions (slots 0-15)
idx = 0
for model_name, prediction in self.last_predictions.items():
if idx < 14: # Leave room for other features
features[idx] = prediction.value
features[idx + 1] = prediction.confidence
idx += 2
# Market context (slots 16-31)
features[16] = np.tanh(context.price_change_1m * 100) # 1m change
features[17] = np.tanh(context.price_change_5m * 100) # 5m change
features[18] = np.tanh(context.volume_ratio - 1) # Volume ratio
features[19] = np.tanh(context.volatility * 100) # Volatility
features[20] = context.current_price / 10000.0 # Normalized price
# Time features
now = context.timestamp
features[21] = now.hour / 24.0
features[22] = now.weekday() / 7.0
# Model agreement features
if len(self.last_predictions) >= 2:
values = [p.value for p in self.last_predictions.values()]
features[23] = np.mean(values) # Average prediction
features[24] = np.std(values) # Prediction variance
features[25] = len(self.last_predictions) # Model count
return features
except Exception as e:
logger.error(f"Error preparing NN features: {e}")
return None
def _calculate_position_size(self, confidence: float, expected_return: float) -> float:
"""Calculate position size based on NN outputs"""
base_size = 0.01 # 0.01 ETH base
# Scale by confidence
confidence_multiplier = max(0.1, min(2.0, confidence * 1.5))
# Scale by expected return
return_multiplier = 1.0 + abs(expected_return) * 0.5
final_size = base_size * confidence_multiplier * return_multiplier
return max(0.001, min(0.05, final_size))
def _generate_reasoning(self, action: str, confidence: float,
expected_return: float, action_probs: np.ndarray) -> str:
"""Generate human-readable reasoning"""
reasons = []
if action == 'BUY':
reasons.append(f"NN suggests BUY ({action_probs[0]:.1%})")
elif action == 'SELL':
reasons.append(f"NN suggests SELL ({action_probs[1]:.1%})")
else:
reasons.append(f"NN suggests HOLD")
if confidence > 0.7:
reasons.append("High confidence")
elif confidence > 0.5:
reasons.append("Moderate confidence")
else:
reasons.append("Low confidence")
if abs(expected_return) > 0.01:
direction = "positive" if expected_return > 0 else "negative"
reasons.append(f"Expected {direction} return: {expected_return:.2%}")
reasons.append(f"Based on {len(self.last_predictions)} NN models")
return " | ".join(reasons)
def _calculate_model_contributions(self) -> Dict[str, float]:
"""Calculate how much each model contributed to the decision"""
contributions = {}
total_confidence = sum(p.confidence for p in self.last_predictions.values()) if self.last_predictions else 1.0
if total_confidence > 0:
for model_name, prediction in self.last_predictions.items():
contributions[model_name] = prediction.confidence / total_confidence
return contributions
def get_status(self) -> Dict[str, Any]:
"""Get NN fusion system status"""
return {
'device': str(self.device),
'training_mode': self.training_mode,
'registered_models': len(self.registered_models),
'recent_predictions': len(self.last_predictions),
'model_parameters': sum(p.numel() for p in self.network.parameters())
}