import streamlit as st import pandas as pd import numpy as np import plotly.express as px import requests import os # ── Page config ────────────────────────────────────────────── st.set_page_config( page_title="Portfolio Monitoring Dashboard", page_icon="📈", layout="wide" ) # ── Hugging Face AI (FinBERT sentiment) ────────────────────── HF_API_KEY = os.environ.get("HF_API_KEY", "") def analyze_sentiment(text: str) -> str: """Calls FinBERT on Hugging Face to get financial sentiment.""" if not HF_API_KEY: return "⚠️ No API key" url = "https://api-inference.huggingface.co/models/ProsusAI/finbert" headers = {"Authorization": f"Bearer {HF_API_KEY}"} try: r = requests.post(url, headers=headers, json={"inputs": text}, timeout=10) result = r.json() if isinstance(result, list) and result: top = max(result[0], key=lambda x: x["score"]) emoji = {"positive": "🟢", "negative": "🔴", "neutral": "🟡"}.get(top["label"].lower(), "⚪") return f"{emoji} {top['label'].capitalize()} ({top['score']:.0%})" except Exception: return "❌ Error" return "❓ Unknown" # ── Load data ───────────────────────────────────────────────── @st.cache_data def load_data(): portfolio = pd.read_csv("portfolio_output.csv") risk_metrics = pd.read_csv("risk_metrics_output.csv") daily_returns = pd.read_csv("portfolio_daily_returns_output.csv", parse_dates=["date"]) return portfolio, risk_metrics, daily_returns try: portfolio, risk_metrics, daily_returns = load_data() data_loaded = True except FileNotFoundError: data_loaded = False # ── Sidebar ─────────────────────────────────────────────────── st.sidebar.title("Portfolio Monitor") st.sidebar.caption("ESCP — Applied Data Science Workshop") # ── Main title ──────────────────────────────────────────────── st.title("📈 Portfolio Monitoring Dashboard") st.caption("Real-time portfolio performance, risk alerts & AI-powered news sentiment") st.markdown("""

👥 Group Project — Clément De Ceukeleire · Laure Dumont · Matéo François · Romain Prudhon

ESCP Business School — Applied Data Science Workshop

""", unsafe_allow_html=True) st.divider() # ── Demo mode if no CSV ─────────────────────────────────────── if not data_loaded: st.warning( "⚠️ No data files found. Showing demo data. " "Upload your CSV files to see real results." ) np.random.seed(42) portfolio = pd.DataFrame({ "Ticker": ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"], "Friendly name": ["Apple", "Microsoft", "Nvidia", "Alphabet", "Amazon"], "market_value": [12000, 9500, 8200, 6100, 5400], "invested_amount": [10000, 8000, 5000, 5500, 6000], "unrealized_pnl": [2000, 1500, 3200, 600, -600], "cumulative_realized_pnl":[500, 300, 200, 100, 50], "total_pnl": [2500, 1800, 3400, 700, -550], "weight": [0.29, 0.23, 0.20, 0.15, 0.13], "asset_concentration_flag": [False, False, False, False, False], "stressed_value": [10200, 8075, 6970, 5185, 4590], "stress_test_loss": [-1800,-1425,-1230, -915, -810], "alert_level": ["Normal","Normal","Normal", "Normal","Warning loss"], }) portfolio["unrealized_return_pct"] = ( portfolio["unrealized_pnl"] / portfolio["invested_amount"] ) dates = pd.date_range(end=pd.Timestamp.today(), periods=120, freq="B") daily_returns = pd.DataFrame({ "date": dates, "portfolio_daily_returns": np.random.normal(0.0005, 0.012, 120) }) risk_metrics = pd.DataFrame({ "Metric": ["Mean daily return", "Daily volatility", "Annualized volatility", "Worst daily return", "Best daily return", "Sharpe ratio"], "Value": [0.0005, 0.012, 0.190, -0.032, 0.028, 0.66] }) # ── Ticker filter ───────────────────────────────────────────── ticker_list = ["All"] + sorted(portfolio["Ticker"].dropna().unique().tolist()) selected = st.sidebar.selectbox("Filter by asset", ticker_list) pv = portfolio if selected == "All" else portfolio[portfolio["Ticker"] == selected] # ── KPI Cards ───────────────────────────────────────────────── st.subheader("Portfolio Summary") c1, c2, c3, c4 = st.columns(4) c1.metric("💰 Invested", f"{pv['invested_amount'].sum():,.0f} €") c2.metric("📊 Market Value", f"{pv['market_value'].sum():,.0f} €") c3.metric("📈 Total P&L", f"{pv['total_pnl'].sum():,.0f} €") c4.metric("🏦 Positions", int(pv["Ticker"].nunique())) st.divider() # ── Charts row 1 ────────────────────────────────────────────── col_l, col_r = st.columns(2) with col_l: st.subheader("🥧 Portfolio Allocation") fig_pie = px.pie(pv, names="Ticker", values="market_value", hole=0.35) fig_pie.update_traces(textinfo="percent+label") st.plotly_chart(fig_pie, use_container_width=True) with col_r: st.subheader("📊 Market Value by Asset") color_col = "alert_level" if "alert_level" in pv.columns else "Ticker" fig_bar = px.bar( pv.sort_values("market_value", ascending=False), x="Ticker", y="market_value", color=color_col, color_discrete_map={ "Normal": "#2ecc71", "Warning loss": "#f39c12", "Critical loss": "#e74c3c" } ) st.plotly_chart(fig_bar, use_container_width=True) # ── Cumulative return ───────────────────────────────────────── st.subheader("📉 Cumulative Portfolio Return") daily_returns["cumulative_return"] = ( (1 + daily_returns["portfolio_daily_returns"]).cumprod() - 1 ) fig_line = px.line(daily_returns, x="date", y="cumulative_return", labels={"cumulative_return": "Cumulative Return", "date": "Date"}) fig_line.add_hline(y=0, line_dash="dash", line_color="black") fig_line.update_traces(line_color="#3498db") st.plotly_chart(fig_line, use_container_width=True) # ── Unrealized return scatter ────────────────────────────────── st.subheader("⚡ Unrealized Return by Asset") if "unrealized_return_pct" in pv.columns: fig_ret = px.bar( pv.sort_values("unrealized_return_pct"), x="Ticker", y="unrealized_return_pct", color=pv["unrealized_return_pct"].apply( lambda x: "Gain" if x >= 0 else "Loss" ), color_discrete_map={"Gain": "#2ecc71", "Loss": "#e74c3c"}, labels={"unrealized_return_pct": "Unrealized Return %"} ) fig_ret.add_hline(y=0, line_dash="dash", line_color="black") st.plotly_chart(fig_ret, use_container_width=True) # ── Risk metrics ────────────────────────────────────────────── st.subheader("🔬 Risk Metrics") st.dataframe(risk_metrics, use_container_width=True, hide_index=True) # ── Stress test ─────────────────────────────────────────────── if "stressed_value" in pv.columns: st.subheader("💥 Stress Test (−15% market shock)") sk1, sk2, sk3 = st.columns(3) sk1.metric("Current Value", f"{pv['market_value'].sum():,.0f} €") sk2.metric("Stressed Value", f"{pv['stressed_value'].sum():,.0f} €", delta=f"{pv['stress_test_loss'].sum():,.0f} €") sk3.metric("Estimated Loss", f"{pv['stress_test_loss'].sum():,.0f} €") # ── Alert table ─────────────────────────────────────────────── st.subheader("🚨 Risk Alert Table") alert_cols = [c for c in ["Ticker", "market_value", "weight", "unrealized_return_pct", "alert_level", "asset_concentration_flag"] if c in pv.columns] st.dataframe(pv[alert_cols].sort_values("market_value", ascending=False), use_container_width=True, hide_index=True) st.divider() # ── AI Sentiment Analysis ───────────────────────────────────── st.subheader("🤖 AI Sentiment Analysis (FinBERT)") st.caption("Powered by Hugging Face — ProsusAI/finbert") news_examples = { "AAPL": "Apple reports record quarterly earnings driven by iPhone sales", "MSFT": "Microsoft faces antitrust investigation in European markets", "NVDA": "Nvidia surges on strong AI chip demand forecast", "GOOGL": "Alphabet announces major layoffs amid cost-cutting efforts", "AMZN": "Amazon expands logistics network with new warehouse openings", } st.info( "Enter a news headline below and click Analyze to get " "an AI-powered sentiment score using FinBERT, " "a model trained specifically on financial text." ) col_input, col_btn = st.columns([4, 1]) with col_input: headline = st.text_input( "News headline", value="Apple reports record quarterly earnings driven by iPhone sales" ) with col_btn: st.write("") st.write("") run_sentiment = st.button("🔍 Analyze") if run_sentiment and headline: with st.spinner("Calling FinBERT model..."): sentiment = analyze_sentiment(headline) st.success(f"**Sentiment result:** {sentiment}") # Pre-loaded examples if st.checkbox("Show sentiment for example headlines"): results = [] for ticker, text in news_examples.items(): with st.spinner(f"Analyzing {ticker}..."): sent = analyze_sentiment(text) results.append({"Ticker": ticker, "Headline": text, "Sentiment": sent}) st.dataframe(pd.DataFrame(results), use_container_width=True, hide_index=True) st.divider() # ── Download ────────────────────────────────────────────────── st.download_button( label="⬇️ Download Portfolio Table (CSV)", data=portfolio.to_csv(index=False).encode("utf-8"), file_name="portfolio_monitoring_output.csv", mime="text/csv" ) st.caption("ESCP Business School — Applied Data Science Workshop | Group Project")