Skip to content

Commit b49e0e0

Browse files
committed
fix price move plots, size limits, +'total' option
1 parent 0da835c commit b49e0e0

File tree

5 files changed

+228
-138
lines changed

5 files changed

+228
-138
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,15 +203,17 @@ mpf.plot(daily,type='line')
203203

204204

205205
```python
206-
mpf.plot(daily,type='renko')
206+
year = pd.read_csv('examples/data/SPY_20110701_20120630_Bollinger.csv',index_col=0,parse_dates=True)
207+
year.index.name = 'Date'
208+
mpf.plot(year,type='renko')
207209
```
208210

209211

210212
![png](https://raw.githubusercontent.com/matplotlib/mplfinance/master/readme_files/readme_8_1.png)
211213

212214

213215
```python
214-
mpf.plot(daily,type='pnf')
216+
mpf.plot(year,type='pnf')
215217
```
216218

217219

examples/price-movement_plots.ipynb

Lines changed: 79 additions & 57 deletions
Large diffs are not rendered by default.

readme_files/readme_5_1.png

19.4 KB
Loading

readme_files/readme_8_1.png

7.26 KB
Loading

src/mplfinance/_utils.py

Lines changed: 145 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,51 @@ def _calculate_atr(atr_length, highs, lows, closes):
103103
atr += tr
104104
return atr/atr_length
105105

106+
def combine_adjacent(arr):
107+
"""Sum like signed adjacent elements
108+
arr : starting array
109+
110+
Returns
111+
-------
112+
output: new summed array
113+
indexes: indexes indicating the first
114+
element summed for each group in arr
115+
"""
116+
output, indexes = [], []
117+
curr_i = 0
118+
while len(arr) > 0:
119+
curr_sign = arr[0]/abs(arr[0])
120+
index = 0
121+
while index < len(arr) and arr[index]/abs(arr[index]) == curr_sign:
122+
index += 1
123+
output.append(sum(arr[:index]))
124+
indexes.append(curr_i)
125+
curr_i += index
126+
127+
for _ in range(index):
128+
arr.pop(0)
129+
return output, indexes
130+
131+
def coalesce_volume_dates(in_volumes, in_dates, indexes):
132+
"""Sums volumes between the indexes and ouputs
133+
dates at the indexes
134+
in_volumes : original volume list
135+
in_dates : original dates list
136+
indexes : list of indexes
137+
138+
Returns
139+
-------
140+
volumes: new volume array
141+
dates: new dates array
142+
"""
143+
volumes, dates = [], []
144+
for i in range(len(indexes)):
145+
dates.append(in_dates[indexes[i]])
146+
to_sum_to = indexes[i+1] if i+1 < len(indexes) else len(in_volumes)
147+
volumes.append(sum(in_volumes[indexes[i]:to_sum_to]))
148+
return volumes, dates
149+
150+
106151
def _updown_colors(upcolor,downcolor,opens,closes,use_prev_close=False):
107152
if upcolor == downcolor:
108153
return upcolor
@@ -130,7 +175,7 @@ def _valid_renko_kwargs():
130175
'brick_size' : { 'Default' : 'atr',
131176
'Validator' : lambda value: isinstance(value,(float,int)) or value == 'atr' },
132177
'atr_length' : { 'Default' : 14,
133-
'Validator' : lambda value: isinstance(value,int) },
178+
'Validator' : lambda value: isinstance(value,int) or value == 'total' },
134179
}
135180

136181
_validate_vkwargs_dict(vkwargs)
@@ -153,7 +198,7 @@ def _valid_pointnfig_kwargs():
153198
'box_size' : { 'Default' : 'atr',
154199
'Validator' : lambda value: isinstance(value,(float,int)) or value == 'atr' },
155200
'atr_length' : { 'Default' : 14,
156-
'Validator' : lambda value: isinstance(value,int) },
201+
'Validator' : lambda value: isinstance(value,int) or value == 'total' },
157202
}
158203

159204
_validate_vkwargs_dict(vkwargs)
@@ -363,67 +408,76 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
363408

364409

365410
if brick_size == 'atr':
366-
brick_size = _calculate_atr(atr_length, highs, lows, closes)
411+
if atr_length == 'total':
412+
brick_size = _calculate_atr(len(closes)-1, highs, lows, closes)
413+
else:
414+
brick_size = _calculate_atr(atr_length, highs, lows, closes)
367415
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
416+
upper_limit = (max(closes) - min(closes)) / 5
417+
lower_limit = (max(closes) - min(closes)) / 32
371418
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))
419+
raise ValueError("Specified brick_size may not be larger than (20% of the close price range of the dataset) which has value: "+ str(upper_limit))
373420
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))
421+
raise ValueError("Specified brick_size may not be smaller than (3.125% of the close price range of the dataset) which has value: "+ str(lower_limit))
375422

376423
alpha = marketcolors['alpha']
377424

378425
uc = mcolors.to_rgba(marketcolors['candle'][ 'up' ], alpha)
379426
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)
427+
euc = mcolors.to_rgba(marketcolors['edge'][ 'up' ], 1.0)
428+
edc = mcolors.to_rgba(marketcolors['edge']['down'], 1.0)
382429

383430
cdiff = []
384431
prev_close_brick = closes[0]
432+
volume_cache = 0 # holds the volumes for the dates that were skipped
433+
new_dates = [] # holds the dates corresponding with the index
434+
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.
435+
385436
for i in range(len(closes)-1):
386437
brick_diff = int((closes[i+1] - prev_close_brick) / brick_size)
387-
cdiff.append(brick_diff)
388-
prev_close_brick += brick_diff * brick_size
438+
if brick_diff == 0:
439+
if volumes is not None:
440+
volume_cache += volumes[i]
441+
continue
389442

390-
bricks = [] # holds bricks, 1 for down bricks, -1 for up bricks
391-
new_dates = [] # holds the dates corresponding with the index
392-
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.
443+
cdiff.extend([int(brick_diff/abs(brick_diff))] * abs(brick_diff))
444+
if volumes is not None:
445+
new_volumes.extend([volumes[i] + volume_cache] * abs(brick_diff))
446+
volume_cache = 0
447+
new_dates.extend([dates[i]] * abs(brick_diff))
448+
prev_close_brick += brick_diff *brick_size
393449

394-
395-
start_price = closes[0]
450+
bricks = [] # holds bricks, 1 for down bricks, -1 for up bricks
451+
curr_price = closes[0]
396452

397-
volume_cache = 0 # holds the volumes for the dates that were skipped
398-
399453
last_diff_sign = 0 # direction the bricks were last going in -1 -> down, 1 -> up
400-
for i in range(len(cdiff)):
401-
num_bricks = abs(cdiff[i])
402-
curr_diff_sign = cdiff[i]/abs(cdiff[i]) if cdiff[i] != 0 else 0
403-
if last_diff_sign != 0 and num_bricks > 0 and curr_diff_sign != last_diff_sign:
404-
num_bricks -= 1
454+
dates_volumes_index = 0 # keeps track of the index of the current date/volume
455+
for diff in cdiff:
456+
457+
curr_diff_sign = diff/abs(diff)
458+
if last_diff_sign != 0 and curr_diff_sign != last_diff_sign:
459+
last_diff_sign = curr_diff_sign
460+
new_dates.pop(dates_volumes_index)
461+
if volumes is not None:
462+
if dates_volumes_index == len(new_volumes)-1:
463+
new_volumes[dates_volumes_index-1] += new_volumes[dates_volumes_index]
464+
else:
465+
new_volumes[dates_volumes_index+1] += new_volumes[dates_volumes_index]
466+
new_volumes.pop(dates_volumes_index)
467+
continue
405468
last_diff_sign = curr_diff_sign
406-
407-
if num_bricks != 0:
408-
new_dates.extend([dates[i]]*num_bricks)
409-
410-
if volumes is not None: # only adds volumes if there are volume values when volume=True
411-
if num_bricks != 0:
412-
new_volumes.extend([volumes[i] + volume_cache]*num_bricks)
413-
volume_cache = 0
414-
else:
415-
volume_cache += volumes[i]
416-
417-
if cdiff[i] > 0:
418-
bricks.extend([1]*num_bricks)
469+
470+
if diff > 0:
471+
bricks.extend([1]*abs(diff))
419472
else:
420-
bricks.extend([-1]*num_bricks)
473+
bricks.extend([-1]*abs(diff))
474+
dates_volumes_index += 1
421475

422-
verts = []
423-
colors = []
424-
edge_colors = []
425-
brick_values = []
426-
prev_num = -1 if bricks[0] > 0 else 0
476+
477+
verts = [] # holds the brick vertices
478+
colors = [] # holds the facecolors for each brick
479+
edge_colors = [] # holds the edgecolors for each brick
480+
brick_values = [] # holds the brick values for each brick
427481
for index, number in enumerate(bricks):
428482
if number == 1: # up brick
429483
colors.append(uc)
@@ -432,11 +486,10 @@ def _construct_renko_collections(dates, highs, lows, volumes, config_renko_param
432486
colors.append(dc)
433487
edge_colors.append(edc)
434488

435-
prev_num += number
436-
brick_y = start_price + (prev_num * brick_size)
437-
brick_values.append(brick_y)
489+
curr_price += (brick_size * number)
490+
brick_values.append(curr_price)
438491

439-
x, y = index, brick_y
492+
x, y = index, curr_price
440493

441494
verts.append((
442495
(x, y),
@@ -488,61 +541,75 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
488541

489542

490543
if box_size == 'atr':
491-
box_size = _calculate_atr(atr_length, highs, lows, closes)
544+
if atr_length == 'total':
545+
box_size = _calculate_atr(len(closes)-1, highs, lows, closes)
546+
else:
547+
box_size = _calculate_atr(atr_length, highs, lows, closes)
492548
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
549+
upper_limit = (max(closes) - min(closes)) / 5
550+
lower_limit = (max(closes) - min(closes)) / 32
496551
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))
552+
raise ValueError("Specified box_size may not be larger than (20% of the close price range of the dataset) which has value: "+ str(upper_limit))
498553
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))
554+
raise ValueError("Specified box_size may not be smaller than (3.125% of the close price range of the dataset) which has value: "+ str(lower_limit))
500555

501556
alpha = marketcolors['alpha']
502557

503558
uc = mcolors.to_rgba(marketcolors['candle'][ 'up' ], alpha)
504559
dc = mcolors.to_rgba(marketcolors['candle']['down'], alpha)
505560
tfc = mcolors.to_rgba(marketcolors['edge']['down'], 0) # transparent face color
506561

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
562+
boxes = [] # each element in an integer representing the number of boxes to be drawn on that indexes column (negative numbers -> Os, positive numbers -> Xs)
563+
prev_close_box = closes[0] # represents the value of the last box in the previous column
511564
volume_cache = 0 # holds the volumes for the dates that were skipped
512-
prev_sign = 0
513-
current_cdiff_index = -1
514-
565+
temp_volumes, temp_dates = [], [] # holds the temp adjusted volumes and dates respectively
566+
515567
for i in range(len(closes)-1):
516568
box_diff = int((closes[i+1] - prev_close_box) / box_size)
517569
if box_diff == 0:
518570
if volumes is not None:
519571
volume_cache += volumes[i]
520572
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-
573+
574+
boxes.append(box_diff)
575+
if volumes is not None:
576+
temp_volumes.append(volumes[i] + volume_cache)
577+
volume_cache = 0
578+
temp_dates.append(dates[i])
536579
prev_close_box += box_diff *box_size
537580

581+
# combine adjacent similarly signed differences
582+
boxes, indexes = combine_adjacent(boxes)
583+
new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes)
584+
585+
#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
586+
first_elem = boxes[0]
587+
boxes = [boxes[i]- int((boxes[i]/abs(boxes[i]))) for i in range(1, len(boxes))]
588+
boxes.insert(0, first_elem)
589+
590+
# adjust volume and dates to make sure volume is combined into non 0 box indexes and only use dates from non 0 box indexes
591+
temp_volumes, temp_dates = [], []
592+
for i in range(len(boxes)):
593+
if boxes[i] == 0:
594+
volume_cache += new_volumes[i]
595+
else:
596+
temp_volumes.append(new_volumes[i] + volume_cache)
597+
volume_cache = 0
598+
temp_dates.append(new_dates[i])
599+
600+
#remove 0s from boxes
601+
boxes = list(filter(lambda diff: diff != 0, boxes))
602+
603+
# combine adjacent similarly signed differences again after 0s removed
604+
boxes, indexes = combine_adjacent(boxes)
605+
new_volumes, new_dates = coalesce_volume_dates(temp_volumes, temp_dates, indexes)
538606

539607
curr_price = closes[0]
540-
541608
box_values = [] # y values for the boxes
542609
circle_patches = [] # list of circle patches to be used to create the cirCollection
543610
line_seg = [] # line segments that make up the Xs
544611

545-
for index, difference in enumerate(cdiff):
612+
for index, difference in enumerate(boxes):
546613
diff = abs(difference)
547614

548615
sign = (difference / abs(difference)) # -1 or 1
@@ -551,13 +618,12 @@ def _construct_pointnfig_collections(dates, highs, lows, volumes, config_pointnf
551618
x = [index] * (diff)
552619
y = [curr_price + (i * box_size * sign) for i in range(start_iteration, diff+start_iteration)]
553620

554-
555621
curr_price += (box_size * sign * (diff))
556622
box_values.append(sum(y) / len(y))
557623

558624
for i in range(len(x)): # x and y have the same length
559625
height = box_size * 0.85
560-
width = (50/box_size)/len(new_dates)
626+
width = 0.6
561627
if height < 0.5:
562628
width = height
563629

0 commit comments

Comments
 (0)