Source code for mcframework.sims.portfolio
"""Portfolio wealth simulation."""
from __future__ import annotations
from typing import Optional
import numpy as np
from numpy.random import Generator
from ..core import MonteCarloSimulation
__all__ = ["PortfolioSimulation"]
[docs]
class PortfolioSimulation(MonteCarloSimulation):
r"""
Compound an initial wealth under log-normal or arithmetic return models.
Let :math:`V_0` be the initial value. Under GBM dynamics the terminal value
after :math:`T` years with :math:`n = 252T` daily steps is
.. math::
V_T = V_0 \exp\left(\sum_{k=1}^n \Big[(\mu - \tfrac{1}{2}\sigma^2)\Delta t
+ \sigma \sqrt{\Delta t}\,Z_k\Big]\right),
where :math:`Z_k \sim \mathcal{N}(0, 1)` i.i.d. The alternative branch
integrates arithmetic returns via :math:`\log(1 + R_k)`.
Attributes
----------
name : str
Default registry label ``"Portfolio Simulation"``.
"""
[docs]
def __init__(self):
super().__init__("Portfolio Simulation")
[docs]
def single_simulation( # pylint: disable=arguments-differ
self,
*,
initial_value: float = 10_000.0,
annual_return: float = 0.07,
volatility: float = 0.20,
years: int = 10,
use_gbm: bool = True,
_rng: Optional[Generator] = None,
**kwargs,
) -> float:
r"""
Simulate the terminal portfolio value under discrete compounding.
Parameters
----------
initial_value : float, default ``10_000``
Starting wealth :math:`V_0` expressed in currency units.
annual_return : float, default ``0.07``
Drift :math:`\mu` expressed as an annualized continuously compounded
rate.
volatility : float, default ``0.20``
Annualized diffusion coefficient :math:`\sigma`.
years : int, default ``10``
Investment horizon :math:`T` in years. The simulation uses daily
steps :math:`\Delta t = 1/252`.
use_gbm : bool, default ``True``
If ``True`` evolve log returns via GBM; otherwise simulate simple
returns and compose them multiplicatively.
**kwargs : Any
Ignored. Reserved for framework compatibility.
Returns
-------
float
Terminal value :math:`V_T`. Under GBM the logarithm follows
:math:`\log V_T \sim \mathcal{N}\big(\log V_0 + (\mu - \tfrac{1}{2}\sigma^2)T,\;\sigma^2 T\big)`.
"""
rng = self._rng(_rng, self.rng)
dt = 1.0 / 252.0 # Daily steps
n = int(years / dt)
if use_gbm: # Geometric Brownian Motion for returns
mu, sigma = annual_return, volatility
rets = rng.normal((mu - 0.5 * sigma * sigma) * dt, sigma * np.sqrt(dt), size=n)
return float(initial_value * np.exp(rets.sum()))
rets = rng.normal(annual_return * dt, volatility * np.sqrt(dt), size=n)
return float(initial_value * np.exp(np.log1p(rets).sum()))