Credit Interest
In some situations, the cash amount in real brokers may be decreased because the operation on assets includes an interest rate. Examples:
-
Short selling of stocks
-
ETFs both long and short
This means that not only the trades make up the profitability of a system, because the interests on the credit take a bite on the account.
To cover this cases backtrader includes (starting with release 1.8.8.96
)
functionality to take this into account.
Extending CommissionInfo
Even if not associated to any order/trade, the discount of cash from the account can be modeled as a commission being charged by the broker. As such and given that backtrader already offers a flexible and extensible commission system, this system has been slightly extended to support credit interest.
A CommissionInfo
can now be instantiated with two new parameters:
-
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
The parameters can also be set via the broker using the following method:
def setcommission(self, commission=0.0, margin=None, mult=1.0, commtype=None, percabs=True, stocklike=False, interest=0.0, interest_long=False, name=None)
Where interest
and interest_long
obviously bear the same meaning as
above.
Applying a CommissionInfo
For a stock with a commission in percentage a typical use scenario with credit interest would be as follows
import backtrader as bt cerebro = bt.Cerebro() comminfo = bt.CommissionInfo(commtype=bt.CommissionInfo.COMM_PERC, # % commission commission=0.005, # 0.5% percabs=True, # perc expressed in abs terms stocklike=True, interest=0.05, # 5% anual credit interest rate ) cerebro.broker.addcommissioninfo(comminfo) ...
This is mostly useful if th end user has own commission schemes.
A simpler case with setcommission
:
import backtrader as bt cerebro = bt.Cerebro() cerebro.broker.setcommission(commtype=bt.CommissionInfo.COMM_PERC, # % commission commission=0.005, # 0.5% percabs=True, # perc expressed in abs terms stocklike=True, interest=0.05, # 5% anual credit interest rate ) ...
And the rest as in any other usual backtrader script.
Some sample scenarios
Long only, no exit, No interest
To establish a minimum baseline, let’s start without interest and letting the script enter the market only long and avoid exiting.
$ ./credit-interest.py --plot --stocklike --long --no-exit 01 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47
The idea should be clear now. Have a flat cash line away from the total portfolio value and showing no deduction.
Long only, no exit and interest
Let’s try to add interest and see what happens (we’ll be adding a huge 15%
interest to try to notice the movements)
$ ./credit-interest.py --plot --stocklike --long --no-exit --interest 0.15 01 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47
NOTHING HAS CHANGED! And this is expected. In most cases the interest is
applied only to short
positions (taken with a credit) and this is a a
long-only position.
Let’s tell the script to do it for long
positions too
$ ./credit-interest.py --plot --stocklike --long --no-exit --interest 0.15 --interest_long 01 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47
And the change is there. There has been a decrement and is huge (given the huge interest which is being taken)
A long-short scenario
This would model something like an ETF
which has a yearly interest and can
be a regular one or an inverse one. First let’s establish the baseline.
$ ./credit-interest.py --plot --stocklike 01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55 02 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47 ... ... 34 2006-12-19 23:59:59 BUY Size: +10 / Price: 4121.01 35 2006-12-19 23:59:59 BUY Size: +10 / Price: 4121.01
Plenty more operations and the system is always in the market.
Because the ETF
will have interest charged on both long and short
operations, the interest will be added for both now:
$ ./credit-interest.py --plot --stocklike --interest 0.15 --interest_long 01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55 02 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47 ... ... 34 2006-12-19 23:59:59 BUY Size: +10 / Price: 4121.01
ACHTUNG: 34
operations instead of 35
. It seems something might be
broken but … NO …
The charged interest is taking a bit into the cash reserves and this ends up not allowing the last order because THERE IS NOT ENOUGH CASH
Removing the interest charge from long operations (even if not real for an ETF) will allow the system to come to the end:
$ ./credit-interest.py --plot --stocklike --interest 0.15 01 2005-03-22 23:59:59 SELL Size: -10 / Price: 3040.55 02 2005-04-11 23:59:59 BUY Size: +10 / Price: 3088.47 ... ... 34 2006-12-19 23:59:59 BUY Size: +10 / Price: 4121.01 35 2006-12-19 23:59:59 BUY Size: +10 / Price: 4121.01
Back in business until the 35th
operation.
A quick comparison with the original shows that the final cash has changed from
7490
(with no interest) to 5418
(applying interest only to short
operations)
Conclusion
This new functionality allows to simulate with even more fidelity backtesting scenarios to try to achieve the dream: a profitable system
Sample usage
$ ./credit-interest.py --help usage: credit-interest.py [-h] [--data DATA] [--fromdate FROMDATE] [--todate TODATE] [--cash CASH] [--period1 PERIOD1] [--period2 PERIOD2] [--interest INTEREST] [--interest_long] [--long | --short] [--no-exit] [--stocklike] [--margin MARGIN] [--mult MULT] [--stake STAKE] [--plot [kwargs]] Sample for Slippage optional arguments: -h, --help show this help message and exit --data DATA Specific data to be read in (default: ../../datas/2005-2006-day-001.txt) --fromdate FROMDATE Starting date in YYYY-MM-DD format (default: None) --todate TODATE Ending date in YYYY-MM-DD format (default: None) --cash CASH Cash to start with (default: 50000) --period1 PERIOD1 Fast moving average period (default: 10) --period2 PERIOD2 Slow moving average period (default: 30) --interest INTEREST Activate credit interest rate (default: 0.0) --interest_long Credit interest rate for long positions (default: False) --long Do a long only strategy (default: False) --short Do a long only strategy (default: False) --no-exit The 1st taken position will not be exited (default: False) --stocklike Consider the asset to be stocklike (default: False) --margin MARGIN Margin for future like instruments (default: 0.0) --mult MULT Multiplier for future like instruments (default: 1.0) --stake STAKE Stake to apply (default: 10) --plot [kwargs], -p [kwargs] Plot the read data applying any kwargs passed For example: --plot style="candle" (to plot candles) (default: None)
Sample code
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import collections import datetime import itertools import backtrader as bt class SMACrossOver(bt.Signal): params = (('p1', 10), ('p2', 30),) def __init__(self): sma1 = bt.indicators.SMA(period=self.p.p1) sma2 = bt.indicators.SMA(period=self.p.p2) self.lines.signal = bt.indicators.CrossOver(sma1, sma2) class NoExit(bt.Signal): def next(self): self.lines.signal[0] = 0.0 class St(bt.SignalStrategy): opcounter = itertools.count(1) def notify_order(self, order): if order.status == bt.Order.Completed: t = '' t += '{:02d}'.format(next(self.opcounter)) t += ' {}'.format(order.data.datetime.datetime()) t += ' BUY ' * order.isbuy() or ' SELL' t += ' Size: {:+d} / Price: {:.2f}' print(t.format(order.executed.size, order.executed.price)) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() cerebro.broker.set_cash(args.cash) dkwargs = dict() if args.fromdate is not None: fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d') dkwargs['fromdate'] = fromdate if args.todate is not None: todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d') dkwargs['todate'] = todate # if dataset is None, args.data has been given data = bt.feeds.BacktraderCSVData(dataname=args.data, **dkwargs) cerebro.adddata(data) cerebro.signal_strategy(St) cerebro.addsizer(bt.sizers.FixedSize, stake=args.stake) sigtype = bt.signal.SIGNAL_LONGSHORT if args.long: sigtype = bt.signal.SIGNAL_LONG elif args.short: sigtype = bt.signal.SIGNAL_SHORT cerebro.add_signal(sigtype, SMACrossOver, p1=args.period1, p2=args.period2) if args.no_exit: if args.long: cerebro.add_signal(bt.signal.SIGNAL_LONGEXIT, NoExit) elif args.short: cerebro.add_signal(bt.signal.SIGNAL_SHORTEXIT, NoExit) comminfo = bt.CommissionInfo( mult=args.mult, margin=args.margin, stocklike=args.stocklike, interest=args.interest, interest_long=args.interest_long) if True: cerebro.broker.addcommissioninfo(comminfo) cerebro.run() if args.plot: pkwargs = dict(style='bar') if args.plot is not True: # evals to True but is not True npkwargs = eval('dict(' + args.plot + ')') # args were passed pkwargs.update(npkwargs) cerebro.plot(**pkwargs) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description='Sample for Slippage') parser.add_argument('--data', required=False, default='../../datas/2005-2006-day-001.txt', help='Specific data to be read in') parser.add_argument('--fromdate', required=False, default=None, help='Starting date in YYYY-MM-DD format') parser.add_argument('--todate', required=False, default=None, help='Ending date in YYYY-MM-DD format') parser.add_argument('--cash', required=False, action='store', type=float, default=50000, help=('Cash to start with')) parser.add_argument('--period1', required=False, action='store', type=int, default=10, help=('Fast moving average period')) parser.add_argument('--period2', required=False, action='store', type=int, default=30, help=('Slow moving average period')) parser.add_argument('--interest', required=False, action='store', default=0.0, type=float, help=('Activate credit interest rate')) parser.add_argument('--interest_long', required=False, action='store_true', help=('Credit interest rate for long positions')) pgroup = parser.add_mutually_exclusive_group() pgroup.add_argument('--long', required=False, action='store_true', help=('Do a long only strategy')) pgroup.add_argument('--short', required=False, action='store_true', help=('Do a long only strategy')) parser.add_argument('--no-exit', required=False, action='store_true', help=('The 1st taken position will not be exited')) parser.add_argument('--stocklike', required=False, action='store_true', help=('Consider the asset to be stocklike')) parser.add_argument('--margin', required=False, action='store', default=0.0, type=float, help=('Margin for future like instruments')) parser.add_argument('--mult', required=False, action='store', default=1.0, type=float, help=('Multiplier for future like instruments')) parser.add_argument('--stake', required=False, action='store', default=10, type=int, help=('Stake to apply')) # Plot options parser.add_argument('--plot', '-p', nargs='?', required=False, metavar='kwargs', const=True, help=('Plot the read data applying any kwargs passed\n' '\n' 'For example:\n' '\n' ' --plot style="candle" (to plot candles)\n')) if pargs is not None: return parser.parse_args(pargs) return parser.parse_args() if __name__ == '__main__': runstrat()