Skip to content
All library documents

Fast Signal-Based Backtesting in R with Transaction Costs and Trade Logs

Article Quant Q&A · Author: mr.T

Summary

The document considers how to backtest a single strategy repeatedly in R when general-purpose frameworks feel too slow. It distinguishes basic signal-to-profit accounting from broader backtesting, which may also include performance reporting or live-trading connectivity. For a simple position series, portfolio profit and loss can be calculated directly from price changes and held positions; compiling the core calculation in C is suggested when additional speed is needed.

The example accounts for proportional transaction fees by charging costs on changes in position, and derives individual trades from those position changes. A journal-based workflow with the PMwR package can record transaction amounts, prices, and timestamps, calculate total profit and loss, and split transactions into trades for per-trade results. The examples demonstrate mechanics, not a validated backtesting system or performance comparison. Correct results still depend on carefully defining signals, execution timing, costs, and what counts as a completed trade; the answer does not establish one universally suitable package.

Key ideas

  • A simple signal and position series can be mapped directly to portfolio profit and loss.
  • Transaction costs can be incorporated by applying proportional fees to changes in position.
  • Position changes provide a basis for creating transaction journals and separating individual trades.
  • PMwR examples show how to calculate aggregate and per-trade profit and loss.
  • The appropriate backtesting tool depends on whether the need is accounting, analytics, optimization, or live connectivity.

Tags

Full text
# what are the packages for effective backtesting in R


# what are the packages for effective backtesting in R












I need a fast package for backtesting in R. I'm going to optimize a lot so I'll be running my strategies many millions of times. I know about packages like `quantstrat` or `SIT` but they are terribly slow for my purposes, I have one strategy, I'm not going to model 20 portfolios at the same time and such. Which package can you recommend?

UPD=========

Yes, I implemented something very simple like

```
signals <- sample(c(-1,0,1),30,replace = T)
```

`-1` open sale

`1` open buy

`0` do nothing and close any position

```
prices <- cumsum(rnorm(30))+100

count_balance <- function(prices,signals){
  p <- c(0,diff(prices))
  s <- c(0,signals[-length(signals)])
  return(   cumsum(p*s)   )
}

count_balance(p = prices,sig = signals)
```

or equivalent in `Rcpp` even faster 30 times

```
#include <Rcpp.h>
using namespace Rcpp;

// [[Rcpp::export]]
NumericVector count_balance_cpp(NumericVector prices, NumericVector signals) {
  int n = prices.size();
  NumericVector result(n);
  result[0] = 0;
  for (int i = 1; i < n; ++i) {
    result[i] = result[i-1] + signals[i-1] * (prices[i] - prices[i-1]);
  }
  return result;
}
```

But I would like a little more

- take into account the commission

- have a trade log so that I know the ratio of profitable losing trades

In princepe I can implement this too, but I will not be completely sure that I did everything right, since I am a very bad programmer. I'm not even sure about the functions that I posted above)

That's why I was looking for a simple, fast, ready-made and most importantly proven library

## Answer by Enrico Schumann (score 4, accepted)

https://quant.stackexchange.com/a/75762

To pick up Mark's suggestion on writing your own code: the raw P/L computation from a vector of signals in R can be quite fast.

```
P <- c(100, 99, 104, 103, 105, 104)  ## price series
S <- c(  0,  1,   1,   0,   1,   0)  ## position to be held
dS <- c(0, diff(S)) ## change in position ==> trades
## [1]  0  1  0 -1  1 -1

portfolio.value <- S*P - cumsum(dS*P)
## [1] 0 0 5 4 4 3
```

But backtesting can mean different things to different people: from bare-bones accounting for P/L, via computation/reporting of statistics, to actual connectivity to live trading. And strategies come in many flavours, too. You might get better answers if you provided more details about your strategy, and how/what you intent to optimize.

Response to update:

If you only need a mapping between signals and P/L, it will be hard to beat a raw implementation, in particular written in C. I am not aware of a package that offers this, but maybe there is. (You might also want to ask this question also on R-SIG-Finance, where at least in the past the maintainers of the packages you mentioned have answered questions.)

That being said, you'll need to decide how much speed you need. Transactions costs are easy to add to the snippet above:

```
## with 10bp proportional fees
dSP <- dS*P
portfolio.value <- S*P - cumsum(dSP) - cumsum(abs(dSP)*0.001)
## [1]  0.000 -0.099  4.901  3.798  3.693  2.589
```

As for a trade log: you have it in the vector `dS`. Here is a sketch how you could handle the trades, using package PMwR (which I maintain).

```
## trades
library("PMwR")
J <- journal(amount = dS, price = P, timestamp = seq_along(P))
J
##    timestamp  amount  price
## 1          1       0    100
## 2          2       1     99
## 3          3       0    104
## 4          4      -1    103
## 5          5       1    105
## 6          6      -1    104
## 
## 6 transactions  

pl(J)
## P/L total         3
## average buy     102
## average sell  103.5
## cum. volume       4
## 
## ‘P/L total’ is in units of instrument;
## ‘volume’ is sum of /absolute/ amounts.
```

`pl` will give the total P/L. But you can easily split up the trades.

```
trade <- dS != 0
trades <- split_trades(amount = dS[trade],
                       price = P[trade],
                       timestamp = seq_along(P)[trade])
## [[1]]
## [[1]]$amount
## [1]  1 -1
## 
## [[1]]$price
## [1]  99 103
## 
## [[1]]$timestamp
## [1] 2 4
## 
## 
## [[2]]
## [[2]]$amount
## [1]  1 -1
## 
## [[2]]$price
## [1] 105 104
## 
## [[2]]$timestamp
## [1] 5 6
```

Call `pl` for each trade.

```
sapply(trades,
       function(x) pl(as.journal(x), pl.only = TRUE))
## [1]  4 -1
```

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.