diff --git a/experiments/backtest.py b/experiments/backtest.py index 3a3017e..1ba5858 100644 --- a/experiments/backtest.py +++ b/experiments/backtest.py @@ -6,6 +6,7 @@ from typing import Callable import numpy as np import pandas as pd +from utils import synthetic_returns # hack to allow importing from parent directory without having a package sys.path.append(str(Path(__file__).parent.parent)) @@ -61,7 +62,7 @@ def run_backtest( post_trade_cash = [] post_trade_quantities = [] - returns = prices.pct_change().dropna() + returns = synthetic_returns(prices).dropna() means = returns.ewm(halflife=125).mean() covariance_df = returns.ewm(halflife=125).cov() days = returns.index diff --git a/experiments/taming.py b/experiments/taming.py index 44e09e1..7cf5cdf 100644 --- a/experiments/taming.py +++ b/experiments/taming.py @@ -7,21 +7,28 @@ import matplotlib.pyplot as plt -def unconstrained_markowitz(inputs: OptimizationInput) -> np.ndarray: +def unconstrained_markowitz( + inputs: OptimizationInput, long_only: bool = False +) -> np.ndarray: """Compute the unconstrained Markowitz portfolio weights.""" n_assets = inputs.prices.shape[1] mu, Sigma = inputs.mean.values, inputs.covariance.values - w = cp.Variable(n_assets) - c = cp.Variable() + if long_only: + w = cp.Variable(n_assets, nonneg=True) + c = cp.Variable(nonneg=True) + else: + w = cp.Variable(n_assets) + c = cp.Variable() objective = mu @ w chol = np.linalg.cholesky(Sigma) constraints = [ cp.sum(w) + c == 1, - cp.norm2(chol @ w) <= inputs.risk_target, + cp.norm2(chol.T @ w) <= inputs.risk_target, ] + problem = cp.Problem(cp.Maximize(objective), constraints) problem.solve(get_solver()) assert problem.status in {cp.OPTIMAL, cp.OPTIMAL_INACCURATE} @@ -30,21 +37,7 @@ def unconstrained_markowitz(inputs: OptimizationInput) -> np.ndarray: def long_only_markowitz(inputs: OptimizationInput) -> np.ndarray: """Compute the long-only Markowitz portfolio weights.""" - n_assets = inputs.prices.shape[1] - - mu, Sigma = inputs.mean.values, inputs.covariance.values - - w = cp.Variable(n_assets, nonneg=True) - c = cp.Variable(nonneg=True) - objective = mu @ w - constraints = [ - cp.sum(w) + c == 1, - cp.quad_form(w, Sigma, assume_PSD=True) <= inputs.risk_target**2, - ] - problem = cp.Problem(cp.Maximize(objective), constraints) - problem.solve(get_solver()) - assert problem.status in {cp.OPTIMAL, cp.OPTIMAL_INACCURATE} - return w.value, c.value + return unconstrained_markowitz(inputs, long_only=True) def equal_weights(inputs: OptimizationInput) -> np.ndarray: diff --git a/experiments/utils.py b/experiments/utils.py new file mode 100644 index 0000000..a62ab64 --- /dev/null +++ b/experiments/utils.py @@ -0,0 +1,8 @@ +import numpy as np + + +def synthetic_returns(prices, sigma_r=0.02236, sigma_eps=0.14142): + returns = prices.pct_change() + + alpha = sigma_r**2 / (sigma_r**2 + sigma_eps**2) + return alpha * (returns + np.random.normal(size=returns.shape) * sigma_eps)