Skip to content

Commit 43600c3

Browse files
Merge pull request #370 from coffincw/master
Add Point and Figure parameter to customize number of boxes needed to reverse the trend
2 parents 9099dfd + 2dabcd3 commit 43600c3

File tree

7 files changed

+891
-271
lines changed

7 files changed

+891
-271
lines changed

examples/price-movement_plots.ipynb

Lines changed: 131 additions & 247 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.

src/mplfinance/_utils.py

Lines changed: 85 additions & 23 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)
@@ -884,10 +886,11 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
884886
first to ensure every time there is a trend change (ex. previous box is
885887
an X, current brick is a O) we draw one less box to account for the price
886888
having to move the previous box's amount before creating a box in the
887-
opposite direction. Next we adjust volume and dates to combine volume into
888-
non 0 box indexes and to only use dates from non 0 box indexes. We then
889-
remove all 0s from the boxes array and once again combine adjacent similarly
890-
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.
891894
892895
Lastly, we enumerate through the boxes to populate the line_seg and circle_patches
893896
arrays. line_seg holds the / and \ line segments that make up an X and
@@ -931,6 +934,7 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
931934

932935
box_size = pointnfig_params['box_size']
933936
atr_length = pointnfig_params['atr_length']
937+
reversal = pointnfig_params['reversal']
934938

935939
if box_size == 'atr':
936940
if atr_length == 'total':
@@ -945,6 +949,9 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
945949
elif box_size < lower_limit:
946950
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))
947951

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

950957
uc = mcolors.to_rgba(marketcolors['ohlc'][ 'up' ], alpha)
@@ -974,27 +981,82 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
974981
boxes, indexes = combine_adjacent(boxes)
975982
new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes)
976983

977-
#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
978-
first_elem = boxes[0]
979-
boxes = [boxes[i]- int((boxes[i]/abs(boxes[i]))) for i in range(1, len(boxes))]
980-
boxes.insert(0, first_elem)
981-
982-
# adjust volume and dates to make sure volume is combined into non 0 box indexes and only use dates from non 0 box indexes
983-
temp_volumes, temp_dates = [], []
984-
for i in range(len(boxes)):
985-
if boxes[i] == 0:
986-
volume_cache += new_volumes[i]
987-
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)
988997
temp_volumes.append(new_volumes[i] + volume_cache)
989-
volume_cache = 0
990998
temp_dates.append(new_dates[i])
991-
992-
#remove 0s from boxes
993-
boxes = list(filter(lambda diff: diff != 0, boxes))
994999

995-
# combine adjacent similarly signed differences again after 0s removed
996-
boxes, indexes = combine_adjacent(boxes)
997-
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
9981060

9991061
curr_price = closes[0]
10001062
box_values = [] # y values for the boxes

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', 14)
2+
version_info = (0, 12, 7, 'alpha', 15)
33

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

tests/reference_images/pnf05.png

54.4 KB
Loading

tests/test_exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,11 @@ def test_figratio_bounds(bolldata):
8181
with pytest.raises(ValueError) as ex:
8282
mpf.plot(df,volume=True,figratio=(10,51),savefig=buf)
8383
assert '"figratio" (aspect ratio) must be between' in str(ex.value)
84+
85+
def test_reversal_box_size_bounds(bolldata):
86+
df = bolldata
87+
buf = io.BytesIO()
88+
mpf.plot(df,type='pnf',pnf_params=dict(box_size=3, reversal=3), volume=True, savefig=buf)
89+
with pytest.raises(ValueError) as ex:
90+
mpf.plot(df,type='pnf',pnf_params=dict(box_size=3, reversal=10), volume=True, savefig=buf)
91+
assert 'Specified reversal must be an integer in the range [1,9]' in str(ex.value)

tests/test_pnf.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,23 @@ def test_pnf04(bolldata):
109109
if result is not None:
110110
print('result=',result)
111111
assert result is None
112+
113+
def test_pnf05(bolldata):
114+
df = bolldata
115+
116+
fname = base+'05.png'
117+
tname = os.path.join(tdir,fname)
118+
rname = os.path.join(refd,fname)
119+
120+
mpf.plot(df,type='pnf',pnf_params=dict(box_size='atr',atr_length='total', reversal=2),mav=(4,6,8),volume=True,savefig=tname)
121+
122+
tsize = os.path.getsize(tname)
123+
print(glob.glob(tname),'[',tsize,'bytes',']')
124+
125+
rsize = os.path.getsize(rname)
126+
print(glob.glob(rname),'[',rsize,'bytes',']')
127+
128+
result = compare_images(rname,tname,tol=IMGCOMP_TOLERANCE)
129+
if result is not None:
130+
print('result=',result)
131+
assert result is None

0 commit comments

Comments
 (0)