knitr::opts_chunk$set(collapse = T, comment = "#>") options(tibble.print_min = 4L, tibble.print_max = 4L) library(stratbuilder2pub) library(TTR) library(quantmod) session <- ssh_connect("test_backtest_user@142.93.143.142", keyfile = "/home/vitaly/Documents/ilia")
When backtesting the model you must:
Describe what you want to do
Write a program for executing it
Get results and analyze the model's statistics
With package stratbuilder2pub
you can do it easily:
We provide tools for creating any midterm financial model
Your program will have a clear interface
You will get statistics in the form of tables and interactive graphs
This document introduces to you basic set of tools of this package.
Let's create a simple mean-reverting strategy using technical indicators from package TTR
. Take two moving averages of adjusted close prices of an asset. One of them will be with a small window (fast ma), and one will be with a big window (slow ma). If fast ma bigger than slow ma, than we should hold the short position. If fast ma is less than slow ma, then we should hold the long position. And we will not trade against the long trend, we will define it through very slow ma(moving average with big window).
So, firstly, create an empty model.
this <- modelStrategy()
Backtester uses internal names. One of them is spread
. It is the central element of every model. For its definition firstly we should define a table from that it will be calculated. Spread will be equal to linier combination of columns of that table. To get coefficients of that combination we should define a function that will calculate coefficients. If only one asset is used in the model, the table usually equals to close or adjusted prices, function for coefficients always return 1, then spread usually equals to close or adjusted prices.
In our model spread
will be equal to adjusted prices of an asset, that we will determine later. Now we need to define indicators, that will be used in backtest. Let's use Exponential Moving Average as moving averages, for this TTR
has a function EMA
. It has 2 arguments x
and n
. x
responsible for time-series to filter and n
for size of window. As we want to use spread
as x
and it is not defined at this step, we will use quote
function for making possible to substitute spread
to EMA
when it will be evaluated. args
argument is a list. The first element of it contains function, other elements are function's argumens. as
argument is the name of indicator. This name can be used later in other indicators and rules.
addIndicator(this, args = list(name = EMA, x = quote(spread), n = 20), as = 'fast_ma') addIndicator(this, args = list(name = EMA, x = quote(spread), n = 100), as = 'slow_ma') addIndicator(this, args = list(name = EMA, x = quote(spread), n = 250), as = 'very_slow_ma')
So, we defined indicators, now let's create rules for enter to position and exit from it. Start with enterance to short position. In condition
argument we must write logical expression indicating when condition is satisfied. For our strategy it is fast_ma > slow_ma & Diff(very_slow_ma, 1) < 0
. In this place we do not have to use quote
function, just write a condition. Diff
function returns diffrence between series and lagged version of it. type
argument must be one of "enter" or "exit", specifying enter to position or exiting from it. In our setting it is "enter". As we want to short asset side
argument will be assigned to -1. And the last argument oco
is responsible for environment of the rule. Rules with the same oco
works together. For example, if position was open by the rule with oco
that equals to "long", then position can only be closed by the rule with the same 'oco'.
addRule(this, condition = fast_ma > slow_ma & Diff(very_slow_ma, 1) < 0, type = 'enter', side = -1, oco = 'short' )
Now add rule for exiting from short position when spread will be less then its ema. In that type of rule we don't need side
argument and we should specify right oco
.
addRule(this, condition = !(fast_ma > slow_ma & Diff(very_slow_ma, 1) < 0), type = 'exit', oco = 'short' )
For long position we should add analogious rules. Note that side
is equal to 1 now and we use another oco
.
addRule(this, condition = fast_ma < slow_ma & Diff(very_slow_ma, 1) > 0, type = 'enter', side = 1, oco = 'long' ) addRule(this, condition = !(fast_ma < slow_ma & Diff(very_slow_ma, 1) > 0), type = 'exit', oco = 'long' )
At this step we almost completly describe strategy. It remains only to set amount of money and prices. Money is expressed in the same currency as the used data. Data in multiple currencies is not supported now, but there are ways to overcome this issue. As we will use data in USD we forget about this. For downloading data we will use quantmod
. In this strategy we only use adjusted prices, so in setUserData
the second argument is equal to Ad(data)
.
data <- getSymbols('RSX', from = Sys.Date() - 365 * 10, src = 'yahoo', auto.assign = FALSE) setUserData(this, Ad(data)) setMoney(this, 100000)
Now evaluate model. Evaluation will be done on the server. Client and server connect via ssh. All statistics of evaluated model will be inserted to your object this
after evaluation ends.
performServer(this)
After completion of evaluation you can get results of the strategy. It is important to know how strategy is performing and how stably it can make a profit. When you look at the PnL graph you can understand almost everything about your strategy.
The first plot is the profit and loss graph. It shows the cost of all instruments plus cash on your account at every moment through a period of backtesting.
plotPnL(this, interactive_plot=FALSE)
Also, there is a possibility to return PnL by months in the format of the coloured table.
plotCalendar(this)
The next graph is about how much money your strategy uses.
plotCapital(this, interactive_plot=FALSE)
You can see that there are multiple separated lines. Every line is responsible for a single trade, of course if some time was between opening and closing trades. The next graph was made to demonstrate the performance of every trade in one plot. Each triangle is a trade. If it is green, then the trade is positive, else it is negative. X-axis shows the absolute value of the profit and loss for trades and Y-axis is the maximum amount of loss that was available while a trade was open. And you can change Y-axis to MFE which means the maximum amount of profit that was available while a trade was open.
plotReturns(this, "MAE", interactive_plot=FALSE)
You can get information for each trade in the format of table with the help of getReportTrades
command.
head(getReportTrades(this))
And tables of statistics. Every statistic described in help, just enter ?getReportStrategy
or ?getReportCalendar
. The first report has
results for whole period of backtesting, the second report -- for every year.
getReportStrategy(this) getReportCalendar(this)
The backtester provide tools for brute force optimization. For example, we can optimize window of moving averages. All available fields for optimazation can be seen via ?addDistribution
.
addDistribution(this, component.type = 'indicator', component.label = 'fast_ma', variable = list(n = seq(5, 50, 5)), label = 'fast.n') addDistribution(this, component.type = 'indicator', component.label = 'slow_ma', variable = list(n = seq(40, 150 , 10)), label = 'slow.n')
This function has multiple arguments. Component.type is a place from where variables come. There are multiple places where you can define variables and then iterate over them. Indicators are one of them. Component.label is the name of a specific component. It is needed if there were two or more indicators and you want to point one of them. Variable is a list that stores your distribution. In this example we want to choose size of window for indicators, the name of this argument is n
, so in the list we should write n
as the name of a list's cell. Label is just the name of your distribution. This name can be used in addDistributionConstraint
function. We can see that our distributions intersect and we want slow window to be larger than fast window. So we can write.
addDistributionConstraint(this, expr = fast.n < slow.n)
Also, our indicator has an argument name
. That argument contains a function, that creates an indicator. In addDistribution we can iterate over different functions too. And you can select multiple labels in component.label
argument. So our indicators will have the same functions but with different windows. label
argument can be omitted if it is unnecessary.
addDistribution(this, component.type = 'indicator', component.label = c('fast_ma', 'slow_ma'), variable = list(name = c(EMA, SMA, DEMA)))
To erase old distributions you can call deleteParamset
function.
To start iteration you need to call applyParamsetServer
function. It has start_date
and end_date
arguments, they indicate the date of start and date of the end of backtesting. If they are missed, then backtest will be done on the whole period of downloaded data. Also, there is nsamples
argument. It indicates how many samples from your distributions to select.
applyParamsetServer(this, nsamples = 5, start_date = '2011-01-01', end_date = '2017-01-01', seed = 42)
After completion of procedure you can get results of backtesting this samples. The first part of this table consist of columns with values of distributions, other columns are statistics from getReportStrategy
.
getBacktestResults(this)
With help of library dplyr
you can sort, filter, add new columns to that table and then select best parameters. Also you can use View
function to see table in full screen. The first column is index of sample. You can evaluate strategy with specific index, then parameters by this index will be inserted to your strategy.
performServer(this, paramset.index = 103, start_date = '2011-01-01', end_date = '2017-01-01') getReportStrategy(this) plotPnL(this, interactive_plot=FALSE)
What if you want to check your strategy on different instruments? The package has an answer. Just create a list of models. To do that you can make a function that returns model and insert different data to different objects. Let's experiment with another realization of mean-reverting strategy.
We will use Bollinger's bands. BBands
function from TTR
can construct them. It returns a table with multiple columns. So we will use notation bb[,'pctB']
.
The strategy
open long when: %B > -1 and previous %B < -1 and * %B < 0
close long when: %B > 0 or unrealized profit and loss > 0.0025 * initial money
open short when: %B < 1 and previous %B > 1 and * %B > 0
close short when: %B < 0 or unrealized profit and loss > 0.0025 * initial money
As you can see some expressions do not depend on the current state (path) and some do. For rules that depend on the path we should specify pathwise = TRUE. These rules will be executed on each step of this backtest, others will be executed once per recalculation of spread. For computation of current unrealized profit and loss, we use sum(unrealized_money_last)
. Initial money can be taken from getMoney(this)
. We used Lag
function to get previous values, it is useful for comparing values ahead of or behind the current values.
createMeanRevertingModel <- function(){ this <- modelStrategy() addIndicator(this, args = list(name = BBands, HLC = quote(spread), n = 100, sd = 0.5), as = 'bb') addRule(this, condition = bb[,'pctB'] > -1 & Lag(bb[,'pctB'] < -1, 1) & bb[,'pctB'] < 0, type = 'enter', side = 1, oco = 'long' ) addRule(this, condition = bb[,'pctB'] > 0, type = 'exit', oco = 'long' ) addRule(this, condition = bb[,'pctB'] < 1 & Lag(bb[,'pctB'] > 1, 1) & bb[,'pctB'] > 0 , type = 'enter', side = -1, oco = 'short' ) addRule(this, condition = bb[,'pctB'] < 0, type = 'exit', oco = 'short' ) addRule(this, condition = sum(unrealized_money_last) > 0.0025 * getMoney(this), type = 'exit', pathwise = TRUE, oco = 'short' ) addRule(this, condition = sum(unrealized_money_last) > 0.0025 * getMoney(this), type = 'exit', pathwise = TRUE, oco = 'long' ) setMoney(this, 100000) return(this) } stocks <- c("EWW", "EWC", "EWQ", "EWU", "EWG", "EWI", "RSX", "EWH", "SPY", "MCHI", "EWY", "EWJ", "EWZ", "EWP", "EZA", "EWS", "EWA", "INDA") data <- lapply(stocks, function(x){ getSymbols(x, from = Sys.Date() - 365 * 10, src = 'yahoo', auto.assign = FALSE) %>% Ad }) %>% set_names(stocks) models <- list() for(x in stocks){ models[[x]] <- createMeanRevertingModel() setUserData(models[[x]], data[[x]]) }
performServer(models) getReportStrategy(models) plotPnL(models, interactive_plot=FALSE)
If we have a list of models then we can create a portfolio of models. Backtests will be summed inside it and plotPnL will draw summed profit and loss graph. Most of all reporting, optimization and performing functions working with list and modelPortfolio as well as with modelStrategy.
portfolio <- modelPortfolio(models) performServer(portfolio) getReportStrategy(portfolio) plotPnL(portfolio, interactive_plot=FALSE)
Let's move on. We saw that discussed mean-reverting strategy does not work well with considered assets. We will give a try for a pair of them. We will create a spread. As we mentioned above we should define a table. It will be adjusted prices. Then we should define a function that will calculate coefficients. This function will take this table as an argument and should return the number of assets to buy and sell. These numbers may not be integers, backtester will round them by itself. Also, we should define how many rows of this table to use. To do that there is a function setLookback
. Let's use 2 years of previously seen data, it is approximately 500 points. And we should say to model how often recalculate coefficients. setLookforward
function is responsible for that. Let's use 10 points. By default if the position was opened and we are at the last point of lookforward period, then spread will be calculated with the same coefficients and then will be recalculated after the closing of the position. To turn off this option you can use setIgnorePosition(this, TRUE)
. We will use the previous function for creating the model and just add the rule for creating coefficients.
this <- createMeanRevertingModel() setLookback(this, 500) setLookForward(this, 10) setBeta(this, function(data, ...){ # dots are arguments, that we do not use, #data is a matrix, that includes lookback + 1 rows # Here we define how we will calculate coefficients # We will do that with help of linear regression colnames(data) <- c('x', 'y') # define the model model <- lm(y~x, data.frame(data)) # get coefficients beta <- c(1, -coefficients(model)[2]) # return coefs, program automatically round them, you can cancel this behavior with function setBetasInt(this, FALSE), # but you have to round them by yourself, if you don't do that, the program will work incorrectly return(beta) })
Now we should add data to our model. We can add data in the format of a list of xts tables.
pair <- c("EWH", "EWS") setUserData(this, data[pair])
Let's evaluate the model and see the results.
performServer(this) getReportStrategy(this) plotPnL(this, interactive_plot=FALSE)
Let's backtest such a strategy that buys an asset for half of the money with the lowest value of RSI oscillator and sells an asset for another half of money with the highest value of RSI. RSI will be calculated from adjusted close prices. In the previous example, we used setBeta for calculating coefficients for 10 steps ahead. Now we will use it just for 1 step and will set ignoring position after the end of the period. The rule for this strategy will be as simple as possible -- always TRUE.
this <- modelStrategy() setLookback(this, 1) setLookForward(this, 1) setIgnorePosition(this, TRUE) addRule(this, as = 'long', condition = TRUE, type = 'enter', side = 1, oco = 'long' )
Now it's time to present function addProgramPart
. It serves us for ability to add your code into backtester. There are many places where you can add code. For now, we need a place, where data tables are defined. We need to calculate RSI. In the backtester as mentioned before there are inner variables. One of them is modelD
. It is an environment and it stores tables of data. In the cell data_raw
it has downloaded data that we passed to our strategy object. So we need to create a new data table, we call it "RSI", and make it equal to RSI applied to every column of "data_raw" cell of object modelD
. We will use this table in setBeta
function, so we should write setBetaData(this, 'RSI')
. After that "RSI" table will be passed to setBeta for getting coefficients.
addProgramPart(this, evolution = list( data = quote({ modelD[['RSI']] <- apply(modelD[['data_raw']], 2, RSI, n = 20) }) )) setBetaData(this, 'RSI')
Almost done. It only remains to determine function in setBeta
. And we can make force coefficients to not be in amount of assets, but in amount of money for each asset with help of setBetasByMoney(this, TRUE)
.
setBeta(this, function(data, ...){ # data here is subset of rows of modelD[['RSI']] # we need only the last row data <- as.numeric(tail(data, 1) ) # sort the values. ord <- order(data) # create array of coefficients beta <- numeric(length(data)) # assign 1 to asset with the lowest RSI beta[head(ord, 1)] <- 1 # assign -1 to asset with the highest RSI beta[tail(ord, 1)] <- -1 return(beta) }) setBetasByMoney(this, TRUE)
Also, let's add commissions to our model. It can be done with setCommission
. The second argument of this function is a quoted expression, that depends on pos_change
argument. It is equal to difference in position between the current moment and previous in backtest. For example, we can set commissions to the module of change of positions nominal multiplied by 0.0005. To get current prices of assets you can use data_raw[i,]
. Where data_raw
stores prices in the format of matrix and its values equal to modelD[["data_raw"]]
, but modelD[["data_raw"]]
table is xts a series; i
is the current moment in backtesting.
setCommission(this, quote({ abs(pos_change) * data_raw[i,] * 0.0005 }))
Let's evaluate the model
setUserData(this, data) performServer(this) getReportStrategy(this) plotPnL(this, interactive_plot=FALSE)
This is the end of the introduction. Thank you for reading.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.