The DJI 10 day streak
It has hit the news. The DJI is hitting all time highs with already 10 consecutive updays and 9 all time highs. See for example:
Many had already, for sure, noticed that the Dow was on such a streak and the article just tells us it’s becoming mainstream. But some questions arise:
-
Is this normal or extraordinary?
-
What happens afterwards?
Let’s put backtrader in motion to answer the questions by crafting an Analyzer to do that: analyze the situation and answer the questions (See below for the code)
Our sample data contains 5923
trading sessions. Let’s see where the current
streak fits in.
Is this normal or extraordinary?
Executing our code shows that 10
such days in a row is something at least
remarkable if not extraordinary.:
$ ./updaystreaks.py --data 099I-DJI --upstreak hilo=True count rank upstreak upleg upleg % drawdown rel drawdown 1987-01-02 1 1 13 219.069946 0.116193 0.017616 0.171407 2017-02-09 2 2 12 822.109375 0.041074 0.001875 0.047548 1970-11-19 3 2 12 66.900024 0.088986 0.010321 0.127055 1929-06-20 4 2 12 32.000000 0.101716 0.031134 0.340625 1991-12-18 5 3 11 315.100098 0.109167 0.011113 0.113614 1955-01-18 6 3 11 22.200012 0.057290 0.014334 0.265765 2017-07-25 7 4 10 622.289062 0.028949 NaN NaN 2013-03-01 8 4 10 488.959961 0.034801 0.008102 0.240919 1996-11-04 9 4 10 348.839844 0.058148 0.004792 0.087605 1973-07-16 10 4 10 53.600037 0.060695 0.095935 1.686565 1959-11-17 11 4 10 31.599976 0.049945 0.011216 0.237342 1959-06-24 12 4 10 36.200012 0.057680 0.020649 0.381215 1955-08-23 13 4 10 25.400024 0.056344 0.008772 0.165353 1933-03-03 14 4 10 12.600002 0.250497 0.142415 0.730158 1920-12-29 15 4 10 8.099998 0.119118 0.022339 0.209876 2016-07-08 16 5 9 778.378906 0.043688 0.016552 0.396003 1996-05-08 17 5 9 334.369629 0.061755 0.002442 0.041990 1989-07-03 18 5 9 141.890137 0.058804 0.007179 0.129677 1968-04-23 19 5 9 38.000000 0.043123 0.070535 1.736842 1967-04-13 20 5 9 49.700012 0.059061 0.006593 0.118713 1967-01-03 21 5 9 55.799988 0.071603 0.006321 0.094982 1965-01-22 22 5 9 18.500000 0.020838 0.031326 1.540541 1964-03-06 23 5 9 19.600037 0.024506 0.016127 0.678570 1955-06-15 24 5 9 12.399994 0.028343 0.005537 0.201613 1955-04-05 25 5 9 16.299988 0.039553 0.010465 0.276074 1954-09-01 26 5 9 18.599976 0.055822 0.009325 0.177419 1945-04-06 27 5 9 9.000000 0.058140 0.008526 0.155555 1929-02-18 28 5 9 21.800018 0.072812 0.086005 1.279815 1921-10-18 29 5 9 4.300003 0.061871 0.008130 0.139536
The current streak, yet to come to an end, is ranked (tied) in 4th position. To notice:
-
The upleg in % is the smallest when the streak is 10 days or longer
-
Three (3) of the days with a streak of 9 updays are slightly smaller in % terms, dating back to 1955, 1964 and 1965
-
This year has another long streak ranked as number 2 with 12 days
What happens afterwards?
Even if the table already shows the drawdown after the streak ended and the relative drawdown (taken from the start of the streak, hence it can be > 100%), the question is better answered visually.
And the charts quickly show that:
- Such long streaks seem to indicate strength with no large drawdowns really to be expected as the reaction
But wait!!!
Ranked as number 1 and also ranked as number 2 we have remarkable dates:
count rank upstreak upleg upleg % drawdown rel drawdown 1987-01-02 1 1 13 219.069946 0.116193 0.017616 0.171407 2017-02-09 2 2 12 822.109375 0.041074 0.001875 0.047548 1970-11-19 3 2 12 66.900024 0.088986 0.010321 0.127055 1929-06-20 4 2 12 32.000000 0.101716 0.031134 0.340625 ...
Indeed, because 1987
and 1929
had later really large bear legs. But not
immediately after the streak as shown by the statistics: the relative drawdown
didn’t go over 100%, hence new highs followed the end of those streaks.
The code
from __future__ import (absolute_import, division, print_function, unicode_literals) import argparse import collections import datetime import itertools import matplotlib.pyplot as plt import pandas as pd import backtrader as bt class UpStreak(bt.Analyzer): params = dict( sep=',', hilo=False, ) def __init__(self): self.upday = bt.ind.UpDayBool() self.curdt = None # streak start date self.incs = dict() # upleg in points self.pincs = dict() # upleg in percentage self.close0 = dict() # starting price for upleg self.peaks = collections.deque() # endng price for upleg self.ddown = dict() # absolute drawdowns self.ddownrel = dict() # relative drawdown (% of upleg retraced) self.rets = collections.defaultdict(int) # holds main results def next(self): curclose = self.data.close[0] lastclose = self.data.close[-1] self.peaks.append((None, None)) while True: dt, peak = self.peaks.popleft() if dt is None: break # all elements seen if peak > curclose: # peak not overdone, update drawdown ddown = 1.0 - curclose / peak self.ddown[dt] = max(self.ddown[dt], ddown) self.peaks.append((dt, peak)) # not done yet inc = self.incs[dt] fall = peak - curclose ddownrel = fall / inc self.ddownrel[dt] = max(self.ddownrel[dt], ddownrel) if self.upday: if self.curdt is None: # streak begins self.curdt = self.strategy.datetime.date() if self.p.hilo: lastclose = self.data.low[-1] self.close0[self.curdt] = lastclose self.incs[self.curdt] = inc = curclose - self.close0[self.curdt] self.pincs[self.curdt] = inc / self.close0[self.curdt] self.rets[self.curdt] += 1 # update current streak else: if self.curdt is not None: # streak ends if self.p.hilo: lastclose = self.data.high[-1] inc = self.incs[self.curdt] fall = lastclose - curclose self.ddownrel[self.curdt] = fall / inc self.ddown[self.curdt] = 1.0 - curclose / lastclose self.peaks.append((self.curdt, lastclose)) self.curdt = None def stop(self): s = sorted( self.rets.items(), reverse=True, key=lambda item: (item[1], item[0]) ) # keep it in dict format self.rets = collections.OrderedDict(s) self.s = collections.OrderedDict(s) self.headers = [ 'date', 'count', 'rank', 'upstreak', 'upleg', 'upleg %', 'drawdown', 'rel drawdown', ] i = 0 count = itertools.count(1) last = float('inf') for dt, streak in self.s.items(): if streak < last: i += 1 last = streak ddown = self.ddown.get(dt, None) ddownrel = self.ddownrel.get(dt, None) inc = self.incs.get(dt, None) pinc = self.pincs.get(dt, None) self.s[dt] = [ next(count), i, streak, inc, pinc, ddown, ddownrel ] def get_dataframe(self): return pd.DataFrame.from_items( self.s.items(), orient='index', columns=self.headers[1:], # skip index ) def print_ranking(self): i = 0 last = float('inf') print(self.p.sep.join(self.headers)) for dt, items in self.s.items(): print( self.p.sep.join( str(x) for x in itertools.chain([dt], items) ) ) def runstrat(args=None): args = parse_args(args) cerebro = bt.Cerebro() kwargs = dict() # Data feed kwargs # 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']): if a: strpfmt = dtfmt + tmfmt * ('T' in a) kwargs[d] = datetime.datetime.strptime(a, strpfmt) fromdate = kwargs.get('fromdate', datetime.date.min) store = bt.stores.VChartFile() data = store.getdata(dataname=args.data, **kwargs) cerebro.adddata(data) cerebro.addanalyzer(UpStreak, **eval('dict(' + args.upstreak + ')')) result = cerebro.run() st0 = result[0] a = st0.analyzers.upstreak # Plot some things # pd.set_option('display.max_columns', 500) pd.set_option('display.expand_frame_repr', False) df = a.get_dataframe() up = df['upstreak'] up9 = df[up >= 9] print(up9) up7 = df[up >= 7] x = up7['upstreak'] y = up7['rel drawdown'] * 100.0 plt.scatter(x, y) plt.ylabel('% Relative Drawdown') plt.xlabel('Updays streak') plt.title('DJI Relative Drawdown after N consecutive UpDays') plt.show() # Plot some things y = up7['drawdown'] * 100.0 plt.ylabel('% Absolute Drawdown') plt.xlabel('Updays streak') plt.title('DJI Drawdown after N consecutive UpDays') plt.scatter(x, y) plt.show() def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=( 'UpDayStreaks' ) ) parser.add_argument('--data', default='', required=True, help='Data Ticker') parser.add_argument('--fromdate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') parser.add_argument('--todate', required=False, default='', help='Date[time] in YYYY-MM-DD[THH:MM:SS] format') parser.add_argument('--cerebro', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--upstreak', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--strat', required=False, default='', metavar='kwargs', help='kwargs in key=value format') parser.add_argument('--plot', required=False, default='', nargs='?', const='{}', metavar='kwargs', help='kwargs in key=value format') return parser.parse_args(pargs) if __name__ == '__main__': runstrat()