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