feat: Add sophisticated Monte Carlo price simulation page

This commit is contained in:
Bobby (aider) 2025-02-17 15:07:31 -08:00
parent a90828eeb4
commit 87287574b2

View File

@ -0,0 +1,227 @@
import streamlit as st
import pandas as pd
import numpy as np
import yfinance as yf
from datetime import datetime, timedelta
from utils.common_utils import get_stock_data
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from typing import Tuple, List, Dict
from scipy import stats
class MonteCarloSimulator:
def __init__(self, data: pd.DataFrame, num_simulations: int, time_horizon: int):
"""
Initialize Monte Carlo simulator
Args:
data (pd.DataFrame): Historical price data
num_simulations (int): Number of simulation paths
time_horizon (int): Number of days to simulate
"""
self.data = data
self.num_simulations = num_simulations
self.time_horizon = time_horizon
self.returns = np.log(data['Close'] / data['Close'].shift(1)).dropna()
self.last_price = data['Close'].iloc[-1]
self.drift = self.returns.mean()
self.volatility = self.returns.std()
def run_simulation(self) -> np.ndarray:
"""Run Monte Carlo simulation and return paths"""
# Generate random walks
daily_returns = np.random.normal(
(self.drift + (self.volatility ** 2) / 2),
self.volatility,
size=(self.time_horizon, self.num_simulations)
)
# Calculate price paths
price_paths = np.zeros_like(daily_returns)
price_paths[0] = self.last_price
for t in range(1, self.time_horizon):
price_paths[t] = price_paths[t-1] * np.exp(daily_returns[t])
return price_paths
def calculate_metrics(self, paths: np.ndarray) -> Dict:
"""Calculate key metrics from simulation results"""
final_prices = paths[-1]
returns = (final_prices - self.last_price) / self.last_price
metrics = {
'Expected Price': np.mean(final_prices),
'Median Price': np.median(final_prices),
'Std Dev': np.std(final_prices),
'Skewness': stats.skew(final_prices),
'Kurtosis': stats.kurtosis(final_prices),
'95% CI Lower': np.percentile(final_prices, 2.5),
'95% CI Upper': np.percentile(final_prices, 97.5),
'Probability Above Current': np.mean(final_prices > self.last_price) * 100,
'Expected Return': np.mean(returns) * 100,
'VaR (95%)': np.percentile(returns, 5) * 100,
'CVaR (95%)': np.mean(returns[returns <= np.percentile(returns, 5)]) * 100
}
return metrics
def create_simulation_plot(paths: np.ndarray, dates: pd.DatetimeIndex,
ticker: str, last_price: float) -> go.Figure:
"""Create an interactive plot of simulation results"""
fig = make_subplots(
rows=2, cols=1,
subplot_titles=('Price Paths', 'Price Distribution at End Date'),
vertical_spacing=0.15,
row_heights=[0.7, 0.3]
)
# Plot confidence intervals
percentiles = np.percentile(paths, [5, 25, 50, 75, 95], axis=1)
# Add price paths
fig.add_trace(
go.Scatter(x=dates, y=percentiles[2], name='Median Path',
line=dict(color='blue', width=2)), row=1, col=1)
# Add confidence intervals
fig.add_trace(
go.Scatter(x=dates, y=percentiles[4], name='95% Confidence',
line=dict(color='rgba(0,100,80,0.2)', width=0)), row=1, col=1)
fig.add_trace(
go.Scatter(x=dates, y=percentiles[0], name='95% Confidence',
fill='tonexty', line=dict(color='rgba(0,100,80,0.2)', width=0)),
row=1, col=1)
# Add starting price line
fig.add_trace(
go.Scatter(x=dates, y=[last_price] * len(dates), name='Current Price',
line=dict(color='red', dash='dash')), row=1, col=1)
# Add histogram of final prices
fig.add_trace(
go.Histogram(x=paths[-1], name='Final Price Distribution',
nbinsx=50), row=2, col=1)
fig.update_layout(
title=f'Monte Carlo Simulation - {ticker}',
showlegend=True,
height=800
)
return fig
def monte_carlo_page():
st.title("Monte Carlo Price Simulation")
# Input parameters
col1, col2 = st.columns(2)
with col1:
ticker = st.text_input("Enter Ticker Symbol", value="AAPL").upper()
start_date = st.date_input(
"Start Date (for historical data)",
value=datetime.now() - timedelta(days=365)
)
end_date = st.date_input("End Date", value=datetime.now())
with col2:
num_simulations = st.number_input(
"Number of Simulations",
min_value=100,
max_value=10000,
value=1000,
step=100
)
time_horizon = st.number_input(
"Time Horizon (Days)",
min_value=5,
max_value=365,
value=30
)
confidence_level = st.slider(
"Confidence Level (%)",
min_value=80,
max_value=99,
value=95
)
if st.button("Run Simulation"):
with st.spinner('Running Monte Carlo simulation...'):
try:
# Get historical data
df = get_stock_data(
ticker,
datetime.combine(start_date, datetime.min.time()),
datetime.combine(end_date, datetime.min.time()),
'daily'
)
if df.empty:
st.error("No data available for the selected period")
return
# Initialize simulator
simulator = MonteCarloSimulator(df, num_simulations, time_horizon)
# Run simulation
paths = simulator.run_simulation()
# Calculate metrics
metrics = simulator.calculate_metrics(paths)
# Generate future dates for plotting
future_dates = pd.date_range(
start=end_date,
periods=time_horizon,
freq='B' # Business days
)
# Display results
col1, col2 = st.columns([3, 1])
with col1:
# Plot results
fig = create_simulation_plot(
paths, future_dates, ticker, simulator.last_price
)
st.plotly_chart(fig, use_container_width=True)
with col2:
st.subheader("Simulation Metrics")
# Price metrics
st.write("##### Price Projections")
st.metric("Expected Price", f"${metrics['Expected Price']:.2f}")
st.metric("95% CI", f"${metrics['95% CI Lower']:.2f} - ${metrics['95% CI Upper']:.2f}")
# Risk metrics
st.write("##### Risk Metrics")
st.metric("Expected Return", f"{metrics['Expected Return']:.1f}%")
st.metric("Value at Risk (95%)", f"{abs(metrics['VaR (95%)']):.1f}%")
st.metric("Conditional VaR", f"{abs(metrics['CVaR (95%)']):.1f}%")
# Distribution metrics
st.write("##### Distribution Metrics")
st.metric("Standard Deviation", f"${metrics['Std Dev']:.2f}")
st.metric("Skewness", f"{metrics['Skewness']:.2f}")
st.metric("Kurtosis", f"{metrics['Kurtosis']:.2f}")
# Add download button for simulation results
final_prices_df = pd.DataFrame({
'Simulation': range(1, num_simulations + 1),
'Final_Price': paths[-1],
'Return': (paths[-1] - simulator.last_price) / simulator.last_price * 100
})
csv = final_prices_df.to_csv(index=False)
st.download_button(
label="Download Simulation Results",
data=csv,
file_name=f'monte_carlo_{ticker}_{datetime.now().strftime("%Y%m%d")}.csv',
mime='text/csv'
)
except Exception as e:
st.error(f"Error during simulation: {str(e)}")
if __name__ == "__main__":
monte_carlo_page()