Skip to content

Commit 3956a92

Browse files
Merge branch 'master' of github.com:matplotlib/mplfinance
2 parents 87fd163 + 43600c3 commit 3956a92

File tree

12 files changed

+1997
-314
lines changed

12 files changed

+1997
-314
lines changed

RELEASE_NOTES.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
####
22
---
33

4-
### <a name="v0.12.7a11"></a>v0.12.7a11 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; To Be released 2021-03-30
4+
### <a name="v0.12.7a12"></a>v0.12.7a12 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; released 2021-04-09
55

6+
- <a name="v0.12.7a12"></a>v0.12.7a12 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; merged 2021-04-08
7+
- Fixed kwarg **`return_calculated_values`**
8+
- It was *not* returning all values for `type='pnf'` (point and figure).
9+
- See **[stackoverflow 66991998](https://stackoverflow.com/questions/66991998/)** for more information.
610
- <a name="v0.12.7a11"></a>v0.12.7a11 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; merged 2021-03-26
711
- Prior to this version, **`xlim`** kwarg accepted only float or int:
812
- float as matplotlib date; (only when `show_nontrading=True`)

examples/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.png
2+
*.jpg

examples/price-movement_plots.ipynb

Lines changed: 131 additions & 247 deletions
Large diffs are not rendered by default.

examples/savefig.ipynb

Lines changed: 54 additions & 22 deletions
Large diffs are not rendered by default.

examples/scratch_pad/pnf_reversal.ipynb

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

examples/scratch_pad/price-movement_ret_calc_vals.ipynb

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

src/mplfinance/_utils.py

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,9 @@ def _valid_pnf_kwargs():
389389
'box_size' : { 'Default' : 'atr',
390390
'Validator' : lambda value: isinstance(value,(float,int)) or value == 'atr' },
391391
'atr_length' : { 'Default' : 14,
392-
'Validator' : lambda value: isinstance(value,int) or value == 'total' },
392+
'Validator' : lambda value: isinstance(value,int) or value == 'total' },
393+
'reversal' : { 'Default' : 1,
394+
'Validator' : lambda value: isinstance(value,int) }
393395
}
394396

395397
_validate_vkwargs_dict(vkwargs)
@@ -855,7 +857,9 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
855857
edgecolors=edge_colors,
856858
linewidths=lw
857859
)
858-
return [rectCollection,], new_dates, new_volumes, brick_values, brick_size
860+
calculated_values = dict(dates=new_dates,volumes=new_volumes,
861+
values=brick_values,size=brick_size)
862+
return [rectCollection,], calculated_values
859863

860864

861865
def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnfig_params, closes, marketcolors=None):
@@ -882,10 +886,11 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
882886
first to ensure every time there is a trend change (ex. previous box is
883887
an X, current brick is a O) we draw one less box to account for the price
884888
having to move the previous box's amount before creating a box in the
885-
opposite direction. Next we adjust volume and dates to combine volume into
886-
non 0 box indexes and to only use dates from non 0 box indexes. We then
887-
remove all 0s from the boxes array and once again combine adjacent similarly
888-
signed differences in boxes.
889+
opposite direction. During this same step we also combine like signed elements
890+
and associated volume/date data ignoring any zero values that are created by
891+
subtracting 1 from the box value. Next we recreate the box array utilizing a
892+
rolling_change and volume_cache to store and sum the changes that don't break
893+
the reversal threshold.
889894
890895
Lastly, we enumerate through the boxes to populate the line_seg and circle_patches
891896
arrays. line_seg holds the / and \ line segments that make up an X and
@@ -929,6 +934,7 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
929934

930935
box_size = pointnfig_params['box_size']
931936
atr_length = pointnfig_params['atr_length']
937+
reversal = pointnfig_params['reversal']
932938

933939
if box_size == 'atr':
934940
if atr_length == 'total':
@@ -943,6 +949,9 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
943949
elif box_size < lower_limit:
944950
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))
945951

952+
if reversal < 1 or reversal > 9:
953+
raise ValueError("Specified reversal must be an integer in the range [1,9]")
954+
946955
alpha = marketcolors['alpha']
947956

948957
uc = mcolors.to_rgba(marketcolors['ohlc'][ 'up' ], alpha)
@@ -972,33 +981,88 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
972981
boxes, indexes = combine_adjacent(boxes)
973982
new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes)
974983

975-
#subtract 1 from the abs of each diff except the first to account for the first box using the last box in the opposite direction
976-
first_elem = boxes[0]
977-
boxes = [boxes[i]- int((boxes[i]/abs(boxes[i]))) for i in range(1, len(boxes))]
978-
boxes.insert(0, first_elem)
979-
980-
# adjust volume and dates to make sure volume is combined into non 0 box indexes and only use dates from non 0 box indexes
981-
temp_volumes, temp_dates = [], []
982-
for i in range(len(boxes)):
983-
if boxes[i] == 0:
984-
volume_cache += new_volumes[i]
985-
else:
984+
adjusted_boxes = [boxes[0]]
985+
temp_volumes, temp_dates = [new_volumes[0]], [new_dates[0]]
986+
volume_cache = 0
987+
988+
# Clean data to subtract 1 from all box # not including the first boxes element and combine like signed adjacent values (after ignoring zeros)
989+
for i in range(1, len(boxes)):
990+
adjusted_value = boxes[i]- int((boxes[i]/abs(boxes[i])))
991+
992+
# not equal to 0 and different signs
993+
if adjusted_value != 0 and adjusted_boxes[-1]*adjusted_value < 0:
994+
995+
# Append adjusted_value, volumes, and date to associated lists
996+
adjusted_boxes.append(adjusted_value)
986997
temp_volumes.append(new_volumes[i] + volume_cache)
987-
volume_cache = 0
988998
temp_dates.append(new_dates[i])
989-
990-
#remove 0s from boxes
991-
boxes = list(filter(lambda diff: diff != 0, boxes))
992999

993-
# combine adjacent similarly signed differences again after 0s removed
994-
boxes, indexes = combine_adjacent(boxes)
995-
new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes)
1000+
# reset volume_cache once we use it
1001+
volume_cache = 0
1002+
1003+
# not equal to 0 and same signs
1004+
elif adjusted_value != 0 and adjusted_boxes[-1]*adjusted_value > 0:
1005+
1006+
# Add adjusted_value and volume values to last added elements
1007+
adjusted_boxes[-1] += adjusted_value
1008+
temp_volumes[-1] += new_volumes[i] + volume_cache
1009+
1010+
# reset volume_cache once we use it
1011+
volume_cache = 0
1012+
1013+
else: # adjusted_value == 0
1014+
volume_cache += new_volumes[i]
1015+
1016+
boxes = [adjusted_boxes[0]]
1017+
new_volumes = [temp_volumes[0]]
1018+
new_dates = [temp_dates[0]]
1019+
1020+
rolling_change = 0
1021+
volume_cache = 0
1022+
biggest_difference = 0 # only used for the last column
1023+
1024+
#Clean data to account for reversal size (added to allow overriding the default reversal of 1)
1025+
for i in range(1, len(adjusted_boxes)):
1026+
1027+
# Add to rolling_change and volume_cache which stores the box and volume values
1028+
rolling_change += adjusted_boxes[i]
1029+
volume_cache += temp_volumes[i]
1030+
1031+
# if rolling_change is the same sign as the previous box and the abs value is bigger than the
1032+
# abs value of biggest_difference then we should replace biggest_difference with rolling_change
1033+
if rolling_change*boxes[-1] > 0 and abs(rolling_change) > abs(biggest_difference):
1034+
biggest_difference = rolling_change
1035+
1036+
# Add to new list if the rolling change is >= the reversal
1037+
if abs(rolling_change) >= reversal:
1038+
1039+
# if rolling_change is the same sign as the previous # of boxes then combine
1040+
if rolling_change*boxes[-1] > 0:
1041+
boxes[-1] += rolling_change
1042+
new_volumes[-1] += volume_cache
1043+
1044+
# otherwise add new box
1045+
else: # < 0 (== 0 can't happen since neither rolling_change or boxes[-1] can be 0)
1046+
boxes.append(rolling_change)
1047+
new_volumes.append(volume_cache)
1048+
new_dates.append(temp_dates[i])
1049+
1050+
# reset rolling_change and volume_cache once we've used them
1051+
rolling_change = 0
1052+
volume_cache = 0
1053+
1054+
# reset biggest_difference as we start from the beginning every time there is a reversal
1055+
biggest_difference = 0
1056+
1057+
# Adjust the last box column if the left over rolling_change is the same sign as the column
1058+
boxes[-1] += biggest_difference
1059+
new_volumes[-1] += volume_cache
9961060

9971061
curr_price = closes[0]
9981062
box_values = [] # y values for the boxes
9991063
circle_patches = [] # list of circle patches to be used to create the cirCollection
10001064
line_seg = [] # line segments that make up the Xs
1001-
1065+
10021066
for index, difference in enumerate(boxes):
10031067
diff = abs(difference)
10041068

@@ -1007,9 +1071,9 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
10071071

10081072
x = [index] * (diff)
10091073
y = [curr_price + (i * box_size * sign) for i in range(start_iteration, diff+start_iteration)]
1010-
1074+
10111075
curr_price += (box_size * sign * (diff))
1012-
box_values.append(sum(y) / len(y))
1076+
box_values.append( y )
10131077

10141078
for i in range(len(x)): # x and y have the same length
10151079
height = box_size * 0.85
@@ -1036,7 +1100,9 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
10361100
linewidths=lw,
10371101
antialiaseds=useAA
10381102
)
1039-
return [cirCollection, xCollection], new_dates, new_volumes, box_values, box_size
1103+
calculated_values = dict(dates=new_dates,counts=boxes,values=box_values,
1104+
volumes=new_volumes,size=box_size)
1105+
return [cirCollection, xCollection], calculated_values
10401106

10411107

10421108
def _construct_aline_collections(alines, dtix=None):

src/mplfinance/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11

2-
version_info = (0, 12, 7, 'alpha', 12)
2+
version_info = (0, 12, 7, 'alpha', 15)
33

44
_specifier_ = {'alpha': 'a','beta': 'b','candidate': 'rc','final': ''}
55

src/mplfinance/plotting.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import numpy as np
88
import copy
99
import io
10+
import os
1011
import math
1112
import warnings
1213
import statistics as stat
@@ -168,7 +169,7 @@ def _valid_plot_kwargs():
168169
'Validator' : lambda value: isinstance(value,dict) or (isinstance(value,list) and all([isinstance(d,dict) for d in value])) },
169170

170171
'savefig' : { 'Default' : None,
171-
'Validator' : lambda value: isinstance(value,dict) or isinstance(value,str) or isinstance(value, io.BytesIO) },
172+
'Validator' : lambda value: isinstance(value,dict) or isinstance(value,str) or isinstance(value, io.BytesIO) or isinstance(value, os.PathLike) },
172173

173174
'block' : { 'Default' : None,
174175
'Validator' : lambda value: isinstance(value,bool) },
@@ -400,16 +401,25 @@ def plot( data, **kwargs ):
400401
collections =_construct_mpf_collections(ptype,dates,xdates,opens,highs,lows,closes,volumes,config,style)
401402

402403
if ptype in VALID_PMOVE_TYPES:
403-
collections, new_dates, volumes, brick_values, size = collections
404-
formatter = IntegerIndexDateTimeFormatter(new_dates, fmtstring)
405-
xdates = np.arange(len(new_dates))
404+
collections, calculated_values = collections
405+
volumes = calculated_values['volumes']
406+
pmove_dates = calculated_values['dates']
407+
pmove_values = calculated_values['values']
408+
if all([isinstance(v,(list,tuple)) for v in pmove_values]):
409+
pmove_avgvals = [sum(v)/len(v) for v in pmove_values]
410+
else:
411+
pmove_avgvals = pmove_values
412+
pmove_size = calculated_values['size']
413+
pmove_counts = calculated_values['counts'] if 'counts' in calculated_values else None
414+
formatter = IntegerIndexDateTimeFormatter(pmove_dates, fmtstring)
415+
xdates = np.arange(len(pmove_dates))
406416

407417
if collections is not None:
408418
for collection in collections:
409419
axA1.add_collection(collection)
410420

411421
if ptype in VALID_PMOVE_TYPES:
412-
mavprices = _plot_mav(axA1,config,xdates,brick_values)
422+
mavprices = _plot_mav(axA1,config,xdates,pmove_avgvals)
413423
else:
414424
mavprices = _plot_mav(axA1,config,xdates,closes)
415425

@@ -428,8 +438,8 @@ def plot( data, **kwargs ):
428438
_lows = lows
429439
_highs = highs
430440
else:
431-
_lows = brick_values
432-
_highs = [brick+size for brick in brick_values]
441+
_lows = pmove_avgvals
442+
_highs = [value+pmove_size for value in pmove_avgvals]
433443

434444
miny = np.nanmin(_lows)
435445
maxy = np.nanmax(_highs)
@@ -458,13 +468,18 @@ def plot( data, **kwargs ):
458468

459469
if config['return_calculated_values'] is not None:
460470
retdict = config['return_calculated_values']
461-
if ptype in VALID_PMOVE_TYPES:
462-
prekey = ptype
463-
retdict[prekey+'_bricks'] = brick_values
464-
retdict[prekey+'_dates'] = mdates.num2date(new_dates)
465-
retdict[prekey+'_size'] = size
466-
if config['volume']:
467-
retdict[prekey+'_volumes'] = volumes
471+
if ptype == 'renko':
472+
retdict['renko_bricks' ] = pmove_values
473+
retdict['renko_dates' ] = mdates.num2date(pmove_dates)
474+
retdict['renko_size' ] = pmove_size
475+
retdict['renko_volumes'] = volumes if config['volume'] else None
476+
elif ptype == 'pnf':
477+
retdict['pnf_dates' ] = mdates.num2date(pmove_dates)
478+
retdict['pnf_counts' ] = pmove_counts
479+
retdict['pnf_values' ] = pmove_values
480+
retdict['pnf_avgvals' ] = pmove_avgvals
481+
retdict['pnf_size' ] = pmove_size
482+
retdict['pnf_volumes' ] = volumes if config['volume'] else None
468483
if config['mav'] is not None:
469484
mav = config['mav']
470485
if len(mav) != len(mavprices):
@@ -480,7 +495,7 @@ def plot( data, **kwargs ):
480495
# Note: these are NOT mutually exclusive, so the order of this
481496
# if/elif is important: VALID_PMOVE_TYPES must be first.
482497
if ptype in VALID_PMOVE_TYPES:
483-
dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(new_dates)])
498+
dtix = pd.DatetimeIndex([dt for dt in mdates.num2date(pmove_dates)])
484499
elif not config['show_nontrading']:
485500
dtix = data.index
486501
else:

tests/reference_images/pnf05.png

54.4 KB
Loading

0 commit comments

Comments
 (0)