Adding Group Exposure Limits to Mean-Variance ETF Optimization
Summary
The document shows how to impose limits on aggregate portfolio exposure to categories of ETFs, such as bond funds or funds in one asset class. In a quadratic-programming formulation, each category is represented by an exposure vector marking which assets belong to it. Adding that vector as an inequality constraint caps the sum of portfolio weights in the group. The worked example uses R's quadratic-programming convention, where the signs of the exposure vector and bound must be chosen to match its constraint direction.
A simulated mean-variance example compares allocations before and after two group caps are added. The reported group weights show that the constraints bind at their specified limits and the remaining allocation shifts to the third group. This illustrates how group restrictions can coexist with budget, target-return, and individual nonnegative-weight constraints. The example is instructional rather than empirical, and it assumes group membership is known and expressed as binary exposures; it does not cover overlapping groups, uncertain estimates, or the broader practical costs of portfolio implementation.
Key ideas
- Represent each constrained ETF category with a vector indicating its member funds.
- Add one inequality constraint per group to cap the total weight assigned to that category.
- Check the quadratic-programming solver's inequality direction when setting the vector and bound signs.
- Group caps can force the optimizer to redistribute weight to unconstrained categories.
Tags
Full text
# Optimizing a portfolio of ETFs
# Optimizing a portfolio of ETFs
I am aware of how to do mean-variance or minimum-variance portfolio optimization with constraints like
- weights must add to 1.0
- no short sells
- max weight in any ticker
using basic quadratic programming techniques. However I am stumped by the following:
My universe of tickers consists of ETFs. Say I want constraints of the form:
- no more than 20% of the portfolio in bond funds (there are many possible bond funds)
- no more than 30% of the portfolio in funds of one asset class (e.g. real estate)
How can I run such a portfolio optimization?
## Answer by michaelv2 (score 10, accepted)
https://quant.stackexchange.com/a/3090
Using solve.QP in R, a straightforward approach is to add a binary exposure vector as an inequality constraint to your Amat matrix for each group that you want to constrain.
The only catch is that values in the exposure and b_0 vectors should be negative, since the function is really satisfying the constraints: A^T b >= b_0.
For a simple mean-variance example with two groups that we want to constrain:
```
library(quadprog)
library(MASS)
# Generate some returns
set.seed(100)
n <- 100 # number of assets
m <- 200 # number of states of the world
rho <- 0.7
sigma <- 0.2
mu <- .1
Cov <- matrix(rho*sigma*sigma, ncol=n, nrow=n)
diag(Cov) <- rep(sigma*sigma, n)
S <- 1 + matrix(mvrnorm(m, rep(mu, n), Sigma=Cov), ncol=n)
# Calculate a covariance matrix
Cov <- var(S)
# Setup quadratic problem
mu <- apply(S, 2, mean)
mu.target <- mean(mu)
bLo <- rep(0, n)
# Define group membership (arbitrary example)
group1 <- matrix(0,100)
group2 <- matrix(0,100)
group3 <- matrix(0,100)
group1[mu <= mean(mu) - .005] <- -1
group2[mu > (mean(mu) - .005) & mu <= (mean(mu) + .005)] <- -1
group3[mu > mean(mu) + .005] <- -1
Amat <- rbind(1, mu)
dim(bLo) <- c(n,1)
bvec <- t(rbind(1, mu.target, bLo))
zMat <- diag(n)
Amat <- t(rbind(Amat, zMat))
Dmat <- Cov
dvec <- rep(0, nrow(Amat))
meq <- 2 # the first two columns are equality constraints
sol <- solve.QP(Dmat=Dmat, dvec=dvec, Amat=Amat, bvec=bvec, meq)
cat(paste("Without group constraints:\n"))
data.frame(Group1=sum(sol$solution * -group1), Group2=sum(sol$solution * -group2), Group3=sum(sol$solution * -group3))
# Add group constraints:
# Group1 <= 20%
# Group2 <= 30%
Amat <- rbind(1, mu, t(group1), t(group2))
dim(bLo) <- c(n,1)
bvec <- t(rbind(1, mu.target, -.20, -.30, bLo))
zMat <- diag(n)
Amat <- t(rbind(Amat, zMat))
Dmat <- Cov
dvec <- rep(0, nrow(Amat))
sol <- solve.QP(Dmat=Dmat, dvec=dvec, Amat=Amat, bvec=bvec, meq)
cat(paste("With group constraints:\n"))
data.frame(Group1=sum(sol$solution * -group1), Group2=sum(sol$solution * -group2), Group3=sum(sol$solution * -group3))
```
Group weights:
```
1 2 3
Without constraints 26.4% 53.1% 20.4%
With constraints 20.0% 30.0% 50.0%
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.