Space62 / app.py
QuantumLearner's picture
Update app.py
009aba0 verified
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import plotly.express as px
import requests
import yfinance as yf
from datetime import datetime, date
import os
st.set_page_config(layout="wide")
FMP_API_KEY = os.getenv("FMP_API_KEY")
# -------------------------------------------------------------------
# Initialize session state defaults
# -------------------------------------------------------------------
if "valid_ticker" not in st.session_state:
st.session_state["valid_ticker"] = None
if "ticker" not in st.session_state:
st.session_state["ticker"] = None
if "hist" not in st.session_state:
st.session_state["hist"] = None
if "consensus" not in st.session_state:
st.session_state["consensus"] = None
if "df_targets" not in st.session_state:
st.session_state["df_targets"] = None
if "df_rss" not in st.session_state:
st.session_state["df_rss"] = None
# -------------------------------------------------------------------
# Column reordering helper: move specified columns to the end
# -------------------------------------------------------------------
def move_columns_to_end(df, cols_to_move):
existing = [col for col in cols_to_move if col in df.columns]
fixed_order = [col for col in df.columns if col not in existing] + existing
return df[fixed_order]
# -------------------------------------------------------------------
# Cache functions
# -------------------------------------------------------------------
@st.cache_data
def fetch_yfinance_data(symbol, period="5y"):
try:
ticker_obj = yf.Ticker(symbol)
hist = ticker_obj.history(period=period)
if hist.empty:
raise ValueError("No historical data found.")
return hist
except:
st.error("Unable to fetch historical price data.")
return None
@st.cache_data
def fetch_fmp_consensus(symbol):
try:
url = f"https://financialmodelingprep.com/api/v4/price-target-consensus?symbol={symbol}&apikey={FMP_API_KEY}"
response = requests.get(url)
data = response.json()
if data and len(data) > 0:
return data[0]
else:
raise ValueError("No consensus data returned.")
except:
st.error("Unable to fetch consensus data.")
return None
@st.cache_data
def fetch_price_target_data(symbol):
try:
url = f"https://financialmodelingprep.com/api/v4/price-target?symbol={symbol}&apikey={FMP_API_KEY}"
response = requests.get(url)
data = response.json()
if data:
df = pd.DataFrame(data)
df['publishedDate'] = pd.to_datetime(df['publishedDate'])
return df
else:
raise ValueError("No price target data returned.")
except:
st.error("Unable to fetch price target data.")
return None
@st.cache_data
def fetch_price_target_rss_feed(num_pages=5):
try:
all_data = []
for page in range(num_pages):
url = f"https://financialmodelingprep.com/api/v4/price-target-rss-feed?page={page}&apikey={FMP_API_KEY}"
response = requests.get(url)
if response.status_code == 200:
data = response.json()
all_data.extend(data)
if all_data:
df = pd.DataFrame(all_data)
df['publishedDate'] = pd.to_datetime(df['publishedDate'])
return df
else:
raise ValueError("No live feed data returned.")
except:
st.error("Unable to fetch live feed data.")
return None
def is_valid_ticker(tkr):
try:
_ = yf.Ticker(tkr).info
return True
except:
return False
# -------------------------------------------------------------------
# Sidebar
# -------------------------------------------------------------------
st.sidebar.title("Analysis Parameters")
with st.sidebar.expander("Page Selection", expanded=True):
page = st.radio(
"Select a page",
["Price Targets by Ticker", "Price Target Live Feed"],
help="Choose a view for detailed stock data or a live feed of recent targets."
)
if page == "Price Targets by Ticker":
with st.sidebar.expander("Analysis Inputs", expanded=True):
ticker = st.text_input(
"Ticker Symbol",
value="AAPL",
help="Enter a valid stock ticker symbol (e.g. AAPL)."
)
run_analysis = st.sidebar.button("Run Analysis")
else:
run_analysis = st.sidebar.button("Run Analysis", help="Fetch the latest live feed data.")
# -------------------------------------------------------------------
# Logic to store data in session state if Run Analysis is clicked
# -------------------------------------------------------------------
if page == "Price Targets by Ticker":
if run_analysis:
if not is_valid_ticker(ticker):
st.session_state["valid_ticker"] = False
else:
st.session_state["valid_ticker"] = True
st.session_state["ticker"] = ticker
st.session_state["hist"] = fetch_yfinance_data(ticker)
st.session_state["consensus"] = fetch_fmp_consensus(ticker)
st.session_state["df_targets"] = fetch_price_target_data(ticker)
elif page == "Price Target Live Feed":
if run_analysis:
st.session_state["df_rss"] = fetch_price_target_rss_feed(num_pages=5)
# -------------------------------------------------------------------
# Main Page Content
# -------------------------------------------------------------------
if page == "Price Targets by Ticker":
st.title("Analyst Price Targets")
if st.session_state["valid_ticker"] is None:
st.markdown("Enter a stock symbol and click **Run Analysis** to load the data.")
elif st.session_state["valid_ticker"] is False:
st.error("Invalid symbol. Please try again.")
else:
ticker = st.session_state["ticker"]
hist = st.session_state["hist"]
consensus = st.session_state["consensus"]
df_targets = st.session_state["df_targets"]
# Fixed bubble size multiplier
bubble_multiplier = 1.2
# -----------------------------------------
# 12 Month Analyst Forecast Consensus
# -----------------------------------------
if hist is not None and consensus is not None:
st.markdown("### Analyst Forecast (12-Month)")
st.write("This chart shows the stock's closing price history. "
"It also shows projected targets for the next year, "
"including high, low, median, and overall consensus.")
def plot_price_data_with_targets(history_df, cons, symbol, forecast_months=12):
last_date = history_df.index[-1]
future_date = last_date + pd.DateOffset(months=forecast_months)
last_close = history_df['Close'][-1]
extended_future_date = future_date + pd.DateOffset(days=90)
fig = go.Figure()
fig.add_trace(go.Scatter(
x=history_df.index,
y=history_df['Close'],
mode='lines',
name='Close Price',
line=dict(color='royalblue', width=2),
hovertemplate='Date: %{x}<br>Price: %{y:.2f}<extra></extra>'
))
fig.add_trace(go.Scatter(
x=[last_date],
y=[last_close],
mode='markers',
marker=dict(color='white', size=12, symbol='circle'),
name="Current Price",
hovertemplate='Date: %{x}<br>Price: %{y:.2f}<extra></extra>'
))
annotations = [
dict(
x=last_date,
y=last_close,
text=f"{round(last_close)}",
font=dict(size=16, color='white'),
showarrow=False,
yshift=30
)
]
targets = [
("Target High", cons["targetHigh"], "green"),
("Target Low", cons["targetLow"], "red"),
("Target Consensus", cons["targetConsensus"], "orange"),
("Target Median", cons["targetMedian"], "purple")
]
for name, val, color in targets:
val_rounded = round(val)
fig.add_trace(go.Scatter(
x=[last_date, future_date],
y=[last_close, val_rounded],
mode='lines',
line=dict(dash='dash', color=color, width=2),
name=name,
hovertemplate=f"{name}: {val_rounded}<extra></extra>"
))
annotations.append(
dict(
x=future_date,
y=val_rounded,
text=f"<b>{val_rounded}</b>",
showarrow=True,
arrowhead=2,
ax=20,
ay=0,
font=dict(color=color, size=20)
)
)
fig.add_shape(
type="line",
x0=last_date,
x1=last_date,
y0=history_df['Close'].min(),
y1=history_df['Close'].max(),
line=dict(color="gray", dash="dot")
)
fig.update_layout(
template='plotly_dark',
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
font=dict(color='white'),
title=dict(text=f"{symbol} Price History & 12-Month Targets", font=dict(color='white')),
legend=dict(
x=0.01, y=0.99,
bordercolor="white",
borderwidth=1,
font=dict(color='white')
),
xaxis=dict(
range=[history_df.index[0], extended_future_date],
showgrid=True,
gridcolor='gray',
title=dict(text="Date", font=dict(color='white')),
tickfont=dict(color='white')
),
yaxis=dict(
showgrid=True,
gridcolor='gray',
title=dict(text="Price", font=dict(color='white')),
tickfont=dict(color='white')
),
annotations=annotations,
margin=dict(l=40, r=40, t=60, b=40)
)
return fig
fig_consensus = plot_price_data_with_targets(hist, consensus, ticker)
st.plotly_chart(fig_consensus, use_container_width=True)
# -----------------------------------------
# Price Target Evolution (Bubble Chart)
# -----------------------------------------
st.markdown("### Analyst Price Target Changes Over Time")
st.write("This chart shows how price targets have shifted. "
"Bubble sizes represent the percentage change from the posted price.")
if df_targets is not None:
def plot_price_target_evolution(df):
df['publishedDate'] = pd.to_datetime(df['publishedDate'], errors='coerce').dt.tz_localize(None)
df['targetChange'] = df['priceTarget'] - df['priceWhenPosted']
df['direction'] = df['targetChange'].apply(
lambda x: "Raised" if x > 0 else ("Lowered" if x < 0 else "No Change")
)
df['percentChange'] = (df['targetChange'] / df['priceWhenPosted']) * 100
color_map = {"Raised": "green", "Lowered": "red", "No Change": "gray"}
colors = df['direction'].map(color_map)
bubble_sizes = abs(df['percentChange']) * bubble_multiplier
df['date'] = df['publishedDate'].dt.date
daily_median = df.groupby('date')['priceTarget'].median()
daily_median.index = pd.to_datetime(daily_median.index)
fig = go.Figure()
# Price When Posted line+markers
fig.add_trace(go.Scatter(
x=df['publishedDate'],
y=df['priceWhenPosted'],
mode='lines+markers',
name='Price When Posted',
line=dict(color='royalblue', width=2, dash='dot'),
marker=dict(size=8),
hovertemplate='Date: %{x}<br>Price When Posted: %{y:.2f}<extra></extra>'
))
# Bubble markers for Price Target
fig.add_trace(go.Scatter(
x=df['publishedDate'],
y=df['priceTarget'],
mode='markers',
name='Price Target',
marker=dict(
size=bubble_sizes,
color=colors,
opacity=0.7,
line=dict(width=1, color='black')
),
hovertemplate=(
"<b>%{customdata[0]}</b><br>"
"Published: %{x}<br>"
"Price Target: %{y:.2f}<br>"
"Price When Posted: %{customdata[1]:.2f}<br>"
"Target Change: %{customdata[2]:.2f}<br>"
"Percent Change: %{customdata[3]:.2f}%<br>"
"Bubble Scale: 2.0"
"<extra></extra>"
),
customdata=df[['newsTitle', 'priceWhenPosted', 'targetChange', 'percentChange']].values
))
# Median line
if not daily_median.empty:
fig.add_trace(go.Scatter(
x=daily_median.index,
y=daily_median.values,
mode='lines',
name='Median Price Target',
line=dict(color='white', dash='dash', width=3, shape='hv'),
hovertemplate='Date: %{x}<br>Median Price Target: %{y:.2f}<extra></extra>'
))
# Annotation for latest price
if not df.empty:
current_date = df['publishedDate'].max()
current_price = df.loc[df['publishedDate'] == current_date, 'priceWhenPosted'].iloc[-1]
fig.add_annotation(
x=current_date,
y=current_price,
text=f"<b>{round(current_price)}</b>",
showarrow=False,
font=dict(size=16, color='white'),
yshift=30
)
fig.update_layout(
template='plotly_dark',
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
font=dict(color='white'),
title=dict(text=f"{ticker}: Posted Price, Price Targets & Daily Median", font=dict(color='white')),
legend=dict(
x=0.01, y=0.99,
bordercolor="white",
borderwidth=1,
font=dict(color='white')
),
xaxis=dict(
showgrid=True,
gridcolor='gray',
title=dict(text="Published Date", font=dict(color='white')),
tickfont=dict(color='white')
),
yaxis=dict(
showgrid=True,
gridcolor='gray',
title=dict(text="Price (USD)", font=dict(color='white')),
tickfont=dict(color='white')
),
margin=dict(l=40, r=40, t=60, b=40)
)
return fig
fig_evolution = plot_price_target_evolution(df_targets)
st.plotly_chart(fig_evolution, use_container_width=True)
st.markdown("### Detailed Historical Price Targets")
st.write("This table lists recent price targets, news headlines, and links.")
df_targets["MovementChart"] = df_targets.apply(
lambda row: [row["priceWhenPosted"], row["priceTarget"]],
axis=1
)
df_targets = move_columns_to_end(
df_targets,
["newsTitle","newsURL","newsPublisher","newsBaseURL","url"]
)
with st.expander("Detailed Data", expanded=False):
st.dataframe(
df_targets,
column_config={
"MovementChart": st.column_config.LineChartColumn(
"From Posted to Target",
help="Line from priceWhenPosted to priceTarget",
)
},
height=300
)
elif page == "Price Target Live Feed":
st.title("Live Analyst Targets")
if st.session_state["df_rss"] is None:
st.markdown("Click **Run Analysis** to fetch the latest feed.")
else:
df_rss = st.session_state["df_rss"]
if not df_rss.empty:
st.markdown("### Latest Analyst Announcements")
st.write("This chart shows a daily view of median percentage changes in targets for various symbols.")
def plot_rss_feed(df):
df['date'] = df['publishedDate'].dt.date
df['targetChange'] = df['priceTarget'] - df['priceWhenPosted']
df['percentChange'] = (df['targetChange'] / df['priceWhenPosted']) * 100
grouped = df.groupby(['date', 'symbol']).agg({
'percentChange': 'median',
'priceTarget': 'median',
'priceWhenPosted': 'median'
}).reset_index()
if grouped.empty:
return None
grouped['date'] = pd.to_datetime(grouped['date'])
fig = px.scatter(
grouped,
x='date',
y='symbol',
size=grouped['percentChange'].abs(),
color='percentChange',
color_continuous_scale='RdYlGn',
title='Daily Median Analyst % Change by Symbol',
labels={'date': 'Date', 'symbol': 'Ticker', 'percentChange': '% Change'}
)
unique_symbols = grouped['symbol'].nunique()
fig.update_layout(
template='plotly_dark',
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
font=dict(color='white'),
title=dict(text='Daily Median Analyst % Change by Symbol', font=dict(color='white')),
xaxis=dict(
showgrid=True,
gridcolor='gray',
title=dict(text="Date", font=dict(color='white')),
tickfont=dict(color='white')
),
yaxis=dict(
showgrid=True,
gridcolor='gray',
title=dict(text="Ticker", font=dict(color='white')),
tickfont=dict(color='white')
),
height=(unique_symbols * 10),
margin=dict(l=40, r=40, t=60, b=40)
)
fig.update_traces(
customdata=grouped[['symbol', 'percentChange', 'priceTarget', 'priceWhenPosted']].values,
hovertemplate=(
"<b>%{customdata[0]}</b><br>"
"Date: %{x}<br>"
"Median % Change: %{customdata[1]:.2f}%<br>"
"Median Target: %{customdata[2]:.2f}<br>"
"Median Posted: %{customdata[3]:.2f}<extra></extra>"
)
)
return fig
feed_fig = plot_rss_feed(df_rss)
if feed_fig:
st.plotly_chart(feed_fig, use_container_width=True)
else:
st.info("No grouped data to plot.")
st.markdown("### Detailed Live Feed Data")
st.write("This table lists recent announcements with their posted price and target.")
df_rss["MovementChart"] = df_rss.apply(
lambda row: [row["priceWhenPosted"], row["priceTarget"]],
axis=1
)
df_rss = move_columns_to_end(
df_rss,
["newsTitle","newsURL","newsPublisher","newsBaseURL","url"]
)
with st.expander("Detailed Data", expanded=False):
st.dataframe(
df_rss,
column_config={
"MovementChart": st.column_config.LineChartColumn(
"From Posted to Target",
help="Line from priceWhenPosted to priceTarget",
)
},
height=300
)
else:
st.info("No live feed data available.")
# Hide default Streamlit style
st.markdown(
"""
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
""",
unsafe_allow_html=True
)