|
import streamlit as st |
|
import pandas as pd |
|
import numpy as np |
|
from langchain_openai import ChatOpenAI |
|
from langchain_community.embeddings import HuggingFaceEmbeddings |
|
from langchain_community.vectorstores import Chroma |
|
import os |
|
from dotenv import load_dotenv |
|
import yfinance as yf |
|
import matplotlib.pyplot as plt |
|
import seaborn as sns |
|
|
|
|
|
from agents import AgentState, AgentState2 |
|
from agents.graph import setup_graph_with_tracking, setup_new_investments_graph |
|
|
|
|
|
load_dotenv() |
|
|
|
|
|
st.set_page_config( |
|
page_title="Cardinal ai", |
|
page_icon="🧭", |
|
layout="wide" |
|
) |
|
|
|
|
|
openai_api_key = os.getenv("OPENAI_API_KEY") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.image("assets/sunrise.svg") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
with st.sidebar: |
|
st.header("Your Profile") |
|
|
|
|
|
risk_level = st.select_slider( |
|
"Risk Tolerance", |
|
options=["Very Conservative", "Conservative", "Moderate", "Aggressive", "Very Aggressive"], |
|
value="Moderate" |
|
) |
|
|
|
|
|
investment_goals = st.text_area("Investment Goals", "Retirement in 20 years, building wealth, passive income") |
|
|
|
st.markdown("") |
|
|
|
|
|
st.header("Your Portfolio") |
|
|
|
|
|
default_portfolio = pd.DataFrame({ |
|
'Ticker': ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'BRK-B'], |
|
'Shares': [10, 5, 3, 2, 4], |
|
'Purchase Price': [250.00, 250.00, 250.00, 250.00, 250.00] |
|
}) |
|
|
|
|
|
portfolio_df = st.data_editor( |
|
default_portfolio, |
|
column_config={ |
|
"Ticker": st.column_config.TextColumn("Ticker Symbol"), |
|
"Shares": st.column_config.NumberColumn("Number of Shares", min_value=0), |
|
"Purchase Price": st.column_config.NumberColumn("Purchase Price ($)", min_value=0.01, format="$%.2f") |
|
}, |
|
num_rows="dynamic", |
|
use_container_width=True |
|
) |
|
|
|
generate_button = st.button("Generate Recommendations", type="secondary", use_container_width=True) |
|
|
|
|
|
|
|
if 'portfolio_analyzed' not in st.session_state: |
|
st.session_state.portfolio_analyzed = False |
|
|
|
if 'final_state' not in st.session_state: |
|
st.session_state.final_state = None |
|
|
|
if 'portfolio_data' not in st.session_state: |
|
st.session_state.portfolio_data = None |
|
|
|
|
|
if generate_button or st.session_state.portfolio_analyzed: |
|
|
|
if generate_button: |
|
|
|
portfolio_data = {} |
|
|
|
|
|
progress_placeholder = st.empty() |
|
status_placeholder = st.empty() |
|
|
|
|
|
with st.spinner("Analyzing your portfolio and generating recommendations..."): |
|
progress_placeholder.progress(0, "Starting analysis...") |
|
status_placeholder.info("Fetching current market data...") |
|
|
|
for _, row in portfolio_df.iterrows(): |
|
ticker = row['Ticker'] |
|
shares = row['Shares'] |
|
purchase_price = row['Purchase Price'] |
|
|
|
try: |
|
|
|
stock = yf.Ticker(ticker) |
|
current_price = stock.history(period="1d")['Close'].iloc[-1] |
|
|
|
|
|
current_value = current_price * shares |
|
purchase_value = purchase_price * shares |
|
gain_loss = current_value - purchase_value |
|
gain_loss_pct = (gain_loss / purchase_value) * 100 |
|
|
|
|
|
portfolio_data[ticker] = { |
|
"shares": shares, |
|
"purchase_price": purchase_price, |
|
"current_price": current_price, |
|
"value": current_value, |
|
"gain_loss": gain_loss, |
|
"gain_loss_pct": gain_loss_pct |
|
} |
|
except Exception as e: |
|
st.error(f"Error processing {ticker}: {e}") |
|
|
|
if len(portfolio_data) == len(portfolio_df): |
|
st.subheader("Current valuation") |
|
|
|
|
|
valuation_data = [] |
|
total_value = 0 |
|
total_cost = 0 |
|
|
|
for ticker, data in portfolio_data.items(): |
|
current_value = data['value'] |
|
purchase_value = data['purchase_price'] * data['shares'] |
|
total_value += current_value |
|
total_cost += purchase_value |
|
|
|
valuation_data.append({ |
|
'Ticker': ticker, |
|
'Shares': data['shares'], |
|
'Purchase Price': f"${data['purchase_price']:.2f}", |
|
'Current Price': f"${data['current_price']:.2f}", |
|
'Cost Basis': f"${purchase_value:.2f}", |
|
'Current Value': f"${current_value:.2f}", |
|
'Gain/Loss ($)': f"${data['gain_loss']:.2f}", |
|
'Gain/Loss (%)': f"{data['gain_loss_pct']:.2f}%" |
|
}) |
|
|
|
|
|
total_gain_loss = total_value - total_cost |
|
total_gain_loss_pct = (total_gain_loss / total_cost * 100) if total_cost > 0 else 0 |
|
|
|
|
|
valuation_df = pd.DataFrame(valuation_data) |
|
st.dataframe(valuation_df, use_container_width=True) |
|
|
|
|
|
|
|
|
|
|
|
|
|
st.subheader("Performance & allocation summary") |
|
|
|
|
|
left_col, right_col = st.columns([1, 1]) |
|
|
|
|
|
with left_col: |
|
cont_col, gap_col = st.columns([2, 1]) |
|
|
|
with cont_col: |
|
|
|
st.write("") |
|
|
|
|
|
st.metric("Total Portfolio Value", f"${total_value:.2f}", f"${total_gain_loss:.2f}", border=True) |
|
|
|
|
|
st.write("") |
|
|
|
|
|
st.metric("Total Cost Basis", f"${total_cost:.2f}", border=True) |
|
|
|
|
|
st.write("") |
|
|
|
|
|
st.metric("Total Return", f"{total_gain_loss_pct:.2f}%", border=True) |
|
with gap_col: |
|
st.markdown("") |
|
|
|
|
|
with right_col: |
|
|
|
st.markdown("") |
|
allocation_data = {} |
|
for ticker, data in portfolio_data.items(): |
|
allocation_data[ticker] = data['value'] |
|
|
|
|
|
fig, ax = plt.subplots(figsize=(4, 3.2)) |
|
|
|
|
|
wedges, texts, autotexts = ax.pie( |
|
allocation_data.values(), |
|
labels=allocation_data.keys(), |
|
autopct='%1.1f%%', |
|
startangle=90, |
|
wedgeprops={'edgecolor': 'white'}, |
|
textprops={'fontsize': 8} |
|
) |
|
|
|
|
|
ax.axis('equal') |
|
|
|
|
|
plt.setp(autotexts, size=8, weight="bold") |
|
plt.setp(texts, size=8) |
|
|
|
|
|
plt.title('Portfolio Allocation by Value', fontsize=10) |
|
|
|
|
|
plt.tight_layout() |
|
|
|
|
|
st.pyplot(fig) |
|
|
|
|
|
initial_state = AgentState( |
|
portfolio_data=portfolio_data, |
|
risk_level=risk_level, |
|
investment_goals=investment_goals, |
|
technical_analysis={}, |
|
news_analysis=[], |
|
fundamental_analysis={}, |
|
rag_context=None, |
|
messages=[{ |
|
"role": "human", |
|
"content": f"Please analyze my portfolio with risk level '{risk_level}' and investment goals: '{investment_goals}'." |
|
}], |
|
next="", |
|
recommendations=[], |
|
portfolio_strengths=[], |
|
portfolio_weaknesses=[], |
|
new_investments=[], |
|
allocation_advice="", |
|
risk_assessment="", |
|
final_report="" |
|
) |
|
|
|
|
|
def track_progress(state): |
|
|
|
|
|
|
|
|
|
if "final_report" in state and state["final_report"]: |
|
progress_placeholder.progress(100, "Complete") |
|
status_placeholder.success("Analysis complete!") |
|
elif "recommendations" in state and state["recommendations"]: |
|
progress_placeholder.progress(90, "Generating Recommendations") |
|
status_placeholder.info("Recommendations generated. Finalizing report...") |
|
elif "rag_context" in state and state["rag_context"]: |
|
progress_placeholder.progress(75, "RAG Analysis") |
|
status_placeholder.info("Value investing analysis complete. Generating recommendations...") |
|
|
|
|
|
elif "news_analysis" in state and state["news_analysis"]: |
|
progress_placeholder.progress(60, "News Analysis") |
|
status_placeholder.info("News analysis complete. Applying value investing principles...") |
|
|
|
|
|
|
|
|
|
if state["news_analysis"]: |
|
st.markdown("") |
|
st.subheader("Market news for your portfolio") |
|
|
|
|
|
top_articles_shown = 0 |
|
for i, news_item in enumerate(state["news_analysis"]): |
|
if isinstance(news_item, dict) and top_articles_shown < 2: |
|
title = news_item.get("title", "") |
|
summary = news_item.get("summary", "") |
|
url = news_item.get("url", "") |
|
urlToImage = news_item.get("urlToImage", "") |
|
sentiment = news_item.get("sentiment", "neutral") |
|
if not summary or summary == "No description available.": |
|
continue |
|
|
|
|
|
img_col, content_col = st.columns([1, 6]) |
|
|
|
|
|
if urlToImage: |
|
with img_col: |
|
st.markdown(""" |
|
<style> |
|
.news-image-container { |
|
width: 120px; |
|
height: 120px; |
|
overflow: hidden; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
border-radius: 8px; |
|
margin: 8px; |
|
margin-top: -16px; |
|
} |
|
.news-image-container img { |
|
height: 100%; |
|
width: auto; |
|
object-fit: cover; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
st.markdown(f""" |
|
<div class="news-image-container"> |
|
<img src="{urlToImage}" alt="News image"> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with content_col: |
|
|
|
if sentiment.lower() == "positive": |
|
st.success(f"**{title}**") |
|
elif sentiment.lower() == "negative": |
|
st.error(f"**{title}**") |
|
else: |
|
st.info(f"**{title}**") |
|
|
|
|
|
truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary |
|
st.write(truncated_summary) |
|
if url: |
|
st.write(f"[Read more]({url})") |
|
|
|
top_articles_shown += 1 |
|
|
|
|
|
if len(state["news_analysis"]) > 2: |
|
with st.expander("View More Market News"): |
|
for i, news_item in enumerate(state["news_analysis"]): |
|
if isinstance(news_item, dict) and i >= 2: |
|
title = news_item.get("title", "") |
|
summary = news_item.get("summary", "") |
|
url = news_item.get("url", "") |
|
urlToImage = news_item.get("urlToImage", "") |
|
sentiment = news_item.get("sentiment", "neutral") |
|
if not summary or summary == "No description available.": |
|
continue |
|
|
|
|
|
img_col, content_col = st.columns([1, 6]) |
|
|
|
|
|
if urlToImage: |
|
with img_col: |
|
st.markdown(""" |
|
<style> |
|
.news-image-container { |
|
width: 120px; |
|
height: 120px; |
|
overflow: hidden; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
border-radius: 8px; |
|
margin: 8px; |
|
margin-top: -16px; |
|
} |
|
.news-image-container img { |
|
height: 100%; |
|
width: auto; |
|
object-fit: cover; |
|
} |
|
</style> |
|
""", unsafe_allow_html=True) |
|
|
|
st.markdown(f""" |
|
<div class="news-image-container"> |
|
<img src="{urlToImage}" alt="News image"> |
|
</div> |
|
""", unsafe_allow_html=True) |
|
|
|
|
|
with content_col: |
|
|
|
if sentiment.lower() == "positive": |
|
st.success(f"**{title}**") |
|
elif sentiment.lower() == "negative": |
|
st.error(f"**{title}**") |
|
else: |
|
st.info(f"**{title}**") |
|
|
|
|
|
truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary |
|
st.write(truncated_summary) |
|
if url: |
|
st.write(f"[Read more]({url})") |
|
|
|
elif "portfolio_analysis" in state and state["portfolio_analysis"]: |
|
progress_placeholder.progress(40, "Portfolio Analysis") |
|
status_placeholder.info("Portfolio analysis complete. Gathering financial news...") |
|
elif "technical_analysis" in state and state["technical_analysis"]: |
|
progress_placeholder.progress(20, "Technical Analysis") |
|
status_placeholder.info("Technical analysis complete. Analyzing portfolio...") |
|
else: |
|
progress_placeholder.progress(0, "Starting analysis...") |
|
status_placeholder.info("Initializing technical analysis...") |
|
|
|
return state |
|
|
|
|
|
graph = setup_graph_with_tracking(track_progress) |
|
|
|
|
|
final_state = graph.invoke(initial_state) |
|
|
|
|
|
st.session_state.final_state = final_state |
|
st.session_state.portfolio_analyzed = True |
|
st.session_state.portfolio_data = portfolio_data |
|
|
|
else: |
|
|
|
final_state = st.session_state.final_state |
|
portfolio_data = st.session_state.portfolio_data |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "final_report" in final_state and final_state["final_report"]: |
|
st.subheader("Portfolio analysis") |
|
|
|
report = final_state["final_report"] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.info(report) |
|
else: |
|
st.error("Unable to generate portfolio analysis. Please try again.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
col1, col2 = st.columns(2) |
|
print("yoyoyo") |
|
print(final_state.get("technical_analysis")); |
|
print(final_state.get("rag_context")); |
|
print(final_state.get("fundamental_analysis")); |
|
|
|
|
|
|
|
with col1: |
|
st.subheader("Portfolio strengths") |
|
strengths = final_state.get("portfolio_strengths", []) |
|
if strengths: |
|
for strength in strengths: |
|
st.write(f"✅ {strength}") |
|
else: |
|
st.write("No specific strengths identified.") |
|
|
|
with col2: |
|
st.subheader("Areas for improvement") |
|
weaknesses = final_state.get("portfolio_weaknesses", []) |
|
if weaknesses: |
|
for weakness in weaknesses: |
|
st.write(f"⚠️ {weakness}") |
|
else: |
|
st.write("No specific areas for improvement identified.") |
|
|
|
|
|
st.subheader("Investment strategy") |
|
|
|
allocation_advice = final_state.get("allocation_advice", "") |
|
risk_assessment = final_state.get("risk_assessment", "") |
|
|
|
if allocation_advice: |
|
st.write(f"**Allocation advice:** {allocation_advice}") |
|
|
|
if risk_assessment: |
|
st.write(f"**Risk assessment:** {risk_assessment}") |
|
|
|
|
|
if "recommendations" in final_state and final_state["recommendations"]: |
|
st.subheader("Key recommendations") |
|
|
|
|
|
cols = st.columns(min(3, len(final_state["recommendations"]))) |
|
|
|
|
|
sorted_recommendations = sorted( |
|
final_state["recommendations"], |
|
key=lambda x: x.get("priority", 5) |
|
) |
|
|
|
|
|
for i, rec in enumerate(sorted_recommendations[:3]): |
|
with cols[i % 3]: |
|
action = rec.get("action", "") |
|
ticker = rec.get("ticker", "") |
|
reasoning = rec.get("reasoning", "") |
|
|
|
|
|
if action == "BUY": |
|
st.success(f"**{action}: {ticker}**") |
|
elif action == "SELL": |
|
st.error(f"**{action}: {ticker}**") |
|
else: |
|
st.info(f"**{action}: {ticker}**") |
|
|
|
|
|
st.markdown(reasoning) |
|
|
|
|
|
if len(sorted_recommendations) > 3: |
|
with st.expander("View all recommendations"): |
|
for rec in sorted_recommendations[3:]: |
|
action = rec.get("action", "") |
|
ticker = rec.get("ticker", "") |
|
reasoning = rec.get("reasoning", "") |
|
|
|
|
|
if action == "BUY": |
|
st.success(f"**{action}: {ticker}**") |
|
elif action == "SELL": |
|
st.error(f"**{action}: {ticker}**") |
|
else: |
|
st.info(f"**{action}: {ticker}**") |
|
|
|
|
|
st.markdown(reasoning) |
|
st.divider() |
|
|
|
|
|
st.divider() |
|
st.subheader("Next step: Discover new investment opportunities") |
|
st.write("Click the button below to discover new investment opportunities that would complement your portfolio.") |
|
|
|
|
|
if st.button("Generate New Investment Recommendations", key="new_inv_button"): |
|
|
|
new_inv_progress_placeholder = st.empty() |
|
new_inv_status_placeholder = st.empty() |
|
|
|
|
|
portfolio_data = st.session_state.portfolio_data |
|
|
|
|
|
initial_state2 = AgentState2( |
|
portfolio_data=portfolio_data, |
|
risk_level=risk_level, |
|
investment_goals=investment_goals, |
|
technical_analysis={}, |
|
news_analysis=[], |
|
fundamental_analysis={}, |
|
high_rank_stocks=[], |
|
new_stock_analysis={}, |
|
portfolio_fit={}, |
|
rag_context=None, |
|
messages=[{ |
|
"role": "human", |
|
"content": f"Please find new investment opportunities that complement my portfolio with risk level '{risk_level}' and investment goals: '{investment_goals}'." |
|
}], |
|
recommendations=[], |
|
portfolio_strengths=[], |
|
portfolio_weaknesses=[], |
|
new_investments=[], |
|
new_investment_summary="", |
|
allocation_advice="", |
|
risk_assessment="", |
|
final_report="" |
|
) |
|
|
|
|
|
def track_new_inv_progress(state): |
|
|
|
|
|
|
|
|
|
if "new_investments" in state and state["new_investments"]: |
|
|
|
new_inv_progress_placeholder.progress(100, "Complete") |
|
new_inv_status_placeholder.success("Analysis complete!") |
|
elif "portfolio_fit" in state and state["portfolio_fit"]: |
|
|
|
new_inv_progress_placeholder.progress(75, "Portfolio Fit Evaluation") |
|
new_inv_status_placeholder.info("Portfolio fit evaluation complete. Generating investment recommendations...") |
|
elif "new_stock_analysis" in state and state["new_stock_analysis"]: |
|
|
|
new_inv_progress_placeholder.progress(50, "New Stock Analysis") |
|
new_inv_status_placeholder.info("Technical analysis of new stocks complete. Evaluating portfolio fit...") |
|
elif "high_rank_stocks" in state and state["high_rank_stocks"]: |
|
|
|
new_inv_progress_placeholder.progress(25, "Finding High-Ranked Stocks") |
|
new_inv_status_placeholder.info("Found high-ranked stocks. Analyzing technical indicators...") |
|
else: |
|
|
|
new_inv_progress_placeholder.progress(0, "Starting analysis...") |
|
new_inv_status_placeholder.info("Searching for high-ranked stocks...") |
|
|
|
return state |
|
|
|
|
|
new_inv_graph = setup_new_investments_graph(track_new_inv_progress) |
|
|
|
|
|
new_inv_progress_placeholder.progress(0, "Starting analysis...") |
|
new_inv_status_placeholder.info("Finding high-ranked stocks...") |
|
|
|
|
|
new_inv_final_state = new_inv_graph.invoke(initial_state2) |
|
|
|
|
|
new_inv_progress_placeholder.empty() |
|
new_inv_status_placeholder.empty() |
|
|
|
|
|
st.subheader("New investment opportunities") |
|
|
|
|
|
new_investment_summary = new_inv_final_state.get("new_investment_summary", "") |
|
if new_investment_summary: |
|
st.write(new_investment_summary) |
|
|
|
print(new_inv_final_state.get("new_stock_analysis")) |
|
|
|
|
|
new_investments = new_inv_final_state.get("new_investments", []) |
|
if new_investments: |
|
print(new_investments) |
|
|
|
sorted_investments = sorted(new_investments, key=lambda x: x.get("priority", 999) if isinstance(x, dict) else 999) |
|
|
|
|
|
cols = st.columns(3) |
|
|
|
|
|
for i, inv in enumerate(sorted_investments): |
|
if isinstance(inv, dict): |
|
with cols[i % 3]: |
|
action = inv.get("action", "") |
|
ticker = inv.get("ticker", "") |
|
reasoning = inv.get("reasoning", "") |
|
|
|
|
|
if action == "BUY": |
|
st.success(f"**{action}: {ticker}**") |
|
elif action == "SELL": |
|
st.error(f"**{action}: {ticker}**") |
|
else: |
|
st.info(f"**{action}: {ticker}**") |
|
|
|
|
|
st.markdown(reasoning) |
|
else: |
|
st.info("No new investment recommendations were generated. Please try again.") |
|
else: |
|
|
|
st.info("Enter your portfolio details and click 'Generate Recommendations' to get personalized investing research, summary, and analysis.") |
|
|