Skip to content

Commit

Permalink
Yearly stats, sync with paper
Browse files Browse the repository at this point in the history
  • Loading branch information
phschiele committed Dec 11, 2023
1 parent f19a180 commit 6aa42c0
Show file tree
Hide file tree
Showing 3 changed files with 318 additions and 68 deletions.
9 changes: 8 additions & 1 deletion experiments/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,21 @@ def n_assets(self) -> int:

def run_backtest(
strategy: Callable, risk_target: float, verbose: bool = False
) -> tuple[pd.Series, pd.DataFrame]:
) -> BacktestResult:
"""
Run a simplified backtest for a given strategy.
At time t we use data from t-lookback to t to compute the optimal portfolio
weights and then execute the trades at time t.
"""

prices, spread, rf = load_data()
training_length = 1250
prices, spread, rf = (
prices.iloc[training_length:],
spread.iloc[training_length:],
rf.iloc[training_length:],
)

n_assets = prices.shape[1]

lookback = 500
Expand Down
76 changes: 35 additions & 41 deletions experiments/markowitz.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
@dataclass
class Data:
w_prev: np.ndarray # (n_assets,) array of previous asset weights
c_prev: float # previous cash weight
idio_mean: np.ndarray # (n_assets,) array of idiosyncratic mean returns
factor_mean: np.ndarray # (n_factors,) array of factor mean returns
risk_free: float # risk-free rate
Expand All @@ -36,17 +35,23 @@ class Data:
def n_assets(self) -> int:
return self.w_prev.size

@property
def volas(self) -> np.ndarray:
return self.idio_volas + np.linalg.norm(
self.F @ self.factor_covariance_chol, axis=1
)


@dataclass
class Parameters:
w_lower: np.ndarray # (n_assets,) array of lower bounds on asset weights
w_upper: np.ndarray # (n_assets,) array of upper bounds on asset weights
c_lower: float # lower bound on cash weight
c_upper: float # upper bound on cash weight
z_lower: np.ndarray # (n_assets,) array of lower bounds on trades
z_upper: np.ndarray # (n_assets,) array of upper bounds on trades
T_max: float # turnover target
L_max: float # leverage limit
w_min: np.ndarray # (n_assets,) array of lower bounds on asset weights
w_max: np.ndarray # (n_assets,) array of upper bounds on asset weights
c_min: float # lower bound on cash weight
c_max: float # upper bound on cash weight
z_min: np.ndarray # (n_assets,) array of lower bounds on trades
z_max: np.ndarray # (n_assets,) array of upper bounds on trades
T_tar: float # turnover target
L_tar: float # leverage target
rho_mean: np.ndarray # (n_assets,) array of mean returns for rho
rho_covariance: float # uncertainty in covariance matrix
gamma_hold: float # holding cost
Expand Down Expand Up @@ -75,17 +80,11 @@ def markowitz(data: Data, param: Parameters) -> tuple[np.ndarray, float, cp.Prob
return_uncertainty = param.rho_mean @ cp.abs(w)
return_wc = mean_return - return_uncertainty

# asset volatilities
factor_volas = cp.norm2(data.F @ data.factor_covariance_chol, axis=1)
volas = factor_volas + data.idio_volas

# portfolio risk
# worst-case (robust) risk
factor_risk = cp.norm2((data.F @ data.factor_covariance_chol).T @ w)
idio_risk = cp.norm2(cp.multiply(data.idio_volas, w))
risk = cp.norm2(cp.hstack([factor_risk, idio_risk]))

# worst-case (robust) risk
risk_uncertainty = param.rho_covariance**0.5 * volas @ cp.abs(w)
risk_uncertainty = param.rho_covariance**0.5 * data.volas @ cp.abs(w)
risk_wc = cp.norm2(cp.hstack([risk, risk_uncertainty]))

asset_holding_cost = data.kappa_short @ cp.pos(-w)
Expand All @@ -97,24 +96,20 @@ def markowitz(data: Data, param: Parameters) -> tuple[np.ndarray, float, cp.Prob
trading_cost = spread_cost + impact_cost

objective = (
return_wc
- param.gamma_risk * cp.pos(risk_wc - param.risk_target)
- param.gamma_hold * holding_cost
- param.gamma_trade * trading_cost
- param.gamma_turn * cp.pos(T - param.T_max)
return_wc - param.gamma_hold * holding_cost - param.gamma_trade * trading_cost
)

constraints = [
cp.sum(w) + c == 1,
c == data.c_prev - cp.sum(z),
param.c_lower <= c,
c <= param.c_upper,
param.w_lower <= w,
w <= param.w_upper,
param.z_lower <= z,
z <= param.z_upper,
L <= param.L_max,
T <= param.T_max,
param.w_min <= w,
w <= param.w_max,
L <= param.L_tar,
param.c_min <= c,
c <= param.c_max,
param.z_min <= z,
z <= param.z_max,
T <= param.T_tar,
risk_wc <= param.risk_target,
]

problem = cp.Problem(cp.Maximize(objective), constraints)
Expand All @@ -130,7 +125,6 @@ def markowitz(data: Data, param: Parameters) -> tuple[np.ndarray, float, cp.Prob
n_assets = 10
data = Data(
w_prev=np.ones(n_assets) / n_assets,
c_prev=0.0,
idio_mean=np.zeros(n_assets),
factor_mean=np.zeros(n_assets),
risk_free=0.0,
Expand All @@ -144,14 +138,14 @@ def markowitz(data: Data, param: Parameters) -> tuple[np.ndarray, float, cp.Prob
)

param = Parameters(
w_lower=np.zeros(n_assets),
w_upper=np.ones(n_assets),
c_lower=0.0,
c_upper=1.0,
z_lower=-np.ones(n_assets),
z_upper=np.ones(n_assets),
T_max=1.0,
L_max=1.0,
w_min=np.zeros(n_assets),
w_max=np.ones(n_assets),
c_min=0.0,
c_max=1.0,
z_min=-np.ones(n_assets),
z_max=np.ones(n_assets),
T_tar=1.0,
L_tar=1.0,
rho_mean=np.zeros(n_assets),
rho_covariance=0.0,
gamma_hold=0.0,
Expand All @@ -161,5 +155,5 @@ def markowitz(data: Data, param: Parameters) -> tuple[np.ndarray, float, cp.Prob
risk_target=0.0,
)

w, c = markowitz(data, param)
w, c, _ = markowitz(data, param)
print(w, c)
Loading

0 comments on commit 6aa42c0

Please sign in to comment.