QuantumLearner commited on
Commit
96c388f
·
verified ·
1 Parent(s): d524548

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +538 -0
app.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import plotly.graph_objects as go
4
+ import plotly.express as px
5
+ import requests
6
+ import yfinance as yf
7
+ from datetime import datetime, date
8
+ import os
9
+
10
+ st.set_page_config(layout="wide")
11
+
12
+ API_KEY = os.getenv("FMP_API_KEY")
13
+
14
+ # -------------------------------------------------------------------
15
+ # Initialize session state defaults
16
+ # -------------------------------------------------------------------
17
+ if "valid_ticker" not in st.session_state:
18
+ st.session_state["valid_ticker"] = None
19
+ if "ticker" not in st.session_state:
20
+ st.session_state["ticker"] = None
21
+ if "hist" not in st.session_state:
22
+ st.session_state["hist"] = None
23
+ if "consensus" not in st.session_state:
24
+ st.session_state["consensus"] = None
25
+ if "df_targets" not in st.session_state:
26
+ st.session_state["df_targets"] = None
27
+ if "df_rss" not in st.session_state:
28
+ st.session_state["df_rss"] = None
29
+
30
+ # -------------------------------------------------------------------
31
+ # Column reordering helper: move specified columns to the end
32
+ # -------------------------------------------------------------------
33
+ def move_columns_to_end(df, cols_to_move):
34
+ existing = [col for col in cols_to_move if col in df.columns]
35
+ fixed_order = [col for col in df.columns if col not in existing] + existing
36
+ return df[fixed_order]
37
+
38
+ # -------------------------------------------------------------------
39
+ # Cache functions
40
+ # -------------------------------------------------------------------
41
+ @st.cache_data
42
+ def fetch_yfinance_data(symbol, period="5y"):
43
+ try:
44
+ ticker_obj = yf.Ticker(symbol)
45
+ hist = ticker_obj.history(period=period)
46
+ if hist.empty:
47
+ raise ValueError("No historical data found.")
48
+ return hist
49
+ except:
50
+ st.error("Unable to fetch historical price data.")
51
+ return None
52
+
53
+ @st.cache_data
54
+ def fetch_fmp_consensus(symbol):
55
+ try:
56
+ url = f"https://financialmodelingprep.com/api/v4/price-target-consensus?symbol={symbol}&apikey={FMP_API_KEY}"
57
+ response = requests.get(url)
58
+ data = response.json()
59
+ if data and len(data) > 0:
60
+ return data[0]
61
+ else:
62
+ raise ValueError("No consensus data returned.")
63
+ except:
64
+ st.error("Unable to fetch consensus data.")
65
+ return None
66
+
67
+ @st.cache_data
68
+ def fetch_price_target_data(symbol):
69
+ try:
70
+ url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={FMP_API_KEY}"
71
+ response = requests.get(url)
72
+ data = response.json()
73
+ if data:
74
+ df = pd.DataFrame(data)
75
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'])
76
+ return df
77
+ else:
78
+ raise ValueError("No price target data returned.")
79
+ except:
80
+ st.error("Unable to fetch price target data.")
81
+ return None
82
+
83
+ @st.cache_data
84
+ def fetch_price_target_rss_feed(num_pages=5):
85
+ try:
86
+ all_data = []
87
+ for page in range(num_pages):
88
+ url = f"https://financialmodelingprep.com/api/v4/price-target-rss-feed?page={page}&apikey={FMP_API_KEY}"
89
+ response = requests.get(url)
90
+ if response.status_code == 200:
91
+ data = response.json()
92
+ all_data.extend(data)
93
+ if all_data:
94
+ df = pd.DataFrame(all_data)
95
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'])
96
+ return df
97
+ else:
98
+ raise ValueError("No live feed data returned.")
99
+ except:
100
+ st.error("Unable to fetch live feed data.")
101
+ return None
102
+
103
+ def is_valid_ticker(tkr):
104
+ try:
105
+ _ = yf.Ticker(tkr).info
106
+ return True
107
+ except:
108
+ return False
109
+
110
+ # -------------------------------------------------------------------
111
+ # Sidebar
112
+ # -------------------------------------------------------------------
113
+ st.sidebar.title("Analysis Parameters")
114
+
115
+ with st.sidebar.expander("Page Selection", expanded=True):
116
+ page = st.radio(
117
+ "Select a page",
118
+ ["Price Targets by Ticker", "Price Target Live Feed"],
119
+ help="Choose a view for detailed stock data or a live feed of recent targets."
120
+ )
121
+
122
+ if page == "Price Targets by Ticker":
123
+ with st.sidebar.expander("Analysis Inputs", expanded=True):
124
+ ticker = st.text_input(
125
+ "Ticker Symbol",
126
+ value="AAPL",
127
+ help="Enter a valid stock ticker symbol (e.g. AAPL)."
128
+ )
129
+ run_analysis = st.sidebar.button("Run Analysis")
130
+ else:
131
+ run_analysis = st.sidebar.button("Run Analysis", help="Fetch the latest live feed data.")
132
+
133
+ # -------------------------------------------------------------------
134
+ # Logic to store data in session state if Run Analysis is clicked
135
+ # -------------------------------------------------------------------
136
+ if page == "Price Targets by Ticker":
137
+ if run_analysis:
138
+ if not is_valid_ticker(ticker):
139
+ st.session_state["valid_ticker"] = False
140
+ else:
141
+ st.session_state["valid_ticker"] = True
142
+ st.session_state["ticker"] = ticker
143
+ st.session_state["hist"] = fetch_yfinance_data(ticker)
144
+ st.session_state["consensus"] = fetch_fmp_consensus(ticker)
145
+ st.session_state["df_targets"] = fetch_price_target_data(ticker)
146
+
147
+ elif page == "Price Target Live Feed":
148
+ if run_analysis:
149
+ st.session_state["df_rss"] = fetch_price_target_rss_feed(num_pages=5)
150
+
151
+ # -------------------------------------------------------------------
152
+ # Main Page Content
153
+ # -------------------------------------------------------------------
154
+ if page == "Price Targets by Ticker":
155
+ st.title("Analyst Price Targets")
156
+
157
+ if st.session_state["valid_ticker"] is None:
158
+ st.markdown("Enter a stock symbol and click **Run Analysis** to load the data.")
159
+ elif st.session_state["valid_ticker"] is False:
160
+ st.error("Invalid symbol. Please try again.")
161
+ else:
162
+ ticker = st.session_state["ticker"]
163
+ hist = st.session_state["hist"]
164
+ consensus = st.session_state["consensus"]
165
+ df_targets = st.session_state["df_targets"]
166
+
167
+ # Fixed bubble size multiplier
168
+ bubble_multiplier = 1.2
169
+
170
+ # -----------------------------------------
171
+ # 12 Month Analyst Forecast Consensus
172
+ # -----------------------------------------
173
+ if hist is not None and consensus is not None:
174
+ st.markdown("### Analyst Forecast (12-Month)")
175
+ st.write("This chart shows the stock's closing price history. "
176
+ "It also shows projected targets for the next year, "
177
+ "including high, low, median, and overall consensus.")
178
+
179
+ def plot_price_data_with_targets(history_df, cons, symbol, forecast_months=12):
180
+ last_date = history_df.index[-1]
181
+ future_date = last_date + pd.DateOffset(months=forecast_months)
182
+ last_close = history_df['Close'][-1]
183
+ extended_future_date = future_date + pd.DateOffset(days=90)
184
+
185
+ fig = go.Figure()
186
+ fig.add_trace(go.Scatter(
187
+ x=history_df.index,
188
+ y=history_df['Close'],
189
+ mode='lines',
190
+ name='Close Price',
191
+ line=dict(color='royalblue', width=2),
192
+ hovertemplate='Date: %{x}<br>Price: %{y:.2f}<extra></extra>'
193
+ ))
194
+ fig.add_trace(go.Scatter(
195
+ x=[last_date],
196
+ y=[last_close],
197
+ mode='markers',
198
+ marker=dict(color='white', size=12, symbol='circle'),
199
+ name="Current Price",
200
+ hovertemplate='Date: %{x}<br>Price: %{y:.2f}<extra></extra>'
201
+ ))
202
+ annotations = [
203
+ dict(
204
+ x=last_date,
205
+ y=last_close,
206
+ text=f"{round(last_close)}",
207
+ font=dict(size=16, color='white'),
208
+ showarrow=False,
209
+ yshift=30
210
+ )
211
+ ]
212
+
213
+ targets = [
214
+ ("Target High", cons["targetHigh"], "green"),
215
+ ("Target Low", cons["targetLow"], "red"),
216
+ ("Target Consensus", cons["targetConsensus"], "orange"),
217
+ ("Target Median", cons["targetMedian"], "purple")
218
+ ]
219
+ for name, val, color in targets:
220
+ val_rounded = round(val)
221
+ fig.add_trace(go.Scatter(
222
+ x=[last_date, future_date],
223
+ y=[last_close, val_rounded],
224
+ mode='lines',
225
+ line=dict(dash='dash', color=color, width=2),
226
+ name=name,
227
+ hovertemplate=f"{name}: {val_rounded}<extra></extra>"
228
+ ))
229
+ annotations.append(
230
+ dict(
231
+ x=future_date,
232
+ y=val_rounded,
233
+ text=f"<b>{val_rounded}</b>",
234
+ showarrow=True,
235
+ arrowhead=2,
236
+ ax=20,
237
+ ay=0,
238
+ font=dict(color=color, size=20)
239
+ )
240
+ )
241
+
242
+ fig.add_shape(
243
+ type="line",
244
+ x0=last_date,
245
+ x1=last_date,
246
+ y0=history_df['Close'].min(),
247
+ y1=history_df['Close'].max(),
248
+ line=dict(color="gray", dash="dot")
249
+ )
250
+
251
+ fig.update_layout(
252
+ template='plotly_dark',
253
+ paper_bgcolor='black',
254
+ plot_bgcolor='black',
255
+ font=dict(color='white'),
256
+ title=dict(text=f"{symbol} Price History & 12-Month Targets", font=dict(color='white')),
257
+ legend=dict(
258
+ x=0.01, y=0.99,
259
+ bordercolor="white",
260
+ borderwidth=1,
261
+ font=dict(color='white')
262
+ ),
263
+ xaxis=dict(
264
+ range=[history_df.index[0], extended_future_date],
265
+ showgrid=True,
266
+ gridcolor='gray',
267
+ title=dict(text="Date", font=dict(color='white')),
268
+ tickfont=dict(color='white')
269
+ ),
270
+ yaxis=dict(
271
+ showgrid=True,
272
+ gridcolor='gray',
273
+ title=dict(text="Price", font=dict(color='white')),
274
+ tickfont=dict(color='white')
275
+ ),
276
+ annotations=annotations,
277
+ margin=dict(l=40, r=40, t=60, b=40)
278
+ )
279
+ return fig
280
+
281
+ fig_consensus = plot_price_data_with_targets(hist, consensus, ticker)
282
+ st.plotly_chart(fig_consensus, use_container_width=True)
283
+
284
+ # -----------------------------------------
285
+ # Price Target Evolution (Bubble Chart)
286
+ # -----------------------------------------
287
+ st.markdown("### Analyst Price Target Changes Over Time")
288
+ st.write("This chart shows how price targets have shifted. "
289
+ "Bubble sizes represent the percentage change from the posted price.")
290
+
291
+ if df_targets is not None:
292
+ def plot_price_target_evolution(df):
293
+ df['publishedDate'] = pd.to_datetime(df['publishedDate'], errors='coerce').dt.tz_localize(None)
294
+ df['targetChange'] = df['priceTarget'] - df['priceWhenPosted']
295
+ df['direction'] = df['targetChange'].apply(
296
+ lambda x: "Raised" if x > 0 else ("Lowered" if x < 0 else "No Change")
297
+ )
298
+ df['percentChange'] = (df['targetChange'] / df['priceWhenPosted']) * 100
299
+
300
+ color_map = {"Raised": "green", "Lowered": "red", "No Change": "gray"}
301
+ colors = df['direction'].map(color_map)
302
+ bubble_sizes = abs(df['percentChange']) * bubble_multiplier
303
+
304
+ df['date'] = df['publishedDate'].dt.date
305
+ daily_median = df.groupby('date')['priceTarget'].median()
306
+ daily_median.index = pd.to_datetime(daily_median.index)
307
+
308
+ fig = go.Figure()
309
+
310
+ # Price When Posted line+markers
311
+ fig.add_trace(go.Scatter(
312
+ x=df['publishedDate'],
313
+ y=df['priceWhenPosted'],
314
+ mode='lines+markers',
315
+ name='Price When Posted',
316
+ line=dict(color='royalblue', width=2, dash='dot'),
317
+ marker=dict(size=8),
318
+ hovertemplate='Date: %{x}<br>Price When Posted: %{y:.2f}<extra></extra>'
319
+ ))
320
+
321
+ # Bubble markers for Price Target
322
+ fig.add_trace(go.Scatter(
323
+ x=df['publishedDate'],
324
+ y=df['priceTarget'],
325
+ mode='markers',
326
+ name='Price Target',
327
+ marker=dict(
328
+ size=bubble_sizes,
329
+ color=colors,
330
+ opacity=0.7,
331
+ line=dict(width=1, color='black')
332
+ ),
333
+ hovertemplate=(
334
+ "<b>%{customdata[0]}</b><br>"
335
+ "Published: %{x}<br>"
336
+ "Price Target: %{y:.2f}<br>"
337
+ "Price When Posted: %{customdata[1]:.2f}<br>"
338
+ "Target Change: %{customdata[2]:.2f}<br>"
339
+ "Percent Change: %{customdata[3]:.2f}%<br>"
340
+ "Bubble Scale: 2.0"
341
+ "<extra></extra>"
342
+ ),
343
+ customdata=df[['newsTitle', 'priceWhenPosted', 'targetChange', 'percentChange']].values
344
+ ))
345
+
346
+ # Median line
347
+ if not daily_median.empty:
348
+ fig.add_trace(go.Scatter(
349
+ x=daily_median.index,
350
+ y=daily_median.values,
351
+ mode='lines',
352
+ name='Median Price Target',
353
+ line=dict(color='white', dash='dash', width=3, shape='hv'),
354
+ hovertemplate='Date: %{x}<br>Median Price Target: %{y:.2f}<extra></extra>'
355
+ ))
356
+
357
+ # Annotation for latest price
358
+ if not df.empty:
359
+ current_date = df['publishedDate'].max()
360
+ current_price = df.loc[df['publishedDate'] == current_date, 'priceWhenPosted'].iloc[-1]
361
+ fig.add_annotation(
362
+ x=current_date,
363
+ y=current_price,
364
+ text=f"<b>{round(current_price)}</b>",
365
+ showarrow=False,
366
+ font=dict(size=16, color='white'),
367
+ yshift=30
368
+ )
369
+
370
+ fig.update_layout(
371
+ template='plotly_dark',
372
+ paper_bgcolor='black',
373
+ plot_bgcolor='black',
374
+ font=dict(color='white'),
375
+ title=dict(text=f"{ticker}: Posted Price, Price Targets & Daily Median", font=dict(color='white')),
376
+ legend=dict(
377
+ x=0.01, y=0.99,
378
+ bordercolor="white",
379
+ borderwidth=1,
380
+ font=dict(color='white')
381
+ ),
382
+ xaxis=dict(
383
+ showgrid=True,
384
+ gridcolor='gray',
385
+ title=dict(text="Published Date", font=dict(color='white')),
386
+ tickfont=dict(color='white')
387
+ ),
388
+ yaxis=dict(
389
+ showgrid=True,
390
+ gridcolor='gray',
391
+ title=dict(text="Price (USD)", font=dict(color='white')),
392
+ tickfont=dict(color='white')
393
+ ),
394
+ margin=dict(l=40, r=40, t=60, b=40)
395
+ )
396
+ return fig
397
+
398
+ fig_evolution = plot_price_target_evolution(df_targets)
399
+ st.plotly_chart(fig_evolution, use_container_width=True)
400
+
401
+ st.markdown("### Detailed Historical Price Targets")
402
+ st.write("This table lists recent price targets, news headlines, and links.")
403
+
404
+ df_targets["MovementChart"] = df_targets.apply(
405
+ lambda row: [row["priceWhenPosted"], row["priceTarget"]],
406
+ axis=1
407
+ )
408
+ df_targets = move_columns_to_end(
409
+ df_targets,
410
+ ["newsTitle","newsURL","newsPublisher","newsBaseURL","url"]
411
+ )
412
+
413
+ with st.expander("Detailed Data", expanded=False):
414
+ st.dataframe(
415
+ df_targets,
416
+ column_config={
417
+ "MovementChart": st.column_config.LineChartColumn(
418
+ "From Posted to Target",
419
+ help="Line from priceWhenPosted to priceTarget",
420
+ )
421
+ },
422
+ height=300
423
+ )
424
+
425
+ elif page == "Price Target Live Feed":
426
+ st.title("Live Analyst Targets")
427
+
428
+ if st.session_state["df_rss"] is None:
429
+ st.markdown("Click **Run Analysis** to fetch the latest feed.")
430
+ else:
431
+ df_rss = st.session_state["df_rss"]
432
+ if not df_rss.empty:
433
+ st.markdown("### Latest Analyst Announcements")
434
+ st.write("This chart shows a daily view of median percentage changes in targets for various symbols.")
435
+
436
+ def plot_rss_feed(df):
437
+ df['date'] = df['publishedDate'].dt.date
438
+ df['targetChange'] = df['priceTarget'] - df['priceWhenPosted']
439
+ df['percentChange'] = (df['targetChange'] / df['priceWhenPosted']) * 100
440
+
441
+ grouped = df.groupby(['date', 'symbol']).agg({
442
+ 'percentChange': 'median',
443
+ 'priceTarget': 'median',
444
+ 'priceWhenPosted': 'median'
445
+ }).reset_index()
446
+
447
+ if grouped.empty:
448
+ return None
449
+
450
+ grouped['date'] = pd.to_datetime(grouped['date'])
451
+ fig = px.scatter(
452
+ grouped,
453
+ x='date',
454
+ y='symbol',
455
+ size=grouped['percentChange'].abs(),
456
+ color='percentChange',
457
+ color_continuous_scale='RdYlGn',
458
+ title='Daily Median Analyst % Change by Symbol',
459
+ labels={'date': 'Date', 'symbol': 'Ticker', 'percentChange': '% Change'}
460
+ )
461
+
462
+ unique_symbols = grouped['symbol'].nunique()
463
+ fig.update_layout(
464
+ template='plotly_dark',
465
+ paper_bgcolor='black',
466
+ plot_bgcolor='black',
467
+ font=dict(color='white'),
468
+ title=dict(text='Daily Median Analyst % Change by Symbol', font=dict(color='white')),
469
+ xaxis=dict(
470
+ showgrid=True,
471
+ gridcolor='gray',
472
+ title=dict(text="Date", font=dict(color='white')),
473
+ tickfont=dict(color='white')
474
+ ),
475
+ yaxis=dict(
476
+ showgrid=True,
477
+ gridcolor='gray',
478
+ title=dict(text="Ticker", font=dict(color='white')),
479
+ tickfont=dict(color='white')
480
+ ),
481
+ height=(unique_symbols * 10),
482
+ margin=dict(l=40, r=40, t=60, b=40)
483
+ )
484
+
485
+ fig.update_traces(
486
+ customdata=grouped[['symbol', 'percentChange', 'priceTarget', 'priceWhenPosted']].values,
487
+ hovertemplate=(
488
+ "<b>%{customdata[0]}</b><br>"
489
+ "Date: %{x}<br>"
490
+ "Median % Change: %{customdata[1]:.2f}%<br>"
491
+ "Median Target: %{customdata[2]:.2f}<br>"
492
+ "Median Posted: %{customdata[3]:.2f}<extra></extra>"
493
+ )
494
+ )
495
+ return fig
496
+
497
+ feed_fig = plot_rss_feed(df_rss)
498
+ if feed_fig:
499
+ st.plotly_chart(feed_fig, use_container_width=True)
500
+ else:
501
+ st.info("No grouped data to plot.")
502
+
503
+ st.markdown("### Detailed Live Feed Data")
504
+ st.write("This table lists recent announcements with their posted price and target.")
505
+
506
+ df_rss["MovementChart"] = df_rss.apply(
507
+ lambda row: [row["priceWhenPosted"], row["priceTarget"]],
508
+ axis=1
509
+ )
510
+ df_rss = move_columns_to_end(
511
+ df_rss,
512
+ ["newsTitle","newsURL","newsPublisher","newsBaseURL","url"]
513
+ )
514
+
515
+ with st.expander("Detailed Data", expanded=False):
516
+ st.dataframe(
517
+ df_rss,
518
+ column_config={
519
+ "MovementChart": st.column_config.LineChartColumn(
520
+ "From Posted to Target",
521
+ help="Line from priceWhenPosted to priceTarget",
522
+ )
523
+ },
524
+ height=300
525
+ )
526
+ else:
527
+ st.info("No live feed data available.")
528
+
529
+ # Hide default Streamlit style
530
+ st.markdown(
531
+ """
532
+ <style>
533
+ #MainMenu {visibility: hidden;}
534
+ footer {visibility: hidden;}
535
+ </style>
536
+ """,
537
+ unsafe_allow_html=True
538
+ )