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 # Import our agent modules from agents import AgentState, AgentState2 from agents.graph import setup_graph_with_tracking, setup_new_investments_graph # Load environment variables load_dotenv() # Configure page st.set_page_config( page_title="Cardinal ai", page_icon="🧭", layout="wide" ) # Initialize OpenAI client openai_api_key = os.getenv("OPENAI_API_KEY") # llm = ChatOpenAI(model="gpt-4-turbo", temperature=0.2, api_key=openai_api_key) # # Initialize embedding model # embeddings = HuggingFaceEmbeddings(model_name="Snowflake/snowflake-arctic-embed-l") # # Initialize Vector DB # vector_db = Chroma(embedding_function=embeddings, persist_directory="./chroma_db") # Streamlit UI components st.image("assets/sunrise.svg") # st.title("Asset Atlas - AI Financial Advisor") # st.subheader("Research, Summary, and Analysis") # with st.expander("About this app", expanded=False): # st.write(""" # This app provides personalized financial advice based on your portfolio, risk tolerance, and investment goals. # It analyzes technical indicators, relevant news, and market trends to offer tailored recommendations. # """) # Sidebar for user inputs with st.sidebar: st.header("Your Profile") # Risk tolerance selection risk_level = st.select_slider( "Risk Tolerance", options=["Very Conservative", "Conservative", "Moderate", "Aggressive", "Very Aggressive"], value="Moderate" ) # Investment goals investment_goals = st.text_area("Investment Goals", "Retirement in 20 years, building wealth, passive income") st.markdown("") # Portfolio input st.header("Your Portfolio") # Default portfolio for demonstration 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] }) # Let user edit the portfolio 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) # Initialize session state to store our analysis results 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 # Main content area if generate_button or st.session_state.portfolio_analyzed: # If we're here because of the session state, we don't need to re-run the analysis if generate_button: # Convert portfolio dataframe to required format portfolio_data = {} # Create a placeholder for the progress indicator progress_placeholder = st.empty() status_placeholder = st.empty() # Show a spinner while processing 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: # Get current price stock = yf.Ticker(ticker) current_price = stock.history(period="1d")['Close'].iloc[-1] # Calculate values current_value = current_price * shares purchase_value = purchase_price * shares gain_loss = current_value - purchase_value gain_loss_pct = (gain_loss / purchase_value) * 100 # Store in portfolio data 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") # Create a dataframe for portfolio 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}%" }) # Calculate total gain/loss total_gain_loss = total_value - total_cost total_gain_loss_pct = (total_gain_loss / total_cost * 100) if total_cost > 0 else 0 # Display the valuation table valuation_df = pd.DataFrame(valuation_data) st.dataframe(valuation_df, use_container_width=True) # Display portfolio valuations and allocations with gain/loss #if portfolio_data: # Create a better layout with metrics on the left and pie chart on the right st.subheader("Performance & allocation summary") # Create two columns for the layout with adjusted ratio left_col, right_col = st.columns([1, 1]) # Left column: stacked metrics with left_col: cont_col, gap_col = st.columns([2, 1]) with cont_col: # Add a bit of vertical space for alignment st.write("") # Total Portfolio Value metric st.metric("Total Portfolio Value", f"${total_value:.2f}", f"${total_gain_loss:.2f}", border=True) # Add some space between metrics st.write("") # Total Cost Basis metric st.metric("Total Cost Basis", f"${total_cost:.2f}", border=True) # Add some space between metrics st.write("") # Total Return metric st.metric("Total Return", f"{total_gain_loss_pct:.2f}%", border=True) with gap_col: st.markdown("") # Right column: pie chart with right_col: # Prepare data for pie chart st.markdown("") allocation_data = {} for ticker, data in portfolio_data.items(): allocation_data[ticker] = data['value'] # Create a figure for the pie chart fig, ax = plt.subplots(figsize=(4, 3.2)) # Create the pie chart wedges, texts, autotexts = ax.pie( allocation_data.values(), labels=allocation_data.keys(), autopct='%1.1f%%', startangle=90, wedgeprops={'edgecolor': 'white'}, textprops={'fontsize': 8} ) # Equal aspect ratio ensures that pie is drawn as a circle ax.axis('equal') # Manually set font sizes plt.setp(autotexts, size=8, weight="bold") plt.setp(texts, size=8) # Add a title plt.title('Portfolio Allocation by Value', fontsize=10) # Use tight layout plt.tight_layout() # Display the pie chart st.pyplot(fig) # Initialize state 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="" ) # Create a custom callback to track progress def track_progress(state): # Simple progress tracking based on what's in the state # Each node adds its output to the state, so we can use that to determine progress # Check which stage we're at based on what's in the 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...") ### SHOW NEWS # Display relevant news with links if available #if "news_analysis" in final_state and final_state["news_analysis"]: if state["news_analysis"]: st.markdown("") st.subheader("Market news for your portfolio") # Display top two news articles outside the expander 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 # Create two columns for image and content img_col, content_col = st.columns([1, 6]) # Display image in left column if available if urlToImage: with img_col: st.markdown(""" """, unsafe_allow_html=True) st.markdown(f"""
News image
""", unsafe_allow_html=True) # Display title, summary, and link in right column with content_col: # Style based on sentiment if sentiment.lower() == "positive": st.success(f"**{title}**") elif sentiment.lower() == "negative": st.error(f"**{title}**") else: st.info(f"**{title}**") # Truncate summary to one line (max 120 characters) 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 # Display remaining news articles in the expander 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 # Create two columns for image and content img_col, content_col = st.columns([1, 6]) # Display image in left column if available if urlToImage: with img_col: st.markdown(""" """, unsafe_allow_html=True) st.markdown(f"""
News image
""", unsafe_allow_html=True) # Display title, summary, and link in right column with content_col: # Style based on sentiment if sentiment.lower() == "positive": st.success(f"**{title}**") elif sentiment.lower() == "negative": st.error(f"**{title}**") else: st.info(f"**{title}**") # Truncate summary to one line (max 120 characters) 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 # Run the graph with progress tracking graph = setup_graph_with_tracking(track_progress) # Run the graph final_state = graph.invoke(initial_state) # Store the final state in session state st.session_state.final_state = final_state st.session_state.portfolio_analyzed = True st.session_state.portfolio_data = portfolio_data else: # Use the stored final state final_state = st.session_state.final_state portfolio_data = st.session_state.portfolio_data # Display the results # Display the executive summary for the end user #st.subheader("Your Investment Portfolio Analysis") # if portfolio_data: # st.subheader("Current valuation") # # Create a dataframe for portfolio 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}%" # }) # # Calculate total gain/loss # total_gain_loss = total_value - total_cost # total_gain_loss_pct = (total_gain_loss / total_cost * 100) if total_cost > 0 else 0 # # Display the valuation table # valuation_df = pd.DataFrame(valuation_data) # st.dataframe(valuation_df, use_container_width=True) # # Display portfolio valuations and allocations with gain/loss # #if portfolio_data: # # Create a better layout with metrics on the left and pie chart on the right # st.subheader("Performance & allocation summary") # # Create two columns for the layout with adjusted ratio # left_col, right_col = st.columns([1, 1]) # # Left column: stacked metrics # with left_col: # cont_col, gap_col = st.columns([2, 1]) # with cont_col: # # Add a bit of vertical space for alignment # st.write("") # # Total Portfolio Value metric # st.metric("Total Portfolio Value", f"${total_value:.2f}", f"${total_gain_loss:.2f}", border=True) # # Add some space between metrics # st.write("") # # Total Cost Basis metric # st.metric("Total Cost Basis", f"${total_cost:.2f}", border=True) # # Add some space between metrics # st.write("") # # Total Return metric # st.metric("Total Return", f"{total_gain_loss_pct:.2f}%", border=True) # with gap_col: # st.markdown("") # # Right column: pie chart # with right_col: # # Prepare data for pie chart # st.markdown("") # allocation_data = {} # for ticker, data in portfolio_data.items(): # allocation_data[ticker] = data['value'] # # Create a figure for the pie chart # fig, ax = plt.subplots(figsize=(4, 3.2)) # # Create the pie chart # wedges, texts, autotexts = ax.pie( # allocation_data.values(), # labels=allocation_data.keys(), # autopct='%1.1f%%', # startangle=90, # wedgeprops={'edgecolor': 'white'}, # textprops={'fontsize': 8} # ) # # Equal aspect ratio ensures that pie is drawn as a circle # ax.axis('equal') # # Manually set font sizes # plt.setp(autotexts, size=8, weight="bold") # plt.setp(texts, size=8) # # Add a title # plt.title('Portfolio Allocation by Value', fontsize=10) # # Use tight layout # plt.tight_layout() # # Display the pie chart # st.pyplot(fig) # Check if we have a final report if "final_report" in final_state and final_state["final_report"]: st.subheader("Portfolio analysis") # Format and display the final report in a clean, professional way report = final_state["final_report"] #st.markdown(report) # st.markdown( # f""" #
# {report} #
# """, # unsafe_allow_html=True # ) st.info(report) else: st.error("Unable to generate portfolio analysis. Please try again.") # Display relevant news with links if available #if "news_analysis" in final_state and final_state["news_analysis"]: # if final_state["news_analysis"]: # st.markdown("") # st.subheader("Market news for your portfolio") # # Display top two news articles outside the expander # top_articles_shown = 0 # for i, news_item in enumerate(final_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 # # Create two columns for image and content # img_col, content_col = st.columns([1, 6]) # # Display image in left column if available # if urlToImage: # with img_col: # st.markdown(""" # # """, unsafe_allow_html=True) # st.markdown(f""" #
# News image #
# """, unsafe_allow_html=True) # # Display title, summary, and link in right column # with content_col: # # Style based on sentiment # if sentiment.lower() == "positive": # st.success(f"**{title}**") # elif sentiment.lower() == "negative": # st.error(f"**{title}**") # else: # st.info(f"**{title}**") # # Truncate summary to one line (max 120 characters) # 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 # # Display remaining news articles in the expander # if len(final_state["news_analysis"]) > 2: # with st.expander("View More Market News"): # for i, news_item in enumerate(final_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 # # Create two columns for image and content # img_col, content_col = st.columns([1, 6]) # # Display image in left column if available # if urlToImage: # with img_col: # st.markdown(""" # # """, unsafe_allow_html=True) # st.markdown(f""" #
# News image #
# """, unsafe_allow_html=True) # # Display title, summary, and link in right column # with content_col: # # Style based on sentiment # if sentiment.lower() == "positive": # st.success(f"**{title}**") # elif sentiment.lower() == "negative": # st.error(f"**{title}**") # else: # st.info(f"**{title}**") # # Truncate summary to one line (max 120 characters) # truncated_summary = summary[:120] + "..." if len(summary) > 120 else summary # st.write(truncated_summary) # if url: # st.write(f"[Read more]({url})") # Display portfolio strengths and weaknesses 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.") # Display allocation advice and risk assessment 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}") # Display key recommendations in an easy-to-read format if "recommendations" in final_state and final_state["recommendations"]: st.subheader("Key recommendations") # Create columns for recommendation cards cols = st.columns(min(3, len(final_state["recommendations"]))) # Sort recommendations by priority (if available) sorted_recommendations = sorted( final_state["recommendations"], key=lambda x: x.get("priority", 5) ) # Display top recommendations in cards for i, rec in enumerate(sorted_recommendations[:3]): # Show top 3 recommendations with cols[i % 3]: action = rec.get("action", "") ticker = rec.get("ticker", "") reasoning = rec.get("reasoning", "") # Style based on action type if action == "BUY": st.success(f"**{action}: {ticker}**") elif action == "SELL": st.error(f"**{action}: {ticker}**") else: # HOLD st.info(f"**{action}: {ticker}**") # Display reasoning with markdown formatting preserved st.markdown(reasoning) # If there are more than 3 recommendations, add a expander for the rest 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", "") # Style based on action type if action == "BUY": st.success(f"**{action}: {ticker}**") elif action == "SELL": st.error(f"**{action}: {ticker}**") else: # HOLD st.info(f"**{action}: {ticker}**") # Display reasoning with markdown formatting preserved st.markdown(reasoning) st.divider() # Add a second generate button for new investment recommendations 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"): # Create a placeholder for the progress bar new_inv_progress_placeholder = st.empty() new_inv_status_placeholder = st.empty() # Get portfolio data from session state portfolio_data = st.session_state.portfolio_data # Initialize the state with user inputs 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="" ) # Create a custom callback to track progress def track_new_inv_progress(state): # Simple progress tracking based on what's in the state # Each node adds its output to the state, so we can use that to determine progress # Check which stage we're at based on what's in the state if "new_investments" in state and state["new_investments"]: # Everything completed new_inv_progress_placeholder.progress(100, "Complete") new_inv_status_placeholder.success("Analysis complete!") elif "portfolio_fit" in state and state["portfolio_fit"]: # Portfolio Fit Evaluation completed 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 Stock Analysis completed 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"]: # Zacks Analysis completed new_inv_progress_placeholder.progress(25, "Finding High-Ranked Stocks") new_inv_status_placeholder.info("Found high-ranked stocks. Analyzing technical indicators...") else: # Just starting new_inv_progress_placeholder.progress(0, "Starting analysis...") new_inv_status_placeholder.info("Searching for high-ranked stocks...") return state # Run the graph with progress tracking new_inv_graph = setup_new_investments_graph(track_new_inv_progress) # Initialize progress new_inv_progress_placeholder.progress(0, "Starting analysis...") new_inv_status_placeholder.info("Finding high-ranked stocks...") # Run the graph new_inv_final_state = new_inv_graph.invoke(initial_state2) # Clear the progress indicators after completion new_inv_progress_placeholder.empty() new_inv_status_placeholder.empty() # Display the new investment recommendations st.subheader("New investment opportunities") # Display the summary 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")) # Display the new investment recommendations new_investments = new_inv_final_state.get("new_investments", []) if new_investments: print(new_investments) # Sort recommendations by priority (lower number = higher priority) sorted_investments = sorted(new_investments, key=lambda x: x.get("priority", 999) if isinstance(x, dict) else 999) # Create a 3-column layout for recommendations cols = st.columns(3) # Display top recommendations 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", "") # Style based on action type if action == "BUY": st.success(f"**{action}: {ticker}**") elif action == "SELL": st.error(f"**{action}: {ticker}**") else: # HOLD st.info(f"**{action}: {ticker}**") # Display reasoning with markdown formatting preserved st.markdown(reasoning) else: st.info("No new investment recommendations were generated. Please try again.") else: # Default display when app first loads st.info("Enter your portfolio details and click 'Generate Recommendations' to get personalized investing research, summary, and analysis.")