Skip to content

Commit e04066b

Browse files
Merge pull request #47 from coffincw/master
Add renko chart support
2 parents cde5008 + 56af314 commit e04066b

File tree

7 files changed

+680
-543
lines changed

7 files changed

+680
-543
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ mpf.plot(daily)
180180
---
181181
<br>
182182

183-
The default plot type, as you can see above, is `'ohlc'`. Other plot types can be specified with the keyword argument `type`, for example, `type='candle'` or `type='line'`
183+
The default plot type, as you can see above, is `'ohlc'`. Other plot types can be specified with the keyword argument `type`, for example, `type='candle'`, `type='line'`, or `type='renko'`
184184

185185

186186
```python
@@ -200,6 +200,15 @@ mpf.plot(daily,type='line')
200200
![png](https://raw.githubusercontent.com/matplotlib/mplfinance/master/readme_files/readme_7_1.png)
201201

202202

203+
204+
```python
205+
mpf.plot(daily,type='renko')
206+
```
207+
208+
209+
![png](https://raw.githubusercontent.com/matplotlib/mplfinance/master/readme_files/readme_8_1.png)
210+
211+
203212
---
204213
<br>
205214

examples/renko_charts.ipynb

Lines changed: 339 additions & 0 deletions
Large diffs are not rendered by default.

readme.ipynb

Lines changed: 94 additions & 514 deletions
Large diffs are not rendered by default.

readme_files/readme_8_1.png

10.2 KB
Loading

src/mplfinance/_arg_validators.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def _mav_validator(mav_value):
5555
if not isinstance(num,int) and num > 1:
5656
return False
5757
return True
58+
5859

5960
def _bypass_kwarg_validation(value):
6061
''' For some kwargs, we either don't know enough, or
@@ -100,24 +101,24 @@ def _process_kwargs(kwargs, vkwargs):
100101
# now validate kwargs, and for any valid kwargs
101102
# replace the appropriate value in config:
102103
for key in kwargs.keys():
103-
if key not in vkwargs:
104-
raise KeyError('Unrecognized kwarg="'+str(key)+'"')
105-
else:
106-
value = kwargs[key]
107-
try:
108-
valid = vkwargs[key]['Validator'](value)
109-
except Exception as ex:
110-
raise ValueError('kwarg "'+key+'" validator raised exception to value: "'+str(value)+'"') from ex
111-
if not valid:
112-
import inspect
113-
v = inspect.getsource(vkwargs[key]['Validator']).strip()
114-
raise ValueError('kwarg "'+key+'" validator returned False for value: "'+str(value)+'"\n '+v)
104+
if key not in vkwargs:
105+
raise KeyError('Unrecognized kwarg="'+str(key)+'"')
106+
else:
107+
value = kwargs[key]
108+
try:
109+
valid = vkwargs[key]['Validator'](value)
110+
except Exception as ex:
111+
raise ValueError('kwarg "'+key+'" validator raised exception to value: "'+str(value)+'"') from ex
112+
if not valid:
113+
import inspect
114+
v = inspect.getsource(vkwargs[key]['Validator']).strip()
115+
raise ValueError('kwarg "'+key+'" validator returned False for value: "'+str(value)+'"\n '+v)
115116

116117
# ---------------------------------------------------------------
117118
# At this point in the loop, if we have not raised an exception,
118119
# then kwarg is valid as far as we can tell, therefore,
119120
# go ahead and replace the appropriate value in config:
120121

121-
config[key] = value
122+
config[key] = value
122123

123124
return config

src/mplfinance/_utils.py

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from matplotlib import colors as mcolors
1111
from matplotlib.collections import LineCollection, PolyCollection
12+
from mplfinance._arg_validators import _process_kwargs, _validate_vkwargs_dict
1213

1314
from six.moves import zip
1415

@@ -81,6 +82,49 @@ def roundTime(dt=None, roundTo=60):
8182
rounding = (seconds+roundTo/2) // roundTo * roundTo
8283
return dt + datetime.timedelta(0,rounding-seconds,-dt.microsecond)
8384

85+
def _calculate_atr(atr_length, highs, lows, closes):
86+
"""Calculate the average true range
87+
atr_length : time period to calculate over
88+
all_highs : list of highs
89+
all_lows : list of lows
90+
all_closes : list of closes
91+
"""
92+
if atr_length < 1:
93+
raise ValueError("Specified atr_length may not be less than 1")
94+
elif atr_length >= len(closes):
95+
raise ValueError("Specified atr_length is larger than the length of the dataset: " + str(len(closes)))
96+
atr = 0
97+
for i in range(len(highs)-atr_length, len(highs)):
98+
high = highs[i]
99+
low = lows[i]
100+
close_prev = closes[i-1]
101+
tr = max(abs(high-low), abs(high-close_prev), abs(low-close_prev))
102+
atr += tr
103+
return atr/atr_length
104+
105+
def renko_reformat_ydata(ydata, dates, old_dates):
106+
"""Reformats ydata to work on renko charts, can lead to unexpected
107+
outputs for the user as the xaxis does not scale evenly with dates.
108+
Missing dates ydata is averaged into the next date and dates that appear
109+
more than once have the same ydata
110+
ydata : y data likely coming from addplot
111+
dates : x-axis dates for the renko chart
112+
old_dates : original dates in the data set
113+
"""
114+
new_ydata = [] # stores new ydata
115+
prev_data = 0
116+
skipped_dates = 0
117+
for i in range(len(ydata)):
118+
if old_dates[i] not in dates:
119+
prev_data += ydata[i]
120+
skipped_dates += 1
121+
else:
122+
dup_dates = dates.count(old_dates[i])
123+
new_ydata.extend([(ydata[i]+prev_data)/(skipped_dates+1)]*dup_dates)
124+
skipped_dates = 0
125+
prev_data = 0
126+
return new_ydata
127+
84128
def _updown_colors(upcolor,downcolor,opens,closes,use_prev_close=False):
85129
if upcolor == downcolor:
86130
return upcolor
@@ -92,6 +136,29 @@ def _updown_colors(upcolor,downcolor,opens,closes,use_prev_close=False):
92136
_list = [ cmap[pre < cls] for cls,pre in zip(closes[1:], closes) ]
93137
return [first] + _list
94138

139+
def _valid_renko_kwargs():
140+
'''
141+
Construct and return the "valid renko kwargs table" for the mplfinance.plot(type='renko') function.
142+
A valid kwargs table is a `dict` of `dict`s. The keys of the outer dict are the
143+
valid key-words for the function. The value for each key is a dict containing
144+
2 specific keys: "Default", and "Validator" with the following values:
145+
"Default" - The default value for the kwarg if none is specified.
146+
"Validator" - A function that takes the caller specified value for the kwarg,
147+
and validates that it is the correct type, and (for kwargs with
148+
a limited set of allowed values) may also validate that the
149+
kwarg value is one of the allowed values.
150+
'''
151+
vkwargs = {
152+
'brick_size' : { 'Default' : 'atr',
153+
'Validator' : lambda value: isinstance(value,(float,int)) or value == 'atr' },
154+
'atr_length' : { 'Default' : 14,
155+
'Validator' : lambda value: isinstance(value,int) },
156+
}
157+
158+
_validate_vkwargs_dict(vkwargs)
159+
160+
return vkwargs
161+
95162
def _construct_ohlc_collections(dates, opens, highs, lows, closes, marketcolors=None):
96163
"""Represent the time, open, high, low, close as a vertical line
97164
ranging from low to high. The left tick is the open and the right
@@ -262,6 +329,121 @@ def _construct_candlestick_collections(dates, opens, highs, lows, closes, market
262329

263330
return rangeCollection, barCollection
264331

332+
def _construct_renko_collections(dates, highs, lows, volumes, config_renko_params, closes, marketcolors=None):
333+
"""Represent the price change with bricks
334+
335+
Parameters
336+
----------
337+
dates : sequence
338+
sequence of dates
339+
highs : sequence
340+
sequence of high values
341+
lows : sequence
342+
sequence of low values
343+
renko_params : dictionary
344+
type : type of renko chart
345+
brick_size : size of each brick
346+
atr_legnth : length of time used for calculating atr
347+
closes : sequence
348+
sequence of closing values
349+
marketcolors : dict of colors: up, down, edge, wick, alpha
350+
351+
Returns
352+
-------
353+
ret : tuple
354+
rectCollection
355+
"""
356+
renko_params = _process_kwargs(config_renko_params, _valid_renko_kwargs())
357+
if marketcolors is None:
358+
marketcolors = _get_mpfstyle('classic')['marketcolors']
359+
print('default market colors:',marketcolors)
360+
361+
brick_size = renko_params['brick_size']
362+
atr_length = renko_params['atr_length']
363+
364+
365+
if brick_size == 'atr':
366+
brick_size = _calculate_atr(atr_length, highs, lows, closes)
367+
else: # is an integer or float
368+
total_atr = _calculate_atr(len(closes)-1, highs, lows, closes)
369+
upper_limit = 1.5*total_atr
370+
lower_limit = 0.01*total_atr
371+
if brick_size > upper_limit:
372+
raise ValueError("Specified brick_size may not be larger than (1.5* the Average True Value of the dataset) which has value: "+ str(upper_limit))
373+
elif brick_size < lower_limit:
374+
raise ValueError("Specified brick_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: "+ str(lower_limit))
375+
376+
alpha = marketcolors['alpha']
377+
378+
uc = mcolors.to_rgba(marketcolors['candle'][ 'up' ], alpha)
379+
dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha)
380+
euc = mcolors.to_rgba(marketcolors['edge'][ 'up' ], 1.0)
381+
edc = mcolors.to_rgba(marketcolors['edge']['down'], 1.0)
382+
383+
cdiff = [(closes[i+1] - closes[i])/brick_size for i in range(len(closes)-1)] # fill cdiff with close price change
384+
385+
bricks = [] # holds bricks, 1 for down bricks, -1 for up bricks
386+
new_dates = [] # holds the dates corresponding with the index
387+
new_volumes = [] # holds the volumes corresponding with the index. If more than one index for the same day then they all have the same volume.
388+
389+
prev_num = 0
390+
start_price = closes[0]
391+
392+
volume_cache = 0 # holds the volumes for the dates that were skipped
393+
394+
395+
for i in range(len(cdiff)):
396+
num_bricks = abs(int(round(cdiff[i], 0)))
397+
398+
if num_bricks != 0:
399+
new_dates.extend([dates[i]]*num_bricks)
400+
401+
if volumes is not None: # only adds volumes if there are volume values when volume=True
402+
if num_bricks != 0:
403+
new_volumes.extend([volumes[i] + volume_cache]*num_bricks)
404+
volume_cache = 0
405+
else:
406+
volume_cache += volumes[i]
407+
408+
if cdiff[i] > 0:
409+
bricks.extend([1]*num_bricks)
410+
else:
411+
bricks.extend([-1]*num_bricks)
412+
413+
verts = []
414+
colors = []
415+
edge_colors = []
416+
brick_values = []
417+
for index, number in enumerate(bricks):
418+
if number == 1: # up brick
419+
colors.append(uc)
420+
edge_colors.append(euc)
421+
else: # down brick
422+
colors.append(dc)
423+
edge_colors.append(edc)
424+
425+
prev_num += number
426+
brick_y = start_price + (prev_num * brick_size)
427+
brick_values.append(brick_y)
428+
x, y = index, brick_y
429+
430+
verts.append((
431+
(x, y),
432+
(x, y+brick_size),
433+
(x+1, y+brick_size),
434+
(x+1, y)))
435+
436+
useAA = 0, # use tuple here
437+
lw = None
438+
rectCollection = PolyCollection(verts,
439+
facecolors=colors,
440+
antialiaseds=useAA,
441+
edgecolors=edge_colors,
442+
linewidths=lw
443+
)
444+
445+
return (rectCollection, ), new_dates, new_volumes, brick_values
446+
265447
from matplotlib.ticker import Formatter
266448
class IntegerIndexDateTimeFormatter(Formatter):
267449
"""

0 commit comments

Comments
 (0)