QuantumLearner commited on
Commit
e4b2bca
·
verified ·
1 Parent(s): 23cad9f

Create requirements.txt

Browse files
Files changed (1) hide show
  1. requirements.txt +597 -0
requirements.txt ADDED
@@ -0,0 +1,597 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.express as px
4
+ import plotly.graph_objects as go
5
+ import requests
6
+ import numpy as np
7
+ from datetime import datetime, date, timedelta
8
+ import os
9
+
10
+ st.set_page_config(layout="wide")
11
+
12
+ FMP_API_KEY = os.getenv("FMP_API_KEY")
13
+
14
+ # -----------------------------
15
+ # Data Fetching
16
+ # -----------------------------
17
+ @st.cache_data
18
+ def fetch_analyst_ratings(ticker):
19
+ """
20
+ Fetches analyst consensus ratings for a given ticker.
21
+ """
22
+ url = f'https://financialmodelingprep.com/api/v4/upgrades-downgrades-consensus?symbol={ticker}&apikey={API_KEY}'
23
+ try:
24
+ r = requests.get(url)
25
+ r.raise_for_status()
26
+ data = r.json()
27
+ return data[0] if data else None
28
+ except Exception:
29
+ return None
30
+
31
+ @st.cache_data
32
+ def fetch_detailed_ratings(ticker):
33
+ """
34
+ Fetches detailed analyst ratings for a given ticker.
35
+ """
36
+ url = f'https://financialmodelingprep.com/api/v4/upgrades-downgrades?symbol={ticker}&apikey={API_KEY}'
37
+ try:
38
+ r = requests.get(url)
39
+ r.raise_for_status()
40
+ data = r.json()
41
+ df = pd.DataFrame(data) if data else pd.DataFrame()
42
+ # Reorder columns if target columns exist
43
+ if not df.empty and any(col in df.columns for col in ['newsBaseURL', 'newsPublisher', 'newsURL', 'newsTitle']):
44
+ target_cols = ['newsBaseURL', 'newsPublisher', 'newsURL', 'newsTitle']
45
+ other_cols = [c for c in df.columns if c not in target_cols]
46
+ new_order = other_cols + target_cols
47
+ df = df[new_order]
48
+ return df
49
+ except Exception:
50
+ return pd.DataFrame()
51
+
52
+ @st.cache_data
53
+ def fetch_upgrades_downgrades_rss_feed(api_key, num_pages=5):
54
+ """
55
+ Fetches up to 'num_pages' pages of upgrades/downgrades data
56
+ from the FMP RSS feed API.
57
+ """
58
+ all_data = []
59
+ for page in range(num_pages):
60
+ url = f"https://financialmodelingprep.com/api/v4/upgrades-downgrades-rss-feed?page={page}&apikey={api_key}"
61
+ response = requests.get(url)
62
+ if response.status_code == 200:
63
+ data = response.json()
64
+ all_data.extend(data)
65
+ else:
66
+ print(f"Error fetching page {page}: {response.status_code}")
67
+ df = pd.DataFrame(all_data)
68
+ if not df.empty and any(col in df.columns for col in ['newsBaseURL', 'newsPublisher', 'newsURL', 'newsTitle']):
69
+ target_cols = ['newsBaseURL', 'newsPublisher', 'newsURL', 'newsTitle']
70
+ other_cols = [c for c in df.columns if c not in target_cols]
71
+ new_order = other_cols + target_cols
72
+ df = df[new_order]
73
+ return df
74
+
75
+ # -----------------------------
76
+ # Plotting
77
+ # -----------------------------
78
+ def plot_analyst_ratings(data):
79
+ """
80
+ Plots a horizontal bar chart of analyst consensus ratings.
81
+ """
82
+ df = pd.DataFrame({
83
+ "rating": ["Strong Buy", "Buy", "Hold", "Sell", "Strong Sell"],
84
+ "count": [
85
+ data["strongBuy"], data["buy"], data["hold"], data["sell"], data["strongSell"]
86
+ ]
87
+ })
88
+ total_ratings = df["count"].sum()
89
+ df["percentage"] = (df["count"] / total_ratings * 100).round(0).astype(int)
90
+ df["text"] = df["count"].astype(str) + " (" + df["percentage"].astype(str) + "%)"
91
+
92
+ color_map = {
93
+ "Strong Buy": "#145A32",
94
+ "Buy": "#27ae60",
95
+ "Hold": "#f1c40f",
96
+ "Sell": "#e74c3c",
97
+ "Strong Sell": "#641E16"
98
+ }
99
+ custom_colors = [color_map[r] for r in df["rating"]]
100
+
101
+ fig = px.bar(
102
+ df,
103
+ x='count',
104
+ y='rating',
105
+ orientation='h',
106
+ color='rating',
107
+ color_discrete_sequence=custom_colors,
108
+ text='text'
109
+ )
110
+ fig.update_traces(
111
+ textposition='outside',
112
+ textfont=dict(size=16, color="white"),
113
+ textangle=0
114
+ )
115
+ fig.update_layout(
116
+ template='plotly_dark',
117
+ paper_bgcolor='#0e1117',
118
+ plot_bgcolor='#0e1117',
119
+ showlegend=False,
120
+ xaxis=dict(showgrid=False, visible=False),
121
+ yaxis=dict(title='', showgrid=False, tickfont=dict(size=18, color="white")),
122
+ margin=dict(l=120, r=10, t=80, b=30),
123
+ height=350,
124
+ title=dict(
125
+ text=(
126
+ f"Analyst Consensus: {data['consensus']}<br>"
127
+ f"<span style='font-size:0.8em;color:white'>Aggregate opinion from {total_ratings} analysts</span>"
128
+ ),
129
+ x=0.015,
130
+ xanchor='left',
131
+ y=0.9,
132
+ yanchor='top',
133
+ font=dict(size=25, color="white")
134
+ ),
135
+ width=None
136
+ )
137
+ return fig
138
+
139
+ def plot_sentiment_over_time(df):
140
+ """
141
+ Plots net sentiment changes over time.
142
+ """
143
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'], errors='coerce')
144
+ grade_map = {
145
+ 'Strong Buy': 2, 'Buy': 2, 'Overweight': 2, 'Outperform': 2,
146
+ 'Hold': 0, 'Neutral': 0,
147
+ 'Underperform': -2, 'Reduce': -2, 'Sell': -2
148
+ }
149
+ df['newGrade_score'] = df['newGrade'].map(grade_map)
150
+ df['previousGrade_score'] = df['previousGrade'].map(grade_map)
151
+ df['sentiment_change'] = df['newGrade_score'] - df['previousGrade_score']
152
+ df['year_month'] = df['publishedDate'].dt.to_period('M')
153
+
154
+ monthly_sentiment = df.groupby('year_month')['sentiment_change'].sum().reset_index()
155
+ monthly_sentiment['year_month'] = monthly_sentiment['year_month'].astype(str)
156
+
157
+ fig = go.Figure()
158
+ fig.add_trace(go.Scatter(
159
+ x=monthly_sentiment['year_month'],
160
+ y=monthly_sentiment['sentiment_change'],
161
+ mode='lines+markers',
162
+ name='Net Sentiment',
163
+ line=dict(color='blue', width=6),
164
+ marker=dict(color='blue', size=8),
165
+ hovertemplate='Month: %{x}<br>Sentiment: %{y}<extra></extra>'
166
+ ))
167
+ fig.update_layout(
168
+ template='plotly_dark',
169
+ paper_bgcolor='#0e1117',
170
+ plot_bgcolor='#0e1117',
171
+ #title="Analyst Sentiment Over Time",
172
+ xaxis=dict(
173
+ title=dict(text='Month', font=dict(color='green', size=20)),
174
+ tickangle=45,
175
+ tickfont=dict(color='white'),
176
+ showgrid=True,
177
+ gridcolor='white'
178
+ ),
179
+ yaxis=dict(
180
+ title=dict(text='Sum of Sentiment Changes', font=dict(color='green', size=20)),
181
+ tickfont=dict(color='white'),
182
+ showgrid=True,
183
+ gridcolor='white'
184
+ ),
185
+ font=dict(color='white'),
186
+ margin=dict(l=40, r=40, t=80, b=80),
187
+ width=None
188
+ )
189
+ return fig
190
+
191
+ def plot_actions_over_time(df):
192
+ """
193
+ Plots a stacked bar chart of analyst actions over time.
194
+ """
195
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'])
196
+ df['year_month'] = df['publishedDate'].dt.to_period('M')
197
+ monthly_counts = df.groupby(['year_month','action'])['symbol'].count().reset_index(name='count')
198
+ monthly_counts['year_month'] = monthly_counts['year_month'].astype(str)
199
+
200
+ pivot_df = monthly_counts.pivot(
201
+ index='year_month',
202
+ columns='action',
203
+ values='count'
204
+ ).fillna(0)
205
+
206
+ action_colors = {
207
+ 'upgrade': 'green',
208
+ 'downgrade': 'red',
209
+ 'hold': 'gray',
210
+ 'initiate': 'orange'
211
+ }
212
+ fig = go.Figure()
213
+ for action in sorted(pivot_df.columns):
214
+ fig.add_trace(go.Bar(
215
+ x=pivot_df.index,
216
+ y=pivot_df[action],
217
+ name=action.capitalize(),
218
+ marker_color=action_colors.get(action, 'blue'),
219
+ hovertemplate=(
220
+ "<b>%{x}</b><br>Action: " + action.capitalize() + "<br>Count: %{y}<extra></extra>"
221
+ )
222
+ ))
223
+ fig.update_layout(
224
+ barmode='stack',
225
+ template='plotly_dark',
226
+ paper_bgcolor='#0e1117',
227
+ plot_bgcolor='#0e1117',
228
+ #title="Upgrades vs. Downgrades vs. Holds Over Time",
229
+ xaxis=dict(
230
+ title=dict(text='Month', font=dict(color='green', size=20)),
231
+ tickangle=45,
232
+ tickfont=dict(color='white'),
233
+ showgrid=True,
234
+ gridcolor='white'
235
+ ),
236
+ yaxis=dict(
237
+ title=dict(text='Count of Actions', font=dict(color='green', size=20)),
238
+ tickfont=dict(color='white'),
239
+ showgrid=True,
240
+ gridcolor='white'
241
+ ),
242
+ legend=dict(font=dict(color='white')),
243
+ margin=dict(l=40, r=40, t=80, b=80),
244
+ width=None
245
+ )
246
+ return fig
247
+
248
+ def plot_animated_transition_heatmap(df):
249
+ """
250
+ Plots an animated heatmap of grade transitions over time.
251
+ """
252
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'], errors='coerce')
253
+ df['year_month'] = df['publishedDate'].dt.to_period('M')
254
+ all_months = sorted(df['year_month'].dropna().unique())
255
+
256
+ all_prev_grades = sorted(df['previousGrade'].dropna().unique())
257
+ all_new_grades = sorted(df['newGrade'].dropna().unique())
258
+
259
+ frames = []
260
+ initial_data = None
261
+
262
+ for idx, month in enumerate(all_months):
263
+ monthly_df = df[df['year_month'] == month]
264
+ transition_counts = monthly_df.groupby(
265
+ ['previousGrade','newGrade']
266
+ ).size().reset_index(name='count')
267
+
268
+ z_data = np.zeros((len(all_prev_grades), len(all_new_grades)), dtype=int)
269
+ for _, row in transition_counts.iterrows():
270
+ r_idx = all_prev_grades.index(row['previousGrade'])
271
+ c_idx = all_new_grades.index(row['newGrade'])
272
+ z_data[r_idx, c_idx] = row['count']
273
+
274
+ z_data_list = z_data.tolist()
275
+ frames.append(
276
+ go.Frame(
277
+ data=[go.Heatmap(
278
+ z=z_data_list,
279
+ coloraxis="coloraxis",
280
+ hovertemplate="Prev: %{y}<br>New: %{x}<br>Count: %{z}<extra></extra>",
281
+ text=z_data_list,
282
+ texttemplate="%{text}",
283
+ textfont=dict(color="white"),
284
+ showscale=True
285
+ )],
286
+ name=str(month)
287
+ )
288
+ )
289
+ if idx == 0:
290
+ initial_data = z_data_list
291
+
292
+ if initial_data is None:
293
+ raise ValueError("No valid data found to plot.")
294
+
295
+ fig = go.Figure(
296
+ data=[go.Heatmap(
297
+ z=initial_data,
298
+ coloraxis="coloraxis",
299
+ hovertemplate="Prev: %{y}<br>New: %{x}<br>Count: %{z}<extra></extra>",
300
+ text=initial_data,
301
+ texttemplate="%{text}",
302
+ textfont=dict(color="white"),
303
+ showscale=True
304
+ )],
305
+ layout=go.Layout(
306
+ # title="Animated Transition Heatmap (Monthly)",
307
+ xaxis=dict(
308
+ title=dict(text="New Grade", font=dict(color="green", size=20), standoff=75),
309
+ tickvals=list(range(len(all_new_grades))),
310
+ ticktext=all_new_grades,
311
+ tickfont=dict(color='white')
312
+ ),
313
+ yaxis=dict(
314
+ title=dict(text="Previous Grade", font=dict(color="green", size=20)),
315
+ tickvals=list(range(len(all_prev_grades))),
316
+ ticktext=all_prev_grades,
317
+ tickfont=dict(color='white'),
318
+ autorange='reversed'
319
+ ),
320
+ template="plotly_dark",
321
+ paper_bgcolor='#0e1117',
322
+ plot_bgcolor='#0e1117'
323
+ ),
324
+ frames=frames
325
+ )
326
+
327
+ fig.update_layout(
328
+ coloraxis=dict(colorscale="Blues"),
329
+ updatemenus=[
330
+ dict(
331
+ type="buttons",
332
+ showactive=False,
333
+ x=0, # Position from left
334
+ y=1.2, # Position from top
335
+ xanchor="left", # Anchor the button's left side to x=0
336
+ yanchor="top", # Anchor the button's top to y=1
337
+ buttons=[
338
+ dict(
339
+ label="Play Animation",
340
+ method="animate",
341
+ args=[
342
+ None,
343
+ {"frame": {"duration": 1000, "redraw": True},
344
+ "transition": {"duration": 500}}
345
+ ]
346
+ )
347
+ ]
348
+ )
349
+ ],
350
+ sliders=[
351
+ dict(
352
+ steps=[
353
+ dict(
354
+ method="animate",
355
+ args=[
356
+ [frame.name],
357
+ {
358
+ "mode": "immediate",
359
+ "frame": {"duration": 500, "redraw": True},
360
+ "transition": {"duration": 300}
361
+ }
362
+ ],
363
+ label=frame.name
364
+ )
365
+ for frame in frames
366
+ ],
367
+ x=0,
368
+ y=-0.1,
369
+ xanchor="left",
370
+ yanchor="top",
371
+ pad=dict(b=50, t=20),
372
+ currentvalue=dict(prefix="Month: ", font=dict(color='white')),
373
+ len=0.9
374
+ )
375
+ ],
376
+ width=None
377
+ )
378
+ return fig
379
+
380
+ def plot_extreme_sentiment_symbols(
381
+ df,
382
+ start_date=None,
383
+ end_date=None,
384
+ top_n=10,
385
+ title= ""
386
+ ):
387
+ """
388
+ Identifies most bullish and bearish symbols based on sentiment changes.
389
+ """
390
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'], errors='coerce')
391
+ df['publishedDate'] = df['publishedDate'].dt.tz_localize(None)
392
+
393
+ if start_date is not None:
394
+ df = df[df['publishedDate'] >= pd.to_datetime(start_date)]
395
+ if end_date is not None:
396
+ df = df[df['publishedDate'] <= pd.to_datetime(end_date)]
397
+
398
+ grade_map = {
399
+ 'Strong Buy': 2, 'Buy': 2, 'Overweight': 2, 'Outperform': 2,
400
+ 'Hold': 0, 'Neutral': 0,
401
+ 'Underperform': -2, 'Reduce': -2, 'Sell': -2
402
+ }
403
+ df['newGrade_score'] = df['newGrade'].map(grade_map)
404
+ df['previousGrade_score'] = df['previousGrade'].map(grade_map)
405
+ df['sentiment_change'] = df['newGrade_score'] - df['previousGrade_score']
406
+
407
+ symbol_sentiment = df.groupby('symbol')['sentiment_change'].sum().reset_index()
408
+ symbol_sentiment.sort_values('sentiment_change', ascending=False, inplace=True)
409
+
410
+ top_bullish = symbol_sentiment.head(top_n)
411
+ top_bearish = symbol_sentiment.tail(top_n)
412
+
413
+ top_bullish['category'] = 'Bullish'
414
+ top_bearish['category'] = 'Bearish'
415
+ combined = pd.concat([top_bullish, top_bearish], ignore_index=True)
416
+
417
+ fig = px.bar(
418
+ combined,
419
+ x='sentiment_change',
420
+ y='symbol',
421
+ color='category',
422
+ orientation='h',
423
+ title=title,
424
+ text='sentiment_change',
425
+ color_discrete_map={'Bullish': 'green', 'Bearish': 'red'}
426
+ )
427
+ n_bars = len(combined)
428
+ fig.update_layout(
429
+ height=50 + (40 * n_bars),
430
+ template='plotly_dark',
431
+ paper_bgcolor='#0e1117',
432
+ plot_bgcolor='#0e1117',
433
+ xaxis=dict(
434
+ title=dict(text='Net Sentiment (Sum of Upgrades/Downgrades)', font=dict(color='green', size=20)),
435
+ tickfont=dict(color='white')
436
+ ),
437
+ yaxis=dict(
438
+ title=dict(text='Symbol', font=dict(color='green', size=20)),
439
+ dtick=1,
440
+ tickfont=dict(color='white')
441
+ ),
442
+ width=None
443
+ )
444
+ fig.update_traces(textposition='auto')
445
+ return fig
446
+
447
+ # -----------------------------
448
+ # Sidebar and Pages
449
+ # -----------------------------
450
+ with st.sidebar:
451
+ st.header("Parameters")
452
+ with st.expander("Select Page", expanded=True):
453
+ page = st.radio("Analyses", ["Actions by Ticker", "Actions Live Feed"], index=0)
454
+
455
+ if page == "Actions by Ticker":
456
+ with st.expander("Ticker Input", expanded=True):
457
+ ticker = st.text_input(
458
+ "Enter Ticker Symbol",
459
+ value="AAPL",
460
+ help="Enter a valid stock ticker symbol (e.g., AAPL)."
461
+ )
462
+ run_button = st.button("Run Analysis")
463
+
464
+ else:
465
+ with st.expander("Date Range", expanded=True):
466
+ # In the sidebar for live feed:
467
+ start_date = st.date_input(
468
+ "Start Date",
469
+ value=date.today() - timedelta(days=45),
470
+ help="Select the start date."
471
+ )
472
+ end_date = st.date_input(
473
+ "End Date",
474
+ value=date.today(),
475
+ help="Select the end date."
476
+ )
477
+ with st.expander("Display Options", expanded=True):
478
+ top_n = st.slider(
479
+ "Number of Symbols",
480
+ min_value=5,
481
+ max_value=20,
482
+ value=10,
483
+ help="How many bullish and bearish symbols to display."
484
+ )
485
+ run_button = st.button("Run Analysis")
486
+
487
+ st.title("Stock Downgrades and Upgrades")
488
+ st.write("This tool provides real-time updates on analyst ratings, shows sentiment trends over time, and displays recent analyst actions.")
489
+
490
+ if 'results' not in st.session_state:
491
+ st.session_state.results = {}
492
+
493
+ # -----------------------------
494
+ # Page 1: Actions by Ticker
495
+ # -----------------------------
496
+ if page == "Actions by Ticker":
497
+ if run_button:
498
+ if not ticker.strip():
499
+ st.error("Please enter a valid ticker symbol.")
500
+ else:
501
+ with st.spinner("Fetching data..."):
502
+ consensus_data = fetch_analyst_ratings(ticker.upper())
503
+ detailed_data = fetch_detailed_ratings(ticker.upper())
504
+ st.session_state.results["consensus"] = consensus_data
505
+ st.session_state.results["detailed"] = detailed_data
506
+
507
+ if "consensus" in st.session_state.results and "detailed" in st.session_state.results:
508
+ consensus_data = st.session_state.results["consensus"]
509
+ detailed_data = st.session_state.results["detailed"]
510
+
511
+ st.subheader(f"Analyst Consensus for {ticker.upper()}")
512
+ st.write("This chart displays the overall analyst consensus rating for the stock. It shows the distribution of ratings such as Strong Buy, Buy, Hold, Sell, and Strong Sell.")
513
+ if consensus_data:
514
+ st.plotly_chart(plot_analyst_ratings(consensus_data), use_container_width=True)
515
+ else:
516
+ st.warning("No consensus data available.")
517
+
518
+ st.subheader(f"Sentiment Trend Over Time for {ticker.upper()}")
519
+ st.write("This line chart tracks changes in analyst sentiment over time. Net sentiment is computed by subtracting the previous rating score from the new rating score using a mapping (Strong Buy, Buy, Overweight, Outperform = 2; Hold, Neutral = 0; Underperform, Reduce, Sell = -2), which indicates the direction of rating adjustments each month.")
520
+ if not detailed_data.empty:
521
+ st.plotly_chart(plot_sentiment_over_time(detailed_data), use_container_width=True)
522
+ else:
523
+ st.warning("No detailed data available for sentiment analysis.")
524
+
525
+ st.subheader(f"Analyst Actions Over Time for {ticker.upper()}")
526
+ st.write("This stacked bar chart shows monthly counts of analyst actions. It shows the number of upgrades, downgrades, and holds over time.")
527
+ if not detailed_data.empty:
528
+ st.plotly_chart(plot_actions_over_time(detailed_data), use_container_width=True)
529
+ else:
530
+ st.warning("No detailed data available for action analysis.")
531
+
532
+ st.subheader(f"Rating Transition Heatmap for {ticker.upper()}")
533
+ st.write("This animated heatmap visualizes transitions between previous and new analyst ratings each month. It shows how frequently ratings change from one category to another. Use the slider or play button to observe changes for specific dates.")
534
+ if not detailed_data.empty:
535
+ try:
536
+ st.plotly_chart(plot_animated_transition_heatmap(detailed_data), use_container_width=True)
537
+ except ValueError:
538
+ st.warning("Insufficient data for transition heatmap.")
539
+ else:
540
+ st.warning("No detailed data available for transitions.")
541
+
542
+ st.subheader(f"Detailed Analyst Action Data for {ticker.upper()}")
543
+ st.write("The table below provides the detailed information on each analyst action and the associated news data.")
544
+ if not detailed_data.empty:
545
+ with st.expander("View Detailed Results", expanded=False):
546
+ st.dataframe(detailed_data)
547
+ else:
548
+ st.warning("No detailed data available.")
549
+
550
+ # -----------------------------
551
+ # Page 2: Actions Live Feed
552
+ # -----------------------------
553
+ else:
554
+ if run_button:
555
+ if start_date > end_date:
556
+ st.error("Start date must be before end date.")
557
+ else:
558
+ with st.spinner("Fetching live feed data..."):
559
+ rss_feed_df = fetch_upgrades_downgrades_rss_feed(API_KEY, num_pages=5)
560
+ st.session_state.results["live_data"] = rss_feed_df
561
+
562
+ if "live_data" in st.session_state.results:
563
+ live_data = st.session_state.results["live_data"]
564
+ if not live_data.empty:
565
+ st.subheader("Top Bullish and Bearish Stocks")
566
+ st.write("This bar chart displays stocks with the highest net sentiment. Net sentiment is computed by subtracting previous rating scores from new rating scores using a defined mapping (e.g., Strong Buy, Buy, Overweight, Outperform = +2; Hold, Neutral = 0; Underperform, Reduce, Sell = -2). This helps identify stocks viewed most positively and negatively by analysts.")
567
+ st.plotly_chart(
568
+ plot_extreme_sentiment_symbols(
569
+ live_data,
570
+ start_date=start_date,
571
+ end_date=end_date,
572
+ top_n=top_n
573
+ ),
574
+ use_container_width=True
575
+ )
576
+
577
+ st.subheader("Detailed Live Feed Data")
578
+ st.write("This table provides a detailed view of the latest analyst upgrades, downgrades, and related news.")
579
+ with st.expander("View Detailed Results", expanded=False):
580
+ st.dataframe(live_data)
581
+ else:
582
+ st.warning("No live data returned from the feed.")
583
+ else:
584
+ st.info("Please run the analysis to fetch live data.")
585
+
586
+
587
+
588
+ # Hide default Streamlit style
589
+ st.markdown(
590
+ """
591
+ <style>
592
+ #MainMenu {visibility: hidden;}
593
+ footer {visibility: hidden;}
594
+ </style>
595
+ """,
596
+ unsafe_allow_html=True
597
+ )