Passive with benchmark (minimum tracking error)

Task

Optimize the trade to get the minimum tracking error given some simple constraints, including that the portfolio is long-only.

Preparation

  • vector of asset prices
  • variance matrix for the assets
  • information about the benchmark
  • current portfolio (if it exists)
  • Portfolio Probe

You need the prices at which assets trade and a variance matrix of the asset returns.  You need either the benchmark to be an asset in the variance matrix or the weight vector of assets for the benchmark.

The holdings of the current portfolio need to be in a vector with names that are the asset identifiers.

You also need to have the Portfolio Probe package loaded into your R session:

require(PortfolioProbe)

If you don’t have Portfolio Probe, see “Demo or Buy”.

Doing the example

You need to have the package loaded into your R session:

require(pprobeData)

Doing it

The inputs we need in order to get our minimum tracking error portfolio are:

  • vector of prices at which the assets may be traded
  • variance matrix of the asset returns
  • desired value of the new portfolio
  • appropriate constraints
  • current portfolio (optional)
  • information about the benchmark

prices

We start by naming the vector of prices that we want to use:

priceVector <- xassetPrices[251,]

These are the prices at the close of the last trading day of 2006.  The first few values are:

> head(priceVector)
 XA101  XA103  XA105  XA107  XA108  XA111 
 33.56  72.25  74.39 192.06   5.91  15.98

The requirement for the prices is that it be a vector of positive numbers with names (that are the asset identifiers).

current portfolio

We create an object to serve as the current portfolio:

curPortfol <- (1:10) * 1000
names(curPortfol) <- colnames(xassetPrices)[1:10]

What is expected is a numeric vector of the number of units of each asset in the portfolio.  The names of the vector are the identifiers of the assets that are used in the price vector and the variance matrix.

> curPortfol
XA101 XA103 XA105 XA107 XA108 XA111 XA113 XA115 
 1000  2000  3000  4000  5000  6000  7000  8000 
XA120 XA126 
 9000 10000

portfolio value

The value of the portfolio that we should specify is the current value of the existing portfolio adjusted by whatever cash flow is desired.  Here we assume we want to take out $30,000 from the portfolio:

cashFlow <- -30000
grossVal <- as.numeric(valuation(curPortfol, 
   priceVector, collapse=TRUE)) + cashFlow

We get the value of the current portfolio assuming the prices we are using and then add the cash flow.  (The as.numeric is merely for cosmetic reasons to make the result simpler.)  We end up with:

> grossVal
[1] 2983430

The gross value of the portfolio that we want is slightly less than $3 million.

Optimization with simple variance matrix

For this option we need:

  • a vector of the asset weights that comprise the benchmark

Further, we need the variance matrix to contain all of those assets.  But the vector of benchmark weights need not contain all the assets in the variance nor need they be in any particular order.

create benchmark weight vector

For the example, we will create a weight vector that equally weights all of the assets in the universe:

benwt <- rep(1/350, 350)
names(benwt) <- rownames(xaLWvar06)

The first few values are:

> head(benwt)
      XA101       XA103       XA105       XA107 
0.002857143 0.002857143 0.002857143 0.002857143 
      XA108       XA111 
0.002857143 0.002857143

The vector of benchmark weights need not have all of the assets that are in the variance matrix nor be in any particular order.  It is mandatory, however, that all of the assets in the benchmark weight vector be in the variance.

optimization specification

We’re now ready to do an optimization.  The only constraint that we impose besides the gross value and being long-only is that no more than 10 assets may be in the portfolio.

opMinTE1 <- trade.optimizer(priceVector, variance=xaLWvar06, 
   existing=curPortfol, gross=grossVal, long.only=TRUE, 
   port.size=10, utility="minimum variance",
   benchmark="EqualWt", bench.weights=list(EqualWt=benwt))

We specify the utility to be minimum variance.  We still would have got the same thing without the specification, but there would have been a warning that it was guessing what utility we wanted.  It may be confusing that we say “minimum variance” to minimize tracking error — hopefully this will become clear in time.

print result

The resulting object is printed like:

> opMinTE1
$new.portfolio
XA373 XA577 XA586 XA587 XA633 XA638 XA653 XA869 
 8972 13024 28037  5249  8739  3210  4622 16966 
XA887 XA931 
11604  4592 

$trade
 XA101  XA103  XA105  XA107  XA108  XA111  XA113 
 -1000  -2000  -3000  -4000  -5000  -6000  -7000 
 XA115  XA120  XA126  XA373  XA577  XA586  XA587 
 -8000  -9000 -10000   8972  13024  28037   5249 
 XA633  XA638  XA653  XA869  XA887  XA931 
  8739   3210   4622  16966  11604   4592 

$results
  objective     negutil        cost     penalty 
6.98082e-06 6.98082e-06 0.00000e+00 0.00000e+00 

$converged
[1] TRUE

$objective.utility
[1] "minimum variance"

$alpha.values
[1] NA

$var.values
V0 -- EqualWt 
  6.98082e-06 

$utility.values
[1] 6.98082e-06

$existing
XA101 XA103 XA105 XA107 XA108 XA111 XA113 XA115 
 1000  2000  3000  4000  5000  6000  7000  8000 
XA120 XA126 
 9000 10000 

$violated
NULL

$timestamp
[1] "Thu Sep 06 12:03:15 2012"
[2] "Thu Sep 06 12:03:19 2012"

$call
trade.optimizer(prices = priceVector, variance = xaLWvar06, existing = curPortfol, 
    gross = grossVal, long.only = TRUE, port.size = 10, utility = "minimum variance", 
    benchmark = "EqualWt", bench.weights = list(EqualWt = benwt))

The first two components are the new (optimal) portfolio and the trade to achieve that.  There are some additional components in the object that are not shown.

Optimization with benchmark in the variance matrix

For this option we need:

  • a variance matrix that includes the benchmark

See “Add benchmark to variance matrix” to create a variance matrix that includes a benchmark.

The command to do the optimization in this case looks like:

opMinTE2 <- trade.optimizer(priceVector, 
   variance=xaLWvar06EqWt, existing=curPortfol, 
   gross=grossVal, long.only=TRUE, port.size=10, 
   utility="minimum variance", benchmark="EqWt")

The only difference between this version and the previous optimization is that the bench.weights argument is not used here, we use a different variance matrix, and the name of the benchmark is different.

Explanation

The trade.optimizer function does the optimization.  Its first argument is the asset prices.

Portfolio value

It is mandatory that the value of the resulting portfolio be specified.  For long-only portfolios it is sufficient to state the desired gross value.  The actual value of the portfolio will (usually) be slightly less than the specification:

> format(grossVal, nsmall=2, big.mark=",")
[1] "2,983,430.00"
> grossVal - as.numeric(valuation(opMinTE1, collapse=TRUE))
[1] 185.27

Tracking error

The tracking error for the optimal portfolio is a modification of the var.values component:

> sqrt(252 * opMinTE1$var.values) * 100
V0 -- EqualWt 
     4.194242

So a tracking error of a little more than 4%.

Remember that this is predicted tracking error.  Since it was the quantity being optimized, it will be biased downward.

Other results

One component of the output to pay special attention to is ‘violated‘ — this states which constraints, if any, are violated.  You want this to be NULL.

It is probably not important whether ‘converged‘ is TRUE or FALSE.  The optimization is likely to be good enough with or without convergence.

Further Details

You can see more about the optimization with the summary of the object:

> summary(opMinTE1)
$results
  objective     negutil        cost     penalty 
6.98082e-06 6.98082e-06 0.00000e+00 0.00000e+00 

$objective.utility
[1] "minimum variance"

$alpha.values
[1] NA

$var.values
V0 -- EqualWt 
  6.98082e-06 

$number.of.assets
         existing             trade 
               10                20 
              new              open 
               10                10 
            close    universe.total 
               10               351 
         tradable   select.universe 
              350               351 
positions.notrade 
                0 

$opening.positions
 [1] "XA373" "XA577" "XA586" "XA587" "XA633"
 [6] "XA638" "XA653" "XA869" "XA887" "XA931"

$closing.positions
 [1] "XA101" "XA103" "XA105" "XA107" "XA108"
 [6] "XA111" "XA113" "XA115" "XA120" "XA126"

$value.limits
        lower   upper
gross 2983132 2983430
net   2983132 2983430
long  2983132 2983430
short       0       0

$valuation.new.portfolio
  gross     net    long   short 
2983245 2983245 2983245       0 

$valuation.trade
     gross        net       long      short 
5996674.73  -30185.27 2983244.73 3013430.00 

$valuation.trade.fraction.of.gross
      gross         net        long       short 
 2.01011827 -0.01011827  1.00000000  1.01011827

This has some pieces that are also in the print method, but new information as well.  All of the current portfolio was sold off — a trade to make the broker really happy.

Checking your work

Visual

There is a visual check that the benchmark is being taken into account.  This is the names of the var.values component of the optimization object:

> opMinTE1$var.values
V0 -- EqualWt 
  6.98082e-06 
> opMinTE2$var.values
 V0 -- EqWt 
6.98082e-06

The first part of the name (‘V0‘ in this case) gives the variance that is involved.  (Since there is only one variance, there can’t be ambiguity in this example.)  If there were only the variance name, that would mean that no benchmark was used for that value.  When there is a ‘ -- ‘ followed by a name, then that name will be the benchmark used.

Computational

Having a benchmark is computationally the same as being short the benchmark 100%.  With that in mind we can cross check the numbers by doing the matrix multiplication to get the tracking error.

The first thing to do is get the weights of the portfolio that we found:

wtMinTE2 <- valuation(opMinTE2)$weight

We want a weight vector that matches the variance matrix that includes the benchmark — that is, we want it to have the same assets in the same order.  But what we have is a short vector that only has the non-zero weights.

We can create the long vector like:

wtMinTE2long <- xaLWvar06EqWt[,1]
wtMinTE2long[] <- 0
wtMinTE2long[names(wtMinTE2)] <- wtMinTE2

We’ve done a common, if slightly ugly, trick to get the vector that we want.  We assign the first column of the variance matrix to the name we want as our long weight vector.  Because we didn’t say ‘drop=FALSE‘ inside the subscripts, the result is a plain vector.  At this point it holds a bunch of numbers we don’t want, but it does have the names that we want.

Now we put all of the numbers in the vector to 0 (but the names are not changed).  Then we add the non-zero weights.

Next we want to add the short position of the benchmark:

wtMinTE2long["EqWt"] <- -1

Assuming the benchmark is last in the variance matrix, we could have reminded ourselves of its name by looking at the last few values of the long weight vector:

> tail(wtMinTE2long)
XA982 XA984 XA986 XA990 XA993  EqWt 
    0     0     0     0     0    -1

Finally we can do the matrix multiply to get the tracking error:

> wtMinTE2long %*% xaLWvar06EqWt %*% wtMinTE2long
            [,1]
[1,] 6.98082e-06
> sqrt(252 * drop(wtMinTE2long %*% xaLWvar06EqWt %*% wtMinTE2long))
[1] 0.04194242

Troubleshooting

  • The variance matrix needs to contain all of the assets that are in the price vector.  It can have additional assets — these will be ignored (except for benchmarks).  The order of the assets in the variance does not matter.
  • All of the prices need to be in the same currency.  You have to check that — the code has no way of knowing.
  • It will still work if the object given as the prices is a one-column or one-row matrix.  But it will complain about other matrices.

See also

Navigate