btest {PMwR} | R 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 The series in 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 |
do.signal |
Logical or numeric vector, a function that
evaluates to When a logical vector, its length must match the
number of observations in prices:
If |
do.rebalance |
Same as |
print.info |
A function, called at the very end of each period,
i.e. after rebalancing. Can also be |
cashflow |
A function or |
b |
burn-in (an integer). Defaults to 1. This may also
be a length-one timestamp of the same class as
|
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 |
... |
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 |
lag |
default is 1 |
convert.weights |
Default is |
trade.at.open |
A logical vector of length one; default is |
tol |
A numeric vector of length one: only rebalance if
the maximum absolute suggested change for at least
one position is greater than |
tol.p |
A numeric vector of length one: only rebalance
those positions for which the relative suggested
change is greater than |
Globals |
A |
prices0 |
A numeric vector (default is |
include.data |
logical. If |
include.timestamp |
logical. If |
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 |
variations |
a list. See Details. |
variations.settings |
a list. See Details. |
replications |
an integer. If set, the function returns a list of
|
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
.
t time open high low close 1 HH:MM:SS <--\ 2 HH:MM:SS <-- - use information 3 HH:MM:SS _________________________ <--/ 4 HH:MM:SS X <- trade here 5 HH:MM:SS
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
.
t time open high low close 1 HH:MM:SS <-- \ 2 HH:MM:SS <-- - use information 3 HH:MM:SS _________________________ <-- / 4 HH:MM:SS X <-- trade here 5 HH:MM:SS
If no OHLC bars are available, a single
series per asset (assumed to be close prices) can
be used. trade.at.open
will automaticall be
set to FALSE
.
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, based on the value of the
portfolio at t - 1
.
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
https://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 (aka target position) |
cash |
cash |
wealth |
time-series of total portfolio value (aka equity curve) |
cum.tc |
transaction costs |
journal |
|
initial.wealth |
initial wealth |
b |
burn-in |
final.position |
final position if |
Globals |
environment |
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.
https://enricoschumann.net/PMwR/;
in particular, see the chapter on backtesting:
https://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
## https://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)