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
## ---
## 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.
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 )
# 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
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' )
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
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.