diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7233c35d..5b20760e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,8 @@ repos: hooks: - id: ruff args: [ --fix, --exit-non-zero-on-fix ] + # Run the formatter + - id: ruff-format - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.43.0 diff --git a/book/marimo/demo.py b/book/marimo/demo.py index 35854d6f..527e6b96 100644 --- a/book/marimo/demo.py +++ b/book/marimo/demo.py @@ -115,9 +115,7 @@ def __(portfolio): @app.cell def __(pd, portfolio, portfolio_resampled): - frame = pd.DataFrame( - {"original": portfolio.nav, "monthly": portfolio_resampled.nav} - ) + frame = pd.DataFrame({"original": portfolio.nav, "monthly": portfolio_resampled.nav}) frame return (frame,) diff --git a/cvx/linalg/cholesky.py b/cvx/linalg/cholesky.py index fc37502c..ab2aa65e 100644 --- a/cvx/linalg/cholesky.py +++ b/cvx/linalg/cholesky.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Cholesky decomposition with numpy""" + from __future__ import annotations import numpy as np diff --git a/cvx/linalg/pca.py b/cvx/linalg/pca.py index fd5c4944..0f373f60 100644 --- a/cvx/linalg/pca.py +++ b/cvx/linalg/pca.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """PCA analysis with numpy""" + from __future__ import annotations from dataclasses import dataclass @@ -28,9 +29,7 @@ class PCA: def __post_init__(self) -> None: if self.n_components > self.returns.shape[1]: - raise ValueError( - "The number of components cannot exceed the number of assets" - ) + raise ValueError("The number of components cannot exceed the number of assets") # compute the principal components without sklearn # 1. compute the correlation @@ -51,9 +50,7 @@ def __post_init__(self) -> None: @property def explained_variance(self) -> Matrix: - return np.array( - self.eigenvalues[: self.n_components] / np.sum(self.eigenvalues) - ) + return np.array(self.eigenvalues[: self.n_components] / np.sum(self.eigenvalues)) @property def cov(self) -> Matrix: diff --git a/cvx/linalg/valid.py b/cvx/linalg/valid.py index 28e54719..3ce2b99a 100644 --- a/cvx/linalg/valid.py +++ b/cvx/linalg/valid.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Extract valid submatrix of a matrix""" + from __future__ import annotations import numpy as np diff --git a/cvx/markowitz/builder.py b/cvx/markowitz/builder.py index fe9c2082..c5f25487 100644 --- a/cvx/markowitz/builder.py +++ b/cvx/markowitz/builder.py @@ -121,13 +121,9 @@ def __post_init__(self) -> None: self.model[M.RISK] = FactorModel(assets=self.assets, factors=self.factors) # add variable for factor weights - self.variables[D.FACTOR_WEIGHTS] = cp.Variable( - self.factors, name=D.FACTOR_WEIGHTS - ) + self.variables[D.FACTOR_WEIGHTS] = cp.Variable(self.factors, name=D.FACTOR_WEIGHTS) # add bounds for factor weights - self.model[M.BOUND_FACTORS] = Bounds( - assets=self.factors, name="factors", acting_on=D.FACTOR_WEIGHTS - ) + self.model[M.BOUND_FACTORS] = Bounds(assets=self.factors, name="factors", acting_on=D.FACTOR_WEIGHTS) # add variable for absolute factor weights self.variables[D._ABS] = cp.Variable(self.factors, name=D._ABS, nonneg=True) @@ -141,9 +137,7 @@ def __post_init__(self) -> None: self.variables[D.WEIGHTS] = cp.Variable(self.assets, name=D.WEIGHTS) # add bounds on assets - self.model[M.BOUND_ASSETS] = Bounds( - assets=self.assets, name="assets", acting_on=D.WEIGHTS - ) + self.model[M.BOUND_ASSETS] = Bounds(assets=self.assets, name="assets", acting_on=D.WEIGHTS) @property @abstractmethod @@ -157,9 +151,7 @@ def build(self) -> _Problem: Build the cvxpy problem """ for name_model, model in self.model.items(): - for name_constraint, constraint in model.constraints( - self.variables - ).items(): + for name_constraint, constraint in model.constraints(self.variables).items(): self.constraints[f"{name_model}_{name_constraint}"] = constraint problem = cp.Problem(self.objective, list(self.constraints.values())) diff --git a/cvx/markowitz/model.py b/cvx/markowitz/model.py index 328cafc0..16c6fd17 100644 --- a/cvx/markowitz/model.py +++ b/cvx/markowitz/model.py @@ -11,8 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Abstract cp model -""" +"""Abstract cp model""" + from __future__ import annotations from abc import ABC, abstractmethod diff --git a/cvx/markowitz/models/bounds.py b/cvx/markowitz/models/bounds.py index 208ffd21..bfa94547 100644 --- a/cvx/markowitz/models/bounds.py +++ b/cvx/markowitz/models/bounds.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Bounds""" + from __future__ import annotations from dataclasses import dataclass @@ -49,17 +50,11 @@ def __post_init__(self) -> None: ) def update(self, **kwargs: Matrix) -> None: - self.data[self._f("lower")].value = fill_vector( - num=self.assets, x=kwargs[self._f("lower")] - ) - self.data[self._f("upper")].value = fill_vector( - num=self.assets, x=kwargs[self._f("upper")] - ) + self.data[self._f("lower")].value = fill_vector(num=self.assets, x=kwargs[self._f("lower")]) + self.data[self._f("upper")].value = fill_vector(num=self.assets, x=kwargs[self._f("upper")]) def constraints(self, variables: Variables) -> Expressions: return { - f"lower bound {self.name}": variables[self.acting_on] - >= self.data[self._f("lower")], - f"upper bound {self.name}": variables[self.acting_on] - <= self.data[self._f("upper")], + f"lower bound {self.name}": variables[self.acting_on] >= self.data[self._f("lower")], + f"upper bound {self.name}": variables[self.acting_on] <= self.data[self._f("upper")], } diff --git a/cvx/markowitz/models/expected_returns.py b/cvx/markowitz/models/expected_returns.py index af6d816f..65d26d7f 100644 --- a/cvx/markowitz/models/expected_returns.py +++ b/cvx/markowitz/models/expected_returns.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Model for expected returns""" + from __future__ import annotations from dataclasses import dataclass @@ -46,9 +47,7 @@ def __post_init__(self) -> None: ) def estimate(self, variables: Variables) -> cp.Expression: - return self.data[D.MU] @ variables[D.WEIGHTS] - self.parameter[ - "mu_uncertainty" - ] @ cp.abs(variables[D.WEIGHTS]) + return self.data[D.MU] @ variables[D.WEIGHTS] - self.parameter["mu_uncertainty"] @ cp.abs(variables[D.WEIGHTS]) def update(self, **kwargs: Matrix) -> None: exp_returns = kwargs[D.MU] @@ -59,6 +58,4 @@ def update(self, **kwargs: Matrix) -> None: if not uncertainty.shape[0] == exp_returns.shape[0]: raise CvxError("Mismatch in length for mu and mu_uncertainty") - self.parameter["mu_uncertainty"].value = fill_vector( - num=self.assets, x=uncertainty - ) + self.parameter["mu_uncertainty"].value = fill_vector(num=self.assets, x=uncertainty) diff --git a/cvx/markowitz/models/holding_costs.py b/cvx/markowitz/models/holding_costs.py index 7682c09b..1053bdd1 100644 --- a/cvx/markowitz/models/holding_costs.py +++ b/cvx/markowitz/models/holding_costs.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Model for holding costs""" + from __future__ import annotations from dataclasses import dataclass @@ -30,16 +31,10 @@ class HoldingCosts(Model): """Model for holding costs""" def __post_init__(self) -> None: - self.data[D.HOLDING_COSTS] = cp.Parameter( - shape=self.assets, name=D.HOLDING_COSTS, value=np.zeros(self.assets) - ) + self.data[D.HOLDING_COSTS] = cp.Parameter(shape=self.assets, name=D.HOLDING_COSTS, value=np.zeros(self.assets)) def estimate(self, variables: Variables) -> cp.Expression: - return cp.sum( - cp.neg(cp.multiply(variables[D.WEIGHTS], self.data[D.HOLDING_COSTS])) - ) + return cp.sum(cp.neg(cp.multiply(variables[D.WEIGHTS], self.data[D.HOLDING_COSTS]))) def update(self, **kwargs: Matrix) -> None: - self.data[D.HOLDING_COSTS].value = fill_vector( - num=self.assets, x=kwargs[D.HOLDING_COSTS] - ) + self.data[D.HOLDING_COSTS].value = fill_vector(num=self.assets, x=kwargs[D.HOLDING_COSTS]) diff --git a/cvx/markowitz/models/trading_costs.py b/cvx/markowitz/models/trading_costs.py index c916bf84..1e22563d 100644 --- a/cvx/markowitz/models/trading_costs.py +++ b/cvx/markowitz/models/trading_costs.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. """Model for trading costs""" + from __future__ import annotations from dataclasses import dataclass @@ -33,9 +34,7 @@ def __post_init__(self) -> None: self.parameter["power"] = cp.Parameter(shape=1, name="power", value=np.ones(1)) # initial weights before rebalancing - self.data["weights"] = cp.Parameter( - shape=self.assets, name="weights", value=np.zeros(self.assets) - ) + self.data["weights"] = cp.Parameter(shape=self.assets, name="weights", value=np.zeros(self.assets)) def estimate(self, variables: Variables) -> cp.Expression: return cp.sum( diff --git a/cvx/markowitz/portfolios/max_sharpe.py b/cvx/markowitz/portfolios/max_sharpe.py index c7ef2a3e..feb08d87 100644 --- a/cvx/markowitz/portfolios/max_sharpe.py +++ b/cvx/markowitz/portfolios/max_sharpe.py @@ -26,7 +26,6 @@ @dataclass(frozen=True) class MaxSharpe(Builder): - """ Minimize the standard deviation of the portfolio returns subject to a set of constraints min StdDev(r_p) @@ -42,12 +41,8 @@ def __post_init__(self) -> None: self.model[M.RETURN] = ExpectedReturns(assets=self.assets) - self.parameter[P.SIGMA_MAX] = cp.Parameter( - nonneg=True, name="maximal volatility" - ) + self.parameter[P.SIGMA_MAX] = cp.Parameter(nonneg=True, name="maximal volatility") self.constraints[C.LONG_ONLY] = self.weights >= 0 self.constraints[C.BUDGET] = cp.sum(self.weights) == 1.0 - self.constraints[C.RISK] = ( - self.risk.estimate(self.variables) <= self.parameter[P.SIGMA_MAX] - ) + self.constraints[C.RISK] = self.risk.estimate(self.variables) <= self.parameter[P.SIGMA_MAX] diff --git a/cvx/markowitz/portfolios/min_var.py b/cvx/markowitz/portfolios/min_var.py index 94fa284e..d0ece499 100644 --- a/cvx/markowitz/portfolios/min_var.py +++ b/cvx/markowitz/portfolios/min_var.py @@ -23,7 +23,6 @@ @dataclass(frozen=True) class MinVar(Builder): - """ Minimize the standard deviation of the portfolio returns subject to a set of constraints min StdDev(r_p) diff --git a/cvx/markowitz/portfolios/soft_risk.py b/cvx/markowitz/portfolios/soft_risk.py index 6dd5fcd8..4845b021 100644 --- a/cvx/markowitz/portfolios/soft_risk.py +++ b/cvx/markowitz/portfolios/soft_risk.py @@ -26,7 +26,6 @@ @dataclass(frozen=True) class SoftRisk(Builder): - """ maximize w^T mu - omega * (sigma - sigma_target)_+ subject to w >= 0, w^T 1 = 1, sigma <= sigma_max @@ -40,9 +39,7 @@ class SoftRisk(Builder): @property def objective(self) -> cp.Maximize: expected_return = self.model[M.RETURN].estimate(self.variables) - soft_risk = cp.pos( - self.parameter[P.OMEGA] * self._sigma - self._sigma_target_times_omega - ) + soft_risk = cp.pos(self.parameter[P.OMEGA] * self._sigma - self._sigma_target_times_omega) return cp.Maximize(expected_return - soft_risk) def __post_init__(self) -> None: @@ -52,9 +49,7 @@ def __post_init__(self) -> None: self.parameter[P.SIGMA_MAX] = cp.Parameter(nonneg=True, name="limit volatility") - self.parameter[P.SIGMA_TARGET] = cp.Parameter( - nonneg=True, name="target volatility" - ) + self.parameter[P.SIGMA_TARGET] = cp.Parameter(nonneg=True, name="target volatility") self.parameter[P.OMEGA] = cp.Parameter(nonneg=True, name="risk priority") self._sigma_target_times_omega._callback = lambda: ( diff --git a/cvx/markowitz/risk/cvar/cvar.py b/cvx/markowitz/risk/cvar/cvar.py index cb8d95ed..23ddbdd7 100644 --- a/cvx/markowitz/risk/cvar/cvar.py +++ b/cvx/markowitz/risk/cvar/cvar.py @@ -50,6 +50,4 @@ def estimate(self, variables: Variables) -> cp.Expression: return -cp.sum_smallest(self.data[D.RETURNS] @ variables[D.WEIGHTS], k=k) / k def update(self, **kwargs: Matrix) -> None: - self.data[D.RETURNS].value = fill_matrix( - rows=self.rows, cols=self.assets, x=kwargs[D.RETURNS] - ) + self.data[D.RETURNS].value = fill_matrix(rows=self.rows, cols=self.assets, x=kwargs[D.RETURNS]) diff --git a/cvx/markowitz/risk/factor/factor.py b/cvx/markowitz/risk/factor/factor.py index 230fb307..5c972154 100644 --- a/cvx/markowitz/risk/factor/factor.py +++ b/cvx/markowitz/risk/factor/factor.py @@ -11,8 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Factor risk model -""" +"""Factor risk model""" + from __future__ import annotations from dataclasses import dataclass @@ -104,13 +104,8 @@ def update(self, **kwargs: Matrix) -> None: if key not in kwargs.keys(): raise CvxError(f"Missing keyword {key}") - if ( - not kwargs[D.IDIOSYNCRATIC_VOLA].shape[0] - == kwargs[D.IDIOSYNCRATIC_VOLA_UNCERTAINTY].shape[0] - ): - raise CvxError( - "Mismatch in length for idiosyncratic_vola and idiosyncratic_vola_uncertainty" - ) + if not kwargs[D.IDIOSYNCRATIC_VOLA].shape[0] == kwargs[D.IDIOSYNCRATIC_VOLA_UNCERTAINTY].shape[0]: + raise CvxError("Mismatch in length for idiosyncratic_vola and idiosyncratic_vola_uncertainty") exposure = kwargs[D.EXPOSURE] k, assets = exposure.shape @@ -119,22 +114,14 @@ def update(self, **kwargs: Matrix) -> None: raise CvxError("Mismatch in length for idiosyncratic_vola and exposure") if not kwargs[D.SYSTEMATIC_VOLA_UNCERTAINTY].shape[0] == k: - raise CvxError( - "Mismatch in length of systematic_vola_uncertainty and exposure" - ) + raise CvxError("Mismatch in length of systematic_vola_uncertainty and exposure") if not kwargs[D.CHOLESKY].shape[0] == k: raise CvxError("Mismatch in size of chol and exposure") - self.data[D.EXPOSURE].value = fill_matrix( - rows=self.factors, cols=self.assets, x=kwargs["exposure"] - ) - self.data[D.IDIOSYNCRATIC_VOLA].value = fill_vector( - num=self.assets, x=kwargs[D.IDIOSYNCRATIC_VOLA] - ) - self.data[D.CHOLESKY].value = fill_matrix( - rows=self.factors, cols=self.factors, x=kwargs[D.CHOLESKY] - ) + self.data[D.EXPOSURE].value = fill_matrix(rows=self.factors, cols=self.assets, x=kwargs["exposure"]) + self.data[D.IDIOSYNCRATIC_VOLA].value = fill_vector(num=self.assets, x=kwargs[D.IDIOSYNCRATIC_VOLA]) + self.data[D.CHOLESKY].value = fill_matrix(rows=self.factors, cols=self.factors, x=kwargs[D.CHOLESKY]) # Robust risk self.data[D.SYSTEMATIC_VOLA_UNCERTAINTY].value = fill_vector( @@ -146,8 +133,6 @@ def update(self, **kwargs: Matrix) -> None: def constraints(self, variables: Variables) -> Expressions: return { - "factors": variables[D.FACTOR_WEIGHTS] - == self.data[D.EXPOSURE] @ variables[D.WEIGHTS], - "_abs": variables[D._ABS] - >= cp.abs(variables[D.FACTOR_WEIGHTS]), # Robust risk dummy variable + "factors": variables[D.FACTOR_WEIGHTS] == self.data[D.EXPOSURE] @ variables[D.WEIGHTS], + "_abs": variables[D._ABS] >= cp.abs(variables[D.FACTOR_WEIGHTS]), # Robust risk dummy variable } diff --git a/cvx/markowitz/risk/sample/sample.py b/cvx/markowitz/risk/sample/sample.py index ea848695..8147b527 100644 --- a/cvx/markowitz/risk/sample/sample.py +++ b/cvx/markowitz/risk/sample/sample.py @@ -11,8 +11,8 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Risk models based on the sample covariance matrix -""" +"""Risk models based on the sample covariance matrix""" + from __future__ import annotations from dataclasses import dataclass @@ -63,15 +63,10 @@ def update(self, **kwargs: Matrix) -> None: if not kwargs[D.CHOLESKY].shape[0] == kwargs[D.VOLA_UNCERTAINTY].shape[0]: raise CvxError("Mismatch in length for chol and vola_uncertainty") - self.data[D.CHOLESKY].value = fill_matrix( - rows=self.assets, cols=self.assets, x=kwargs[D.CHOLESKY] - ) - self.data[D.VOLA_UNCERTAINTY].value = fill_vector( - num=self.assets, x=kwargs[D.VOLA_UNCERTAINTY] - ) + self.data[D.CHOLESKY].value = fill_matrix(rows=self.assets, cols=self.assets, x=kwargs[D.CHOLESKY]) + self.data[D.VOLA_UNCERTAINTY].value = fill_vector(num=self.assets, x=kwargs[D.VOLA_UNCERTAINTY]) def constraints(self, variables: Variables) -> Expressions: return { - "dummy": variables[D._ABS] - >= cp.abs(variables[D.WEIGHTS]), # Robust risk dummy variable + "dummy": variables[D._ABS] >= cp.abs(variables[D.WEIGHTS]), # Robust risk dummy variable } diff --git a/experiments/minRisk2.py b/experiments/minRisk2.py index ef82978c..82212fbc 100644 --- a/experiments/minRisk2.py +++ b/experiments/minRisk2.py @@ -25,14 +25,10 @@ builder = MinVar(assets=20) - builder.parameter["max_concentration"] = cp.Parameter( - 1, name="max_concentration", nonneg=True - ) + builder.parameter["max_concentration"] = cp.Parameter(1, name="max_concentration", nonneg=True) # You can add constraints before you build the problem - builder.constraints[C.CONCENTRATION] = ( - cp.sum_largest(builder.weights, 2) <= builder.parameter["max_concentration"] - ) + builder.constraints[C.CONCENTRATION] = cp.sum_largest(builder.weights, 2) <= builder.parameter["max_concentration"] # here we add a constraints # w[19] + w[17] <= 0.0001 @@ -44,9 +40,7 @@ row[19] = 1 row[17] = 1 - for name, constraint in approx( - "xxx", row @ builder.weights, 0.0, builder.parameter["random"] - ): + for name, constraint in approx("xxx", row @ builder.weights, 0.0, builder.parameter["random"]): # print(constraint) builder.constraints[name] = constraint diff --git a/experiments/playground.ipynb b/experiments/playground.ipynb index c7b5e0e7..0ebe4001 100644 --- a/experiments/playground.ipynb +++ b/experiments/playground.ipynb @@ -6,8 +6,8 @@ "metadata": {}, "outputs": [], "source": [ - "dict1 = {\"a\":1, \"b\":2}\n", - "dict2 = {\"a\":10, \"b\":20, \"c\":3, \"d\":4}" + "dict1 = {\"a\": 1, \"b\": 2}\n", + "dict2 = {\"a\": 10, \"b\": 20, \"c\": 3, \"d\": 4}" ] }, { @@ -48,16 +48,18 @@ "evalue": "Invalid dimensions (2,) for Parameter value.", "output_type": "error", "traceback": [ - "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", - "\u001B[0;31mValueError\u001B[0m Traceback (most recent call last)", - "Cell \u001B[0;32mIn[25], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[43ma\u001B[49m\u001B[38;5;241;43m.\u001B[39;49m\u001B[43mvalue\u001B[49m \u001B[38;5;241m=\u001B[39m np\u001B[38;5;241m.\u001B[39mzeros(\u001B[38;5;241m2\u001B[39m)\n", - "File \u001B[0;32m~/Documents/Stanford/Research/My papers/markowitz/code/cvxmarkowitz/.venv/lib/python3.9/site-packages/cvxpy/expressions/constants/parameter.py:87\u001B[0m, in \u001B[0;36mParameter.value\u001B[0;34m(self, val)\u001B[0m\n\u001B[1;32m 85\u001B[0m \u001B[39m@value\u001B[39m\u001B[39m.\u001B[39msetter\n\u001B[1;32m 86\u001B[0m \u001B[39mdef\u001B[39;00m \u001B[39mvalue\u001B[39m(\u001B[39mself\u001B[39m, val):\n\u001B[0;32m---> 87\u001B[0m \u001B[39mself\u001B[39m\u001B[39m.\u001B[39m_value \u001B[39m=\u001B[39m \u001B[39mself\u001B[39;49m\u001B[39m.\u001B[39;49m_validate_value(val)\n", - "File \u001B[0;32m~/Documents/Stanford/Research/My papers/markowitz/code/cvxmarkowitz/.venv/lib/python3.9/site-packages/cvxpy/expressions/leaf.py:394\u001B[0m, in \u001B[0;36mLeaf._validate_value\u001B[0;34m(self, val)\u001B[0m\n\u001B[1;32m 392\u001B[0m val \u001B[39m=\u001B[39m intf\u001B[39m.\u001B[39mconvert(val)\n\u001B[1;32m 393\u001B[0m \u001B[39mif\u001B[39;00m intf\u001B[39m.\u001B[39mshape(val) \u001B[39m!=\u001B[39m \u001B[39mself\u001B[39m\u001B[39m.\u001B[39mshape:\n\u001B[0;32m--> 394\u001B[0m \u001B[39mraise\u001B[39;00m \u001B[39mValueError\u001B[39;00m(\n\u001B[1;32m 395\u001B[0m \u001B[39m\"\u001B[39m\u001B[39mInvalid dimensions \u001B[39m\u001B[39m%s\u001B[39;00m\u001B[39m for \u001B[39m\u001B[39m%s\u001B[39;00m\u001B[39m value.\u001B[39m\u001B[39m\"\u001B[39m \u001B[39m%\u001B[39m\n\u001B[1;32m 396\u001B[0m (intf\u001B[39m.\u001B[39mshape(val), \u001B[39mself\u001B[39m\u001B[39m.\u001B[39m\u001B[39m__class__\u001B[39m\u001B[39m.\u001B[39m\u001B[39m__name__\u001B[39m)\n\u001B[1;32m 397\u001B[0m )\n\u001B[1;32m 398\u001B[0m projection \u001B[39m=\u001B[39m \u001B[39mself\u001B[39m\u001B[39m.\u001B[39mproject(val)\n\u001B[1;32m 399\u001B[0m \u001B[39m# ^ might be a numpy array, or sparse scipy matrix.\u001B[39;00m\n", - "\u001B[0;31mValueError\u001B[0m: Invalid dimensions (2,) for Parameter value." + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mValueError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[25], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43ma\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mvalue\u001b[49m \u001b[38;5;241m=\u001b[39m np\u001b[38;5;241m.\u001b[39mzeros(\u001b[38;5;241m2\u001b[39m)\n", + "File \u001b[0;32m~/Documents/Stanford/Research/My papers/markowitz/code/cvxmarkowitz/.venv/lib/python3.9/site-packages/cvxpy/expressions/constants/parameter.py:87\u001b[0m, in \u001b[0;36mParameter.value\u001b[0;34m(self, val)\u001b[0m\n\u001b[1;32m 85\u001b[0m \u001b[39m@value\u001b[39m\u001b[39m.\u001b[39msetter\n\u001b[1;32m 86\u001b[0m \u001b[39mdef\u001b[39;00m \u001b[39mvalue\u001b[39m(\u001b[39mself\u001b[39m, val):\n\u001b[0;32m---> 87\u001b[0m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m_value \u001b[39m=\u001b[39m \u001b[39mself\u001b[39;49m\u001b[39m.\u001b[39;49m_validate_value(val)\n", + "File \u001b[0;32m~/Documents/Stanford/Research/My papers/markowitz/code/cvxmarkowitz/.venv/lib/python3.9/site-packages/cvxpy/expressions/leaf.py:394\u001b[0m, in \u001b[0;36mLeaf._validate_value\u001b[0;34m(self, val)\u001b[0m\n\u001b[1;32m 392\u001b[0m val \u001b[39m=\u001b[39m intf\u001b[39m.\u001b[39mconvert(val)\n\u001b[1;32m 393\u001b[0m \u001b[39mif\u001b[39;00m intf\u001b[39m.\u001b[39mshape(val) \u001b[39m!=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mshape:\n\u001b[0;32m--> 394\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mValueError\u001b[39;00m(\n\u001b[1;32m 395\u001b[0m \u001b[39m\"\u001b[39m\u001b[39mInvalid dimensions \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m for \u001b[39m\u001b[39m%s\u001b[39;00m\u001b[39m value.\u001b[39m\u001b[39m\"\u001b[39m \u001b[39m%\u001b[39m\n\u001b[1;32m 396\u001b[0m (intf\u001b[39m.\u001b[39mshape(val), \u001b[39mself\u001b[39m\u001b[39m.\u001b[39m\u001b[39m__class__\u001b[39m\u001b[39m.\u001b[39m\u001b[39m__name__\u001b[39m)\n\u001b[1;32m 397\u001b[0m )\n\u001b[1;32m 398\u001b[0m projection \u001b[39m=\u001b[39m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mproject(val)\n\u001b[1;32m 399\u001b[0m \u001b[39m# ^ might be a numpy array, or sparse scipy matrix.\u001b[39;00m\n", + "\u001b[0;31mValueError\u001b[0m: Invalid dimensions (2,) for Parameter value." ] } ], - "source": "a.value = np.zeros(1)" + "source": [ + "a.value = np.zeros(1)" + ] }, { "cell_type": "code", @@ -96,7 +98,13 @@ } ], "source": [ - "0.7**2 + 0.75**2 + 0.1**2*(0.5**2 + 0.1**2 + 0.2**2) + (0.2*0.7+0.1*0.75)**2 + 0.3**2*(0.5**2 + 0.1**2 + 0.2**2)" + "(\n", + " 0.7**2\n", + " + 0.75**2\n", + " + 0.1**2 * (0.5**2 + 0.1**2 + 0.2**2)\n", + " + (0.2 * 0.7 + 0.1 * 0.75) ** 2\n", + " + 0.3**2 * (0.5**2 + 0.1**2 + 0.2**2)\n", + ")" ] }, { @@ -116,7 +124,7 @@ } ], "source": [ - "0.7**2 + 0.75**2 + (0.2*0.7+0.1*0.75)**2" + "0.7**2 + 0.75**2 + (0.2 * 0.7 + 0.1 * 0.75) ** 2" ] }, { @@ -136,7 +144,7 @@ } ], "source": [ - "1.098725+0.030000000000000006" + "1.098725 + 0.030000000000000006" ] }, { @@ -156,7 +164,7 @@ } ], "source": [ - "0.1**2*(0.5**2 + 0.1**2 + 0.2**2) + 0.3**2*(0.5**2 + 0.1**2 + 0.2**2)" + "0.1**2 * (0.5**2 + 0.1**2 + 0.2**2) + 0.3**2 * (0.5**2 + 0.1**2 + 0.2**2)" ] }, { @@ -218,16 +226,16 @@ } ], "source": [ - "Sigma = np.array([[2,0.4], [0.4, 3]])\n", + "Sigma = np.array([[2, 0.4], [0.4, 3]])\n", "d = np.array([0.15, 0.3])\n", "\n", - "ones = np.ones(2).reshape(-1,1)\n", + "ones = np.ones(2).reshape(-1, 1)\n", "\n", - "A1 = np.hstack([2*(Sigma + np.outer(d,d)), ones])\n", - "A2 = np.hstack([ones.T, np.zeros((1,1))])\n", + "A1 = np.hstack([2 * (Sigma + np.outer(d, d)), ones])\n", + "A2 = np.hstack([ones.T, np.zeros((1, 1))])\n", "A = np.vstack([A1, A2])\n", "\n", - "b = np.vstack([np.zeros((2,1)), np.ones((1,1))])\n", + "b = np.vstack([np.zeros((2, 1)), np.ones((1, 1))])\n", "w_star = np.linalg.solve(A, b)\n", "w_star" ] @@ -310,7 +318,7 @@ "e = cp.Parameter(shape=n, value=np.zeros(n), nonneg=True)\n", "\n", "\n", - "argument = cp.hstack([L@w, cp.sum(w)])\n", + "argument = cp.hstack([L @ w, cp.sum(w)])\n", "obj = cp.norm2(argument)\n", "\n", "# obj = cp.norm2(L@w)\n", @@ -318,7 +326,6 @@ "for i in range(11):\n", " print(argument[i].is_convex())\n", " print(argument.is_incr(i))\n", - " \n", "\n", "\n", "# obj.is_convex()\n", @@ -348,7 +355,7 @@ "\n", "w = cp.Variable()\n", "\n", - "prob = cp.Problem(cp.Minimize(cp.square(a*w)))\n", + "prob = cp.Problem(cp.Minimize(cp.square(a * w)))\n", "\n", "prob.is_dpp()" ] diff --git a/experiments/softRisk.py b/experiments/softRisk.py index 90b10678..b7f6dc06 100644 --- a/experiments/softRisk.py +++ b/experiments/softRisk.py @@ -15,11 +15,7 @@ def run(path: str = "data/stock_prices.csv") -> None: - returns = ( - pd.read_csv(path, index_col=0, header=0, parse_dates=True) - .pct_change() - .dropna(axis=0, how="all") - ) + returns = pd.read_csv(path, index_col=0, header=0, parse_dates=True).pct_change().dropna(axis=0, how="all") n_components = 10 assets = returns.shape[1] @@ -27,9 +23,7 @@ def run(path: str = "data/stock_prices.csv") -> None: logger.info(f"Returns: \n{returns}") pca = PCA(returns=returns.values, n_components=n_components) lower_bound_factors = pd.Series(data=-1.0, index=range(n_components)) - upper_bound_factors = pd.Series( - data=+1.0, index=range(n_components) - ) # len(pca.factors))) + upper_bound_factors = pd.Series(data=+1.0, index=range(n_components)) # len(pca.factors))) lower_bound_assets = pd.Series(data=0.0, index=returns.columns) upper_bound_assets = pd.Series(data=1.0, index=returns.columns) diff --git a/tests/conftest.py b/tests/conftest.py index 434d4479..9ad99a75 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ """global fixtures""" + from __future__ import annotations from pathlib import Path @@ -13,8 +14,6 @@ def resource_fixture(): return Path(__file__).parent / "resources" -@pytest.fixture( - params=[s for s in [cp.ECOS, cp.CLARABEL] if s in cp.installed_solvers()] -) +@pytest.fixture(params=[s for s in [cp.ECOS, cp.CLARABEL] if s in cp.installed_solvers()]) def solver(request): return request.param diff --git a/tests/test_linalg/test_pca.py b/tests/test_linalg/test_pca.py index 03526278..80bbc866 100644 --- a/tests/test_linalg/test_pca.py +++ b/tests/test_linalg/test_pca.py @@ -9,9 +9,7 @@ @pytest.fixture() def returns(resource_dir): - prices = pd.read_csv( - resource_dir / "stock_prices.csv", index_col=0, header=0, parse_dates=True - ) + prices = pd.read_csv(resource_dir / "stock_prices.csv", index_col=0, header=0, parse_dates=True) return prices.pct_change().fillna(0.0).values diff --git a/tests/test_markowitz/test_bounds.py b/tests/test_markowitz/test_bounds.py index 0229ae11..24c7223a 100644 --- a/tests/test_markowitz/test_bounds.py +++ b/tests/test_markowitz/test_bounds.py @@ -27,12 +27,8 @@ def test_constraints(): } ) - assert bounds.data[D.LOWER_BOUND_ASSETS].value == pytest.approx( - np.array([0.1, 0.2, 0]) - ) - assert bounds.data[D.UPPER_BOUND_ASSETS].value == pytest.approx( - np.array([0.3, 0.4, 0.5]) - ) + assert bounds.data[D.LOWER_BOUND_ASSETS].value == pytest.approx(np.array([0.1, 0.2, 0])) + assert bounds.data[D.UPPER_BOUND_ASSETS].value == pytest.approx(np.array([0.3, 0.4, 0.5])) assert len(bounds.constraints(variables)) == 2 diff --git a/tests/test_markowitz/test_models/test_expected_returns.py b/tests/test_markowitz/test_models/test_expected_returns.py index 25feb457..483f0b98 100644 --- a/tests/test_markowitz/test_models/test_expected_returns.py +++ b/tests/test_markowitz/test_models/test_expected_returns.py @@ -16,9 +16,7 @@ def test_expected_returns(): # expected returns not explicitly set are zero assert model.data[D.MU].value == pytest.approx(np.array([0.1, 0.2, 0.0])) - assert model.parameter["mu_uncertainty"].value == pytest.approx( - np.array([0.0, 0.0, 0.0]) - ) + assert model.parameter["mu_uncertainty"].value == pytest.approx(np.array([0.0, 0.0, 0.0])) weights = cp.Variable(assets) weights.value = np.array([1.0, 1.0, 2.0]) @@ -38,9 +36,7 @@ def test_expected_returns_robust(): # expected returns not explicitly set are zero assert model.data["mu"].value == pytest.approx(np.array([0.1, 0.2, 0.0])) - assert model.parameter["mu_uncertainty"].value == pytest.approx( - np.array([0.01, 0.03, 0.0]) - ) + assert model.parameter["mu_uncertainty"].value == pytest.approx(np.array([0.01, 0.03, 0.0])) weights = cp.Variable(assets) weights.value = np.array([1.0, 1.0, 2.0]) diff --git a/tests/test_markowitz/test_portfolios/test_min_var.py b/tests/test_markowitz/test_portfolios/test_min_var.py index 4663825e..8dc46de5 100644 --- a/tests/test_markowitz/test_portfolios/test_min_var.py +++ b/tests/test_markowitz/test_portfolios/test_min_var.py @@ -74,9 +74,7 @@ def test_min_var(solver): np.testing.assert_almost_equal(problem.value, 0.9354143466222262) - np.testing.assert_almost_equal( - problem.weights, np.array([0.75, 0.25, 0.0, 0.0]), decimal=3 - ) + np.testing.assert_almost_equal(problem.weights, np.array([0.75, 0.25, 0.0, 0.0]), decimal=3) assert objective == pytest.approx(0.9354143, abs=1e-5) diff --git a/tests/test_markowitz/test_risk/test_factor/test_factor.py b/tests/test_markowitz/test_risk/test_factor/test_factor.py index 28796443..876a7987 100644 --- a/tests/test_markowitz/test_risk/test_factor/test_factor.py +++ b/tests/test_markowitz/test_risk/test_factor/test_factor.py @@ -16,9 +16,7 @@ @pytest.fixture() def returns(resource_dir): - prices = pd.read_csv( - resource_dir / "stock_prices.csv", index_col=0, header=0, parse_dates=True - ) + prices = pd.read_csv(resource_dir / "stock_prices.csv", index_col=0, header=0, parse_dates=True) return prices.pct_change().fillna(0.0).values @@ -107,9 +105,7 @@ def test_estimate_risk(solver): data = dict(problem.data) # test that the exposure is correct, e.g. the factor weights match the exposure * asset weights - assert data[(M.RISK, "exposure")].value @ problem.weights == pytest.approx( - problem.factor_weights, abs=1e-6 - ) + assert data[(M.RISK, "exposure")].value @ problem.weights == pytest.approx(problem.factor_weights, abs=1e-6) # test all entries of y are smaller than 0.1 assert np.all([problem.factor_weights <= 0.1 + 1e-4]) @@ -141,17 +137,13 @@ def test_factor_mini(): # Note: dummy is abs(factor_weights) variables[D._ABS] = cp.abs(variables[D.FACTOR_WEIGHTS]) - assert variables[D.FACTOR_WEIGHTS].value == pytest.approx( - np.array([0.7, 0.75]), abs=1e-6 - ) + assert variables[D.FACTOR_WEIGHTS].value == pytest.approx(np.array([0.7, 0.75]), abs=1e-6) residual = np.sqrt(0.03) systematic = np.sqrt(1.098725) assert model._residual_risk(variables=variables).value == pytest.approx(residual) - assert model._systematic_risk(variables=variables).value == pytest.approx( - systematic - ) + assert model._systematic_risk(variables=variables).value == pytest.approx(systematic) total = np.linalg.norm(np.array([residual, systematic])) diff --git a/tests/test_markowitz/test_risk/test_sample/test_sample.py b/tests/test_markowitz/test_risk/test_sample/test_sample.py index 551bc809..17622b19 100644 --- a/tests/test_markowitz/test_risk/test_sample/test_sample.py +++ b/tests/test_markowitz/test_risk/test_sample/test_sample.py @@ -20,9 +20,7 @@ def test_sample(): } ) - vola = riskmodel.estimate( - {D.WEIGHTS: np.array([1.0, 1.0]), D._ABS: np.array([1.0, 1.0])} - ).value + vola = riskmodel.estimate({D.WEIGHTS: np.array([1.0, 1.0]), D._ABS: np.array([1.0, 1.0])}).value np.testing.assert_almost_equal(vola, 2.0) @@ -58,9 +56,7 @@ def test_robust_sample(): ) # Note: dummy should be abs(weights) - vola = riskmodel.estimate( - {D.WEIGHTS: np.array([1.0, -1.0]), D._ABS: np.array([1.0, 1.0])} - ).value + vola = riskmodel.estimate({D.WEIGHTS: np.array([1.0, -1.0]), D._ABS: np.array([1.0, 1.0])}).value np.testing.assert_almost_equal(vola, np.sqrt(2.09)) diff --git a/tests/test_markowitz/test_utils/test_aux.py b/tests/test_markowitz/test_utils/test_aux.py index 9ca4b9c4..aa9d6e3a 100644 --- a/tests/test_markowitz/test_utils/test_aux.py +++ b/tests/test_markowitz/test_utils/test_aux.py @@ -10,6 +10,4 @@ def test_fill_vector(): def test_fill_matrix(): a = np.ones((2, 2)) - np.allclose( - fill_matrix(rows=3, cols=3, x=a), np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]]) - ) + np.allclose(fill_matrix(rows=3, cols=3, x=a), np.array([[1, 1, 0], [1, 1, 0], [0, 0, 0]]))