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")