The martingalebot
package provides functions to download
cryptocurrency price data from Binance and to perform backtesting and
parameter optimization for a single pair martingale trading strategy as
implemented by single pair dca bots on 3commas, Pionex, TradeSanta, Mizar, OKX,
Bitget
and others.
There are three different functions to download data from Binance:
get_binance_klines(),
get_binance_klines_from_csv()
,
get_binance_prices_from_csv
. The function
get_binance_klines()
can download candlestick data
directly. The user can specify the trading pair, the start and end time
and the time frame for the candles. For example, to download hourly
candles from ETHUSDT
from the first of January to the first
of March 2023, one could specify:
get_binance_klines(symbol = 'ETHUSDT',
start_time = '2023-01-01',
end_time = '2023-03-01',
interval = '1h')
#> open_time open high low close close_time
#> 1: 2023-01-01 00:00:00 1196.18 1197.43 1193.60 1196.13 2023-01-01 00:59:59
#> 2: 2023-01-01 01:00:00 1196.13 1196.70 1192.72 1194.09 2023-01-01 01:59:59
#> 3: 2023-01-01 02:00:00 1194.09 1196.37 1193.84 1196.02 2023-01-01 02:59:59
#> 4: 2023-01-01 03:00:00 1196.01 1196.74 1194.11 1195.40 2023-01-01 03:59:59
#> 5: 2023-01-01 04:00:00 1195.41 1195.41 1191.71 1194.04 2023-01-01 04:59:59
#> ---
#> 1413: 2023-02-28 20:00:00 1642.06 1643.37 1622.46 1628.01 2023-02-28 20:59:59
#> 1414: 2023-02-28 21:00:00 1628.01 1633.35 1621.54 1627.57 2023-02-28 21:59:59
#> 1415: 2023-02-28 22:00:00 1627.57 1627.58 1596.66 1605.19 2023-02-28 22:59:59
#> 1416: 2023-02-28 23:00:00 1605.19 1613.75 1598.23 1612.45 2023-02-28 23:59:59
#> 1417: 2023-03-01 00:00:00 1612.46 1612.46 1603.00 1605.23 2023-03-01 00:59:59
An advantage of get_binance_klines()
is that it can
download price data up to the current time. A disadvantage is that the
lowest time frame for the candles is 1 minute.
The function get_binance_klines_from_csv()
downloads
candlestick data via csv files from https://data.binance.vision/. The advantage of this
method is that it is faster for large amounts of data and that that the
lowest time frame for the candles is 1 second. A disadvantage is that it
can only download price data up to 1-2 days ago as the csv files on https://data.binance.vision are
only updated once per day.
The function get_binance_prices_from_csv()
also
downloads price data via csv files from https://data.binance.vision/ and thus shares the same
advantages and disadvantages, but it downloads aggregated trades instead
of candlestick data. This allows for an even lower time resolution as it
returns all traded prices of a coin over time. Knowing the exact price
at each point in time is particularly helpful for backtesting martingale
bots with trailing buy and sell orders. The function
get_binance_prices_from_csv()
returns a data frame with
only two columns. See, for example:
get_binance_prices_from_csv('LTCBTC',
start_time = '2023-01-01',
end_time = '2023-02-01', progressbar = F)
#> time price
#> 1: 2023-01-01 00:00:07 0.004243
#> 2: 2023-01-01 00:00:58 0.004244
#> 3: 2023-01-01 00:01:31 0.004244
#> 4: 2023-01-01 00:01:32 0.004243
#> 5: 2023-01-01 00:01:32 0.004243
#> ---
#> 445947: 2023-02-01 23:59:19 0.004250
#> 445948: 2023-02-01 23:59:24 0.004249
#> 445949: 2023-02-01 23:59:30 0.004248
#> 445950: 2023-02-01 23:59:30 0.004247
#> 445951: 2023-02-01 23:59:35 0.004247
Since this function returns very large amounts of data for frequently
traded pairs such as BTCUSDT
, it is, by default,
parallelized and shows a progress bar. Currently, the functions
backtest
and grid_search
are implemented in
such a way that they expect the price data to be in the format as
returned by this function.
To perform a backtest of a martingale bot, we first download price
data for a specific time period and trading pair with
get_binance_prices_from_csv()
and then apply
backtest
to it. The tested martingale bot can be set up
with the following parameters:
base_order_volume
: The size of the base order (in
the quote currency)
first_safety_order_volume
: The size of the first
safety order (in the quote currency)
n_safety_orders
: The maximum number of safety
orders
pricescale
: Price deviation to open safety orders (%
from initial order)
volumescale
: With what number should the funds used
by the last safety order be multiplied?
take_profit
: At what percentage in profit should the
bot close the deal?
stepscale
: With what number should the price
deviation percentage used by the last safety order be
multiplied.
stoploss
: At what percentage of draw down should a
stop-loss be triggered? If set to zero (default), a stop-loss will never
be triggered.
start_asap
: Should new deals be started immediately
after the previous deal was closed. If set to FALSE
new
deals are only started where the logical vector deal_start
in data
is TRUE
.
If we don’t specify any of these arguments, the default parameter
settings will be used. To show the default settings, type
args(backtest)
or go to the help file with
?backtest
.
<- get_binance_prices_from_csv('PYRUSDT',
dat start_time = '2022-12-01',
end_time = '2023-03-01',
progressbar = F)
backtest(data = dat)
#> # A tibble: 1 × 9
#> profit n_trades max_draw_down requir…¹ cover…² down_…³ max_t…⁴ perce…⁵ n_sto…⁶
#> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 16.8 87 10.5 503. 19.2 12.5 9.88 3.01 0
#> # … with abbreviated variable names ¹required_capital, ²covered_deviation,
#> # ³down_tolerance, ⁴max_time, ⁵percent_inactive, ⁶n_stoploss
The backtest function returns the following measures:
profit
: The percentage of profit the bot made during
the tested time period.
n_trades
: The number of deals (cycles) that have
been closed.
max_draw_down
: The biggest draw down in percent hat
occurred.
required_capital
: How much capital is needed to run
a bot with the used parameter settings.
covered deviation
: The percentage price deviation
from the initial order to the last safety order.
down_tolerance
: The percentage price deviation from
the initial order price to the take profit price when all safety orders
are used up.
max_time
: The maximum number of days the bot was in
a stuck position (maximum number of days of being fully
invested).
percent_inactive
: The percentage of time the bot was
in a stuck position. That is, all safety orders were filled and the bot
was fully invested.
n_stoploss
: The number of stop-losses that had been
triggered.
If the argument plot
is TRUE
, an
interactive plot showing the changes in capital and price of the traded
cryptocurrency over time is produced. Buys, sells and stop-losses are
displayed as red, green and blue dots, respectively.
backtest(data = dat, plot = T)
By default, new trades are started as soon as possible. If the price
data set contains a logical vector deal_start
and the
argument start_asap
is set to FALSE
, new deals
are only started where the logical vector deal_start
in
data
is TRUE
. We can add the Relative Strength
Index (RSI) as a deal start condition to the data by using the function
add_rsi
. We can specify the time frame for the candles, the
number of candles that are considered and the cutoff for creating the
logical vector deal_start
. In the following example, new
deals are only started if the hourly RSI is below 30. You can see in the
plot that there are no buys (red dots) at peaks of the price curve
anymore. However, the performance is slightly worse because there are
now less trades in total.
<- add_rsi(dat, time_period = "1 hour", n = 7, cutoff = 30)
dat2 backtest(data = dat2, start_asap = F, plot = T)
To find the best parameter set for a given time period, we can
perform a grid search using the function grid_search
. This
function takes possible values of martingale bot parameters, runs the
function backtest
with each possible combination of these
values and returns the results as a date frame. Each row of this data
frame contains the result of one possible combination of parameters.
Since doing a grid search can be computationally expensive, the
grid_search
function is parallelized by default.
By default, grid_search
uses a broad range of
parameters. For example, for n_safety_orders
, values
between 6 and 16 in steps of 2 are tested (see
args(grid_search)
for default ranges of parameters).
However, we could also use, for, examples, values between 4 and 6, by
explicitly specifying it:
<- grid_search(data = dat, n_safety_orders = 4:6, progressbar = F)
res
res#> # A tibble: 532 × 19
#> profit n_trades max_draw_down requi…¹ cover…² down_…³ max_t…⁴ perce…⁵ n_sto…⁶
#> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <int>
#> 1 37.7 88 14.0 119. 15 8.01 24.0 33.8 0
#> 2 37.7 88 14.0 119. 15 8.01 24.0 33.8 0
#> 3 37.7 88 14.0 119. 15 8.01 24.0 33.8 0
#> 4 37.7 88 14.0 119. 15 8.01 24.0 33.8 0
#> 5 35.2 163 16.3 70 18 8 24.0 29.4 0
#> 6 35.2 163 16.3 70 18 8 24.0 29.4 0
#> 7 35.2 163 16.3 70 18 8 24.0 29.4 0
#> 8 35.2 163 16.3 70 18 8 24.0 29.4 0
#> 9 34.8 226 14.1 109. 15.6 8.50 24.0 32.5 0
#> 10 34.8 226 14.1 109. 15.6 8.50 24.0 32.5 0
#> # … with 522 more rows, 10 more variables: base_order_volume <dbl>,
#> # first_safety_order_volume <dbl>, n_safety_orders <int>, pricescale <dbl>,
#> # volumescale <dbl>, take_profit <dbl>, stepscale <dbl>, start_asap <lgl>,
#> # stoploss <dbl>, compound <lgl>, and abbreviated variable names
#> # ¹required_capital, ²covered_deviation, ³down_tolerance, ⁴max_time,
#> # ⁵percent_inactive, ⁶n_stoploss
The rows of the returned data frame are ordered by the column
profit
. In the first row, we see the set of parameters that
led to highest profit. In order to plot the best performing parameter
set, we can pass the values of the first row in res
as
arguments to backtest
by using the function
do.call
.
do.call(backtest, c(as.list(res[1,]), list(data = dat, plot = T)))
Alternatively, we could do this more elegantly with the
tidyverse
package.
library(tidyverse)
grid_search(data = dat, n_safety_orders = 4:6, progressbar = F) %>%
slice(1) %>%
pmap_df(backtest, data = dat, plot = T)
Instead of picking the most profitable parameter constellation, we
could also pick the one with the best compromise between
profit
and max_draw_down
by replacing the
command slice(1)
with
slice_max(profit - max_draw_down)
.
It should be noted that the grid_search
function also
has the following arguments that allow to restrict the search space:
min_covered_deviation
: the minimum percentage price
deviation from the initial order to the last safety order a given
parameter combination must have. Parameter combinations that have a
covered price deviation less than this value are discarded and not
tested.
min_down_tolerance
: the minimum price down tolerance
(i.e. percentage price deviation from the initial order price to the
take profit price when all safety orders are filled) a given parameter
combination must have. Parameter combinations that have a price down
tolerance less than this value are discarded and not tested.
max_required_capital
: the maximum capital a given
parameter combination can require. Parameters that require more capital
than this value are discarded and not tested.
This can be handy because we might only want to search for optimal parameter combinations within a set of parameters that have minimum “down tolerance” and thus have certain robustness against sudden price drops. In this case, it would be a waste of computation time if we tested all possible combinations of parameters.
Instead of performing a grid search, we can also search for the best parameter combination with optimization algorithms. For example, we could use a technique called Simulated Annealing:
library(optimization)
#Define the optimization function:
<- function(x) {
optim_fun <- backtest(n_safety_orders = x[1], pricescale = x[2],
y volumescale = x[3], take_profit = x[4], stepscale = x[5],
data = dat)
#Note that we return minus the profit because the optimization algorithm
#expects a value that we want to reduce as much as possible
- y$profit
}
#Define lower and upper bound and start values of parameters
<- c(6, 1, 1, 1, 0.8)
lower <- c(16, 3.5, 2, 3.5, 1.1)
upper <- c(8, 2.4, 1.5, 2.4, 1)
start
#Perform optimization
<- optim_sa(optim_fun, start = start, lower = lower, upper = upper,
res control = list(nlimit = 200))$par
#Plot the best parameter combination found by the optimization algorithm:
backtest(n_safety_orders = round(res[1]), pricescale = res[2],
volumescale = res[3], take_profit = res[4], stepscale = res[5],
data = dat, plot = T)
Instead of optimizing the profit, we could also optimize another
criterion. For example, we could seek for the best compromise between
profit
, max_draw_down
,
time_inactive
, and down_tolerance
by defining
the optimization function like this:
<- function(x) {
optim_fun <- backtest(n_safety_orders = x[1], pricescale = x[2],
y volumescale = x[3], take_profit = x[4], stepscale = x[5],
data = dat)
with(y, max_draw_down + time_inactive - down_tolerance - profit)
}
Here, all measures are weighted equally. However, we could also give them different weights by multiplying them with different numbers or we could apply various transformation functions to them.
We could also perform an optimization using the genetic algorithm
from package GA
. Here’s an example:
library(GA)
#Define the optimization function:
<- function(x) {
optim_fun <- backtest(n_safety_orders = x[1], pricescale = x[2],
y volumescale = x[3], take_profit = x[4], stepscale = x[5],
data = dat)
#Note that this algorithm expects a value that we want to increase as much
#as possible. Therefore, we return the positive profit here:
$profit
y
}
#Define lower and upper bound of parameters
<- c(6, 1, 1, 1, 0.8)
lower <- c(16, 3.5, 2, 3.5, 1.1)
upper
#Perform optimization
<- ga(type = 'real-valued', optim_fun, lower = lower,
res upper = upper, maxiter = 200)@solution
#Plot the best parameter combination found by the optimization algorithm:
backtest(n_safety_orders = round(res[1]), pricescale = res[2],
volumescale = res[3], take_profit = res[4], stepscale = res[5],
data = dat, plot = T)
In the previous examples, we used the same data for training and testing the algorithm. However, this most likely resulted in over-fitting and over-optimistic performance estimation. A better strategy would be to strictly separate testing and learning by using cross-validation.
We first download a longer time period of price data so that we have more data for training and testing:
<- get_binance_prices_from_csv("ATOMUSDT",
dat start_time = '2022-01-01',
end_time = '2023-03-03', progressbar = F)
Next, we split our data into many different test and training time
periods. We can use the function create_timeslices
to
create start and end times of the different splits. It has the following
4 arguments.
train_months
The duration of the training periods in
months
test_month
The duration of the testing periods in
months
shift_months
The number of months pairs of test and
training periods are shifted to each other. The smaller this number the
more pairs of test and training data sets can be created
data
The price data set
For example, if we want to use 4 months for training, 4 months for testing and create training and testing periods every month, we could specify:
<- create_timeslices(train_months = 4, test_months = 4,
slices shift_months = 1, data = dat)
slices#> # A tibble: 7 × 5
#> period start_train end_train start_test
#> <dbl> <dttm> <dttm> <dttm>
#> 1 1 2022-01-01 00:00:02 2022-05-02 19:00:02 2022-05-02 19:00:02
#> 2 2 2022-01-31 10:30:02 2022-06-02 05:30:02 2022-06-02 05:30:02
#> 3 3 2022-03-02 21:00:02 2022-07-02 16:00:02 2022-07-02 16:00:02
#> 4 4 2022-04-02 08:30:02 2022-08-02 02:30:02 2022-08-02 02:30:02
#> 5 5 2022-05-02 19:00:02 2022-09-01 13:00:02 2022-09-01 13:00:02
#> 6 6 2022-06-02 05:30:02 2022-10-01 23:30:02 2022-10-01 23:30:02
#> 7 7 2022-07-02 16:00:02 2022-11-01 09:00:02 2022-11-01 09:00:02
#> # … with 1 more variable: end_test <dttm>
Note that these time periods are partially overlapping. If we want to
have non-overlapping time periods, we could specify
shift_months = 4
.
We can now perform cross-validation by iterating over the rows of
slices
. At each iteration, we perform a grid search for the
best parameter combination using the training data and then apply this
parameter combination to the test data. For simplicity, we only return
the final performance in the test data.
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
#Get test and training data of the present row / iteration
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data #Find the best parameter combination in the training data
<- grid_search(data = train_data, progressbar = F)[1,]
best #Apply this parameter combination to the test data
pmap_df(best, backtest, data = test_data)
})#> # A tibble: 7 × 11
#> # Groups: start_test [7]
#> start_test end_test profit n_trades max_draw_down requi…¹
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 -30.3 5 68.0 406.
#> 2 2022-06-02 05:30:02 2022-10-01 23:30:02 21.8 192 30.2 4685.
#> 3 2022-07-02 16:00:02 2022-11-01 09:00:02 26.4 232 11.0 1402.
#> 4 2022-08-02 02:30:02 2022-12-01 19:30:02 -2.91 335 32.6 2763.
#> 5 2022-09-01 13:00:02 2023-01-01 06:00:02 -7.95 80 32.4 4461.
#> 6 2022-10-01 23:30:02 2023-01-31 16:30:02 10.4 28 39.0 170
#> 7 2022-11-01 09:00:02 2023-03-03 03:00:02 -0.951 15 39.8 170
#> # … with 5 more variables: covered_deviation <dbl>, down_tolerance <dbl>,
#> # max_time <dbl>, percent_inactive <dbl>, n_stoploss <int>, and abbreviated
#> # variable name ¹required_capital
We can see that only 3 of the 7 tested time periods were in profit.
This is because we only maximized profitability during training, which
likely led to the selection of “aggressive” or risky strategies that
work well in the training set but poorly in the test set due to little
robustness against sudden price drops. This is illustrated by the the
relatively small price down tolerance, which varied between 8.2 and 10.9
% for the selected parameter combinations (see column
down_tolerance
in the above table). A potential solution to
this problem is therefore to restrict the search space to those
parameter combinations that have a minimum price down tolerance of, for
example, 12 %. We can do this by using the argument
min_down_tolerance
of the grid_search
function:
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data <- grid_search(data = train_data, min_down_tolerance = 12,
best progressbar = F)[1,]
pmap_df(best, backtest, data = test_data)
})#> # A tibble: 7 × 11
#> # Groups: start_test [7]
#> start_test end_test profit n_tra…¹ max_d…² requi…³ cover…⁴
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 -29.0 5 67.4 602. 20.1
#> 2 2022-06-02 05:30:02 2022-10-01 23:30:02 14.5 240 28.8 5430. 14.6
#> 3 2022-07-02 16:00:02 2022-11-01 09:00:02 14.5 236 8.01 5430. 14.6
#> 4 2022-08-02 02:30:02 2022-12-01 19:30:02 21.0 534 8.38 709. 17.6
#> 5 2022-09-01 13:00:02 2023-01-01 06:00:02 15.4 174 10.0 5430. 14.6
#> 6 2022-10-01 23:30:02 2023-01-31 16:30:02 14.9 43 36.6 170 28.8
#> 7 2022-11-01 09:00:02 2023-03-03 03:00:02 3.18 35 36.7 170 28.8
#> # … with 4 more variables: down_tolerance <dbl>, max_time <dbl>,
#> # percent_inactive <dbl>, n_stoploss <int>, and abbreviated variable names
#> # ¹n_trades, ²max_draw_down, ³required_capital, ⁴covered_deviation
Except for the first time period, all time periods are now in profit. However, this more conservative strategy came with the price of slightly lower profits in the second and third time periods.
Alternatively, we could also select the most profitable parameter combination only among those combinations that had little draw down and did not result in “red bags” for extended periods of time. For example, to select the most profitable parameter combination among those combinations that had no more than 30% draw down and that were no longer than 3% of the time fully invested in the training period, we could do:
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data <- grid_search(data = train_data, progressbar = F) %>%
best filter(max_draw_down < 30 & percent_inactive < 3) %>%
slice(1)
pmap_df(best, backtest, data = test_data)
})#> # A tibble: 7 × 11
#> # Groups: start_test [7]
#> start_test end_test profit n_tra…¹ max_d…² requi…³ cover…⁴
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 -27.6 5 66.6 1375. 20.8
#> 2 2022-06-02 05:30:02 2022-10-01 23:30:02 17.7 546 10.5 4461. 14.3
#> 3 2022-07-02 16:00:02 2022-11-01 09:00:02 18.7 361 10.2 4461. 13.4
#> 4 2022-08-02 02:30:02 2022-12-01 19:30:02 20.5 158 5.80 4461. 19.5
#> 5 2022-09-01 13:00:02 2023-01-01 06:00:02 -17.7 256 31.8 5430. 16
#> 6 2022-10-01 23:30:02 2023-01-31 16:30:02 11.7 69 32.1 170 41.6
#> 7 2022-11-01 09:00:02 2023-03-03 03:00:02 4.23 119 34.0 130 21.6
#> # … with 4 more variables: down_tolerance <dbl>, max_time <dbl>,
#> # percent_inactive <dbl>, n_stoploss <int>, and abbreviated variable names
#> # ¹n_trades, ²max_draw_down, ³required_capital, ⁴covered_deviation
Another option would be to select the parameter combination that
maximizes a combination of measures, such as
profit - max_draw_down - percent_inactive
.
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data <- grid_search(data = train_data, progressbar = F) %>%
best slice_max(profit - max_draw_down - percent_inactive)
pmap_df(best, backtest, data = test_data)
})#> # A tibble: 28 × 11
#> # Groups: start_test, end_test [7]
#> start_test end_test profit n_trades max_draw_down requi…¹
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 -21.0 5 64.4 2763.
#> 2 2022-05-02 19:00:02 2022-09-01 13:00:02 -4.52 162 36.2 2763.
#> 3 2022-05-02 19:00:02 2022-09-01 13:00:02 -22.3 155 43.3 2763.
#> 4 2022-05-02 19:00:02 2022-09-01 13:00:02 -5.78 127 49.9 2763.
#> 5 2022-06-02 05:30:02 2022-10-01 23:30:02 21.8 192 30.2 4685.
#> 6 2022-06-02 05:30:02 2022-10-01 23:30:02 -6.17 239 26.2 4685.
#> 7 2022-06-02 05:30:02 2022-10-01 23:30:02 -16.7 216 31.6 4685.
#> 8 2022-06-02 05:30:02 2022-10-01 23:30:02 21.8 192 30.2 4685.
#> 9 2022-07-02 16:00:02 2022-11-01 09:00:02 20.1 358 10.8 1375.
#> 10 2022-07-02 16:00:02 2022-11-01 09:00:02 20.1 358 10.8 1375.
#> # … with 18 more rows, 5 more variables: covered_deviation <dbl>,
#> # down_tolerance <dbl>, max_time <dbl>, percent_inactive <dbl>,
#> # n_stoploss <int>, and abbreviated variable name ¹required_capital
Instead of performing a grid search, we could also search for the best parameter combination using an optimiziation algorithm, such as simulated annealing:
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data
#Define the optimization function:
<- function(x) {
optim_fun <- backtest(n_safety_orders = x[1], pricescale = x[2],
y volumescale = x[3], take_profit = x[4], stepscale = x[5],
data = train_data)
#Note that we have to change signs here as compared to above
with(y, -profit + max_draw_down + percent_inactive)
}
#Define lower and upper bound and start values of parameters
<- c(6, 1, 1, 1, 0.8)
lower <- c(16, 3.5, 2, 3.5, 1.1)
upper <- c(8, 2.4, 1.5, 2.4, 1)
start
#Perform optimization
<- optim_sa(optim_fun, start = start, lower = lower, upper = upper,
res control = list(nlimit = 200))$par
#Apply best parameter combination to test data
backtest(n_safety_orders = round(res[1]), pricescale = res[2],
volumescale = res[3], take_profit = res[4], stepscale = res[5],
data = test_data)
})#> # A tibble: 7 × 11
#> # Groups: start_test [7]
#> start_test end_test profit n_trades max_draw_down requi…¹
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 -34.2 23 69.7 862.
#> 2 2022-06-02 05:30:02 2022-10-01 23:30:02 0.590 166 0.212 68091.
#> 3 2022-07-02 16:00:02 2022-11-01 09:00:02 7.53 339 3.35 2876.
#> 4 2022-08-02 02:30:02 2022-12-01 19:30:02 21.0 304 7.17 22464.
#> 5 2022-09-01 13:00:02 2023-01-01 06:00:02 5.16 105 6.27 3173.
#> 6 2022-10-01 23:30:02 2023-01-31 16:30:02 4.66 39 42.8 490.
#> 7 2022-11-01 09:00:02 2023-03-03 03:00:02 8.17 43 34.3 109.
#> # … with 5 more variables: covered_deviation <dbl>, down_tolerance <dbl>,
#> # max_time <dbl>, percent_inactive <dbl>, n_stoploss <int>, and abbreviated
#> # variable name ¹required_capital
Similarly, we could apply the genetic algorithm from package
GA
:
library(tidyverse)
%>%
slices group_by(start_test, end_test) %>%
summarise({
<- filter(dat, between(time, start_train, end_train))
train_data <- filter(dat, between(time, start_test, end_test))
test_data
#Define the optimization function:
<- function(x) {
optim_fun <- backtest(n_safety_orders = x[1], pricescale = x[2],
y volumescale = x[3], take_profit = x[4], stepscale = x[5],
data = train_data)
with(y, profit - max_draw_down - percent_inactive)
}
#Define lower and upper bound of parameters
<- c(6, 1, 1, 1, 0.8)
lower <- c(16, 3.5, 2, 3.5, 1.1)
upper
#Perform optimization
<- ga(type = 'real-valued', optim_fun, lower = lower,
res upper = upper, maxiter = 200, monitor = F)@solution
#Apply best parameter combination to test data
backtest(n_safety_orders = round(res[1]), pricescale = res[2],
volumescale = res[3], take_profit = res[4], stepscale = res[5],
data = test_data)
})#> # A tibble: 7 × 11
#> # Groups: start_test [7]
#> start_test end_test profit n_trades max_draw_d…¹ requi…²
#> <dttm> <dttm> <dbl> <int> <dbl> <dbl>
#> 1 2022-05-02 19:00:02 2022-09-01 13:00:02 0.00460 6 0.106 56847.
#> 2 2022-06-02 05:30:02 2022-10-01 23:30:02 15.3 215 28.2 9781.
#> 3 2022-07-02 16:00:02 2022-11-01 09:00:02 15.2 286 8.06 6744.
#> 4 2022-08-02 02:30:02 2022-12-01 19:30:02 -2.88 255 31.7 28078.
#> 5 2022-09-01 13:00:02 2023-01-01 06:00:02 34.1 148 9.19 4840.
#> 6 2022-10-01 23:30:02 2023-01-31 16:30:02 0.364 40 43.9 574.
#> 7 2022-11-01 09:00:02 2023-03-03 03:00:02 7.91 34 33.5 160.
#> # … with 5 more variables: covered_deviation <dbl>, down_tolerance <dbl>,
#> # max_time <dbl>, percent_inactive <dbl>, n_stoploss <int>, and abbreviated
#> # variable names ¹max_draw_down, ²required_capital