don-unagi commited on
Commit
c90e00d
·
1 Parent(s): 52ecdd4
agents/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .state import AgentState, AgentState2
2
+ from .portfolio_analyzer import portfolio_analyzer
3
+ from .news_analyzer import news_analyzer
4
+ from .technical_analyzer import technical_analyzer, calculate_rsi
5
+ from .recommendation_engine import recommendation_engine
6
+ from .graph import setup_graph_with_tracking, setup_new_investments_graph
7
+ from .rag_analyzer import rag_analyzer
8
+ from .zacks_analyzer import zacks_analyzer
9
+ from .new_investment_recommender import new_investment_recommender
10
+
11
+ __all__ = [
12
+ 'AgentState',
13
+ 'AgentState2',
14
+ 'portfolio_analyzer',
15
+ 'news_analyzer',
16
+ 'technical_analyzer',
17
+ 'rag_analyzer',
18
+ 'recommendation_engine',
19
+ 'setup_graph_with_tracking',
20
+ 'setup_new_investments_graph',
21
+ 'zacks_analyzer',
22
+ 'new_investment_recommender',
23
+ 'calculate_rsi'
24
+ ]
agents/graph.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langgraph.graph import StateGraph, END
2
+ from .state import AgentState, AgentState2
3
+ from .portfolio_analyzer import portfolio_analyzer
4
+ from .news_analyzer import news_analyzer
5
+ from .technical_analyzer import technical_analyzer
6
+ from .recommendation_engine import recommendation_engine
7
+ from .rag_analyzer import rag_analyzer
8
+ from .zacks_analyzer import zacks_analyzer
9
+ from .new_investment_recommender import new_investment_recommender
10
+ from .new_stock_analyzer import new_stock_analyzer
11
+ from .portfolio_fit_evaluator import portfolio_fit_evaluator
12
+
13
+ # def setup_graph():
14
+ # """Set up and compile the LangGraph workflow with a direct sequential flow.
15
+
16
+ # The workflow follows a fixed sequence without a supervisor:
17
+ # PortfolioAnalyzer -> TechnicalAnalyzer -> NewsAnalyzer -> RAGAnalyzer -> RecommendationEngine -> END
18
+ # """
19
+ # # Define state schema
20
+ # workflow = StateGraph(AgentState)
21
+
22
+ # # Add nodes
23
+ # workflow.add_node("PortfolioAnalyzer", portfolio_analyzer)
24
+ # workflow.add_node("NewsAnalyzer", news_analyzer)
25
+ # workflow.add_node("TechnicalAnalyzer", technical_analyzer)
26
+ # workflow.add_node("RAGAnalyzer", rag_analyzer)
27
+ # workflow.add_node("RecommendationEngine", recommendation_engine)
28
+
29
+ # # Define direct sequential edges between agents
30
+ # workflow.add_edge("PortfolioAnalyzer", "TechnicalAnalyzer")
31
+ # workflow.add_edge("TechnicalAnalyzer", "NewsAnalyzer")
32
+ # workflow.add_edge("NewsAnalyzer", "RAGAnalyzer")
33
+ # workflow.add_edge("RAGAnalyzer", "RecommendationEngine")
34
+ # workflow.add_edge("RecommendationEngine", END)
35
+
36
+ # # Set entry point
37
+ # workflow.set_entry_point("PortfolioAnalyzer")
38
+
39
+ # return workflow.compile()
40
+
41
+ def setup_graph_with_tracking(progress_callback=None):
42
+ """Set up and compile the LangGraph workflow with progress tracking and direct sequential flow.
43
+
44
+ The workflow follows a fixed sequence without a supervisor:
45
+ TechnicalAnalyzer -> PortfolioAnalyzer -> NewsAnalyzer -> RAGAnalyzer -> RecommendationEngine -> END
46
+ """
47
+ # Define state schema
48
+ workflow = StateGraph(AgentState)
49
+
50
+ # Add nodes with progress tracking wrappers if callback is provided
51
+ workflow.add_node("PortfolioAnalyzer",
52
+ lambda state: progress_callback(portfolio_analyzer(state)) if progress_callback else portfolio_analyzer(state))
53
+
54
+ workflow.add_node("NewsAnalyzer",
55
+ lambda state: progress_callback(news_analyzer(state)) if progress_callback else news_analyzer(state))
56
+
57
+ workflow.add_node("TechnicalAnalyzer",
58
+ lambda state: progress_callback(technical_analyzer(state)) if progress_callback else technical_analyzer(state))
59
+
60
+ workflow.add_node("RAGAnalyzer",
61
+ lambda state: progress_callback(rag_analyzer(state)) if progress_callback else rag_analyzer(state))
62
+
63
+ workflow.add_node("RecommendationEngine",
64
+ lambda state: progress_callback(recommendation_engine(state)) if progress_callback else recommendation_engine(state))
65
+
66
+ # Define direct sequential edges between agents
67
+ workflow.add_edge("TechnicalAnalyzer", "PortfolioAnalyzer")
68
+ workflow.add_edge("PortfolioAnalyzer", "NewsAnalyzer")
69
+ workflow.add_edge("NewsAnalyzer", "RAGAnalyzer")
70
+ workflow.add_edge("RAGAnalyzer", "RecommendationEngine")
71
+ workflow.add_edge("RecommendationEngine", END)
72
+
73
+ # Set entry point
74
+ workflow.set_entry_point("TechnicalAnalyzer")
75
+
76
+ return workflow.compile()
77
+
78
+ def setup_new_investments_graph(progress_callback=None):
79
+ """Set up and compile a LangGraph workflow for finding new investment opportunities.
80
+
81
+ The workflow follows a fixed sequence:
82
+ ZacksAnalyzer -> NewStockAnalyzer -> PortfolioFitEvaluator -> NewInvestmentRecommender -> END
83
+
84
+ This workflow:
85
+ 1. Fetches high-ranked stocks from Zacks
86
+ 2. Performs technical analysis only on these new stocks
87
+ 3. Evaluates how these new stocks fit into the existing portfolio
88
+ 4. Recommends only stocks that have been properly analyzed
89
+ """
90
+ # Define state schema
91
+ workflow = StateGraph(AgentState2)
92
+
93
+ # Add nodes with progress tracking wrappers if callback is provided
94
+ workflow.add_node("ZacksAnalyzer",
95
+ lambda state: progress_callback(zacks_analyzer(state)) if progress_callback else zacks_analyzer(state))
96
+
97
+ # This node will analyze only the new high-ranked stocks, not the entire portfolio
98
+ workflow.add_node("NewStockAnalyzer",
99
+ lambda state: progress_callback(new_stock_analyzer(state)) if progress_callback else new_stock_analyzer(state))
100
+
101
+ # This node evaluates how new stocks fit into the existing portfolio context
102
+ workflow.add_node("PortfolioFitEvaluator",
103
+ lambda state: progress_callback(portfolio_fit_evaluator(state)) if progress_callback else portfolio_fit_evaluator(state))
104
+
105
+ # This node recommends only stocks that have been properly analyzed
106
+ workflow.add_node("NewInvestmentRecommender",
107
+ lambda state: progress_callback(new_investment_recommender(state)) if progress_callback else new_investment_recommender(state))
108
+
109
+ # Define direct sequential edges between agents
110
+ workflow.add_edge("ZacksAnalyzer", "NewStockAnalyzer")
111
+ workflow.add_edge("NewStockAnalyzer", "PortfolioFitEvaluator")
112
+ workflow.add_edge("PortfolioFitEvaluator", "NewInvestmentRecommender")
113
+ workflow.add_edge("NewInvestmentRecommender", END)
114
+
115
+ # Set entry point
116
+ workflow.set_entry_point("ZacksAnalyzer")
117
+
118
+ return workflow.compile()
agents/new_investment_recommender.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
5
+ from typing import Dict, List
6
+ from .state import AgentState
7
+
8
+ def create_new_investment_recommender():
9
+ """Create a new investment recommender using LangChain."""
10
+ # Define the function schema for structured output
11
+ function_def = {
12
+ "name": "generate_new_investment_recommendations",
13
+ "description": "Generate new investment recommendations based on analyzed high-rank stocks and portfolio fit",
14
+ "parameters": {
15
+ "type": "object",
16
+ "properties": {
17
+ "new_investment_summary": {
18
+ "type": "string",
19
+ "description": "Summary of the new investment opportunities"
20
+ },
21
+ "new_investments": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "properties": {
26
+ "ticker": {
27
+ "type": "string",
28
+ "description": "Stock ticker symbol"
29
+ },
30
+ "action": {
31
+ "type": "string",
32
+ "enum": ["BUY"],
33
+ "description": "Recommended action for this stock"
34
+ },
35
+ "reasoning": {
36
+ "type": "string",
37
+ "description": "Detailed reasoning for the recommendation"
38
+ },
39
+ "priority": {
40
+ "type": "integer",
41
+ "description": "Priority level (1-5, where 1 is highest priority)",
42
+ "minimum": 1,
43
+ "maximum": 5
44
+ }
45
+ },
46
+ "required": ["ticker", "action", "reasoning", "priority"]
47
+ }
48
+ }
49
+ },
50
+ "required": ["new_investment_summary", "new_investments"]
51
+ }
52
+ }
53
+
54
+ # Create the prompt template
55
+ prompt = ChatPromptTemplate.from_messages([
56
+ ("system", """You are an expert financial advisor specializing in identifying new investment opportunities.
57
+
58
+ Your task is to recommend only high-ranked stocks that have been properly analyzed and evaluated for portfolio fit.
59
+
60
+ Consider the following when making recommendations:
61
+ 1. Technical analysis signals (RSI, moving averages, etc.)
62
+ 2. Fundamental analysis metrics (PE ratios, debt-to-equity, growth rates, etc.)
63
+ 3. Prioritize stocks that have been evaluated as good fits for the portfolio
64
+ 4. Consider the user's risk tolerance and investment goals
65
+ 5. Focus on stocks that complement the existing portfolio and improve diversification
66
+
67
+ Provide clear, actionable recommendations with detailed reasoning.
68
+
69
+ IMPORTANT: For the reasoning of each recommendation, include a more detailed technical and fundamental analysis section:
70
+ - For technical analysis: Include specific insights about RSI levels, MACD signals, moving average crossovers, and what these indicators suggest about momentum and trend direction.
71
+ - For fundamental analysis: Include specific metrics like P/E ratio compared to industry average, debt-to-equity ratio, earnings growth, and what these metrics suggest about the company's valuation and financial health.
72
+
73
+
74
+ """),
75
+ ("human", """
76
+ I need recommendations for new investments based on the following information:
77
+
78
+ User's Portfolio:
79
+ {portfolio_data}
80
+
81
+ User's Risk Tolerance: {risk_level}
82
+ User's Investment Goals: {investment_goals}
83
+
84
+ High-Ranked Stocks:
85
+ {high_rank_stocks}
86
+
87
+ New Stock Analysis:
88
+ {new_stock_analysis}
89
+
90
+ Portfolio Fit Evaluation:
91
+ {portfolio_fit}
92
+
93
+ Please provide specific recommendations for new investments that would complement my existing portfolio.
94
+ Only recommend stocks that have been properly analyzed and evaluated for portfolio fit.
95
+ """)
96
+ ])
97
+
98
+ # Create the LLM
99
+ llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.5, api_key=os.getenv("OPENAI_API_KEY"))
100
+
101
+ # Create the structured output chain
102
+ chain = prompt | llm.bind_functions(functions=[function_def], function_call={"name": "generate_new_investment_recommendations"}) | JsonOutputFunctionsParser()
103
+
104
+ return chain
105
+
106
+ def new_investment_recommender(state: AgentState) -> AgentState:
107
+ """Recommends new investments based on analyzed high-rank stocks and portfolio fit evaluation."""
108
+ try:
109
+ # Get the portfolio data
110
+ portfolio = state["portfolio_data"]
111
+
112
+ # Get the high-rank stocks
113
+ high_rank_stocks = state.get("high_rank_stocks", [])
114
+
115
+ # Get the new stock analysis
116
+ new_stock_analysis = state.get("new_stock_analysis", {})
117
+
118
+ # Get the portfolio fit evaluation
119
+ portfolio_fit = state.get("portfolio_fit", {})
120
+
121
+ # If no stocks have been analyzed or evaluated, return early
122
+ if not new_stock_analysis or not portfolio_fit.get("evaluated_stocks"):
123
+ state["messages"] = state.get("messages", []) + [{
124
+ "role": "ai",
125
+ "content": "[NewInvestmentRecommender] No properly analyzed stocks to recommend."
126
+ }]
127
+ state["new_investment_summary"] = "No properly analyzed stocks to recommend."
128
+ state["new_investments"] = []
129
+ return state
130
+
131
+ # Get user preferences
132
+ risk_level = state.get("risk_level", 5)
133
+ investment_goals = state.get("investment_goals", "Growth")
134
+
135
+ # Create the recommender
136
+ recommender = create_new_investment_recommender()
137
+
138
+ # Generate recommendations
139
+ result = recommender.invoke({
140
+ "portfolio_data": portfolio,
141
+ "risk_level": risk_level,
142
+ "investment_goals": investment_goals,
143
+ "high_rank_stocks": high_rank_stocks,
144
+ "new_stock_analysis": new_stock_analysis,
145
+ "portfolio_fit": portfolio_fit
146
+ })
147
+
148
+ # Ensure we have the required fields
149
+ if "new_investment_summary" not in result:
150
+ result["new_investment_summary"] = f"Analysis of {len(high_rank_stocks)} high-ranked stocks."
151
+
152
+ if "new_investments" not in result:
153
+ result["new_investments"] = []
154
+
155
+ except Exception as e:
156
+ # Handle any errors in the recommendation engine
157
+ state["messages"] = state.get("messages", []) + [{
158
+ "role": "ai",
159
+ "content": f"[NewInvestmentRecommender] Error generating recommendations: {str(e)}"
160
+ }]
161
+
162
+ result = {
163
+ "new_investment_summary": "Error in analysis",
164
+ "new_investments": []
165
+ }
166
+
167
+ # Update state with recommendations
168
+ state["new_investment_summary"] = result.get("new_investment_summary", "")
169
+ state["new_investments"] = result.get("new_investments", [])
170
+
171
+ # Add message to communication
172
+ state["messages"] = state.get("messages", []) + [{
173
+ "role": "ai",
174
+ "content": f"[NewInvestmentRecommender] I've analyzed the properly evaluated stocks and generated {len(result.get('new_investments', []))} recommendations for new investments."
175
+ }]
176
+
177
+ return state
agents/new_stock_analyzer.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import numpy as np
3
+ import pandas as pd
4
+ from typing import Dict, Any, List
5
+ from .state import AgentState
6
+
7
+ def calculate_rsi(prices, period=14):
8
+ """Calculate Relative Strength Index."""
9
+ # Calculate price changes
10
+ delta = prices.diff()
11
+
12
+ # Separate gains and losses
13
+ gain = delta.clip(lower=0)
14
+ loss = -delta.clip(upper=0)
15
+
16
+ # Calculate average gain and loss
17
+ avg_gain = gain.rolling(window=period).mean()
18
+ avg_loss = loss.rolling(window=period).mean()
19
+
20
+ # Calculate relative strength (RS)
21
+ rs = avg_gain / avg_loss
22
+
23
+ # Calculate RSI
24
+ rsi = 100 - (100 / (1 + rs))
25
+
26
+ return rsi
27
+
28
+ def calculate_macd(prices, fast=12, slow=26, signal=9):
29
+ """Calculate Moving Average Convergence Divergence."""
30
+ # Calculate EMAs
31
+ ema_fast = prices.ewm(span=fast, adjust=False).mean()
32
+ ema_slow = prices.ewm(span=slow, adjust=False).mean()
33
+
34
+ # Calculate MACD line
35
+ macd_line = ema_fast - ema_slow
36
+
37
+ # Calculate signal line
38
+ signal_line = macd_line.ewm(span=signal, adjust=False).mean()
39
+
40
+ # Calculate histogram
41
+ histogram = macd_line - signal_line
42
+
43
+ return {
44
+ 'macd_line': macd_line,
45
+ 'signal_line': signal_line,
46
+ 'histogram': histogram
47
+ }
48
+
49
+ def get_technical_indicators(ticker_obj, hist):
50
+ """Calculate technical indicators for a stock without making judgments."""
51
+ if hist.empty:
52
+ return None
53
+
54
+ # Calculate basic technical indicators
55
+ hist['MA20'] = hist['Close'].rolling(window=20).mean()
56
+ hist['MA50'] = hist['Close'].rolling(window=50).mean()
57
+ hist['MA200'] = hist['Close'].rolling(window=200).mean()
58
+ hist['RSI'] = calculate_rsi(hist['Close'])
59
+
60
+ # Calculate MACD
61
+ macd = calculate_macd(hist['Close'])
62
+ hist['MACD_Line'] = macd['macd_line']
63
+ hist['MACD_Signal'] = macd['signal_line']
64
+ hist['MACD_Histogram'] = macd['histogram']
65
+
66
+ # Calculate Bollinger Bands
67
+ hist['BB_Middle'] = hist['Close'].rolling(window=20).mean()
68
+ std = hist['Close'].rolling(window=20).std()
69
+ hist['BB_Upper'] = hist['BB_Middle'] + (std * 2)
70
+ hist['BB_Lower'] = hist['BB_Middle'] - (std * 2)
71
+
72
+ # Get latest values
73
+ latest = hist.iloc[-1]
74
+
75
+ # Get fundamental data
76
+ info = ticker_obj.info
77
+
78
+ return {
79
+ 'current_price': latest['Close'],
80
+ 'technical_indicators': {
81
+ 'ma20': latest['MA20'],
82
+ 'ma50': latest['MA50'],
83
+ 'ma200': latest['MA200'],
84
+ 'rsi': latest['RSI'],
85
+ 'macd_line': latest['MACD_Line'],
86
+ 'macd_signal': latest['MACD_Signal'],
87
+ 'macd_histogram': latest['MACD_Histogram'],
88
+ 'bb_upper': latest['BB_Upper'],
89
+ 'bb_middle': latest['BB_Middle'],
90
+ 'bb_lower': latest['BB_Lower'],
91
+ 'volume': latest['Volume']
92
+ },
93
+ 'fundamental_data': {
94
+ 'symbol': ticker_obj.ticker,
95
+ 'price': info.get('currentPrice'),
96
+ 'pe_ratio': info.get('trailingPE'),
97
+ 'peg_ratio': info.get('pegRatio'),
98
+ 'debt_to_equity': info.get('debtToEquity'),
99
+ 'forward_pe': info.get('forwardPE'),
100
+ 'beta': info.get('beta'),
101
+ 'return_on_equity': info.get('returnOnEquity'),
102
+ 'free_cash_flow': info.get('freeCashflow'),
103
+ 'revenue_growth': info.get('revenueGrowth'),
104
+ 'earnings_growth': info.get('earningsGrowth'),
105
+ 'dividend_yield': info.get('dividendYield'),
106
+ 'market_cap': info.get('marketCap'),
107
+ 'profit_margins': info.get('profitMargins'),
108
+ 'price_to_book': info.get('priceToBook')
109
+ }
110
+ }
111
+
112
+ def new_stock_analyzer(state: AgentState) -> AgentState:
113
+ """Performs technical analysis only on the new high-ranked stocks from Zacks."""
114
+ # Get the high-rank stocks from the Zacks analyzer
115
+ high_rank_stocks = state.get("high_rank_stocks", [])
116
+
117
+ if not high_rank_stocks:
118
+ state["messages"] = state.get("messages", []) + [{
119
+ "role": "ai",
120
+ "content": "[NewStockAnalyzer] No high-ranked stocks found to analyze."
121
+ }]
122
+ state["new_stock_analysis"] = {}
123
+ return state
124
+
125
+ # Extract tickers from high-rank stocks
126
+ tickers = [stock['symbol'] for stock in high_rank_stocks]
127
+ analysis_results = {}
128
+
129
+ # Collect technical data for all new stocks
130
+ for ticker in tickers:
131
+ try:
132
+ # Get stock data
133
+ stock = yf.Ticker(ticker)
134
+ hist = stock.history(period="6mo")
135
+
136
+ if not hist.empty:
137
+ # Get comprehensive technical indicators without judgments
138
+ indicators = get_technical_indicators(stock, hist)
139
+
140
+ if indicators:
141
+ analysis_results[ticker] = indicators
142
+ else:
143
+ analysis_results[ticker] = {"error": "Failed to calculate indicators"}
144
+ else:
145
+ analysis_results[ticker] = {"error": "No historical data available"}
146
+
147
+ except Exception as e:
148
+ print(f"Error analyzing {ticker}: {str(e)}")
149
+ analysis_results[ticker] = {"error": str(e)}
150
+
151
+ # Update state with new stock analysis
152
+ state["new_stock_analysis"] = analysis_results
153
+
154
+ # Add message to communication
155
+ state["messages"] = state.get("messages", []) + [{
156
+ "role": "ai",
157
+ "content": f"[NewStockAnalyzer] I've calculated technical indicators and gathered fundamental data for {len(analysis_results)} new high-ranked stocks."
158
+ }]
159
+
160
+ return state
agents/news_analyzer.py ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import datetime
3
+ from newsapi import NewsApiClient
4
+ from typing import List, Dict, Any
5
+ from langchain_openai import ChatOpenAI
6
+ from langchain_core.prompts import ChatPromptTemplate
7
+ from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
8
+ from .state import AgentState
9
+
10
+ def fetch_news(tickers, days=7):
11
+ """Fetch news for the given tickers from NewsAPI."""
12
+ # You need to set NEWSAPI_KEY in your .env file
13
+ newsapi_key = os.getenv("NEWSAPI_KEY")
14
+ if not newsapi_key:
15
+ print("NewsAPI key not found. Please add NEWSAPI_KEY to your .env file.")
16
+ return []
17
+
18
+ newsapi = NewsApiClient(api_key=newsapi_key)
19
+ news_items = []
20
+
21
+ # Get current date and date from n days ago
22
+ today = datetime.datetime.now().strftime('%Y-%m-%d')
23
+ days_ago = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime('%Y-%m-%d')
24
+
25
+ for ticker in tickers:
26
+ try:
27
+ # Search for news about the ticker
28
+ all_articles = newsapi.get_everything(
29
+ q=ticker,
30
+ from_param=days_ago,
31
+ to=today,
32
+ language='en',
33
+ sort_by='relevancy',
34
+ page_size=3 # Get top 3 news items per ticker
35
+ )
36
+
37
+ # Process the articles
38
+ if all_articles['status'] == 'ok' and all_articles['totalResults'] > 0:
39
+ for article in all_articles['articles']:
40
+ news_items.append({
41
+ 'ticker': ticker,
42
+ 'title': article['title'],
43
+ 'summary': article['description'] or "No description available.",
44
+ 'url': article['url'],
45
+ 'urlToImage': article['urlToImage'],
46
+ 'published_at': article['publishedAt'],
47
+ 'content': article.get('content', '')
48
+ })
49
+
50
+ # If no news found, add a placeholder
51
+ if not all_articles['articles']:
52
+ news_items.append({
53
+ 'ticker': ticker,
54
+ 'title': f"No recent news found for {ticker}",
55
+ 'summary': "No relevant news articles were found for this ticker in the past week.",
56
+ 'url': "",
57
+ 'urlToImage': "",
58
+ 'published_at': today,
59
+ 'content': ""
60
+ })
61
+
62
+ except Exception as e:
63
+ # Handle API errors or other exceptions
64
+ print(f"Error fetching news for {ticker}: {str(e)}")
65
+ news_items.append({
66
+ 'ticker': ticker,
67
+ 'title': f"Error fetching news for {ticker}",
68
+ 'summary': f"Could not retrieve news due to an error: {str(e)}",
69
+ 'url': "",
70
+ 'urlToImage': "",
71
+ 'published_at': today,
72
+ 'content': ""
73
+ })
74
+
75
+ return news_items
76
+
77
+ def create_news_analyzer_chain():
78
+ """Create a chain for news analysis using LLM."""
79
+ # Define function for structured output
80
+ function_def = {
81
+ "name": "analyze_news",
82
+ "description": "Analyze financial news articles for sentiment and impact",
83
+ "parameters": {
84
+ "type": "object",
85
+ "properties": {
86
+ "articles": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object",
90
+ "properties": {
91
+ "ticker": {
92
+ "type": "string",
93
+ "description": "Stock ticker symbol"
94
+ },
95
+ "title": {
96
+ "type": "string",
97
+ "description": "News article title"
98
+ },
99
+ "summary": {
100
+ "type": "string",
101
+ "description": "Summary of the news article"
102
+ },
103
+ "url": {
104
+ "type": "string",
105
+ "description": "URL of the news article"
106
+ },
107
+ "urlToImage": {
108
+ "type": "string",
109
+ "description": "URL of image from the news article"
110
+ },
111
+ "published_at": {
112
+ "type": "string",
113
+ "description": "Publication date of the article"
114
+ },
115
+ "sentiment": {
116
+ "type": "string",
117
+ "enum": ["positive", "negative", "neutral"],
118
+ "description": "Sentiment of the article towards the stock"
119
+ },
120
+ "impact_analysis": {
121
+ "type": "string",
122
+ "description": "Analysis of how this news might impact the stock"
123
+ },
124
+ "key_points": {
125
+ "type": "array",
126
+ "items": {
127
+ "type": "string"
128
+ },
129
+ "description": "Key points extracted from the news article"
130
+ },
131
+ "market_implications": {
132
+ "type": "string",
133
+ "description": "Broader market implications of this news"
134
+ }
135
+ },
136
+ "required": ["ticker", "title", "summary", "sentiment", "impact_analysis", "url", "urlToImage"]
137
+ }
138
+ },
139
+ "overall_market_sentiment": {
140
+ "type": "string",
141
+ "description": "Overall market sentiment based on all news articles"
142
+ },
143
+ "key_trends": {
144
+ "type": "array",
145
+ "items": {
146
+ "type": "string"
147
+ },
148
+ "description": "Key trends identified across all news articles"
149
+ }
150
+ },
151
+ "required": ["articles", "overall_market_sentiment"]
152
+ }
153
+ }
154
+
155
+ # Create prompt template
156
+ prompt = ChatPromptTemplate.from_template("""
157
+ You are a financial news analyst. Analyze the following news articles about stocks.
158
+
159
+ For each article, determine the sentiment (positive, negative, or neutral) and provide a brief analysis of how this news might impact the stock.
160
+ Focus on the news content itself and its potential market implications.
161
+
162
+ IMPORTANT: Only include articles that are directly relevant to the stocks in the portfolio or to financial markets in general.
163
+ Exclude any articles that are not related to financial markets, stocks, or investing.
164
+
165
+ NEWS ARTICLES:
166
+ {news_articles}
167
+
168
+ PORTFOLIO CONTEXT:
169
+ {portfolio_context}
170
+
171
+ Provide a detailed sentiment analysis for each article and an overall market sentiment assessment.
172
+ Focus on the news content and market implications rather than current portfolio performance.
173
+ """)
174
+
175
+ #CRITICAL: For each article, you MUST include the URL exactly as provided in the input. Do not modify or omit the URL.
176
+
177
+ # Create LLM
178
+ llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2)
179
+
180
+ # Create chain
181
+ chain = (
182
+ prompt
183
+ | llm.bind_functions(functions=[function_def], function_call="analyze_news")
184
+ | JsonOutputFunctionsParser()
185
+ )
186
+
187
+ return chain
188
+
189
+ # Initialize news analyzer chain
190
+ news_analyzer_chain = create_news_analyzer_chain()
191
+
192
+ def news_analyzer(state: AgentState) -> AgentState:
193
+ """Gathers and analyzes news for relevant assets using NewsAPI and LLM."""
194
+ portfolio = state["portfolio_data"]
195
+ tickers = list(portfolio.keys())
196
+
197
+ try:
198
+ # Fetch news
199
+ news_items = fetch_news(tickers)
200
+
201
+ if not news_items:
202
+ # If no news found, return empty analysis
203
+ state["news_analysis"] = []
204
+ state["messages"] = state.get("messages", []) + [{
205
+ "role": "ai",
206
+ "content": "[NewsAnalyzer] I couldn't find any relevant news for your portfolio stocks."
207
+ }]
208
+ return state
209
+
210
+ # Format portfolio context - simplified to focus on tickers only
211
+ portfolio_context = "Portfolio contains the following tickers:\n"
212
+ for ticker in tickers:
213
+ portfolio_context += f"{ticker}\n"
214
+
215
+ # Format news articles for LLM
216
+ news_articles_str = ""
217
+ for item in news_items:
218
+ news_articles_str += f"TICKER: {item['ticker']}\n"
219
+ news_articles_str += f"TITLE: {item['title']}\n"
220
+ news_articles_str += f"SUMMARY: {item['summary']}\n"
221
+ news_articles_str += f"URL: {item['url']}\n"
222
+ news_articles_str += f"URLTOIMAGE: {item['urlToImage']}\n"
223
+ news_articles_str += f"PUBLISHED: {item['published_at']}\n"
224
+ if item['content']:
225
+ news_articles_str += f"CONTENT: {item['content']}\n"
226
+ news_articles_str += "\n---\n\n"
227
+
228
+ # Analyze news with LLM
229
+ try:
230
+ result = news_analyzer_chain.invoke({
231
+ "news_articles": news_articles_str,
232
+ "portfolio_context": portfolio_context
233
+ })
234
+
235
+ # Update state with analyzed news
236
+ state["news_analysis"] = result["articles"]
237
+
238
+ # Add message to communication
239
+ state["messages"] = state.get("messages", []) + [{
240
+ "role": "ai",
241
+ "content": f"[NewsAnalyzer] I've analyzed recent news for your portfolio stocks. Overall market sentiment: {result['overall_market_sentiment']}"
242
+ }]
243
+ except Exception as e:
244
+ print(f"Error analyzing news with LLM: {str(e)}")
245
+ # Create a default analysis if LLM fails
246
+ default_analysis = []
247
+ for item in news_items[:5]: # Process up to 5 news items
248
+ default_analysis.append({
249
+ "ticker": item["ticker"],
250
+ "title": item["title"],
251
+ "summary": item["summary"],
252
+ "url": item["url"],
253
+ "urlToImage": item["urlToImage"],
254
+ "published_at": item.get("published_at", ""),
255
+ "sentiment": "neutral",
256
+ "impact_analysis": "Could not analyze impact due to processing error.",
257
+ "key_points": ["News content could not be analyzed"],
258
+ "market_implications": "Unknown due to processing limitations"
259
+ })
260
+
261
+ state["news_analysis"] = default_analysis
262
+ state["messages"] = state.get("messages", []) + [{
263
+ "role": "ai",
264
+ "content": "[NewsAnalyzer] I found some news for your portfolio stocks, but couldn't perform detailed analysis."
265
+ }]
266
+ except Exception as e:
267
+ print(f"Error in news analyzer: {str(e)}")
268
+ state["news_analysis"] = []
269
+ state["messages"] = state.get("messages", []) + [{
270
+ "role": "ai",
271
+ "content": f"[NewsAnalyzer] I encountered an error while analyzing news: {str(e)}"
272
+ }]
273
+
274
+ return state
agents/portfolio_analyzer.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import numpy as np
3
+ from typing import Dict, Any
4
+ from langchain_openai import ChatOpenAI
5
+ from langchain_core.prompts import ChatPromptTemplate
6
+ from langchain.schema import StrOutputParser
7
+ from .state import AgentState
8
+ import os
9
+
10
+ from dotenv import load_dotenv
11
+ load_dotenv()
12
+
13
+ def get_portfolio_data(portfolio):
14
+ """Fetch historical data for portfolio assets."""
15
+ tickers = list(portfolio.keys())
16
+
17
+ # Get historical data for portfolio assets
18
+ historical_data = {}
19
+
20
+ for ticker in tickers:
21
+ try:
22
+ ticker_obj = yf.Ticker(ticker)
23
+ data = ticker_obj.history(period="1y")
24
+ historical_data[ticker] = data
25
+ except Exception as e:
26
+ print(f"Error fetching data for {ticker}: {e}")
27
+
28
+ return historical_data
29
+
30
+ def calculate_portfolio_metrics(portfolio, historical_data):
31
+ """Calculate portfolio metrics."""
32
+ # Calculate portfolio metrics
33
+ total_value = sum(portfolio[ticker]["value"] for ticker in portfolio)
34
+ allocations = {ticker: portfolio[ticker]["value"] / total_value for ticker in portfolio}
35
+
36
+ # Calculate volatility and returns
37
+ returns = {}
38
+ volatility = {}
39
+ for ticker in historical_data:
40
+ if not historical_data[ticker].empty:
41
+ price_data = historical_data[ticker]['Close']
42
+ daily_returns = price_data.pct_change().dropna()
43
+ returns[ticker] = daily_returns.mean() * 252 # Annualized return
44
+ volatility[ticker] = daily_returns.std() * np.sqrt(252) # Annualized volatility
45
+
46
+ return {
47
+ "total_value": total_value,
48
+ "allocations": allocations,
49
+ "returns": returns,
50
+ "volatility": volatility
51
+ }
52
+
53
+ def create_portfolio_analyzer_chain():
54
+ """Create a chain for portfolio analysis using LLM."""
55
+ # Define prompt template
56
+ prompt = ChatPromptTemplate.from_template("""
57
+ You are a portfolio analysis expert. Analyze the following portfolio information and provide insights.
58
+
59
+ Portfolio Data:
60
+ {portfolio_data}
61
+
62
+ Portfolio Metrics:
63
+ {portfolio_metrics}
64
+
65
+ Fundamental Data:
66
+ {fundamental_data}
67
+
68
+ Risk Level: {risk_level}
69
+ Investment Goals: {investment_goals}
70
+
71
+ Provide a comprehensive analysis of the portfolio including:
72
+ 1. Overall portfolio composition and diversification
73
+ 2. Risk assessment based on volatility and beta
74
+ 3. Valuation assessment based on fundamental metrics
75
+ 4. Alignment with the user's risk level and investment goals
76
+ 5. Areas of concern or potential improvement
77
+
78
+ Your analysis should be detailed, insightful, and actionable.
79
+ """)
80
+ openai_api_key = os.getenv("OPENAI_API_KEY")
81
+ # Create LLM
82
+ llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2, api_key=openai_api_key)
83
+
84
+ # Create chain
85
+ chain = prompt | llm | StrOutputParser()
86
+
87
+ return chain
88
+
89
+ # Initialize portfolio analyzer chain
90
+ portfolio_analyzer_chain = create_portfolio_analyzer_chain()
91
+
92
+ def portfolio_analyzer(state: AgentState) -> AgentState:
93
+ """Analyzes the current portfolio composition using LLM."""
94
+ portfolio = state["portfolio_data"]
95
+ risk_level = state["risk_level"]
96
+ investment_goals = state["investment_goals"]
97
+
98
+ # Get portfolio data
99
+ historical_data = get_portfolio_data(portfolio)
100
+
101
+ # Calculate portfolio metrics
102
+ portfolio_metrics = calculate_portfolio_metrics(portfolio, historical_data)
103
+
104
+ # Format data for LLM
105
+ portfolio_data_str = ""
106
+ for ticker, data in portfolio.items():
107
+ portfolio_data_str += f"{ticker}: {data['shares']} shares at ${data['purchase_price']:.2f}, "
108
+ portfolio_data_str += f"current value: ${data['value']:.2f}, "
109
+ portfolio_data_str += f"gain/loss: {data['gain_loss_pct']:.2f}%\n"
110
+
111
+ # Get fundamental data from technical analysis if available
112
+ fundamental_data = {}
113
+ if "technical_analysis" in state:
114
+ for ticker, analysis in state["technical_analysis"].items():
115
+ if "fundamental_data" in analysis:
116
+ fundamental_data[ticker] = analysis["fundamental_data"]
117
+
118
+ # Format fundamental data for LLM
119
+ fundamental_data_str = ""
120
+ for ticker, data in fundamental_data.items():
121
+ fundamental_data_str += f"{ticker}:\n"
122
+ for key, value in data.items():
123
+ if value is not None:
124
+ fundamental_data_str += f" {key}: {value}\n"
125
+ fundamental_data_str += "\n"
126
+
127
+ # Format portfolio metrics for LLM
128
+ portfolio_metrics_str = ""
129
+ portfolio_metrics_str += f"Total Value: ${portfolio_metrics['total_value']:.2f}\n\n"
130
+
131
+ portfolio_metrics_str += "Allocations:\n"
132
+ for ticker, allocation in portfolio_metrics['allocations'].items():
133
+ portfolio_metrics_str += f" {ticker}: {allocation*100:.2f}%\n"
134
+
135
+ portfolio_metrics_str += "\nAnnualized Returns:\n"
136
+ for ticker, ret in portfolio_metrics['returns'].items():
137
+ portfolio_metrics_str += f" {ticker}: {ret*100:.2f}%\n"
138
+
139
+ portfolio_metrics_str += "\nAnnualized Volatility:\n"
140
+ for ticker, vol in portfolio_metrics['volatility'].items():
141
+ portfolio_metrics_str += f" {ticker}: {vol*100:.2f}%\n"
142
+
143
+ # Get LLM analysis
144
+ analysis = portfolio_analyzer_chain.invoke({
145
+ "portfolio_data": portfolio_data_str,
146
+ "portfolio_metrics": portfolio_metrics_str,
147
+ "fundamental_data": fundamental_data_str,
148
+ "risk_level": risk_level,
149
+ "investment_goals": investment_goals
150
+ })
151
+
152
+ # Update state
153
+ state["portfolio_analysis"] = {
154
+ "total_value": portfolio_metrics["total_value"],
155
+ "allocations": portfolio_metrics["allocations"],
156
+ "returns": portfolio_metrics["returns"],
157
+ "volatility": portfolio_metrics["volatility"],
158
+ "fundamental_data": fundamental_data,
159
+ "llm_analysis": analysis
160
+ }
161
+
162
+ # Add message to communication
163
+ state["messages"] = state.get("messages", []) + [{
164
+ "role": "ai",
165
+ "content": f"[PortfolioAnalyzer] I've analyzed your portfolio. Here are my findings:\n\n{analysis}"
166
+ }]
167
+
168
+ return state
agents/portfolio_fit_evaluator.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
5
+ from typing import Dict, List
6
+ from .state import AgentState
7
+
8
+ def create_portfolio_fit_evaluator():
9
+ """Create a portfolio fit evaluator using LangChain."""
10
+ # Define the function schema for structured output
11
+ function_def = {
12
+ "name": "evaluate_portfolio_fit",
13
+ "description": "Evaluate how new stocks fit into the existing portfolio",
14
+ "parameters": {
15
+ "type": "object",
16
+ "properties": {
17
+ "portfolio_fit_summary": {
18
+ "type": "string",
19
+ "description": "Summary of how the new stocks fit into the existing portfolio"
20
+ },
21
+ "evaluated_stocks": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "properties": {
26
+ "ticker": {
27
+ "type": "string",
28
+ "description": "Stock ticker symbol"
29
+ },
30
+ "portfolio_fit_score": {
31
+ "type": "integer",
32
+ "description": "Score from 1-10 indicating how well the stock fits in the portfolio (10 being best)",
33
+ "minimum": 1,
34
+ "maximum": 10
35
+ },
36
+ "diversification_impact": {
37
+ "type": "string",
38
+ "description": "How this stock would impact portfolio diversification"
39
+ },
40
+ "risk_impact": {
41
+ "type": "string",
42
+ "description": "How this stock would impact portfolio risk"
43
+ },
44
+ "sector_balance": {
45
+ "type": "string",
46
+ "description": "How this stock would affect sector balance in the portfolio"
47
+ },
48
+ "recommendation": {
49
+ "type": "string",
50
+ "enum": ["STRONG_FIT", "MODERATE_FIT", "POOR_FIT"],
51
+ "description": "Overall recommendation for portfolio fit"
52
+ }
53
+ },
54
+ "required": ["ticker", "portfolio_fit_score", "diversification_impact", "risk_impact", "sector_balance", "recommendation"]
55
+ }
56
+ }
57
+ },
58
+ "required": ["portfolio_fit_summary", "evaluated_stocks"]
59
+ }
60
+ }
61
+
62
+ # Create the prompt template
63
+ prompt = ChatPromptTemplate.from_messages([
64
+ ("system", """You are an expert portfolio manager specializing in evaluating how new investments fit into existing portfolios.
65
+
66
+ Your task is to analyze new high-ranked stocks and evaluate how well they would fit into the user's existing portfolio.
67
+
68
+ Consider the following when evaluating portfolio fit:
69
+ 1. Diversification across sectors, industries, and asset classes
70
+ 2. Risk profile and how it aligns with the user's risk tolerance
71
+ 3. Correlation with existing holdings
72
+ 4. Impact on overall portfolio performance
73
+ 5. Balance between growth and value investments
74
+ 6. Geographic diversification if applicable
75
+
76
+ Provide a detailed evaluation of each stock's fit within the portfolio context.
77
+ """),
78
+ ("human", """
79
+ I need to evaluate how these new high-ranked stocks would fit into my existing portfolio:
80
+
81
+ User's Portfolio:
82
+ {portfolio_data}
83
+
84
+ User's Risk Tolerance: {risk_level}
85
+ User's Investment Goals: {investment_goals}
86
+
87
+ Technical Analysis of Existing Portfolio:
88
+ {technical_analysis}
89
+
90
+ New High-Ranked Stocks Analysis:
91
+ {new_stock_analysis}
92
+
93
+ Please evaluate how each new stock would fit into my existing portfolio.
94
+ """)
95
+ ])
96
+
97
+ # Create the LLM
98
+ llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2, api_key=os.getenv("OPENAI_API_KEY"))
99
+
100
+ # Create the structured output chain
101
+ chain = prompt | llm.bind_functions(functions=[function_def], function_call={"name": "evaluate_portfolio_fit"}) | JsonOutputFunctionsParser()
102
+
103
+ return chain
104
+
105
+ def portfolio_fit_evaluator(state: AgentState) -> AgentState:
106
+ """Evaluates how new high-ranked stocks fit into the existing portfolio context."""
107
+ try:
108
+ # Get the portfolio data
109
+ portfolio = state["portfolio_data"]
110
+
111
+ # Get the technical analysis of the existing portfolio
112
+ technical_analysis = state.get("technical_analysis", {})
113
+
114
+ # Get the analysis of new stocks
115
+ new_stock_analysis = state.get("new_stock_analysis", {})
116
+
117
+ # If no new stocks to evaluate, return early
118
+ if not new_stock_analysis:
119
+ state["messages"] = state.get("messages", []) + [{
120
+ "role": "ai",
121
+ "content": "[PortfolioFitEvaluator] No new stocks to evaluate for portfolio fit."
122
+ }]
123
+ state["portfolio_fit"] = {
124
+ "portfolio_fit_summary": "No new stocks to evaluate.",
125
+ "evaluated_stocks": []
126
+ }
127
+ return state
128
+
129
+ # Get user preferences
130
+ risk_level = state.get("risk_level", 5)
131
+ investment_goals = state.get("investment_goals", "Growth")
132
+
133
+ # Create the evaluator
134
+ evaluator = create_portfolio_fit_evaluator()
135
+
136
+ # Generate evaluation
137
+ result = evaluator.invoke({
138
+ "portfolio_data": portfolio,
139
+ "risk_level": risk_level,
140
+ "investment_goals": investment_goals,
141
+ "technical_analysis": technical_analysis,
142
+ "new_stock_analysis": new_stock_analysis
143
+ })
144
+
145
+ # Ensure we have the required fields
146
+ if "portfolio_fit_summary" not in result:
147
+ result["portfolio_fit_summary"] = f"Evaluation of {len(new_stock_analysis)} new stocks for portfolio fit."
148
+
149
+ if "evaluated_stocks" not in result:
150
+ result["evaluated_stocks"] = []
151
+
152
+ except Exception as e:
153
+ # Handle any errors in the evaluation
154
+ state["messages"] = state.get("messages", []) + [{
155
+ "role": "ai",
156
+ "content": f"[PortfolioFitEvaluator] Error evaluating portfolio fit: {str(e)}"
157
+ }]
158
+
159
+ result = {
160
+ "portfolio_fit_summary": "Error in evaluation",
161
+ "evaluated_stocks": []
162
+ }
163
+
164
+ # Update state with portfolio fit evaluation
165
+ state["portfolio_fit"] = result
166
+
167
+ # Add message to communication
168
+ state["messages"] = state.get("messages", []) + [{
169
+ "role": "ai",
170
+ "content": f"[PortfolioFitEvaluator] I've evaluated how {len(result.get('evaluated_stocks', []))} new stocks would fit into your existing portfolio."
171
+ }]
172
+
173
+ return state
agents/rag_analyzer.py ADDED
@@ -0,0 +1,309 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from langchain_openai import ChatOpenAI
2
+ from langchain_core.prompts import ChatPromptTemplate
3
+ from langchain.schema import StrOutputParser
4
+ from operator import itemgetter
5
+ from langchain_community.embeddings import HuggingFaceEmbeddings
6
+ from langchain_community.vectorstores import Chroma
7
+ from .state import AgentState
8
+ import os
9
+
10
+ # Initialize embedding model
11
+ embeddings = HuggingFaceEmbeddings(model_name="Snowflake/snowflake-arctic-embed-l")
12
+
13
+ # Initialize Vector DB
14
+ vector_db = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
15
+
16
+ # Define RAG prompt template for technical analysis interpretation
17
+ TECHNICAL_RAG_PROMPT = """
18
+ CONTEXT:
19
+ {context}
20
+
21
+ TECHNICAL INDICATORS:
22
+ {technical_data}
23
+
24
+ USER PORTFOLIO INFORMATION:
25
+ Risk Level: {risk_level}
26
+ Investment Goals: {investment_goals}
27
+
28
+ You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on the stock.
29
+
30
+ For each stock, analyze:
31
+ 1. What the technical indicators suggest about current market sentiment and potential price movements
32
+ 2. How the fundamental data aligns with value investing principles
33
+ 3. Whether the stock appears to be undervalued, fairly valued, or overvalued
34
+ 4. How this stock fits within the user's risk profile and investment goals
35
+
36
+ Provide a balanced analysis that considers both technical signals and fundamental principles.
37
+ """
38
+
39
+ technical_rag_prompt = ChatPromptTemplate.from_template(TECHNICAL_RAG_PROMPT)
40
+
41
+ # Define RAG prompt template for portfolio insights
42
+ PORTFOLIO_RAG_PROMPT = """
43
+ CONTEXT:
44
+ {context}
45
+
46
+ USER PORTFOLIO INFORMATION:
47
+ Risk Level: {risk_level}
48
+ Investment Goals: {investment_goals}
49
+ Portfolio: {portfolio}
50
+
51
+ You are a financial advisor with expertise in fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd to provide insights on the user's portfolio and investment strategy.
52
+ Focus on applying fundamental analysis principles to the user's specific situation.
53
+
54
+ Answer the following questions:
55
+ 1. What would Graham and Dodd recommend for this investor with their risk level and goals?
56
+ 2. What fundamental analysis principles should be applied to this portfolio?
57
+ 3. How should this investor think about value investing in today's market?
58
+
59
+ Provide your answers in a clear, concise format.
60
+ """
61
+
62
+ portfolio_rag_prompt = ChatPromptTemplate.from_template(PORTFOLIO_RAG_PROMPT)
63
+
64
+ # Define a batch analysis prompt for multiple stocks
65
+ BATCH_ANALYSIS_PROMPT = """
66
+ CONTEXT:
67
+ {context}
68
+
69
+ USER PORTFOLIO INFORMATION:
70
+ Risk Level: {risk_level}
71
+ Investment Goals: {investment_goals}
72
+
73
+ STOCKS TO ANALYZE:
74
+ {stocks_data}
75
+
76
+ You are a financial advisor with expertise in both technical and fundamental analysis. Use the available context from "Security Analysis" by Graham and Dodd along with the technical indicators to provide insights on each stock.
77
+
78
+ For each stock, provide a brief analysis that includes:
79
+ 1. What the technical indicators suggest about current market sentiment
80
+ 2. Whether the stock appears to be undervalued, fairly valued, or overvalued
81
+ 3. How this stock fits within the user's risk profile and investment goals
82
+
83
+ Format your response as a JSON-like structure with ticker symbols as keys and analysis as values.
84
+ """
85
+
86
+ batch_analysis_prompt = ChatPromptTemplate.from_template(BATCH_ANALYSIS_PROMPT)
87
+
88
+ def setup_technical_rag_chain():
89
+ """Set up the RAG chain for technical analysis interpretation."""
90
+ # Initialize LLM - using a faster model for individual stock analysis
91
+ llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.2)
92
+
93
+ # Create retriever
94
+ retriever = vector_db.as_retriever(search_kwargs={"k": 3})
95
+
96
+ # Create RAG chain for technical analysis
97
+ technical_rag_chain = (
98
+ {
99
+ "context": lambda x: retriever.invoke("technical analysis indicators interpretation value investing"),
100
+ "technical_data": itemgetter("technical_data"),
101
+ "risk_level": itemgetter("risk_level"),
102
+ "investment_goals": itemgetter("investment_goals")
103
+ }
104
+ | technical_rag_prompt
105
+ | llm
106
+ | StrOutputParser()
107
+ )
108
+
109
+ return technical_rag_chain
110
+
111
+ def setup_portfolio_rag_chain():
112
+ """Set up the RAG chain for portfolio insights."""
113
+ # Initialize LLM - using GPT-4o-mini for better performance while maintaining quality
114
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
115
+
116
+ # Create retriever
117
+ retriever = vector_db.as_retriever(search_kwargs={"k": 5})
118
+
119
+ # Create RAG chain for portfolio insights
120
+ portfolio_rag_chain = (
121
+ {
122
+ "context": lambda x: retriever.invoke("value investing portfolio analysis Graham Dodd"),
123
+ "risk_level": itemgetter("risk_level"),
124
+ "investment_goals": itemgetter("investment_goals"),
125
+ "portfolio": itemgetter("portfolio")
126
+ }
127
+ | portfolio_rag_prompt
128
+ | llm
129
+ | StrOutputParser()
130
+ )
131
+
132
+ return portfolio_rag_chain
133
+
134
+ def setup_batch_analysis_chain():
135
+ """Set up the RAG chain for batch stock analysis."""
136
+ # Initialize LLM - using GPT-4o-mini for better performance while maintaining quality
137
+ llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2)
138
+
139
+ # Create retriever
140
+ retriever = vector_db.as_retriever(search_kwargs={"k": 4})
141
+
142
+ # Create RAG chain for batch analysis
143
+ batch_analysis_chain = (
144
+ {
145
+ "context": lambda x: retriever.invoke("technical analysis fundamental analysis value investing"),
146
+ "stocks_data": itemgetter("stocks_data"),
147
+ "risk_level": itemgetter("risk_level"),
148
+ "investment_goals": itemgetter("investment_goals")
149
+ }
150
+ | batch_analysis_prompt
151
+ | llm
152
+ | StrOutputParser()
153
+ )
154
+
155
+ return batch_analysis_chain
156
+
157
+ # Initialize the RAG chains
158
+ technical_rag_chain = setup_technical_rag_chain()
159
+ portfolio_rag_chain = setup_portfolio_rag_chain()
160
+ batch_analysis_chain = setup_batch_analysis_chain()
161
+
162
+ def get_stock_interpretation(technical_data_str, risk_level, investment_goals):
163
+ """Get RAG-based interpretation for a stock's technical data."""
164
+ return technical_rag_chain.invoke({
165
+ "technical_data": technical_data_str,
166
+ "risk_level": risk_level,
167
+ "investment_goals": investment_goals
168
+ })
169
+
170
+ def get_portfolio_insights(portfolio_str, risk_level, investment_goals):
171
+ """Get RAG-based insights for a portfolio."""
172
+ return portfolio_rag_chain.invoke({
173
+ "risk_level": risk_level,
174
+ "investment_goals": investment_goals,
175
+ "portfolio": portfolio_str
176
+ })
177
+
178
+ def rag_analyzer(state: AgentState) -> AgentState:
179
+ """Performs RAG-based analysis on technical data and portfolio."""
180
+ portfolio = state["portfolio_data"]
181
+ risk_level = state["risk_level"]
182
+ investment_goals = state["investment_goals"]
183
+ technical_analysis = state.get("technical_analysis", {})
184
+
185
+ # Format portfolio for RAG
186
+ portfolio_str = ""
187
+ for ticker, data in portfolio.items():
188
+ portfolio_str += f"{ticker}: {data['shares']} shares at ${data['purchase_price']:.2f}, "
189
+ portfolio_str += f"current value: ${data['value']:.2f}, "
190
+ portfolio_str += f"gain/loss: {data['gain_loss_pct']:.2f}%\n"
191
+
192
+ # Generate portfolio-level insights using RAG (single call instead of multiple)
193
+ portfolio_insights_text = get_portfolio_insights(
194
+ portfolio_str,
195
+ risk_level,
196
+ investment_goals
197
+ )
198
+
199
+ # Parse the portfolio insights into structured format
200
+ lines = portfolio_insights_text.strip().split('\n')
201
+ portfolio_insights = []
202
+
203
+ # Extract questions and answers from the response
204
+ current_question = None
205
+ current_answer = []
206
+
207
+ for line in lines:
208
+ if line.startswith('1.') or line.startswith('2.') or line.startswith('3.'):
209
+ # If we have a previous question, save it
210
+ if current_question is not None:
211
+ portfolio_insights.append({
212
+ "question": current_question,
213
+ "response": '\n'.join(current_answer).strip()
214
+ })
215
+ current_answer = []
216
+
217
+ # Extract new question
218
+ parts = line.split(':', 1)
219
+ if len(parts) > 1:
220
+ current_question = parts[0].strip()
221
+ current_answer.append(parts[1].strip())
222
+ else:
223
+ current_question = line.strip()
224
+ elif current_question is not None:
225
+ current_answer.append(line)
226
+
227
+ # Add the last question/answer if exists
228
+ if current_question is not None and current_answer:
229
+ portfolio_insights.append({
230
+ "question": current_question,
231
+ "response": '\n'.join(current_answer).strip()
232
+ })
233
+
234
+ # If we couldn't parse properly, create a fallback structure
235
+ if not portfolio_insights:
236
+ portfolio_insights = [
237
+ {"question": "Portfolio Analysis", "response": portfolio_insights_text}
238
+ ]
239
+
240
+ # Only process technical analysis if there are stocks to analyze
241
+ if technical_analysis:
242
+ # Prepare batch analysis data instead of individual calls
243
+ stocks_data = ""
244
+ for ticker, data in technical_analysis.items():
245
+ if "error" not in data:
246
+ stocks_data += f"Stock: {ticker}\n"
247
+ stocks_data += f"Current Price: ${data['current_price']:.2f}\n"
248
+ stocks_data += "Technical Indicators:\n"
249
+ for key, value in data['technical_indicators'].items():
250
+ stocks_data += f" {key}: {value}\n"
251
+ stocks_data += "Fundamental Data:\n"
252
+ for key, value in data['fundamental_data'].items():
253
+ if value is not None:
254
+ stocks_data += f" {key}: {value}\n"
255
+ stocks_data += "\n---\n\n"
256
+
257
+ # Only make the batch analysis call if we have stocks to analyze
258
+ if stocks_data:
259
+ try:
260
+ # Get batch analysis for all stocks in one call
261
+ batch_analysis_result = batch_analysis_chain.invoke({
262
+ "stocks_data": stocks_data,
263
+ "risk_level": risk_level,
264
+ "investment_goals": investment_goals
265
+ })
266
+
267
+ # Parse the batch results and update technical_analysis
268
+ # This is a simplified parsing - in production you might want more robust parsing
269
+ current_ticker = None
270
+ current_analysis = []
271
+
272
+ for line in batch_analysis_result.split('\n'):
273
+ if ':' in line and line.split(':')[0].strip() in technical_analysis:
274
+ # If we have a previous ticker, save its analysis
275
+ if current_ticker is not None and current_ticker in technical_analysis:
276
+ technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
277
+ current_analysis = []
278
+
279
+ # Start new ticker
280
+ current_ticker = line.split(':')[0].strip()
281
+ current_analysis.append(line.split(':', 1)[1].strip())
282
+ elif current_ticker is not None:
283
+ current_analysis.append(line)
284
+
285
+ # Add the last ticker's analysis
286
+ if current_ticker is not None and current_ticker in technical_analysis:
287
+ technical_analysis[current_ticker]["rag_interpretation"] = '\n'.join(current_analysis).strip()
288
+ except Exception as e:
289
+ # Fallback to a simpler approach if batch processing fails
290
+ for ticker, data in technical_analysis.items():
291
+ if "error" not in data:
292
+ technical_analysis[ticker]["rag_interpretation"] = f"Analysis unavailable due to processing error: {str(e)}"
293
+
294
+ # Combine portfolio insights
295
+ combined_insights = "\n\n".join([f"Q: {insight['question']}\nA: {insight['response']}" for insight in portfolio_insights])
296
+
297
+ # Update state with RAG insights
298
+ state["technical_analysis"] = technical_analysis
299
+ state["rag_context"] = combined_insights
300
+ state["fundamental_analysis"] = state.get("fundamental_analysis", {})
301
+ state["fundamental_analysis"]["security_analysis_insights"] = portfolio_insights
302
+
303
+ # Add message to communication
304
+ state["messages"] = state.get("messages", []) + [{
305
+ "role": "ai",
306
+ "content": f"[RAGAnalyzer] I've provided insights on your portfolio and individual stocks based on value investing principles from Security Analysis by Graham and Dodd."
307
+ }]
308
+
309
+ return state
agents/recommendation_engine.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from langchain_openai import ChatOpenAI
3
+ from langchain_core.prompts import ChatPromptTemplate
4
+ from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
5
+ from typing import Dict, List
6
+ from .state import AgentState
7
+
8
+ def create_recommendation_engine():
9
+ """Create a recommendation engine using LangChain."""
10
+ # Define the function schema for structured output
11
+ function_def = {
12
+ "name": "generate_recommendations",
13
+ "description": "Generate investment recommendations based on portfolio analysis",
14
+ "parameters": {
15
+ "type": "object",
16
+ "properties": {
17
+ "portfolio_summary": {
18
+ "type": "string",
19
+ "description": "Summary of the current portfolio status"
20
+ },
21
+ "recommendations": {
22
+ "type": "array",
23
+ "items": {
24
+ "type": "object",
25
+ "properties": {
26
+ "ticker": {
27
+ "type": "string",
28
+ "description": "Stock ticker symbol"
29
+ },
30
+ "action": {
31
+ "type": "string",
32
+ "enum": ["BUY", "SELL", "HOLD"],
33
+ "description": "Recommended action for this stock"
34
+ },
35
+ "reasoning": {
36
+ "type": "string",
37
+ "description": "Detailed reasoning for the recommendation"
38
+ },
39
+ "priority": {
40
+ "type": "integer",
41
+ "description": "Priority level (1-5, where 1 is highest priority)",
42
+ "minimum": 1,
43
+ "maximum": 5
44
+ }
45
+ },
46
+ "required": ["ticker", "action", "reasoning", "priority"]
47
+ }
48
+ },
49
+ "portfolio_strengths": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "string",
53
+ "description": "Key strength of the portfolio"
54
+ },
55
+ "description": "List of portfolio strengths"
56
+ },
57
+ "portfolio_weaknesses": {
58
+ "type": "array",
59
+ "items": {
60
+ "type": "string",
61
+ "description": "Key weakness or area for improvement in the portfolio"
62
+ },
63
+ "description": "List of portfolio weaknesses or areas for improvement"
64
+ },
65
+ "allocation_advice": {
66
+ "type": "string",
67
+ "description": "Advice on portfolio allocation and diversification"
68
+ },
69
+ "risk_assessment": {
70
+ "type": "string",
71
+ "description": "Assessment of portfolio risk relative to user's risk tolerance"
72
+ },
73
+ "final_report": {
74
+ "type": "string",
75
+ "description": "Comprehensive final report with all recommendations and analysis"
76
+ }
77
+ },
78
+ "required": ["portfolio_summary", "recommendations", "portfolio_strengths", "portfolio_weaknesses", "allocation_advice", "risk_assessment", "final_report"]
79
+ }
80
+ }
81
+
82
+ # Create prompt template
83
+ prompt = ChatPromptTemplate.from_template("""
84
+ You are an expert financial advisor. Based on the following information, provide personalized investment recommendations.
85
+
86
+ PORTFOLIO:
87
+ {portfolio}
88
+
89
+ RISK PROFILE:
90
+ {risk_level}
91
+
92
+ INVESTMENT GOALS:
93
+ {investment_goals}
94
+
95
+ TECHNICAL ANALYSIS:
96
+ {technical_analysis}
97
+
98
+ FUNDAMENTAL DATA:
99
+ {fundamental_data}
100
+
101
+ RELEVANT NEWS:
102
+ {news}
103
+
104
+ SECURITY ANALYSIS INSIGHTS (Graham & Dodd):
105
+ {rag_insights}
106
+
107
+ AGENT DISCUSSIONS:
108
+ {agent_discussions}
109
+
110
+ Provide comprehensive investment recommendations that incorporate:
111
+ 1. Technical analysis signals (RSI, moving averages, etc.)
112
+ 2. Fundamental analysis metrics (PE ratios, debt-to-equity, growth rates, etc.)
113
+ 3. Recent news sentiment and impact
114
+ 4. Value investing principles from Graham & Dodd's Security Analysis
115
+ 5. Alignment with the user's risk profile and investment goals
116
+
117
+ For each holding, provide a clear BUY/SELL/HOLD recommendation with detailed reasoning and priority level.
118
+
119
+ IMPORTANT: For the reasoning of each recommendation, include a more detailed technical and fundamental analysis section:
120
+ - For technical analysis: Include specific insights about RSI levels, MACD signals, moving average crossovers, and what these indicators suggest about momentum and trend direction.
121
+ - For fundamental analysis: Include specific metrics like P/E ratio compared to industry average, debt-to-equity ratio, earnings growth, and what these metrics suggest about the company's valuation and financial health.
122
+ - Use technical jargon appropriately to demonstrate expertise while still being clear.
123
+ - Format the reasoning to clearly separate technical insights from fundamental insights.
124
+
125
+ Identify 3-5 key strengths and weaknesses of the current portfolio.
126
+ Assess the overall risk and provide allocation advice.
127
+
128
+ Remember that your recommendations will be presented to a busy executive who needs concise, actionable insights.
129
+ Focus on clarity and brevity in your explanations.
130
+ """)
131
+
132
+ # Create LLM
133
+ llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2)
134
+
135
+ # Create chain
136
+ chain = (
137
+ prompt
138
+ | llm.bind_functions(functions=[function_def], function_call="generate_recommendations")
139
+ | JsonOutputFunctionsParser()
140
+ )
141
+
142
+ return chain
143
+
144
+ # Initialize recommendation engine
145
+ recommendation_engine_chain = create_recommendation_engine()
146
+
147
+ def recommendation_engine(state: AgentState) -> AgentState:
148
+ """Generates investment recommendations based on all analyses including RAG insights."""
149
+ portfolio = state["portfolio_data"]
150
+ risk_level = state["risk_level"]
151
+ investment_goals = state["investment_goals"]
152
+ tech_analysis = state.get("technical_analysis", {})
153
+ news = state.get("news_analysis", [])
154
+ rag_insights = state.get("rag_context", "No RAG insights available.")
155
+ messages = state.get("messages", [])
156
+ fundamental_analysis = state.get("fundamental_analysis", {})
157
+
158
+ # Extract fundamental data and RAG interpretations from technical analysis
159
+ fundamental_data = {}
160
+ rag_interpretations = {}
161
+
162
+ for ticker in tech_analysis:
163
+ # Extract fundamental data
164
+ if isinstance(tech_analysis[ticker], dict):
165
+ if 'fundamental_data' in tech_analysis[ticker]:
166
+ fundamental_data[ticker] = tech_analysis[ticker]['fundamental_data']
167
+
168
+ # Extract RAG interpretations
169
+ if 'rag_interpretation' in tech_analysis[ticker]:
170
+ rag_interpretations[ticker] = tech_analysis[ticker]['rag_interpretation']
171
+
172
+ # Combine RAG interpretations with portfolio-level insights
173
+ combined_rag_insights = rag_insights
174
+ if rag_interpretations:
175
+ combined_rag_insights += "\n\n## Stock-Specific Interpretations:\n\n"
176
+ for ticker, interpretation in rag_interpretations.items():
177
+ combined_rag_insights += f"### {ticker}:\n{interpretation}\n\n"
178
+
179
+ # Format agent discussions
180
+ agent_discussions = "\n\n".join([f"{msg['role']}: {msg['content']}" for msg in messages])
181
+
182
+ # Invoke recommendation engine
183
+ try:
184
+ result = recommendation_engine_chain.invoke({
185
+ "portfolio": str(portfolio),
186
+ "risk_level": risk_level,
187
+ "investment_goals": investment_goals,
188
+ "technical_analysis": str(tech_analysis),
189
+ "fundamental_data": str(fundamental_data),
190
+ "news": str(news),
191
+ "rag_insights": combined_rag_insights,
192
+ "agent_discussions": agent_discussions
193
+ })
194
+
195
+ # Ensure we have all required fields
196
+ if "recommendations" not in result or not result["recommendations"]:
197
+ # Generate default recommendations if none were provided
198
+ result["recommendations"] = []
199
+ for ticker in portfolio:
200
+ result["recommendations"].append({
201
+ "ticker": ticker,
202
+ "action": "HOLD",
203
+ "reasoning": "Default recommendation due to insufficient analysis data.",
204
+ "priority": 3
205
+ })
206
+
207
+ if "final_report" not in result or not result["final_report"]:
208
+ # Generate a default final report
209
+ result["final_report"] = f"""
210
+ # Investment Portfolio Analysis Report
211
+
212
+ ## Portfolio Overview
213
+ Risk Level: {risk_level}
214
+ Investment Goals: {investment_goals}
215
+
216
+ ## Key Recommendations
217
+ {', '.join([f"{rec['ticker']}: {rec['action']}" for rec in result.get("recommendations", [])])}
218
+
219
+ ## Summary
220
+ This is a default report generated due to insufficient analysis data.
221
+ """
222
+
223
+ # Ensure other fields exist
224
+ if "portfolio_strengths" not in result:
225
+ result["portfolio_strengths"] = ["Diversification across multiple assets"]
226
+
227
+ if "portfolio_weaknesses" not in result:
228
+ result["portfolio_weaknesses"] = ["Potential for improved sector allocation"]
229
+
230
+ if "allocation_advice" not in result:
231
+ result["allocation_advice"] = "Consider maintaining a balanced portfolio aligned with your risk tolerance."
232
+
233
+ if "risk_assessment" not in result:
234
+ result["risk_assessment"] = f"Your portfolio appears to be aligned with your {risk_level} risk tolerance."
235
+
236
+ if "portfolio_summary" not in result:
237
+ result["portfolio_summary"] = f"Portfolio with {len(portfolio)} assets analyzed."
238
+
239
+ except Exception as e:
240
+ # Handle any errors in the recommendation engine
241
+ print(f"Error in recommendation engine: {str(e)}")
242
+ result = {
243
+ "recommendations": [],
244
+ "final_report": f"Error generating recommendations: {str(e)}",
245
+ "portfolio_strengths": [],
246
+ "portfolio_weaknesses": [],
247
+ "allocation_advice": "",
248
+ "risk_assessment": "",
249
+ "portfolio_summary": "Error in analysis"
250
+ }
251
+
252
+ # Update state with recommendations and enhanced data
253
+ state["recommendations"] = result["recommendations"]
254
+ state["final_report"] = result["final_report"]
255
+
256
+ # Add new structured data to state
257
+ state["portfolio_strengths"] = result.get("portfolio_strengths", [])
258
+ state["portfolio_weaknesses"] = result.get("portfolio_weaknesses", [])
259
+ state["allocation_advice"] = result.get("allocation_advice", "")
260
+ state["risk_assessment"] = result.get("risk_assessment", "")
261
+
262
+ # Add message to communication
263
+ state["messages"] = state.get("messages", []) + [{
264
+ "role": "ai",
265
+ "content": f"[RecommendationEngine] I've generated the following recommendations:\n\n{result.get('portfolio_summary', '')}\n\n"
266
+ f"Risk Assessment: {result.get('risk_assessment', '')}\n\n"
267
+ f"Allocation Advice: {result.get('allocation_advice', '')}"
268
+ }]
269
+
270
+ return state
agents/state.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import TypedDict, Dict, Any, List, Optional, Annotated
2
+ import operator
3
+
4
+ # Define state types for LangGraph
5
+ class AgentState(TypedDict):
6
+ # Input data
7
+ portfolio_data: Dict[str, Any]
8
+ risk_level: str
9
+ investment_goals: str
10
+
11
+ # Analysis data
12
+ technical_analysis: Dict[str, Any]
13
+ news_analysis: List[Dict[str, Any]]
14
+ fundamental_analysis: Dict[str, Any]
15
+
16
+ # RAG data
17
+ rag_context: Optional[str]
18
+
19
+ # Agent communication
20
+ messages: Annotated[List[Dict[str, Any]], operator.add]
21
+
22
+ # Output data
23
+ recommendations: List[Dict[str, str]]
24
+ portfolio_strengths: List[str]
25
+ portfolio_weaknesses: List[str]
26
+ new_investments: List[Dict[str, Any]]
27
+ allocation_advice: str
28
+ risk_assessment: str
29
+ final_report: str
30
+
31
+
32
+ # Define state types for LangGraph
33
+ class AgentState2(TypedDict):
34
+ # Input data
35
+ portfolio_data: Dict[str, Any]
36
+ risk_level: str
37
+ investment_goals: str
38
+
39
+ # Analysis data
40
+ technical_analysis: Dict[str, Any]
41
+ news_analysis: List[Dict[str, Any]]
42
+ fundamental_analysis: Dict[str, Any]
43
+
44
+ # New investment workflow data
45
+ high_rank_stocks: List[Dict[str, Any]]
46
+ new_stock_analysis: Dict[str, Any]
47
+ portfolio_fit: Dict[str, Any]
48
+
49
+ # RAG data
50
+ rag_context: Optional[str]
51
+
52
+ # Agent communication
53
+ messages: Annotated[List[Dict[str, Any]], operator.add]
54
+
55
+ # Output data
56
+ recommendations: List[Dict[str, str]]
57
+ portfolio_strengths: List[str]
58
+ portfolio_weaknesses: List[str]
59
+ new_investments: List[Dict[str, Any]]
60
+ new_investment_summary: str
61
+ allocation_advice: str
62
+ risk_assessment: str
63
+ final_report: str
agents/technical_analyzer.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import numpy as np
3
+ import pandas as pd
4
+ from typing import Dict, Any, List
5
+ from .state import AgentState
6
+
7
+ def calculate_rsi(prices, period=14):
8
+ """Calculate Relative Strength Index."""
9
+ # Calculate price changes
10
+ delta = prices.diff()
11
+
12
+ # Separate gains and losses
13
+ gain = delta.clip(lower=0)
14
+ loss = -delta.clip(upper=0)
15
+
16
+ # Calculate average gain and loss
17
+ avg_gain = gain.rolling(window=period).mean()
18
+ avg_loss = loss.rolling(window=period).mean()
19
+
20
+ # Calculate relative strength (RS)
21
+ rs = avg_gain / avg_loss
22
+
23
+ # Calculate RSI
24
+ rsi = 100 - (100 / (1 + rs))
25
+
26
+ return rsi
27
+
28
+ def calculate_macd(prices, fast=12, slow=26, signal=9):
29
+ """Calculate Moving Average Convergence Divergence."""
30
+ # Calculate EMAs
31
+ ema_fast = prices.ewm(span=fast, adjust=False).mean()
32
+ ema_slow = prices.ewm(span=slow, adjust=False).mean()
33
+
34
+ # Calculate MACD line
35
+ macd_line = ema_fast - ema_slow
36
+
37
+ # Calculate signal line
38
+ signal_line = macd_line.ewm(span=signal, adjust=False).mean()
39
+
40
+ # Calculate histogram
41
+ histogram = macd_line - signal_line
42
+
43
+ return {
44
+ 'macd_line': macd_line,
45
+ 'signal_line': signal_line,
46
+ 'histogram': histogram
47
+ }
48
+
49
+ def get_technical_indicators(ticker_obj, hist):
50
+ """Calculate technical indicators for a stock without making judgments."""
51
+ if hist.empty:
52
+ return None
53
+
54
+ # Calculate basic technical indicators
55
+ hist['MA20'] = hist['Close'].rolling(window=20).mean()
56
+ hist['MA50'] = hist['Close'].rolling(window=50).mean()
57
+ hist['MA200'] = hist['Close'].rolling(window=200).mean()
58
+ hist['RSI'] = calculate_rsi(hist['Close'])
59
+
60
+ # Calculate MACD
61
+ macd = calculate_macd(hist['Close'])
62
+ hist['MACD_Line'] = macd['macd_line']
63
+ hist['MACD_Signal'] = macd['signal_line']
64
+ hist['MACD_Histogram'] = macd['histogram']
65
+
66
+ # Calculate Bollinger Bands
67
+ hist['BB_Middle'] = hist['Close'].rolling(window=20).mean()
68
+ std = hist['Close'].rolling(window=20).std()
69
+ hist['BB_Upper'] = hist['BB_Middle'] + (std * 2)
70
+ hist['BB_Lower'] = hist['BB_Middle'] - (std * 2)
71
+
72
+ # Get latest values
73
+ latest = hist.iloc[-1]
74
+
75
+ # Get fundamental data
76
+ info = ticker_obj.info
77
+
78
+ return {
79
+ 'current_price': latest['Close'],
80
+ 'technical_indicators': {
81
+ 'ma20': latest['MA20'],
82
+ 'ma50': latest['MA50'],
83
+ 'ma200': latest['MA200'],
84
+ 'rsi': latest['RSI'],
85
+ 'macd_line': latest['MACD_Line'],
86
+ 'macd_signal': latest['MACD_Signal'],
87
+ 'macd_histogram': latest['MACD_Histogram'],
88
+ 'bb_upper': latest['BB_Upper'],
89
+ 'bb_middle': latest['BB_Middle'],
90
+ 'bb_lower': latest['BB_Lower'],
91
+ 'volume': latest['Volume']
92
+ },
93
+ 'fundamental_data': {
94
+ 'symbol': ticker_obj.ticker,
95
+ 'price': info.get('currentPrice'),
96
+ 'pe_ratio': info.get('trailingPE'),
97
+ 'peg_ratio': info.get('pegRatio'),
98
+ 'debt_to_equity': info.get('debtToEquity'),
99
+ 'forward_pe': info.get('forwardPE'),
100
+ 'beta': info.get('beta'),
101
+ 'return_on_equity': info.get('returnOnEquity'),
102
+ 'free_cash_flow': info.get('freeCashflow'),
103
+ 'revenue_growth': info.get('revenueGrowth'),
104
+ 'earnings_growth': info.get('earningsGrowth'),
105
+ 'dividend_yield': info.get('dividendYield'),
106
+ 'market_cap': info.get('marketCap'),
107
+ 'profit_margins': info.get('profitMargins'),
108
+ 'price_to_book': info.get('priceToBook')
109
+ }
110
+ }
111
+
112
+ def technical_analyzer(state: AgentState) -> AgentState:
113
+ """Performs comprehensive technical analysis on portfolio assets."""
114
+ portfolio = state["portfolio_data"]
115
+
116
+ tickers = list(portfolio.keys())
117
+ analysis_results = {}
118
+
119
+ # Collect technical data for all stocks
120
+ for ticker in tickers:
121
+ try:
122
+ # Get stock data
123
+ stock = yf.Ticker(ticker)
124
+ hist = stock.history(period="6mo")
125
+
126
+ if not hist.empty:
127
+ # Get comprehensive technical indicators without judgments
128
+ indicators = get_technical_indicators(stock, hist)
129
+
130
+ if indicators:
131
+ analysis_results[ticker] = indicators
132
+ else:
133
+ analysis_results[ticker] = {"error": "Failed to calculate indicators"}
134
+ else:
135
+ analysis_results[ticker] = {"error": "No historical data available"}
136
+
137
+ except Exception as e:
138
+ print(f"Error analyzing {ticker}: {str(e)}")
139
+ analysis_results[ticker] = {"error": str(e)}
140
+
141
+ # Update state with technical analysis
142
+ state["technical_analysis"] = analysis_results
143
+
144
+ # Add message to communication
145
+ state["messages"] = state.get("messages", []) + [{
146
+ "role": "ai",
147
+ "content": f"[TechnicalAnalyzer] I've calculated technical indicators and gathered fundamental data for your portfolio stocks."
148
+ }]
149
+
150
+ return state
agents/zacks_analyzer.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
4
+ from zacks import find_rank_1_stocks
5
+ from .state import AgentState
6
+
7
+ def zacks_analyzer(state: AgentState) -> AgentState:
8
+ """Fetches high-rank stocks from Zacks for potential new investments."""
9
+ try:
10
+ # Get 5 rank 1 stocks from Zacks
11
+ high_rank_stocks = find_rank_1_stocks(n=15)
12
+
13
+ #TODO if really aggressive use sp400
14
+
15
+ # Store the high-rank stocks in the state
16
+ state["high_rank_stocks"] = high_rank_stocks
17
+
18
+ # Add message to communication
19
+ stock_symbols = [stock['symbol'] for stock in high_rank_stocks]
20
+ state["messages"] = state.get("messages", []) + [{
21
+ "role": "ai",
22
+ "content": f"[ZacksAnalyzer] I've identified {len(high_rank_stocks)} high-ranked stocks from Zacks: {', '.join(stock_symbols)}"
23
+ }]
24
+
25
+ except Exception as e:
26
+ # Handle any errors
27
+ state["messages"] = state.get("messages", []) + [{
28
+ "role": "ai",
29
+ "content": f"[ZacksAnalyzer] Error fetching high-rank stocks: {str(e)}"
30
+ }]
31
+ state["high_rank_stocks"] = []
32
+
33
+ return state
app.py ADDED
@@ -0,0 +1,918 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ from langchain_openai import ChatOpenAI
5
+ from langchain_community.embeddings import HuggingFaceEmbeddings
6
+ from langchain_community.vectorstores import Chroma
7
+ import os
8
+ from dotenv import load_dotenv
9
+ import yfinance as yf
10
+ import matplotlib.pyplot as plt
11
+ import seaborn as sns
12
+
13
+ # Import our agent modules
14
+ from agents import AgentState, AgentState2
15
+ from agents.graph import setup_graph_with_tracking, setup_new_investments_graph
16
+
17
+ # Load environment variables
18
+ load_dotenv()
19
+
20
+ # Configure page
21
+ st.set_page_config(
22
+ page_title="Cardinal ai",
23
+ page_icon="🧭",
24
+ layout="wide"
25
+ )
26
+
27
+ # Initialize OpenAI client
28
+ openai_api_key = os.getenv("OPENAI_API_KEY")
29
+ # llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2, api_key=openai_api_key)
30
+
31
+ # # Initialize embedding model
32
+ # embeddings = HuggingFaceEmbeddings(model_name="Snowflake/snowflake-arctic-embed-l")
33
+
34
+ # # Initialize Vector DB
35
+ # vector_db = Chroma(embedding_function=embeddings, persist_directory="./chroma_db")
36
+
37
+ # Streamlit UI components
38
+ st.image("assets/sunrise.svg")
39
+ # st.title("Asset Atlas - AI Financial Advisor")
40
+ # st.subheader("Research, Summary, and Analysis")
41
+
42
+ # with st.expander("About this app", expanded=False):
43
+ # st.write("""
44
+ # This app provides personalized financial advice based on your portfolio, risk tolerance, and investment goals.
45
+ # It analyzes technical indicators, relevant news, and market trends to offer tailored recommendations.
46
+ # """)
47
+
48
+ # Sidebar for user inputs
49
+ with st.sidebar:
50
+ st.header("Your Profile")
51
+
52
+ # Risk tolerance selection
53
+ risk_level = st.select_slider(
54
+ "Risk Tolerance",
55
+ options=["Very Conservative", "Conservative", "Moderate", "Aggressive", "Very Aggressive"],
56
+ value="Moderate"
57
+ )
58
+
59
+ # Investment goals
60
+ investment_goals = st.text_area("Investment Goals", "Retirement in 20 years, building wealth, passive income")
61
+
62
+ st.markdown("")
63
+
64
+ # Portfolio input
65
+ st.header("Your Portfolio")
66
+
67
+ # Default portfolio for demonstration
68
+ default_portfolio = pd.DataFrame({
69
+ 'Ticker': ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'BRK-B'],
70
+ 'Shares': [10, 5, 3, 2, 4],
71
+ 'Purchase Price': [250.00, 250.00, 250.00, 250.00, 250.00]
72
+ })
73
+
74
+ # Let user edit the portfolio
75
+ portfolio_df = st.data_editor(
76
+ default_portfolio,
77
+ column_config={
78
+ "Ticker": st.column_config.TextColumn("Ticker Symbol"),
79
+ "Shares": st.column_config.NumberColumn("Number of Shares", min_value=0),
80
+ "Purchase Price": st.column_config.NumberColumn("Purchase Price ($)", min_value=0.01, format="$%.2f")
81
+ },
82
+ num_rows="dynamic",
83
+ use_container_width=True
84
+ )
85
+
86
+ generate_button = st.button("Generate Recommendations", type="secondary", use_container_width=True)
87
+
88
+
89
+ # Initialize session state to store our analysis results
90
+ if 'portfolio_analyzed' not in st.session_state:
91
+ st.session_state.portfolio_analyzed = False
92
+
93
+ if 'final_state' not in st.session_state:
94
+ st.session_state.final_state = None
95
+
96
+ if 'portfolio_data' not in st.session_state:
97
+ st.session_state.portfolio_data = None
98
+
99
+ # Main content area
100
+ if generate_button or st.session_state.portfolio_analyzed:
101
+ # If we're here because of the session state, we don't need to re-run the analysis
102
+ if generate_button:
103
+ # Convert portfolio dataframe to required format
104
+ portfolio_data = {}
105
+
106
+ # Create a placeholder for the progress indicator
107
+ progress_placeholder = st.empty()
108
+ status_placeholder = st.empty()
109
+
110
+ # Show a spinner while processing
111
+ with st.spinner("Analyzing your portfolio and generating recommendations..."):
112
+ progress_placeholder.progress(0, "Starting analysis...")
113
+ status_placeholder.info("Fetching current market data...")
114
+
115
+ for _, row in portfolio_df.iterrows():
116
+ ticker = row['Ticker']
117
+ shares = row['Shares']
118
+ purchase_price = row['Purchase Price']
119
+
120
+ try:
121
+ # Get current price
122
+ stock = yf.Ticker(ticker)
123
+ current_price = stock.history(period="1d")['Close'].iloc[-1]
124
+
125
+ # Calculate values
126
+ current_value = current_price * shares
127
+ purchase_value = purchase_price * shares
128
+ gain_loss = current_value - purchase_value
129
+ gain_loss_pct = (gain_loss / purchase_value) * 100
130
+
131
+ # Store in portfolio data
132
+ portfolio_data[ticker] = {
133
+ "shares": shares,
134
+ "purchase_price": purchase_price,
135
+ "current_price": current_price,
136
+ "value": current_value,
137
+ "gain_loss": gain_loss,
138
+ "gain_loss_pct": gain_loss_pct
139
+ }
140
+ except Exception as e:
141
+ st.error(f"Error processing {ticker}: {e}")
142
+
143
+ if len(portfolio_data) == len(portfolio_df):
144
+ st.subheader("Current valuation")
145
+
146
+ # Create a dataframe for portfolio valuation
147
+ valuation_data = []
148
+ total_value = 0
149
+ total_cost = 0
150
+
151
+ for ticker, data in portfolio_data.items():
152
+ current_value = data['value']
153
+ purchase_value = data['purchase_price'] * data['shares']
154
+ total_value += current_value
155
+ total_cost += purchase_value
156
+
157
+ valuation_data.append({
158
+ 'Ticker': ticker,
159
+ 'Shares': data['shares'],
160
+ 'Purchase Price': f"${data['purchase_price']:.2f}",
161
+ 'Current Price': f"${data['current_price']:.2f}",
162
+ 'Cost Basis': f"${purchase_value:.2f}",
163
+ 'Current Value': f"${current_value:.2f}",
164
+ 'Gain/Loss ($)': f"${data['gain_loss']:.2f}",
165
+ 'Gain/Loss (%)': f"{data['gain_loss_pct']:.2f}%"
166
+ })
167
+
168
+ # Calculate total gain/loss
169
+ total_gain_loss = total_value - total_cost
170
+ total_gain_loss_pct = (total_gain_loss / total_cost * 100) if total_cost > 0 else 0
171
+
172
+ # Display the valuation table
173
+ valuation_df = pd.DataFrame(valuation_data)
174
+ st.dataframe(valuation_df, use_container_width=True)
175
+
176
+
177
+ # Display portfolio valuations and allocations with gain/loss
178
+ #if portfolio_data:
179
+
180
+ # Create a better layout with metrics on the left and pie chart on the right
181
+ st.subheader("Performance & allocation summary")
182
+
183
+ # Create two columns for the layout with adjusted ratio
184
+ left_col, right_col = st.columns([1, 1])
185
+
186
+ # Left column: stacked metrics
187
+ with left_col:
188
+ cont_col, gap_col = st.columns([2, 1])
189
+
190
+ with cont_col:
191
+ # Add a bit of vertical space for alignment
192
+ st.write("")
193
+
194
+ # Total Portfolio Value metric
195
+ st.metric("Total Portfolio Value", f"${total_value:.2f}", f"${total_gain_loss:.2f}", border=True)
196
+
197
+ # Add some space between metrics
198
+ st.write("")
199
+
200
+ # Total Cost Basis metric
201
+ st.metric("Total Cost Basis", f"${total_cost:.2f}", border=True)
202
+
203
+ # Add some space between metrics
204
+ st.write("")
205
+
206
+ # Total Return metric
207
+ st.metric("Total Return", f"{total_gain_loss_pct:.2f}%", border=True)
208
+ with gap_col:
209
+ st.markdown("")
210
+
211
+ # Right column: pie chart
212
+ with right_col:
213
+ # Prepare data for pie chart
214
+ st.markdown("")
215
+ allocation_data = {}
216
+ for ticker, data in portfolio_data.items():
217
+ allocation_data[ticker] = data['value']
218
+
219
+ # Create a figure for the pie chart
220
+ fig, ax = plt.subplots(figsize=(4, 3.2))
221
+
222
+ # Create the pie chart
223
+ wedges, texts, autotexts = ax.pie(
224
+ allocation_data.values(),
225
+ labels=allocation_data.keys(),
226
+ autopct='%1.1f%%',
227
+ startangle=90,
228
+ wedgeprops={'edgecolor': 'white'},
229
+ textprops={'fontsize': 8}
230
+ )
231
+
232
+ # Equal aspect ratio ensures that pie is drawn as a circle
233
+ ax.axis('equal')
234
+
235
+ # Manually set font sizes
236
+ plt.setp(autotexts, size=8, weight="bold")
237
+ plt.setp(texts, size=8)
238
+
239
+ # Add a title
240
+ plt.title('Portfolio Allocation by Value', fontsize=10)
241
+
242
+ # Use tight layout
243
+ plt.tight_layout()
244
+
245
+ # Display the pie chart
246
+ st.pyplot(fig)
247
+
248
+ # Initialize state
249
+ initial_state = AgentState(
250
+ portfolio_data=portfolio_data,
251
+ risk_level=risk_level,
252
+ investment_goals=investment_goals,
253
+ technical_analysis={},
254
+ news_analysis=[],
255
+ fundamental_analysis={},
256
+ rag_context=None,
257
+ messages=[{
258
+ "role": "human",
259
+ "content": f"Please analyze my portfolio with risk level '{risk_level}' and investment goals: '{investment_goals}'."
260
+ }],
261
+ next="",
262
+ recommendations=[],
263
+ portfolio_strengths=[],
264
+ portfolio_weaknesses=[],
265
+ new_investments=[],
266
+ allocation_advice="",
267
+ risk_assessment="",
268
+ final_report=""
269
+ )
270
+
271
+ # Create a custom callback to track progress
272
+ def track_progress(state):
273
+ # Simple progress tracking based on what's in the state
274
+ # Each node adds its output to the state, so we can use that to determine progress
275
+
276
+ # Check which stage we're at based on what's in the state
277
+ if "final_report" in state and state["final_report"]:
278
+ progress_placeholder.progress(100, "Complete")
279
+ status_placeholder.success("Analysis complete!")
280
+ elif "recommendations" in state and state["recommendations"]:
281
+ progress_placeholder.progress(90, "Generating Recommendations")
282
+ status_placeholder.info("Recommendations generated. Finalizing report...")
283
+ elif "rag_context" in state and state["rag_context"]:
284
+ progress_placeholder.progress(75, "RAG Analysis")
285
+ status_placeholder.info("Value investing analysis complete. Generating recommendations...")
286
+
287
+ ### SHOW NEWS
288
+ # Display relevant news with links if available
289
+ #if "news_analysis" in final_state and final_state["news_analysis"]:
290
+ if state["news_analysis"]:
291
+ st.markdown("")
292
+ st.subheader("Market news for your portfolio")
293
+
294
+ # Display top two news articles outside the expander
295
+ top_articles_shown = 0
296
+ for i, news_item in enumerate(state["news_analysis"]):
297
+ if isinstance(news_item, dict) and top_articles_shown < 2:
298
+ title = news_item.get("title", "")
299
+ summary = news_item.get("summary", "")
300
+ url = news_item.get("url", "")
301
+ urlToImage = news_item.get("urlToImage", "")
302
+ sentiment = news_item.get("sentiment", "neutral")
303
+ if not summary or summary == "No description available.":
304
+ continue
305
+
306
+ # Create two columns for image and content
307
+ img_col, content_col = st.columns([1, 6])
308
+
309
+ # Display image in left column if available
310
+ if urlToImage:
311
+ with img_col:
312
+ st.markdown("""
313
+ <style>
314
+ .news-image-container {
315
+ width: 120px;
316
+ height: 120px;
317
+ overflow: hidden;
318
+ display: flex;
319
+ align-items: center;
320
+ justify-content: center;
321
+ border-radius: 8px;
322
+ margin: 8px;
323
+ margin-top: -16px;
324
+ }
325
+ .news-image-container img {
326
+ height: 100%;
327
+ width: auto;
328
+ object-fit: cover;
329
+ }
330
+ </style>
331
+ """, unsafe_allow_html=True)
332
+
333
+ st.markdown(f"""
334
+ <div class="news-image-container">
335
+ <img src="{urlToImage}" alt="News image">
336
+ </div>
337
+ """, unsafe_allow_html=True)
338
+
339
+ # Display title, summary, and link in right column
340
+ with content_col:
341
+ # Style based on sentiment
342
+ if sentiment.lower() == "positive":
343
+ st.success(f"**{title}**")
344
+ elif sentiment.lower() == "negative":
345
+ st.error(f"**{title}**")
346
+ else:
347
+ st.info(f"**{title}**")
348
+
349
+ # Truncate summary to one line (max 120 characters)
350
+ truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary
351
+ st.write(truncated_summary)
352
+ if url:
353
+ st.write(f"[Read more]({url})")
354
+
355
+ top_articles_shown += 1
356
+
357
+ # Display remaining news articles in the expander
358
+ if len(state["news_analysis"]) > 2:
359
+ with st.expander("View More Market News"):
360
+ for i, news_item in enumerate(state["news_analysis"]):
361
+ if isinstance(news_item, dict) and i >= 2:
362
+ title = news_item.get("title", "")
363
+ summary = news_item.get("summary", "")
364
+ url = news_item.get("url", "")
365
+ urlToImage = news_item.get("urlToImage", "")
366
+ sentiment = news_item.get("sentiment", "neutral")
367
+ if not summary or summary == "No description available.":
368
+ continue
369
+
370
+ # Create two columns for image and content
371
+ img_col, content_col = st.columns([1, 6])
372
+
373
+ # Display image in left column if available
374
+ if urlToImage:
375
+ with img_col:
376
+ st.markdown("""
377
+ <style>
378
+ .news-image-container {
379
+ width: 120px;
380
+ height: 120px;
381
+ overflow: hidden;
382
+ display: flex;
383
+ align-items: center;
384
+ justify-content: center;
385
+ border-radius: 8px;
386
+ margin: 8px;
387
+ margin-top: -16px;
388
+ }
389
+ .news-image-container img {
390
+ height: 100%;
391
+ width: auto;
392
+ object-fit: cover;
393
+ }
394
+ </style>
395
+ """, unsafe_allow_html=True)
396
+
397
+ st.markdown(f"""
398
+ <div class="news-image-container">
399
+ <img src="{urlToImage}" alt="News image">
400
+ </div>
401
+ """, unsafe_allow_html=True)
402
+
403
+ # Display title, summary, and link in right column
404
+ with content_col:
405
+ # Style based on sentiment
406
+ if sentiment.lower() == "positive":
407
+ st.success(f"**{title}**")
408
+ elif sentiment.lower() == "negative":
409
+ st.error(f"**{title}**")
410
+ else:
411
+ st.info(f"**{title}**")
412
+
413
+ # Truncate summary to one line (max 120 characters)
414
+ truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary
415
+ st.write(truncated_summary)
416
+ if url:
417
+ st.write(f"[Read more]({url})")
418
+
419
+
420
+
421
+
422
+ elif "news_analysis" in state and state["news_analysis"]:
423
+ progress_placeholder.progress(60, "News Analysis")
424
+ status_placeholder.info("News analysis complete. Applying value investing principles...")
425
+ elif "portfolio_analysis" in state and state["portfolio_analysis"]:
426
+ progress_placeholder.progress(40, "Portfolio Analysis")
427
+ status_placeholder.info("Portfolio analysis complete. Gathering financial news...")
428
+ elif "technical_analysis" in state and state["technical_analysis"]:
429
+ progress_placeholder.progress(20, "Technical Analysis")
430
+ status_placeholder.info("Technical analysis complete. Analyzing portfolio...")
431
+ else:
432
+ progress_placeholder.progress(0, "Starting analysis...")
433
+ status_placeholder.info("Initializing technical analysis...")
434
+
435
+ return state
436
+
437
+ # Run the graph with progress tracking
438
+ graph = setup_graph_with_tracking(track_progress)
439
+
440
+ # Run the graph
441
+ final_state = graph.invoke(initial_state)
442
+
443
+ # Store the final state in session state
444
+ st.session_state.final_state = final_state
445
+ st.session_state.portfolio_analyzed = True
446
+ st.session_state.portfolio_data = portfolio_data
447
+
448
+ else:
449
+ # Use the stored final state
450
+ final_state = st.session_state.final_state
451
+ portfolio_data = st.session_state.portfolio_data
452
+
453
+ # Display the results
454
+ # Display the executive summary for the end user
455
+ #st.subheader("Your Investment Portfolio Analysis")
456
+ # if portfolio_data:
457
+ # st.subheader("Current valuation")
458
+
459
+ # # Create a dataframe for portfolio valuation
460
+ # valuation_data = []
461
+ # total_value = 0
462
+ # total_cost = 0
463
+
464
+ # for ticker, data in portfolio_data.items():
465
+ # current_value = data['value']
466
+ # purchase_value = data['purchase_price'] * data['shares']
467
+ # total_value += current_value
468
+ # total_cost += purchase_value
469
+
470
+ # valuation_data.append({
471
+ # 'Ticker': ticker,
472
+ # 'Shares': data['shares'],
473
+ # 'Purchase Price': f"${data['purchase_price']:.2f}",
474
+ # 'Current Price': f"${data['current_price']:.2f}",
475
+ # 'Cost Basis': f"${purchase_value:.2f}",
476
+ # 'Current Value': f"${current_value:.2f}",
477
+ # 'Gain/Loss ($)': f"${data['gain_loss']:.2f}",
478
+ # 'Gain/Loss (%)': f"{data['gain_loss_pct']:.2f}%"
479
+ # })
480
+
481
+ # # Calculate total gain/loss
482
+ # total_gain_loss = total_value - total_cost
483
+ # total_gain_loss_pct = (total_gain_loss / total_cost * 100) if total_cost > 0 else 0
484
+
485
+ # # Display the valuation table
486
+ # valuation_df = pd.DataFrame(valuation_data)
487
+ # st.dataframe(valuation_df, use_container_width=True)
488
+
489
+
490
+ # # Display portfolio valuations and allocations with gain/loss
491
+ # #if portfolio_data:
492
+
493
+ # # Create a better layout with metrics on the left and pie chart on the right
494
+ # st.subheader("Performance & allocation summary")
495
+
496
+ # # Create two columns for the layout with adjusted ratio
497
+ # left_col, right_col = st.columns([1, 1])
498
+
499
+ # # Left column: stacked metrics
500
+ # with left_col:
501
+ # cont_col, gap_col = st.columns([2, 1])
502
+
503
+ # with cont_col:
504
+ # # Add a bit of vertical space for alignment
505
+ # st.write("")
506
+
507
+ # # Total Portfolio Value metric
508
+ # st.metric("Total Portfolio Value", f"${total_value:.2f}", f"${total_gain_loss:.2f}", border=True)
509
+
510
+ # # Add some space between metrics
511
+ # st.write("")
512
+
513
+ # # Total Cost Basis metric
514
+ # st.metric("Total Cost Basis", f"${total_cost:.2f}", border=True)
515
+
516
+ # # Add some space between metrics
517
+ # st.write("")
518
+
519
+ # # Total Return metric
520
+ # st.metric("Total Return", f"{total_gain_loss_pct:.2f}%", border=True)
521
+ # with gap_col:
522
+ # st.markdown("")
523
+
524
+ # # Right column: pie chart
525
+ # with right_col:
526
+ # # Prepare data for pie chart
527
+ # st.markdown("")
528
+ # allocation_data = {}
529
+ # for ticker, data in portfolio_data.items():
530
+ # allocation_data[ticker] = data['value']
531
+
532
+ # # Create a figure for the pie chart
533
+ # fig, ax = plt.subplots(figsize=(4, 3.2))
534
+
535
+ # # Create the pie chart
536
+ # wedges, texts, autotexts = ax.pie(
537
+ # allocation_data.values(),
538
+ # labels=allocation_data.keys(),
539
+ # autopct='%1.1f%%',
540
+ # startangle=90,
541
+ # wedgeprops={'edgecolor': 'white'},
542
+ # textprops={'fontsize': 8}
543
+ # )
544
+
545
+ # # Equal aspect ratio ensures that pie is drawn as a circle
546
+ # ax.axis('equal')
547
+
548
+ # # Manually set font sizes
549
+ # plt.setp(autotexts, size=8, weight="bold")
550
+ # plt.setp(texts, size=8)
551
+
552
+ # # Add a title
553
+ # plt.title('Portfolio Allocation by Value', fontsize=10)
554
+
555
+ # # Use tight layout
556
+ # plt.tight_layout()
557
+
558
+ # # Display the pie chart
559
+ # st.pyplot(fig)
560
+
561
+ # Check if we have a final report
562
+ if "final_report" in final_state and final_state["final_report"]:
563
+ st.subheader("Portfolio analysis")
564
+ # Format and display the final report in a clean, professional way
565
+ report = final_state["final_report"]
566
+ #st.markdown(report)
567
+ # st.markdown(
568
+ # f"""
569
+ # <div style="background-color: #060F35;">
570
+ # {report}
571
+ # </div>
572
+ # """,
573
+ # unsafe_allow_html=True
574
+ # )
575
+ st.info(report)
576
+ else:
577
+ st.error("Unable to generate portfolio analysis. Please try again.")
578
+
579
+ # Display relevant news with links if available
580
+ #if "news_analysis" in final_state and final_state["news_analysis"]:
581
+ # if final_state["news_analysis"]:
582
+ # st.markdown("")
583
+ # st.subheader("Market news for your portfolio")
584
+
585
+ # # Display top two news articles outside the expander
586
+ # top_articles_shown = 0
587
+ # for i, news_item in enumerate(final_state["news_analysis"]):
588
+ # if isinstance(news_item, dict) and top_articles_shown < 2:
589
+ # title = news_item.get("title", "")
590
+ # summary = news_item.get("summary", "")
591
+ # url = news_item.get("url", "")
592
+ # urlToImage = news_item.get("urlToImage", "")
593
+ # sentiment = news_item.get("sentiment", "neutral")
594
+ # if not summary or summary == "No description available.":
595
+ # continue
596
+
597
+ # # Create two columns for image and content
598
+ # img_col, content_col = st.columns([1, 6])
599
+
600
+ # # Display image in left column if available
601
+ # if urlToImage:
602
+ # with img_col:
603
+ # st.markdown("""
604
+ # <style>
605
+ # .news-image-container {
606
+ # width: 120px;
607
+ # height: 120px;
608
+ # overflow: hidden;
609
+ # display: flex;
610
+ # align-items: center;
611
+ # justify-content: center;
612
+ # border-radius: 8px;
613
+ # margin: 8px;
614
+ # margin-top: -16px;
615
+ # }
616
+ # .news-image-container img {
617
+ # height: 100%;
618
+ # width: auto;
619
+ # object-fit: cover;
620
+ # }
621
+ # </style>
622
+ # """, unsafe_allow_html=True)
623
+
624
+ # st.markdown(f"""
625
+ # <div class="news-image-container">
626
+ # <img src="{urlToImage}" alt="News image">
627
+ # </div>
628
+ # """, unsafe_allow_html=True)
629
+
630
+ # # Display title, summary, and link in right column
631
+ # with content_col:
632
+ # # Style based on sentiment
633
+ # if sentiment.lower() == "positive":
634
+ # st.success(f"**{title}**")
635
+ # elif sentiment.lower() == "negative":
636
+ # st.error(f"**{title}**")
637
+ # else:
638
+ # st.info(f"**{title}**")
639
+
640
+ # # Truncate summary to one line (max 120 characters)
641
+ # truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary
642
+ # st.write(truncated_summary)
643
+ # if url:
644
+ # st.write(f"[Read more]({url})")
645
+
646
+ # top_articles_shown += 1
647
+
648
+ # # Display remaining news articles in the expander
649
+ # if len(final_state["news_analysis"]) > 2:
650
+ # with st.expander("View More Market News"):
651
+ # for i, news_item in enumerate(final_state["news_analysis"]):
652
+ # if isinstance(news_item, dict) and i >= 2:
653
+ # title = news_item.get("title", "")
654
+ # summary = news_item.get("summary", "")
655
+ # url = news_item.get("url", "")
656
+ # urlToImage = news_item.get("urlToImage", "")
657
+ # sentiment = news_item.get("sentiment", "neutral")
658
+ # if not summary or summary == "No description available.":
659
+ # continue
660
+
661
+ # # Create two columns for image and content
662
+ # img_col, content_col = st.columns([1, 6])
663
+
664
+ # # Display image in left column if available
665
+ # if urlToImage:
666
+ # with img_col:
667
+ # st.markdown("""
668
+ # <style>
669
+ # .news-image-container {
670
+ # width: 120px;
671
+ # height: 120px;
672
+ # overflow: hidden;
673
+ # display: flex;
674
+ # align-items: center;
675
+ # justify-content: center;
676
+ # border-radius: 8px;
677
+ # margin: 8px;
678
+ # margin-top: -16px;
679
+ # }
680
+ # .news-image-container img {
681
+ # height: 100%;
682
+ # width: auto;
683
+ # object-fit: cover;
684
+ # }
685
+ # </style>
686
+ # """, unsafe_allow_html=True)
687
+
688
+ # st.markdown(f"""
689
+ # <div class="news-image-container">
690
+ # <img src="{urlToImage}" alt="News image">
691
+ # </div>
692
+ # """, unsafe_allow_html=True)
693
+
694
+ # # Display title, summary, and link in right column
695
+ # with content_col:
696
+ # # Style based on sentiment
697
+ # if sentiment.lower() == "positive":
698
+ # st.success(f"**{title}**")
699
+ # elif sentiment.lower() == "negative":
700
+ # st.error(f"**{title}**")
701
+ # else:
702
+ # st.info(f"**{title}**")
703
+
704
+ # # Truncate summary to one line (max 120 characters)
705
+ # truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary
706
+ # st.write(truncated_summary)
707
+ # if url:
708
+ # st.write(f"[Read more]({url})")
709
+
710
+
711
+ # Display portfolio strengths and weaknesses
712
+ col1, col2 = st.columns(2)
713
+
714
+ with col1:
715
+ st.subheader("Portfolio strengths")
716
+ strengths = final_state.get("portfolio_strengths", [])
717
+ if strengths:
718
+ for strength in strengths:
719
+ st.write(f"✅ {strength}")
720
+ else:
721
+ st.write("No specific strengths identified.")
722
+
723
+ with col2:
724
+ st.subheader("Areas for improvement")
725
+ weaknesses = final_state.get("portfolio_weaknesses", [])
726
+ if weaknesses:
727
+ for weakness in weaknesses:
728
+ st.write(f"⚠️ {weakness}")
729
+ else:
730
+ st.write("No specific areas for improvement identified.")
731
+
732
+ # Display allocation advice and risk assessment
733
+ st.subheader("Investment strategy")
734
+
735
+ allocation_advice = final_state.get("allocation_advice", "")
736
+ risk_assessment = final_state.get("risk_assessment", "")
737
+
738
+ if allocation_advice:
739
+ st.write(f"**Allocation advice:** {allocation_advice}")
740
+
741
+ if risk_assessment:
742
+ st.write(f"**Risk assessment:** {risk_assessment}")
743
+
744
+ # Display key recommendations in an easy-to-read format
745
+ if "recommendations" in final_state and final_state["recommendations"]:
746
+ st.subheader("Key recommendations")
747
+
748
+ # Create columns for recommendation cards
749
+ cols = st.columns(min(3, len(final_state["recommendations"])))
750
+
751
+ # Sort recommendations by priority (if available)
752
+ sorted_recommendations = sorted(
753
+ final_state["recommendations"],
754
+ key=lambda x: x.get("priority", 5)
755
+ )
756
+
757
+ # Display top recommendations in cards
758
+ for i, rec in enumerate(sorted_recommendations[:3]): # Show top 3 recommendations
759
+ with cols[i % 3]:
760
+ action = rec.get("action", "")
761
+ ticker = rec.get("ticker", "")
762
+ reasoning = rec.get("reasoning", "")
763
+
764
+ # Style based on action type
765
+ if action == "BUY":
766
+ st.success(f"**{action}: {ticker}**")
767
+ elif action == "SELL":
768
+ st.error(f"**{action}: {ticker}**")
769
+ else: # HOLD
770
+ st.info(f"**{action}: {ticker}**")
771
+
772
+ # Display reasoning with markdown formatting preserved
773
+ st.markdown(reasoning)
774
+
775
+ # If there are more than 3 recommendations, add a expander for the rest
776
+ if len(sorted_recommendations) > 3:
777
+ with st.expander("View all recommendations"):
778
+ for rec in sorted_recommendations[3:]:
779
+ action = rec.get("action", "")
780
+ ticker = rec.get("ticker", "")
781
+ reasoning = rec.get("reasoning", "")
782
+
783
+ # Style based on action type
784
+ if action == "BUY":
785
+ st.success(f"**{action}: {ticker}**")
786
+ elif action == "SELL":
787
+ st.error(f"**{action}: {ticker}**")
788
+ else: # HOLD
789
+ st.info(f"**{action}: {ticker}**")
790
+
791
+ # Display reasoning with markdown formatting preserved
792
+ st.markdown(reasoning)
793
+ st.divider()
794
+
795
+ # Add a second generate button for new investment recommendations
796
+ st.divider()
797
+ st.subheader("Next step: Discover new investment opportunities")
798
+ st.write("Click the button below to discover new investment opportunities that would complement your portfolio.")
799
+
800
+
801
+ if st.button("Generate New Investment Recommendations", key="new_inv_button"):
802
+ # Create a placeholder for the progress bar
803
+ new_inv_progress_placeholder = st.empty()
804
+ new_inv_status_placeholder = st.empty()
805
+
806
+ # Get portfolio data from session state
807
+ portfolio_data = st.session_state.portfolio_data
808
+
809
+ # Initialize the state with user inputs
810
+ initial_state2 = AgentState2(
811
+ portfolio_data=portfolio_data,
812
+ risk_level=risk_level,
813
+ investment_goals=investment_goals,
814
+ technical_analysis={},
815
+ news_analysis=[],
816
+ fundamental_analysis={},
817
+ high_rank_stocks=[],
818
+ new_stock_analysis={},
819
+ portfolio_fit={},
820
+ rag_context=None,
821
+ messages=[{
822
+ "role": "human",
823
+ "content": f"Please find new investment opportunities that complement my portfolio with risk level '{risk_level}' and investment goals: '{investment_goals}'."
824
+ }],
825
+ recommendations=[],
826
+ portfolio_strengths=[],
827
+ portfolio_weaknesses=[],
828
+ new_investments=[],
829
+ new_investment_summary="",
830
+ allocation_advice="",
831
+ risk_assessment="",
832
+ final_report=""
833
+ )
834
+
835
+ # Create a custom callback to track progress
836
+ def track_new_inv_progress(state):
837
+ # Simple progress tracking based on what's in the state
838
+ # Each node adds its output to the state, so we can use that to determine progress
839
+
840
+ # Check which stage we're at based on what's in the state
841
+ if "new_investments" in state and state["new_investments"]:
842
+ # Everything completed
843
+ new_inv_progress_placeholder.progress(100, "Complete")
844
+ new_inv_status_placeholder.success("Analysis complete!")
845
+ elif "portfolio_fit" in state and state["portfolio_fit"]:
846
+ # Portfolio Fit Evaluation completed
847
+ new_inv_progress_placeholder.progress(75, "Portfolio Fit Evaluation")
848
+ new_inv_status_placeholder.info("Portfolio fit evaluation complete. Generating investment recommendations...")
849
+ elif "new_stock_analysis" in state and state["new_stock_analysis"]:
850
+ # New Stock Analysis completed
851
+ new_inv_progress_placeholder.progress(50, "New Stock Analysis")
852
+ new_inv_status_placeholder.info("Technical analysis of new stocks complete. Evaluating portfolio fit...")
853
+ elif "high_rank_stocks" in state and state["high_rank_stocks"]:
854
+ # Zacks Analysis completed
855
+ new_inv_progress_placeholder.progress(25, "Finding High-Ranked Stocks")
856
+ new_inv_status_placeholder.info("Found high-ranked stocks. Analyzing technical indicators...")
857
+ else:
858
+ # Just starting
859
+ new_inv_progress_placeholder.progress(0, "Starting analysis...")
860
+ new_inv_status_placeholder.info("Searching for high-ranked stocks...")
861
+
862
+ return state
863
+
864
+ # Run the graph with progress tracking
865
+ new_inv_graph = setup_new_investments_graph(track_new_inv_progress)
866
+
867
+ # Initialize progress
868
+ new_inv_progress_placeholder.progress(0, "Starting analysis...")
869
+ new_inv_status_placeholder.info("Finding high-ranked stocks...")
870
+
871
+ # Run the graph
872
+ new_inv_final_state = new_inv_graph.invoke(initial_state2)
873
+
874
+ # Clear the progress indicators after completion
875
+ new_inv_progress_placeholder.empty()
876
+ new_inv_status_placeholder.empty()
877
+
878
+ # Display the new investment recommendations
879
+ st.subheader("New investment opportunities")
880
+
881
+ # Display the summary
882
+ new_investment_summary = new_inv_final_state.get("new_investment_summary", "")
883
+ if new_investment_summary:
884
+ st.write(new_investment_summary)
885
+
886
+ # Display the new investment recommendations
887
+ new_investments = new_inv_final_state.get("new_investments", [])
888
+ if new_investments:
889
+ print(new_investments)
890
+ # Sort recommendations by priority (lower number = higher priority)
891
+ sorted_investments = sorted(new_investments, key=lambda x: x.get("priority", 999) if isinstance(x, dict) else 999)
892
+
893
+ # Create a 3-column layout for recommendations
894
+ cols = st.columns(3)
895
+
896
+ # Display top recommendations
897
+ for i, inv in enumerate(sorted_investments):
898
+ if isinstance(inv, dict):
899
+ with cols[i % 3]:
900
+ action = inv.get("action", "")
901
+ ticker = inv.get("ticker", "")
902
+ reasoning = inv.get("reasoning", "")
903
+
904
+ # Style based on action type
905
+ if action == "BUY":
906
+ st.success(f"**{action}: {ticker}**")
907
+ elif action == "SELL":
908
+ st.error(f"**{action}: {ticker}**")
909
+ else: # HOLD
910
+ st.info(f"**{action}: {ticker}**")
911
+
912
+ # Display reasoning with markdown formatting preserved
913
+ st.markdown(reasoning)
914
+ else:
915
+ st.info("No new investment recommendations were generated. Please try again.")
916
+ else:
917
+ # Default display when app first loads
918
+ st.info("Enter your portfolio details and click 'Generate Recommendations' to get personalized investing research, summary, and analysis.")
assets/sunrise.svg ADDED
requirements.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ streamlit
2
+ pandas
3
+ numpy
4
+ matplotlib
5
+ seaborn
6
+ yfinance
7
+ requests
8
+ python-dotenv
9
+ langchain>=0.2.14
10
+ langchain-openai>=0.1.23
11
+ langchain-core>=0.2.35
12
+ langgraph>=0.2.14
13
+ langchain-community
14
+ chromadb
15
+ sentence-transformers
16
+ newsapi-python
17
+ pdfplumber
18
+ jupyter
19
+ tiktoken
20
+ qdrant-client
21
+ arxiv
22
+ langchain-text-splitters
23
+ lxml