change dict to OrderedDict to support py3.5

This commit is contained in:
robertmartin8
2020-06-07 14:11:52 +08:00
parent 8d991da378
commit 95fd0b3245
8 changed files with 53 additions and 118 deletions

View File

@@ -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(

View File

@@ -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):
"""

View File

@@ -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")

View File

@@ -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:

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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

View File

@@ -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()