def _process_parameters(dim: OptNumeric, mean: OptNumeric, cov: OptNumeric, df: Optional[float]): if dim is None: if cov is None: dim = 1 cov = np.array([[1.]], dtype=float) else: if isinstance(cov, (float, int)): cov = np.array([[cov]], dtype=float) dim = len(cov) cov = np.asarray(cov, dtype=float).reshape(dim, dim) else: if not np.isscalar(dim): raise ValueError("Dimension of random variable must be a scalar.") if mean is None: mean = np.zeros(dim) elif isinstance(mean, (float, int)): mean = np.repeat(mean, dim) else: mean = np.asarray(mean, dtype=float) # checks done here if mean.shape[0] != dim or mean.ndim != 1: raise ValueError("Array 'mean' must be a vector of length %d." % dim) if not is_psd(cov): raise ValueError("Matrix 'cov' must be positive semi-definite") if df is None: df = 4.6692 # Random Feigenbaum Number elif df <= 0: raise ValueError("Degrees of freedom 'df' must be greater than 0") return dim, mean, cov, df
def _get_covariance_matrix(cov_or_data: np.ndarray, freq: Frequency): """ Helper to derive a covariance matrix. If simulated cube is put in, derive empirical covariance matrix. Otherwise, check that matrix is a covariance matrix and return it. """ assert cov_or_data.ndim in ( 2, 3 ), "only 2D covariance matrix or a 3D simulation returns tensor is allowed" cov_or_data = np.asarray(cov_or_data) n = cov_or_data.shape[ -1] # number of assets must be last dimension of cube or covariance matrix if cov_or_data.ndim == 3: # if 3D, get empirical covariance matrix assert n == cov_or_data.shape[ 2], "number of weights does not match simulation cube asset quantity" assert freq is not None, "frequency must be specified if deriving covariance structure from simulated data" cov_or_data = portfolio_cov(cov_or_data, freq) assert is_psd( cov_or_data), "covariance matrix is not positive semi-definite" return cov_or_data # this will be a covariance matrix
def log_lik(self, data: np.ndarray): if not is_psd(self.sigma): return -np.inf if hasattr(self, '_df') and self._df <= 0: # t copula return -np.inf return super().log_lik(data)
def log_lik(self, data: np.ndarray, *, to_pobs=True, ties="average"): if not is_psd(self.sigma): return -np.inf if hasattr(self, '_df') and getattr(self, "_df", 0) <= 0: # t copula return -np.inf return super().log_lik(data, to_pobs=to_pobs, ties=ties)
def __init__(self, data: np.ndarray, time_unit='monthly'): """ Returns an OptData class which is an enhancement of ndarray with helper methods Parameters ---------- data: ndarray 3D tensor where the first axis represents the time period, the second axis represents the trials and the third axis represents the assets time_unit: {int, 'monthly', 'quarterly', 'semi-annually', 'yearly'}, optional Specifies how many units (first axis) is required to represent a year. For example, if each time period represents a month, set this to 12. If quarterly, set to 4. Defaults to 12 which means 1 period represents a month. Alternatively, specify one of 'monthly', 'quarterly', 'semi-annually' or 'yearly' """ assert data.ndim == 3, "Data must be 3 dimensional with shape like (t, n, a) where `t` represents the time " \ "periods, `n` represents the trials and `a` represents the assets" periods, trials, n_assets = data.shape self.time_unit = translate_frequency(time_unit) # empirical covariance taken along the time-asset axis then averaged by trials # annualized data a = (data + 1).reshape(periods // self.time_unit, self.time_unit, trials, n_assets).prod(1) - 1 cov_mat = np.mean( [np.cov(a[i].T) for i in range(periods // self.time_unit)], 0) cov_mat = near_psd(cov_mat) assert is_psd( cov_mat), "covariance matrix must be positive semi-definite" if np.allclose(np.diag(cov_mat), 1) and np.alltrue(np.abs(cov_mat) <= 1): warnings.warn( "The covariance matrix feels like a correlation matrix. Are you sure it's correct?" ) self.n_years = periods / self.time_unit self.n_assets = n_assets # minor optimization when using rebalanced optimization. This is essentially a cache self._unrebalanced_returns_data: Optional[np.ndarray] = None self._cov_mat = cov_mat
def _process_parameters(dim: OptNumeric, mean: OptNumeric, cov: OptNumeric, df: Optional[float]): if dim is None: if cov is None: dim = 1 cov = np.array([[1.]], dtype=float) else: if isinstance(cov, (float, int)): cov = np.array([[cov]], dtype=float) dim = len(cov) cov = np.asarray(cov, dtype=float).reshape(dim, dim) else: if not np.isscalar(dim): raise ValueError( "Dimension of random variable must be a scalar.") if mean is None: mean = np.zeros(dim) elif isinstance(mean, (float, int)): mean = np.repeat(mean, dim) else: mean = np.asarray(mean, dtype=float) # checks done here if mean.shape[0] != dim or mean.ndim != 1: raise ValueError("Array 'mean' must be a vector of length %d." % dim) if not is_psd(cov): raise ValueError("Matrix 'cov' must be positive semi-definite") if df is None: df = 4.6692 # Random Feigenbaum Number elif df <= 0: raise ValueError("Degrees of freedom 'df' must be greater than 0") return dim, mean, cov, df
def sharpe_ratio_bmk_m(data: np.ndarray, weights: Vector, bmk_weights: Vector, freq: Frequency, cov: np.ndarray = None, geometric: bool = True, rebalance: bool = True): """ Calculates the Sharpe Ratio of the portfolio over the benchmark. The benchmark components must be placed after the portfolio components in the simulated returns cube. Parameters ---------- data Monte carlo simulation data. This must be 3 dimensional with the axis representing time, trial and asset respectively. weights Weights of the portfolio. This must be 1 dimensional and must match the dimension of the data's last axis. bmk_weights Weights of the benchmark portfolio. freq Frequency of the data. Can either be a string ('week', 'month', 'quarter', 'semi-annual', 'year') or an integer specifying the number of units per year. Week: 52, Month: 12, Quarter: 4, Semi-annual: 6, Year: 1. cov Covariance matrix. If not specified, the empirical covariance matrix from the joined data and benchmark data will be derived. geometric If True, calculates the geometric mean, otherwise, calculates the arithmetic mean. rebalance If True, portfolio is assumed to be rebalanced at every step. Returns ------- float Sharpe ratio of the portfolio over the benchmark Examples -------- >>> from perfana.datasets import load_cube >>> from perfana.monte_carlo import sharpe_ratio_bmk_m >>> cube = load_cube() >>> weights = [0.25, 0.18, 0.13, 0.11, 0.24, 0.05, 0.04] >>> bmk_weights = [0.65, 0.35] >>> freq = 'quarterly' >>> sharpe_ratio_bmk_m(cube, weights, bmk_weights, freq) -0.2186945589389277 """ gr = annualized_bmk_returns_m(data, weights, bmk_weights, freq, geometric, rebalance) if cov is not None: n = len(weights) + len(bmk_weights) assert cov.shape == (n, n), "covariance matrix shape incorrect" assert is_psd(cov), "covariance matrix is not positive semi-definite" data = cov te = tracking_error_m(data, weights, bmk_weights, freq) return gr / te