Step 1 (order scheduling): build a signal to be tested. It can be build complex as you want, however the output should have 3 outcomes buy, sell, neutral.
Step 2 (order execution): backtest the signal. In this step we consider stop loss, take profit, transaction fees, funding fees…
If the signal build in Step 1 depends on some parameters, it is possible to iterate Step 2. multiple times in order to find the best settings.
# data: datasets with columns date, close, open, high, low, signal# capital: capital used for each trade, e.g. 1000.# stop_loss: stop loss rate (in %). # take_profit: take profit rate (in %)# txs_fee: transaction fee (in %)# funding: daily funding rate (in %, should be specified if futures are used)# trailing: TRUE for using the trailing stop, FALSE for not using.# retracement: retracement rate (in %). Used only if trailing = TRUE. # quiet: if TRUE the function will not display any update messages. backtest_strategy <-function(data, capital =1000, stop_loss =0, take_profit =0, txs_fee =0, funding =0, trailing =FALSE, retracement =2, quiet =FALSE){# initialize the trading list (general information) td <-list() td$capital <- capital # capital used for each trade td$funding <- funding/100# funding rate (in %, should be specified if futures are used) td$txs_fee <- txs_fee/100# transaction fees (in %)# stop loss parameters td$stop_loss <- stop_loss/100# stop loss rate (in %) td$stop_loss_price <-0# stop loss price td$is_used_stop_loss <-ifelse(stop_loss ==0, 0, 1) # 1 if the stop loss is used, 0 otherwise td$is_stop_loss <-FALSE# stop loss condition# take profit parameters td$take_profit <- take_profit/100# take profit rate (in %) td$take_profit_price <-0# take profit price td$is_used_take_profit <-ifelse(take_profit ==0, 0, 1) # 1 if the take profit is used of 0 otherwise td$is_take_profit <-FALSE# take profit condition# trailing parameters td$trailing <- trailing td$retracement <- retracement/100 td$best_price <-0# trade data td$data <-list() # list with all the trades td$last_trade <-list() # contains the last trade td$trade_open <-""# if trade_open = "" no trade are open, can be either "buy" (long) or "sell" (short) n_trade <-1# counter with the number of trades i <-1while(i <=nrow(data)){ df <- data[i, ] df$signal <-as.character(df$signal)if (!quiet){message("Processing: ", round(i/nrow(data)*100, 2), "% \r", appendLF =FALSE)flush.console() }# if trailing = TRUE: update take profit and stop loss if (td$trade_open =="sell"&& (td$last_trade$entry_price/df$open -1) >= td$retracement & trailing) { td$take_profit_price <- td$take_profit_price*(1- td$retracement) td$stop_loss_price <- td$stop_loss_price*(1- td$retracement) } elseif(td$trade_open =="buy"&& (df$open/td$last_trade$entry_price -1) > td$retracement & trailing) { td$take_profit_price <- td$take_profit_price*(1+ td$retracement) td$stop_loss_price <- td$stop_loss_price*(1+ td$retracement) }# check stop loss conditions if (td$trade_open =="sell") { td$is_stop_loss <- df$high >= td$stop_loss_price & td$is_used_stop_loss td$is_take_profit <- df$low <= td$take_profit_price & td$is_used_take_profit df$signal <-ifelse(td$is_stop_loss | td$is_take_profit, "buy", df$signal) } elseif(td$trade_open =="buy") { td$is_stop_loss <- df$low <= td$stop_loss_price & td$is_used_stop_loss td$is_take_profit <- df$high >= td$take_profit_price & td$is_used_take_profit df$signal <-ifelse(td$is_stop_loss | td$is_take_profit, "sell", df$signal) }# open a "buy" trade if no other trade is already opened if (df$signal =="buy"& td$trade_open =="") {# capital spent to enter into the trade df$entry_capital <- td$capital # entry price (close price) df$entry_price <- df$close # transaction fees df$txs_fee <- df$entry_capital*td$txs_fee # quantity bought at entry price df$entry_amount <- df$entry_capital/df$entry_price # trade side df$side <-"long"# store the information in td$last_trade td$last_trade <- df # update general information td$trade_open <-"buy"# set the actual trade to "buy" td$stop_loss_price <- df$entry_price*(1- td$stop_loss) # stop loss price td$take_profit_price <- df$entry_price*(1+ td$take_profit) # take profit price# "buy" trade when a "sell" trade was previously opened } elseif (df$signal =="buy"& td$trade_open =="sell") {# exit price if(td$is_stop_loss){ td$last_trade$exit_price <- td$stop_loss_price td$last_trade$side <-"short (stop loss)" } elseif(td$is_take_profit) { td$last_trade$exit_price <- td$take_profit_price td$last_trade$side <-"short (take profit)" } else { td$last_trade$exit_price <- df$close td$last_trade$side <-"short" } # time to exit the trade in days td$last_trade$ttm <-as.numeric(difftime(df$date, td$last_trade$date, units ="days")) # capital when exit the trade td$last_trade$exit_capital <- td$last_trade$exit_price*td$last_trade$entry_amount# amount bought to exit the short position td$last_trade$exit_amount <- td$last_trade$exit_capital/td$last_trade$exit_price # funding fees paid td$last_trade$funding <- td$last_trade$entry_capital*(exp(td$funding*td$last_trade$ttm) -1) # gross P&L (with fees and funding) td$last_trade$pnl <- td$last_trade$entry_capital - td$last_trade$exit_capital # net P&L (without fees and funding) td$last_trade$net_pnl <- td$last_trade$pnl - td$last_trade$txs_fee - td$last_trade$funding # gROC: return on capital (with fees and funding) td$last_trade$ret <- td$last_trade$pnl/td$last_trade$entry_capital # nROC: net return on capital (without fees and funding) td$last_trade$net_ret <- td$last_trade$net_pnl/td$last_trade$entry_capital# date in which the trade has been closed td$last_trade$date_close <- df$date# store the trade intto the list of trades td$data[[n_trade]] <- td$last_trade # update general informations td$trade_open <-""# set the actual trade to "" td$last_trade <-list() # remove last trade n_trade <- n_trade +1# update trade index td$stop_loss_price <-0# reset the stop loss price td$take_profit_price <-0# reset the take profit price td$is_take_profit <-FALSE td$is_stop_loss <-FALSE }# open a "sell" trade if no other trade is already opened if (df$signal =="sell"& td$trade_open =="") {# capital spent to enter into the trade df$entry_capital <- td$capital # entry price (close price) df$entry_price <- df$close # transaction fees df$txs_fee <- df$entry_capital*td$txs_fee # quantity bought at entry price df$entry_amount <- df$entry_capital/df$entry_price # trade side df$side <-"short"# store the information in td$last_trade td$last_trade <- df # update general information td$trade_open <-"sell"# set the actual trade to "buy" td$stop_loss_price <- df$entry_price*(1+ td$stop_loss) # stop loss price td$take_profit_price <- df$entry_price*(1- td$take_profit) # take profit price# "sell" trade when a "buy" trade was previously opened } elseif (df$signal =="sell"& td$trade_open =="buy") {# exit price if(td$is_stop_loss){ td$last_trade$exit_price <- td$stop_loss_price td$last_trade$side <-"long (stop loss)" } elseif(td$is_take_profit) { td$last_trade$exit_price <- td$take_profit_price td$last_trade$side <-"long (take profit)" } else { td$last_trade$exit_price <- df$close td$last_trade$side <-"long" } # time to exit the trade in days td$last_trade$ttm <-as.numeric(difftime(df$date, td$last_trade$date, units ="days")) # capital when exit the trade td$last_trade$exit_capital <- td$last_trade$exit_price*td$last_trade$entry_amount# amount bought to exit the short position td$last_trade$exit_amount <- td$last_trade$exit_capital/td$last_trade$exit_price # funding fees paid td$last_trade$funding <- td$last_trade$entry_capital*(exp(td$funding*td$last_trade$ttm) -1) # gross P&L (with fees and funding) td$last_trade$pnl <--(td$last_trade$entry_capital - td$last_trade$exit_capital)# net P&L (without fees and funding) td$last_trade$net_pnl <- td$last_trade$pnl - td$last_trade$txs_fee - td$last_trade$funding # gROC: return on capital (with fees and funding) td$last_trade$ret <- td$last_trade$pnl/td$last_trade$entry_capital # nROC: net return on capital (without fees and funding) td$last_trade$net_ret <- td$last_trade$net_pnl/td$last_trade$entry_capital# date in which the trade has been closed td$last_trade$date_close <- df$date# store the trade intto the list of trades td$data[[n_trade]] <- td$last_trade # update general informations td$trade_open <-""# set the actual trade to "" td$last_trade <-list() # remove last trade n_trade <- n_trade +1# update trade index td$stop_loss_price <-0# reset the stop loss price td$take_profit_price <-0# reset the take profit price td$is_take_profit <-FALSE td$is_stop_loss <-FALSE } i <- i +1 }return(td)}