Space19 / app.py
QuantumLearner's picture
Update app.py
b5c64e6 verified
import os
import yfinance as yf
import pandas as pd
import numpy as np
import requests
import plotly.graph_objects as go
import streamlit as st
from datetime import timedelta
from scipy.stats import norm
# Load API key from environment variables
FMP_API_KEY = os.getenv("FMP_API_KEY")
# Define functions
def fetch_earnings_data(ticker, limit=99):
"""
Fetch earnings data from the Financial Modeling Prep API.
"""
try:
url = f"https://financialmodelingprep.com/api/v3/earnings-surprises/{ticker}?apikey={FMP_API_KEY}"
response = requests.get(url)
response.raise_for_status()
data = response.json()
earnings_data = pd.DataFrame(data)
earnings_data['date'] = pd.to_datetime(earnings_data['date'])
earnings_data.set_index('date', inplace=True)
earnings_data.rename(
columns={
'actualEarningResult': 'Actual EPS',
'estimatedEarning': 'EPS Estimate'
},
inplace=True
)
earnings_data['Surprise(%)'] = (
(earnings_data['Actual EPS'] - earnings_data['EPS Estimate'])
/ earnings_data['EPS Estimate']
) * 100
earnings_data = earnings_data.dropna(subset=['EPS Estimate'])
return earnings_data.head(limit)
except Exception as e:
st.warning(f"There was an issue fetching earnings data: {e}")
return pd.DataFrame()
def fetch_stock_data(ticker, start_date, end_date, buffer_days):
"""
Fetch historical stock data using yfinance.
"""
try:
start_date = start_date - pd.Timedelta(days=buffer_days)
end_date = end_date + pd.Timedelta(days=buffer_days)
stock_data = yf.download(ticker, start=start_date, end=end_date, auto_adjust=False)
if isinstance(stock_data.columns, pd.MultiIndex): # Flatten multi-index
stock_data.columns = stock_data.columns.get_level_values(0)
if stock_data.empty:
raise ValueError(f"No data found for {ticker} from {start_date} to {end_date}")
if len(stock_data) < buffer_days:
raise ValueError(f"Insufficient data points for {ticker}")
stock_data.index = stock_data.index.tz_localize(None)
return stock_data
except Exception as e:
st.warning(f"There was an issue fetching stock data: {e}")
return pd.DataFrame()
def calculate_metrics(stock_data):
"""
Add metrics like daily returns and rolling volatility to the stock data.
"""
if not stock_data.empty:
stock_data['Returns'] = stock_data['Close'].pct_change()
stock_data['20D Volatility'] = stock_data['Returns'].rolling(window=20).std()
return stock_data
def ensure_window_size(subset, earning_date, pre_announcement_window, post_announcement_window):
"""
Ensure subset has the full range of dates around the earnings date.
"""
expected_dates = [earning_date + pd.Timedelta(days=i) for i in range(-pre_announcement_window, post_announcement_window + 1)]
for expected_date in expected_dates:
if expected_date not in subset.index:
subset.loc[expected_date] = np.nan
return subset.sort_index()
def plot_stock_price_with_earnings(stock_data, earnings_dates, ticker):
"""
Plot stock prices with earnings surprises.
"""
fig = go.Figure()
fig.add_trace(go.Scatter(x=stock_data.index, y=stock_data['Close'], mode='lines', name='Stock Price', line=dict(color='blue')))
scaling_factor = 1.2
max_marker_size = 30
added_positive_legend = False
added_negative_legend = False
for index, row in earnings_dates.iterrows():
date = index
if date not in stock_data.index:
date = stock_data.index[stock_data.index.get_indexer([date], method='nearest')[0]]
surprise = row['Surprise(%)']
marker_size = abs(surprise) * scaling_factor if not np.isnan(surprise) else 10
marker_size = min(marker_size, max_marker_size)
color = 'green' if surprise > 0 else 'red'
marker = '^' if surprise > 0 else 'v'
if surprise > 0:
name = 'Positive EPS Surprise' if not added_positive_legend else None
added_positive_legend = True
else:
name = 'Negative EPS Surprise' if not added_negative_legend else None
added_negative_legend = True
fig.add_trace(go.Scatter(x=[date], y=[stock_data.loc[date, 'Close']], mode='markers',
marker=dict(symbol='triangle-up' if marker == '^' else 'triangle-down', size=10 if name is not None else marker_size, color=color),
name=name, showlegend=name is not None))
fig.update_layout(title=f'{ticker} Stock Price with Earnings Surprise',
xaxis_title='Date', yaxis_title='Stock Price',
legend_title='Legend', template='plotly_white',
height=600, width=1200)
return fig
def plot_normalized_price_movements(stock_data, earnings_dates, ticker, pre_announcement_window, post_announcement_window, upper_threshold, lower_threshold):
"""
Plot normalized price movements around earnings dates.
"""
all_normalized_prices = []
for earning_date in earnings_dates.index:
start = earning_date - pd.Timedelta(days=pre_announcement_window)
end = earning_date + pd.Timedelta(days=post_announcement_window)
subset = stock_data.loc[start:end]['Close'].copy()
subset = ensure_window_size(subset, earning_date, pre_announcement_window, post_announcement_window)
subset.ffill(inplace=True)
subset.bfill(inplace=True)
subset = subset / subset[0]
all_normalized_prices.append(subset.tolist())
above_count = 0
below_count = 0
between_count = 0
for prices in all_normalized_prices:
if max(prices) > upper_threshold:
above_count += 1
elif min(prices) < lower_threshold:
below_count += 1
else:
between_count += 1
total_periods = len(all_normalized_prices)
prob_above = above_count / total_periods
prob_below = below_count / total_periods
prob_between = between_count / total_periods
latest_close_price = stock_data['Close'].iloc[-1]
actual_upper_threshold = latest_close_price * upper_threshold
actual_lower_threshold = latest_close_price * lower_threshold
window_days = list(range(-pre_announcement_window, post_announcement_window + 1))
fig = go.Figure()
for prices in all_normalized_prices:
if len(prices) == len(window_days):
fig.add_trace(go.Scatter(x=window_days, y=prices, mode='lines', line=dict(width=1), opacity=0.5, showlegend=False))
fig.add_hline(y=upper_threshold, line_dash="dash", line_color="green", annotation_text=f"+{(upper_threshold-1)*100:.2f}% Threshold (Price: {round(actual_upper_threshold, 2)})", annotation_position="top left")
fig.add_hline(y=lower_threshold, line_dash="dash", line_color="orange", annotation_text=f"-{(1-lower_threshold)*100:.2f}% Threshold (Price: {round(actual_lower_threshold, 2)})", annotation_position="bottom left")
fig.add_vline(x=0, line_dash="dash", line_color="red")
fig.update_layout(title=f"Normalized Price Movements Around Earnings Dates for {ticker}", xaxis_title="Days Relative to Earnings Date", yaxis_title="Normalized Price", legend_title="Legend", template='plotly_white', height=600, width=1200)
fig.add_trace(go.Scatter(x=[None], y=[None], mode='markers', marker=dict(size=10, color='white'), showlegend=True, name=f"Prob. Above +{(upper_threshold-1)*100:.2f}%: {prob_above:.2%}"))
fig.add_trace(go.Scatter(x=[None], y=[None], mode='markers', marker=dict(size=10, color='white'), showlegend=True, name=f"Prob. Below -{(1-lower_threshold)*100:.2f}%: {prob_below:.2%}"))
fig.add_trace(go.Scatter(x=[None], y=[None], mode='markers', marker=dict(size=10, color='white'), showlegend=True, name=f"Prob. Between: {prob_between:.2%}"))
return fig
def plot_volatility_around_earnings(stock_data, earnings_dates, window=5):
"""
Plot 20-day rolling volatility around earnings dates.
"""
volatilities = []
for earnings_date in earnings_dates.index:
start_date = earnings_date - timedelta(days=window)
end_date = earnings_date + timedelta(days=window)
subset = stock_data.loc[start_date:end_date, '20D Volatility']
date_range = pd.date_range(start=start_date, end=end_date)
subset = subset.reindex(date_range, fill_value=np.nan).fillna(method='ffill').fillna(method='bfill')
normalized_volatility = subset - subset.iloc[0]
volatilities.append(normalized_volatility.values)
volatility_data = pd.DataFrame(volatilities, index=earnings_dates.index)
fig = go.Figure()
for i in range(volatility_data.shape[0]):
fig.add_trace(go.Scatter(x=np.arange(-window, window + 1), y=volatility_data.iloc[i], mode='lines', showlegend=False, line=dict(width=1)))
fig.add_shape(dict(type="line", x0=0, y0=volatility_data.min().min(), x1=0, y1=volatility_data.max().max(), line=dict(color="red", width=2, dash="dash")))
fig.update_layout(title='20-Day Rolling Volatility Around Earnings Announcements', xaxis_title='Days Relative to Earnings', yaxis_title='20-Day Volatility', xaxis=dict(tickmode='array', tickvals=np.arange(-window, window + 1, 1)), template='plotly_white')
return fig
def plot_volume_around_earnings(stock_data, earnings_dates, window=5):
"""
Plot reindexed volume around earnings dates.
"""
volumes = []
for earnings_date in earnings_dates.index:
start_date = earnings_date - timedelta(days=window)
end_date = earnings_date + timedelta(days=window)
subset = stock_data.loc[start_date:end_date, 'Volume']
date_range = pd.date_range(start=start_date, end=end_date)
subset = subset.reindex(date_range, fill_value=np.nan).fillna(method='ffill').fillna(method='bfill')
normalized_volume = subset - subset.iloc[0]
volumes.append(normalized_volume.values)
volume_data = pd.DataFrame(volumes, index=earnings_dates.index)
fig = go.Figure()
for i in range(volume_data.shape[0]):
fig.add_trace(go.Scatter(x=np.arange(-window, window + 1), y=volume_data.iloc[i], mode='lines', showlegend=False, line=dict(width=1)))
fig.add_shape(dict(type="line", x0=0, y0=volume_data.min().min(), x1=0, y1=volume_data.max().max(), line=dict(color="red", width=2, dash="dash")))
fig.update_layout(title='Reindexed Volume Around Earnings Announcements', xaxis_title='Days Relative to Earnings', yaxis_title='Reindexed Volume', xaxis=dict(tickmode='array', tickvals=np.arange(-window, window + 1, 1)), template='plotly_white')
return fig
def compute_price_effect(earnings_date, stock_data):
"""
Compute price effects around earnings dates.
"""
try:
closest_date = stock_data.index[np.argmin(np.abs(stock_data.index - earnings_date))]
price_before_date = closest_date - pd.Timedelta(days=1)
price_on_date = closest_date
price_after_date = closest_date + pd.Timedelta(days=1)
price_before = stock_data.loc[:price_before_date, 'Close'].ffill().iloc[-1]
price_on = stock_data.loc[price_on_date, 'Close']
price_after = stock_data.loc[price_after_date:, 'Close'].bfill().iloc[0]
price_effect = ((price_after - price_before) / price_before) * 100
return price_before, price_on, price_after, price_effect
except (KeyError, IndexError) as e:
print(f"Missing data for date: {earnings_date} with error: {e}")
return None, None, None, None
def plot_price_effects(earnings_dates):
"""
Plot price effects around earnings dates.
"""
latest_earnings_data = earnings_dates.sort_index(ascending=False).head(14).sort_index()
fig = go.Figure()
positions = list(range(len(latest_earnings_data)))
width = 0.25
fig.add_trace(go.Bar(x=[pos - width for pos in positions], y=latest_earnings_data['Price Before'], width=width, name='Price Before', marker_color='blue'))
fig.add_trace(go.Bar(x=positions, y=latest_earnings_data['Price On'], width=width, name='Price On', marker_color='cyan'))
fig.add_trace(go.Bar(x=[pos + width for pos in positions], y=latest_earnings_data['Price After'], width=width, name='Price After', marker_color='lightblue'))
fig.add_trace(go.Scatter(x=positions, y=latest_earnings_data['Surprise(%)'], mode='lines+markers+text', name='Surprise(%)', marker=dict(color='red', size=8), text=[f"{round(val, 2)}%" for val in latest_earnings_data['Surprise(%)']], textposition="top center", yaxis='y2'))
fig.add_trace(go.Scatter(x=positions, y=latest_earnings_data['Price Effect (%)'], mode='lines+markers+text', name='Price Effect (%)', marker=dict(color='green', size=8), text=[f"{round(val, 2)}%" for val in latest_earnings_data['Price Effect (%)']], textposition="top center", yaxis='y2'))
fig.update_layout(title='Earnings Data with Surprise and Price Effect', xaxis=dict(tickmode='array', tickvals=positions, ticktext=latest_earnings_data.index.strftime('%Y-%m-%d'), tickangle=45), barmode='group', yaxis=dict(title='Price', side='left'), yaxis2=dict(title='Percentage (%)', overlaying='y', side='right', tickmode='auto', nticks=10, range=[min(latest_earnings_data['Surprise(%)'].min(), latest_earnings_data['Price Effect (%)'].min()) - 5, max(latest_earnings_data['Surprise(%)'].max(), latest_earnings_data['Price Effect (%)'].max()) + 5]), legend=dict(x=0.01, y=0.99, bordercolor="Black", borderwidth=1), template='plotly_white')
return fig
def plot_surprise_vs_price_effect(earnings_dates):
"""
Plot earnings surprise vs. price effect.
"""
filtered_earnings_data = earnings_dates.dropna(subset=['Surprise(%)', 'Price Effect (%)'])
if filtered_earnings_data.empty:
st.warning("Not enough data to plot Surprise vs. Price Effect.")
return go.Figure()
slope, intercept = np.polyfit(filtered_earnings_data['Surprise(%)'], filtered_earnings_data['Price Effect (%)'], 1)
x = np.array(filtered_earnings_data['Surprise(%)'])
y_pred = slope * x + intercept
correlation_matrix = np.corrcoef(filtered_earnings_data['Surprise(%)'], filtered_earnings_data['Price Effect (%)'])
correlation_xy = correlation_matrix[0, 1]
r_squared = correlation_xy**2
fig = go.Figure()
fig.add_trace(go.Scatter(x=filtered_earnings_data['Surprise(%)'], y=filtered_earnings_data['Price Effect (%)'], mode='markers', marker=dict(color='blue', size=8), name='Data Points'))
fig.add_trace(go.Scatter(x=x, y=y_pred, mode='lines', line=dict(color='red'), name=f'y={slope:.3f}x + {intercept:.3f}'))
fig.update_layout(title='Earnings Surprise vs. Price Effect', xaxis_title='Earnings Surprise(%)', yaxis_title='Price Effect(%)', template='plotly_white', height=600, width=1200, showlegend=True)
fig.add_annotation(x=0.05, y=0.95, xref='paper', yref='paper', text=f'R-squared = {r_squared:.3f}', showarrow=False, font=dict(size=15, color='green'))
return fig
def monte_carlo_simulation(ticker, annual_iv, days_to_earnings, upper_target, lower_target, stock_data, num_simulations=10000):
"""
Perform Monte Carlo simulation for stock price movements.
"""
current_price = stock_data['Close'].iloc[-1]
daily_iv = annual_iv / np.sqrt(252)
daily_returns = np.random.normal(0, daily_iv, (days_to_earnings, num_simulations))
price_paths = np.zeros_like(daily_returns)
price_paths[0] = current_price
for t in range(1, days_to_earnings):
price_paths[t] = price_paths[t-1] * (1 + daily_returns[t])
final_prices = price_paths[-1]
above_target = np.sum(final_prices > upper_target)
below_target = np.sum(final_prices < lower_target)
between_targets = num_simulations - above_target - below_target
prob_above = above_target / num_simulations
prob_below = below_target / num_simulations
prob_between = between_targets / num_simulations
fig = go.Figure()
for i in range(num_simulations):
fig.add_trace(go.Scatter(x=np.arange(days_to_earnings), y=price_paths[:, i], mode='lines', line=dict(color='lightblue', width=1), opacity=0.1, showlegend=False))
fig.add_trace(go.Scatter(x=[0, days_to_earnings-1], y=[upper_target, upper_target], mode='lines', line=dict(color='red', dash='dash'), name=f'Upper Target: {round(upper_target, 2)}'))
fig.add_trace(go.Scatter(x=[0, days_to_earnings-1], y=[lower_target, lower_target], mode='lines', line=dict(color='green', dash='dash'), name=f'Lower Target: {round(lower_target, 2)}'))
fig.add_trace(go.Scatter(x=[0, 0], y=[price_paths.min(), price_paths.max()], mode='lines', line=dict(color='red', dash='dash'), showlegend=False))
fig.add_annotation(x=0.05, y=0.95, xref='paper', yref='paper', text=f'P(>{round(upper_target, 2)}): {prob_above:.2%}<br>P(<{round(lower_target, 2)}): {prob_below:.2%}<br>P({round(lower_target, 2)}-{round(upper_target, 2)}): {prob_between:.2%}', showarrow=False, font=dict(size=12), bordercolor='black', borderwidth=1)
fig.update_layout(title=f"Monte Carlo Simulation of {ticker}'s Stock Price Over {days_to_earnings} Days", xaxis_title='Days', yaxis_title='Stock Price', template='plotly_white', height=600, width=1200, showlegend=True)
return fig
# Streamlit app
st.set_page_config(layout="wide")
st.title("Earnings Announcements Analysis")
st.write(
"""
This tool helps you analyze the impact of earnings announcements
on a company's stock price. By providing a ticker symbol and configuring the analysis parameters in the sidebar,
you can explore various aspects of stock price behavior around earnings dates and the likelihood of future movements.
"""
)
with st.expander("Key Features", expanded=False):
st.write(
"""
- **Stock Price with Earnings Surprises**: Visualize the stock price movement with indicators for positive and negative earnings surprises.
- **Normalized Price Movements**: Examine how the stock price changes relative to its price on the earnings announcement date.
- **Volatility Analysis**: Assess the stock's volatility around earnings dates to understand the market's reaction.
- **Volume Trends**: Analyze the trading volume before and after earnings announcements.
- **Price Effects**: Compare stock prices before, during, and after earnings to quantify the impact.
- **Earnings Surprise vs. Price Effect**: Investigate the correlation between earnings surprises and subsequent price changes.
- **Monte Carlo Simulations**: Use advanced statistical techniques to predict future price movements and estimate the probabilities of reaching specific price targets.
"""
)
st.sidebar.title("Input Parameters")
with st.sidebar.expander("How to Use", expanded=False):
st.write("""
**How to use this app:**
1. Enter the ticker symbol for the stock you want to analyze.
2. Adjust the pre and post-announcement windows to define the period around earnings dates.
3. Set the threshold percentage for price movement analysis.
4. Configure buffer days for fetching stock data.
5. Enter the implied volatility and days until earnings for Monte Carlo simulation.
6. Set the number of simulations for more precise results.
7. Check the box if you wish to run the Monte Carlo simulations (may slow down the app).
8. Click the "Run Analysis" button to start the analysis.
""")
with st.sidebar.expander("Ticker and Date Selection", expanded=True):
ticker = st.text_input("Enter Ticker Symbol", "MSFT", help="Enter the ticker symbol of the stock you want to analyze.")
pre_announcement_window = st.number_input("Pre-announcement Window (days)", value=5, min_value=1, help="Set the number of days before the earnings announcement to include in the analysis.")
post_announcement_window = st.number_input("Post-announcement Window (days)", value=10, min_value=1, help="Set the number of days after the earnings announcement to include in the analysis.")
with st.sidebar.expander("Analysis Parameters", expanded=True):
threshold_percentage = st.number_input("Threshold Percentage", value=0.10, min_value=0.01, max_value=1.0, step=0.01, help="Set the threshold for percentage changes in price analysis.")
buffer_days = st.number_input("Buffer Days", value=10, min_value=1, help="Set the number of buffer days around the earnings dates when fetching stock data.")
days_until_earnings = st.number_input("Days Until Earnings", value=10, min_value=1, help="Enter the number of days until the earnings announcement for the Monte Carlo simulation.")
with st.sidebar.expander("Monte Carlo Simulation", expanded=False):
run_simulation = st.checkbox("Run Monte Carlo Simulations (will take a few seconds)", value=False, help="Whether to run Monte Carlo Simulation Analysis. May slow down the app.")
implied_volatility = st.number_input("Implied Volatility", value=0.30, min_value=0.01, max_value=1.0, step=0.01, help="Enter the implied volatility for the Monte Carlo simulation.")
num_simulations = st.number_input("Number of Simulations for Monte Carlo", value=10000, min_value=100, help="Set the number of simulations for the Monte Carlo analysis.")
if st.sidebar.button("Run Analysis"):
try:
if not FMP_API_KEY:
st.error("Please set your FMP_API_KEY in the environment variables.")
else:
earnings_dates = fetch_earnings_data(ticker)
if earnings_dates.empty:
st.error("Failed to fetch earnings data. Please check the ticker or API key.")
else:
current_time = pd.Timestamp.now().tz_localize(None)
future_eps_estimate = earnings_dates.loc[earnings_dates.index > current_time]
if not future_eps_estimate.empty:
future_eps_estimate = future_eps_estimate.iloc[0]['EPS Estimate']
else:
future_eps_estimate = None
stock_data = fetch_stock_data(ticker, earnings_dates.index.min(), earnings_dates.index.max(), buffer_days)
if stock_data.empty:
st.error("Failed to fetch stock data. Please try again later.")
else:
stock_data = calculate_metrics(stock_data)
latest_close_price = stock_data['Close'].iloc[-1]
upper_threshold = 1 + threshold_percentage
lower_threshold = 1 - threshold_percentage
st.subheader("Earnings Announcements Data")
st.dataframe(earnings_dates)
st.subheader("Stock Price with Earnings Surprises")
st.markdown("This chart shows the stock price movements with markers indicating earnings surprises.")
st.plotly_chart(plot_stock_price_with_earnings(stock_data, earnings_dates, ticker), use_container_width=True)
st.subheader("Normalized Price Movements Around Earnings Dates")
st.markdown("This plot shows the normalized price movements of the stock around earnings dates.")
st.plotly_chart(plot_normalized_price_movements(stock_data, earnings_dates, ticker, pre_announcement_window, post_announcement_window, upper_threshold, lower_threshold), use_container_width=True)
st.subheader("Volatility Around Earnings Dates")
st.markdown("This plot shows the 20-day rolling volatility of the stock price around earnings dates.")
st.plotly_chart(plot_volatility_around_earnings(stock_data, earnings_dates), use_container_width=True)
st.subheader("Volume Around Earnings Dates")
st.markdown("This plot shows the trading volume changes around earnings dates.")
st.plotly_chart(plot_volume_around_earnings(stock_data, earnings_dates), use_container_width=True)
price_effects = earnings_dates.index.to_series().apply(compute_price_effect, stock_data=stock_data)
earnings_dates[['Price Before', 'Price On', 'Price After', 'Price Effect (%)']] = pd.DataFrame(price_effects.tolist(), index=earnings_dates.index)
earnings_dates.dropna(subset=['Price Before', 'Price On', 'Price After'], inplace=True)
st.subheader("Price Effects Around Earnings Dates")
st.markdown("This bar chart compares the stock prices before, on, and after the earnings dates.")
st.plotly_chart(plot_price_effects(earnings_dates), use_container_width=True)
st.subheader("Earnings Surprise vs. Price Effect")
st.markdown("This scatter plot shows the relationship between earnings surprise percentages and the resulting price effects.")
st.plotly_chart(plot_surprise_vs_price_effect(earnings_dates), use_container_width=True)
if run_simulation:
up_target = latest_close_price * upper_threshold
down_target = latest_close_price * lower_threshold
st.subheader("Monte Carlo Simulation for Price Movements")
st.markdown("We simulate multiple price paths using the stock's implied volatility to estimate the probabilities of the stock price reaching given targets.")
st.plotly_chart(monte_carlo_simulation(ticker, implied_volatility, days_until_earnings, up_target, down_target, stock_data, num_simulations), use_container_width=True)
except Exception as e:
st.error(f"An error occurred while running the analysis: {e}")
hide_streamlit_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)