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 bbands_market_maker.cpp file into working directory.

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

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

  int    n          = parameters["n"         ];
  double k          = parameters["k"         ];
  int    timeFrame  = parameters["timeframe" ];

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

  // initialize indicators
  BBands bbands( n, k );

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


  Order* buy;
  Order* sell;

  double levelLongEnter;
  double levelLongExit;
  double levelShortEnter;
  double levelShortExit;

  // define events logic

  // any trade exit event declaration
  std::function<void()> onTradeExit;

  // long closer logic
  std::function<void()> quoteLongExit = [&]() {

    // quote sell with current levelLongExit price
    sell = new Order( OrderSide::SELL, OrderType::LIMIT, levelLongExit, "close long", idTrade );
    // attach events to order:
    //   when long position closed ( sold ) exit logic triggered
    sell->onExecuted  = onTradeExit;
    //   when long closing order cancelled quote long closing order again
    sell->onCancelled = quoteLongExit;
    // send order to exchange
    bt.SendOrder( sell );

  };

  // long opener logic
  std::function<void()> quoteLongEnter = [&]() {

    // quote buy with current levelLongEnter price
    buy = new Order( OrderSide::BUY, OrderType::LIMIT, levelLongEnter, "long", idTrade );
    // attach events to order:
    //   when long opening order executed
    buy->onExecuted = [&]() {
      // change state to LONG
      state = ProcessingState::LONG;
      // change onCancelled event for short opener so the next time when
      // short opener cancelled it will trigger long order exiter
      sell->onCancelled = quoteLongExit;
      // or in case it is accidently executed there is no need in quoting long exiter
      // because it is already closed
      sell->onExecuted  = [&]() {
        // so just add relevant comment to it
        sell->comment = "close long on cancel";
        // and trigger exit logic
        onTradeExit();
      };
    };
    //  when long opening order cancelled qoute it again
    buy->onCancelled = quoteLongEnter;
    // send order to exchange
    bt.SendOrder( buy );

  };

  // short order closer logic
  std::function<void()> quoteShortExit = [&]() {

    // quote buy with current levelShortExit price
    buy = new Order( OrderSide::BUY, OrderType::LIMIT, levelShortExit, "close short", idTrade );
    // attach events to order:
    //   when short position closed ( bought ) exit logic triggered
    buy->onExecuted  = onTradeExit;
    //   when short closing order cancelled it again
    buy->onCancelled = quoteShortExit;
    // send order to exchange
    bt.SendOrder( buy );

  };

  // short opener logic
  std::function<void()> quoteShortEnter = [&]() {
    // quote sell with current levelShortEnter price
    sell = new Order( OrderSide::SELL, OrderType::LIMIT, levelShortEnter, "short", idTrade );
    // attach events to order:
    //   when short opening order executed
    sell->onExecuted = [&]() {
      // change state to SHORT
      state = ProcessingState::SHORT;
      // change onCancelled event for long opener so the next time when
      // long opener cancelled it will trigger short order exiter
      buy->onCancelled = quoteShortExit;
      // or in case it is accidently executed there is no need in quoting short exiter
      // because it is already closed
      buy->onExecuted  = [&]() {
        // so just add relevant comment to it
        buy->comment = "close short on cancel";
        // and trigger exit logic
        onTradeExit();
      };
    };
    //  when short opening order cancelled qoute it again
    sell->onCancelled = quoteShortEnter;
    // send order to exchange
    bt.SendOrder( sell );

  };

  // trade exit logic
  onTradeExit = [&]() {

    // reset state to FLAT
    state = ProcessingState::FLAT;
    // increment trade id
    idTrade++;
    // and start quoting long and short openers
    quoteLongEnter ();
    quoteShortEnter();

  };


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

  };
  bt.onMarketClose = [&]() {
    // forbid trading and close open positions
    isTradingHours = false;
    // if state was SHORT
    if( state == ProcessingState::SHORT ) {
      // redefine cancel logic of short closer:
      //   when cancelled
      buy->onCancelled = [&]() {
        // create market order to close short position
        Order* buy = new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "close short (EOD)", idTrade );
        // attach events to order:
        //   when market order executed
        buy->onExecuted = [&]() {
          // reset state to initial
          state = ProcessingState::INIT;
          // and increment trade id
          idTrade++;
        };
        // send order to exchange
        bt.SendOrder( buy );
      };
      //  when executed
      buy->onExecuted = [&]() {
        // reset state to initial
        state = ProcessingState::INIT;
        // and increment trade id
        idTrade++;
      };
      // send cancel request to exchange
      buy->Cancel();

    }
    // if state was LONG
    if( state == ProcessingState::LONG ) {
      // redefine cancel logic of long closer:
      //   when cancelled
      sell->onCancelled = [&]() {
        // create market order to close short position
        Order* sell = new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "close long (EOD)", idTrade );
        // attach events to order:
        //   when market order executed
        sell->onExecuted = [&]() {
          // reset state to initial
          state = ProcessingState::INIT;
          // and increment trade id
          idTrade++;
        };
        // send order to exchange
        bt.SendOrder( sell );

      };
      //  when executed
      sell->onExecuted = [&]() {
        // reset state to initial
        state = ProcessingState::INIT;
        // and increment trade id
        idTrade++;
      };
      // send cancel request to exchange
      sell->Cancel();
    }
    // if state was FLAT
    if( state == ProcessingState::FLAT ) {

      // let's try to cancel openers

      // redefine short opener logic
      // in case short opener executed
      sell->onExecuted = [&]() {
        // mark it as failed to cancel
        sell->comment = "short cancel failed (EOD)";
        // send long opener cancel request to exchange
        buy->Cancel();
        // create market order to close short position
        Order* buy = new Order( OrderSide::BUY, OrderType::MARKET, NA_REAL, "close short (EOD)", idTrade );
        // attach events to order:
        //   when market order executed
        buy->onExecuted = [&]() {
          // reset state to initial
          state = ProcessingState::INIT;
          // and increment trade id
          idTrade++;
        };
        // send order to exchange
        bt.SendOrder( buy );
      };
      // in case short opener cancelled
      sell->onCancelled = [&]() {
        // mark it as cancelled
        sell->comment = "short cancel (EOD)";
        // reset state to initial
        state = ProcessingState::INIT;
        // and increment trade id
        idTrade++;
      };
      // send cancel request to exchange
      sell->Cancel();

      // redefine long opener logic
      // in case long opener executed
      buy->onExecuted = [&]() {
        // mark it as failed to cancel
        buy->comment = "long cancel failed (EOD)";
        // send short opener cancel request to exchange
        sell->Cancel();
        // create market order to close long position
        Order* sell = new Order( OrderSide::SELL, OrderType::MARKET, NA_REAL, "close long (EOD)", idTrade );
        // attach events to order:
        //   when market order executed
        sell->onExecuted = [&]() {
          // reset state to initial
          state = ProcessingState::INIT;
          // and increment trade id
          idTrade++;
        };
        // send order to exchange
        bt.SendOrder( sell );
      };
      // in case long opener cancelled
      buy->onCancelled = [&]() {
        // mark it as cancelled
        buy->comment = "long cancel (EOD)";
        // reset state to initial
        state = ProcessingState::INIT;
        // and increment trade id
        idTrade++;
      };
      // send cancel request to exchange
      buy->Cancel();

    }

  };

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

    // add values to indicators
    bbands.Add( candle.close );
    // if bbands not formed yet do nothing
    if( not bbands.IsFormed() ) return;

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

    // update current levels
    levelLongEnter  = bbands.GetValue().lower;
    levelLongExit   = bbands.GetValue().sma;
    levelShortEnter = bbands.GetValue().upper;
    levelShortExit  = bbands.GetValue().sma;

    if( state == ProcessingState::INIT ) {

      // in case of initial state
      // start quoting long and short openers
      quoteLongEnter();
      quoteShortEnter();
      // and change state to FLAT
      state = ProcessingState::FLAT;

    } else {

      // if not in initial state
      // move current orders according to set logic
      bt.CancelOrders();

    }

  };

  // 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( bbands.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/bbands_market_maker.cpp' )
Rcpp::sourceCpp( strategy_cpp_file )

Run Backtest

# set strategy parameters
parameters = data.table(
  n         = 100,
  k         = 0.5,
  timeframe = 60
)

# set options, see 'Options' section
options = list(
  cost    = list( tradeAbs = -0.01 ),
  latency = 0.1, # 100 milliseconds
  allow_limit_to_hit_market = TRUE
)


# see how fast back testing done on over 2 millin ticks
system.time( { test = bbands_market_maker( ticks, parameters, options ) } )
##    user  system elapsed 
##   0.186   0.027   0.214
# lets limit testing to one date
interval = '2016-09-08'
test = bbands_market_maker( ticks[ time %bw% interval ], parameters, options )
test
## $summary
##                   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: 2016-09-08 09:30:00 2016-09-08 15:59:59           1           1        17 17     10       7    16      1   94.12     5.88     5.1   -25.41    3.31 0.82 -0.25 0.56   -0.6 2016-09-08 12:18:34 2016-09-08 14:35:04             0    NaN     Inf       NaN  -0.06
## 
## $trades
##     id_trade id_sent id_enter id_exit           time_sent          time_enter           time_exit  side price_enter price_exit         pnl         mtm      mtm_min    mtm_max  cost    pnl_rel    mtm_rel mtm_min_rel mtm_max_rel   cost_rel  state
##  1:        1    9608     9611    9957 2016-09-08 11:10:00 2016-09-08 11:10:00 2016-09-08 11:15:26  long    105.8200   106.0459  0.20585000  0.23000000  0.000000000 0.23000000 -0.02  19.452844  21.735022   0.0000000   21.735022 -1.8900019 closed
##  2:        2    9957    10357   10989 2016-09-08 11:15:28 2016-09-08 11:24:08 2016-09-08 11:41:35  long    105.9054   105.9391  0.01374212  0.03464212 -0.125357878 0.03464212 -0.02   1.297585   3.271045 -11.8367834    3.271045 -1.8884786 closed
##  3:        3   10989    11071   11140 2016-09-08 11:41:37 2016-09-08 11:44:53 2016-09-08 11:47:28  long    105.8686   105.9237  0.03515457  0.05640457 -0.008595427 0.05640457 -0.02   3.320586   5.327791  -0.8118958    5.327791 -1.8891343 closed
##  4:        4   11140    11174   11833 2016-09-08 11:47:51 2016-09-08 11:49:00 2016-09-08 12:08:46 short    106.0100   105.9213  0.06865000  0.10000000 -0.115000000 0.10000000 -0.02   6.475804   9.433072 -10.8480332    9.433072 -1.8866145 closed
##  5:        5   11833    12022   12126 2016-09-08 12:08:54 2016-09-08 12:14:29 2016-09-08 12:18:27  long    105.8400   105.9017  0.04175000  0.07000000  0.000000000 0.07000000 -0.02   3.944633   6.613757   0.0000000    6.613757 -1.8896447 closed
##  6:        6   12126    12166   15897 2016-09-08 12:18:34 2016-09-08 12:20:12 2016-09-08 14:00:36  long    105.8537   105.6047 -0.26898677 -0.23373677 -0.603736769 0.02626323 -0.02 -25.411174 -22.081107 -57.0349982    2.481087 -1.8893995 closed
##  7:        7   15897    15908   16155 2016-09-08 14:00:36 2016-09-08 14:00:37 2016-09-08 14:01:19 short    105.6750   105.6025  0.05242455  0.08497455 -0.235025455 0.08497455 -0.02   4.960923   8.041123 -22.2404080    8.041123 -1.8925957 closed
##  8:        8   16155    16178   16253 2016-09-08 14:01:19 2016-09-08 14:01:35 2016-09-08 14:02:21  long    105.5337   105.5993  0.04556528  0.06626528 -0.083734723 0.06626528 -0.02   4.317603   6.279061  -7.9344035    6.279061 -1.8951286 closed
##  9:        9   16253    16289   16788 2016-09-08 14:02:22 2016-09-08 14:02:28 2016-09-08 14:09:54 short    105.6671   105.5934  0.05368542  0.08713542 -0.222864582 0.08713542 -0.02   5.080616   8.246217 -21.0911918    8.246217 -1.8927361 closed
## 10:       10   16788    16933   16952 2016-09-08 14:09:56 2016-09-08 14:12:47 2016-09-08 14:13:25 short    105.6538   105.5902  0.04358466  0.06383466 -0.006165343 0.06383466 -0.02   4.125232   6.041868  -0.5835418    6.041868 -1.8929744 closed
## 11:       11   16952    16978   17255 2016-09-08 14:13:34 2016-09-08 14:14:29 2016-09-08 14:20:14  long    105.5259   105.5814  0.03554829  0.07414829 -0.075851710 0.07414829 -0.02   3.368681   7.026552  -7.1879742    7.026552 -1.8952702 closed
## 12:       12   17255    17305   17906 2016-09-08 14:20:15 2016-09-08 14:21:47 2016-09-08 14:35:04 short    105.6461   105.5835  0.04253268  0.06608268 -0.173917325 0.06608268 -0.02   4.025959   6.255099 -16.4622597    6.255099 -1.8931133 closed
## 13:       13   17906    18046   18099 2016-09-08 14:35:09 2016-09-08 14:39:22 2016-09-08 14:40:21  long    105.5117   105.5733  0.04169042  0.06834042 -0.001659576 0.06834042 -0.02   3.951262   6.477049  -0.1572884    6.477049 -1.8955251 closed
## 14:       14   18099    18227   18810 2016-09-08 14:40:25 2016-09-08 14:43:00 2016-09-08 14:58:01 short    105.6335   105.5705  0.04291653  0.06846653 -0.136533473 0.06846653 -0.02   4.062777   6.481519 -12.9252099    6.481519 -1.8933394 closed
## 15:       15   18810    18872   19189 2016-09-08 14:58:05 2016-09-08 14:59:01 2016-09-08 15:06:42 short    105.6337   105.5772  0.03642021  0.07367021 -0.136329786 0.07367021 -0.02   3.447785   6.974122 -12.9059026    6.974122 -1.8933357 closed
## 16:       16   19189    19356   19430 2016-09-08 15:06:45 2016-09-08 15:12:54 2016-09-08 15:14:31  long    105.5135   105.5783  0.04488294  0.06653294 -0.003467056 0.06653294 -0.02   4.253764   6.305635  -0.3285890    6.305635 -1.8954926 closed
## 17:       17   19430    19621   19983 2016-09-08 15:14:40 2016-09-08 15:21:25 2016-09-08 15:31:13  long    105.5162   105.5951  0.05892356  0.08382356 -0.066176443 0.08382356 -0.02   5.584315   7.944143  -6.2716869    7.944143 -1.8954440 closed
## 18:       18   19983    20127      NA 2016-09-08 15:31:13 2016-09-08 15:32:56                <NA>  long    105.5474         NA          NA -0.03743160 -0.217431597 0.02256840 -0.01         NA  -3.546424 -20.6003684    2.138224 -0.9474413 opened
## 
## $orders
##      id_trade id_sent id_processed           time_sent      time_processed price_init price_exec side  type      state    comment
##   1:        1    9608         9611 2016-09-08 11:10:00 2016-09-08 11:10:00   105.9348     105.82  buy limit   executed       long
##   2:        1    9608         9644 2016-09-08 11:10:00 2016-09-08 11:11:16   106.2540         NA sell limit  cancelled      short
##   3:        1    9644         9768 2016-09-08 11:11:17 2016-09-08 11:12:00   106.0816         NA sell limit  cancelled close long
##   4:        1    9768         9869 2016-09-08 11:12:00 2016-09-08 11:13:01   106.0738         NA sell limit  cancelled close long
##   5:        1    9869         9915 2016-09-08 11:13:02 2016-09-08 11:14:00   106.0648         NA sell limit  cancelled close long
##  ---                                                                                                                             
## 355:       18   21412        21532 2016-09-08 15:55:03 2016-09-08 15:56:04   105.5761         NA sell limit  cancelled close long
## 356:       18   21532        21645 2016-09-08 15:56:05 2016-09-08 15:57:05   105.5743         NA sell limit  cancelled close long
## 357:       18   21645        21731 2016-09-08 15:57:08 2016-09-08 15:58:00   105.5734         NA sell limit  cancelled close long
## 358:       18   21731        21897 2016-09-08 15:58:00 2016-09-08 15:59:00   105.5727         NA sell limit  cancelled close long
## 359:       18   21897           NA 2016-09-08 15:59:00                <NA>   105.5726         NA sell limit registered close long
## 
## $indicators
##                     time    open   high     low   close volume    id    lower    upper      sma         pnl     drawdown
##   1: 2016-09-08 09:31:00 107.110 107.26 107.030 107.160  48933   212       NA       NA       NA 0.000000000  0.000000000
##   2: 2016-09-08 09:32:00 107.160 107.18 106.640 106.800 179364   478       NA       NA       NA 0.000000000  0.000000000
##   3: 2016-09-08 09:33:00 106.800 106.96 106.760 106.910  51495   799       NA       NA       NA 0.000000000  0.000000000
##   4: 2016-09-08 09:34:00 106.900 106.98 106.830 106.980  37592   942       NA       NA       NA 0.000000000  0.000000000
##   5: 2016-09-08 09:35:00 106.950 107.06 106.900 106.900  48085  1120       NA       NA       NA 0.000000000  0.000000000
##  ---                                                                                                                    
## 384: 2016-09-08 15:55:00 105.455 105.47 105.370 105.390  25467 21405 105.5256 105.6266 105.5761 0.004134348 -0.001727555
## 385: 2016-09-08 15:56:00 105.390 105.39 105.340 105.345  35934 21530 105.5226 105.6261 105.5743 0.003707999 -0.002153903
## 386: 2016-09-08 15:57:00 105.345 105.41 105.335 105.410  32188 21633 105.5212 105.6257 105.5734 0.004323836 -0.001538067
## 387: 2016-09-08 15:58:00 105.445 105.47 105.420 105.420  16367 21718 105.5201 105.6254 105.5727 0.004418580 -0.001443322
## 388: 2016-09-08 15:59:00 105.430 105.49 105.430 105.460  42548 21895 105.5199 105.6254 105.5726 0.004797557 -0.001064346
## 
## $daily_performance
##          date      return         pnl      drawdown      avg_pnl n_per_day
## 1: 2016-09-08 0.005271277 0.005271277 -0.0005906252 0.0003309365        17

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 = 'Bollinger Bands' )
# indicators
plot_ts( test$indicators[ ,.( time, upper, sma, lower ) ],
  col = c( 'darkgoldenrod', 'chocolate', 'darkgoldenrod' ), 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 = ~lower, name = 'lower', line = list( color = 'goldenrod' ) ) %>%
  add_lines( x = ~time, y = ~sma, name = 'sma', line = list( color = 'darkgreen' ) ) %>%
  add_lines( x = ~time, y = ~upper, name = 'upper', 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

© 2016 Stanislav Kovalevsky. All rights reserved.