Custom constraints

This demonstration shows how to properly pass constraints to the parmaspec function. First, a custom function or its gradient must take as arguments the following:

  1. w – the decision vector
  2. optvars – the program specific list containing such items as the scenario matrix (in the case of the EV or MAD problems, this is the centered scenario matrix).
  3. uservars – a list of user specific item which can be used in the custom constraints.

There is one key thing to consider when working with the decision vector, namely that it may contain more than just the portfolio weights. For example, in the CVaR and CDaR problems, the Value at Risk (VaR) and Drawdown at Risk (DaR) are also decisions variables as is the minimum value in the Minimax problem. Further, in the fractional problem, the multiplier is also a decision variable to be optimized. To properly address this complication, the optvars list contains a set of indices which contain the position of these values as demonstrated in the examples that follow.

LP vs NLP and sector constraints

First, the accuracy of the NLP vs LP formulation is established for the problem with properly set up constraints. As noted in the vignette and help files, in order for the NLP problem to remain convex, the inequalities have to be convex and the equalities affine. The example that follows uses a simple sector based constraint.

library(parma)
if(!is.loaded('etfdata')) data(etfdata)
R = as.matrix(timeSeries::returns(etfdata))
m = 14
# Constraint 1:
# The joint exposure to (1, 2, 8, 9, 10) to be less than 40%
# The joint exposure to (3, 4, 7, 11) to be less than 40%
# The joint exposure to (5, 6, 12, 13, 14) to be less than 40%
s1=s2=s3=rep(0,m)
idx1 = c(1,2,8,9,10)
idx2 = c(3,4,7,11)
idx3 = c(5,6,13,14)
s1[idx1]=1
s2[idx2]=1
s3[idx3]=1
# Unlike the NLP formulation, we do not have to worry about the extra VaR
# parameter nor the fractional scaling parameter which are handled
# internally by the LP problem setup
sectmatrix = cbind(rbind(t(s1), t(s2), t(s3)))
spec1 = parmaspec(scenario = R[, -6], forecast = colMeans(R[, -6]), risk = 'CVaR',
 riskType = 'optimal', options = list(alpha = 0.05), LB = rep(0, 14), UB = rep(0.2,
 14), budget = 1)
spec2 = parmaspec(scenario = R[, -6], forecast = colMeans(R[, -6]), risk = 'CVaR',
 riskType = 'optimal', options = list(alpha = 0.05), LB = rep(0, 14), UB = rep(0.2,
 14), budget = 1, ineq.mat = sectmatrix, ineq.LB = c(0, 0, 0), ineq.UB = c(0.4,
 0.4, 0.4))
sol1_lp = parmasolve(spec1, type = 'LP')
# spec 2 is LP because of the ineq.mat LP matrices
sol2_lp = parmasolve(spec2)

That takes care of the LP optimization. Next, the NLP custom inequality and Jacobian are created and passed to the specification function. Note the use of the indices from the optvars list:

  1. widx – the index of the location of the weights
  2. midx – the index of the location of the multiplier (for fractional programming problems)
  3. vidx – the index of any problem specific values (for CVaR, CDaR and MiniMax formulations)

Finally, the optvars$fm value defines the size of the decision vector w. The inequality representation here follows from the fractional problem setup, and the user is strongly urged to read the vignette section on how to properly work with these types of problems. Additionally, note that the nloptr solver represents inequalities as g(x)< =0 and equalities as h(x)=0.

uservars = list()
uservars$sectmatrix = sectmatrix
secfun = function(w, optvars, uservars) {
    ineqc = w[optvars$midx] * c(0, 0, 0) - uservars$sectmatrix %*% w[optvars$widx]
    ineqc = c(ineqc, uservars$sectmatrix %*% w[optvars$widx] - w[optvars$midx] *
        c(0.4, 0.4, 0.4))
    return(ineqc)
}
# gradient
secjac = function(w, optvars, uservars) {
    # size of problem: optvars$fm nrows = n.constraints = 3x2
    widx = optvars$widx
    midx = optvars$midx
    g = matrix(0, ncol = optvars$fm, nrow = 6)
    g[1:3, widx] = -uservars$sectmatrix
    g[4:6, widx] = uservars$sectmatrix
    g[1:3, midx] = c(0, 0, 0)
    g[4:6, midx] = c(-0.4, -0.4, -0.4)
    return(g)
}
spec3 = parmaspec(scenario = R[, -6], forecast = colMeans(R[, -6]), risk = 'CVaR',
    riskType = 'optimal', options = list(alpha = 0.05), LB = rep(0, 14), UB = rep(0.2,
        14), budget = 1, ineqfun = list(secfun), ineqgrad = list(secjac), uservars = uservars)
sol1_nlp = parmasolve(spec3, type = 'NLP')

The following table displays the results of the optimization. Notice how the sector unconstrained problem has allocated most weight to sector 3, whilst the user imposed constraints have resulted in a more equal redistribution of weights across the 3 sectors. In practice, whether such constraints are detrimental to active bets or an aid to risk reduction by recognizing the uncertainty in the forecast is an open question. Finally, as expected, the LP and NLP formulations using the custom constraint return equal results. While this is a simple example, more complex NLP constraints can be considered, a detailed example of which is available in function parma.test7 in the parma.tests folder of the package.

##              LP (no-cons) LP (cons) NLP (cons)
## EEM                0.2000    0.0866     0.0866
## EWC                0.2000    0.1134     0.1134
## EWA                0.0456    0.2000     0.2000
## EWL                0.1544    0.2000     0.2000
## EPP                0.2000    0.0000     0.0000
## EZA                0.2000    0.2000     0.2000
## IWF                0.0000    0.2000     0.2000
## CvaR               0.0447    0.0412     0.0412
## VaR                0.0278    0.0253    -0.0253
## Reward             0.0005    0.0004     0.0004
## Risk/Reward       96.5438  100.4153   100.4173
## Sect_1             0.1544    0.4000     0.4000
## Sect_2             0.0456    0.2000     0.2000
## Sect_3             0.8000    0.4000     0.4000
## Elapsed(sec)       1.9355    1.0752     2.5166