feat: Add sophisticated Monte Carlo price simulation page
This commit is contained in:
parent
a90828eeb4
commit
87287574b2
227
src/pages/analysis/monte_carlo_page.py
Normal file
227
src/pages/analysis/monte_carlo_page.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user