ClementDeCeukeleire commited on
Commit
3e124ca
Β·
verified Β·
1 Parent(s): 3ddf748

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +245 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,247 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  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:
25
+ r = requests.post(url, headers=headers,
26
+ json={"inputs": text}, timeout=10)
27
+ result = r.json()
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.image(
54
+ "https://upload.wikimedia.org/wikipedia/commons/"
55
+ "3/3b/Chart_icon_NOUN_project.svg",
56
+ width=80
57
+ )
58
+ st.sidebar.title("Portfolio Monitor")
59
+ st.sidebar.caption("ESCP β€” Applied Data Science Workshop")
60
+
61
+ # ── Main title ────────────────────────────────────────────────
62
+ st.title("πŸ“ˆ Portfolio Monitoring Dashboard")
63
+ st.caption("Real-time portfolio performance, risk alerts & AI-powered news sentiment")
64
+ st.divider()
65
+
66
+ # ── Demo mode if no CSV ───────────────────────────────────────
67
+ if not data_loaded:
68
+ st.warning(
69
+ "⚠️ No data files found. Showing demo data. "
70
+ "Upload your CSV files to see real results."
71
+ )
72
+ np.random.seed(42)
73
+ portfolio = pd.DataFrame({
74
+ "Ticker": ["AAPL", "MSFT", "NVDA", "GOOGL", "AMZN"],
75
+ "Friendly name": ["Apple", "Microsoft", "Nvidia",
76
+ "Alphabet", "Amazon"],
77
+ "market_value": [12000, 9500, 8200, 6100, 5400],
78
+ "invested_amount": [10000, 8000, 5000, 5500, 6000],
79
+ "unrealized_pnl": [2000, 1500, 3200, 600, -600],
80
+ "cumulative_realized_pnl":[500, 300, 200, 100, 50],
81
+ "total_pnl": [2500, 1800, 3400, 700, -550],
82
+ "weight": [0.29, 0.23, 0.20, 0.15, 0.13],
83
+ "asset_concentration_flag": [False, False, False, False, False],
84
+ "stressed_value": [10200, 8075, 6970, 5185, 4590],
85
+ "stress_test_loss": [-1800,-1425,-1230, -915, -810],
86
+ "alert_level": ["Normal","Normal","Normal",
87
+ "Normal","Warning loss"],
88
+ })
89
+ portfolio["unrealized_return_pct"] = (
90
+ portfolio["unrealized_pnl"] / portfolio["invested_amount"]
91
+ )
92
+
93
+ dates = pd.date_range(end=pd.Timestamp.today(), periods=120, freq="B")
94
+ daily_returns = pd.DataFrame({
95
+ "date": dates,
96
+ "portfolio_daily_returns": np.random.normal(0.0005, 0.012, 120)
97
+ })
98
+ risk_metrics = pd.DataFrame({
99
+ "Metric": ["Mean daily return", "Daily volatility",
100
+ "Annualized volatility", "Worst daily return",
101
+ "Best daily return", "Sharpe ratio"],
102
+ "Value": [0.0005, 0.012, 0.190, -0.032, 0.028, 0.66]
103
+ })
104
+
105
+ # ── Ticker filter ─────────────────────────────────────────────
106
+ ticker_list = ["All"] + sorted(portfolio["Ticker"].dropna().unique().tolist())
107
+ selected = st.sidebar.selectbox("Filter by asset", ticker_list)
108
+ pv = portfolio if selected == "All" else portfolio[portfolio["Ticker"] == selected]
109
+
110
+ # ── KPI Cards ─────────────────────────────────────────────────
111
+ st.subheader("Portfolio Summary")
112
+ c1, c2, c3, c4 = st.columns(4)
113
+ c1.metric("πŸ’° Invested", f"{pv['invested_amount'].sum():,.0f} €")
114
+ c2.metric("πŸ“Š Market Value", f"{pv['market_value'].sum():,.0f} €")
115
+ c3.metric("πŸ“ˆ Total P&L", f"{pv['total_pnl'].sum():,.0f} €")
116
+ c4.metric("🏦 Positions", int(pv["Ticker"].nunique()))
117
+ st.divider()
118
+
119
+ # ── Charts row 1 ──────────────────────────────────────────────
120
+ col_l, col_r = st.columns(2)
121
+
122
+ with col_l:
123
+ st.subheader("πŸ₯§ Portfolio Allocation")
124
+ fig_pie = px.pie(pv, names="Ticker", values="market_value",
125
+ hole=0.35)
126
+ fig_pie.update_traces(textinfo="percent+label")
127
+ st.plotly_chart(fig_pie, use_container_width=True)
128
+
129
+ with col_r:
130
+ st.subheader("πŸ“Š Market Value by Asset")
131
+ color_col = "alert_level" if "alert_level" in pv.columns else "Ticker"
132
+ fig_bar = px.bar(
133
+ pv.sort_values("market_value", ascending=False),
134
+ x="Ticker", y="market_value", color=color_col,
135
+ color_discrete_map={
136
+ "Normal": "#2ecc71",
137
+ "Warning loss": "#f39c12",
138
+ "Critical loss": "#e74c3c"
139
+ }
140
+ )
141
+ st.plotly_chart(fig_bar, use_container_width=True)
142
+
143
+ # ── Cumulative return ─────────────────────────────────────────
144
+ st.subheader("πŸ“‰ Cumulative Portfolio Return")
145
+ daily_returns["cumulative_return"] = (
146
+ (1 + daily_returns["portfolio_daily_returns"]).cumprod() - 1
147
+ )
148
+ fig_line = px.line(daily_returns, x="date", y="cumulative_return",
149
+ labels={"cumulative_return": "Cumulative Return",
150
+ "date": "Date"})
151
+ fig_line.add_hline(y=0, line_dash="dash", line_color="black")
152
+ fig_line.update_traces(line_color="#3498db")
153
+ st.plotly_chart(fig_line, use_container_width=True)
154
+
155
+ # ── Unrealized return scatter ──────────────────────────────────
156
+ st.subheader("⚑ Unrealized Return by Asset")
157
+ if "unrealized_return_pct" in pv.columns:
158
+ fig_ret = px.bar(
159
+ pv.sort_values("unrealized_return_pct"),
160
+ x="Ticker", y="unrealized_return_pct",
161
+ color=pv["unrealized_return_pct"].apply(
162
+ lambda x: "Gain" if x >= 0 else "Loss"
163
+ ),
164
+ color_discrete_map={"Gain": "#2ecc71", "Loss": "#e74c3c"},
165
+ labels={"unrealized_return_pct": "Unrealized Return %"}
166
+ )
167
+ fig_ret.add_hline(y=0, line_dash="dash", line_color="black")
168
+ st.plotly_chart(fig_ret, use_container_width=True)
169
+
170
+ # ── Risk metrics ──────────────────────────────────────────────
171
+ st.subheader("πŸ”¬ Risk Metrics")
172
+ st.dataframe(risk_metrics, use_container_width=True, hide_index=True)
173
+
174
+ # ── Stress test ───────────────────────────────────────────────
175
+ if "stressed_value" in pv.columns:
176
+ st.subheader("πŸ’₯ Stress Test (βˆ’15% market shock)")
177
+ sk1, sk2, sk3 = st.columns(3)
178
+ sk1.metric("Current Value", f"{pv['market_value'].sum():,.0f} €")
179
+ sk2.metric("Stressed Value", f"{pv['stressed_value'].sum():,.0f} €",
180
+ delta=f"{pv['stress_test_loss'].sum():,.0f} €")
181
+ sk3.metric("Estimated Loss", f"{pv['stress_test_loss'].sum():,.0f} €")
182
+
183
+ # ── Alert table ───────────────────────────────────────────────
184
+ st.subheader("🚨 Risk Alert Table")
185
+ alert_cols = [c for c in ["Ticker", "market_value", "weight",
186
+ "unrealized_return_pct", "alert_level",
187
+ "asset_concentration_flag"] if c in pv.columns]
188
+ st.dataframe(pv[alert_cols].sort_values("market_value", ascending=False),
189
+ use_container_width=True, hide_index=True)
190
+
191
+ st.divider()
192
+
193
+ # ── AI Sentiment Analysis ─────────────────────────────────────
194
+ st.subheader("πŸ€– AI Sentiment Analysis (FinBERT)")
195
+ st.caption("Powered by Hugging Face β€” ProsusAI/finbert")
196
+
197
+ news_examples = {
198
+ "AAPL": "Apple reports record quarterly earnings driven by iPhone sales",
199
+ "MSFT": "Microsoft faces antitrust investigation in European markets",
200
+ "NVDA": "Nvidia surges on strong AI chip demand forecast",
201
+ "GOOGL": "Alphabet announces major layoffs amid cost-cutting efforts",
202
+ "AMZN": "Amazon expands logistics network with new warehouse openings",
203
+ }
204
+
205
+ st.info(
206
+ "Enter a news headline below and click Analyze to get "
207
+ "an AI-powered sentiment score using FinBERT, "
208
+ "a model trained specifically on financial text."
209
+ )
210
+
211
+ col_input, col_btn = st.columns([4, 1])
212
+ with col_input:
213
+ headline = st.text_input(
214
+ "News headline",
215
+ value="Apple reports record quarterly earnings driven by iPhone sales"
216
+ )
217
+ with col_btn:
218
+ st.write("")
219
+ st.write("")
220
+ run_sentiment = st.button("πŸ” Analyze")
221
+
222
+ if run_sentiment and headline:
223
+ with st.spinner("Calling FinBERT model..."):
224
+ sentiment = analyze_sentiment(headline)
225
+ st.success(f"**Sentiment result:** {sentiment}")
226
+
227
+ # Pre-loaded examples
228
+ if st.checkbox("Show sentiment for example headlines"):
229
+ results = []
230
+ for ticker, text in news_examples.items():
231
+ with st.spinner(f"Analyzing {ticker}..."):
232
+ sent = analyze_sentiment(text)
233
+ results.append({"Ticker": ticker, "Headline": text, "Sentiment": sent})
234
+ st.dataframe(pd.DataFrame(results), use_container_width=True, hide_index=True)
235
+
236
+ st.divider()
237
+
238
+ # ── Download ──────────────────────────────────────────────────
239
+ st.download_button(
240
+ label="⬇️ Download Portfolio Table (CSV)",
241
+ data=portfolio.to_csv(index=False).encode("utf-8"),
242
+ file_name="portfolio_monitoring_output.csv",
243
+ mime="text/csv"
244
+ )
245
 
246
+ st.caption("ESCP Business School β€” Applied Data Science Workshop | Group Project")
247
+ ```