add app
Browse files- agents/__init__.py +24 -0
- agents/graph.py +118 -0
- agents/new_investment_recommender.py +177 -0
- agents/new_stock_analyzer.py +160 -0
- agents/news_analyzer.py +274 -0
- agents/portfolio_analyzer.py +168 -0
- agents/portfolio_fit_evaluator.py +173 -0
- agents/rag_analyzer.py +309 -0
- agents/recommendation_engine.py +270 -0
- agents/state.py +63 -0
- agents/technical_analyzer.py +150 -0
- agents/zacks_analyzer.py +33 -0
- app.py +918 -0
- assets/sunrise.svg +0 -0
- requirements.txt +23 -0
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
|