277 lines
11 KiB
Python
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())
|
|
} |