btest: Backtesting Investment Strategies

View source: R/btest.R

btestR Documentation

Backtesting Investment Strategies

Description

Testing trading and investment strategies.

Usage

btest(prices, signal,
      do.signal = TRUE, do.rebalance = TRUE,
      print.info = NULL, b = 1, fraction = 1,
      initial.position = 0, initial.cash = 0,
      final.position = FALSE,
      cashflow = NULL, tc = 0, ...,
      add = FALSE, lag = 1, convert.weights = FALSE,
      trade.at.open = TRUE, tol = 1e-5, tol.p = NA,
      Globals = list(),
      prices0 = NULL,
      include.data = FALSE, include.timestamp = TRUE,
      timestamp, instrument,
      progressBar = FALSE,
      variations, variations.settings, replications)

Arguments

prices

For a single asset, a matrix of prices with four columns: open, high, low and close. For n assets, a list of length four: prices[[1]] is then a matrix with n columns containing the open prices for the assets; prices[[2]] is a matrix with the high prices, and so on. If only close prices are used, then for a single asset either a matrix of one column or a numeric vector; for multiple assets a list of length one, containing the matrix of close prices. For example, with 100 close prices of 5 assets, the prices should be arranged in a matrix p of size 100 times 5; and prices = list(p).

The series in prices are used both as transaction prices and for valuing open positions. If signals are to be based on other series, such other series should be passed via the ... argument.

Prices must be ordered by time (though the timestamps need not be provided).

signal

A function that evaluates to the position in units of the instruments suggested by the trading rule. If convert.weights is TRUE, signal should return the suggested position as weights (which need not sum to 1). If signal returns NULL, the current position is kept. See Details.

do.signal

Logical or numeric vector, a function that evaluates to TRUE or FALSE, or a string.

When a logical vector, its length must match the number of observations in prices: do.signal then corresponds to the rows in prices at which a signal is computed. Alternatively, these rows may also be specified as integers. If a length-one TRUE or FALSE, the value is recycled to match the number of observations in prices. Default is TRUE: a signal is then computed in every period.

do.signal may also be the string “firstofmonth”, “lastofmonth”, “firstofquarter” or “lastofquarter”; in these cases, timestamp needs to specified and must be coercable to Date.

If timestamp is specified, do.signal may also be a vector of the same class as timestamp (typically Date or POSIXct). If the timestamps specified in do.signal do not occur in timestamp, a signal is computed on the next possible time instance.

do.rebalance

Same as do.signal, but it may return a logical vector of length equal to the number of assets, which indicates which assets to rebalance. Can also be the string "do.signal", in which case the value of do.signal is copied. do.rebalance is called after signal computation, so it can access the suggested position of the current period (via SuggestedPortfolio(0).

print.info

A function, called at the very end of each period, i.e. after rebalancing. Can also be NULL, in which case nothing is printed.

cashflow

A function or NULL (default).

b

burn-in (an integer). Defaults to 1. This may also be a length-one timestamp of the same class as timestamp, in which case the data up to (and including) b are skipped.

fraction

amount of rebalancing to be done: a scalar between 0 and 1

initial.position

a numeric vector: initial portfolio in units of instruments. If supplied, this will also be the initial suggested position.

initial.cash

a numeric vector of length 1. Defaults to 0.

final.position

logical

tc

transaction costs as a fraction of turnover (e.g., 0.001 means 0.1%). May also be a function that evaluates to such a fraction. More-complex computations may be specified with argument cashflow.

...

other named arguments. All functions (signal, do.signal, do.rebalance, print.info, cashflow) will have access to these arguments. See Details for reserved argument names.

add

Default is FALSE. TRUE is not implemented – but would mean that signal should evaluate to changes in position, i.e. orders.

lag

default is 1

convert.weights

Default is FALSE. If TRUE, the value of signal will be considered a weight vector and automatically translated into (fractional) position sizes.

trade.at.open

A logical vector of length one; default is TRUE.

tol

A numeric vector of length one: only rebalance if the maximum absolute suggested change for at least one position is greater than tol. Default is 0.00001 (which practically means always rebalance).

tol.p

A numeric vector of length one: only rebalance those positions for which the relative suggested change is greater than tol.p. Default is NA: always rebalance.

Globals

A list of named elements. See Details.

prices0

A numeric vector (default is NULL). Only used if b is 0 and an initial portfolio (initial.position) is specified.

include.data

logical. If TRUE, all passed data are stored in final btest object. See Section Value. See also argument include.timestamp.

include.timestamp

logical. If TRUE, timestamp is stored in final btest object. If timestamp is missing, integers 1, 2, ... are used. See Section Value. See also argument include.data.

timestamp

a vector of timestamps, along prices (optional; mainly used for print method and journal)

instrument

character vector of instrument names (optional; mainly used for print method and journal)

progressBar

logical: display txtProgressBar?

variations

a list. See Details.

variations.settings

a list. See Details.

replications

an integer. If set, the function returns a list of btest objects. Each btest has an attribute replication, which records the replication number.

Details

The function provides infrastructure for testing trading rules. Essentially, btest does accounting: keep track of transactions and positions, value open positions, etc. The ingredients are price time-series (single series or OHLC bars), which need not be equally spaced; and several functions that map these series and other pieces of information into positions.

How btest works

btest runs a loop from b + 1 to NROW(prices). In iteration t, a signal can be computed based on information from periods prior to t. Trading then takes place at the opening price of t. For slow-to-compute signals this is reasonable if there is a time lag between close and open. For daily prices, for instance, signals could be computed overnight. For higher frequencies, such as every minute, the signal function should be fast to compute. Alternatively, it may be better to use a larger time offset (i.e. use a longer time lag) and to trade at the close of t by setting argument trade.at.open to FALSE.

If no OHLC bars are available, a single series per asset (assumed to be close prices) can be used.

The trade logic needs to be coded in the function signal. Arguments to that function must be named and need to be passed with .... Certain names are reserved and cannot be used as arguments: Open, High, Low, Close, Wealth, Cash, Time, Timestamp, Portfolio, SuggestedPortfolio, Globals. Further reserved names may be added in the future: it is suggested to not start an argument name with a capital letter.

The function signal must evaluate to the target position in units of the instruments. To work with weights, set convert.weights to TRUE, and btest will translate the weights into positions.

Accessing data

Within signal (and also other function arguments, such as do.signal), you can access data via special functions such as Close. These are automatically added as arguments to signal. Currently, the following functions are available: Open, High, Low, Close, Wealth, Cash, Time, Timestamp, Portfolio, SuggestedPortfolio, Globals. Globals is special: it is an environment, which can be used to persistently store data during the run of btest. Use the argument Globals to add initial objects. See the Examples below and the manual.

Additional functions may be added to btest in the future. The names of those functions will always be in title case. Hence, it is recommended to not use argument names for signal, etc. that start with a capital letter.

Replications and variations

btest allows to run backtests in parallel. See the examples at http://enricoschumann.net/notes/parallel-backtests.html.

The argument variations.settings is a list with the following defaults:

method

character: supported are "loop", "parallel" (or "snow") and "multicore"

load.balancing

logical

cores

numeric

Value

A list with class attribute btest. The list comprises:

position

actual portfolio holdings

suggested.position

suggested holdings

cash

cash

wealth

time-series of total portfolio value (aka equity curve)

cum.tc

transaction costs

journal

journal of trades. Only includes trades done during the backtest, not initial positions.

initial.wealth

initial wealth

b

burn-in

final.position

final position if final.position is TRUE; otherwise NA

Globals

environment Globals

When include.timestamp is TRUE, the timestamp is included. If no timestamp was specified, integers 1, 2, ... are used instead.

When include.data is TRUE, essentially all information (prices, instrument, the actual call and functions signal etc.) are stored in the list as well.

Author(s)

Enrico Schumann es@enricoschumann.net

References

Schumann, E. (2023) Portfolio Management with R. http://enricoschumann.net/PMwR/; in particular, see the chapter on backtesting:
http://enricoschumann.net/R/packages/PMwR/manual/PMwR.html#backtesting

Schumann, E. (2018) Backtesting.
https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3374195

Examples

## For more examples, please see the Manual
## http://enricoschumann.net/R/packages/PMwR/manual/PMwR.html

## 1 - a simple rule
timestamp <- structure(c(16679L, 16680L, 16681L, 16682L,
                         16685L, 16686L, 16687L, 16688L,
                         16689L, 16692L, 16693L),
                       class = "Date")
prices <- c(3182, 3205, 3272, 3185, 3201,
            3236, 3272, 3224, 3194, 3188, 3213)
data.frame(timestamp, prices)


signal <- function()     ## buy when last price is
    if (Close() < 3200)  ## below 3200, else sell
        1 else 0         ## (more precisely: build position of 1
                         ##  when price < 3200, else reduce
                         ##  position to 0)

solution <- btest(prices = prices, signal = signal)
journal(solution)


## with Date timestamps
solution <- btest(prices = prices, signal = signal,
                  timestamp = timestamp)
journal(solution)



## 2 - a simple MA model
## Not run: 
library("PMwR")
library("NMOF")

dax <- DAX[[1]]

n <- 5
ma <- MA(dax, n, pad = NA)

ma_strat <-  function(ma) {
    if (Close() > ma[Time()])
        1
    else
        0
}


plot(as.Date(row.names(DAX)), dax, type = "l", xlab = "", ylab = "DAX")
lines(as.Date(row.names(DAX)), ma, type = "l")

res <- btest(prices = dax,
             signal = ma_strat,
             b = n, ma = ma)

par(mfrow = c(3, 1))
plot(as.Date(row.names(DAX)), dax, type = "l",
     xlab = "", ylab = "DAX")
plot(as.Date(row.names(DAX)), res$wealth, type = "l",
     xlab = "", ylab = "Equity")
plot(as.Date(row.names(DAX)), position(res), type = "s",
     xlab = "", ylab = "Position")

## End(Not run)

enricoschumann/PMwR documentation built on March 27, 2024, 2:25 p.m.