Create app.py
Browse files
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()
|