Creating Advanced Custom Indicators

Making a Price-Axis Volume Indicator for Tradovate Trader

This article assumes that you have working knowledge of JavaScript as well as a basic understanding of the Custom Indicators module. For more information on Custom Indicators see the official documentation for Custom Indicators.

Creating Custom Indicators is a hot topic in the Tradovate community — we receive many requests for indicators, and the development community is currently thriving. That’s probably because using JavaScript to create your own trading indicators is quite an alluring feature. Today I’ll be discussing some of the more advanced and ‘hidden’ features of the Custom Indicators module. By the end of this article, you’ll understand how to create a price-axis-oriented volume indicator. Boot up your Trader application and follow along!

Setting Up Our Indicator

The first thing we will do is create a new Custom Indicator. Drag the Code Explorer module into your workspace. Then select ‘File → New’ from the top bar. We’ll add some requirements and remove the unnecessary starting code:

We now have a basic starting point to work from. We will need the predef tools for defining our user inputs, and the graphics op , px , and du functions to work with Scale Bound values. Notice our map currently returns an empty object — you can return an empty object in the case that you want your indicator to do nothing, gracefully. We will change this, but it’s good to know.

Using a Non-Standard Grid

The next thing we need to think about is how to align our drawings on the price axis. Conveniently, there’s a good way of doing this. Let’s create some container objects. We’ll make one container for the bars, and one container for the text values.

We’re using Container type Display Objects. We’re also taking advantage of some of the advanced options for Display Objects.

  • origin — this field allows us to specify where we want the (0, 0) point to be located in chart space. Here, we’ve set our (0, 0) point to the top right hand corner of our chart.
  • global — this tells the object to render only one single object, and not to render on a bar-by-bar basis. This is good for performance in cases where we need to gather data before rendering it.

With these containers, we’re prepared to create a price-axis aligned indicator. Let’s move on to gathering and storing our data points.

Handling Volume Profile Data

To track the exact volumes at each price point, we’ll need to combine a few strategies. The first thing we need to do is make the profile method available within our map function. Add the requirements field to your indicator’s module.exports object:

This bit of code will tell the indicator that the profile method should exist on map ’s d parameter. This is a lesser known feature of the Custom Indicator module, but a powerful one. Now, when we write d.profile() in our indicator code, it will return an array of objects with this shape:

{ price: number, bidVol: number, askVol: number, vol: number }

With this data, we can create a map of prices to delta-volumes quite easily. In the init method of your indicator class, assign an empty object to this class instance:

Now let’s use our d.profile() method to populate this object.

This allows us to store the profile information for each individual price, updated on each tick, and accumulated per bar. Now we have a locally stored data source that we can look to when we draw the bars.

We have just one issue — If we were to log this object, we’d realize that it contains every data point recorded for this contract. It’s much more likely that users will want to know the deltas over a session. Before we start drawing bars, let’s ensure that our trading session is adjustable.

Setting Trading Session Dates

We will need to setup an open and close time for our indicator. We can add these as user defined properties (so that our users can determine the trading session times that they care about).

Add this to your module.exports object:

Now there are user defined parameters for the market open and close times. Because we need to consider that the trading session our users may care about could start yesterday or end tomorrow, we have declared boolean values to describe these cases. A user would define the hours in 24-hour format. Our defaults are set to open at 5PM yesterday and close at 6PM today. These parameters allow us to be very specific about the timeframe that we care about.

We still need to use these values in our indicator:

We can use init to setup the initial values. Then we limit our indicator to render nothing during off hours at the top of our map function. With these values set up, we will only be using data from the timeframe during which our user is concerned.

Drawing the Delta Volumes

Now we can begin to populate the container objects we created earlier. Because we’re using global Display Objects, we only want to run our calculations and populate the containers if map is being called on the last bar. If we do our calculations and container population on interstitial bars, we will be calculating values based on an incomplete set, and we will push data that will ultimately get thrown away, thus wasting resources. Instead, we will use the d.isLast() method to determine whether or not we are looking at the latest bar.

Before we populate the container objects, let’s discuss what we have so far. We store our byPrice keys for easy access. With these keys, we can use array methods to manipulate our data. We find the highest and lowest price in our set using map and reduce . We use those values to determine the price range that our byPrice object covers. We also need the greatestDelta to help us scale our drawing — using the longest bar should suffice. Finally, we use the range and a special indicator property called this.contractInfo to determine the number of bars to render. Let’s discuss this ‘hidden’ indicator property.

this.contractInfo has a few fields that we can use. In this case, we’re using tickSize . tickSize gives us a number that represents the size of each tick for this contract. This is necessary when developing indicators that rely on the tick size of a contract to render. There are also two other fields on the contractInfo object — contract which is the string contract name (like ESZ1), and product which is the unqualified product portion of the contract name (like ES).

Moving forward, let’s finally begin rendering our indicator’s bars.

We have to do a bit of calculating per bar. We need to determine the bar position and dimensions, as well as the text position. Luckily, we already have all the data we need to make it happen. All we have to do now is create the actual Display Objects and push them into the containers. We will start with the bars.

Note in the snippet above that all of our pixel values for widths are negative — that’s because of our grid orientation. Because we have the X-axis’ zero locked to the right of the chart, we need to use negative numbers to move into the visible quadrant of our graph.

This is a Shapes type Display Object. Each bar needs a unique key , so we use the price (which is unique per data set) to append a value to our key. We can populate this object with primitives — in our case Rectangle s. We want our bars to be locked to the price axis, so we use px(0) for it’s position.x value. The Y axis value is determined by the price we are rendering at. We use the contractInfo.tickSize to center our bars on the price in question. Finally, we use the state of the delta value — positive or negative — to determine the bar color.

Next up is the Text object:

This object is nice and simple. It displays the delta value at the textPt which we already calculated. We use the same technique as with the bars to give each text object a unique key. I use the color '#999' (a mid-tone grey) so that our text can be seen on both of the Trader app’s dark or light color themes.

Cleaning Up

This indicator works fairly well at this point. However, we could make some improvements.

  • It would be nice if we could let the user select their own colors.
  • If we look at this indicator on a variety of contracts, it becomes clear that the scaling could also be better.
  • Furthermore, when we zoom out the whole thing turns into a jumbled mess .

But fear not! We can solve all of these problems gracefully.

Let’s first tackle the jumbled-mess factor. We can use render conditions to select the scale ranges that are appropriate for certain elements. Add this code to your containers:

To solve our other two issues, we’ll add some more user defined parameters. In your module.exports object’s params field, add three more values:

positiveColor and negativeColor will be for the delta bars’ coloring. scaleFactorPx will determine the coefficient used for barWidth . This will let users increase the value for contracts with low traded volume (so that the bars aren’t tiny). We can modify our code to use these values instead of the constant values that we had been using.

Here’s a gist with the whole finished product — there are major additions to this in its current form since I released this article quite some time after I ran this indicator through community testing. There is a section of code that combines bars together so that when you zoom out you can see the combined deltas for a set of prices. This is also all configurable of course!

Disclaimer: Futures trading and algorithmic trading involve a substantial risk of loss which should be understood prior to trading and may not be suitable for all investors. Therefore, carefully consider whether trading is suitable for you. The information provided in this article is for the sole purpose of education and assistance in making independent investment decisions. Tradovate, LLC has taken reasonable measures to ensure the accuracy of the information contained herein; however, Tradovate, LLC does not guarantee its accuracy, and is not liable for any loss or damage which may arise directly or indirectly from such content or from an inability to access such information, for any delay in or failure of the transmission or the receipt of any instruction or notification in connection therewith. Any recommendations or trading analysis found herein are provided for educational and illustrative purposes only and should not be used in connection with the formation or execution of any trading decisions.