Spaces:
Running
Running
| 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 βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(""" | |
| <div style=" | |
| background-color: #1A3C6E; | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| "> | |
| <p style="color: white; font-size: 13px; margin: 0; text-align: center;"> | |
| π₯ <b>Group Project</b> β | |
| ClΓ©ment De Ceukeleire Β· Laure Dumont Β· MatΓ©o FranΓ§ois Β· Romain Prudhon | |
| </p> | |
| <p style="color: #A9CCE3; font-size: 12px; margin: 4px 0 0 0; text-align: center;"> | |
| ESCP Business School β Applied Data Science Workshop | |
| </p> | |
| </div> | |
| """, 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") |