A vectorised backtest usually holds a position series: 1 for long, minus 1 for short, 0 for flat. The cost of trading comes from the changes in that series, and the way you count them matters.
Take eight bars of positions: 0, 1, 1, 1, -1, -1, 1, 0.
Code: Select all
pos = pd.Series([0, 1, 1, 1, -1, -1, 1, 0])
units = pos.diff().abs().fillna(0) # 0,1,0,0,2,0,2,1
cost = units * 0.5 # 0.5 pip per unit traded
print(units.sum(), cost.sum()) # 6.0 3.0
A common version of the bug is cost = (pos != pos.shift()).sum() * c. That counts changes, not units, so each flip counts as one. It reports 4 changes after dropping the first row, which is a 33% undercount of the cost, and it makes strategies that flip often look better than they are.
The size matters most for systems that are always in the market. A reversal system flipping 15 times a day pays 30 units a day, which at 0.5 pip is 15 pips, before a single stop has been hit.
Cost per unit should come from the spread you really pay, and it's best to use an hourly figure instead of one number.