The post today focuses on neither Backtrader or Tradingview (could pigs be flying?). Instead, we are going to create a tool that can be used to analyze which price levels attract the most volume for a particular stock. This is known generally as “Volume at Price”.
Yes, I know Tradingview has such an indicator for Pro subscribers. However, not all of the Backtest-Rookies audience are Tradingview subscribers.
It is also worth noting that this post is not aimed at the beginner. The code sample can certainly be used by anyone but understanding each part of the code might be a stretch if you are new.
Volume At Price
Volume at price indicators simply shows which price levels see the most volume. Some will break it down into buying and selling volume if that data is available. When using volume at price analysis, traders can confirm key support or resistance levels as market participants rush to buy stock when it is considered cheap or conversely sell it when taking profits.
Setup
As mentioned in the introduction. The code in this post does not use Backtrader. Instead, we start working with the following packages/libraries:
- Quandl
- Pandas
- Numpy
- Matplotlib
Quandl will be used for downloading example data. Once you are comfortable with the code, it can easily be replaced with a CSV file or other source. Panda’s and Numpy are used for working with the data we download from Quandl. Finally, Matplotlib is used to plot the final chart. It is worth noting that Pandas, Numpy and Matplotlib go hand in hand and are used extensively by those in the data science and technology communities.
All of these packages are available via pip
. To install them simply:
pip install <insert package name>
Remember to replace pip
with pip3
if you already have python2
installed.
References:
Pandas: https://pandas.pydata.org/
Numpy: http://www.numpy.org/
Matplotlib: https://matplotlib.org/
Disclaimer:
Matplotlib, Pandas, and Numpy are packages that I tend to use only when I have a specific task in mind. I don’t live in these libraries. They are all large, powerful and worthy of many blogs of their own. As such, do not assume the code examples in this post follow the best practices for those libraries.
The Code
Without further ado, here is the code. We will discuss more in the commentary below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
''' Author: www.backtest-rookies.com MIT License Copyright (c) 2018 backtest-rookies.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ''' import quandl import pandas as pd from datetime import datetime import matplotlib.pyplot as plt from matplotlib.ticker import FuncFormatter import numpy as np import argparse def parse_args(): parser = argparse.ArgumentParser(description='Volume at Price Chart') parser.add_argument('qcode', help='The Quandle code, including database \ prefix e.g. WIKI/F of the instrument being tested') parser.add_argument('start', help='Starting date in YYYY-MM-DD format') parser.add_argument('end', help='Ending date in YYYY-MM-DD format') parser.add_argument('round', type=int, help='Round to the Nearest') return parser.parse_args() def get_quandl_data(ticker, start, end): data = quandl.get(ticker, start_date=start, end_date=end) return data def custom_round(x, base=5): return int(base * round(float(x)/base)) def round_and_group(df, base=5): # https://stackoverflow.com/questions/40372030/pandas-round-to-the-nearest-n # Extract the data we want df = data[['Close', 'Volume']].copy() # Round to nearest X df['Close'] = df['Close'].apply(lambda x: custom_round(x, base=base)) # Remove the date index df = df.set_index('Close') df = df.groupby(['Close']).sum() return df def thousands(x, pos): 'The two args are the value and tick position' return '%1.0fK' % (x*1e-3) def create_plot(x_series, x_label, y_pos, y_tick_labels, colour, title): # Excample horizontal bar chart code taken from: # https://matplotlib.org/gallery/lines_bars_and_markers/barh.html plt.rcdefaults() fig, ax = plt.subplots() ax.barh(y_pos, x_series, align='center',color=colour) ax.set_yticks(y_pos) ax.set_yticklabels(y_tick_labels) formatter = FuncFormatter(thousands) ax.xaxis.set_major_formatter(formatter) ax.set_xlabel(x_label) ax.set_title(title) plt.xticks(rotation=325) plt.show() # Setup api quandl_api = "INSERT YOUR API KEY" quandl.ApiConfig.api_key = quandl_api #Get Args args = parse_args() print("VOLUME AT PRICE") print("QCODE: {}".format(args.qcode)) print("START: {}".format(args.start)) print("END: {}".format(args.end)) # Get data data = get_quandl_data(args.qcode, args.start, args.end) # Massage the data data = round_and_group(data, base=args.round) # Prepare data for the chart y_pos = np.arange(len(data.index.values)) # Get the chart plt = create_plot( data['Volume'], # x_series 'Volume', # x_label y_pos, # Y positioning data.index.values, # y_tick_labels 'Green', # Bar color 'VOLUME AT PRICE: {}'.format(args.qcode) # Title ) |
Commentary
Quandl is used as a convenient, predictable data source for sharing the code online. Using Quandl ensures everyone can use the example code. It was not selected for any other reason. In fact, some people may wish to replace Quandl with their own data source or even CSV files. Note that if you do decide to use Quandl at a source you should add your API key or remove the following two lines. However, if you do remove them, you shall be limited to a certain number of API calls per day.
1 2 3 |
# Setup api quandl_api = "INSERT YOUR API KEY" quandl.ApiConfig.api_key = quandl_api |
Once we have our data we need to trim it for the parts that are useful. For volume at price charts, we only need the volume
and close
data. Note that some people may wish to change this the HL2
(high + low / 2) to give a more central point for the volume traded in that day/bar. Others may prefer something completely different. After all, the traded volume was unlikely to all happen at the close price.
After we have extracted a copy of the data we want to work with, here comes the tricky part. We now have the volume for close of every bar but it is just a chronological list of values. Everything is still indexed by date. We want to be able to ignore the date something happened and instead focus on the price levels.
So to tackle this, the first thing we do is round every close price to a reasonable level for the data we are working with. What does that mean? Well, we probably don’t want to see the volume $1 price levels for a stock measured over 15 years and now trades for $700 a share. We would end up with data overload on the chart with very little visibility of key levels. Instead of $10, $20 or $50 dollar steps might be more appropriate.
Using thecustom_round()
function, we are able to specify which key levels we want to focus on. The function is applied to the Close
column of the Pandas dataframe and rounds each Close
to an appropriate nearby level. Full credit where it is due for this part of the code. It came from Andy on StackOverflow with a great example of how to “round to the nearest N” in Pandas.
So now we have centered each piece of volume around a key level. However, we are now left with lots of entries at the same price level. Therefore, we must now group and sum the rows. To do this we group by the Close
price and sum the volume levels.
Finally, once we have our data ready, all that is left to do is to plot it! Again, the horizontal bar chart is not entirely my own. It was taken mostly from the standard example on the MatplotLib website. See here: https://matplotlib.org/gallery/lines_bars_and_markers/barh.html
Testing
In order to build confidence in the rounding and summing, we need to perform some controlled tests on a small selection of data. To do this, we will go through a full example, checking the raw data and verifying it is correct at each stage described above.
Note: This section assumes you have saved the code as vol_at_price.py
.
Let’s take a look at Amazon over a short period of time to make verification a little simpler.
python3 vol_at_price.py WIKI/AMZN 2018-03-01 2018-03-20 5
Now let’s add some print statements to the round_and_group()
function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def round_and_group(df, base=5): # Extract the data we want df = data[['Close', 'Volume']].copy() print('Before Rounding') print(df) # Round to nearest X df['Close'] = df['Close'].apply(lambda x: custom_round(x, base=base)) print('Before Summing') print(df) # Remove the date index df = df.set_index('Close') df = df.groupby(['Close']).sum() print('After Summing') print(df) return df |
Then run the script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
Volume At Price QCODE: WIKI/AMZN START: 2018-03-01 END: 2018-03-20 Before Rounding Close Volume Date 2018-03-01 1493.45 6835230.0 2018-03-02 1500.25 6587564.0 2018-03-05 1523.61 5233934.0 2018-03-06 1537.64 4561718.0 2018-03-07 1545.00 4174123.0 2018-03-08 1551.86 3512528.0 2018-03-09 1578.89 4417059.0 2018-03-12 1598.39 5115886.0 2018-03-13 1588.18 6427066.0 2018-03-14 1591.00 4164395.0 2018-03-15 1582.32 4026744.0 2018-03-16 1571.68 5145054.0 2018-03-19 1544.93 6376619.0 2018-03-20 1586.51 4507049.0 Before Summing Close Volume Date 2018-03-01 1495 6835230.0 2018-03-02 1500 6587564.0 2018-03-05 1525 5233934.0 2018-03-06 1540 4561718.0 2018-03-07 1545 4174123.0 2018-03-08 1550 3512528.0 2018-03-09 1580 4417059.0 2018-03-12 1600 5115886.0 2018-03-13 1590 6427066.0 2018-03-14 1590 4164395.0 2018-03-15 1580 4026744.0 2018-03-16 1570 5145054.0 2018-03-19 1545 6376619.0 2018-03-20 1585 4507049.0 After Summing Volume Close 1495 6835230.0 1500 6587564.0 1525 5233934.0 1540 4561718.0 1545 10550742.0 1550 3512528.0 1570 5145054.0 1580 8443803.0 1585 4507049.0 1590 10591461.0 1600 5115886.0 |
The key level to watch for here is 1545
it has been summed from the 07th March and the 19th of March into a single row. 1590
has also been summed from the 13th and 14th of March. As such, when we create the chart we can see that these two levels saw the most volume over the test period. Here is the final chart:
Hi and sorry for the necropost. First of all I wanted to say thanks for all the examples you’ve given, this is all invaluable information for someone starting out in this business. I have a question regarding this exercise in particular, especially when it comes to using data not from Quandl. Quandl’s limits have become apparent, so using Alpha Vantage seems to be the logical step forward. However, in attempting to jerry-rig this code to make it work using that base data, I’ve been getting a strange error involving unsupported operand types for a specific operation.
First, the modified code (after having already imported compact AMZN data though Alpha Vantage):
import quandl
import pandas as pd
from datetime import datetime
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter
import numpy as np
import argparse
import backtrader as bt
def parse_args():
parser = argparse.ArgumentParser(description=’Volume at Price Chart’)
parser.add_argument(’round’, type=int, help=’Round to the Nearest’)
return parser.parse_args()
def custom_round(x, base=5):
return int(base * round(float(x)/base))
def round_and_group(df, base=5):
# https://stackoverflow.com/questions/40372030/pandas-round-to-the-nearest-n
# Extract the data we want
df = data[[‘Close’, ‘Volume’]].copy()
print(‘Before Rounding’)
print(df)
# Round to nearest X
df[‘Close’] = df[‘Close’].apply(lambda x: custom_round(x, base=base))
print(‘Before Summing’)
print(df)
# Remove the date index
df = df.set_index(‘Close’)
df = df.groupby([‘Close’]).sum()
print(‘After Summing’)
print(df)
return df
def thousands(x, pos):
‘The two args are the value and tick position’
return ‘%1.0fK’ % (x*1e-3)
def create_plot(x_series, x_label, y_pos, y_tick_labels, colour, title):
# Excample horizontal bar chart code taken from:
# https://matplotlib.org/gallery/lines_bars_and_markers/barh.html
plt.rcdefaults()
fig, ax = plt.subplots()
ax.barh(y_pos, x_series, align=’center’,color=colour)
ax.set_yticks(y_pos)
ax.set_yticklabels(y_tick_labels)
formatter = FuncFormatter(thousands)
ax.xaxis.set_major_formatter(formatter)
ax.set_xlabel(x_label)
ax.set_title(title)
plt.xticks(rotation=325)
plt.show()
#Get Args
args = parse_args()
print(“VOLUME AT PRICE”)
# Get data
datapath = ‘/home/goop/python-apps/trading/AMZN-alpha-vantage-compact.csv’
data = bt.feeds.BacktraderCSVData(dataname=datapath)
# Massage the data
data = round_and_group(data, base=args.round)
# Prepare data for the chart
y_pos = np.arange(len(data.index.values))
# Get the chart
plt = create_plot(
data[‘Volume’], # x_series
‘Volume’, # x_label
y_pos, # Y positioning
data.index.values, # y_tick_labels
‘Green’ # Bar color
)
The error running from Terminal (using Ubuntu):
VOLUME AT PRICE
Traceback (most recent call last):
File “backtrader_rookie_volume_at_price.py”, line 72, in
data = round_and_group(data, base=args.round)
File “backtrader_rookie_volume_at_price.py”, line 24, in round_and_group
df = data[[‘Close’, ‘Volume’]].copy()
File “/home/adrien/.local/lib/python3.6/site-packages/backtrader/lineseries.py”, line 467, in __getitem__
return self.lines[0][key]
File “/home/adrien/.local/lib/python3.6/site-packages/backtrader/linebuffer.py”, line 163, in __getitem__
return self.array[self.idx + ago]
TypeError: unsupported operand type(s) for +: ‘int’ and ‘list’
Thanks for any help, as I’m stumped!
Hi!
I guess you are not just trying to change the data source to alpha vantage. Are you trying to also do something in Backtrader with it?
The error seems to be from backtrader and I see you are passing the file from alpha vantage to it. The following line to be precise
‘data = bt.feeds.BacktraderCSVData(dataname=datapath)’
Then you are trying to perform numpy operations on it.
Perhaps remove all backtrader logic and try to replace the data source first?
Good luck!
Thanks for your prompt response. I found that everything worked better once I gave in and used Quandl, and to simplify the workload I just went with it. Looking forward to more of your content, and thank you for taking the time to try and help. 🙂
Many thanks for this ! Is there a way to turn this code into an indicator for backtrader ?
Hi ! I understand that this code is not for backtrader, but would it be possible to turn this into a backtrader indicator to backtest a support /resistance strategy around it ? Thanks