Source code for aleatory.processes.analytical.geometric_brownian

"""
Geometric Brownian Motion
"""

import numpy as np
from scipy.stats import lognorm

from aleatory.processes.base_analytical import SPAnalytical, SPAnalyticalMarginals
from aleatory.processes.analytical.brownian_motion import BrownianMotion
from aleatory.utils.utils import (
    check_positive_number,
    check_numeric,
    get_times,
    check_positive_integer,
)


[docs]class GBM(SPAnalyticalMarginals): r""" Geometric Brownian Motion ========================= A Geometric Brownian Motion object. .. image:: ../_static/geometric_brownian_motion_drawn.png Notes ----- A Geometric Brownian Motion :math:`\{X(t) : t \geq 0\}` is characterised by the following SDE. .. math:: dX_t = \mu X_t dt + \sigma X_t dW_t \ \ \ \ t\in (0,T] with initial condition :math:`X_0 = x_0\geq0`, where - :math:`\mu` is the drift - :math:`\sigma>0` is the volatility - :math:`W_t` is a standard Brownian Motion. The solution to this equation can be written as .. math:: X_t = x_0\exp\left((\mu + \frac{\sigma^2}{2} )t +\sigma W_t\right) and each :math:`X_t` follows a log-normal distribution. Examples -------- .. highlight:: python .. code-block:: python from aleatory.processes import GBM process = GBM() fig = process.draw(n=100, N=5, figsize=(12, 7)) fig.show() Constructor, Methods, and Attributes ------------------------------------ """
[docs] def __init__(self, drift=1.0, volatility=0.5, initial=1.0, T=1.0, rng=None): """ :param float drift: the parameter :math:`\\mu` in the above SDE :param float volatility: the parameter :math:`\\sigma>0` in the above SDE :param float initial: the initial condition :math:`x_0` in the above SDE :param float T: the right hand endpoint of the time interval :math:`[0,T]` for the process :param numpy.random.Generator rng: a custom random number generator """ super().__init__(T=T, rng=rng, initial=initial) self.drift = drift self.volatility = volatility self._brownian_motion = BrownianMotion(T=T, rng=rng) self.name = f"Geometric Brownian Motion $X(\\mu={self.drift}, \\sigma={self.volatility})$" self.n = None self.times = None
def __str__(self): return "Geometric Brownian motion with drift {d} and volatility {v} on [0, {T}].".format( T=str(self.T), d=str(self.drift), v=str(self.volatility) ) def __repr__(self): return "GeometricBrownianMotion(drift={d}, volatility={v}, T={T})".format( T=str(self.T), d=str(self.drift), v=str(self.volatility) ) @property def T(self): return self._T @T.setter def T(self, value): check_positive_number(value, "Time end") self._T = float(value) # Keep internal Brownian motion aligned with the process horizon. if hasattr(self, "_brownian_motion") and self._brownian_motion is not None: self._brownian_motion.T = self._T @property def drift(self): """Geometric Brownian motion drift parameter.""" return self._drift @drift.setter def drift(self, value): check_numeric(value, "Drift") self._drift = value @property def volatility(self): """Geometric Brownian motion volatility parameter.""" return self._volatility @volatility.setter def volatility(self, value): check_positive_number(value, "Volatility") self._volatility = value @property def initial(self): """Geometric Brownian motion initial point.""" return self._initial @initial.setter def initial(self, value): check_positive_number(value, "Initial Point") self._initial = value def _sample_geometric_brownian_motion(self, n): """Generate a realization of a geometric Brownian motion.""" check_positive_integer(n) check_positive_number(self.initial, "Initial") self.n = n self.times = get_times(self.T, n) return self.initial * np.exp( (self.drift - 0.5 * self.volatility**2) * self.times + self.volatility * self._brownian_motion.sample(n) ) def _sample_geometric_brownian_motion_at(self, times): """Generate a realization of a Geometric Brownian motion.""" self.times = times return self.initial * np.exp( (self.drift - 0.5 * self.volatility**2) * times + self.volatility * self._brownian_motion.sample_at(times) ) def sample(self, n): """Generate a realization.""" return self._sample_geometric_brownian_motion(n) def sample_at(self, times): """Generate a realization using specified times.""" return self._sample_geometric_brownian_motion_at(times) def _process_expectation(self, times=None): if times is None: times = self.times return self.initial * np.exp(self.drift * times) def _process_variance(self, times=None): if times is None: times = self.times variances = ( (self.initial**2) * np.exp(2 * self.drift * times) * (np.exp(times * self.volatility**2) - 1) ) return variances def _process_stds(self, times=None): if times is None: times = self.times variances = self._process_variance(times=times) stds = np.sqrt(variances) return stds def _process_covariance(self, times=None): if times is None: times = self.times t = np.asarray(times) tt = t[:, None] + t[None, :] tmin = np.minimum(t[:, None], t[None, :]) covariances = ( self.initial**2 * np.exp(self.drift * tt) * (np.exp(self.volatility**2 * tmin) - 1) ) return covariances def get_marginal(self, t): mu_x = np.log(self.initial) + (self.drift - 0.5 * self.volatility**2) * t sigma_x = self.volatility * np.sqrt(t) marginal = lognorm(s=sigma_x, scale=np.exp(mu_x)) return marginal def marginal_expectation(self, times=None): expectations = self._process_expectation(times=times) return expectations def marginal_variance(self, times=None): variances = self._process_variance(times=times) return variances
if __name__ == "__main__": p = GBM() # p.draw(n=200, N=200, T=9.0, figsize=(12, 7)) # p.draw(n=200, N=200, T=4.0, figsize=(12, 7)) # p.plot(n=100, N=100) # p.plot(n=100, N=100, T=3.0) # p.draw(n=100, N=100) # p.plot_covariance(times=np.linspace(0, 1, 100)) p.plot_kernel(times=np.linspace(0, 1, 100)) p.plot_kernel3d(times=np.linspace(0, 1, 100), title="Covariance Kernel of GBM") p.plot_paths_and_kernel(n=100, N=5) # p.plot_mean_variance(times=np.linspace(0, 1, 100))