def __init__(self): self.weights = list() self.seriated_distances = None self.seriated_correlations = None self.ordered_indices = None self.clusters = None self.returns_estimator = ReturnsEstimators() self.risk_metrics = RiskMetrics() self.risk_estimator = RiskEstimators()
def setUp(self): """ Initialize and get the test data """ # Stock prices data to test the Covariance functions project_path = os.path.dirname(__file__) data_path = project_path + '/test_data/stock_prices.csv' self.data = pd.read_csv(data_path, parse_dates=True, index_col="Date") # And series of returns ret_est = ReturnsEstimators() self.returns = ret_est.calculate_returns(self.data)
def test_hrp_with_input_as_covariance_matrix(self): """ Test HRP when passing a covariance matrix as input. """ hrp = HierarchicalRiskParity() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) hrp.allocate(asset_names=self.data.columns, covariance_matrix=returns.cov()) weights = hrp.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_valuerror_with_no_asset_names(self): """ Test ValueError when not supplying a list of asset names and no other input. """ with self.assertRaises(ValueError): cla = CriticalLineAlgorithm() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() cla.allocate(expected_asset_returns=expected_returns, covariance_matrix=covariance)
def test_hrp_with_nan_inputs(self): """ Test HRP with NaN inputs """ hrp = HierarchicalRiskParity() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) covariance = returns.cov() covariance *= np.nan hrp.allocate(covariance_matrix=covariance) weights = hrp.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_valuerror_with_no_asset_names(self): """ Test ValueError when not supplying a list of asset names and no other input """ with self.assertRaises(ValueError): mvo = MeanVarianceOptimisation() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() mvo.allocate(expected_asset_returns=expected_returns, covariance_matrix=covariance.values)
def test_herc_with_asset_returns_as_none(self): """ Test HERC when asset returns are not required for calculating the weights. """ herc = HierarchicalEqualRiskContribution() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) herc.allocate(asset_names=self.data.columns, covariance_matrix=returns.cov(), optimal_num_clusters=5, risk_measure='equal_weighting') weights = herc.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_herc_with_input_as_covariance_matrix(self): """ Test HERC when passing a covariance matrix as input. """ herc = HierarchicalEqualRiskContribution() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) herc.allocate(asset_names=self.data.columns, covariance_matrix=returns.cov(), optimal_num_clusters=6, asset_returns=returns) weights = herc.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_entropy_pooling_for_all_constraints(self): """ Test the Entropy Pooling algorithm for both equality and inequality constraints. """ returns = ReturnsEstimators().calculate_returns(self.data.iloc[:, :5]) num_time_stamps = returns.shape[0] p_initial = np.array([1 / num_time_stamps] * num_time_stamps) equality_matrix = returns[['EEM', 'EWG']].values equality_vector = [2.3e-02, 3.3e-02] inequality_matrix = returns[['TIP', 'EWJ', 'EFA']].values inequality_vector = [1.1e-3, 2.2e-4, 3.8e-6] ep_solver = EntropyPooling() ep_solver.calculate_posterior_probabilities( prior_probabilities=p_initial, equality_matrix=equality_matrix, equality_vector=equality_vector, inequality_matrix=inequality_matrix, inequality_vector=inequality_vector) assert len(ep_solver.posterior_probabilities) == len(p_initial) np.testing.assert_almost_equal( equality_matrix.T.dot( ep_solver.posterior_probabilities)[0][0].round(3), equality_vector[0]) np.testing.assert_almost_equal( equality_matrix.T.dot( ep_solver.posterior_probabilities)[1][0].round(3), equality_vector[1]) assert ep_solver.posterior_probabilities.T.dot( inequality_matrix)[0][0] >= 1.1e-3 assert inequality_matrix.T.dot( ep_solver.posterior_probabilities)[1][0] >= 2.2e-4 assert inequality_matrix.T.dot( ep_solver.posterior_probabilities)[2][0] >= 3.8e-6
def test_entropy_pooling_for_only_equality_constraints(self): # pylint: disable=invalid-name """ Test Entropy Pooling when passing only equality constraints. """ returns = ReturnsEstimators().calculate_returns(self.data.iloc[:, :5]) num_time_stamps = returns.shape[0] p_initial = np.array([1 / num_time_stamps] * num_time_stamps) equality_matrix = returns[['EEM', 'EWG']].values equality_vector = [2.3e-02, 3.3e-02] ep_solver = EntropyPooling() ep_solver.calculate_posterior_probabilities( prior_probabilities=p_initial, equality_matrix=equality_matrix, equality_vector=equality_vector) assert len(ep_solver.posterior_probabilities) == len(p_initial) np.testing.assert_almost_equal( equality_matrix.T.dot( ep_solver.posterior_probabilities)[0][0].round(3), equality_vector[0]) np.testing.assert_almost_equal( equality_matrix.T.dot( ep_solver.posterior_probabilities)[1][0].round(3), equality_vector[1])
def test_custom_objective_function(self): """ Test custom portfolio objective and allocation constraints. """ mvo = MeanVarianceOptimisation() custom_obj = 'cp.Minimize(risk)' constraints = ['cp.sum(weights) == 1', 'weights >= 0', 'weights <= 1'] non_cvxpy_variables = { 'num_assets': self.data.shape[1], 'covariance': self.data.cov(), 'expected_returns': ReturnsEstimators().calculate_mean_historical_returns( asset_prices=self.data, resample_by='W') } cvxpy_variables = [ 'risk = cp.quad_form(weights, covariance)', 'portfolio_return = cp.matmul(weights, expected_returns)' ] mvo.allocate_custom_objective(non_cvxpy_variables=non_cvxpy_variables, cvxpy_variables=cvxpy_variables, objective_function=custom_obj, constraints=constraints) weights = mvo.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] assert mvo.asset_names == list(map(str, range(mvo.num_assets))) assert mvo.portfolio_return == 0.012854555899642236 assert mvo.portfolio_risk == 3.0340907720046832 np.testing.assert_almost_equal(np.sum(weights), 1)
def test_value_error_for_custom_obj_optimal_weights(self): # pylint: disable=invalid-name """ Test ValueError when no optimal weights are found for custom objective solution. """ with self.assertRaises(ValueError): mvo = MeanVarianceOptimisation() custom_obj = 'cp.Minimize(risk - kappa)' constraints = [ 'cp.sum(weights) == 1', 'weights >= 0', 'weights <= 1' ] non_cvxpy_variables = { 'num_assets': self.data.shape[1], 'covariance': self.data.cov(), 'expected_returns': ReturnsEstimators().calculate_mean_historical_returns( asset_prices=self.data, resample_by='W') } cvxpy_variables = [ 'risk = cp.quad_form(weights, covariance)', 'portfolio_return = cp.matmul(weights, expected_returns)', 'kappa = cp.Variable(1)' ] mvo.allocate_custom_objective( non_cvxpy_variables=non_cvxpy_variables, cvxpy_variables=cvxpy_variables, objective_function=custom_obj, constraints=constraints)
def __init__(self, confidence_level=0.05): """ Initialise. :param confidence_level: (float) The confidence level (alpha) used for calculating expected shortfall and conditional drawdown at risk. """ self.weights = list() self.clusters = None self.ordered_indices = None self.cluster_children = None self.optimal_num_clusters = None self.returns_estimator = ReturnsEstimators() self.risk_estimator = RiskEstimators() self.risk_metrics = RiskMetrics() self.confidence_level = confidence_level
def test_no_asset_names_by_passing_cov(self): """ Test MVO when not supplying a list of asset names but passing covariance matrix as input """ mvo = MeanVarianceOptimisation() expected_returns = ReturnsEstimators( ).calculate_exponential_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() mvo.allocate(expected_asset_returns=expected_returns, covariance_matrix=covariance) weights = mvo.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_exception_in_plotting_efficient_frontier(self): # pylint: disable=invalid-name, protected-access """ Test raising of exception when plotting the efficient frontier. """ mvo = MeanVarianceOptimisation() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() plot = mvo.plot_efficient_frontier( covariance=covariance, max_return=1.0, expected_asset_returns=expected_returns) assert len(plot._A) == 41
def test_plotting_efficient_frontier(self): # pylint: disable=invalid-name, protected-access """ Test the plotting of the efficient frontier. """ mvo = MeanVarianceOptimisation() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() plot = mvo.plot_efficient_frontier( covariance=covariance, max_return=1.0, expected_asset_returns=expected_returns) assert plot.axes.xaxis.label._text == 'Volatility' assert plot.axes.yaxis.label._text == 'Return' assert len(plot._A) == 41
def test_mvo_with_input_as_returns_and_covariance(self): # pylint: disable=invalid-name """ Test MVO when we pass expected returns and covariance matrix as input. """ mvo = MeanVarianceOptimisation() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data, resample_by='W') covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data, resample_by='W').cov() mvo.allocate(covariance_matrix=covariance, expected_asset_returns=expected_returns, asset_names=self.data.columns) weights = mvo.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_valuerror_with_no_asset_names(self): """ Test ValueError when not supplying a list of asset names and no other input. """ with self.assertRaises(ValueError): hrp = HierarchicalRiskParity() returns = ReturnsEstimators().calculate_returns( asset_prices=self.data) hrp.allocate(asset_returns=returns.values)
def test_value_error_with_no_asset_names(self): """ Test ValueError when not supplying a list of asset names and no other input """ with self.assertRaises(ValueError): herc = HierarchicalEqualRiskContribution() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) herc.allocate(asset_returns=returns.values, optimal_num_clusters=6)
def __init__(self, calculate_expected_returns='mean', risk_free_rate=0.03): """ Constructor. :param calculate_expected_returns: (str) The method to use for calculation of expected returns. Currently supports: ``mean``, ``exponential``. """ self.weights = list() self.asset_names = None self.num_assets = None self.portfolio_risk = None self.portfolio_return = None self.portfolio_sharpe_ratio = None self.calculate_expected_returns = calculate_expected_returns self.returns_estimator = ReturnsEstimators() self.risk_estimators = RiskEstimators() self.weight_bounds = (0, 1) self.risk_free_rate = risk_free_rate
def test_cla_with_input_as_returns_and_covariance(self): # pylint: disable=invalid-name """ Test CLA when we pass expected returns and covariance matrix as input. """ cla = CriticalLineAlgorithm() expected_returns = ReturnsEstimators( ).calculate_mean_historical_returns(asset_prices=self.data) covariance = ReturnsEstimators().calculate_returns( asset_prices=self.data).cov() cla.allocate(covariance_matrix=covariance, expected_asset_returns=expected_returns, asset_names=self.data.columns) weights = cla.weights.values weights[weights <= 1e-15] = 0 # Convert very very small numbers to 0 for turning_point in weights: assert (turning_point >= 0).all() assert len(turning_point) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(turning_point), 1)
def test_hrp_with_input_as_distance_matrix(self): """ Test HRP when passing a distance matrix as input. """ hrp = HierarchicalRiskParity() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) covariance = returns.cov() corr = RiskEstimators.cov_to_corr(covariance) corr = pd.DataFrame(corr, index=covariance.columns, columns=covariance.columns) distance_matrix = np.sqrt((1 - corr).round(5) / 2) hrp.allocate(asset_names=self.data.columns, covariance_matrix=covariance, distance_matrix=distance_matrix) weights = hrp.weights.values[0] self.assertTrue((weights >= 0).all()) self.assertTrue(len(weights) == self.data.shape[1]) self.assertAlmostEqual(np.sum(weights), 1)
def setUp(self): """ Initialize and load data """ project_path = os.path.dirname(__file__) # Loading the price series of ETFs price_data_path = project_path + '/test_data/stock_prices.csv' self.price_data = pd.read_csv(price_data_path, parse_dates=True, index_col="Date") # Transforming series of prices to series of returns ret_est = ReturnsEstimators() self.returns_data = ret_est.calculate_returns(self.price_data) # Loading the classification tree of ETFs classification_tree_path = project_path + '/test_data/classification_tree.csv' self.classification_tree = pd.read_csv(classification_tree_path)
def test_no_asset_names_with_asset_returns(self): """ Test HRP when not supplying a list of asset names and when the user passes asset_returns. """ hrp = HierarchicalRiskParity() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) hrp.allocate(asset_returns=returns) weights = hrp.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_herc_with_input_as_returns(self): """ Test HERC when passing asset returns dataframe as input. """ herc = HierarchicalEqualRiskContribution() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) herc.allocate(asset_returns=returns, asset_names=self.data.columns) weights = herc.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def test_no_asset_names_with_asset_returns(self): """ Test HERC when not supplying a list of asset names and when the user passes asset_returns. """ herc = HierarchicalEqualRiskContribution() returns = ReturnsEstimators().calculate_returns(asset_prices=self.data) herc.allocate(asset_returns=returns, optimal_num_clusters=6) weights = herc.weights.values[0] assert (weights >= 0).all() assert len(weights) == self.data.shape[1] np.testing.assert_almost_equal(np.sum(weights), 1)
def __init__(self, weight_bounds=(0, 1), calculate_expected_returns="mean"): """ Initialise the storage arrays and some preprocessing. :param weight_bounds: (tuple) A tuple specifying the lower and upper bound ranges for the portfolio weights. :param calculate_expected_returns: (str) The method to use for calculation of expected returns. Currently supports ``mean`` and ``exponential`` """ self.weight_bounds = weight_bounds self.calculate_expected_returns = calculate_expected_returns self.weights = list() self.lambdas = list() self.gammas = list() self.free_weights = list() self.max_sharpe = None self.min_var = None self.efficient_frontier_means = None self.efficient_frontier_sigma = None self.returns_estimator = ReturnsEstimators() self.risk_estimators = RiskEstimators()
def test_value_error_for_no_inequality_vector(self): """ Test ValueError when no inequality vector is specified. """ with self.assertRaises(ValueError): returns = ReturnsEstimators().calculate_returns( self.data.iloc[:, :5]) num_time_stamps = returns.shape[0] p_initial = np.array([1 / num_time_stamps] * num_time_stamps) inequality_matrix = returns[['TIP', 'EWJ', 'EFA']].values ep_solver = EntropyPooling() ep_solver.calculate_posterior_probabilities( prior_probabilities=p_initial, inequality_matrix=inequality_matrix)
def test_value_error_all_inputs_null(self): """ Test ValueError when no equality and inequality matricies have been specified. """ with self.assertRaises(ValueError): returns = ReturnsEstimators().calculate_returns( self.data.iloc[:, :5]) num_time_stamps = returns.shape[0] p_initial = np.array([1 / num_time_stamps] * num_time_stamps) equality_vector = [2.3e-02, 3.3e-02] inequality_vector = [1.1e-3, 2.2e-4, 3.8e-6] ep_solver = EntropyPooling() ep_solver.calculate_posterior_probabilities( prior_probabilities=p_initial, equality_vector=equality_vector, inequality_vector=inequality_vector)
def test_value_error_for_diff_lengths_of_inequality_constraints(self): # pylint: disable=invalid-name """ Test ValueError when the lengths of inequality matrix and inequality vector are different. """ with self.assertRaises(ValueError): returns = ReturnsEstimators().calculate_returns( self.data.iloc[:, :5]) num_time_stamps = returns.shape[0] p_initial = np.array([1 / num_time_stamps] * num_time_stamps) inequality_matrix = returns[['EEM', 'EWG']].values inequality_vector = [2.3e-02] ep_solver = EntropyPooling() ep_solver.calculate_posterior_probabilities( prior_probabilities=p_initial, inequality_matrix=inequality_matrix, inequality_vector=inequality_vector)