BTFD (Buy The F… Dip)

Note Added operations log for the CloseClose approach (updated sample below too)

A post in Reddit calling for replication of a BTFD strategy turned out to be the little push needed to add yet another feature to backtrader: leverage

The links:

In the Reddit thread there was finally no full replication of the results from Dark Bid. But it can be done. Although one has to point something out from the start:

Dark Bid is not showing a true chart and the explanation about the strategy is really lacking and probably would be deemed as wrong by most

Let’s have a direct look at the chart here (remember the chart is from the Dark Bid BTFD post)

The interpretation most people would probably do:

Buy The Close: this part is clear. Buy during the closing auction

When S&P Is Down At Least 1%: in most cases that means that the current close is 1% below the previous close

Hold For 2 Days: the interpretation here would be to sell at the close 2 trading sessions later

2x Leverage: Each buy operation is leveraged 2x (or 100% credit)

Additionals:

The data seems to run from 1990-01-01 to 2016-09-30

The starting cash value seems to start at 100,000

Note Notice there are no commissions or credit interest being charged.

The sample below is configured with the winning settings, but the first execution will respect the interpreted rules.

$ ./btfd.py --plot --strat approach="'closeclose'"

Note The sample accepts an --offline parameter to use a file previously downloaded from Yahoo to speed up testing. If not used, the data with the default dates quoted above and the default ticker ^GSPC will be downloaded each and every time from Yahoo

Houston, we have a problem! Although not so big as expected. Observations:

Our asset has gone from 100,000 to 602,816 and this seems right on spot with the BTFD chart above

Our 100,000 cash, on the other hand, has only gone up to 357,277 which seems 10x below the expected value over 3,000,000 (or 3M )

In this case value and value_unlever are equal, which means that the strategy is NOT IN THE MARKET

But the most interesting thing is: the clear difference in the red lines which show the value over time

Our line has very quick ups and downs This is due to leverage. When the strategy is bought the 2x leverage allows to buy two times the asset. But when the strategy is in cash, the cash is not worth twice its value.

The BTFD line doesn’t exhibit this behavior and that’s because it is ALWAYS leveraged

Ok. First part of the BTFD mystery … SOLVED! But we still have a problem and this is matching the performance achieved by Dark Bid. Without much ado, a run of the sample with the default winning settings.

$ ./btfd.py --plot --strat approach="'highlow'"

Note approach=highlow is the default setting if the sample is run with no approach argument to the strategy

In this case:

The difference between the high and the low is used. Of course this isn’t “Is Down At least 1% as indicated in the BTFD chart. This is something different, because the low may also have happened before the high and many would see this as being up rather than down

Note The strategy allows these approaches: closeclose , highlow , highclose and openclose .

But:

The value of the system is 3,184,118 . Ant this is a winner because the visual inspection of the BTFD chart shows that the final value is over 3M

The unleveraged value ( value_unlever ) of the system is: 1,592,608 which is not the above. This simply tells us that the strategy IS IN THE MARKET

Obviously and in about 1 or 2 days, the strategy will sell the position and the real cash value will be around 1,592,608 and by no means close to 3,184,118 .

The 1.5M value was the best observation achieved by the original Reddit poster.

Conclusion

The complete BTFD mystery is solved. The keys:

The value line plotted in the chart isn’t real, because it is always leveraged and not only when being bought

The approach is not really to buy when the asset is at least 1% down as commonly understood (previous close to current close) but when the asset has at least a 1% difference between the high and the low

The real gains are roughly 16x (from 100,000 to roughly 1,592,000 ) and in NO CASE 31x (from 100,000 to 3.1M )

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), ('prorder', False), ('prtrade', False), ('prdata', False), ) 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() if self.p.prdata: print(','.join(str(x) for x in ['DATA', 'CLOSE', self.data.datetime.date().isoformat(), self.data.close[0], float('NaN')])) else: if self.pctdown <= self.p.fall: self.order_target_percent(target=self.p.target) self.barexit = len(self) + self.p.hold if self.p.prdata: print(','.join(str(x) for x in ['DATA', 'OPEN', self.data.datetime.date().isoformat(), self.data.close[0], self.pctdown[0]])) def start(self): if self.p.prtrade: print(','.join( ['TRADE', 'Status', 'Date', 'Value', 'PnL', 'Commission'])) if self.p.prorder: print(','.join( ['ORDER', 'Type', 'Date', 'Price', 'Size', 'Commission'])) if self.p.prdata: print(','.join(['DATA', 'Action', 'Date', 'Price', 'PctDown'])) def notify_order(self, order): if order.status in [order.Margin, order.Rejected, order.Canceled]: print('ORDER FAILED with status:', order.getstatusname()) elif order.status == order.Completed: if self.p.prorder: print(','.join(map(str, [ 'ORDER', 'BUY' * order.isbuy() or 'SELL', self.data.num2date(order.executed.dt).date().isoformat(), order.executed.price, order.executed.size, order.executed.comm, ] ))) def notify_trade(self, trade): if not self.p.prtrade: return 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()

Operations Logs

CloseClose Approach

Execution:

$ ./btfd.py --strat approach="'closeclose'",prorder=True,prdata=True

Result: