Commissions: Stocks vs Futures
Agnosticity
Before going forward let’s remember that backtrader
tries to remain agnostic
as to what the data represents. Different commission schemes can be applied to
the same data set.
Let’s see how it can be done.
Using the broker shortcuts
This keeps the end user away from CommissionInfo
objects because a
commission scheme can be created/set with a single function call. Within the
regular cerebro
creation/set-up process, just add a call to
setcommission
over the broker
member attribute. The following call sets
a usual commission scheme for Eurostoxx50 futures when working with
Interactive Brokers:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)
Since most users will usually just test a single instrument, that’s all that’s
down to it. If you have given a name
to your data feed, because several
instruments are being considered simultaneously on a chart, this call can be
slightly extended to look as follows:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0, name='Eurostoxxx50')
In this case this on-the-fly commission scheme will only applied to instruments
whose name matches Eurostoxx50
.
The meaning of the setcommission parameters
-
commission
(default:0.0
)Monetary units in absolute or percentage terms each action costs.
In the above example it is 2.0 euros per contract for a
buy
and again 2.0 euros per contract for asell
.The important issue here is when to use absolute or percentage values.
-
If
margin
evaluates toFalse
(it is False, 0 or None for example) then it will be considered thatcommission
expresses a percentage of theprice
timessize
operatin value -
If
margin
is something else, it is considered the operations are happenning on afutures
like intstrument andcommission
is a fixed price persize
contracts
-
-
margin
(default:None
)Margin money needed when operating with
futures
like instruments. As expressed above-
If a no
margin
is set, thecommission
will be understood to be indicated in percentage and applied toprice * size
components of abuy
orsell
operation -
If a
margin
is set, thecommission
will be understood to be a fixed value which is multiplied by thesize
component ofbuy
orsell
operation
-
-
mult
(default: 1.0)For
future
like instruments this determines the multiplicator to apply to profit and loss calculations.This is what makes futures attractive and risky at the same time.
-
name
(default: None)Limit the application of the commission scheme to instruments matching
name
This can be set during the creation of a data feed.
If left unset, the scheme will apply to any data present in the system.
Two examples now: stocks vs futures
The futures example from above:
cerebro.broker.setcommission(commission=2.0, margin=2000.0, mult=10.0)
A example for stocks:
cerebro.broker.setcommission(commission=0.005) # 0.5% of the operation value
Note
The 2nd syntax doesn’t set margin and mult and backtrader attempts a
smart approach by considering the commission to be %
based.
To fully specify commission schemes, a subclass of CommissionInfo
needs
to be created
Creating permanent Commission schemes
A more permanent commission scheme can be created by working directly with
CommissionInfo
classes. The user could choose to have this definition
somewhere:
import backtrader as bt
commEurostoxx50 = bt.CommissionInfo(commission=2.0, margin=2000.0, mult=10.0)
To later apply it in another Python module with addcommissioninfo
:
from mycomm import commEurostoxx50
...
cerebro.broker.addcommissioninfo(commEuroStoxx50, name='Eurostoxxx50')
CommissionInfo
is an object which uses a params
declaration just like
other objects in the backtrader
environment. As such the above can be also
expressed as:
import backtrader as bt
class CommEurostoxx50(bt.CommissionInfo):
params = dict(commission=2.0, margin=2000.0, mult=10.0)
And later:
from mycomm import CommEurostoxx50
...
cerebro.broker.addcommissioninfoCommEuroStoxx50(), name='Eurostoxxx50')
Now a “real” comparison with a SMA Crossover
Using a SimpleMovingAverage crossover as the entry/exit signal the same data set
is going to be tested with a futures
like commission scheme and then with a
stocks
like one.
Note
Futures positions could also not only be given the enter/exit behavior but a reversal behavior on each occassion. But this example is about comparing the commission schemes.
The code (see at the bottom for the full strategy) is the same and the scheme can be chosen before the strategy is defined.
futures_like = True
if futures_like:
commission, margin, mult = 2.0, 2000.0, 10.0
else:
commission, margin, mult = 0.005, None, 1
Just set futures_like
to false to run with the stocks
like scheme.
Some logging code has been added to evaluate the impact of the differrent commission schemes. Let’s concentrate on just the 2 first operations.
For futures:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 2000.00, Comm 2.00
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 2000.00, Comm 2.00
2006-04-12, OPERATION PROFIT, GROSS 328.00, NET 324.00
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 2000.00, Comm 2.00
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 2000.00, Comm 2.00
2006-05-02, OPERATION PROFIT, GROSS -243.30, NET -247.30
For stocks:
2006-03-09, BUY CREATE, 3757.59
2006-03-10, BUY EXECUTED, Price: 3754.13, Cost: 3754.13, Comm 18.77
2006-04-11, SELL CREATE, 3788.81
2006-04-12, SELL EXECUTED, Price: 3786.93, Cost: 3786.93, Comm 18.93
2006-04-12, OPERATION PROFIT, GROSS 32.80, NET -4.91
2006-04-20, BUY CREATE, 3860.00
2006-04-21, BUY EXECUTED, Price: 3863.57, Cost: 3863.57, Comm 19.32
2006-04-28, SELL CREATE, 3839.90
2006-05-02, SELL EXECUTED, Price: 3839.24, Cost: 3839.24, Comm 19.20
2006-05-02, OPERATION PROFIT, GROSS -24.33, NET -62.84
The 1st operation has the following prices:
-
BUY (Execution) -> 3754.13 / SELL (Execution) -> 3786.93
-
Futures Profit & Loss (with commission): 324.0
-
Stocks Profit & Loss (with commission): -4.91
-
Hey!! Commission has fully eaten up any profit on the stocks
operation
but has only meant a small dent to the futures
one.
The 2nd operation:
-
BUY (Execution) ->
3863.57
/ SELL (Execution) ->3389.24
-
Futures Profit & Loss (with commission):
-247.30
-
Stocks Profit & Loss (with commission):
-62.84
-
The bite has been sensibly larger for this negative operation with futures
But:
-
Futures accumulated net profit & loss:
324.00 + (-247.30) = 76.70
-
Stocks accumulated net profit & loss:
(-4.91) + (-62.84) = -67.75
The accumulated effect can be seen on the charts below, where it can also be seen that at the end of the full year, futures have produced a larger profit, but have also suffered a larger drawdown (were deeper underwater)
But the important thing: whether futures
or stocks
… it can be
backtested.
Commissions for futures
Commissions for stocks
The code
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import backtrader as bt
import backtrader.feeds as btfeeds
import backtrader.indicators as btind
futures_like = True
if futures_like:
commission, margin, mult = 2.0, 2000.0, 10.0
else:
commission, margin, mult = 0.005, None, 1
class SMACrossOver(bt.Strategy):
def log(self, txt, dt=None):
''' Logging function fot this strategy'''
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def notify(self, order):
if order.status in [order.Submitted, order.Accepted]:
# Buy/Sell order submitted/accepted to/by broker - Nothing to do
return
# Check if an order has been completed
# Attention: broker could reject order if not enougth cash
if order.status in [order.Completed, order.Canceled, order.Margin]:
if order.isbuy():
self.log(
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
self.opsize = order.executed.size
else: # Sell
self.log('SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
(order.executed.price,
order.executed.value,
order.executed.comm))
gross_pnl = (order.executed.price - self.buyprice) * \
self.opsize
if margin:
gross_pnl *= mult
net_pnl = gross_pnl - self.buycomm - order.executed.comm
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
(gross_pnl, net_pnl))
def __init__(self):
sma = btind.SMA(self.data)
# > 0 crossing up / < 0 crossing down
self.buysell_sig = btind.CrossOver(self.data, sma)
def next(self):
if self.buysell_sig > 0:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.buy() # keep order ref to avoid 2nd orders
elif self.position and self.buysell_sig < 0:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.sell()
if __name__ == '__main__':
# Create a cerebro entity
cerebro = bt.Cerebro()
# Add a strategy
cerebro.addstrategy(SMACrossOver)
# Create a Data Feed
datapath = ('../../datas/2006-day-001.txt')
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Add the Data Feed to Cerebro
cerebro.adddata(data)
# set commission scheme -- CHANGE HERE TO PLAY
cerebro.broker.setcommission(
commission=commission, margin=margin, mult=mult)
# Run over everything
cerebro.run()
# Plot the result
cerebro.plot()
Reference
class backtrader.CommInfoBase()
Base Class for the Commission Schemes.
Params:
-
commission
(def:0.0
): base commission value in percentage or monetary units -
mult
(def1.0
): multiplier applied to the asset for value/profit -
margin
(def:None
): amount of monetary units needed to open/hold an operation. It only applies if the final_stocklike
attribute in the class is set toFalse
-
automargin
(def:False
): Used by the methodget_margin
to automatically calculate the margin/guarantees needed with the following policy-
Use param
margin
if paramautomargin
evaluates toFalse
-
Use param
mult
and usemult * price
ifautomargin < 0
-
Use param
automargin
and useautomargin * price
ifautomargin > 0
-
-
commtype
(def:None
): Supported values areCommInfoBase.COMM_PERC
(commission to be understood as %) andCommInfoBase.COMM_FIXED
(commission to be understood as monetary units)The default value of
None
is a supported value to retain compatibility with the legacyCommissionInfo
object. Ifcommtype
is set to None, then the following applies:-
margin
isNone
: Internal_commtype
is set toCOMM_PERC
and_stocklike
is set toTrue
(Operating %-wise with Stocks) -
margin
is notNone
:_commtype
set toCOMM_FIXED
and_stocklike
set toFalse
(Operating with fixed rount-trip commission with Futures)
If this param is set to something else than
None
, then it will be passed to the internal_commtype
attribute and the same will be done with the paramstocklike
and the internal attribute_stocklike
-
-
stocklike
(def:False
): Indicates if the instrument is Stock-like or Futures-like (see thecommtype
discussion above) -
percabs
(def:False
): whencommtype
is set to COMM_PERC, whether the parametercommission
has to be understood as XX% or 0.XXIf this param is
True
: 0.XX If this param isFalse
: XX% -
interest
(def:0.0
)If this is non-zero, this is the yearly interest charged for holding a short selling position. This is mostly meant for stock short-selling
The formula:
days * price * abs(size) * (interest / 365)
It must be specified in absolute terms: 0.05 -> 5%
Note
the behavior can be changed by overriding the method:
_get_credit_interest
-
interest_long
(def:False
)Some products like ETFs get charged on interest for short and long positions. If ths is
True
andinterest
is non-zero the interest will be charged on both directions -
leverage
(def:1.0
)Amount of leverage for the asset with regards to the needed cash
- ``_stocklike``()
Final value to use for Stock-like/Futures-like behavior
- ``_commtype``()
Final value to use for PERC vs FIXED commissions
This two are used internally instead of the declared params to enable the()
compatibility check described above for the legacy ``CommissionInfo``()
object()
class backtrader.CommissionInfo()
Base Class for the actual Commission Schemes.
CommInfoBase was created to keep suppor for the original, incomplete,
support provided by backtrader. New commission schemes derive from this
class which subclasses CommInfoBase
.
The default value of percabs
is also changed to True
Params:
-
percabs
(def: True): whencommtype
is set to COMM_PERC, whether the parametercommission
has to be understood as XX% or 0.XXIf this param is True: 0.XX If this param is False: XX%
get_leverage()
Returns the level of leverage allowed for this comission scheme
getsize(price, cash)
Returns the needed size to meet a cash operation at a given price
getoperationcost(size, price)
Returns the needed amount of cash an operation would cost
getvaluesize(size, price)
Returns the value of size for given a price. For future-like
objects it is fixed at size * margin
getvalue(position, price)
Returns the value of a position given a price. For future-like
objects it is fixed at size * margin
get_margin(price)
Returns the actual margin/guarantees needed for a single item of the asset at the given price. The default implementation has this policy:
-
Use param
margin
if paramautomargin
evaluates toFalse
-
Use param
mult
, i.e.mult * price
ifautomargin < 0
-
Use param
automargin
, i.e.automargin * price
ifautomargin > 0
getcommission(size, price)
Calculates the commission of an operation at a given price
_getcommission(size, price, pseudoexec)
Calculates the commission of an operation at a given price
pseudoexec: if True the operation has not yet been executed
profitandloss(size, price, newprice)
Return actual profit and loss a position has
cashadjust(size, price, newprice)
Calculates cash adjustment for a given price difference
get_credit_interest(data, pos, dt)
Calculates the credit due for short selling or product specific
_get_credit_interest(data, size, price, days, dt0, dt1)
This method returns the cost in terms of credit interest charged by the broker.
In the case of size > 0
this method will only be called if the
parameter to the class interest_long
is True
The formulat for the calculation of the credit interest rate is:
The formula: days * price * abs(size) * (interest / 365)
Params:
* `data`: data feed for which interest is charged
* `size`: current position size. > 0 for long positions and < 0 for
short positions (this parameter will not be `0`)
* `price`: current position price
* `days`: number of days elapsed since last credit calculation
(this is (dt0 - dt1).days)
* `dt0`: (datetime.datetime) current datetime
* `dt1`: (datetime.datetime) datetime of previous calculation
dt0
and dt1
are not used in the default implementation and are
provided as extra input for overridden methods