Path: blob/master/src/FMNM/Processes.py
1700 views
#!/usr/bin/env python31# -*- coding: utf-8 -*-2"""3Created on Sat Jul 27 17:06:01 201945@author: cantaro866"""78import numpy as np9import scipy.stats as ss10from FMNM.probabilities import VG_pdf11from scipy.optimize import minimize12from statsmodels.tools.numdiff import approx_hess13import pandas as pd141516class Diffusion_process:17"""18Class for the diffusion process:19r = risk free constant rate20sig = constant diffusion coefficient21mu = constant drift22"""2324def __init__(self, r=0.1, sig=0.2, mu=0.1):25self.r = r26self.mu = mu27if sig <= 0:28raise ValueError("sig must be positive")29else:30self.sig = sig3132def exp_RV(self, S0, T, N):33W = ss.norm.rvs((self.r - 0.5 * self.sig**2) * T, np.sqrt(T) * self.sig, N)34S_T = S0 * np.exp(W)35return S_T.reshape((N, 1))363738class Merton_process:39"""40Class for the Merton process:41r = risk free constant rate42sig = constant diffusion coefficient43lam = jump activity44muJ = jump mean45sigJ = jump standard deviation46"""4748def __init__(self, r=0.1, sig=0.2, lam=0.8, muJ=0, sigJ=0.5):49self.r = r50self.lam = lam51self.muJ = muJ52if sig < 0 or sigJ < 0:53raise ValueError("sig and sigJ must be positive")54else:55self.sig = sig56self.sigJ = sigJ5758# moments59self.var = self.sig**2 + self.lam * self.sigJ**2 + self.lam * self.muJ**260self.skew = self.lam * (3 * self.sigJ**2 * self.muJ + self.muJ**3) / self.var ** (1.5)61self.kurt = self.lam * (3 * self.sigJ**3 + 6 * self.sigJ**2 * self.muJ**2 + self.muJ**4) / self.var**26263def exp_RV(self, S0, T, N):64m = self.lam * (np.exp(self.muJ + (self.sigJ**2) / 2) - 1) # coefficient m65W = ss.norm.rvs(0, 1, N) # The normal RV vector66P = ss.poisson.rvs(self.lam * T, size=N) # Poisson random vector (number of jumps)67Jumps = np.asarray([ss.norm.rvs(self.muJ, self.sigJ, ind).sum() for ind in P]) # Jumps vector68S_T = S0 * np.exp(69(self.r - 0.5 * self.sig**2 - m) * T + np.sqrt(T) * self.sig * W + Jumps70) # Martingale exponential Merton71return S_T.reshape((N, 1))727374class VG_process:75"""76Class for the Variance Gamma process:77r = risk free constant rate78Using the representation of Brownian subordination, the parameters are:79theta = drift of the Brownian motion80sigma = standard deviation of the Brownian motion81kappa = variance of the of the Gamma process82"""8384def __init__(self, r=0.1, sigma=0.2, theta=-0.1, kappa=0.1):85self.r = r86self.c = self.r87self.theta = theta88self.kappa = kappa89if sigma < 0:90raise ValueError("sigma must be positive")91else:92self.sigma = sigma9394# moments95self.mean = self.c + self.theta96self.var = self.sigma**2 + self.theta**2 * self.kappa97self.skew = (2 * self.theta**3 * self.kappa**2 + 3 * self.sigma**2 * self.theta * self.kappa) / (98self.var ** (1.5)99)100self.kurt = (1013 * self.sigma**4 * self.kappa102+ 12 * self.sigma**2 * self.theta**2 * self.kappa**2103+ 6 * self.theta**4 * self.kappa**3104) / (self.var**2)105106def exp_RV(self, S0, T, N):107w = -np.log(1 - self.theta * self.kappa - self.kappa / 2 * self.sigma**2) / self.kappa # coefficient w108rho = 1 / self.kappa109G = ss.gamma(rho * T).rvs(N) / rho # The gamma RV110Norm = ss.norm.rvs(0, 1, N) # The normal RV111VG = self.theta * G + self.sigma * np.sqrt(G) * Norm # VG process at final time G112S_T = S0 * np.exp((self.r - w) * T + VG) # Martingale exponential VG113return S_T.reshape((N, 1))114115def path(self, T=1, N=10000, paths=1):116"""117Creates Variance Gamma paths118N = number of time points (time steps are N-1)119paths = number of generated paths120"""121dt = T / (N - 1) # time interval122X0 = np.zeros((paths, 1))123G = ss.gamma(dt / self.kappa, scale=self.kappa).rvs(size=(paths, N - 1)) # The gamma RV124Norm = ss.norm.rvs(loc=0, scale=1, size=(paths, N - 1)) # The normal RV125increments = self.c * dt + self.theta * G + self.sigma * np.sqrt(G) * Norm126X = np.concatenate((X0, increments), axis=1).cumsum(1)127return X128129def fit_from_data(self, data, dt=1, method="Nelder-Mead"):130"""131Fit the 4 parameters of the VG process using MM (method of moments),132Nelder-Mead, L-BFGS-B.133134data (array): datapoints135dt (float): is the increment time136137Returns (c, theta, sigma, kappa)138"""139X = data140sigma_mm = np.std(X) / np.sqrt(dt)141kappa_mm = dt * ss.kurtosis(X) / 3142theta_mm = np.sqrt(dt) * ss.skew(X) * sigma_mm / (3 * kappa_mm)143c_mm = np.mean(X) / dt - theta_mm144145def log_likely(x, data, T):146return (-1) * np.sum(np.log(VG_pdf(data, T, x[0], x[1], x[2], x[3])))147148if method == "L-BFGS-B":149if theta_mm < 0:150result = minimize(151log_likely,152x0=[c_mm, theta_mm, sigma_mm, kappa_mm],153method="L-BFGS-B",154args=(X, dt),155tol=1e-8,156bounds=[[-0.5, 0.5], [-0.6, -1e-15], [1e-15, 1], [1e-15, 2]],157)158else:159result = minimize(160log_likely,161x0=[c_mm, theta_mm, sigma_mm, kappa_mm],162method="L-BFGS-B",163args=(X, dt),164tol=1e-8,165bounds=[[-0.5, 0.5], [1e-15, 0.6], [1e-15, 1], [1e-15, 2]],166)167print(result.message)168elif method == "Nelder-Mead":169result = minimize(170log_likely,171x0=[c_mm, theta_mm, sigma_mm, kappa_mm],172method="Nelder-Mead",173args=(X, dt),174options={"disp": False, "maxfev": 3000},175tol=1e-8,176)177print(result.message)178elif "MM":179self.c, self.theta, self.sigma, self.kappa = (180c_mm,181theta_mm,182sigma_mm,183kappa_mm,184)185return186self.c, self.theta, self.sigma, self.kappa = result.x187188189class Heston_process:190"""191Class for the Heston process:192r = risk free constant rate193rho = correlation between stock noise and variance noise194theta = long term mean of the variance process195sigma = volatility coefficient of the variance process196kappa = mean reversion coefficient for the variance process197"""198199def __init__(self, mu=0.1, rho=0, sigma=0.2, theta=-0.1, kappa=0.1):200self.mu = mu201if np.abs(rho) > 1:202raise ValueError("|rho| must be <=1")203self.rho = rho204if theta < 0 or sigma < 0 or kappa < 0:205raise ValueError("sigma,theta,kappa must be positive")206else:207self.theta = theta208self.sigma = sigma209self.kappa = kappa210211def path(self, S0, v0, N, T=1):212"""213Produces one path of the Heston process.214N = number of time steps215T = Time in years216Returns two arrays S (price) and v (variance).217"""218219MU = np.array([0, 0])220COV = np.matrix([[1, self.rho], [self.rho, 1]])221W = ss.multivariate_normal.rvs(mean=MU, cov=COV, size=N - 1)222W_S = W[:, 0] # Stock Brownian motion: W_1223W_v = W[:, 1] # Variance Brownian motion: W_2224225# Initialize vectors226T_vec, dt = np.linspace(0, T, N, retstep=True)227dt_sq = np.sqrt(dt)228229X0 = np.log(S0)230v = np.zeros(N)231v[0] = v0232X = np.zeros(N)233X[0] = X0234235# Generate paths236for t in range(0, N - 1):237v_sq = np.sqrt(v[t])238v[t + 1] = np.abs(v[t] + self.kappa * (self.theta - v[t]) * dt + self.sigma * v_sq * dt_sq * W_v[t])239X[t + 1] = X[t] + (self.mu - 0.5 * v[t]) * dt + v_sq * dt_sq * W_S[t]240241return np.exp(X), v242243244class NIG_process:245"""246Class for the Normal Inverse Gaussian process:247r = risk free constant rate248Using the representation of Brownian subordination, the parameters are:249theta = drift of the Brownian motion250sigma = standard deviation of the Brownian motion251kappa = variance of the of the Gamma process252"""253254def __init__(self, r=0.1, sigma=0.2, theta=-0.1, kappa=0.1):255self.r = r256self.theta = theta257if sigma < 0 or kappa < 0:258raise ValueError("sigma and kappa must be positive")259else:260self.sigma = sigma261self.kappa = kappa262263# moments264self.var = self.sigma**2 + self.theta**2 * self.kappa265self.skew = (3 * self.theta**3 * self.kappa**2 + 3 * self.sigma**2 * self.theta * self.kappa) / (266self.var ** (1.5)267)268self.kurt = (2693 * self.sigma**4 * self.kappa270+ 18 * self.sigma**2 * self.theta**2 * self.kappa**2271+ 15 * self.theta**4 * self.kappa**3272) / (self.var**2)273274def exp_RV(self, S0, T, N):275lam = T**2 / self.kappa # scale for the IG process276mu_s = T / lam # scaled mean277w = (1 - np.sqrt(1 - 2 * self.theta * self.kappa - self.kappa * self.sigma**2)) / self.kappa278IG = ss.invgauss.rvs(mu=mu_s, scale=lam, size=N) # The IG RV279Norm = ss.norm.rvs(0, 1, N) # The normal RV280X = self.theta * IG + self.sigma * np.sqrt(IG) * Norm # NIG random vector281S_T = S0 * np.exp((self.r - w) * T + X) # exponential dynamics282return S_T.reshape((N, 1))283284285class GARCH:286"""287Class for the GARCH(1,1) process. Variance process:288289V(t) = omega + alpha R^2(t-1) + beta V(t-1)290291VL: Unconditional variance >=0292alpha: coefficient > 0293beta: coefficient > 0294gamma = 1 - alpha - beta295omega = gamma*VL296"""297298def __init__(self, VL=0.04, alpha=0.08, beta=0.9):299if VL < 0 or alpha <= 0 or beta <= 0:300raise ValueError("VL>=0, alpha>0 and beta>0")301else:302self.VL = VL303self.alpha = alpha304self.beta = beta305self.gamma = 1 - self.alpha - self.beta306self.omega = self.gamma * self.VL307308def path(self, N=1000):309"""310Generates a path with N points.311Returns the return process R and the variance process var312"""313eps = ss.norm.rvs(loc=0, scale=1, size=N)314R = np.zeros_like(eps)315var = np.zeros_like(eps)316for i in range(N):317var[i] = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var[i - 1]318R[i] = np.sqrt(var[i]) * eps[i]319return R, var320321def fit_from_data(self, data, disp=True):322"""323MLE estimator for the GARCH324"""325# Automatic re-scaling:326# 1. the solver has problems with positive derivative in linesearch.327# 2. the log has overflows using small values328n = np.floor(np.log10(np.abs(data.mean())))329R = data / 10**n330331# initial guesses332a0 = 0.05333b0 = 0.9334g0 = 1 - a0 - b0335w0 = g0 * np.var(R)336337# bounds and constraint338bounds = ((0, None), (0, 1), (0, 1))339340def sum_small_1(x):341return 1 - x[1] - x[2]342343cons = {"fun": sum_small_1, "type": "ineq"}344345def log_likely(x):346var = R[0] ** 2 # initial variance347N = len(R)348log_lik = 0349for i in range(1, N):350var = x[0] + x[1] * R[i - 1] ** 2 + x[2] * var # variance update351log_lik += -np.log(var) - (R[i] ** 2 / var)352return (-1) * log_lik353354result = minimize(355log_likely,356x0=[w0, a0, b0],357method="SLSQP",358bounds=bounds,359constraints=cons,360tol=1e-8,361options={"maxiter": 150},362)363print(result.message)364self.omega = result.x[0] * 10 ** (2 * n)365self.alpha, self.beta = result.x[1:]366self.gamma = 1 - self.alpha - self.beta367self.VL = self.omega / self.gamma368369if disp is True:370hess = approx_hess(result.x, log_likely) # hessian by finite differences371se = np.sqrt(np.diag(np.linalg.inv(hess))) # standard error372cv = ss.norm.ppf(1.0 - 0.05 / 2.0) # alpha=0.05373p_val = ss.norm.sf(np.abs(result.x / se)) # survival function374375df = pd.DataFrame(index=["omega", "alpha", "beta"])376df["Params"] = result.x377df["SE"] = se378df["P-val"] = p_val379df["95% CI lower"] = result.x - cv * se380df["95% CI upper"] = result.x + cv * se381df.loc["omega", ["Params", "SE", "95% CI lower", "95% CI upper"]] *= 10 ** (2 * n)382print(df)383384def log_likelihood(self, R, last_var=True):385"""386Computes the log-likelihood and optionally returns the last value387of the variance388"""389var = R[0] ** 2 # initial variance390N = len(R)391log_lik = 0392log_2pi = np.log(2 * np.pi)393for i in range(1, N):394var = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var # variance update395log_lik += 0.5 * (-log_2pi - np.log(var) - (R[i] ** 2 / var))396if last_var is True:397return log_lik, var398else:399return log_lik400401def generate_var(self, R, R0, var0):402"""403generate the variance process.404R (array): return array405R0: initial value of the returns406var0: initial value of the variance407"""408N = len(R)409var = np.zeros(N)410var[0] = self.omega + self.alpha * (R0**2) + self.beta * var0411for i in range(1, N):412var[i] = self.omega + self.alpha * R[i - 1] ** 2 + self.beta * var[i - 1]413return var414415416class OU_process:417"""418Class for the OU process:419theta = long term mean420sigma = diffusion coefficient421kappa = mean reversion coefficient422"""423424def __init__(self, sigma=0.2, theta=-0.1, kappa=0.1):425self.theta = theta426if sigma < 0 or kappa < 0:427raise ValueError("sigma,theta,kappa must be positive")428else:429self.sigma = sigma430self.kappa = kappa431432def path(self, X0=0, T=1, N=10000, paths=1):433"""434Produces a matrix of OU process: X[N, paths]435X0 = starting point436N = number of time points (there are N-1 time steps)437T = Time in years438paths = number of paths439"""440441dt = T / (N - 1)442X = np.zeros((N, paths))443X[0, :] = X0444W = ss.norm.rvs(loc=0, scale=1, size=(N - 1, paths))445446std_dt = np.sqrt(self.sigma**2 / (2 * self.kappa) * (1 - np.exp(-2 * self.kappa * dt)))447for t in range(0, N - 1):448X[t + 1, :] = self.theta + np.exp(-self.kappa * dt) * (X[t, :] - self.theta) + std_dt * W[t, :]449450return X451452453