Impose transaction costs

Task

Include trading costs in an optimization.

Preparation

This presumes that you can do a basic portfolio optimization.  For example, that you have mastered “Passive, no benchmark (minimum variance)”.

  • Portfolio Probe

You 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

 Doing it

We will do optimizations that:

  • impose simplistic linear costs
  • impose simplistic linear costs that differ for buying and selling
  • impose non-linear costs

None of the examples are complete — see the “Further details” section for the missing ingredient.

Simplistic linear costs

We perform an optimization that assumes that trading costs are 10 basis points:

opTC10bps <- trade.optimizer(priceVector, 
   variance=xaLWvar06, existing=curPortfol, 
   gross=grossVal, long.only=TRUE, 
   utility="minimum variance", 
   long.buy.cost=0.001 * priceVector)

This optimization is correct in the sense that it tells the optimizer that trading costs are 10 basis points.  But the optimization is (almost surely) wrong in the sense of doing the right thing even if trading costs really are 10 basis points — see the “Further details” section below.

Costs that differ for buying and selling

Real costs are different depending on whether you are buying or selling.  In a long-only portfolio you merely do a sell.  But when you buy, you are also committing yourself to a subsequent sell.

Here we impose 20 basis point costs for buying and 10 basis points for selling:

opTC2010bps <- trade.optimizer(priceVector, 
   variance=xaLWvar06, existing=curPortfol, 
   gross=grossVal, long.only=TRUE, 
   utility="minimum variance", 
   long.buy.cost=0.002 * priceVector, 
   long.sell.cost=0.001 * priceVector)

Non-linear costs

The cost of small trades is linear, but bigger trades have market impact and grow faster than linear.  Suppose that we believe that market impact grows with an exponent of 0.6 — so slightly faster than the square root of the inventory model.

First we want to create a two-column matrix that holds the trading cost coefficients per asset.  A quick stand-in is:

tcNonlin <- cbind(priceVector * .001, 
   rep(1:5, length=350) * .005)

This looks like:

> head(tcNonlin)
         [,1]  [,2]
XA101 0.03356 0.005
XA103 0.07225 0.010
XA105 0.07439 0.015
XA107 0.19206 0.020
XA108 0.00591 0.025
XA111 0.01598 0.005

Now we are ready to do the optimization:

opTCnonlin <- trade.optimizer(priceVector, 
   variance=xaLWvar06, existing=curPortfol, 
   gross=grossVal, long.only=TRUE, 
   utility="minimum variance", 
   long.buy.cost=tcNonlin, cost.par=c(1, 1.6))

Explanation

 Linear costs

The value given as long.buy.cost (and mates) should be a vector with names that are the asset identifiers.  All of the tradable assets must be included.  A one-column matrix also works.

There is, of course, no reason that the costs need to be proportional to prices.

Non-linear costs

The cost.par argument is  a vector of the exponents that go with each column of the cost arguments (long.buy.cost and its mates).  (But see the “Further details” section below.)

In the example we had:

long.buy.cost=tcNonlin, cost.par=c(1, 1.6)

with

> head(tcNonlin, 3)
         [,1]  [,2]
XA101 0.03356 0.005
XA103 0.07225 0.010
XA105 0.07439 0.015

If asset XA101 trades 99 shares (buy or sell), then the cost for it will be:

> 0.03356 * 99 + 0.005 * 99 ^ 1.6
[1] 11.1205

The numbers in the first column of long.buy.cost go with the first element of cost.par.  The second column goes with the second element of cost.par, and so on.  You can have as many columns as you like.  In particular, there can be an element of cost.par that is zero, which means there is a fixed cost for trading the asset at all.

Buys versus sells

The full set of cost coefficient arguments is:

  • long.buy.cost
  • long.sell.cost
  • short.buy.cost
  • short.sell.cost

Ones that are not given will default to the value of long.buy.cost (hence if you are giving costs, you always want to give this argument).

If cost.par is given, then all four of these arguments need to have the same number of columns.  The number of columns, of course, has to be the length of cost.par.

Further details

 Costs relative to utility

In the examples we are minimizing variance.  In this case it is easy to see that we must be missing something even if we exactly reproduce our trading costs.

The cost (divided — by default — by the gross value of the portfolio) is added to the utility.  So we are adding dollars to something to do with squared returns.  Without costs we get the same thing whether the variance is for daily returns or for returns in percent and annualized.  But we get an entirely different effect if we add the same costs in these two cases.

Portfolio Probe has the ucost argument (that defaults to 1) which scales the cost before it is put into the utility.  So you might include something like:

ucost = 0.002

as an argument in an optimization.

We can do some exploration using the first optimization.  The default value of ucost is 1 so that is its value with that first optimization.  We can redo the optimization setting ucost to zero (that is, no trading costs):

opTC0 <- update(opTC10bps, ucost=0)

Now we can get the turnover for optimizations that have intermediate values of ucost:

turnTC <- numeric(5)
for(i in 1:5) {
   turnTC[i] <- valuation(update(opTC10bps, ucost=2^-i),
      trade=TRUE, collapse=TRUE)
} 
names(turnTC) <- 2^-(1:5)

Now we can add the turnover for the two portfolios that we already have:

turnTCall <- c("1"=unname(valuation(opTC10bps, 
   trade=TRUE, collapse=TRUE)), turnTC, 
   "0"=unname(valuation(opTC0, trade=TRUE, 
   collapse=TRUE)))

Figure 1 is a cleaned up version of:

plot(as.numeric(names(turnTCall)), turnTCall, type="l")

Figure 1: Turnover versus ucost value for the simplistic linear costs.

The drop in the amount of trading stops at ucost=.25.  So it appears in this example that ucost should be less than .25 (and larger than zero).  Remember that if we change the scaling of the variance, then ucost needs to change as well.

An element of melding costs into the utility is that the costs need to be amortized.  That is, the expected holding period makes a difference.  You need to get a higher rate of value out of an asset that you expect to hold for a day as opposed to an asset you expect to hold for a decade.

Non-linear costs

asset-specific exponents

Above it was stated that cost.par was to be a vector.  Actually it can be a matrix with rows that correspond to assets and as many columns as long.buy.cost.  For example, we might give a matrix like the following to the cost.par argument:

> head(tcCostpar)
      [,1] [,2]
XA101    1 1.57
XA103    1 1.75
XA105    1 1.47
XA107    1 1.59
XA108    1 1.75
XA111    1 1.72

If all of the rows were equal, then it would be just the same as giving cost.par a vector equal to one of the rows.

per trade versus per share

The exponent that you want to use depends on what coefficients you have.  The calculation done in the optimizer is on the trade as a whole — it sees the number of shares traded.

If your coefficients are from a model of trade size with an exponent of 0.6, then put 0.6 into cost.par.  (Hint: I don’t think so, the exponent should be greater than 1.)

However, if the coefficients you have is for the cost per share, then you need to do a little elementary calculus to get the cost per trade size and the exponent is going to be 1.6 (the original exponent plus one).

Troubleshooting

  • Is a reasonable value of ucost given to match the trading costs to the utility?
  • If you have non-linear costs, are the coefficients and exponents correctly matched in regards to per share versus total shares?

See also

Navigate