Path-dependent strategies

In this note I show how a simple path-dependent strategy – a stop loss – can be implemented with btest. To keep it simple, we look only at the univariate case, i.e. a single time-series. For data, we use the time-series of the DAX (a German stock-market index), which is included in the PMwR package.

library("PMwR")

prices <- c(DAX[[1]])
timestamp <- as.Date(row.names(DAX))

The first strategy: buy once, on the first timestamp, and then sell if the price moves more than stop.loss percent below the entry price.

buy_once_SL <- function(stop.loss) {
    if (Time(0) == 1) {        
        Globals$entry <- Close(0)
        1
    } else if (Portfolio() && Close() < Globals$entry * (1 - stop.loss))
        0
}

bt.buy_once_SL <- btest(prices = prices,
                        signal = buy_once_SL,
                        timestamp = timestamp,
                        stop.loss = 0.05,
                        b = 0)

journal(bt.buy_once_SL)
   instrument   timestamp  amount    price
1     asset 1  2014-01-02       1  9400.04
2     asset 1  2014-10-13      -1  8812.43

2 transactions

Remarks: Since we have a calendar schedule when to buy, there is no need for a burnin, so b is set to zero. Also, within the signal function, we test whether Time(0) equals 1, i.e. we use no lag. The else clause starts with the test Portfolio(): If we are not invested (i.e. we have been stopped out), the position will be zero, which evaluates to FALSE. For this example, such a test would not have been necessary, since the second condition is very fast to compute. But in general, for a path-dependent strategy, btest needs to loop through every single data point, and then not doing unnecessary work helps to speed up things.

Suppose we had wanted a trailing stop, i.e. sell when stop.loss percent below the highest price observed after the position was opened. In that case, we simply would have to update the entry price (which should then better be named high or reference or something similar). This update would have to happen whenever we have an open position. In any case, the entry is stored in Globals, which is an environment provided by btest. For our purposes, we can treat it just like a list, only that it is never copied. Thus, the objects we put into it are not local, but are persistent between invocations of the signal function.

In the example above, once we have closed the position, we will never again invest. Suppose that instead, we would want to reinstate a new position at the first day of every quarter. We would need only few changes to buy_once_SL.

library("datetimeutils")
trade.dates <- nth_day(timestamp, period = "quarter", n = "first")

buy_quarterly_SL <- function(stop.loss, trade.dates) {
    if (Timestamp(0) %in% trade.dates) {
        Globals$entry <- Close(0)
        1
    } else if (Portfolio() && Close() < Globals$entry * (1 - stop.loss))
        0
}

bt.buy_quarterly_SL <- btest(prices = prices,
                             signal = buy_quarterly_SL,
                             timestamp = timestamp,
                             stop.loss = 0.05,
                             trade.dates = trade.dates,
                             b = 0)

journal(bt.buy_quarterly_SL)
   instrument   timestamp  amount     price
1     asset 1  2014-01-02       1   9400.04
2     asset 1  2014-08-04      -1   9154.14
3     asset 1  2014-10-01       1   9382.03
4     asset 1  2014-10-13      -1   8812.43
5     asset 1  2015-01-02       1   9764.73
6     asset 1  2015-05-06      -1  11350.15
7     asset 1  2015-07-01       1  11180.50
8     asset 1  2015-08-21      -1  10124.52
9     asset 1  2015-10-01       1   9509.25

9 transactions

Note that with this specification, the entry price is updated every quarter, even when we have not been stopped out.

Finally, we may want to compare the results with a buy-and-hold strategy. The signal function is so simple that we can inline it.

bt.buy_hold <- btest(prices = prices,
                     signal = function() 1,
                     timestamp = timestamp,
                     b = 0)

journal(bt.buy_hold)
   instrument   timestamp  amount    price
1     asset 1  2014-01-02       1  9400.04

1 transaction

We plot the resulting equity curves.

library("plotseries")  ## https://github.com/enricoschumann/plotseries
library("zoo")
plotseries(merge(as.zoo(NAVseries(bt.buy_hold)),
                 as.zoo(NAVseries(bt.buy_once_SL)),
                 as.zoo(NAVseries(bt.buy_quarterly_SL))),
           add0 = TRUE,
           add1 = FALSE,
           col = c(grey(0.5), "darkgreen", "goldenrod3"),
           labels = c("buy & hold", "buy once", "buy quarterly"),
           add.returns = FALSE,
           add.dollars = FALSE,
           big.mark = ",")
path-dependent-strategies-1.png