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

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +134 -399
src/streamlit_app.py CHANGED
@@ -2,40 +2,23 @@ import streamlit as st
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,462 +28,214 @@ def analyze_sentiment(text: str) -> str:
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
- )
 
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
  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")