Load Data

library( QuantTools )
# load ticks data set
data( ticks )
ticks
##                         time   price volume      id
##       1: 2016-01-19 09:30:00  98.390    100       1
##       2: 2016-01-19 09:30:01  98.350    100       2
##       3: 2016-01-19 09:30:01  98.370    100       3
##       4: 2016-01-19 09:30:01  98.360    100       4
##       5: 2016-01-19 09:30:01  98.370    114       5
##      ---                                           
## 2310006: 2016-09-14 10:42:08 111.160    100 2310006
## 2310007: 2016-09-14 10:42:10 111.150    100 2310007
## 2310008: 2016-09-14 10:42:10 111.150    200 2310008
## 2310009: 2016-09-14 10:42:11 111.130    200 2310009
## 2310010: 2016-09-14 10:42:11 111.135    100 2310010

Note: Wide tables or code blocks can be read with ease by holding Shift and scrolling a mouse wheel.

Define Strategy

Put the following sma_crossover.cpp file into working directory.

// [[Rcpp::plugins(cpp11)]]
// [[Rcpp::depends(QuantTools)]]
#include <Rcpp.h>
#include "BackTest.h"

// [[Rcpp::export]]
Rcpp::List sma_crossover(
    Rcpp::DataFrame ticks,
    Rcpp::List parameters,
    Rcpp::List options,
    bool fast = false
  ) {

  int    fastPeriod = parameters["period_fast" ];
  int    slowPeriod = parameters["period_slow" ];
  int    timeFrame  = parameters["timeframe"   ];

  // define strategy states
  enum class ProcessingState{ LONG, FLAT, SHORT };
  ProcessingState state = ProcessingState::FLAT;
  int idTrade = 1;

  // initialize indicators
  Sma smaFast( fastPeriod );
  Sma smaSlow( slowPeriod );
  Crossover crossover;

  // initialize Processor
  Processor bt( timeFrame );
  // set options
  bt.SetOptions( options );
  // if trading hours not set then isTradingHours set true
  bool isTradingHours = not bt.IsTradingHoursSet();

  // define market open/close events
  bt.onMarketOpen  = [&]() {
    // allow trading
    isTradingHours = true;

  };
  bt.onMarketClose = [&]() {
    // forbid trading and close open positions
    isTradingHours = false;
    if( state == ProcessingState::SHORT ) {
      bt.SendOrder(
        new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "close short (EOD)", idTrade++ )
      );
    }
    if( state == ProcessingState::LONG ) {
      bt.SendOrder(
        new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "close long (EOD)", idTrade++ )
      );
    }
    state = ProcessingState::FLAT;

  };

  // define what to do when new candle is formed
  bt.onCandle = [&]( Candle candle ) {

    // add values to indicators
    smaSlow.Add( candle.close );
    smaFast.Add( candle.close );

    // if moving averages not formed yet do nothing
    if( not smaFast.IsFormed() or not smaSlow.IsFormed() ) return;

    // update crossover
    crossover.Add( std::pair< double, double >( smaFast.GetValue(), smaSlow.GetValue() ) );

    if( not bt.CanTrade()  ) return;
    if( not isTradingHours ) return;

    // if smaFast is above smaSlow and current state is not long
    if( crossover.IsAbove() and state != ProcessingState::LONG ) {
      // if strategy has no position then buy
      if( state == ProcessingState::FLAT ) {
        bt.SendOrder(
          new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "long", idTrade )
        );
      }
      // if strategy has short position then close short position and open long position
      if( state == ProcessingState::SHORT ) {
        bt.SendOrder(
          new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "close short", idTrade++ )
        );
        bt.SendOrder(
          new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "reverse short", idTrade )
        );
      }
      // set state to long
      state = ProcessingState::LONG;

    }
    // if smaFast is below smaSlow and current state is not short
    if( crossover.IsBelow() and state != ProcessingState::SHORT ) {
      // if strategy has no position then sell
      if( state == ProcessingState::FLAT ) {
        bt.SendOrder(
          new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "short", idTrade )
        );
      }
      // if strategy has long position then close long position and open short position
      if( state == ProcessingState::LONG ) {
        bt.SendOrder(
          new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "close long", idTrade++ )
        );
        bt.SendOrder(
          new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "reverse long", idTrade )
        );
      }
      // set state to short
      state = ProcessingState::SHORT;

    }

  };

  // run back test on tick data
  bt.Feed( ticks );

  // test summary
  Rcpp::List summary = bt.GetSummary();

  // if fast return only summary
  if( fast ) return summary;

  // combine candles and indicators history
  Rcpp::List indicators = ListBuilder().AsDataTable()
    .Add( bt.GetCandles()                                )
    .Add( "sma_slow", smaSlow.GetHistory()               )
    .Add( "sma_fast", smaFast.GetHistory()               )
    .Add( "pnl"     , bt.GetOnCandleMarketValueHistory() )
    .Add( "drawdown", bt.GetOnCandleDrawDownHistory()    );

  // return back test summary, trades, orders and candles/indicators
  return ListBuilder()
    .Add( "summary"          , summary                              )
    .Add( "trades"           , bt.GetTrades()                       )
    .Add( "orders"           , bt.GetOrders()                       )
    .Add( "indicators"       , indicators                           )
    .Add( "daily_performance", bt.GetOnDayClosePerformanceHistory() );

}

Note: It is good practice to have .cpp file per strategy. It is perfect to have a package of your strategies so you don’t need to recompile them every time you restart an R session.

Compile it

# file above can be found in examples directory
strategy_cpp_file = system.file( package = 'QuantTools', 'examples/sma_crossover.cpp' )
Rcpp::sourceCpp( strategy_cpp_file )

Run Backtest

# set strategy parameters
parameters = data.table(
  period_fast = 50,
  period_slow = 30,
  timeframe   = 60 # seconds
)
# set options, see 'Options' section
options = list(
  cost = list( tradeAbs = -0.01 ),
  latency = 0.1 # 100 milliseconds
)
# see how fast back testing done on over 2 millin ticks
system.time( { test = sma_crossover( ticks, parameters, options ) } )
##    user  system elapsed 
##   0.074   0.008   0.084
# lets limit testing to one date
interval = '2016-09-08'
test = sma_crossover( ticks[ time %bw% interval ], parameters, options )
test
## $summary
##    period_fast period_slow timeframe                from                  to days_tested days_traded n_per_day  n n_long n_short n_win n_loss pct_win pct_loss avg_win avg_loss avg_pnl  win loss  pnl max_dd        max_dd_start max_dd_end max_dd_length sharpe sortino r_squared avg_dd
## 1:          50          30        60 2016-09-08 09:30:00 2016-09-08 15:59:59           1           1        11 11      5       6     6      5   54.55    45.45   10.23   -11.93    0.16 0.61 -0.6 0.02  -0.85 2016-09-08 14:00:45       <NA>           NaN    NaN     Inf       NaN  -0.68
## 
## $trades
##     id_trade id_sent id_enter id_exit           time_sent          time_enter           time_exit  side price_enter price_exit   pnl mtm_min mtm_max  cost    pnl_rel mtm_min_rel mtm_max_rel   cost_rel  state
##  1:        1    7790     7794    8101 2016-09-08 10:41:00 2016-09-08 10:41:07 2016-09-08 10:45:04 short      105.92     105.77  0.13  -0.010   0.180 -0.02  12.273414  -0.9441088   16.993958 -1.8882175 closed
##  2:        2    8097     8101   10310 2016-09-08 10:45:01 2016-09-08 10:45:04 2016-09-08 11:23:19  long      105.77     105.97  0.18  -0.120   0.330 -0.02  17.018058 -11.3453720   31.199773 -1.8908953 closed
##  3:        3   10305    10310   11173 2016-09-08 11:23:17 2016-09-08 11:23:19 2016-09-08 11:48:59 short      105.97     106.00 -0.05  -0.030   0.190 -0.02  -4.718317  -2.8309899   17.929603 -1.8873266 closed
##  4:        4   11171    11173   11496 2016-09-08 11:48:19 2016-09-08 11:48:59 2016-09-08 12:02:05  long      106.00     106.05  0.03  -0.030   0.125 -0.02   2.830189  -2.8301887   11.792453 -1.8867925 closed
##  5:        5   11493    11496   12339 2016-09-08 12:02:04 2016-09-08 12:02:05 2016-09-08 12:27:06 short      106.05     105.80  0.23  -0.050   0.285 -0.02  21.687883  -4.7147572   26.874116 -1.8859029 closed
##  6:        6   12337    12339   13508 2016-09-08 12:27:04 2016-09-08 12:27:06 2016-09-08 13:11:07  long      105.80     105.65 -0.17  -0.250   0.000 -0.02 -16.068053 -23.6294896    0.000000 -1.8903592 closed
##  7:        7   13504    13508   13832 2016-09-08 13:11:03 2016-09-08 13:11:07 2016-09-08 13:24:19 short      105.65     105.58  0.05  -0.010   0.100 -0.02   4.732608  -0.9465215    9.465215 -1.8930431 closed
##  8:        8   13829    13832   17152 2016-09-08 13:24:10 2016-09-08 13:24:19 2016-09-08 14:17:07  long      105.58     105.48 -0.12  -0.330   0.330 -0.02 -11.365789 -31.2559197   31.255920 -1.8942982 closed
##  9:        9   17142    17152   19139 2016-09-08 14:17:02 2016-09-08 14:17:07 2016-09-08 15:05:14 short      105.48     105.65 -0.19  -0.340   0.020 -0.02 -18.012893 -32.2335988    1.896094 -1.8960940 closed
## 10:       10   19124    19139   19351 2016-09-08 15:05:12 2016-09-08 15:05:14 2016-09-08 15:12:25  long      105.65     105.57 -0.10  -0.120   0.060 -0.02  -9.465215 -11.3582584    5.679129 -1.8930431 closed
## 11:       11   19339    19351   19577 2016-09-08 15:12:12 2016-09-08 15:12:25 2016-09-08 15:20:05 short      105.57     105.52  0.03  -0.035   0.060 -0.02   2.841716  -3.3153358    5.683433 -1.8944776 closed
## 12:       12   19565    19577      NA 2016-09-08 15:20:00 2016-09-08 15:20:05                <NA>  long      105.52         NA    NA  -0.190   0.090 -0.01         NA -18.0060652    8.529189 -0.9476876 opened
## 
## $orders
##     id_trade id_sent id_processed           time_sent      time_processed price_init price_exec side   type    state       comment
##  1:        1    7790         7794 2016-09-08 10:41:00 2016-09-08 10:41:07         NA     105.92 sell market executed         short
##  2:        1    8097         8101 2016-09-08 10:45:01 2016-09-08 10:45:04         NA     105.77  buy market executed   close short
##  3:        2    8097         8101 2016-09-08 10:45:01 2016-09-08 10:45:04         NA     105.77  buy market executed reverse short
##  4:        2   10305        10310 2016-09-08 11:23:17 2016-09-08 11:23:19         NA     105.97 sell market executed    close long
##  5:        3   10305        10310 2016-09-08 11:23:17 2016-09-08 11:23:19         NA     105.97 sell market executed  reverse long
##  6:        3   11171        11173 2016-09-08 11:48:19 2016-09-08 11:48:59         NA     106.00  buy market executed   close short
##  7:        4   11171        11173 2016-09-08 11:48:19 2016-09-08 11:48:59         NA     106.00  buy market executed reverse short
##  8:        4   11493        11496 2016-09-08 12:02:04 2016-09-08 12:02:05         NA     106.05 sell market executed    close long
##  9:        5   11493        11496 2016-09-08 12:02:04 2016-09-08 12:02:05         NA     106.05 sell market executed  reverse long
## 10:        5   12337        12339 2016-09-08 12:27:04 2016-09-08 12:27:06         NA     105.80  buy market executed   close short
## 11:        6   12337        12339 2016-09-08 12:27:04 2016-09-08 12:27:06         NA     105.80  buy market executed reverse short
## 12:        6   13504        13508 2016-09-08 13:11:03 2016-09-08 13:11:07         NA     105.65 sell market executed    close long
## 13:        7   13504        13508 2016-09-08 13:11:03 2016-09-08 13:11:07         NA     105.65 sell market executed  reverse long
## 14:        7   13829        13832 2016-09-08 13:24:10 2016-09-08 13:24:19         NA     105.58  buy market executed   close short
## 15:        8   13829        13832 2016-09-08 13:24:10 2016-09-08 13:24:19         NA     105.58  buy market executed reverse short
## 16:        8   17142        17152 2016-09-08 14:17:02 2016-09-08 14:17:07         NA     105.48 sell market executed    close long
## 17:        9   17142        17152 2016-09-08 14:17:02 2016-09-08 14:17:07         NA     105.48 sell market executed  reverse long
## 18:        9   19124        19139 2016-09-08 15:05:12 2016-09-08 15:05:14         NA     105.65  buy market executed   close short
## 19:       10   19124        19139 2016-09-08 15:05:12 2016-09-08 15:05:14         NA     105.65  buy market executed reverse short
## 20:       10   19339        19351 2016-09-08 15:12:12 2016-09-08 15:12:25         NA     105.57 sell market executed    close long
## 21:       11   19339        19351 2016-09-08 15:12:12 2016-09-08 15:12:25         NA     105.57 sell market executed  reverse long
## 22:       11   19565        19577 2016-09-08 15:20:00 2016-09-08 15:20:05         NA     105.52  buy market executed   close short
## 23:       12   19565        19577 2016-09-08 15:20:00 2016-09-08 15:20:05         NA     105.52  buy market executed reverse short
##     id_trade id_sent id_processed           time_sent      time_processed price_init price_exec side   type    state       comment
## 
## $indicators
##                     time    open   high     low   close volume    id sma_slow sma_fast           pnl     drawdown
##   1: 2016-09-08 09:31:00 107.110 107.26 107.030 107.160  48933   212       NA       NA  0.0000000000  0.000000000
##   2: 2016-09-08 09:32:00 107.160 107.18 106.640 106.800 179364   478       NA       NA  0.0000000000  0.000000000
##   3: 2016-09-08 09:33:00 106.800 106.96 106.760 106.910  51495   799       NA       NA  0.0000000000  0.000000000
##   4: 2016-09-08 09:34:00 106.900 106.98 106.830 106.980  37592   942       NA       NA  0.0000000000  0.000000000
##   5: 2016-09-08 09:35:00 106.950 107.06 106.900 106.900  48085  1120       NA       NA  0.0000000000  0.000000000
##  ---                                                                                                             
## 384: 2016-09-08 15:55:00 105.455 105.47 105.370 105.390  25467 21405 105.4755 105.5062 -0.0010566339 -0.007957804
## 385: 2016-09-08 15:56:00 105.390 105.39 105.340 105.345  35934 21530 105.4693 105.5001 -0.0014830933 -0.008384263
## 386: 2016-09-08 15:57:00 105.345 105.41 105.335 105.410  32188 21633 105.4663 105.4971 -0.0008670963 -0.007768267
## 387: 2016-09-08 15:58:00 105.445 105.47 105.420 105.420  16367 21718 105.4640 105.4941 -0.0007723276 -0.007673498
## 388: 2016-09-08 15:59:00 105.430 105.49 105.430 105.460  42548 21895 105.4637 105.4921 -0.0003932525 -0.007294423
## 
## $daily_performance
##          date       return          pnl     drawdown      avg_pnl n_per_day
## 1: 2016-09-08 8.059129e-05 8.059129e-05 -0.006820579 1.594182e-05        11

Visualize Results

Static

Here is code to reproduce this plot:

# plot result
layout( matrix( 1:2, ncol = 1 ), height = c( 2, 1 ) )
# 1
par( mar = c( 0, 4, 2, 4 ), family = 'sans', xaxt = 'n' )
# candles
plot_ts( test$indicators[ time %bw% interval ], type = 'candle', main = 'SMA Crossover' )
# indicators
plot_ts( test$indicators[ ,.( time, sma_slow, sma_fast ) ],
  col = c( 'goldenrod', 'darkmagenta' ), legend = 'topleft', add = TRUE )
# orders
plot_ts( test$orders[ side == 'buy' ,.( time_processed, price_exec ) ],
  col = 'darkolivegreen', type = 'p', pch = 24, legend = 'n', add = TRUE, last_values = FALSE )
plot_ts( test$orders[ side == 'sell',.( time_processed, price_exec ) ],
  col = 'darkred', type = 'p', pch = 25, legend = 'n', add = TRUE, last_values = FALSE )
# 2
par( xaxt = 's', mar = c( 4, 4, 0, 4 ) )
# performance
plot_ts( test$indicators[, .( time, `P&L, %` = pnl * 100, `Draw Down, %` = drawdown * 100 ) ],
  col = c( 'darkolivegreen', 'darkred' ), legend = 'bottomleft' )

Interactive

Here is code to reproduce this plot:

library( plotly ) # install.packages( 'plotly' )
p = plot_ly( symbols = c( 'triangle-up', 'triangle-down' ), 
             colors = c( 'darkseagreen', 'firebrick' ) ) %>% #
  add_data( data = test$indicators ) %>%
  # candle low-high
  add_segments( x = ~time - parameters$timeframe / 2, y = ~low,
    xend = ~time - parameters$timeframe / 2, yend = ~high,
    showlegend = F, name = 'candle', line = list( color = 'cornflowerblue' )
  ) %>%
  # candle open-close
  add_segments( x = ~time - parameters$timeframe, y = ~open,
    xend = ~time, yend = ~close,
    showlegend = F, name = 'candle', line = list( shape = 'hvh', color = 'cornflowerblue' )
  ) %>%
  # add indicators
  add_lines( x = ~time, y = ~sma_slow, name = 'sma slow', line = list( color = 'goldenrod' ) ) %>%
  add_lines( x = ~time, y = ~sma_fast, name = 'sma fast', line = list( color = 'darkmagenta' ) ) %>%
  # and orders
  add_data( data = test$orders ) %>%
  add_markers( x = ~time_processed, y = ~price_exec, color = ~side, text = ~comment,
               marker = list( size = 10 ), symbol = ~side
  ) %>%
  layout(
    xaxis = list( title = '', rangeselector = list( visible = FALSE ) ),
    yaxis = list( title = '' )
  ) %>%
  config( scrollZoom = T, autosizable = T, queueLength = 1 )
p

Parameters Map

It is very informative to see how parameters set affects your strategy performance. It is absolutly no problems for QuantTools to show exactly that. Lets run multiple tests first and plot results then.

# create parameters combinations
parameters = CJ(
  latency     = 0:1,
  timeframe   = c( 1, 15, 30, 60 ) * 60,
  period_fast = 1:20 * 10,
  period_slow = 1:20 * 10
)
# preview parameters
parameters
##       latency timeframe period_fast period_slow
##    1:       0        60          10          10
##    2:       0        60          10          20
##    3:       0        60          10          30
##    4:       0        60          10          40
##    5:       0        60          10          50
##   ---                                          
## 3196:       1      3600         200         160
## 3197:       1      3600         200         170
## 3198:       1      3600         200         180
## 3199:       1      3600         200         190
## 3200:       1      3600         200         200
# remove latency from options because we will vary it during testing
options$latency = NULL
# run tests. We set 'fast = T' to return only test summary:
system.time({
  tests = parameters[, sma_crossover( 
    ticks, 
    data.table( period_fast, period_slow, timeframe ), 
    # add latency option
    c( options, latency = latency ), fast = T ), 
                     by = .( test_id = 1:nrow( parameters ) ) ]
  })
##    user  system elapsed 
## 193.449   2.275 196.946
# preview tests result
tests
##       test_id period_fast period_slow timeframe                from                  to days_tested days_traded n_per_day    n n_long n_short n_win n_loss pct_win pct_loss avg_win avg_loss avg_pnl    win    loss    pnl max_dd        max_dd_start          max_dd_end max_dd_length sharpe sortino r_squared avg_dd
##    1:       1          10          10        60 2016-01-19 09:30:00 2016-09-14 10:42:11         167           0       NaN    0      0       0     0      0    0.00     0.00    0.00     0.00    0.00   0.00    0.00   0.00   0.00                <NA>                <NA>           NaN    NaN     NaN       NaN   0.00
##    2:       2          10          20        60 2016-01-19 09:30:00 2016-09-14 10:42:11         167         167     21.63 3613   1807    1806  1122   2491   31.05    68.95   23.10   -13.55   -2.17 259.20 -337.63 -78.43 -85.11 2016-01-21 10:20:58                <NA>           NaN  -4.59   -5.72      0.91 -50.99
##    3:       3          10          30        60 2016-01-19 09:30:00 2016-09-14 10:42:11         167         167     15.25 2547   1274    1273   755   1792   29.64    70.36   29.96   -15.50   -2.03 226.22 -277.84 -51.62 -59.28 2016-01-21 09:45:25                <NA>           NaN  -3.15   -4.33      0.90 -36.45
##    4:       4          10          40        60 2016-01-19 09:30:00 2016-09-14 10:42:11         167         166     12.69 2106   1053    1053   632   1474   30.01    69.99   33.32   -16.34   -1.44 210.58 -240.81 -30.23 -40.22 2016-01-27 15:21:42                <NA>           NaN  -1.69   -2.59      0.63 -24.88
##    5:       5          10          50        60 2016-01-19 09:30:00 2016-09-14 10:42:11         167         166     11.10 1842    921     921   505   1337   27.42    72.58   38.57   -16.83   -1.64 194.80 -225.02 -30.22 -44.45 2016-01-27 15:21:42                <NA>           NaN  -1.67   -2.53      0.82 -24.92
##   ---                                                                                                                                                                                                                                                                                                                  
## 3196:    3196         200         160      3600 2016-01-19 09:30:00 2016-09-14 10:42:11         167           6      0.83    5      2       3     4      1   80.00    20.00  646.07  -997.66  317.33  25.84   -9.98  15.87 -15.16 2016-07-21 09:51:10                <NA>           NaN   1.39    2.17      0.65  -4.43
## 3197:    3197         200         170      3600 2016-01-19 09:30:00 2016-09-14 10:42:11         167           5      0.80    4      2       2     4      0  100.00     0.00  483.40     0.00  483.40  19.34    0.00  19.34 -18.77 2016-07-21 09:51:10                <NA>           NaN   0.37    0.54      0.48  -5.09
## 3198:    3198         200         180      3600 2016-01-19 09:30:00 2016-09-14 10:42:11         167           6      1.00    6      3       3     6      0  100.00     0.00  377.42     0.00  377.42  22.64    0.00  22.64 -19.39 2016-07-21 09:51:10                <NA>           NaN   0.57    0.86      0.52  -5.27
## 3199:    3199         200         190      3600 2016-01-19 09:30:00 2016-09-14 10:42:11         167           8      0.88    7      4       3     7      0  100.00     0.00  394.10     0.00  394.10  27.59    0.00  27.59 -14.43 2016-03-04 13:45:38 2016-04-27 09:30:00            54   1.45    2.77      0.78  -3.46
## 3200:    3200         200         200      3600 2016-01-19 09:30:00 2016-09-14 10:42:11         167           0       NaN    0      0       0     0      0    0.00     0.00    0.00     0.00    0.00   0.00    0.00   0.00   0.00                <NA>                <NA>           NaN    NaN     NaN       NaN   0.00

We run 3200 tests on 2310010 ticks in just a few minutes. Quite impressive!

multi_heatmap( cbind( parameters, tests ), names( parameters ), 'pnl' ) 

© 2016 Stanislav Kovalevsky. All rights reserved.