ClementDeCeukeleire commited on
Commit
e3daab3
Β·
verified Β·
1 Parent(s): a5dd63f

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +399 -134
src/streamlit_app.py CHANGED
@@ -2,23 +2,40 @@ import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
  import plotly.express as px
 
5
  import requests
6
  import os
7
 
8
- # ── Page config ──────────────────────────────────────────────
9
  st.set_page_config(
10
  page_title="Portfolio Monitoring Dashboard",
11
  page_icon="πŸ“ˆ",
12
  layout="wide"
13
  )
14
 
15
- # ── Hugging Face AI (FinBERT sentiment) ──────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  HF_API_KEY = os.environ.get("HF_API_KEY", "")
17
 
18
  def analyze_sentiment(text: str) -> str:
19
- """Calls FinBERT on Hugging Face to get financial sentiment."""
20
  if not HF_API_KEY:
21
- return "⚠️ No API key"
22
  url = "https://api-inference.huggingface.co/models/ProsusAI/finbert"
23
  headers = {"Authorization": f"Bearer {HF_API_KEY}"}
24
  try:
@@ -28,214 +45,462 @@ def analyze_sentiment(text: str) -> str:
28
  if isinstance(result, list) and result:
29
  top = max(result[0], key=lambda x: x["score"])
30
  emoji = {"positive": "🟒", "negative": "πŸ”΄",
31
- "neutral": "🟑"}.get(top["label"].lower(), "βšͺ")
32
  return f"{emoji} {top['label'].capitalize()} ({top['score']:.0%})"
33
  except Exception:
34
- return "❌ Error"
35
  return "❓ Unknown"
36
 
37
- # ── Load data ─────────────────────────────────────────────────
38
- @st.cache_data
39
- def load_data():
40
- portfolio = pd.read_csv("portfolio_output.csv")
41
- risk_metrics = pd.read_csv("risk_metrics_output.csv")
42
- daily_returns = pd.read_csv("portfolio_daily_returns_output.csv",
43
- parse_dates=["date"])
44
- return portfolio, risk_metrics, daily_returns
45
-
46
- try:
47
- portfolio, risk_metrics, daily_returns = load_data()
48
- data_loaded = True
49
- except FileNotFoundError:
50
- data_loaded = False
51
-
52
  # ── Sidebar ───────────────────────────────────────────────────
53
- st.sidebar.title("Portfolio Monitor")
54
- st.sidebar.caption("ESCP β€” Applied Data Science Workshop")
 
 
 
 
 
 
 
55
 
56
- # ── Main title ────────────────────────────────────────────────
57
- st.title("πŸ“ˆ Portfolio Monitoring Dashboard")
58
- st.caption("Real-time portfolio performance, risk alerts & AI-powered news sentiment")
59
- st.divider()
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
- # ── Demo mode if no CSV ───────────────────────────────────────
62
- if not data_loaded:
63
- st.warning(
64
- "⚠️ No data files found. Showing demo data. "
65
- "Upload your CSV files to see real results."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  np.random.seed(42)
 
68
  portfolio = pd.DataFrame({
69
- "Ticker": ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
70
- "Friendly name": ["Apple", "Microsoft", "Nvidia",
71
- "Alphabet", "Amazon"],
72
- "market_value": [12000, 9500, 8200, 6100, 5400],
73
- "invested_amount": [10000, 8000, 5000, 5500, 6000],
74
- "unrealized_pnl": [2000, 1500, 3200, 600, -600],
75
- "cumulative_realized_pnl":[500, 300, 200, 100, 50],
76
- "total_pnl": [2500, 1800, 3400, 700, -550],
77
- "weight": [0.29, 0.23, 0.20, 0.15, 0.13],
78
- "asset_concentration_flag": [False, False, False, False, False],
79
- "stressed_value": [10200, 8075, 6970, 5185, 4590],
80
- "stress_test_loss": [-1800,-1425,-1230, -915, -810],
81
- "alert_level": ["Normal","Normal","Normal",
82
- "Normal","Warning loss"],
 
 
 
 
 
 
83
  })
84
- portfolio["unrealized_return_pct"] = (
85
- portfolio["unrealized_pnl"] / portfolio["invested_amount"]
86
- )
87
 
88
- dates = pd.date_range(end=pd.Timestamp.today(), periods=120, freq="B")
 
 
89
  daily_returns = pd.DataFrame({
90
  "date": dates,
91
- "portfolio_daily_returns": np.random.normal(0.0005, 0.012, 120)
92
  })
 
 
 
93
  risk_metrics = pd.DataFrame({
94
- "Metric": ["Mean daily return", "Daily volatility",
95
- "Annualized volatility", "Worst daily return",
96
- "Best daily return", "Sharpe ratio"],
97
- "Value": [0.0005, 0.012, 0.190, -0.032, 0.028, 0.66]
 
 
98
  })
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  # ── Ticker filter ─────────────────────────────────────────────
101
  ticker_list = ["All"] + sorted(portfolio["Ticker"].dropna().unique().tolist())
102
- selected = st.sidebar.selectbox("Filter by asset", ticker_list)
 
 
 
 
103
  pv = portfolio if selected == "All" else portfolio[portfolio["Ticker"] == selected]
104
 
 
 
105
  # ── KPI Cards ─────────────────────────────────────────────────
106
  st.subheader("Portfolio Summary")
 
107
  c1, c2, c3, c4 = st.columns(4)
108
- c1.metric("πŸ’° Invested", f"{pv['invested_amount'].sum():,.0f} €")
109
- c2.metric("πŸ“Š Market Value", f"{pv['market_value'].sum():,.0f} €")
110
- c3.metric("πŸ“ˆ Total P&L", f"{pv['total_pnl'].sum():,.0f} €")
111
- c4.metric("🏦 Positions", int(pv["Ticker"].nunique()))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  st.divider()
113
 
114
- # ── Charts row 1 ──────────────────────────────────────────────
115
  col_l, col_r = st.columns(2)
116
 
117
  with col_l:
118
  st.subheader("πŸ₯§ Portfolio Allocation")
119
- fig_pie = px.pie(pv, names="Ticker", values="market_value",
120
- hole=0.35)
 
 
 
 
121
  fig_pie.update_traces(textinfo="percent+label")
 
122
  st.plotly_chart(fig_pie, use_container_width=True)
123
 
124
  with col_r:
125
  st.subheader("πŸ“Š Market Value by Asset")
126
- color_col = "alert_level" if "alert_level" in pv.columns else "Ticker"
 
 
 
 
 
127
  fig_bar = px.bar(
128
  pv.sort_values("market_value", ascending=False),
129
- x="Ticker", y="market_value", color=color_col,
130
- color_discrete_map={
131
- "Normal": "#2ecc71",
132
- "Warning loss": "#f39c12",
133
- "Critical loss": "#e74c3c"
134
- }
135
  )
 
136
  st.plotly_chart(fig_bar, use_container_width=True)
137
 
138
  # ── Cumulative return ─────────────────────────────────────────
139
  st.subheader("πŸ“‰ Cumulative Portfolio Return")
 
 
140
  daily_returns["cumulative_return"] = (
141
  (1 + daily_returns["portfolio_daily_returns"]).cumprod() - 1
142
  )
143
- fig_line = px.line(daily_returns, x="date", y="cumulative_return",
144
- labels={"cumulative_return": "Cumulative Return",
145
- "date": "Date"})
146
- fig_line.add_hline(y=0, line_dash="dash", line_color="black")
147
- fig_line.update_traces(line_color="#3498db")
 
 
 
148
  st.plotly_chart(fig_line, use_container_width=True)
149
 
150
- # ── Unrealized return scatter ──────────────────────────────────
151
- st.subheader("⚑ Unrealized Return by Asset")
152
- if "unrealized_return_pct" in pv.columns:
153
- fig_ret = px.bar(
154
- pv.sort_values("unrealized_return_pct"),
155
- x="Ticker", y="unrealized_return_pct",
156
- color=pv["unrealized_return_pct"].apply(
157
- lambda x: "Gain" if x >= 0 else "Loss"
158
- ),
159
- color_discrete_map={"Gain": "#2ecc71", "Loss": "#e74c3c"},
160
- labels={"unrealized_return_pct": "Unrealized Return %"}
161
- )
162
- fig_ret.add_hline(y=0, line_dash="dash", line_color="black")
163
- st.plotly_chart(fig_ret, use_container_width=True)
 
 
 
 
 
 
164
 
165
  # ── Risk metrics ──────────────────────────────────────────────
166
  st.subheader("πŸ”¬ Risk Metrics")
 
167
  st.dataframe(risk_metrics, use_container_width=True, hide_index=True)
 
168
 
169
  # ── Stress test ───────────────────────────────────────────────
170
- if "stressed_value" in pv.columns:
171
- st.subheader("πŸ’₯ Stress Test (βˆ’15% market shock)")
172
- sk1, sk2, sk3 = st.columns(3)
173
- sk1.metric("Current Value", f"{pv['market_value'].sum():,.0f} €")
174
- sk2.metric("Stressed Value", f"{pv['stressed_value'].sum():,.0f} €",
175
- delta=f"{pv['stress_test_loss'].sum():,.0f} €")
176
- sk3.metric("Estimated Loss", f"{pv['stress_test_loss'].sum():,.0f} €")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  # ── Alert table ───────────────────────────────────────────────
179
  st.subheader("🚨 Risk Alert Table")
180
- alert_cols = [c for c in ["Ticker", "market_value", "weight",
181
- "unrealized_return_pct", "alert_level",
182
- "asset_concentration_flag"] if c in pv.columns]
183
- st.dataframe(pv[alert_cols].sort_values("market_value", ascending=False),
184
- use_container_width=True, hide_index=True)
185
 
186
- st.divider()
 
 
 
 
 
187
 
188
- # ── AI Sentiment Analysis ─────────────────────────────────────
189
- st.subheader("πŸ€– AI Sentiment Analysis (FinBERT)")
190
- st.caption("Powered by Hugging Face β€” ProsusAI/finbert")
 
 
 
 
 
 
 
191
 
192
- news_examples = {
193
- "AAPL": "Apple reports record quarterly earnings driven by iPhone sales",
194
- "MSFT": "Microsoft faces antitrust investigation in European markets",
195
- "NVDA": "Nvidia surges on strong AI chip demand forecast",
196
- "GOOGL": "Alphabet announces major layoffs amid cost-cutting efforts",
197
- "AMZN": "Amazon expands logistics network with new warehouse openings",
198
- }
199
 
200
- st.info(
201
- "Enter a news headline below and click Analyze to get "
202
- "an AI-powered sentiment score using FinBERT, "
203
- "a model trained specifically on financial text."
 
204
  )
205
 
 
 
 
 
 
 
 
 
 
206
  col_input, col_btn = st.columns([4, 1])
207
  with col_input:
208
  headline = st.text_input(
209
- "News headline",
210
- value="Apple reports record quarterly earnings driven by iPhone sales"
 
211
  )
212
  with col_btn:
213
  st.write("")
214
  st.write("")
215
- run_sentiment = st.button("πŸ” Analyze")
216
 
217
- if run_sentiment and headline:
218
  with st.spinner("Calling FinBERT model..."):
219
- sentiment = analyze_sentiment(headline)
220
- st.success(f"**Sentiment result:** {sentiment}")
221
-
222
- # Pre-loaded examples
223
- if st.checkbox("Show sentiment for example headlines"):
224
- results = []
225
- for ticker, text in news_examples.items():
226
- with st.spinner(f"Analyzing {ticker}..."):
227
- sent = analyze_sentiment(text)
228
- results.append({"Ticker": ticker, "Headline": text, "Sentiment": sent})
229
- st.dataframe(pd.DataFrame(results), use_container_width=True, hide_index=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  st.divider()
232
 
233
  # ── Download ──────────────────────────────────────────────────
 
 
234
  st.download_button(
235
- label="⬇️ Download Portfolio Table (CSV)",
236
  data=portfolio.to_csv(index=False).encode("utf-8"),
237
  file_name="portfolio_monitoring_output.csv",
238
- mime="text/csv"
 
239
  )
240
 
241
- st.caption("ESCP Business School β€” Applied Data Science Workshop | Group Project")
 
 
 
 
 
2
  import pandas as pd
3
  import numpy as np
4
  import plotly.express as px
5
+ import plotly.graph_objects as go
6
  import requests
7
  import os
8
 
 
9
  st.set_page_config(
10
  page_title="Portfolio Monitoring Dashboard",
11
  page_icon="πŸ“ˆ",
12
  layout="wide"
13
  )
14
 
15
+ # ── Custom CSS ────────────────────────────────────────────────
16
+ st.markdown("""
17
+ <style>
18
+ .block-container { padding-top: 2rem; }
19
+ .metric-label { font-size: 13px !important; }
20
+ .stAlert { border-radius: 10px; }
21
+ div[data-testid="stSidebarContent"] { padding-top: 1.5rem; }
22
+ .step-box {
23
+ background: #f8f9fa;
24
+ border-left: 4px solid #3498db;
25
+ border-radius: 6px;
26
+ padding: 0.8rem 1rem;
27
+ margin-bottom: 0.6rem;
28
+ font-size: 14px;
29
+ }
30
+ </style>
31
+ """, unsafe_allow_html=True)
32
+
33
+ # ── Hugging Face AI ───────────────────────────────────────────
34
  HF_API_KEY = os.environ.get("HF_API_KEY", "")
35
 
36
  def analyze_sentiment(text: str) -> str:
 
37
  if not HF_API_KEY:
38
+ return "⚠️ No API key configured"
39
  url = "https://api-inference.huggingface.co/models/ProsusAI/finbert"
40
  headers = {"Authorization": f"Bearer {HF_API_KEY}"}
41
  try:
 
45
  if isinstance(result, list) and result:
46
  top = max(result[0], key=lambda x: x["score"])
47
  emoji = {"positive": "🟒", "negative": "πŸ”΄",
48
+ "neutral": "🟑"}.get(top["label"].lower(), "βšͺ")
49
  return f"{emoji} {top['label'].capitalize()} ({top['score']:.0%})"
50
  except Exception:
51
+ return "❌ Connection error"
52
  return "❓ Unknown"
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  # ── Sidebar ───────────────────────────────────────────────────
55
+ with st.sidebar:
56
+ st.image(
57
+ "https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/"
58
+ "Matplotlib_icon.svg/120px-Matplotlib_icon.svg.png",
59
+ width=50
60
+ )
61
+ st.title("Portfolio Monitor")
62
+ st.caption("ESCP β€” Applied Data Science Workshop")
63
+ st.divider()
64
 
65
+ # ── How to use ────────────────────────────────────────────
66
+ with st.expander("❓ How to use this app", expanded=True):
67
+ st.markdown("""
68
+ <div class='step-box'>
69
+ <b>Step 1</b> β€” Run the Google Colab notebook with your transaction data
70
+ </div>
71
+ <div class='step-box'>
72
+ <b>Step 2</b> β€” Download the 4 CSV files generated by Colab
73
+ </div>
74
+ <div class='step-box'>
75
+ <b>Step 3</b> β€” Upload them below using the file uploader
76
+ </div>
77
+ <div class='step-box'>
78
+ <b>Step 4</b> β€” Your dashboard updates automatically πŸŽ‰
79
+ </div>
80
+ """, unsafe_allow_html=True)
81
 
82
+ st.divider()
83
+
84
+ # ── File uploaders ────────────────────────────────────────
85
+ st.subheader("πŸ“‚ Upload your CSV files")
86
+ st.caption("Generated by the Google Colab notebook")
87
+
88
+ f_portfolio = st.file_uploader(
89
+ "portfolio_output.csv",
90
+ type="csv", key="portfolio",
91
+ help="Main portfolio table with positions and P&L"
92
+ )
93
+ f_risk = st.file_uploader(
94
+ "risk_metrics_output.csv",
95
+ type="csv", key="risk",
96
+ help="Risk indicators computed from daily returns"
97
+ )
98
+ f_returns = st.file_uploader(
99
+ "portfolio_daily_returns_output.csv",
100
+ type="csv", key="returns",
101
+ help="Daily portfolio return series"
102
  )
103
+ f_features = st.file_uploader(
104
+ "asset_features_output.csv",
105
+ type="csv", key="features",
106
+ help="Per-asset features used by the ML model"
107
+ )
108
+
109
+ st.divider()
110
+ use_demo = st.toggle("🎯 Use demo data instead", value=False,
111
+ help="Load synthetic data to preview the dashboard")
112
+
113
+ # ── Load data ─────────────────────────────────────────────────
114
+ @st.cache_data
115
+ def load_demo():
116
  np.random.seed(42)
117
+
118
  portfolio = pd.DataFrame({
119
+ "Ticker": ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
120
+ "Friendly name": ["Apple Inc.", "Microsoft Corp.",
121
+ "Nvidia Corp.", "Alphabet Inc.", "Amazon.com"],
122
+ "cumulated_qte": [50, 30, 20, 15, 25],
123
+ "average_entry": [158.0, 290.0, 410.0, 128.0, 165.0],
124
+ "last_price": [213.5, 378.9, 875.4, 172.3, 198.7],
125
+ "invested_amount": [7900, 8700, 8200, 1920, 4125],
126
+ "market_value": [10675, 11367, 17508, 2585, 4968],
127
+ "unrealized_pnl": [2775, 2667, 9308, 665, 843],
128
+ "cumulative_realized_pnl": [420, 310, 180, 95, 60],
129
+ "total_pnl": [3195, 2977, 9488, 760, 903],
130
+ "weight": [0.225, 0.239, 0.368, 0.054, 0.104],
131
+ "asset_concentration_flag":[False, False, True, False, False],
132
+ "stressed_value": [9074, 9662, 14882, 2197, 4222],
133
+ "stress_test_loss": [-1601,-1705,-2626,-388,-745],
134
+ "alert_level": ["Normal","Normal","Normal","Normal","Normal"],
135
+ "unrealized_return_pct": [0.35, 0.31, 1.13, 0.35, 0.20],
136
+ "volatility_30d": [0.22, 0.19, 0.41, 0.20, 0.23],
137
+ "momentum_30d": [0.05, 0.04, 0.12, 0.03, 0.06],
138
+ "max_drawdown": [-0.15,-0.12,-0.30,-0.13,-0.18],
139
  })
 
 
 
140
 
141
+ dates = pd.date_range(end=pd.Timestamp.today(), periods=120, freq="B")
142
+ rets = np.random.normal(0.0006, 0.011, 120)
143
+ rets[15] = -0.032; rets[47] = -0.028; rets[83] = 0.031
144
  daily_returns = pd.DataFrame({
145
  "date": dates,
146
+ "portfolio_daily_returns": rets
147
  })
148
+
149
+ r = daily_returns["portfolio_daily_returns"]
150
+ sharpe = (r.mean() / r.std()) * np.sqrt(252)
151
  risk_metrics = pd.DataFrame({
152
+ "Metric": ["Mean daily return","Daily volatility",
153
+ "Annualized volatility","Worst day",
154
+ "Best day","Sharpe ratio (approx.)"],
155
+ "Value": [f"{r.mean():.4%}", f"{r.std():.4%}",
156
+ f"{r.std()*np.sqrt(252):.2%}", f"{r.min():.4%}",
157
+ f"{r.max():.4%}", f"{sharpe:.2f}"]
158
  })
159
 
160
+ return portfolio, daily_returns, risk_metrics
161
+
162
+ def load_uploaded(f_portfolio, f_risk, f_returns):
163
+ portfolio = pd.read_csv(f_portfolio)
164
+ risk_metrics = pd.read_csv(f_risk)
165
+ daily_returns = pd.read_csv(f_returns, parse_dates=["date"])
166
+ return portfolio, daily_returns, risk_metrics
167
+
168
+ # ── Decide which data to use ────��─────────────────────────────
169
+ all_uploaded = all([f_portfolio, f_risk, f_returns])
170
+
171
+ if use_demo:
172
+ portfolio, daily_returns, risk_metrics = load_demo()
173
+ data_source = "demo"
174
+ elif all_uploaded:
175
+ try:
176
+ portfolio, daily_returns, risk_metrics = load_uploaded(
177
+ f_portfolio, f_risk, f_returns
178
+ )
179
+ data_source = "real"
180
+ except Exception as e:
181
+ st.error(f"❌ Error reading files: {e}")
182
+ st.stop()
183
+ else:
184
+ data_source = "none"
185
+
186
+ # ── Main title ────────────────────────────────────────────────
187
+ st.title("πŸ“ˆ Portfolio Monitoring Dashboard")
188
+ st.caption(
189
+ "Real-time portfolio performance, risk alerts & "
190
+ "AI-powered news sentiment analysis"
191
+ )
192
+
193
+ if data_source == "demo":
194
+ st.info("🎯 **Demo mode** β€” showing synthetic data. "
195
+ "Upload your CSV files in the sidebar to see your real portfolio.")
196
+ elif data_source == "real":
197
+ st.success("βœ… **Your portfolio data is loaded.** "
198
+ "All figures below reflect your real transactions.")
199
+ else:
200
+ # ── Onboarding screen ──────────────────────────────────────
201
+ st.divider()
202
+ st.subheader("πŸ‘‹ Welcome! Let's get started.")
203
+
204
+ col1, col2, col3, col4 = st.columns(4)
205
+
206
+ with col1:
207
+ st.markdown("""
208
+ ### 1️⃣ Run Colab
209
+ Open the **Google Colab notebook** and run all cells with your transaction data.
210
+ """)
211
+ with col2:
212
+ st.markdown("""
213
+ ### 2️⃣ Download CSVs
214
+ After execution, download the **4 CSV files** from the Colab file panel (πŸ“ on the left).
215
+ """)
216
+ with col3:
217
+ st.markdown("""
218
+ ### 3️⃣ Upload here
219
+ Use the **file uploaders in the sidebar** (πŸ‘ˆ) to upload your 4 CSV files.
220
+ """)
221
+ with col4:
222
+ st.markdown("""
223
+ ### 4️⃣ Explore
224
+ Your **dashboard updates instantly** with your real portfolio data and live analytics.
225
+ """)
226
+
227
+ st.divider()
228
+ st.markdown("""
229
+ #### πŸ“ The 4 files you need to upload:
230
+ | File | Content |
231
+ |------|---------|
232
+ | `portfolio_output.csv` | Your positions, P&L, weights, alerts |
233
+ | `risk_metrics_output.csv` | Volatility, Sharpe ratio, best/worst day |
234
+ | `portfolio_daily_returns_output.csv` | Daily return series for the chart |
235
+ | `asset_features_output.csv` | Per-asset features from the ML model |
236
+
237
+ > πŸ’‘ **Don't have the files yet?** Toggle **"Use demo data"** in the sidebar
238
+ > to preview the full dashboard with synthetic data.
239
+ """)
240
+ st.stop()
241
+
242
+ st.divider()
243
+
244
+ # ── Computed fields ───────────────────────────────────────────
245
+ if "unrealized_return_pct" not in portfolio.columns:
246
+ portfolio["unrealized_return_pct"] = np.where(
247
+ portfolio["invested_amount"] > 0,
248
+ portfolio["unrealized_pnl"] / portfolio["invested_amount"],
249
+ np.nan
250
+ )
251
+
252
  # ── Ticker filter ─────────────────────────────────────────────
253
  ticker_list = ["All"] + sorted(portfolio["Ticker"].dropna().unique().tolist())
254
+ selected = st.selectbox(
255
+ "πŸ” Filter by asset",
256
+ ticker_list,
257
+ help="Select a specific asset or view the full portfolio"
258
+ )
259
  pv = portfolio if selected == "All" else portfolio[portfolio["Ticker"] == selected]
260
 
261
+ st.divider()
262
+
263
  # ── KPI Cards ─────────────────────────────────────────────────
264
  st.subheader("Portfolio Summary")
265
+
266
  c1, c2, c3, c4 = st.columns(4)
267
+ c1.metric(
268
+ "πŸ’° Total Invested",
269
+ f"{pv['invested_amount'].sum():,.0f} €",
270
+ help="Sum of all buy amounts including fees"
271
+ )
272
+ c2.metric(
273
+ "πŸ“Š Market Value",
274
+ f"{pv['market_value'].sum():,.0f} €",
275
+ delta=f"{pv['unrealized_pnl'].sum():,.0f} € unrealized",
276
+ help="Current value based on latest prices"
277
+ )
278
+ c3.metric(
279
+ "πŸ“ˆ Total P&L",
280
+ f"{pv['total_pnl'].sum():,.0f} €",
281
+ help="Realized + unrealized gains and losses"
282
+ )
283
+ c4.metric(
284
+ "🏦 Active Positions",
285
+ int(pv["Ticker"].nunique()),
286
+ help="Number of assets currently held"
287
+ )
288
  st.divider()
289
 
290
+ # ── Allocation + Bar ──────────────────────────────────────────
291
  col_l, col_r = st.columns(2)
292
 
293
  with col_l:
294
  st.subheader("πŸ₯§ Portfolio Allocation")
295
+ st.caption("Current weights by market value")
296
+ fig_pie = px.pie(
297
+ pv, names="Ticker", values="market_value",
298
+ hole=0.4,
299
+ color_discrete_sequence=px.colors.qualitative.Set2
300
+ )
301
  fig_pie.update_traces(textinfo="percent+label")
302
+ fig_pie.update_layout(showlegend=False, margin=dict(t=10, b=10))
303
  st.plotly_chart(fig_pie, use_container_width=True)
304
 
305
  with col_r:
306
  st.subheader("πŸ“Š Market Value by Asset")
307
+ st.caption("Color = alert level based on unrealized return")
308
+ color_map = {
309
+ "Normal": "#2ecc71",
310
+ "Warning loss": "#f39c12",
311
+ "Critical loss": "#e74c3c"
312
+ }
313
  fig_bar = px.bar(
314
  pv.sort_values("market_value", ascending=False),
315
+ x="Ticker", y="market_value",
316
+ color="alert_level",
317
+ color_discrete_map=color_map,
318
+ labels={"market_value": "Market Value (€)",
319
+ "alert_level": "Alert Level"}
 
320
  )
321
+ fig_bar.update_layout(margin=dict(t=10, b=10))
322
  st.plotly_chart(fig_bar, use_container_width=True)
323
 
324
  # ── Cumulative return ─────────────────────────────────────────
325
  st.subheader("πŸ“‰ Cumulative Portfolio Return")
326
+ st.caption("Based on daily weighted returns since portfolio inception")
327
+
328
  daily_returns["cumulative_return"] = (
329
  (1 + daily_returns["portfolio_daily_returns"]).cumprod() - 1
330
  )
331
+ fig_line = px.line(
332
+ daily_returns, x="date", y="cumulative_return",
333
+ labels={"cumulative_return": "Cumulative Return", "date": "Date"}
334
+ )
335
+ fig_line.add_hline(y=0, line_dash="dash", line_color="gray",
336
+ annotation_text="Break-even")
337
+ fig_line.update_traces(line_color="#3498db", line_width=2.5)
338
+ fig_line.update_layout(margin=dict(t=10, b=10))
339
  st.plotly_chart(fig_line, use_container_width=True)
340
 
341
+ # ── Unrealized return ─────────────────────────────────────────
342
+ st.subheader("⚑ Unrealized Return % by Asset")
343
+ st.caption("Percentage gain or loss on current open positions")
344
+
345
+ pv2 = pv.copy()
346
+ pv2["color"] = pv2["unrealized_return_pct"].apply(
347
+ lambda x: "Gain 🟒" if x >= 0 else "Loss πŸ”΄"
348
+ )
349
+ fig_ret = px.bar(
350
+ pv2.sort_values("unrealized_return_pct"),
351
+ x="Ticker", y="unrealized_return_pct",
352
+ color="color",
353
+ color_discrete_map={"Gain 🟒": "#2ecc71", "Loss πŸ”΄": "#e74c3c"},
354
+ text=pv2["unrealized_return_pct"].apply(lambda x: f"{x:.1%}"),
355
+ labels={"unrealized_return_pct": "Unrealized Return %", "color": ""}
356
+ )
357
+ fig_ret.add_hline(y=0, line_dash="dash", line_color="gray")
358
+ fig_ret.update_layout(margin=dict(t=10, b=10))
359
+ st.plotly_chart(fig_ret, use_container_width=True)
360
+ st.divider()
361
 
362
  # ── Risk metrics ──────────────────────────────────────────────
363
  st.subheader("πŸ”¬ Risk Metrics")
364
+ st.caption("Computed from the daily return series of your portfolio")
365
  st.dataframe(risk_metrics, use_container_width=True, hide_index=True)
366
+ st.divider()
367
 
368
  # ── Stress test ───────────────────────────────────────────────
369
+ st.subheader("πŸ’₯ Stress Test β€” Simulated βˆ’15% Market Shock")
370
+ st.caption(
371
+ "Estimates your portfolio loss if all positions dropped by 15% simultaneously. "
372
+ "This is a simplified scenario β€” real crashes are rarely uniform."
373
+ )
374
+
375
+ sk1, sk2, sk3 = st.columns(3)
376
+ sk1.metric("Current Value",
377
+ f"{pv['market_value'].sum():,.0f} €")
378
+ sk2.metric("Stressed Value",
379
+ f"{pv['stressed_value'].sum():,.0f} €",
380
+ delta=f"{pv['stress_test_loss'].sum():,.0f} €")
381
+ sk3.metric("Estimated Loss",
382
+ f"{pv['stress_test_loss'].sum():,.0f} €")
383
+
384
+ fig_stress = px.bar(
385
+ pv, x="Ticker",
386
+ y=["market_value", "stressed_value"],
387
+ barmode="group",
388
+ color_discrete_map={
389
+ "market_value": "#3498db",
390
+ "stressed_value": "#e74c3c"
391
+ },
392
+ labels={"value": "Value (€)", "variable": "Scenario"}
393
+ )
394
+ fig_stress.update_layout(margin=dict(t=10, b=10))
395
+ st.plotly_chart(fig_stress, use_container_width=True)
396
+ st.divider()
397
 
398
  # ── Alert table ───────────────────────────────────────────────
399
  st.subheader("🚨 Risk Alert Table")
400
+ st.caption(
401
+ "🟒 Normal = above βˆ’10% | "
402
+ "🟠 Warning = below βˆ’10% | "
403
+ "πŸ”΄ Critical = below βˆ’20%"
404
+ )
405
 
406
+ alert_cols = [c for c in [
407
+ "Ticker", "Friendly name", "market_value", "weight",
408
+ "unrealized_return_pct", "alert_level", "asset_concentration_flag"
409
+ ] if c in pv.columns]
410
+
411
+ alert_df = pv[alert_cols].copy()
412
 
413
+ if "market_value" in alert_df.columns:
414
+ alert_df["market_value"] = alert_df["market_value"].apply(
415
+ lambda x: f"{x:,.0f} €"
416
+ )
417
+ if "weight" in alert_df.columns:
418
+ alert_df["weight"] = alert_df["weight"].apply(lambda x: f"{x:.1%}")
419
+ if "unrealized_return_pct" in alert_df.columns:
420
+ alert_df["unrealized_return_pct"] = alert_df["unrealized_return_pct"].apply(
421
+ lambda x: f"{x:.1%}"
422
+ )
423
 
424
+ alert_df.columns = [c.replace("_", " ").title() for c in alert_df.columns]
425
+ st.dataframe(
426
+ alert_df.sort_values("Market Value", ascending=False),
427
+ use_container_width=True,
428
+ hide_index=True
429
+ )
430
+ st.divider()
431
 
432
+ # ── AI Sentiment ──────────────────────────────────────────────
433
+ st.subheader("πŸ€– AI News Sentiment Analysis")
434
+ st.caption(
435
+ "Powered by **FinBERT** (ProsusAI/finbert) β€” a model trained specifically "
436
+ "on financial news text via Hugging Face Inference API"
437
  )
438
 
439
+ if not HF_API_KEY:
440
+ st.warning(
441
+ "⚠️ **AI not activated.** To enable, go to your Space β†’ "
442
+ "Settings β†’ Repository secrets β†’ add `HF_API_KEY` with your "
443
+ "Hugging Face token."
444
+ )
445
+ else:
446
+ st.success("βœ… FinBERT connected and ready.")
447
+
448
  col_input, col_btn = st.columns([4, 1])
449
  with col_input:
450
  headline = st.text_input(
451
+ "πŸ“° Enter a financial news headline to analyze",
452
+ value="Apple reports record quarterly earnings driven by iPhone sales",
453
+ placeholder="e.g. Nvidia shares surge on AI chip demand..."
454
  )
455
  with col_btn:
456
  st.write("")
457
  st.write("")
458
+ run = st.button("πŸ” Analyze", use_container_width=True)
459
 
460
+ if run and headline:
461
  with st.spinner("Calling FinBERT model..."):
462
+ result = analyze_sentiment(headline)
463
+ st.success(f"**Sentiment result:** {result}")
464
+
465
+ with st.expander("πŸ“° Analyze example headlines for each asset in your portfolio"):
466
+ examples = {
467
+ "AAPL": "Apple reports record quarterly earnings driven by iPhone sales",
468
+ "MSFT": "Microsoft faces antitrust investigation in European markets",
469
+ "NVDA": "Nvidia surges on strong AI chip demand forecast",
470
+ "GOOGL": "Alphabet announces major layoffs amid cost-cutting efforts",
471
+ "AMZN": "Amazon expands logistics network with new warehouse openings",
472
+ }
473
+ if st.button("▢️ Run all sentiment analyses"):
474
+ rows = []
475
+ for ticker, text in examples.items():
476
+ with st.spinner(f"Analyzing {ticker}..."):
477
+ sent = analyze_sentiment(text)
478
+ rows.append({
479
+ "Ticker": ticker,
480
+ "Headline": text,
481
+ "Sentiment": sent
482
+ })
483
+ st.dataframe(
484
+ pd.DataFrame(rows),
485
+ use_container_width=True,
486
+ hide_index=True
487
+ )
488
 
489
  st.divider()
490
 
491
  # ── Download ──────────────────────────────────────────────────
492
+ st.subheader("⬇️ Export")
493
+ st.caption("Download your current portfolio snapshot as a CSV file")
494
  st.download_button(
495
+ label="Download Portfolio Table (CSV)",
496
  data=portfolio.to_csv(index=False).encode("utf-8"),
497
  file_name="portfolio_monitoring_output.csv",
498
+ mime="text/csv",
499
+ use_container_width=False
500
  )
501
 
502
+ st.divider()
503
+ st.caption(
504
+ "ESCP Business School β€” Applied Data Science Workshop | "
505
+ "Group Project | Portfolio Monitoring Tool"
506
+ )