mirror of
https://github.com/robertmartin8/PyPortfolioOpt.git
synced 2022-11-27 18:02:41 +03:00
change dict to OrderedDict to support py3.5
This commit is contained in:
@@ -6,7 +6,7 @@ optimisation.
|
||||
Additionally, we define a general utility function ``portfolio_performance`` to
|
||||
evaluate return and risk for a given set of portfolio weights.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import json
|
||||
import warnings
|
||||
import numpy as np
|
||||
@@ -48,14 +48,25 @@ class BaseOptimizer:
|
||||
# Outputs
|
||||
self.weights = None
|
||||
|
||||
def set_weights(self, weights):
|
||||
def _make_output_weights(self, weights=None):
|
||||
"""
|
||||
Utility function to set weights.
|
||||
Utility function to make output weight dict from weight attribute (np.array). If no
|
||||
arguments passed, use self.tickers and self.weights. If one argument is passed, assume
|
||||
it is an alternative weight array so use self.tickers and the argument.
|
||||
"""
|
||||
if weights is None:
|
||||
weights = self.weights
|
||||
|
||||
:param weights: {ticker: weight} dictionary
|
||||
:type weights: dict
|
||||
return collections.OrderedDict(zip(self.tickers, weights))
|
||||
|
||||
def set_weights(self, input_weights):
|
||||
"""
|
||||
self.weights = np.array([weights[ticker] for ticker in self.tickers])
|
||||
Utility function to set weights attribute (np.array) from user input
|
||||
|
||||
:param input_weights: {ticker: weight} dict
|
||||
:type input_weights: dict
|
||||
"""
|
||||
self.weights = np.array([input_weights[ticker] for ticker in self.tickers])
|
||||
|
||||
def clean_weights(self, cutoff=1e-4, rounding=5):
|
||||
"""
|
||||
@@ -68,7 +79,7 @@ class BaseOptimizer:
|
||||
Set to None if rounding is not desired.
|
||||
:type rounding: int, optional
|
||||
:return: asset weights
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if self.weights is None:
|
||||
raise AttributeError("Weights not yet computed")
|
||||
@@ -78,7 +89,8 @@ class BaseOptimizer:
|
||||
if not isinstance(rounding, int) or rounding < 1:
|
||||
raise ValueError("rounding must be a positive integer")
|
||||
clean_weights = np.round(clean_weights, rounding)
|
||||
return dict(zip(self.tickers, clean_weights))
|
||||
|
||||
return self._make_output_weights(clean_weights)
|
||||
|
||||
def save_weights_to_file(self, filename="weights.csv"):
|
||||
"""
|
||||
@@ -95,9 +107,11 @@ class BaseOptimizer:
|
||||
elif ext == "json":
|
||||
with open(filename, "w") as fp:
|
||||
json.dump(clean_weights, fp)
|
||||
else:
|
||||
elif ext == "txt":
|
||||
with open(filename, "w") as f:
|
||||
f.write(str(clean_weights))
|
||||
f.write(str(dict(clean_weights)))
|
||||
else:
|
||||
raise NotImplementedError("Only supports .txt .json .csv")
|
||||
|
||||
|
||||
class BaseConvexOptimizer(BaseOptimizer):
|
||||
@@ -206,6 +220,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
if opt.status != "optimal":
|
||||
raise exceptions.OptimizationError
|
||||
self.weights = self._w.value.round(16) + 0.0 # +0.0 removes signed zero
|
||||
return self._make_output_weights()
|
||||
|
||||
def add_objective(self, new_objective, **kwargs):
|
||||
"""
|
||||
@@ -300,7 +315,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
:type weights_sum_to_one: bool, optional
|
||||
:raises OptimizationError: if the objective is nonconvex or constraints nonlinear.
|
||||
:return: asset weights for the efficient risk portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
# custom_objective must have the right signature (w, **kwargs)
|
||||
self._objective = custom_objective(self._w, **kwargs)
|
||||
@@ -311,8 +326,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
if weights_sum_to_one:
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
|
||||
self._solve_cvxpy_opt_problem()
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
def nonconvex_objective(
|
||||
self,
|
||||
@@ -355,7 +369,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
User beware: different optimisers require different inputs.
|
||||
:type solver: string
|
||||
:return: asset weights that optimise the custom objective
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
# Sanitise inputs
|
||||
if not isinstance(objective_args, tuple):
|
||||
@@ -383,7 +397,7 @@ class BaseConvexOptimizer(BaseOptimizer):
|
||||
constraints=final_constraints,
|
||||
)
|
||||
self.weights = result["x"]
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._make_output_weights()
|
||||
|
||||
|
||||
def portfolio_performance(
|
||||
|
||||
@@ -426,7 +426,7 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
|
||||
:param risk_aversion: risk aversion parameter, defaults to 1
|
||||
:type risk_aversion: positive float, optional
|
||||
:return: asset weights implied by returns
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if risk_aversion is None:
|
||||
risk_aversion = self.risk_aversion
|
||||
@@ -436,7 +436,7 @@ class BlackLittermanModel(base_optimizer.BaseOptimizer):
|
||||
b = self.posterior_rets
|
||||
raw_weights = np.linalg.solve(A, b)
|
||||
self.weights = raw_weights / raw_weights.sum()
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._make_output_weights()
|
||||
|
||||
def optimize(self, risk_aversion=None):
|
||||
"""
|
||||
|
||||
@@ -376,7 +376,7 @@ class CLA(base_optimizer.BaseOptimizer):
|
||||
Maximise the Sharpe ratio.
|
||||
|
||||
:return: asset weights for the volatility-minimising portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if not self.w:
|
||||
self._solve()
|
||||
@@ -391,14 +391,14 @@ class CLA(base_optimizer.BaseOptimizer):
|
||||
sr.append(b)
|
||||
|
||||
self.weights = w_sr[sr.index(max(sr))].reshape((self.n_assets,))
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._make_output_weights()
|
||||
|
||||
def min_volatility(self):
|
||||
"""
|
||||
Minimise volatility.
|
||||
|
||||
:return: asset weights for the volatility-minimising portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if not self.w:
|
||||
self._solve()
|
||||
@@ -408,7 +408,7 @@ class CLA(base_optimizer.BaseOptimizer):
|
||||
var.append(a)
|
||||
# return min(var)**.5, self.w[var.index(min(var))]
|
||||
self.weights = self.w[var.index(min(var))].reshape((self.n_assets,))
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._make_output_weights()
|
||||
|
||||
def efficient_frontier(self, points=100):
|
||||
"""
|
||||
@@ -441,51 +441,6 @@ class CLA(base_optimizer.BaseOptimizer):
|
||||
self.frontier_values = (mu, sigma, weights)
|
||||
return mu, sigma, weights
|
||||
|
||||
def plot_efficient_frontier(
|
||||
self, points=100, show_assets=True, filename=None, showfig=True
|
||||
):
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
raise ImportError("Please install matplotlib via pip or poetry")
|
||||
|
||||
warnings.warn(
|
||||
"This method is deprecated and will be removed in v1.2.0. "
|
||||
"Please use pypfopt.plotting instead"
|
||||
)
|
||||
optimal_ret, optimal_risk, _ = self.portfolio_performance()
|
||||
|
||||
if self.frontier_values is None:
|
||||
self.efficient_frontier(points=points)
|
||||
|
||||
mus, sigmas, _ = self.frontier_values
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
ax.plot(sigmas, mus, label="Efficient frontier")
|
||||
|
||||
if show_assets:
|
||||
ax.scatter(
|
||||
np.sqrt(np.diag(self.cov_matrix)),
|
||||
self.expected_returns,
|
||||
s=30,
|
||||
color="k",
|
||||
label="assets",
|
||||
)
|
||||
|
||||
ax.scatter(
|
||||
optimal_risk, optimal_ret, marker="x", s=100, color="r", label="optimal"
|
||||
)
|
||||
ax.legend()
|
||||
ax.set_xlabel("Volatility")
|
||||
ax.set_ylabel("Return")
|
||||
|
||||
if filename:
|
||||
plt.savefig(fname=filename, dpi=300)
|
||||
|
||||
if showfig:
|
||||
plt.show()
|
||||
return ax
|
||||
|
||||
def set_weights(self, _):
|
||||
# Overrides parent method since set_weights does nothing.
|
||||
raise NotImplementedError("set_weights does nothing for CLA")
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
The ``discrete_allocation`` module contains the ``DiscreteAllocation`` class, which
|
||||
offers multiple methods to generate a discrete portfolio allocation from continuous weights.
|
||||
"""
|
||||
|
||||
import collections
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import cvxpy as cp
|
||||
@@ -233,7 +233,7 @@ class DiscreteAllocation:
|
||||
available_funds -= price
|
||||
|
||||
self.allocation = self._remove_zero_positions(
|
||||
dict(zip([i[0] for i in self.weights], shares_bought))
|
||||
collections.OrderedDict(zip([i[0] for i in self.weights], shares_bought))
|
||||
)
|
||||
|
||||
if verbose:
|
||||
@@ -311,7 +311,7 @@ class DiscreteAllocation:
|
||||
|
||||
vals = np.rint(x.value)
|
||||
self.allocation = self._remove_zero_positions(
|
||||
dict(zip([i[0] for i in self.weights], vals))
|
||||
collections.OrderedDict(zip([i[0] for i in self.weights], vals))
|
||||
)
|
||||
|
||||
if verbose:
|
||||
|
||||
@@ -139,7 +139,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
Minimise volatility.
|
||||
|
||||
:return: asset weights for the volatility-minimising portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
self._objective = objective_functions.portfolio_variance(
|
||||
self._w, self.cov_matrix
|
||||
@@ -149,8 +149,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
|
||||
self._solve_cvxpy_opt_problem()
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
def max_sharpe(self, risk_free_rate=0.02):
|
||||
"""
|
||||
@@ -166,7 +165,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
:type risk_free_rate: float, optional
|
||||
:raises ValueError: if ``risk_free_rate`` is non-numeric
|
||||
:return: asset weights for the Sharpe-maximising portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if not isinstance(risk_free_rate, (int, float)):
|
||||
raise ValueError("risk_free_rate should be numeric")
|
||||
@@ -213,7 +212,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
self._solve_cvxpy_opt_problem()
|
||||
# Inverse-transform
|
||||
self.weights = (self._w.value / k.value).round(16) + 0.0
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._make_output_weights()
|
||||
|
||||
def max_quadratic_utility(self, risk_aversion=1, market_neutral=False):
|
||||
r"""
|
||||
@@ -230,7 +229,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
defaults to False. Requires negative lower weight bound.
|
||||
:param market_neutral: bool, optional
|
||||
:return: asset weights for the maximum-utility portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if risk_aversion <= 0:
|
||||
raise ValueError("risk aversion coefficient must be greater than zero")
|
||||
@@ -247,8 +246,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
else:
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
|
||||
self._solve_cvxpy_opt_problem()
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
def efficient_risk(self, target_volatility, market_neutral=False):
|
||||
"""
|
||||
@@ -263,7 +261,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
:raises ValueError: if no portfolio can be found with volatility equal to ``target_volatility``
|
||||
:raises ValueError: if ``risk_free_rate`` is non-numeric
|
||||
:return: asset weights for the efficient risk portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if not isinstance(target_volatility, (float, int)) or target_volatility < 0:
|
||||
raise ValueError("target_volatility should be a positive float")
|
||||
@@ -286,8 +284,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
else:
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
|
||||
self._solve_cvxpy_opt_problem()
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
def efficient_return(self, target_return, market_neutral=False):
|
||||
"""
|
||||
@@ -301,7 +298,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
:raises ValueError: if ``target_return`` is not a positive float
|
||||
:raises ValueError: if no portfolio can be found with return equal to ``target_return``
|
||||
:return: asset weights for the Markowitz portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if not isinstance(target_return, float) or target_return < 0:
|
||||
raise ValueError("target_return should be a positive float")
|
||||
@@ -333,9 +330,7 @@ class EfficientFrontier(base_optimizer.BaseConvexOptimizer):
|
||||
else:
|
||||
self._constraints.append(cp.sum(self._w) == 1)
|
||||
|
||||
self._solve_cvxpy_opt_problem()
|
||||
|
||||
return dict(zip(self.tickers, self.weights))
|
||||
return self._solve_cvxpy_opt_problem()
|
||||
|
||||
def portfolio_performance(self, verbose=False, risk_free_rate=0.02):
|
||||
"""
|
||||
|
||||
@@ -12,6 +12,7 @@ Currently implemented:
|
||||
permission from Marcos Lopez de Prado (2016).
|
||||
"""
|
||||
|
||||
import collections
|
||||
import warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
@@ -143,7 +144,7 @@ class HRPOpt(base_optimizer.BaseOptimizer):
|
||||
Construct a hierarchical risk parity portfolio
|
||||
|
||||
:return: weights for the HRP portfolio
|
||||
:rtype: dict
|
||||
:rtype: OrderedDict
|
||||
"""
|
||||
if self.returns is None:
|
||||
cov = self.cov_matrix
|
||||
@@ -155,47 +156,17 @@ class HRPOpt(base_optimizer.BaseOptimizer):
|
||||
# per https://stackoverflow.com/questions/18952587/
|
||||
|
||||
# this can avoid some nasty floating point issues
|
||||
matrix = np.sqrt(np.clip((1.0 - corr) / 2., a_min=0.0, a_max=1.0))
|
||||
matrix = np.sqrt(np.clip((1.0 - corr) / 2.0, a_min=0.0, a_max=1.0))
|
||||
dist = ssd.squareform(matrix, checks=False)
|
||||
|
||||
self.clusters = sch.linkage(dist, "single")
|
||||
sort_ix = HRPOpt._get_quasi_diag(self.clusters)
|
||||
ordered_tickers = corr.index[sort_ix].tolist()
|
||||
hrp = HRPOpt._raw_hrp_allocation(cov, ordered_tickers)
|
||||
weights = dict(hrp.sort_index())
|
||||
weights = collections.OrderedDict(hrp.sort_index())
|
||||
self.set_weights(weights)
|
||||
return weights
|
||||
|
||||
def plot_dendrogram(self, show_tickers=True, filename=None, showfig=True):
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
except (ModuleNotFoundError, ImportError):
|
||||
raise ImportError("Please install matplotlib via pip or poetry")
|
||||
|
||||
warnings.warn(
|
||||
"This method is deprecated and will be removed in v1.2.0. "
|
||||
"Please use pypfopt.Plotting instead"
|
||||
)
|
||||
|
||||
if self.clusters is None:
|
||||
self.optimize()
|
||||
|
||||
fig, ax = plt.subplots()
|
||||
if show_tickers:
|
||||
sch.dendrogram(self.clusters, labels=self.tickers, ax=ax)
|
||||
plt.xticks(rotation=90)
|
||||
plt.tight_layout()
|
||||
else:
|
||||
sch.dendrogram(self.clusters, ax=ax)
|
||||
|
||||
if filename:
|
||||
plt.savefig(fname=filename, dpi=300)
|
||||
|
||||
if showfig:
|
||||
plt.show()
|
||||
|
||||
return ax
|
||||
|
||||
def portfolio_performance(self, verbose=False, risk_free_rate=0.02, frequency=252):
|
||||
"""
|
||||
After optimising, calculate (and optionally print) the performance of the optimal
|
||||
|
||||
@@ -19,7 +19,6 @@ The format of the data input is the same as that in :ref:`expected-returns`.
|
||||
- Oracle Approximating shrinkage
|
||||
|
||||
- covariance to correlation matrix
|
||||
- plot of the covariance matrix (deprecated)
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
@@ -189,6 +189,7 @@ def test_set_weights():
|
||||
def test_save_weights_to_file():
|
||||
ef = setup_efficient_frontier()
|
||||
ef.min_volatility()
|
||||
|
||||
ef.save_weights_to_file("tests/test.txt")
|
||||
with open("tests/test.txt", "r") as f:
|
||||
file = f.read()
|
||||
|
||||
Reference in New Issue
Block a user