BTFD - Reality Bites
The previous post managed to replicate the BTFD
strategy, finding out that
the real gains were 16x
rather than 31x
.
But as pointed out during the replication:
-
No commission was charged
-
No interest was charged for using a
2x
leverage
And that raises the obvious question:
- How much of that 16x will be there when commission and interest are charged?
Luckily the previous sample is flexible enough to experiment with it. To have some visual feedback and verification, the following code will be added to the strategy
def start(self): print(','.join(['TRADE', 'STATUS', 'Value', 'PNL', 'COMMISSION'])) def notify_order(self, order): if order.status in [order.Margin]: print('ORDER FAILED with status:', order.getstatusname()) def notify_trade(self, trade): if trade.isclosed: print(','.join(map(str, [ 'TRADE', 'CLOSE', self.data.num2date(trade.dtclose).date().isoformat(), trade.value, trade.pnl, trade.commission, ] ))) elif trade.justopened: print(','.join(map(str, [ 'TRADE', 'OPEN', self.data.num2date(trade.dtopen).date().isoformat(), trade.value, trade.pnl, trade.commission, ] )))
It’s all about the following:
-
Seeing how trades are opened and closed (value, profit and loss, value and commission)
-
Providing feedback if an order is being rejected with
Margin
due to insufficient fundsNote
Because there will be an adjustment of the amount of money to invest, to leave room for commission, some orders could not be accepted by the broker. This visual feedback allows identifying the situation
Verification
First a quick test to see that some orders are not accepted.
$ ./btfd.py --comminfo commission=0.001,leverage=2.0 --strat target=1.0 TRADE,STATUS,Value,PNL,COMMISSION ORDER FAILED with status: Margin ORDER FAILED with status: Margin TRADE,OPEN,1990-01-08,199345.2,0.0,199.3452 TRADE,CLOSE,1990-01-10,0.0,-1460.28,397.23012
Notice:
-
We apply
target=1.0
which means: try to invest 100% of the capital. This is the default, but it is there as a reference. -
commission=0.001
or0.1%
to ensure we will sometimes meet the margin -
The 1st two orders are rejected with
Margin
-
The 3rd order is accepted. This is not an error. The system tries to invest
100%
of the capital, but the asset has a price and this is used to calculate the size of the stake. Size is rounded down from the actual result of calculating the potential size from the actual available cash. This rounding down has left room enough for the commission with this 3rd order. -
The trade notifications (
OPEN
andCLOSE
) show the opening commission and the final total commission an the value which is close to200k
, showing the2x
leverage in action.The opening commission is
199.3452
which is0.1%
of the leveraged value which is:199,345.2
The remaining tests will be made with target=0.99x
where x
will ensure
room enough for the selected commission.
Reality Bites
Let’s go for some real examples
Target 99.8% - Commission 0.1%
./btfd.py --comminfo commission=0.001,leverage=2.0 --strat target=0.998 --plot
Blistering Barnacles!!! Not only is the BTFD
strategy by no means close to
the 16x
gains: IT LOSES MOST OF THE MONEY.
- From
100,000
down to roughly4,027
Note
The down to value is the non-leveraged value, because this is the approximate value that will be back in the system when the position is closed
Target 99.9% - Commission 0.05%
It may well have been that the commission is too aggressive. Let’s go for half of it
./btfd.py --comminfo commission=0.0005,leverage=2.0 --strat target=0.999 --plot
NO, NO. The commission was not that aggressive, because the system still
loses money, going down from 100,000
down to around 69,000
(the
non-leverage value)
Target 99.95% - Commission 0.025%
Commission is divided by two again
./btfd.py --comminfo commission=0.00025,leverage=2.0 --strat target=0.9995 --plot
Finally the system makes money:
-
The initial
100,000
are taken up to331,459
for3x
gains. -
But this doesn’t match the performance of the asset which has gone up to over
600k
Note
The sample accepts --fromdate YYYY-MM-DD
and --todate YYYY-MM-DD
to
select to which period the strategy has to be applied. This would allow
testing similar scenarios for different date ranges.
Conclusion
The 16x
gains do not hold when confronted with commission. For the
commission offered by some brokers (no cap and %-based) one would need a very
good deal to make sure the system makes money.
And in this case in which the strategy is applied to the S&P500
, the
BTFD
strategy doesn’t match the performance of the index.
No interest rate has been applied. Using commissions is enough to see how far
away is 16x
from any potential profits. In any case, a run with a 2%
interest rate would be executed like this
./btfd.py --comminfo commission=0.00025,leverage=2.0,interest=0.02,interest_long=True --strat target=0.9995 --plot
interest_long=True
is needed, because the default behavior for charging
interest is to do it only for short positions
Sample usage
$ ./btfd.py --help usage: btfd.py [-h] [--offline] [--data TICKER] [--fromdate YYYY-MM-DD[THH:MM:SS]] [--todate YYYY-MM-DD[THH:MM:SS]] [--cerebro kwargs] [--broker kwargs] [--valobserver kwargs] [--strat kwargs] [--comminfo kwargs] [--plot [kwargs]] BTFD - http://dark-bid.com/BTFD-only-strategy-that-matters.html - https://www. reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/ optional arguments: -h, --help show this help message and exit --offline Use offline file with ticker name (default: False) --data TICKER Yahoo ticker to download (default: ^GSPC) --fromdate YYYY-MM-DD[THH:MM:SS] Starting date[time] (default: 1990-01-01) --todate YYYY-MM-DD[THH:MM:SS] Ending date[time] (default: 2016-10-01) --cerebro kwargs kwargs in key=value format (default: stdstats=False) --broker kwargs kwargs in key=value format (default: cash=100000.0, coc=True) --valobserver kwargs kwargs in key=value format (default: assetstart=100000.0) --strat kwargs kwargs in key=value format (default: approach="highlow") --comminfo kwargs kwargs in key=value format (default: leverage=2.0) --plot [kwargs] kwargs in key=value format (default: )
Sample Code
from __future__ import (absolute_import, division, print_function, unicode_literals) # References: # - https://www.reddit.com/r/algotrading/comments/5jez2b/can_anyone_replicate_this_strategy/ # - http://dark-bid.com/BTFD-only-strategy-that-matters.html import argparse import datetime import backtrader as bt class ValueUnlever(bt.observers.Value): '''Extension of regular Value observer to add leveraged view''' lines = ('value_lever', 'asset') params = (('assetstart', 100000.0), ('lever', True),) def next(self): super(ValueUnlever, self).next() if self.p.lever: self.lines.value_lever[0] = self._owner.broker._valuelever if len(self) == 1: self.lines.asset[0] = self.p.assetstart else: change = self.data[0] / self.data[-1] self.lines.asset[0] = change * self.lines.asset[-1] class St(bt.Strategy): params = ( ('fall', -0.01), ('hold', 2), ('approach', 'highlow'), ('target', 1.0) ) def __init__(self): if self.p.approach == 'closeclose': self.pctdown = self.data.close / self.data.close(-1) - 1.0 elif self.p.approach == 'openclose': self.pctdown = self.data.close / self.data.open - 1.0 elif self.p.approach == 'highclose': self.pctdown = self.data.close / self.data.high - 1.0 elif self.p.approach == 'highlow': self.pctdown = self.data.low / self.data.high - 1.0 def next(self): if self.position: if len(self) == self.barexit: self.close() else: if self.pctdown <= self.p.fall: self.order_target_percent(target=self.p.target) self.barexit = len(self) + self.p.hold def start(self): print(','.join(['TRADE', 'STATUS', 'Value', 'PNL', 'COMMISSION'])) def notify_order(self, order): if order.status in [order.Margin, order.Rejected, order.Canceled]: print('ORDER FAILED with status:', order.getstatusname()) def notify_trade(self, trade): if trade.isclosed: print(','.join(map(str, [ 'TRADE', 'CLOSE', self.data.num2date(trade.dtclose).date().isoformat(), trade.value, trade.pnl, trade.commission, ] ))) elif trade.justopened: print(','.join(map(str, [ 'TRADE', 'OPEN', self.data.num2date(trade.dtopen).date().isoformat(), trade.value, trade.pnl, trade.commission, ] ))) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() # Data feed kwargs kwargs = dict() # Parse from/to-date dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S' for a, d in ((getattr(args, x), x) for x in ['fromdate', 'todate']): kwargs[d] = datetime.datetime.strptime(a, dtfmt + tmfmt * ('T' in a)) if not args.offline: YahooData = bt.feeds.YahooFinanceData else: YahooData = bt.feeds.YahooFinanceCSVData # Data feed - no plot - observer will do the job data = YahooData(dataname=args.data, plot=False, **kwargs) cerebro.adddata(data) # Broker cerebro.broker = bt.brokers.BackBroker(**eval('dict(' + args.broker + ')')) # Add a commission cerebro.broker.setcommission(**eval('dict(' + args.comminfo + ')')) # Strategy cerebro.addstrategy(St, **eval('dict(' + args.strat + ')')) # Add specific observer cerebro.addobserver(ValueUnlever, **eval('dict(' + args.valobserver + ')')) # Execute cerebro.run(**eval('dict(' + args.cerebro + ')')) if args.plot: # Plot if requested to cerebro.plot(**eval('dict(' + args.plot + ')')) def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=(' - '.join([ 'BTFD', 'http://dark-bid.com/BTFD-only-strategy-that-matters.html', ('https://www.reddit.com/r/algotrading/comments/5jez2b/' 'can_anyone_replicate_this_strategy/')])) ) parser.add_argument('--offline', required=False, action='store_true', help='Use offline file with ticker name') parser.add_argument('--data', required=False, default='^GSPC', metavar='TICKER', help='Yahoo ticker to download') parser.add_argument('--fromdate', required=False, default='1990-01-01', metavar='YYYY-MM-DD[THH:MM:SS]', help='Starting date[time]') parser.add_argument('--todate', required=False, default='2016-10-01', metavar='YYYY-MM-DD[THH:MM:SS]', help='Ending date[time]') parser.add_argument('--cerebro', required=False, default='stdstats=False', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--broker', required=False, default='cash=100000.0, coc=True', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--valobserver', required=False, default='assetstart=100000.0', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--strat', required=False, default='approach="highlow"', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--comminfo', required=False, default='leverage=2.0', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--plot', required=False, default='', nargs='?', const='volume=False', metavar='kwargs', help='kwargs in key=value format') return parser.parse_args(pargs) if __name__ == '__main__': runstrat()