Skip to content

Commit 0da835c

Browse files
Merge pull request #72 from coffincw/master
Add ability to create Point & Figure plots
2 parents ae0aa1a + 4f2f7b8 commit 0da835c

File tree

6 files changed

+740
-449
lines changed

6 files changed

+740
-449
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
- **[Adding Your Own Technical Studies to Plots](https://github.com/matplotlib/mplfinance/blob/master/examples/addplot.ipynb)**
1919
- **[Saving the Plot to a File](https://github.com/matplotlib/mplfinance/blob/master/examples/savefig.ipynb)**
2020
- **[Customizing the Appearance of Plots](https://github.com/matplotlib/mplfinance/blob/master/examples/customization_and_styles.ipynb)**
21-
- **[Renko Plots](https://github.com/matplotlib/mplfinance/blob/master/examples/renko_charts.ipynb)**
21+
- **[Price-Movement Plots (Renko, P&F, etc)](https://github.com/matplotlib/mplfinance/blob/master/examples/price-movement_plots.ipynb)**
2222
- Technical Studies (presently in development)
2323
- **[Latest Release Info](https://github.com/matplotlib/mplfinance#release)**
2424
- **[Some Background History About This Package](https://github.com/matplotlib/mplfinance#history)**
@@ -181,7 +181,7 @@ mpf.plot(daily)
181181
---
182182
<br>
183183

184-
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'`
184+
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'`, `type='renko'`, or `type='pnf'`
185185

186186

187187
```python
@@ -210,6 +210,13 @@ mpf.plot(daily,type='renko')
210210
![png](https://raw.githubusercontent.com/matplotlib/mplfinance/master/readme_files/readme_8_1.png)
211211

212212

213+
```python
214+
mpf.plot(daily,type='pnf')
215+
```
216+
217+
218+
![png](https://raw.githubusercontent.com/matplotlib/mplfinance/master/readme_files/readme_5_1.png)
219+
213220
---
214221
<br>
215222

examples/price-movement_plots.ipynb

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

examples/renko_charts.ipynb

Lines changed: 0 additions & 339 deletions
This file was deleted.

readme_files/readme_5_1.png

14.8 KB
Loading

src/mplfinance/_utils.py

Lines changed: 178 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
import datetime
99

1010
from matplotlib import colors as mcolors
11-
from matplotlib.collections import LineCollection, PolyCollection
11+
from matplotlib.patches import Ellipse
12+
from matplotlib.collections import LineCollection, PolyCollection, PatchCollection
1213
from mplfinance._arg_validators import _process_kwargs, _validate_vkwargs_dict
1314

1415
from six.moves import zip
@@ -102,29 +103,6 @@ def _calculate_atr(atr_length, highs, lows, closes):
102103
atr += tr
103104
return atr/atr_length
104105

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-
128106
def _updown_colors(upcolor,downcolor,opens,closes,use_prev_close=False):
129107
if upcolor == downcolor:
130108
return upcolor
@@ -138,10 +116,10 @@ def _updown_colors(upcolor,downcolor,opens,closes,use_prev_close=False):
138116

139117
def _valid_renko_kwargs():
140118
'''
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:
119+
Construct and return the "valid renko kwargs table" for the mplfinance.plot(type='renko')
120+
function. A valid kwargs table is a `dict` of `dict`s. The keys of the outer dict are
121+
the valid key-words for the function. The value for each key is a dict containing 2
122+
specific keys: "Default", and "Validator" with the following values:
145123
"Default" - The default value for the kwarg if none is specified.
146124
"Validator" - A function that takes the caller specified value for the kwarg,
147125
and validates that it is the correct type, and (for kwargs with
@@ -159,6 +137,29 @@ def _valid_renko_kwargs():
159137

160138
return vkwargs
161139

140+
def _valid_pointnfig_kwargs():
141+
'''
142+
Construct and return the "valid pointnfig kwargs table" for the mplfinance.plot(type='pnf')
143+
function. A valid kwargs table is a `dict` of `dict`s. The keys of the outer dict are
144+
the valid key-words for the function. The value for each key is a dict containing 2
145+
specific keys: "Default", and "Validator" with the following values:
146+
"Default" - The default value for the kwarg if none is specified.
147+
"Validator" - A function that takes the caller specified value for the kwarg,
148+
and validates that it is the correct type, and (for kwargs with
149+
a limited set of allowed values) may also validate that the
150+
kwarg value is one of the allowed values.
151+
'''
152+
vkwargs = {
153+
'box_size' : { 'Default' : 'atr',
154+
'Validator' : lambda value: isinstance(value,(float,int)) or value == 'atr' },
155+
'atr_length' : { 'Default' : 14,
156+
'Validator' : lambda value: isinstance(value,int) },
157+
}
158+
159+
_validate_vkwargs_dict(vkwargs)
160+
161+
return vkwargs
162+
162163
def _construct_ohlc_collections(dates, opens, highs, lows, closes, marketcolors=None):
163164
"""Represent the time, open, high, low, close as a vertical line
164165
ranging from low to high. The left tick is the open and the right
@@ -340,10 +341,9 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
340341
sequence of high values
341342
lows : sequence
342343
sequence of low values
343-
renko_params : dictionary
344-
type : type of renko chart
344+
config_renko_params : kwargs table (dictionary)
345345
brick_size : size of each brick
346-
atr_legnth : length of time used for calculating atr
346+
atr_length : length of time used for calculating atr
347347
closes : sequence
348348
sequence of closing values
349349
marketcolors : dict of colors: up, down, edge, wick, alpha
@@ -379,22 +379,27 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
379379
dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha)
380380
euc = mcolors.to_rgba(marketcolors['edge'][ 'up' ], 1.0)
381381
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
382+
383+
cdiff = []
384+
prev_close_brick = closes[0]
385+
for i in range(len(closes)-1):
386+
brick_diff = int((closes[i+1] - prev_close_brick) / brick_size)
387+
cdiff.append(brick_diff)
388+
prev_close_brick += brick_diff * brick_size
384389

385390
bricks = [] # holds bricks, 1 for down bricks, -1 for up bricks
386391
new_dates = [] # holds the dates corresponding with the index
387392
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.
388393

389-
prev_num = 0
394+
390395
start_price = closes[0]
391396

392397
volume_cache = 0 # holds the volumes for the dates that were skipped
393398

394399
last_diff_sign = 0 # direction the bricks were last going in -1 -> down, 1 -> up
395400
for i in range(len(cdiff)):
396-
num_bricks = abs(int(cdiff[i]))
397-
curr_diff_sign = cdiff[i]/abs(cdiff[i])
401+
num_bricks = abs(cdiff[i])
402+
curr_diff_sign = cdiff[i]/abs(cdiff[i]) if cdiff[i] != 0 else 0
398403
if last_diff_sign != 0 and num_bricks > 0 and curr_diff_sign != last_diff_sign:
399404
num_bricks -= 1
400405
last_diff_sign = curr_diff_sign
@@ -418,6 +423,7 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
418423
colors = []
419424
edge_colors = []
420425
brick_values = []
426+
prev_num = -1 if bricks[0] > 0 else 0
421427
for index, number in enumerate(bricks):
422428
if number == 1: # up brick
423429
colors.append(uc)
@@ -429,6 +435,7 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
429435
prev_num += number
430436
brick_y = start_price + (prev_num * brick_size)
431437
brick_values.append(brick_y)
438+
432439
x, y = index, brick_y
433440

434441
verts.append((
@@ -440,14 +447,144 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
440447
useAA = 0, # use tuple here
441448
lw = None
442449
rectCollection = PolyCollection(verts,
443-
facecolors=colors,
444-
antialiaseds=useAA,
445-
edgecolors=edge_colors,
446-
linewidths=lw
447-
)
450+
facecolors=colors,
451+
antialiaseds=useAA,
452+
edgecolors=edge_colors,
453+
linewidths=lw
454+
)
448455

449456
return (rectCollection, ), new_dates, new_volumes, brick_values, brick_size
450457

458+
def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnfig_params, closes, marketcolors=None):
459+
"""Represent the price change with Xs and Os
460+
461+
Parameters
462+
----------
463+
dates : sequence
464+
sequence of dates
465+
highs : sequence
466+
sequence of high values
467+
lows : sequence
468+
sequence of low values
469+
config_pointnfig_params : kwargs table (dictionary)
470+
box_size : size of each box
471+
atr_length : length of time used for calculating atr
472+
closes : sequence
473+
sequence of closing values
474+
marketcolors : dict of colors: up, down, edge, wick, alpha
475+
476+
Returns
477+
-------
478+
ret : tuple
479+
rectCollection
480+
"""
481+
pointnfig_params = _process_kwargs(config_pointnfig_params, _valid_pointnfig_kwargs())
482+
if marketcolors is None:
483+
marketcolors = _get_mpfstyle('classic')['marketcolors']
484+
print('default market colors:',marketcolors)
485+
486+
box_size = pointnfig_params['box_size']
487+
atr_length = pointnfig_params['atr_length']
488+
489+
490+
if box_size == 'atr':
491+
box_size = _calculate_atr(atr_length, highs, lows, closes)
492+
else: # is an integer or float
493+
total_atr = _calculate_atr(len(closes)-1, highs, lows, closes)
494+
upper_limit = 5*total_atr
495+
lower_limit = 0.01*total_atr
496+
if box_size > upper_limit:
497+
raise ValueError("Specified box_size may not be larger than (1.5* the Average True Value of the dataset) which has value: "+ str(upper_limit))
498+
elif box_size < lower_limit:
499+
raise ValueError("Specified box_size may not be smaller than (0.01* the Average True Value of the dataset) which has value: "+ str(lower_limit))
500+
501+
alpha = marketcolors['alpha']
502+
503+
uc = mcolors.to_rgba(marketcolors['candle'][ 'up' ], alpha)
504+
dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha)
505+
tfc = mcolors.to_rgba(marketcolors['edge']['down'], 0) # transparent face color
506+
507+
cdiff = []
508+
prev_close_box = closes[0]
509+
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.
510+
new_dates = [] # holds the dates corresponding with the index
511+
volume_cache = 0 # holds the volumes for the dates that were skipped
512+
prev_sign = 0
513+
current_cdiff_index = -1
514+
515+
for i in range(len(closes)-1):
516+
box_diff = int((closes[i+1] - prev_close_box) / box_size)
517+
if box_diff == 0:
518+
if volumes is not None:
519+
volume_cache += volumes[i]
520+
continue
521+
sign = box_diff / abs(box_diff)
522+
if sign == prev_sign:
523+
cdiff[current_cdiff_index] += box_diff
524+
if volumes is not None:
525+
new_volumes[current_cdiff_index] += volumes[i] + volume_cache
526+
volume_cache = 0
527+
else:
528+
cdiff.append(box_diff)
529+
if volumes is not None:
530+
new_volumes.append(volumes[i] + volume_cache)
531+
volume_cache = 0
532+
new_dates.append(dates[i])
533+
prev_sign = sign
534+
current_cdiff_index += 1
535+
536+
prev_close_box += box_diff *box_size
537+
538+
539+
curr_price = closes[0]
540+
541+
box_values = [] # y values for the boxes
542+
circle_patches = [] # list of circle patches to be used to create the cirCollection
543+
line_seg = [] # line segments that make up the Xs
544+
545+
for index, difference in enumerate(cdiff):
546+
diff = abs(difference)
547+
548+
sign = (difference / abs(difference)) # -1 or 1
549+
start_iteration = 0 if sign > 0 else 1
550+
551+
x = [index] * (diff)
552+
y = [curr_price + (i * box_size * sign) for i in range(start_iteration, diff+start_iteration)]
553+
554+
555+
curr_price += (box_size * sign * (diff))
556+
box_values.append(sum(y) / len(y))
557+
558+
for i in range(len(x)): # x and y have the same length
559+
height = box_size * 0.85
560+
width = (50/box_size)/len(new_dates)
561+
if height < 0.5:
562+
width = height
563+
564+
padding = (box_size * 0.075)
565+
if sign == 1: # X
566+
line_seg.append([(x[i]-width/2, y[i] + padding), (x[i]+width/2, y[i]+height + padding)]) # create / part of the X
567+
line_seg.append([(x[i]-width/2, y[i]+height+padding), (x[i]+width/2, y[i]+padding)]) # create \ part of the X
568+
else: # O
569+
circle_patches.append(Ellipse((x[i], y[i]-(height/2) - padding), width, height))
570+
571+
useAA = 0, # use tuple here
572+
lw = 0.5
573+
574+
cirCollection = PatchCollection(circle_patches)
575+
cirCollection.set_facecolor([tfc] * len(circle_patches))
576+
cirCollection.set_edgecolor([dc] * len(circle_patches))
577+
578+
xCollection = LineCollection(line_seg,
579+
colors=[uc] * len(line_seg),
580+
linewidths=lw,
581+
antialiaseds=useAA
582+
)
583+
584+
return (cirCollection, xCollection), new_dates, new_volumes, box_values, box_size
585+
586+
587+
451588
from matplotlib.ticker import Formatter
452589
class IntegerIndexDateTimeFormatter(Formatter):
453590
"""

0 commit comments

Comments
 (0)