414 lines
14 KiB
Python
414 lines
14 KiB
Python
"""
|
|
Signal Processor
|
|
===============
|
|
|
|
Processes signals from multiple sources and generates actionable trading recommendations
|
|
with specific entry/exit prices using TradingAgents AI.
|
|
"""
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Dict, List, Optional, Any, Tuple
|
|
from datetime import datetime, timedelta
|
|
from dataclasses import dataclass
|
|
import statistics
|
|
|
|
# Import TradingAgents
|
|
import sys
|
|
sys.path.append('..')
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
|
|
# Import our modules
|
|
from .ibkr_connector import IBKRConnector, Position
|
|
from .data_aggregator import DataAggregator, MarketSignal
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@dataclass
|
|
class TradingRecommendation:
|
|
"""Complete trading recommendation with entry/exit points"""
|
|
ticker: str
|
|
action: str # 'BUY', 'SELL', 'HOLD'
|
|
current_price: float
|
|
entry_price_min: float
|
|
entry_price_max: float
|
|
target_price_1: float
|
|
target_price_2: float
|
|
stop_loss: float
|
|
confidence: float # 0-100
|
|
position_size: float # Percentage of portfolio
|
|
reasoning: str
|
|
data_sources: List[str]
|
|
risk_level: str # 'LOW', 'MEDIUM', 'HIGH'
|
|
timestamp: datetime
|
|
|
|
|
|
class SignalProcessor:
|
|
"""
|
|
Processes signals and generates trading recommendations
|
|
"""
|
|
|
|
def __init__(self,
|
|
ibkr_connector: IBKRConnector,
|
|
data_aggregator: DataAggregator,
|
|
config: Optional[Dict] = None):
|
|
"""
|
|
Initialize signal processor
|
|
|
|
Args:
|
|
ibkr_connector: IBKR connection instance
|
|
data_aggregator: Data aggregator instance
|
|
config: Configuration dictionary
|
|
"""
|
|
self.ibkr = ibkr_connector
|
|
self.data_agg = data_aggregator
|
|
self.config = config or DEFAULT_CONFIG.copy()
|
|
|
|
# Configure TradingAgents for fast processing
|
|
self.config['deep_think_llm'] = 'gpt-4o-mini'
|
|
self.config['quick_think_llm'] = 'gpt-4o-mini'
|
|
self.config['max_debate_rounds'] = 1
|
|
|
|
self.trading_agents = TradingAgentsGraph(debug=False, config=self.config)
|
|
self.recommendations: List[TradingRecommendation] = []
|
|
|
|
async def calculate_technical_levels(self, ticker: str) -> Dict[str, float]:
|
|
"""
|
|
Calculate technical support/resistance levels
|
|
|
|
Args:
|
|
ticker: Stock ticker
|
|
|
|
Returns:
|
|
Dictionary with technical levels
|
|
"""
|
|
try:
|
|
import yfinance as yf
|
|
stock = yf.Ticker(ticker)
|
|
|
|
# Get recent price data
|
|
hist = stock.history(period="3mo")
|
|
|
|
if hist.empty:
|
|
return {}
|
|
|
|
current_price = hist['Close'].iloc[-1]
|
|
high_3m = hist['High'].max()
|
|
low_3m = hist['Low'].min()
|
|
|
|
# Calculate moving averages
|
|
ma_20 = hist['Close'].tail(20).mean()
|
|
ma_50 = hist['Close'].tail(50).mean() if len(hist) >= 50 else ma_20
|
|
|
|
# Calculate support/resistance levels
|
|
# Using pivot points
|
|
last_high = hist['High'].iloc[-1]
|
|
last_low = hist['Low'].iloc[-1]
|
|
last_close = hist['Close'].iloc[-1]
|
|
pivot = (last_high + last_low + last_close) / 3
|
|
|
|
resistance_1 = 2 * pivot - last_low
|
|
resistance_2 = pivot + (last_high - last_low)
|
|
support_1 = 2 * pivot - last_high
|
|
support_2 = pivot - (last_high - last_low)
|
|
|
|
# Calculate RSI
|
|
delta = hist['Close'].diff()
|
|
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
|
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
|
rs = gain / loss
|
|
rsi = 100 - (100 / (1 + rs))
|
|
current_rsi = rsi.iloc[-1]
|
|
|
|
return {
|
|
'current_price': current_price,
|
|
'resistance_1': resistance_1,
|
|
'resistance_2': resistance_2,
|
|
'support_1': support_1,
|
|
'support_2': support_2,
|
|
'ma_20': ma_20,
|
|
'ma_50': ma_50,
|
|
'high_3m': high_3m,
|
|
'low_3m': low_3m,
|
|
'rsi': current_rsi,
|
|
'pivot': pivot
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error calculating technical levels for {ticker}: {e}")
|
|
return {}
|
|
|
|
async def analyze_position_sizing(self,
|
|
ticker: str,
|
|
confidence: float,
|
|
risk_level: str) -> float:
|
|
"""
|
|
Calculate appropriate position size based on portfolio and risk
|
|
|
|
Args:
|
|
ticker: Stock ticker
|
|
confidence: Signal confidence (0-100)
|
|
risk_level: Risk level of the trade
|
|
|
|
Returns:
|
|
Position size as percentage of portfolio
|
|
"""
|
|
# Get current portfolio info
|
|
await self.ibkr.sync_portfolio()
|
|
portfolio_summary = self.ibkr.get_portfolio_summary()
|
|
|
|
# Check if we already have a position
|
|
existing_position = await self.ibkr.get_position(ticker)
|
|
|
|
# Base position sizing rules
|
|
max_position = 0.20 # Max 20% in any single position
|
|
base_size = 0.10 # Base 10% position
|
|
|
|
# Adjust based on confidence
|
|
if confidence > 90:
|
|
size_multiplier = 1.5
|
|
elif confidence > 80:
|
|
size_multiplier = 1.2
|
|
elif confidence > 70:
|
|
size_multiplier = 1.0
|
|
else:
|
|
size_multiplier = 0.7
|
|
|
|
# Adjust based on risk
|
|
risk_multipliers = {
|
|
'LOW': 1.2,
|
|
'MEDIUM': 1.0,
|
|
'HIGH': 0.6
|
|
}
|
|
risk_multiplier = risk_multipliers.get(risk_level, 1.0)
|
|
|
|
# Calculate position size
|
|
position_size = base_size * size_multiplier * risk_multiplier
|
|
|
|
# If we already have a position, consider scaling
|
|
if existing_position:
|
|
current_weight = existing_position.market_value / portfolio_summary['total_value']
|
|
remaining_capacity = max_position - current_weight
|
|
position_size = min(position_size, remaining_capacity)
|
|
|
|
# Ensure within limits
|
|
position_size = max(0.05, min(position_size, max_position)) # Between 5% and 20%
|
|
|
|
return round(position_size, 3)
|
|
|
|
async def process_signals(self, ticker: str) -> Optional[TradingRecommendation]:
|
|
"""
|
|
Process all signals for a ticker and generate recommendation
|
|
|
|
Args:
|
|
ticker: Stock ticker
|
|
|
|
Returns:
|
|
Trading recommendation if signals are strong enough
|
|
"""
|
|
try:
|
|
# Get all signals for this ticker
|
|
signals = [s for s in self.data_agg.market_signals if s.ticker == ticker]
|
|
|
|
if not signals:
|
|
logger.info(f"No signals for {ticker}")
|
|
return None
|
|
|
|
# Calculate technical levels
|
|
tech_levels = await self.calculate_technical_levels(ticker)
|
|
|
|
if not tech_levels:
|
|
logger.warning(f"Could not calculate technical levels for {ticker}")
|
|
return None
|
|
|
|
current_price = tech_levels['current_price']
|
|
|
|
# Run TradingAgents analysis
|
|
logger.info(f"Running TradingAgents analysis for {ticker}")
|
|
_, ai_decision = self.trading_agents.propagate(ticker, datetime.now().strftime('%Y-%m-%d'))
|
|
|
|
# Combine AI decision with our signals
|
|
avg_confidence = statistics.mean([s.confidence for s in signals])
|
|
|
|
# Determine action based on signals and AI
|
|
buy_signals = [s for s in signals if s.action == 'BUY']
|
|
sell_signals = [s for s in signals if s.action == 'SELL']
|
|
|
|
if len(buy_signals) > len(sell_signals) and 'buy' in ai_decision.lower():
|
|
action = 'BUY'
|
|
# Entry points near support or current price
|
|
entry_min = min(current_price * 0.995, tech_levels['support_1'])
|
|
entry_max = current_price * 1.005
|
|
|
|
# Targets based on resistance levels
|
|
target_1 = tech_levels['resistance_1']
|
|
target_2 = tech_levels['resistance_2']
|
|
|
|
# Stop loss below support
|
|
stop_loss = tech_levels['support_1'] * 0.98
|
|
|
|
elif len(sell_signals) > len(buy_signals) or 'sell' in ai_decision.lower():
|
|
action = 'SELL'
|
|
# For selling, entry is current price range
|
|
entry_min = current_price * 0.995
|
|
entry_max = current_price * 1.01
|
|
|
|
# Targets for selling (lower prices)
|
|
target_1 = tech_levels['support_1']
|
|
target_2 = tech_levels['support_2']
|
|
|
|
# Stop loss above resistance for shorts
|
|
stop_loss = tech_levels['resistance_1'] * 1.02
|
|
|
|
else:
|
|
action = 'HOLD'
|
|
entry_min = entry_max = current_price
|
|
target_1 = target_2 = current_price
|
|
stop_loss = current_price * 0.95
|
|
|
|
# Determine risk level
|
|
rsi = tech_levels.get('rsi', 50)
|
|
if rsi > 70 or rsi < 30:
|
|
risk_level = 'HIGH'
|
|
elif 40 <= rsi <= 60:
|
|
risk_level = 'LOW'
|
|
else:
|
|
risk_level = 'MEDIUM'
|
|
|
|
# Calculate position size
|
|
position_size = await self.analyze_position_sizing(ticker, avg_confidence, risk_level)
|
|
|
|
# Build reasoning
|
|
reasoning_parts = [
|
|
f"AI Analysis: {ai_decision[:200]}",
|
|
f"Signals: {len(buy_signals)} buy, {len(sell_signals)} sell"
|
|
]
|
|
|
|
for signal in signals[:2]: # Include top 2 signals
|
|
if signal.signal_type == 'congressional':
|
|
reasoning_parts.append(f"Congressional activity detected")
|
|
elif signal.signal_type == 'insider':
|
|
reasoning_parts.append(f"Insider buying activity")
|
|
elif signal.signal_type == 'sentiment':
|
|
reasoning_parts.append(f"Positive market sentiment")
|
|
|
|
reasoning_parts.append(f"RSI: {rsi:.1f}")
|
|
|
|
# Create recommendation
|
|
recommendation = TradingRecommendation(
|
|
ticker=ticker,
|
|
action=action,
|
|
current_price=current_price,
|
|
entry_price_min=round(entry_min, 2),
|
|
entry_price_max=round(entry_max, 2),
|
|
target_price_1=round(target_1, 2),
|
|
target_price_2=round(target_2, 2),
|
|
stop_loss=round(stop_loss, 2),
|
|
confidence=avg_confidence,
|
|
position_size=position_size,
|
|
reasoning=' | '.join(reasoning_parts),
|
|
data_sources=[s.signal_type for s in signals],
|
|
risk_level=risk_level,
|
|
timestamp=datetime.now()
|
|
)
|
|
|
|
self.recommendations.append(recommendation)
|
|
return recommendation
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error processing signals for {ticker}: {e}")
|
|
return None
|
|
|
|
async def process_portfolio(self) -> List[TradingRecommendation]:
|
|
"""
|
|
Process entire portfolio and generate recommendations
|
|
|
|
Returns:
|
|
List of trading recommendations
|
|
"""
|
|
# Get current positions
|
|
await self.ibkr.sync_portfolio()
|
|
positions = self.ibkr.positions
|
|
|
|
# Get tickers from portfolio
|
|
portfolio_tickers = list(positions.keys())
|
|
|
|
# Also analyze some high-opportunity tickers even if not in portfolio
|
|
watchlist = ["NVDA", "TSLA", "AAPL", "MSFT", "AVGO"]
|
|
all_tickers = list(set(portfolio_tickers + watchlist))
|
|
|
|
# Get signals for all tickers
|
|
await self.data_agg.aggregate_signals(all_tickers)
|
|
|
|
# Process each ticker
|
|
recommendations = []
|
|
for ticker in all_tickers:
|
|
logger.info(f"Processing {ticker}")
|
|
rec = await self.process_signals(ticker)
|
|
if rec and rec.action != 'HOLD':
|
|
recommendations.append(rec)
|
|
|
|
# Sort by confidence
|
|
recommendations.sort(key=lambda x: x.confidence, reverse=True)
|
|
|
|
logger.info(f"Generated {len(recommendations)} recommendations")
|
|
return recommendations
|
|
|
|
def format_recommendation(self, rec: TradingRecommendation) -> str:
|
|
"""
|
|
Format recommendation for display
|
|
|
|
Args:
|
|
rec: Trading recommendation
|
|
|
|
Returns:
|
|
Formatted string
|
|
"""
|
|
# Calculate percentage gains
|
|
gain_1 = ((rec.target_price_1 - rec.current_price) / rec.current_price) * 100
|
|
gain_2 = ((rec.target_price_2 - rec.current_price) / rec.current_price) * 100
|
|
loss = ((rec.stop_loss - rec.current_price) / rec.current_price) * 100
|
|
|
|
formatted = f"""
|
|
━━━━━━━━━━━━━━━━━━━━━━━
|
|
🎯 ACTION: {rec.action}
|
|
📈 TICKER: {rec.ticker}
|
|
💰 CURRENT: ${rec.current_price:.2f}
|
|
📍 ENTRY: ${rec.entry_price_min:.2f} - ${rec.entry_price_max:.2f}
|
|
🎯 TARGET 1: ${rec.target_price_1:.2f} ({gain_1:+.1f}%)
|
|
🎯 TARGET 2: ${rec.target_price_2:.2f} ({gain_2:+.1f}%)
|
|
🛑 STOP LOSS: ${rec.stop_loss:.2f} ({loss:.1f}%)
|
|
📊 CONFIDENCE: {rec.confidence:.0f}%
|
|
💼 POSITION SIZE: {rec.position_size:.1%} of portfolio
|
|
⚠️ RISK: {rec.risk_level}
|
|
|
|
📝 REASONING:
|
|
{rec.reasoning}
|
|
|
|
📅 Generated: {rec.timestamp.strftime('%Y-%m-%d %H:%M')}
|
|
━━━━━━━━━━━━━━━━━━━━━━━
|
|
"""
|
|
return formatted
|
|
|
|
|
|
# Example usage
|
|
async def main():
|
|
"""Example of using the signal processor"""
|
|
# Initialize components
|
|
ibkr = IBKRConnector()
|
|
data_agg = DataAggregator()
|
|
processor = SignalProcessor(ibkr, data_agg)
|
|
|
|
# Connect to IBKR (would need TWS running)
|
|
# await ibkr.connect()
|
|
|
|
# Process a single ticker
|
|
rec = await processor.process_signals("NVDA")
|
|
if rec:
|
|
print(processor.format_recommendation(rec))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main()) |