def monte_carlo_simulation(simulations: int = 1000, days: int = 252, visualize: bool = False) -> str:
"""
Runs a Monte Carlo simulation using Geometric Brownian Motion (Log Returns).
Args:
simulations: Number of paths to simulate.
days: Number of days to project forward.
visualize: If True, returns a histogram of final outcomes.
"""
data, weights = _get_portfolio_data()
if data is None:
return "Portfolio is empty."
# Use Log Returns for additivity
log_returns = np.log(data / data.shift(1)).dropna()
mean_log_returns = log_returns.mean()
cov_matrix = log_returns.cov()
# Cholesky Decomposition
try:
L = np.linalg.cholesky(cov_matrix)
except np.linalg.LinAlgError:
# Fallback for non-positive definite matrix (e.g., too few data points)
return "Covariance matrix is not positive definite. Insufficient data history."
portfolio_sims = np.zeros((days, simulations))
initial_value = 1.0
for i in range(simulations):
Z = np.random.normal(size=(days, len(weights)))
# Correlated random shocks
daily_shocks = np.dot(Z, L.T)
# GBM: S_t = S_0 * exp( (mu - 0.5*sigma^2)*t + sigma*W_t )
# Here we simulate daily steps
daily_log_ret = mean_log_returns.values + daily_shocks
# Portfolio level log return
port_log_ret = np.dot(daily_log_ret, weights)
# Accumulate log returns
cum_log_ret = np.cumsum(port_log_ret)
portfolio_sims[:, i] = initial_value * np.exp(cum_log_ret)
final_values = portfolio_sims[-1, :]
returns = (final_values - 1) * 100 # Convert to percentage
expected_return = np.mean(final_values) - 1
worst_case = np.percentile(final_values, 5) - 1
best_case = np.percentile(final_values, 95) - 1
result = (f"Monte Carlo Results ({simulations} sims, {days} days) [Log Normal]:\n"
f"Expected Return: {expected_return:.2%}\n"
f"5th Percentile (VaR 95%): {worst_case:.2%}\n"
f"95th Percentile (Upside): {best_case:.2%}")
if visualize:
try:
from tools.visualizer import plot_histogram
chart = plot_histogram(
returns,
bins=50,
title=f"Monte Carlo Simulation - {simulations} Paths ({days} days)",
x_label="Return (%)",
percentiles=[5, 50, 95]
)
result += f"\n\n{chart}"
except Exception as e:
logger.error(f"Error generating visualization: {e}")
result += f"\n(Visualization error: {str(e)})"
return result