<- c("SP500", "DTWEXAFEGS") # "SP500" does not contain dividends; note: "DTWEXM" discontinued as of Jan 2020
factors_r <- c("DGS10", "BAMLH0A0HYM2") factors_d
Random weights
Need to generate uniformly distributed weights \(\mathbf{w}=(w_{1},w_{2},\ldots,w_{N})\) such that \(\sum_{j=1}^{N}w_{i}=1\) and \(w_{i}\geq0\):
Approach 1: tempting to use \(w_{i}=\frac{u_{i}}{\sum_{j=1}^{N}u_{i}}\) where \(u_{i}\sim U(0,1)\) but the distribution of \(\mathbf{w}\) is not uniform
Approach 2: instead, generate \(\text{Exp}(1)\) and then normalize
Can also scale random weights by \(M\), e.g. if sum of weights must be 10% then multiply weights by 10%.
<- function(n_sim, n_assets) {
rand_weights1
<- matrix(runif(n_sim * n_assets), nrow = n_sim, ncol = n_assets)
rand_exp <- rand_exp / rowSums(rand_exp)
result
return(result)
}
<- 3
n_assets <- 10000 n_sim
<- rand_weights1(n_sim, n_assets) approach1
Approach 2(a): uniform sample from the simplex (http://mathoverflow.net/a/76258) and then normalize
- If \(u\sim U(0,1)\) then \(-\ln(u)\) is an \(\text{Exp}(1)\) distribution
This is also known as generating a random vector from the symmetric Dirichlet distribution.
<- function(n_sim, n_assets, lmbda) {
rand_weights2a
# inverse transform sampling: https://en.wikipedia.org/wiki/Inverse_transform_sampling
<- matrix(-log(1 - runif(n_sim * n_assets)) / lmbda, nrow = n_sim, ncol = n_assets)
rand_exp <- rand_exp / rowSums(rand_exp)
result
return(result)
}
<- 1 lmbda
<- rand_weights2a(n_sim, n_assets, lmbda) approach2a
Approach 2(b): directly generate \(\text{Exp}(1)\) and then normalize
<- function(n_sim, n_assets) {
rand_weights2b
<- matrix(rexp(n_sim * n_assets), nrow = n_sim, ncol = n_assets)
rand_exp <- rand_exp / rowSums(rand_exp)
result
return(result)
}
<- rand_weights2b(n_sim, n_assets) approach2b
Random turnover
How to generate random weights between lower bound \(a\) and upper bound \(b\) that sum to zero?
Approach 1: tempting to multiply random weights by \(M\) and then subtract by \(\frac{M}{N}\) but the distribution is not between \(a\) and \(b\)
Approach 2: instead, use an iterative approach for random turnover:
- Generate \(N-1\) uniformly distributed weights between \(a\) and \(b\)
- For \(u_{N}\) compute sum of values and subtract from \(M\)
- If \(u_{N}\) is between \(a\) and \(b\), then keep; otherwise, discard
Then add random turnover to previous period’s random weights.
<- function(n_sim, n_assets, lower, upper, target) {
rand_turnover1
<- upper - lower
rng
<- rand_weights2b(n_sim, n_assets) * rng
result <- result - rng / n_assets
result
return(result)
}
<- -0.05
lower <- 0.05
upper <- 0 target
<- rand_turnover1(n_sim, n_assets, lower, upper, target) approach1
<- function(n_assets, lower, upper, target) {
rand_iterative
<- runif(n_assets - 1, min = lower, max = upper)
result <- target - sum(result)
temp
while (!((temp <= upper) && (temp >= lower))) {
<- runif(n_assets - 1, min = lower, max = upper)
result <- target - sum(result)
temp
}
<- append(result, temp)
result
return(result)
}
<- function(n_sim, n_assets, lower, upper, target) {
rand_turnover2
<- list()
result_ls
for (i in 1:n_sim) {
<- rand_iterative(n_assets, lower, upper, target)
result_sim <- append(result_ls, list(result_sim))
result_ls
}
<- do.call(rbind, result_ls)
result
return(result)
}
<- rand_turnover2(n_sim, n_assets, lower, upper, target) approach2
Mean-variance
library(CVXR)
<- function(x, scale) {
geometric_mean
<- prod(1 + x) ^ (scale / length(x)) - 1
result
return(result)
}
- https://www.adrian.idv.hk/2021-06-22-kkt/
- https://or.stackexchange.com/a/3738
- https://bookdown.org/compfinezbook/introFinRbook/Portfolio-Theory-with-Matrix-Algebra.html#algorithm-for-computing-efficient-frontier
- https://palomar.home.ece.ust.hk/MAFS6010R_lectures/slides_robust_portfolio.html
<- "BAICX" # fund inception date is "2011-11-28" tickers
<- na.omit(returns_xts)[ , factors] # extended history
returns_x_xts <- apply(returns_x_xts, 2, geometric_mean, scale = scale[["periods"]])
mu <- cov(overlap_x_xts) * scale[["periods"]] * scale[["overlap"]] sigma
Maximize means
\[ \begin{aligned} \begin{array}{rrcl} \displaystyle\min&-\mathbf{w}^{T}\mu\\ \textrm{s.t.}&\mathbf{w}^{T}e&=&1\\ &\mathbf{w}^T\Sigma\mathbf{w}&\leq&\sigma^{2}\\ \end{array} \end{aligned} \]
To incorporate these conditions into one equation, introduce new variables \(\lambda_{i}\) that are the Lagrange multipliers and define a new function \(\mathcal{L}\) as follows:
\[ \begin{aligned} \mathcal{L}(\mathbf{w},\lambda)&=-\mathbf{w}^{T}\mu-\lambda_{1}(\mathbf{w}^{T}e-1) \end{aligned} \]
Then, to minimize this function, take derivatives with respect to \(w\) and Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial w}&=-\mu-\lambda_{1}e=0\\ \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial \lambda_{1}}&=\mathbf{w}e^T-1=0 \end{aligned} \]
Simplify the equations above in matrix form and solve for the Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \begin{bmatrix} -\mu & e \\ e^{T} & 0 \end{bmatrix} \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} 0 \\ 1 \end{bmatrix} \\ \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} -\mu & e \\ e^{T} & 0 \end{bmatrix}^{-1} \begin{bmatrix} 0 \\ 1 \end{bmatrix} \end{aligned} \]
<- function(mu, sigma, target) {
max_mean_optim
<- Variable(length(mu))
params
<- Maximize(t(params) %*% mu)
obj
<- list(sum(params) == 1, params >= 0,
cons quad_form(params, sigma) <= target ^ 2)
<- Problem(obj, cons)
prob
<- solve(prob)$getValue(params)
result
return(result)
}
<- 0.06 target
<- t(max_mean_optim(mu, sigma, target))
params1 params1
[,1] [,2] [,3] [,4]
[1,] 0.4961023 0.5038977 7.919319e-09 4.929859e-09
%*% mu params1
[,1]
[1,] 0.0472976
sqrt(params1 %*% sigma %*% t(params1))
[,1]
[1,] 0.06
# # install.packages("devtools")
# devtools::install_github("jasonjfoster/rolloptim") # roll (>= 1.1.7)
# library(rolloptim)
#
# mu <- roll_mean(returns_x_xts, 5)
# sigma <- roll_cov(returns_x_xts, width = 5)
#
# xx <- roll_crossprod(returns_x_xts, returns_x_xts, 5)
# xy <- roll_crossprod(returns_x_xts, returns_y_xts, 5)
#
# roll_max_mean(mu)
Minimize variance
\[ \begin{aligned} \begin{array}{rrcl} \displaystyle\min&\frac{1}{2}\mathbf{w}^T\Sigma\mathbf{w}\\ \textrm{s.t.}&\mathbf{w}^{T}e&=&1\\ &\mu^{T}\mathbf{w}&\geq&M\\ \end{array} \end{aligned} \]
To incorporate these conditions into one equation, introduce new variables \(\lambda_{i}\) that are the Lagrange multipliers and define a new function \(\mathcal{L}\) as follows:
\[ \begin{aligned} \mathcal{L}(\mathbf{w},\lambda)&=\frac{1}{2}\mathbf{w}^{T}\Sigma\mathbf{w}-\lambda_{1}(\mathbf{w}^{T}e-1) \end{aligned} \]
Then, to minimize this function, take derivatives with respect to \(w\) and Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial w}&=\mathbf{w}\Sigma-\lambda_{1}e=0\\ \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial \lambda_{1}}&=\mathbf{w}e^T-1=0 \end{aligned} \]
Simplify the equations above in matrix form and solve for the Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \begin{bmatrix} \Sigma & e \\ e^{T} & 0 \end{bmatrix} \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} 0 \\ 1 \end{bmatrix} \\ \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} \Sigma & e \\ e^{T} & 0 \end{bmatrix}^{-1} \begin{bmatrix} 0 \\ 1 \end{bmatrix} \end{aligned} \]
<- function(mu, sigma, target) {
min_var_optim
<- Variable(length(mu))
params
<- Minimize(quad_form(params, sigma))
obj
<- list(sum(params) == 1, params >= 0,
cons sum(mu * params) >= target)
<- Problem(obj, cons)
prob
<- solve(prob)$getValue(params)
result
return(result)
}
<- 0.03 target
<- t(min_var_optim(mu, sigma, target))
params2 params2
[,1] [,2] [,3] [,4]
[1,] 0.29584 0.4463629 0.2577972 1.877369e-21
%*% mu params2
[,1]
[1,] 0.03
sqrt(params2 %*% sigma %*% t(params2))
[,1]
[1,] 0.0372178
# roll_min_var(sigma)
Maximize utility
\[ \begin{aligned} \begin{array}{rrcl} \displaystyle\min&\frac{1}{2}\delta(\mathbf{w}^{T}\Sigma\mathbf{w})-\mu^{T}\mathbf{w}\\ \textrm{s.t.}&\mathbf{w}^{T}e&=&1\\ \end{array} \end{aligned} \]
To incorporate these conditions into one equation, introduce new variables \(\lambda_{i}\) that are the Lagrange multipliers and define a new function \(\mathcal{L}\) as follows:
\[ \begin{aligned} \mathcal{L}(\mathbf{w},\lambda)&=\frac{1}{2}\mathbf{w}^{T}\Sigma\mathbf{w}-\mu^{T}\mathbf{w}-\lambda_{1}(\mathbf{w}^{T}e-1) \end{aligned} \]
Then, to minimize this function, take derivatives with respect to \(w\) and Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial w}&=\mathbf{w}\Sigma-\mu^{T}-\lambda_{1}e=0\\ \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial \lambda_{1}}&=\mathbf{w}e^T-1=0 \end{aligned} \]
Simplify the equations above in matrix form and solve for the Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \begin{bmatrix} \Sigma & e \\ e^{T} & 0 \end{bmatrix} \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} \mu^{T} \\ 1 \end{bmatrix} \\ \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} \Sigma & e \\ e^{T} & 0 \end{bmatrix}^{-1} \begin{bmatrix} \mu^{T} \\ 1 \end{bmatrix} \end{aligned} \]
<- function(mu, sigma, target) {
max_utility_optim
<- Variable(length(mu))
params
<- Minimize(0.5 * target * quad_form(params, sigma) - t(mu) %*% params)
obj
<- list(sum(params) == 1, params >= 0)
cons
<- Problem(obj, cons)
prob
<- solve(prob)$getValue(params)
result
return(result)
}
<- 0.5
ir <- ir / 0.06 # ir / std (see Black-Litterman) target
<- t(max_utility_optim(mu, sigma, target))
params3 params3
[,1] [,2] [,3] [,4]
[1,] 0.5584661 0.4415339 9.327449e-23 4.233534e-23
%*% mu params3
[,1]
[1,] 0.05158278
sqrt(params3 %*% sigma %*% t(params3))
[,1]
[1,] 0.0672845
# roll_max_utility(mu, sigma)
Minimize residual sum of squares
\[ \begin{aligned} \begin{array}{rrcl} \displaystyle\min&\frac{1}{2}\delta(\mathbf{w}^{T}X^{T}X\mathbf{w})-X^{T}y\mathbf{w}\\ \textrm{s.t.}&\mathbf{w}^{T}e&=&1\\ \end{array} \end{aligned} \]
To incorporate these conditions into one equation, introduce new variables \(\lambda_{i}\) that are the Lagrange multipliers and define a new function \(\mathcal{L}\) as follows:
\[ \begin{aligned} \mathcal{L}(\mathbf{w},\lambda)&=\frac{1}{2}\mathbf{w}^{T}X^{T}X\mathbf{w}-X^{T}y\mathbf{w}-\lambda_{1}(\mathbf{w}^{T}e-1) \end{aligned} \]
Then, to minimize this function, take derivatives with respect to \(w\) and Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial w}&=\mathbf{w}X^{T}X-X^{T}y-\lambda_{1}e=0\\ \frac{\partial\mathcal{L}(\mathbf{w},\lambda)}{\partial \lambda_{1}}&=\mathbf{w}e^T-1=0 \end{aligned} \]
Simplify the equations above in matrix form and solve for the Lagrange multipliers \(\lambda_{i}\):
\[ \begin{aligned} \begin{bmatrix} X^{T}X & e \\ e^{T} & 0 \end{bmatrix} \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} X^{T}y \\ 1 \end{bmatrix} \\ \begin{bmatrix} \mathbf{w} \\ -\lambda_{1} \end{bmatrix} &= \begin{bmatrix} X^{T}X & e \\ e^{T} & 0 \end{bmatrix}^{-1} \begin{bmatrix} X^{T}y \\ 1 \end{bmatrix} \end{aligned} \]
<- function(mu, sigma) {
min_rss_optim1
<- Variable(length(mu))
params
<- Minimize(0.5 * quad_form(params, sigma) - t(mu) %*% params)
obj
<- list(sum(params) == 1, params >= 0)
cons
<- Problem(obj, cons)
prob
<- solve(prob)$getValue(params)
result
return(result)
}
<- t(min_rss_optim1(crossprod(overlap_x_xts, overlap_y_xts), crossprod(overlap_x_xts)))
params4 params4
[,1] [,2] [,3] [,4]
[1,] 0.374955 7.191451e-23 0.625045 2.959672e-23
%*% mu params4
[,1]
[1,] 0.03039745
sqrt(params4 %*% sigma %*% t(params4))
[,1]
[1,] 0.05050206
# roll_min_rss(xx, xy)
<- function(x, y) {
min_rss_optim2
<- Variable(ncol(x))
params
<- Minimize(sum_squares(y - x %*% params))
obj
<- list(sum(params) == 1, params >= 0)
cons
<- Problem(obj, cons)
prob
<- solve(prob)$getValue(params)
result
return(result)
}
<- t(min_rss_optim2(coredata(overlap_x_xts), coredata(overlap_y_xts)))
params5 params5
[,1] [,2] [,3] [,4]
[1,] 0.374955 -1.364843e-20 0.625045 -8.927092e-21
%*% mu params5
[,1]
[1,] 0.03039745
sqrt(params5 %*% sigma %*% t(params5))
[,1]
[1,] 0.05050206
round(data.frame("max_pnl" = t(params1) * 100,
"min_risk" = t(params2) * 100,
"max_utility" = t(params3) * 100,
"min_rss1" = t(params4) * 100,
"min_rss2" = t(params5) * 100), 2)
max_pnl min_risk max_utility min_rss1 min_rss2
1 49.61 29.58 55.85 37.5 37.5
2 50.39 44.64 44.15 0.0 0.0
3 0.00 25.78 0.00 62.5 62.5
4 0.00 0.00 0.00 0.0 0.0