The Making of the Volume Delta Grid

Fully Utilizing the Tradovate Custom Indicators Graphics Module

Tradovate
Tradovate
14 min readMay 26, 2021

--

The Volume Delta Grid in action.

For the last few months I’ve been hard at work mastering all of the ins-and-outs of our newest custom indicators feature, the Graphics Module. This module opens up a whole new world of possibilities when it comes to developing your own indicators — the sky is the limit! We now offer the power to draw arbitrary shapes, lines, and text to a chart using a simple but flexible coordinate system.

Today I’d like to offer an exclusive behind-the-scenes look at how I built Tradovate’s Volume Delta Grid indicator. This indicator utilizes many of the new Graphics module features, so if you’re interested in learning about them (or about creating custom indicators in general) this is a great place to start.

Structure Of An Indicator

Although this is a rather advanced indicator, we will start with the basics. An indicator is not just the calculator code you write, but a definition for how the Trader application should treat the entire indicator module you create. The structure of custom indicator code is designed so that if you know JavaScript, you’ll be right at home. This is evident in the module.exports portion of all indicator code.

I like to think of an indicator as the composition of three domains — the Calculator class, the helper functions I refer to as tools, and the export object. Each domain comes together as a part of the whole indicator.

…an indicator [is] the composition of three domains — the Calculator class, the helper functions I refer to as tools, and the export object.

The Calculator

The Calculator is the main event. It is what we typically think of as the indicator itself. However, the Calculator class is actually only one part of the indicator — the part that contains all of our drawing logic. For the Volume Delta Grid, this is how we can define our calculator:

Basis of the Calculator class.

The Calculator class has two primary functions.

  1. init — this is called when the indicator is first instantiated. This is where we should initialize tools we’re composing to make this indicator.
  2. map — the calculation logic. This is called for every bar rendered, with a few exceptions. We’ll discuss those later.

The most operative part is the map function. It takes one parameter, which we usually call d for data. d is an InputEntity object and obeys the interfaces on this page. A standard indicator uses d as the type InputEntity, but if you change the inputType parameter on the export object to "bar" then d will be type BarInputEntity.

The calculator class has another function called filter, but that is beyond the scope of this article. The Calculator class is documented here.

Tools

I refer to the helper functions that we use to calculate an indicator’s return value as tools. A tool is just a reusable piece of code that is not necessarily specific to the indicator you are building, but merely encapsulates a portion of the calculation. Think of each tool as a specialist, it should be built to do one task and do it well. I typically try to follow an interface for tools as well. The tools we use at Tradovate generally look like this:

The basic structure of our Bid-Ask tool.

This is the basis of the BidAskTool which we will use to perform a large portion of the calculation for the Volume Delta Grid. I always try to make my tools follow this pattern. The BidAskTool function returns a function which we can use to do the actual work. We call the main function as a means of initializing the internal function, which is the active part of the tool. Because of its function wrapper, we can add parts to the internal tool while encapsulating parameters (like the marketOpenTime parameter we pass to the BidAskTool outer function). If we need to hold onto some state, we can add it into the reset function, which gets called before we return bat — in other words on initialization. Here’s how we can use tools:

Instantiating our BidAskTool from the calculator’s init function.

We simply call the initializer function (the outer function) and we get the active tool in return. We set it as a property on this — currently this refers to our Volume Delta Grid class instance. Our BidAskTool requires one parameter, marketOpenTime which we pull from the user provided parameters. We can access the user provided parameters by using the this.props field present on the calculator class.

The Export Object

The export object is a definition for how we want the Trader app to treat our indicator. It is from this export object that we can tell Trader how we want data to flow in and out of the indicator, what shape we expect that data to be in, the display name for the indicator, the input parameters, and more. It is important to understand the export object’s interface. This page will tell you a bit more about all of the possible fields that the export object may contain.

Here’s what our Volume Delta Grid indicator’s module.exports looks like:

The export object for the Volume Delta Grid indicator.

Each of these properties controls some way in which this indicator is processed.

  • areaChoice — when you create any indicator and add it to a chart, a modal appears and lets you choose your parameters. One parameter is whether you want an overlay or a new area. An overlay is rendered on top of bars, where a new area has its own dedicated chart space. For the Volume Delta Grid, we want a new area, so we use "new".
  • name — the name of the indicator. This is a unique identifier, and if you have two with the same name things get weird. To prevent unusual errors, and indicators not updating correctly, try to give the name parameter something unique.
  • description — the displayed name of the indicator in the Trader UI. This is how your indicator appears when choosing it from the indicator drop down menu. Two indicators could have the same description without error, but there will be an ambiguity when your users attempt to add your indicator to the chart. Again, try to use a unique and descriptive name.
  • calculator — the actual calculator class to use, in our case VolumeDeltaGrid.
  • inputType — this is important if you need volume information, which we do. If you need more than Open-High-Low-Close values of a bar, you’ll need to set the inputType to "bars".
  • tags — this is how we group indicators in the drop down menu. You can use the predef module to get predefined values for tags.
  • params — these are custom parameters you may use for your indicator. Here we use one parameter, marketOpenTime . This is so that users can determine the time over which they want to measure cumulative volume. The value itself refers to the hour that the cumulative values reset. When creating an instance of your indicator to add to a chart, the parameters you define here will appear in the modal for users to set. There are good options for predefined values in the predef.paramSpecs namespace object.
  • schemeStyles — this is how to define the line and plot colors for your indicator. This property is not terribly important to the Volume Delta Grid indicator since it is primarily text generated via code, and not lines. However you can use this field to great extent in an indicator that plots multiple lines or more than one value.

There are even more properties available, but these are the ones that we need to make the Volume Delta Grid work.

Writing the Bid Ask Tool

The first thing that we will do is write our BidAskTool. What we want to do is accumulate day volumes, as well as display per-bar values. We will start by adding some state to our tool:

Using state variables in our Bid-Ask tool.

We need to track the current date, today. We need a way to collect bid, ask and total volumes for the day in question — the accum array. And we also need to know what the last bar’s timestamp was, lastTs.

Now we will populate the accum array and to perform our calculations. We will be doing this all from the push function. First let’s determine how to gather our data.

Dealing With Time

One of the hurdles of making the Volume Delta Grid accurate was dealing with dates. All bars have a timestamp property which we can invoke to get a JSON encoded time string. To draw the grid accurately, we need to populate the array based on the current trading day. We want to store the day cumulative bids, asks and total day volume. This is what we need the marketOpenTime parameter for. We also want to accommodate all sorts of view ranges, from volumetric bars, to tick-based bars; minute bars to day bars. Since all timestamps are JSON encoded time strings, we can conveniently wrap them in Date objects to be easily manipulated.

Dealing with time is easy, actually.

I chose to default to 30 days ago in milliseconds in the case that lastTs is null. This value isn’t of great importance as long as it’s old enough that the bat.state.accum array will get cleared on the next bar, which we determine in the following if statement. There are a lot of conditions so I’ve spread them out and added comments to be more readable. The gist is that if the last bar wasn’t within the marketOpenTime hour, but the current bar is within that hour, then we have changed trading days and we want to reset the cumulative values. Using a parameter leaves it open for a user to set any hour they’d like, so someone who doesn’t start their trading day until 6 PM could set the marketOpenTime parameter to 18 and the day accumulated values will reset at 6 PM. Now that we know we’re handling time correctly, we need to make our calculations.

Operating on Bar Data

Now we have a solid understanding of the when of our bars, but we still need to construct some data to send back to our calculator class. We also need to actually find the accumulated values for our time period as well. Enter reduce. reduce is a native function of JavaScript arrays that allows you to perform a function on every element of an array, ultimately reducing the set of elements to a single element. As parameters, it takes both a function to perform on the array values and a starting value. The function we pass takes the form (a, b) => value where a is the accumulated value so far, or the start value, b is the next from the array, and value is what we return as the new accumulated value. Here’s an important reduce function for summing the values in an array:

A simple sum for arrays of numbers.

We can use the sum function on any array of numbers. It does just what its name describes — sums an array of values. We provide the function (a, b) => a + b, and the seed value 0. reduce takes the accumulated value so far, a, and adds it to the next value b. This is about the simplest reduce operation that you could perform. We can take this a step further as well — one thing that you’ll notice about our d parameter in the map function and bat tool is that each value on d is a function. We can write a helper function to take a field that we want to call from as well as the array we want to reduce. We’ll call this function sumBy:

Specialized sum for reusability.

Now when we pass our arrays full of d objects, we can choose the function we want to sum by:

An example of our new helper function.

This will be nicely reusable for any object that could get a value from a function. Let’s try it to construct our return value for the push function we’ve been building:

Our new helper function in action.

We create a data object with all the values we want to render. There are quite a few, but our sumBy helper function makes it all surprisingly readable. To accommodate any number of bars, I’ve chosen to create the average accumulated volume by considering all bars from the current day. Now that we have all of our data prepared nicely, it’s time to start rendering things.

Using the Graphics Module

The graphics module allows us to use a declarative, data-driven style to render our custom indicators. Recall that a Calculator class has two primary functions, init and map. Let’s discuss map in a bit more detail now. map takes bar data (in real-time) and transforms that data into a rendering scheme that can be interpreted by the Trader application. The return of map can be a simple value (in which case a line is rendered), or it can be an object with custom value names and custom rendering schemes. Since the release of the Graphics Module, there is a new optional field that can be included in the return value of map — the graphics field. Here’s how we can structure that return:

Structure of the graphics return object.

Our graphics field must be an object with the items field — an array of GraphicsObject objects. The format of a GraphicsObject varies from type to type, and invalid combinations will prevent your graphics from rendering. So before you think it’s a bug, make sure you look at your graphics items for format errors. This so far has been the most common mistake I’ve been asked about regarding the graphics module. All of the details of available GraphicsObjects and their expected formats are well documented here.

Populating the Graphics Items Array

There is a lot of information for us to render here. But we can break it down. The first thing I’d like to do is introduce a helper function that will let us draw our horizontal lines a bit more easily.

A helper for creating our horizontal lines.

This function takes a cellHt value, expected in pixels, and an n number, the number of lines to draw. Pay close attention to these objects — they’re graphics objects! The primary object is a LineSegments type object, documented here. It keeps an array of Line objects (documented here) to render. We use the optional infiniteStart and infiniteEnd fields on our Line objects to designate that we want these lines to go on forever in both directions. We ultimately return the LineSegments object we created, so we can call this function in the items array of our graphics return object:

Using our helper to draw horizontal lines.

Coordinates

Before we proceed with more graphics, let’s discuss how we can measure graphics’ coordinate space using the graphics helpers. You’ll notice we use the px helper function a lot in the createGrid helper. These are special functions for graphics coordinates and we can require these helper functions from './tools/graphics' in the code explorer. It has two sister methods — du and op.

  • px allows us to measure units in pixels. This is pretty straightforward but won’t scale with your chart.
  • du stands for domain units and allows us to describe graphics objects coordinates that will scale with the chart itself. In the X axis, du is the bar index. In the Y axis du is price in points.
  • op allows us to combine px and du values arbitrarily. This is a powerful feature. Imagine that you want to render at 30 pixels above the 3800 price line for whatever reason. With op you can write op(px(30), '+', du(3800)) for your y value in a coordinate, and whatever you rendered would appear 30 pixels above the 3800 price.

Now that we know a bit more about coordinates, let’s tackle the verticals. This is simpler since we can work on a per-bar basis, only drawing one line per bar.

Adding the verticals.

This line will be drawn for every bar, halfway between this and the next in du. This is because du measures bar index in the X axis, each value is an integer one greater than the last. Adding .5 to the index will place the line half a bars width beyond the current index.

Drawing Data as Text

Before we can render any of our data into these cells, we need to actually derive that data from the d parameter and our tools. Let’s finally use our BidAskTool.

Pulling our values out of the Bid-Ask tool and making them suitable for rendering.

For the most part, we can simply use the values returned by our Bid-Ask tool. The one exception is the time string. For the most recent bar, we want to track the elapsed time so far, live. So we need to treat that bar differently. Instead of taking the open-timestamp from the last bar and subtracting it from the next bar’s timestamp, we take the current time (right this second) and subtract the bar’s open time. We can use the d.isLast() property to tell us if this bar is the most recent. Since we already made our timeStr variable, it’s trivial to add this to a text graphics object:

Adding a text object.

Our time string becomes a Text type graphics object, documented here. We place this string six cells from the top (our row height times five, plus half the row height so it is centered). The 'centerMiddle' alignment keeps things nice and even. We can repeat this for every one of our values. Let’s look at a text with conditional color — red when it shows a negative value, green when it’s positive. I do this for the day cumulative bid - ask volume.

A text object with conditional colors.

You can view all of the graphics objects in full detail by looking at the open source code of the Volume Delta Grid. Search for it on the Trader Community, install it, and then inspect it via the Code Explorer.

Need help with any of that? Click the tab that says Tradovate Help on the right hand lower-center of the app. Each of those FAQ links start up a WalkMe session — it’s basically a guided tour of how to accomplish a task in the Trader app. Look in the How to’s section at the bottom to get a WalkMe tour for Community Indicators.

Conditional Rendering and Containers

If you are following along and playing with what we’ve built so far, there are definite problems. If you zoom out, things start looking pretty bad. And they don’t just look bad, they’re causing some performance issues. Luckily, we can tackle this issue with conditional rendering, and containers. First we wrap the portion of our indicator that we want to conditionally render in a container, then we define a conditions field — this is an optional field on all graphics objects.

A container with a render condition.

There are two available fields for visibility conditions — scaleRangeX and scaleRangeY. Each field pertains to its respective scale in the viewport. In this example, the bar size in the X axis must be at least 64 pixels wide for this container to render its children (we have to be really zoomed in to use the Volume Delta Grid). If we wrap our current graphics objects in this container, we will solve our performance issue, and remove the ugly overlapping values.

We can use this approach for the global labels as well — you may be wondering how you can get those one-time rendered global texts which I use for labels in the grid. Here’s the solution I used:

A container that only renders its children for the most recent bar.

You should also add the global: true field to the text objects you wish to have as one-time rendered objects.

The VisibilityConditions object is documented here.

Fallback Container

Now we have a new problem — when we’re zoomed out, our grid doesn’t render anything at all, leading users to think it doesn’t work. Our previous container’s conditional rendering just begs for a fallback value for this exact scenario. We can just add another conditional element that will only render under 64 pixel-width bars.

Yet another conditional container, this time to show a Zoom In hint.

This is a global text object, so we want to apply the same tactic as we discussed for the global labels, only rendering for the most recent bar. You may be asking, ‘Why the array?’. It’s for conditionally adding this element to the graphics items list.

Using spread syntax will add items only if the array you spread is non-empty. Otherwise, it just does nothing.

Now when we are zoomed out of our chart, the Volume grid will display a Zoom to View Grid text hint. Now users won’t think it’s broken.

Hack This Code

I’ve seen some suggestions already on what more could be done with this indicator. And I agree, a lot more could be done with it! That’s why we love and encourage an open source approach to the custom indicator community. As I mentioned before, any indicator that is shared on the Trader community can be viewed and even changed to meet your own specifications. All you need to do is install an indicator, open it in the Code Explorer, and hack away!

--

--