SpencerCPurdy commited on
Commit
f21f227
·
verified ·
1 Parent(s): c02e959

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1507 -0
app.py ADDED
@@ -0,0 +1,1507 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LLM-Powered Multi-Agent Trading System
3
+ Author: Spencer Purdy
4
+ Description: A sophisticated multi-agent trading system leveraging real LLM reasoning for market analysis.
5
+ Features specialized agents for fundamental, technical, sentiment analysis, and risk management,
6
+ coordinated by a DQN reinforcement learning agent.
7
+ """
8
+
9
+ # Install required packages
10
+ # !pip install -q transformers torch numpy pandas scikit-learn plotly gradio yfinance ta scipy gymnasium accelerate openai newsapi-python
11
+
12
+ # Core imports
13
+ import numpy as np
14
+ import pandas as pd
15
+ import torch
16
+ import torch.nn as nn
17
+ import torch.optim as optim
18
+ from datetime import datetime, timedelta
19
+ import gradio as gr
20
+ import plotly.graph_objects as go
21
+ import plotly.express as px
22
+ from plotly.subplots import make_subplots
23
+ import json
24
+ import random
25
+ from typing import Dict, List, Tuple, Optional, Any
26
+ from dataclasses import dataclass
27
+ from collections import deque
28
+ import warnings
29
+ import os
30
+ import openai
31
+ from newsapi import NewsApiClient
32
+ warnings.filterwarnings('ignore')
33
+
34
+ # Technical analysis
35
+ import ta
36
+
37
+ # Transformers for sentiment analysis
38
+ from transformers import (
39
+ AutoTokenizer,
40
+ AutoModelForSequenceClassification,
41
+ pipeline
42
+ )
43
+
44
+ # Set random seeds for reproducibility
45
+ np.random.seed(42)
46
+ torch.manual_seed(42)
47
+ random.seed(42)
48
+
49
+ # Configuration constants
50
+ TRADING_DAYS_PER_YEAR = 252
51
+ RISK_FREE_RATE = 0.02
52
+ MAX_POSITION_SIZE = 0.25 # Maximum 25% of portfolio in single position
53
+ MIN_CASH_RESERVE = 0.1 # Minimum 10% cash reserve
54
+
55
+ @dataclass
56
+ class MarketSignal:
57
+ """Data class for agent signals"""
58
+ agent_name: str
59
+ signal_type: str # 'buy', 'sell', 'hold'
60
+ confidence: float # 0-1
61
+ reasoning: str
62
+ metadata: Dict[str, Any]
63
+
64
+ @dataclass
65
+ class TradingDecision:
66
+ """Data class for trading decisions"""
67
+ action: str # 'buy', 'sell', 'hold'
68
+ size: float # Position size as fraction of portfolio
69
+ stop_loss: float
70
+ take_profit: float
71
+ confidence: float
72
+ reasoning: Dict[str, str]
73
+
74
+ class BaseAgent:
75
+ """Base class for all trading agents"""
76
+
77
+ def __init__(self, name: str):
78
+ self.name = name
79
+ self.history = []
80
+
81
+ def analyze(self, market_data: pd.DataFrame, portfolio_state: Dict) -> MarketSignal:
82
+ """Analyze market data and return signal"""
83
+ raise NotImplementedError
84
+
85
+ def update_history(self, signal: MarketSignal, outcome: float):
86
+ """Update agent history with signal and outcome"""
87
+ self.history.append({
88
+ 'timestamp': datetime.now(),
89
+ 'signal': signal,
90
+ 'outcome': outcome
91
+ })
92
+
93
+ class FundamentalAnalystAgent(BaseAgent):
94
+ """Agent specializing in fundamental analysis using real OpenAI LLM reasoning"""
95
+
96
+ def __init__(self, api_key: str):
97
+ super().__init__("Fundamental Analyst")
98
+
99
+ # Initialize OpenAI client
100
+ self.api_key = api_key
101
+ openai.api_key = self.api_key
102
+ self.model = "gpt-3.5-turbo" # Using GPT-3.5 for cost efficiency
103
+
104
+ # System prompt for consistent analysis
105
+ self.system_prompt = """You are a professional fundamental analyst with deep expertise in financial markets.
106
+ Analyze the provided market data and give a clear trading recommendation (BUY, SELL, or HOLD) with detailed reasoning.
107
+ Consider price movements, volume patterns, volatility, and market positioning.
108
+ Be specific about the factors driving your recommendation and provide a confidence level (0-1).
109
+ Format your response as:
110
+ RECOMMENDATION: [BUY/SELL/HOLD]
111
+ CONFIDENCE: [0.0-1.0]
112
+ REASONING: [Your detailed analysis]"""
113
+
114
+ def _prepare_market_analysis(self, market_data: pd.DataFrame, portfolio_state: Dict) -> str:
115
+ """Prepare comprehensive market analysis for LLM"""
116
+
117
+ # Calculate key metrics
118
+ current_price = market_data['close'].iloc[-1]
119
+ price_change_1d = market_data['close'].pct_change().iloc[-1] * 100
120
+ price_change_5d = (market_data['close'].iloc[-1] / market_data['close'].iloc[-5] - 1) * 100
121
+ price_change_20d = (market_data['close'].iloc[-1] / market_data['close'].iloc[-20] - 1) * 100
122
+
123
+ # Volume analysis
124
+ avg_volume_20d = market_data['volume'].iloc[-20:].mean()
125
+ current_volume = market_data['volume'].iloc[-1]
126
+ volume_ratio = current_volume / avg_volume_20d
127
+
128
+ # Price metrics
129
+ high_20d = market_data['high'].iloc[-20:].max()
130
+ low_20d = market_data['low'].iloc[-20:].min()
131
+ price_position = (current_price - low_20d) / (high_20d - low_20d) if high_20d > low_20d else 0.5
132
+
133
+ # Moving averages
134
+ ma_5 = market_data['close'].iloc[-5:].mean()
135
+ ma_20 = market_data['close'].iloc[-20:].mean()
136
+ ma_50 = market_data['close'].iloc[-50:].mean() if len(market_data) >= 50 else ma_20
137
+
138
+ # Volatility
139
+ returns = market_data['close'].pct_change()
140
+ volatility = returns.iloc[-20:].std() * np.sqrt(252) * 100
141
+
142
+ # Support and resistance levels
143
+ support = market_data['low'].iloc[-20:].min()
144
+ resistance = market_data['high'].iloc[-20:].max()
145
+
146
+ # Portfolio context
147
+ cash_ratio = portfolio_state.get('cash', 100000) / portfolio_state.get('total_value', 100000)
148
+
149
+ analysis = f"""Market Analysis Report:
150
+
151
+ PRICE ACTION:
152
+ - Current Price: ${current_price:.2f}
153
+ - 1-Day Change: {price_change_1d:+.2f}%
154
+ - 5-Day Change: {price_change_5d:+.2f}%
155
+ - 20-Day Change: {price_change_20d:+.2f}%
156
+ - Position in 20-Day Range: {price_position:.1%} (0% = at low, 100% = at high)
157
+
158
+ TECHNICAL LEVELS:
159
+ - 5-Day MA: ${ma_5:.2f} ({'+' if current_price > ma_5 else '-'}{abs(current_price/ma_5 - 1)*100:.1f}%)
160
+ - 20-Day MA: ${ma_20:.2f} ({'+' if current_price > ma_20 else '-'}{abs(current_price/ma_20 - 1)*100:.1f}%)
161
+ - 50-Day MA: ${ma_50:.2f} ({'+' if current_price > ma_50 else '-'}{abs(current_price/ma_50 - 1)*100:.1f}%)
162
+ - Support Level: ${support:.2f}
163
+ - Resistance Level: ${resistance:.2f}
164
+
165
+ VOLUME ANALYSIS:
166
+ - Current Volume: {current_volume:,.0f}
167
+ - Volume vs 20-Day Average: {volume_ratio:.2f}x
168
+ - Volume Trend: {'High' if volume_ratio > 1.5 else 'Above Average' if volume_ratio > 1.2 else 'Average' if volume_ratio > 0.8 else 'Below Average'}
169
+
170
+ RISK METRICS:
171
+ - Annualized Volatility: {volatility:.1f}%
172
+ - Risk Level: {'High' if volatility > 30 else 'Moderate' if volatility > 20 else 'Low'}
173
+
174
+ PORTFOLIO CONTEXT:
175
+ - Cash Available: {cash_ratio:.1%} of portfolio
176
+ - Risk Capacity: {'High' if cash_ratio > 0.3 else 'Moderate' if cash_ratio > 0.15 else 'Low'}
177
+
178
+ Based on this comprehensive analysis, provide your trading recommendation."""
179
+
180
+ return analysis
181
+
182
+ def analyze(self, market_data: pd.DataFrame, portfolio_state: Dict) -> MarketSignal:
183
+ """Perform fundamental analysis using OpenAI LLM"""
184
+
185
+ # Prepare market analysis
186
+ market_analysis = self._prepare_market_analysis(market_data, portfolio_state)
187
+
188
+ try:
189
+ # Call OpenAI API for analysis
190
+ response = openai.ChatCompletion.create(
191
+ model=self.model,
192
+ messages=[
193
+ {"role": "system", "content": self.system_prompt},
194
+ {"role": "user", "content": market_analysis}
195
+ ],
196
+ temperature=0.3, # Lower temperature for more consistent analysis
197
+ max_tokens=500
198
+ )
199
+
200
+ # Extract LLM response
201
+ llm_analysis = response.choices[0].message.content
202
+
203
+ # Parse the response
204
+ lines = llm_analysis.split('\n')
205
+ recommendation = 'hold'
206
+ confidence = 0.5
207
+ reasoning = ""
208
+
209
+ for line in lines:
210
+ if line.startswith('RECOMMENDATION:'):
211
+ rec_text = line.split(':', 1)[1].strip().upper()
212
+ if 'BUY' in rec_text:
213
+ recommendation = 'buy'
214
+ elif 'SELL' in rec_text:
215
+ recommendation = 'sell'
216
+ else:
217
+ recommendation = 'hold'
218
+ elif line.startswith('CONFIDENCE:'):
219
+ try:
220
+ confidence = float(line.split(':', 1)[1].strip())
221
+ confidence = max(0.0, min(1.0, confidence)) # Ensure within bounds
222
+ except:
223
+ confidence = 0.5
224
+ elif line.startswith('REASONING:'):
225
+ reasoning = line.split(':', 1)[1].strip()
226
+ elif reasoning and line.strip(): # Continue reasoning on subsequent lines
227
+ reasoning += " " + line.strip()
228
+
229
+ # Ensure we have valid reasoning
230
+ if not reasoning:
231
+ reasoning = "LLM analysis suggests " + recommendation + " based on current market conditions."
232
+
233
+ except Exception as e:
234
+ print(f"OpenAI API error: {e}")
235
+ # Fallback to rule-based analysis if API fails
236
+ returns = market_data['close'].pct_change()
237
+ recent_return = returns.iloc[-20:].mean()
238
+
239
+ if recent_return > 0.001:
240
+ recommendation = 'buy'
241
+ confidence = 0.6
242
+ reasoning = "Positive momentum detected in recent price action based on technical indicators"
243
+ elif recent_return < -0.001:
244
+ recommendation = 'sell'
245
+ confidence = 0.6
246
+ reasoning = "Negative momentum suggests caution based on recent price movements"
247
+ else:
248
+ recommendation = 'hold'
249
+ confidence = 0.5
250
+ reasoning = "Market showing neutral patterns, maintaining current position"
251
+
252
+ # Extract numerical data for metadata
253
+ current_price = market_data['close'].iloc[-1]
254
+ price_change = market_data['close'].pct_change().iloc[-1]
255
+
256
+ return MarketSignal(
257
+ agent_name=self.name,
258
+ signal_type=recommendation,
259
+ confidence=confidence,
260
+ reasoning=reasoning,
261
+ metadata={
262
+ 'current_price': round(current_price, 2),
263
+ 'price_change': round(price_change, 4),
264
+ 'llm_model': self.model,
265
+ 'analysis_timestamp': datetime.now().isoformat()
266
+ }
267
+ )
268
+
269
+ class TechnicalAnalystAgent(BaseAgent):
270
+ """Agent specializing in technical analysis using numerical transformers"""
271
+
272
+ def __init__(self):
273
+ super().__init__("Technical Analyst")
274
+ self.lookback_period = 20
275
+ self.transformer_model = self._build_price_transformer()
276
+
277
+ def _build_price_transformer(self):
278
+ """Build a transformer model for price prediction"""
279
+ class PriceTransformer(nn.Module):
280
+ def __init__(self, input_dim=7, hidden_dim=64, num_heads=4, num_layers=2):
281
+ super().__init__()
282
+ self.input_projection = nn.Linear(input_dim, hidden_dim)
283
+ self.positional_encoding = nn.Parameter(torch.randn(1, 100, hidden_dim))
284
+ encoder_layer = nn.TransformerEncoderLayer(
285
+ d_model=hidden_dim,
286
+ nhead=num_heads,
287
+ dim_feedforward=hidden_dim * 4,
288
+ dropout=0.1,
289
+ batch_first=True
290
+ )
291
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
292
+ self.output_projection = nn.Linear(hidden_dim, 3) # Buy, Hold, Sell
293
+
294
+ def forward(self, x):
295
+ x = self.input_projection(x)
296
+ seq_len = x.size(1)
297
+ x = x + self.positional_encoding[:, :seq_len, :]
298
+ x = self.transformer(x)
299
+ x = self.output_projection(x[:, -1, :]) # Use last timestep
300
+ return torch.softmax(x, dim=-1)
301
+
302
+ return PriceTransformer()
303
+
304
+ def analyze(self, market_data: pd.DataFrame, portfolio_state: Dict) -> MarketSignal:
305
+ """Perform technical analysis using indicators and transformer model"""
306
+
307
+ # Calculate technical indicators
308
+ df = market_data.copy()
309
+
310
+ # Add technical indicators using ta library
311
+ df['rsi'] = ta.momentum.RSIIndicator(df['close']).rsi()
312
+ df['macd'] = ta.trend.MACD(df['close']).macd()
313
+ df['bb_high'] = ta.volatility.BollingerBands(df['close']).bollinger_hband()
314
+ df['bb_low'] = ta.volatility.BollingerBands(df['close']).bollinger_lband()
315
+ df['volume_sma'] = df['volume'].rolling(window=20).mean()
316
+
317
+ # Prepare features for transformer
318
+ features = ['open', 'high', 'low', 'close', 'volume', 'rsi', 'macd']
319
+
320
+ # Get last lookback_period rows
321
+ recent_data = df[features].iloc[-self.lookback_period:].fillna(method='ffill').fillna(0)
322
+
323
+ # Normalize features
324
+ normalized_data = (recent_data - recent_data.mean()) / (recent_data.std() + 1e-8)
325
+
326
+ # Convert to tensor
327
+ input_tensor = torch.FloatTensor(normalized_data.values).unsqueeze(0)
328
+
329
+ # Get transformer prediction
330
+ with torch.no_grad():
331
+ predictions = self.transformer_model(input_tensor)
332
+ buy_prob, hold_prob, sell_prob = predictions[0].numpy()
333
+
334
+ # Determine signal based on transformer output
335
+ if buy_prob > 0.6:
336
+ signal_type = 'buy'
337
+ confidence = float(buy_prob)
338
+ reasoning = "Strong bullish technical setup detected by transformer model"
339
+ elif sell_prob > 0.6:
340
+ signal_type = 'sell'
341
+ confidence = float(sell_prob)
342
+ reasoning = "Bearish technical pattern identified by transformer model"
343
+ else:
344
+ signal_type = 'hold'
345
+ confidence = float(hold_prob)
346
+ reasoning = "No clear technical direction detected"
347
+
348
+ # Enhance reasoning with specific technical indicators
349
+ current_rsi = df['rsi'].iloc[-1]
350
+ if current_rsi < 30 and signal_type != 'buy':
351
+ reasoning += f" (Note: RSI at {current_rsi:.1f} indicates oversold conditions)"
352
+ elif current_rsi > 70 and signal_type != 'sell':
353
+ reasoning += f" (Note: RSI at {current_rsi:.1f} indicates overbought conditions)"
354
+
355
+ # Check Bollinger Bands
356
+ current_price = df['close'].iloc[-1]
357
+ bb_high = df['bb_high'].iloc[-1]
358
+ bb_low = df['bb_low'].iloc[-1]
359
+
360
+ if not np.isnan(bb_high) and not np.isnan(bb_low):
361
+ if current_price > bb_high:
362
+ reasoning += " Price breaking above upper Bollinger Band"
363
+ elif current_price < bb_low:
364
+ reasoning += " Price breaking below lower Bollinger Band"
365
+
366
+ # MACD analysis
367
+ macd_value = df['macd'].iloc[-1]
368
+ macd_signal = ta.trend.MACD(df['close']).macd_signal().iloc[-1]
369
+
370
+ if not np.isnan(macd_value) and not np.isnan(macd_signal):
371
+ if macd_value > macd_signal and macd_value > 0:
372
+ reasoning += " MACD showing bullish crossover above zero"
373
+ elif macd_value < macd_signal and macd_value < 0:
374
+ reasoning += " MACD showing bearish crossover below zero"
375
+
376
+ return MarketSignal(
377
+ agent_name=self.name,
378
+ signal_type=signal_type,
379
+ confidence=confidence,
380
+ reasoning=reasoning,
381
+ metadata={
382
+ 'rsi': round(current_rsi, 2) if not np.isnan(current_rsi) else 50,
383
+ 'buy_probability': round(buy_prob, 3),
384
+ 'sell_probability': round(sell_prob, 3),
385
+ 'hold_probability': round(hold_prob, 3),
386
+ 'current_price': round(current_price, 2),
387
+ 'bb_position': 'above' if current_price > bb_high else 'below' if current_price < bb_low else 'within'
388
+ }
389
+ )
390
+
391
+ class SentimentAnalystAgent(BaseAgent):
392
+ """Agent specializing in sentiment analysis using FinBERT and real news data"""
393
+
394
+ def __init__(self, news_api_key: str = None):
395
+ super().__init__("Sentiment Analyst")
396
+ # Initialize FinBERT for financial sentiment analysis
397
+ self.tokenizer = AutoTokenizer.from_pretrained("ProsusAI/finbert")
398
+ self.model = AutoModelForSequenceClassification.from_pretrained("ProsusAI/finbert")
399
+ self.sentiment_pipeline = pipeline(
400
+ "sentiment-analysis",
401
+ model=self.model,
402
+ tokenizer=self.tokenizer,
403
+ device=-1 # CPU
404
+ )
405
+
406
+ # Initialize news API client
407
+ self.news_api_key = news_api_key
408
+ if self.news_api_key:
409
+ self.newsapi = NewsApiClient(api_key=self.news_api_key)
410
+ else:
411
+ self.newsapi = None
412
+
413
+ def _fetch_real_news(self, symbol: str = None) -> List[str]:
414
+ """Fetch real news articles from NewsAPI"""
415
+
416
+ if not self.newsapi:
417
+ return self._generate_contextual_news()
418
+
419
+ try:
420
+ # Search for financial news
421
+ query = f"stock market {symbol if symbol else 'trading'} finance"
422
+
423
+ # Get top headlines
424
+ headlines = self.newsapi.get_top_headlines(
425
+ q=query,
426
+ category='business',
427
+ language='en',
428
+ page_size=10
429
+ )
430
+
431
+ # Extract article titles and descriptions
432
+ news_items = []
433
+
434
+ if headlines['status'] == 'ok' and headlines['articles']:
435
+ for article in headlines['articles'][:10]:
436
+ if article['title']:
437
+ news_items.append(article['title'])
438
+ if article['description']:
439
+ news_items.append(article['description'])
440
+
441
+ # If not enough news, get everything
442
+ if len(news_items) < 5:
443
+ all_articles = self.newsapi.get_everything(
444
+ q=query,
445
+ language='en',
446
+ sort_by='relevancy',
447
+ page_size=10
448
+ )
449
+
450
+ if all_articles['status'] == 'ok' and all_articles['articles']:
451
+ for article in all_articles['articles']:
452
+ if article['title'] and article['title'] not in news_items:
453
+ news_items.append(article['title'])
454
+ if len(news_items) >= 10:
455
+ break
456
+
457
+ return news_items[:10] if news_items else self._generate_contextual_news()
458
+
459
+ except Exception as e:
460
+ print(f"News API error: {e}")
461
+ return self._generate_contextual_news()
462
+
463
+ def _generate_contextual_news(self) -> List[str]:
464
+ """Generate contextual market news as fallback"""
465
+
466
+ base_headlines = [
467
+ "Federal Reserve signals potential rate changes amid economic uncertainty",
468
+ "Tech stocks rally as earnings season approaches",
469
+ "Global markets react to latest inflation data",
470
+ "Energy sector sees volatility amid geopolitical tensions",
471
+ "Analysts upgrade outlook for financial sector",
472
+ "Retail sales data exceeds expectations",
473
+ "Manufacturing index shows signs of recovery",
474
+ "Currency markets stabilize after central bank interventions",
475
+ "Commodity prices surge on supply chain concerns",
476
+ "Corporate earnings beat analyst estimates"
477
+ ]
478
+
479
+ # Add some variation
480
+ selected = random.sample(base_headlines, min(5, len(base_headlines)))
481
+ return selected
482
+
483
+ def analyze(self, market_data: pd.DataFrame, portfolio_state: Dict) -> MarketSignal:
484
+ """Perform sentiment analysis on real or contextual news"""
485
+
486
+ # Fetch news items
487
+ news_items = self._fetch_real_news()
488
+
489
+ # Analyze sentiment for each news item
490
+ sentiments = []
491
+ for news in news_items:
492
+ try:
493
+ # Truncate to 512 characters for FinBERT
494
+ result = self.sentiment_pipeline(news[:512])[0]
495
+ sentiments.append(result)
496
+ except Exception as e:
497
+ print(f"Sentiment analysis error: {e}")
498
+ sentiments.append({'label': 'neutral', 'score': 0.5})
499
+
500
+ # Aggregate sentiments
501
+ positive_count = sum(1 for s in sentiments if s['label'] == 'positive')
502
+ negative_count = sum(1 for s in sentiments if s['label'] == 'negative')
503
+ neutral_count = sum(1 for s in sentiments if s['label'] == 'neutral')
504
+
505
+ # Calculate weighted scores
506
+ positive_scores = [s['score'] for s in sentiments if s['label'] == 'positive']
507
+ negative_scores = [s['score'] for s in sentiments if s['label'] == 'negative']
508
+
509
+ positive_score = sum(positive_scores) / len(sentiments) if sentiments else 0
510
+ negative_score = sum(negative_scores) / len(sentiments) if sentiments else 0
511
+ neutral_score = 1 - positive_score - negative_score
512
+
513
+ # Calculate net sentiment
514
+ net_sentiment = positive_score - negative_score
515
+
516
+ # Determine signal based on sentiment analysis
517
+ if net_sentiment > 0.2 and positive_count > negative_count * 1.5:
518
+ signal_type = 'buy'
519
+ confidence = min(0.8, positive_score + 0.2)
520
+ reasoning = f"Positive sentiment detected across {positive_count}/{len(sentiments)} news items"
521
+ elif net_sentiment < -0.2 and negative_count > positive_count * 1.5:
522
+ signal_type = 'sell'
523
+ confidence = min(0.8, negative_score + 0.2)
524
+ reasoning = f"Negative sentiment dominates with {negative_count}/{len(sentiments)} bearish news items"
525
+ else:
526
+ signal_type = 'hold'
527
+ confidence = 0.5 + abs(net_sentiment) * 0.3
528
+ reasoning = "Mixed sentiment suggests maintaining current position"
529
+
530
+ # Add specific news context to reasoning
531
+ if news_items and len(news_items) > 0:
532
+ reasoning += f". Key headline: '{news_items[0][:100]}...'"
533
+
534
+ # Consider market context
535
+ recent_volatility = market_data['close'].pct_change().iloc[-20:].std()
536
+ if recent_volatility > 0.02:
537
+ confidence *= 0.9 # Reduce confidence in high volatility
538
+ reasoning += " (Confidence adjusted for high market volatility)"
539
+
540
+ return MarketSignal(
541
+ agent_name=self.name,
542
+ signal_type=signal_type,
543
+ confidence=confidence,
544
+ reasoning=reasoning,
545
+ metadata={
546
+ 'positive_sentiment': round(positive_score, 3),
547
+ 'negative_sentiment': round(negative_score, 3),
548
+ 'neutral_sentiment': round(neutral_score, 3),
549
+ 'net_sentiment': round(net_sentiment, 3),
550
+ 'news_analyzed': len(news_items),
551
+ 'sentiment_distribution': {
552
+ 'positive': positive_count,
553
+ 'negative': negative_count,
554
+ 'neutral': neutral_count
555
+ },
556
+ 'data_source': 'real_news' if self.newsapi else 'contextual'
557
+ }
558
+ )
559
+
560
+ class RiskManagerAgent(BaseAgent):
561
+ """Agent specializing in risk management and position sizing"""
562
+
563
+ def __init__(self):
564
+ super().__init__("Risk Manager")
565
+ self.max_drawdown_threshold = 0.15 # 15% max drawdown
566
+ self.var_confidence = 0.95 # 95% VaR
567
+
568
+ def calculate_var(self, returns: pd.Series, confidence: float = 0.95) -> float:
569
+ """Calculate Value at Risk"""
570
+ if len(returns) < 20:
571
+ return 0.02 # Default 2% VaR if insufficient data
572
+ return np.percentile(returns, (1 - confidence) * 100)
573
+
574
+ def calculate_sharpe_ratio(self, returns: pd.Series) -> float:
575
+ """Calculate Sharpe ratio"""
576
+ if len(returns) < 2:
577
+ return 0.0
578
+ excess_returns = returns - RISK_FREE_RATE / TRADING_DAYS_PER_YEAR
579
+ return np.sqrt(TRADING_DAYS_PER_YEAR) * excess_returns.mean() / (returns.std() + 1e-8)
580
+
581
+ def analyze(self, market_data: pd.DataFrame, portfolio_state: Dict) -> MarketSignal:
582
+ """Perform risk analysis and provide risk-adjusted recommendations"""
583
+
584
+ returns = market_data['close'].pct_change().dropna()
585
+ current_price = market_data['close'].iloc[-1]
586
+
587
+ # Calculate comprehensive risk metrics
588
+ volatility = returns.iloc[-20:].std() * np.sqrt(TRADING_DAYS_PER_YEAR)
589
+ var = self.calculate_var(returns.iloc[-100:])
590
+ sharpe = self.calculate_sharpe_ratio(returns.iloc[-60:])
591
+
592
+ # Calculate maximum drawdown
593
+ cum_returns = (1 + returns).cumprod()
594
+ running_max = cum_returns.expanding().max()
595
+ drawdown = (cum_returns - running_max) / running_max
596
+ max_drawdown = abs(drawdown.min())
597
+
598
+ # Check portfolio metrics
599
+ current_positions = portfolio_state.get('total_position_value', 0)
600
+ portfolio_value = portfolio_state.get('total_value', 100000)
601
+ cash_ratio = portfolio_state.get('cash', portfolio_value) / portfolio_value
602
+
603
+ # Calculate current portfolio drawdown
604
+ if 'peak_value' in portfolio_state:
605
+ current_drawdown = (portfolio_state['peak_value'] - portfolio_value) / portfolio_state['peak_value']
606
+ else:
607
+ current_drawdown = 0
608
+
609
+ # Comprehensive risk assessment
610
+ risk_factors = []
611
+ risk_score = 0
612
+
613
+ if volatility > 0.3: # High volatility
614
+ risk_factors.append("high_volatility")
615
+ risk_score += 2
616
+
617
+ if current_drawdown > self.max_drawdown_threshold * 0.8:
618
+ risk_factors.append("approaching_max_drawdown")
619
+ risk_score += 3
620
+
621
+ if cash_ratio < MIN_CASH_RESERVE:
622
+ risk_factors.append("low_cash_reserves")
623
+ risk_score += 2
624
+
625
+ if sharpe < 0.5:
626
+ risk_factors.append("poor_risk_adjusted_returns")
627
+ risk_score += 1
628
+
629
+ if var < -0.05: # 5% VaR threshold
630
+ risk_factors.append("high_value_at_risk")
631
+ risk_score += 2
632
+
633
+ # Kelly Criterion for position sizing
634
+ win_rate = (returns > 0).mean()
635
+ avg_win = returns[returns > 0].mean() if (returns > 0).any() else 0
636
+ avg_loss = abs(returns[returns < 0].mean()) if (returns < 0).any() else 1
637
+
638
+ kelly_fraction = 0
639
+ if avg_loss > 0 and win_rate > 0:
640
+ odds = avg_win / avg_loss
641
+ kelly_fraction = (win_rate * odds - (1 - win_rate)) / odds
642
+ kelly_fraction = max(0, min(kelly_fraction, 0.25)) # Cap at 25%
643
+
644
+ # Determine signal based on comprehensive risk assessment
645
+ if risk_score >= 5:
646
+ signal_type = 'sell'
647
+ confidence = min(0.7 + risk_score * 0.05, 0.9)
648
+ reasoning = f"Multiple risk factors detected ({len(risk_factors)}): {', '.join(risk_factors)}. Risk score: {risk_score}/10"
649
+ elif risk_score >= 3:
650
+ signal_type = 'hold'
651
+ confidence = 0.6
652
+ reasoning = f"Elevated risk levels detected. Factors: {', '.join(risk_factors)}. Recommend caution"
653
+ else:
654
+ # Low risk environment - check for opportunities
655
+ if sharpe > 1.0 and volatility < 0.2 and cash_ratio > 0.2:
656
+ signal_type = 'buy'
657
+ confidence = 0.7
658
+ reasoning = f"Risk metrics favorable. Sharpe: {sharpe:.2f}, Vol: {volatility:.1%}, Cash available"
659
+ else:
660
+ signal_type = 'hold'
661
+ confidence = 0.5
662
+ reasoning = "Risk levels acceptable, maintaining current exposure"
663
+
664
+ # Add Kelly Criterion to reasoning
665
+ if kelly_fraction > 0:
666
+ reasoning += f". Optimal position size (Kelly): {kelly_fraction:.1%}"
667
+
668
+ return MarketSignal(
669
+ agent_name=self.name,
670
+ signal_type=signal_type,
671
+ confidence=min(confidence, 0.9),
672
+ reasoning=reasoning,
673
+ metadata={
674
+ 'volatility': round(volatility, 3),
675
+ 'var_95': round(var, 3),
676
+ 'sharpe_ratio': round(sharpe, 2),
677
+ 'max_drawdown': round(max_drawdown, 3),
678
+ 'current_drawdown': round(current_drawdown, 3),
679
+ 'risk_factors': risk_factors,
680
+ 'risk_score': risk_score,
681
+ 'cash_ratio': round(cash_ratio, 2),
682
+ 'kelly_fraction': round(kelly_fraction, 3),
683
+ 'win_rate': round(win_rate, 2)
684
+ }
685
+ )
686
+
687
+ # DQN Implementation for Multi-Agent Coordination
688
+ class DQN(nn.Module):
689
+ """Deep Q-Network for learning from agent recommendations"""
690
+
691
+ def __init__(self, state_size: int, action_size: int):
692
+ super(DQN, self).__init__()
693
+ self.fc1 = nn.Linear(state_size, 128)
694
+ self.fc2 = nn.Linear(128, 64)
695
+ self.fc3 = nn.Linear(64, 32)
696
+ self.fc4 = nn.Linear(32, action_size)
697
+
698
+ def forward(self, x):
699
+ x = torch.relu(self.fc1(x))
700
+ x = torch.relu(self.fc2(x))
701
+ x = torch.relu(self.fc3(x))
702
+ return self.fc4(x)
703
+
704
+ class ReinforcementLearningAgent:
705
+ """DQN agent that learns from multi-agent recommendations"""
706
+
707
+ def __init__(self, state_size: int = 16, action_size: int = 3):
708
+ self.state_size = state_size
709
+ self.action_size = action_size
710
+ self.memory = deque(maxlen=2000)
711
+ self.epsilon = 0.1 # Exploration rate
712
+ self.gamma = 0.95 # Discount factor
713
+ self.learning_rate = 0.001
714
+
715
+ # Neural networks
716
+ self.q_network = DQN(state_size, action_size)
717
+ self.target_network = DQN(state_size, action_size)
718
+ self.optimizer = optim.Adam(self.q_network.parameters(), lr=self.learning_rate)
719
+
720
+ # Update target network
721
+ self.update_target_network()
722
+
723
+ def update_target_network(self):
724
+ """Copy weights from main network to target network"""
725
+ self.target_network.load_state_dict(self.q_network.state_dict())
726
+
727
+ def remember(self, state, action, reward, next_state, done):
728
+ """Store experience in replay memory"""
729
+ self.memory.append((state, action, reward, next_state, done))
730
+
731
+ def act(self, state):
732
+ """Choose action based on epsilon-greedy policy"""
733
+ if random.random() <= self.epsilon:
734
+ return random.randrange(self.action_size)
735
+
736
+ state_tensor = torch.FloatTensor(state).unsqueeze(0)
737
+ q_values = self.q_network(state_tensor)
738
+ return np.argmax(q_values.detach().numpy())
739
+
740
+ def replay(self, batch_size: int = 32):
741
+ """Train the model on a batch of experiences"""
742
+ if len(self.memory) < batch_size:
743
+ return
744
+
745
+ batch = random.sample(self.memory, batch_size)
746
+ states = torch.FloatTensor([e[0] for e in batch])
747
+ actions = torch.LongTensor([e[1] for e in batch])
748
+ rewards = torch.FloatTensor([e[2] for e in batch])
749
+ next_states = torch.FloatTensor([e[3] for e in batch])
750
+ dones = torch.FloatTensor([e[4] for e in batch])
751
+
752
+ current_q_values = self.q_network(states).gather(1, actions.unsqueeze(1))
753
+ next_q_values = self.target_network(next_states).max(1)[0].detach()
754
+ target_q_values = rewards + (self.gamma * next_q_values * (1 - dones))
755
+
756
+ loss = nn.MSELoss()(current_q_values.squeeze(), target_q_values)
757
+
758
+ self.optimizer.zero_grad()
759
+ loss.backward()
760
+ self.optimizer.step()
761
+
762
+ class MultiAgentTradingSystem:
763
+ """Main trading system coordinating multiple agents"""
764
+
765
+ def __init__(self, initial_capital: float = 100000, openai_api_key: str = None, news_api_key: str = None):
766
+ self.initial_capital = initial_capital
767
+ self.openai_api_key = openai_api_key
768
+ self.news_api_key = news_api_key
769
+ self.reset()
770
+
771
+ # Initialize agents
772
+ print("Initializing trading agents...")
773
+
774
+ # Check for API keys
775
+ if not self.openai_api_key:
776
+ print("Warning: OpenAI API key not provided. Fundamental analysis will use fallback methods.")
777
+
778
+ if not self.news_api_key:
779
+ print("Warning: News API key not provided. Sentiment analysis will use contextual news.")
780
+
781
+ self.fundamental_agent = FundamentalAnalystAgent(self.openai_api_key)
782
+ self.technical_agent = TechnicalAnalystAgent()
783
+ self.sentiment_agent = SentimentAnalystAgent(self.news_api_key)
784
+ self.risk_agent = RiskManagerAgent()
785
+
786
+ print("All agents initialized successfully")
787
+
788
+ # Initialize RL coordinator
789
+ self.rl_agent = ReinforcementLearningAgent(state_size=16, action_size=3)
790
+
791
+ # Trading history
792
+ self.trade_history = []
793
+ self.performance_history = []
794
+
795
+ def reset(self):
796
+ """Reset portfolio to initial state"""
797
+ self.portfolio = {
798
+ 'cash': self.initial_capital,
799
+ 'positions': {},
800
+ 'total_value': self.initial_capital,
801
+ 'peak_value': self.initial_capital,
802
+ 'total_position_value': 0
803
+ }
804
+
805
+ def get_portfolio_value(self, current_prices: Dict[str, float]) -> float:
806
+ """Calculate total portfolio value"""
807
+ total = self.portfolio['cash']
808
+ for symbol, position in self.portfolio['positions'].items():
809
+ if symbol in current_prices:
810
+ total += position['shares'] * current_prices[symbol]
811
+ return total
812
+
813
+ def aggregate_signals(self, signals: List[MarketSignal]) -> Tuple[str, float, Dict]:
814
+ """Aggregate signals from multiple agents using weighted voting"""
815
+ buy_score = 0
816
+ sell_score = 0
817
+ hold_score = 0
818
+
819
+ reasoning = {}
820
+
821
+ # Weight signals by confidence
822
+ for signal in signals:
823
+ weight = signal.confidence
824
+ reasoning[signal.agent_name] = {
825
+ 'signal': signal.signal_type,
826
+ 'confidence': signal.confidence,
827
+ 'reasoning': signal.reasoning
828
+ }
829
+
830
+ if signal.signal_type == 'buy':
831
+ buy_score += weight
832
+ elif signal.signal_type == 'sell':
833
+ sell_score += weight
834
+ else:
835
+ hold_score += weight
836
+
837
+ # Normalize scores
838
+ total_score = buy_score + sell_score + hold_score
839
+ if total_score > 0:
840
+ buy_score /= total_score
841
+ sell_score /= total_score
842
+ hold_score /= total_score
843
+
844
+ # Determine action based on weighted voting
845
+ if buy_score > 0.5:
846
+ action = 'buy'
847
+ confidence = buy_score
848
+ elif sell_score > 0.5:
849
+ action = 'sell'
850
+ confidence = sell_score
851
+ else:
852
+ action = 'hold'
853
+ confidence = hold_score
854
+
855
+ return action, confidence, reasoning
856
+
857
+ def create_state_vector(self, market_data: pd.DataFrame, signals: List[MarketSignal]) -> np.ndarray:
858
+ """Create state vector for RL agent"""
859
+
860
+ # Market features
861
+ returns = market_data['close'].pct_change()
862
+ current_return = returns.iloc[-1]
863
+ volatility = returns.iloc[-20:].std()
864
+ momentum = returns.iloc[-20:].mean()
865
+
866
+ # Technical indicators
867
+ rsi = ta.momentum.RSIIndicator(market_data['close']).rsi().iloc[-1]
868
+
869
+ # Signal features
870
+ buy_signals = sum(1 for s in signals if s.signal_type == 'buy')
871
+ sell_signals = sum(1 for s in signals if s.signal_type == 'sell')
872
+ avg_confidence = np.mean([s.confidence for s in signals])
873
+
874
+ # Portfolio features
875
+ cash_ratio = self.portfolio['cash'] / self.portfolio['total_value']
876
+ position_ratio = self.portfolio['total_position_value'] / self.portfolio['total_value']
877
+
878
+ # Risk metrics from risk agent
879
+ risk_metadata = next((s.metadata for s in signals if s.agent_name == "Risk Manager"), {})
880
+ risk_score = risk_metadata.get('risk_score', 0) / 10.0 # Normalize
881
+
882
+ # Create state vector
883
+ state = np.array([
884
+ current_return,
885
+ volatility,
886
+ momentum,
887
+ rsi / 100.0 if not np.isnan(rsi) else 0.5,
888
+ buy_signals / len(signals),
889
+ sell_signals / len(signals),
890
+ avg_confidence,
891
+ cash_ratio,
892
+ position_ratio,
893
+ risk_score,
894
+ # Agent-specific confidences
895
+ signals[0].confidence, # Fundamental
896
+ signals[1].confidence, # Technical
897
+ signals[2].confidence, # Sentiment
898
+ signals[3].confidence, # Risk
899
+ 0, 0 # Padding for consistent size
900
+ ])
901
+
902
+ return state[:self.rl_agent.state_size]
903
+
904
+ def execute_trade(self, symbol: str, action: str, confidence: float,
905
+ current_price: float, reasoning: Dict) -> Dict:
906
+ """Execute trading decision"""
907
+
908
+ trade_result = {
909
+ 'timestamp': datetime.now(),
910
+ 'symbol': symbol,
911
+ 'action': action,
912
+ 'price': current_price,
913
+ 'confidence': confidence,
914
+ 'reasoning': reasoning,
915
+ 'executed': False,
916
+ 'shares': 0,
917
+ 'value': 0
918
+ }
919
+
920
+ if action == 'buy':
921
+ # Calculate position size based on confidence and risk limits
922
+ max_position_value = self.portfolio['total_value'] * MAX_POSITION_SIZE
923
+
924
+ # Use Kelly Criterion from risk agent if available
925
+ risk_metadata = reasoning.get('Risk Manager', {})
926
+ if isinstance(risk_metadata, dict) and 'metadata' in reasoning['Risk Manager']:
927
+ kelly_fraction = reasoning['Risk Manager']['metadata'].get('kelly_fraction', 0.1)
928
+ else:
929
+ kelly_fraction = 0.1
930
+
931
+ position_value = min(
932
+ self.portfolio['cash'] * confidence * kelly_fraction,
933
+ max_position_value
934
+ )
935
+
936
+ if position_value > current_price:
937
+ shares = int(position_value / current_price)
938
+ cost = shares * current_price
939
+
940
+ # Ensure minimum cash reserve
941
+ if cost <= self.portfolio['cash'] * (1 - MIN_CASH_RESERVE):
942
+ # Execute buy
943
+ self.portfolio['cash'] -= cost
944
+
945
+ if symbol in self.portfolio['positions']:
946
+ # Update existing position
947
+ old_shares = self.portfolio['positions'][symbol]['shares']
948
+ old_avg_price = self.portfolio['positions'][symbol]['avg_price']
949
+ new_shares = old_shares + shares
950
+ new_avg_price = (old_avg_price * old_shares + cost) / new_shares
951
+
952
+ self.portfolio['positions'][symbol]['shares'] = new_shares
953
+ self.portfolio['positions'][symbol]['avg_price'] = new_avg_price
954
+ else:
955
+ # Create new position
956
+ self.portfolio['positions'][symbol] = {
957
+ 'shares': shares,
958
+ 'avg_price': current_price
959
+ }
960
+
961
+ trade_result['executed'] = True
962
+ trade_result['shares'] = shares
963
+ trade_result['value'] = cost
964
+
965
+ elif action == 'sell' and symbol in self.portfolio['positions']:
966
+ # Sell a portion of position based on confidence
967
+ position = self.portfolio['positions'][symbol]
968
+ shares_to_sell = int(position['shares'] * confidence * 0.5)
969
+
970
+ if shares_to_sell > 0:
971
+ revenue = shares_to_sell * current_price
972
+ self.portfolio['cash'] += revenue
973
+ position['shares'] -= shares_to_sell
974
+
975
+ if position['shares'] == 0:
976
+ del self.portfolio['positions'][symbol]
977
+
978
+ trade_result['executed'] = True
979
+ trade_result['shares'] = -shares_to_sell
980
+ trade_result['value'] = revenue
981
+
982
+ # Update portfolio metrics
983
+ self.portfolio['total_position_value'] = sum(
984
+ pos['shares'] * pos['avg_price']
985
+ for pos in self.portfolio['positions'].values()
986
+ )
987
+
988
+ return trade_result
989
+
990
+ class MarketSimulator:
991
+ """Simulate realistic market data for demonstration"""
992
+
993
+ def __init__(self, base_price: float = 100, volatility: float = 0.02):
994
+ self.base_price = base_price
995
+ self.volatility = volatility
996
+ self.drift = 0.0001
997
+
998
+ def generate_market_data(self, days: int = 252) -> pd.DataFrame:
999
+ """Generate simulated market data with realistic patterns"""
1000
+
1001
+ dates = pd.date_range(end=datetime.now(), periods=days, freq='D')
1002
+
1003
+ # Generate price series using geometric Brownian motion
1004
+ returns = np.random.normal(self.drift, self.volatility, days)
1005
+
1006
+ # Add market regimes
1007
+ regime_length = days // 4
1008
+ for i in range(0, days, regime_length):
1009
+ regime_type = i // regime_length % 4
1010
+ if regime_type == 0: # Bull market
1011
+ returns[i:i+regime_length] += np.random.normal(0.0005, 0.0001, min(regime_length, days-i))
1012
+ elif regime_type == 1: # Bear market
1013
+ returns[i:i+regime_length] += np.random.normal(-0.0005, 0.0001, min(regime_length, days-i))
1014
+ elif regime_type == 2: # High volatility
1015
+ returns[i:i+regime_length] *= 1.5
1016
+ # regime_type == 3 is normal market
1017
+
1018
+ price_series = self.base_price * np.exp(np.cumsum(returns))
1019
+
1020
+ # Add some mean reversion
1021
+ ma_20 = pd.Series(price_series).rolling(20).mean()
1022
+ mean_reversion_strength = 0.1
1023
+ for i in range(20, len(price_series)):
1024
+ if not np.isnan(ma_20.iloc[i]):
1025
+ deviation = (price_series[i] - ma_20.iloc[i]) / ma_20.iloc[i]
1026
+ price_series[i] *= (1 - mean_reversion_strength * deviation)
1027
+
1028
+ # Generate OHLCV data
1029
+ data = {
1030
+ 'date': dates,
1031
+ 'open': price_series * np.random.uniform(0.99, 1.01, days),
1032
+ 'high': price_series * np.random.uniform(1.01, 1.03, days),
1033
+ 'low': price_series * np.random.uniform(0.97, 0.99, days),
1034
+ 'close': price_series,
1035
+ 'volume': np.random.lognormal(np.log(1000000), 0.5, days)
1036
+ }
1037
+
1038
+ df = pd.DataFrame(data)
1039
+ df.set_index('date', inplace=True)
1040
+
1041
+ # Ensure OHLC consistency
1042
+ df['high'] = df[['open', 'high', 'close']].max(axis=1)
1043
+ df['low'] = df[['open', 'low', 'close']].min(axis=1)
1044
+
1045
+ return df
1046
+
1047
+ def run_backtest(trading_system: MultiAgentTradingSystem,
1048
+ market_data: pd.DataFrame,
1049
+ symbol: str = "DEMO") -> Tuple[pd.DataFrame, List[Dict]]:
1050
+ """Run comprehensive backtest simulation"""
1051
+
1052
+ performance_history = []
1053
+ trade_history = []
1054
+
1055
+ # Run simulation day by day
1056
+ print("Starting backtest simulation...")
1057
+ for i in range(50, len(market_data)):
1058
+ # Progress indicator
1059
+ if i % 50 == 0:
1060
+ print(f"Processing day {i}/{len(market_data)}")
1061
+
1062
+ # Get historical data up to current day
1063
+ historical_data = market_data.iloc[:i+1]
1064
+ current_price = historical_data['close'].iloc[-1]
1065
+
1066
+ # Get signals from all agents
1067
+ signals = [
1068
+ trading_system.fundamental_agent.analyze(historical_data, trading_system.portfolio),
1069
+ trading_system.technical_agent.analyze(historical_data, trading_system.portfolio),
1070
+ trading_system.sentiment_agent.analyze(historical_data, trading_system.portfolio),
1071
+ trading_system.risk_agent.analyze(historical_data, trading_system.portfolio)
1072
+ ]
1073
+
1074
+ # Create state vector for RL
1075
+ state = trading_system.create_state_vector(historical_data, signals)
1076
+
1077
+ # Get RL agent action
1078
+ rl_action = trading_system.rl_agent.act(state)
1079
+ action_map = {0: 'buy', 1: 'hold', 2: 'sell'}
1080
+ rl_recommendation = action_map[rl_action]
1081
+
1082
+ # Aggregate signals
1083
+ action, confidence, reasoning = trading_system.aggregate_signals(signals)
1084
+
1085
+ # Blend with RL recommendation
1086
+ if rl_recommendation == action:
1087
+ confidence = min(confidence * 1.1, 0.95)
1088
+
1089
+ # Execute trade
1090
+ trade_result = trading_system.execute_trade(
1091
+ symbol, action, confidence, current_price, reasoning
1092
+ )
1093
+
1094
+ if trade_result['executed']:
1095
+ trade_history.append(trade_result)
1096
+
1097
+ # Update portfolio value
1098
+ trading_system.portfolio['total_value'] = trading_system.get_portfolio_value({symbol: current_price})
1099
+ trading_system.portfolio['peak_value'] = max(
1100
+ trading_system.portfolio['peak_value'],
1101
+ trading_system.portfolio['total_value']
1102
+ )
1103
+
1104
+ # Calculate performance metrics
1105
+ returns = (trading_system.portfolio['total_value'] - trading_system.initial_capital) / trading_system.initial_capital
1106
+
1107
+ performance_history.append({
1108
+ 'date': historical_data.index[-1],
1109
+ 'portfolio_value': trading_system.portfolio['total_value'],
1110
+ 'returns': returns,
1111
+ 'price': current_price,
1112
+ 'cash': trading_system.portfolio['cash'],
1113
+ 'position_value': trading_system.portfolio['total_position_value'],
1114
+ 'action': action,
1115
+ 'confidence': confidence,
1116
+ 'signals': {s.agent_name: s.signal_type for s in signals}
1117
+ })
1118
+
1119
+ # Train RL agent
1120
+ if i > 51: # Need previous state
1121
+ prev_state = trading_system.create_state_vector(market_data.iloc[:i], signals)
1122
+ reward = (trading_system.portfolio['total_value'] - performance_history[-2]['portfolio_value']) / trading_system.initial_capital
1123
+ trading_system.rl_agent.remember(prev_state, rl_action, reward, state, False)
1124
+
1125
+ if i % 10 == 0: # Train every 10 steps
1126
+ trading_system.rl_agent.replay()
1127
+
1128
+ # Update target network periodically
1129
+ if i % 100 == 0:
1130
+ trading_system.rl_agent.update_target_network()
1131
+
1132
+ print("Backtest completed")
1133
+ return pd.DataFrame(performance_history), trade_history
1134
+
1135
+ def calculate_performance_metrics(performance_df: pd.DataFrame) -> Dict[str, float]:
1136
+ """Calculate comprehensive performance metrics"""
1137
+
1138
+ # Basic metrics
1139
+ total_return = performance_df['returns'].iloc[-1]
1140
+
1141
+ # Calculate daily returns
1142
+ portfolio_values = performance_df['portfolio_value'].values
1143
+ daily_returns = np.diff(portfolio_values) / portfolio_values[:-1]
1144
+
1145
+ # Sharpe ratio
1146
+ if len(daily_returns) > 0 and daily_returns.std() > 0:
1147
+ sharpe_ratio = np.sqrt(252) * daily_returns.mean() / daily_returns.std()
1148
+ else:
1149
+ sharpe_ratio = 0
1150
+
1151
+ # Maximum drawdown
1152
+ peak = np.maximum.accumulate(portfolio_values)
1153
+ drawdown = (peak - portfolio_values) / peak
1154
+ max_drawdown = np.max(drawdown)
1155
+
1156
+ # Win rate
1157
+ winning_days = np.sum(daily_returns > 0)
1158
+ total_days = len(daily_returns)
1159
+ win_rate = winning_days / total_days if total_days > 0 else 0
1160
+
1161
+ # Volatility
1162
+ annual_volatility = daily_returns.std() * np.sqrt(252) if len(daily_returns) > 0 else 0
1163
+
1164
+ # Calculate Sortino ratio (downside deviation)
1165
+ negative_returns = daily_returns[daily_returns < 0]
1166
+ downside_deviation = negative_returns.std() * np.sqrt(252) if len(negative_returns) > 0 else 1
1167
+ sortino_ratio = (daily_returns.mean() * 252 - RISK_FREE_RATE) / downside_deviation if downside_deviation > 0 else 0
1168
+
1169
+ # Calmar ratio
1170
+ calmar_ratio = (total_return * 252 / len(performance_df)) / max_drawdown if max_drawdown > 0 else 0
1171
+
1172
+ return {
1173
+ 'total_return': total_return,
1174
+ 'annual_return': ((1 + total_return) ** (252 / len(performance_df)) - 1) if len(performance_df) > 0 else 0,
1175
+ 'sharpe_ratio': sharpe_ratio,
1176
+ 'sortino_ratio': sortino_ratio,
1177
+ 'calmar_ratio': calmar_ratio,
1178
+ 'max_drawdown': max_drawdown,
1179
+ 'win_rate': win_rate,
1180
+ 'annual_volatility': annual_volatility
1181
+ }
1182
+
1183
+ # Gradio Interface
1184
+ def create_gradio_interface():
1185
+ """Create professional Gradio interface for the trading system"""
1186
+
1187
+ def run_simulation(initial_capital, volatility, trading_days, openai_api_key, news_api_key):
1188
+ """Run comprehensive trading simulation"""
1189
+
1190
+ print(f"Starting simulation with capital: ${initial_capital:,.2f}")
1191
+
1192
+ # Initialize trading system with API keys
1193
+ trading_system = MultiAgentTradingSystem(
1194
+ initial_capital=float(initial_capital),
1195
+ openai_api_key=openai_api_key if openai_api_key else None,
1196
+ news_api_key=news_api_key if news_api_key else None
1197
+ )
1198
+
1199
+ # Generate market data
1200
+ market_simulator = MarketSimulator(volatility=float(volatility))
1201
+ market_data = market_simulator.generate_market_data(int(trading_days))
1202
+
1203
+ # Run backtest
1204
+ performance_df, trade_history = run_backtest(trading_system, market_data, "DEMO")
1205
+
1206
+ # Calculate metrics
1207
+ metrics = calculate_performance_metrics(performance_df)
1208
+
1209
+ # Create performance visualization
1210
+ fig = make_subplots(
1211
+ rows=4, cols=1,
1212
+ subplot_titles=(
1213
+ 'Portfolio Value vs Market Price',
1214
+ 'Portfolio Allocation',
1215
+ 'Agent Signal Distribution',
1216
+ 'Drawdown Analysis'
1217
+ ),
1218
+ vertical_spacing=0.08,
1219
+ row_heights=[0.35, 0.25, 0.20, 0.20]
1220
+ )
1221
+
1222
+ # Portfolio value and market price
1223
+ fig.add_trace(
1224
+ go.Scatter(
1225
+ x=performance_df['date'],
1226
+ y=performance_df['portfolio_value'],
1227
+ name='Portfolio Value',
1228
+ line=dict(color='blue', width=2)
1229
+ ),
1230
+ row=1, col=1
1231
+ )
1232
+
1233
+ # Normalize market price for comparison
1234
+ normalized_price = performance_df['price'] * initial_capital / performance_df['price'].iloc[0]
1235
+ fig.add_trace(
1236
+ go.Scatter(
1237
+ x=performance_df['date'],
1238
+ y=normalized_price,
1239
+ name='Buy & Hold',
1240
+ line=dict(color='gray', dash='dash')
1241
+ ),
1242
+ row=1, col=1
1243
+ )
1244
+
1245
+ # Portfolio allocation
1246
+ fig.add_trace(
1247
+ go.Scatter(
1248
+ x=performance_df['date'],
1249
+ y=performance_df['cash'],
1250
+ name='Cash',
1251
+ fill='tonexty',
1252
+ stackgroup='one',
1253
+ line=dict(color='green')
1254
+ ),
1255
+ row=2, col=1
1256
+ )
1257
+ fig.add_trace(
1258
+ go.Scatter(
1259
+ x=performance_df['date'],
1260
+ y=performance_df['position_value'],
1261
+ name='Positions',
1262
+ fill='tonexty',
1263
+ stackgroup='one',
1264
+ line=dict(color='orange')
1265
+ ),
1266
+ row=2, col=1
1267
+ )
1268
+
1269
+ # Agent signals analysis
1270
+ agent_signals = pd.DataFrame([p['signals'] for p in performance_df.to_dict('records')])
1271
+ signal_counts = {}
1272
+ for agent in ['Fundamental Analyst', 'Technical Analyst', 'Sentiment Analyst', 'Risk Manager']:
1273
+ if agent in agent_signals.columns:
1274
+ signal_counts[agent] = agent_signals[agent].value_counts().to_dict()
1275
+
1276
+ # Create stacked bar chart for signals
1277
+ agents = list(signal_counts.keys())
1278
+ buy_counts = [signal_counts[agent].get('buy', 0) for agent in agents]
1279
+ hold_counts = [signal_counts[agent].get('hold', 0) for agent in agents]
1280
+ sell_counts = [signal_counts[agent].get('sell', 0) for agent in agents]
1281
+
1282
+ fig.add_trace(
1283
+ go.Bar(name='Buy', x=agents, y=buy_counts, marker_color='green'),
1284
+ row=3, col=1
1285
+ )
1286
+ fig.add_trace(
1287
+ go.Bar(name='Hold', x=agents, y=hold_counts, marker_color='yellow'),
1288
+ row=3, col=1
1289
+ )
1290
+ fig.add_trace(
1291
+ go.Bar(name='Sell', x=agents, y=sell_counts, marker_color='red'),
1292
+ row=3, col=1
1293
+ )
1294
+
1295
+ # Drawdown chart
1296
+ portfolio_values = performance_df['portfolio_value'].values
1297
+ peak = np.maximum.accumulate(portfolio_values)
1298
+ drawdown = (peak - portfolio_values) / peak * 100 # Convert to percentage
1299
+
1300
+ fig.add_trace(
1301
+ go.Scatter(
1302
+ x=performance_df['date'],
1303
+ y=-drawdown, # Negative for visual clarity
1304
+ fill='tozeroy',
1305
+ name='Drawdown %',
1306
+ line=dict(color='red')
1307
+ ),
1308
+ row=4, col=1
1309
+ )
1310
+
1311
+ # Update layout
1312
+ fig.update_layout(
1313
+ height=1200,
1314
+ showlegend=True,
1315
+ title_text=f"Multi-Agent Trading System Performance Analysis",
1316
+ title_font_size=20
1317
+ )
1318
+
1319
+ fig.update_xaxes(title_text="Date", row=4, col=1)
1320
+ fig.update_yaxes(title_text="Value ($)", row=1, col=1)
1321
+ fig.update_yaxes(title_text="Value ($)", row=2, col=1)
1322
+ fig.update_yaxes(title_text="Signal Count", row=3, col=1)
1323
+ fig.update_yaxes(title_text="Drawdown (%)", row=4, col=1)
1324
+
1325
+ # Stack bars
1326
+ fig.update_layout(barmode='stack')
1327
+
1328
+ # Create metrics summary
1329
+ metrics_text = f"""
1330
+ ## Performance Metrics
1331
+
1332
+ **Returns**
1333
+ - Total Return: {metrics['total_return']*100:.2f}%
1334
+ - Annualized Return: {metrics['annual_return']*100:.2f}%
1335
+
1336
+ **Risk Metrics**
1337
+ - Sharpe Ratio: {metrics['sharpe_ratio']:.2f}
1338
+ - Sortino Ratio: {metrics['sortino_ratio']:.2f}
1339
+ - Calmar Ratio: {metrics['calmar_ratio']:.2f}
1340
+ - Maximum Drawdown: {metrics['max_drawdown']*100:.2f}%
1341
+ - Annual Volatility: {metrics['annual_volatility']*100:.1f}%
1342
+
1343
+ **Trading Statistics**
1344
+ - Win Rate: {metrics['win_rate']*100:.1f}%
1345
+ - Total Trades: {len(trade_history)}
1346
+ - Average Confidence: {performance_df['confidence'].mean():.2%}
1347
+
1348
+ **Portfolio Summary**
1349
+ - Final Portfolio Value: ${performance_df['portfolio_value'].iloc[-1]:,.2f}
1350
+ - Final Cash Position: ${performance_df['cash'].iloc[-1]:,.2f}
1351
+ - Final Position Value: ${performance_df['position_value'].iloc[-1]:,.2f}
1352
+ """
1353
+
1354
+ # Create trade history table
1355
+ if trade_history:
1356
+ # Get last 20 trades
1357
+ recent_trades = trade_history[-20:]
1358
+ trade_df = pd.DataFrame([
1359
+ {
1360
+ 'Date': t['timestamp'].strftime('%Y-%m-%d'),
1361
+ 'Action': t['action'].upper(),
1362
+ 'Shares': f"{t['shares']:+d}",
1363
+ 'Price': f"${t['price']:.2f}",
1364
+ 'Value': f"${abs(t['value']):,.2f}",
1365
+ 'Confidence': f"{t['confidence']:.1%}"
1366
+ }
1367
+ for t in recent_trades
1368
+ ])
1369
+ else:
1370
+ trade_df = pd.DataFrame()
1371
+
1372
+ # Agent analysis for the last day
1373
+ if performance_df['signals'].iloc[-1]:
1374
+ last_signals = performance_df['signals'].iloc[-1]
1375
+ agent_summary = "## Latest Agent Consensus\n\n"
1376
+
1377
+ for agent_name, signal in last_signals.items():
1378
+ agent_summary += f"**{agent_name}**: {signal.upper()}\n"
1379
+
1380
+ agent_summary += f"\n**Final Decision**: {performance_df['action'].iloc[-1].upper()} "
1381
+ agent_summary += f"with {performance_df['confidence'].iloc[-1]:.1%} confidence"
1382
+ else:
1383
+ agent_summary = "## Agent Analysis\n\nNo signals available"
1384
+
1385
+ return fig, metrics_text, trade_df, agent_summary
1386
+
1387
+ # Create Gradio interface
1388
+ with gr.Blocks(title="LLM-Powered Multi-Agent Trading System", theme=gr.themes.Base()) as interface:
1389
+ gr.Markdown("""
1390
+ # LLM-Powered Multi-Agent Trading System
1391
+
1392
+ This professional trading system demonstrates sophisticated multi-agent coordination for algorithmic trading:
1393
+
1394
+ **Core Components:**
1395
+ - **Fundamental Analysis Agent**: Uses OpenAI LLM for real market analysis and trading insights
1396
+ - **Technical Analysis Agent**: Employs transformer neural networks for price pattern recognition
1397
+ - **Sentiment Analysis Agent**: Leverages FinBERT for financial sentiment analysis with real news data
1398
+ - **Risk Management Agent**: Monitors portfolio risk metrics and provides risk-adjusted recommendations
1399
+ - **DQN Coordinator**: Deep Q-Network that learns optimal trading strategies from agent signals
1400
+
1401
+ **Key Features:**
1402
+ - Real LLM integration for genuine fundamental analysis reasoning
1403
+ - Advanced neural network models for pattern recognition
1404
+ - Comprehensive risk management framework with Kelly Criterion
1405
+ - Reinforcement learning for strategy optimization
1406
+ - Professional-grade backtesting and performance analytics
1407
+
1408
+ Author: Spencer Purdy
1409
+ """)
1410
+
1411
+ with gr.Row():
1412
+ with gr.Column(scale=1):
1413
+ gr.Markdown("### Simulation Parameters")
1414
+ initial_capital = gr.Number(
1415
+ value=100000,
1416
+ label="Initial Capital ($)",
1417
+ minimum=10000,
1418
+ maximum=1000000,
1419
+ info="Starting capital for the simulation"
1420
+ )
1421
+ volatility = gr.Slider(
1422
+ minimum=0.01,
1423
+ maximum=0.05,
1424
+ value=0.02,
1425
+ step=0.005,
1426
+ label="Market Volatility",
1427
+ info="Annual volatility of the simulated market"
1428
+ )
1429
+ trading_days = gr.Slider(
1430
+ minimum=100,
1431
+ maximum=500,
1432
+ value=252,
1433
+ step=10,
1434
+ label="Trading Days",
1435
+ info="Number of trading days to simulate"
1436
+ )
1437
+
1438
+ gr.Markdown("### API Keys (Optional)")
1439
+ openai_api_key = gr.Textbox(
1440
+ label="OpenAI API Key",
1441
+ placeholder="sk-...",
1442
+ type="password",
1443
+ info="For real LLM fundamental analysis (leave empty for fallback)"
1444
+ )
1445
+ news_api_key = gr.Textbox(
1446
+ label="News API Key",
1447
+ placeholder="Your NewsAPI key",
1448
+ type="password",
1449
+ info="For real news sentiment analysis (leave empty for contextual news)"
1450
+ )
1451
+
1452
+ run_button = gr.Button("Run Simulation", variant="primary", size="lg")
1453
+
1454
+ with gr.Row():
1455
+ with gr.Column(scale=3):
1456
+ performance_plot = gr.Plot(label="Performance Analysis Dashboard")
1457
+
1458
+ with gr.Column(scale=1):
1459
+ metrics_display = gr.Markdown(label="Performance Metrics")
1460
+
1461
+ with gr.Row():
1462
+ with gr.Column():
1463
+ trade_table = gr.DataFrame(
1464
+ label="Recent Trading Activity (Last 20 Trades)",
1465
+ headers=["Date", "Action", "Shares", "Price", "Value", "Confidence"]
1466
+ )
1467
+
1468
+ with gr.Row():
1469
+ with gr.Column():
1470
+ agent_display = gr.Markdown(label="Agent Analysis")
1471
+
1472
+ # Connect interface
1473
+ run_button.click(
1474
+ fn=run_simulation,
1475
+ inputs=[initial_capital, volatility, trading_days, openai_api_key, news_api_key],
1476
+ outputs=[performance_plot, metrics_display, trade_table, agent_display]
1477
+ )
1478
+
1479
+ # Add professional examples
1480
+ gr.Examples(
1481
+ examples=[
1482
+ [100000, 0.02, 252, "", ""], # Standard market conditions
1483
+ [50000, 0.03, 365, "", ""], # Higher volatility environment
1484
+ [200000, 0.015, 180, "", ""], # Lower volatility environment
1485
+ ],
1486
+ inputs=[initial_capital, volatility, trading_days, openai_api_key, news_api_key],
1487
+ label="Example Configurations"
1488
+ )
1489
+
1490
+ gr.Markdown("""
1491
+ ---
1492
+ **Note**: This system uses sophisticated machine learning models including real LLM integration for fundamental analysis.
1493
+ For best results, provide an OpenAI API key for genuine LLM reasoning and a News API key for real news sentiment analysis.
1494
+ The simulation may take a few moments to initialize and run. All trading decisions are for demonstration
1495
+ purposes only and should not be used for actual trading without proper validation and risk assessment.
1496
+
1497
+ **API Key Information**:
1498
+ - OpenAI API Key: Get yours at https://platform.openai.com/api-keys
1499
+ - News API Key: Get yours at https://newsapi.org/register
1500
+ """)
1501
+
1502
+ return interface
1503
+
1504
+ # Launch the application
1505
+ if __name__ == "__main__":
1506
+ interface = create_gradio_interface()
1507
+ interface.launch()