Tradovate Developer Documentation
Tradovate Developer Documentation
Introduction
Please check the user guide on how to access custom indicators in Tradovate Trader here
Javascript
When the app is based on Javascript, charting is supposed to be Javascript-based too. As well as
built-in technical indicators that a must-have feature of trading application. The next step is just
to expose the internal API to the public to make custom indicators possible.
In spite of old popular beliefs, modern Javascript is not scary unreliable some-glue-for-html slow
stuff anymore like it was just several years ago. New standards like ES6 made Javascript
friendlier for developers with an object-oriented background. If you are familiar with C#, C++,
Java, or Scala, you can find out a lot of surprising similarities.
The range of open-source packages can satisfy (up to some degree, of course) any data science
needs: you can find out as digital signal processing libraries as well as machine learning ones.
The most advanced of them use your GPU even on a mobile phone behind the scene to speed
up calculations.
Because there are so many high-quality materials about the language, we will not try to teach
you how to program. Instead, we will focus on how to plug your algorithms to Tradovate Trader.
Simple Price Offset
Imagine that we need an indicator that will show a line with some offset from input series data.
Something like Offset Indicator = Input Value - 2.0. It could be used as a stop loss line with our
magic number.
Our indicator's file is a Javascript's CommonJS module. The module exports a definition of the
indicator for Tradovate Trader: how to uniquely identify our indicator in the library of indicators,
how to calculate values, how to plot them, default colors, and styles. The application expects
that the definition implements Indicator interface.
class offset {
map(d) {
module.exports = {
name: "exampleOffset",
calculator: offset
};
The export here will tell the app to add an indicator with the unique name exampleOffset and
calculations for this indicator are coded in class offset. The application expects the class
implements Calculator interface.
The name field plays the role of a machine-readable identifier. We don't expect it to be some
nice looking text.
To be a calculator, the class should implement at least one function: Calculator.map. The
function maps or translates an input value to output one, that's it. The input value is an object
that points to just one item in input series and the app iterates through the whole series one by
one in some sort of a loop. Here is a pseudo-code that can give some clues what is going on
under the hood:
const inputSeries = [
...];
const outputSeries = [];
The application calls the map function with four arguments: the current item, index of the item
in the input series, input series, and a series with previously calculated values. As for our Price
Offset Indicator, it is enough just to use the input item.
As soon as we put our indicator to the app via Code Explorer, the Charts module will show it in
the list of indicators with the name 'EXAMPLEOFFSET'. It will just plot a grey line by default with
some offset below the input. Later, we will learn how to customize our indicators with a human-
friendlier name.
Parameterized Price Offset
In the previous example, we introduced some magic numbers to specify the offset. But it would
be more convenient to introduce some flexibility and let us choose the number at the time
when we place the indicator on the chart. To do it, we need to extend our module to get the
next content:
class offset {
map(d) {
module.exports = {
name: "exampleOffset",
calculator: offset,
params: {
offset: {
type: "number",
def: 2.0,
restrictions: {
step: 0.25,
min: 0.0
};
Now our calculator has this.props object that includes all parameters specified by a user when
the indicator was placed on a chart. To help the app and the user to figure out what parameters
are expected, we added the params section to the module export. It tells the app that we
expect one parameter named offset and it should be edited as number with default
value 2.0 and some restrictions on the value.
Exponential Moving Average is a little bit more complex indicator: it has to keep in mind its
previous calculations.
The app doesn't restrict the calculator class how it can use its fields or functions. As result, the
most simple way to keep the state of the previous calculation is by saving it in the object's fields.
Like, this.initialSum = this.initialSum + d.value(). Here is our EMA that has a lot in common with
our previous indicator:
class ema {
init() {
this.previousMA = undefined;
this.initialSum = 0;
map(d, index) {
let result;
this.initialSum += d.value();
else {
this.previousMA = result;
return result;
module.exports = {
name: "exampleEma",
calculator: ema,
params: {
period: {
type: "number",
def: 10,
restrictions: {
step: 1,
min: 3
};
We added one more function to the calculator class - an implementation of Calculator.init. The
application calls this optional function before the calculation loop. Our EMA uses it to initialize
the state's fields.
Human-friendlier EMA
First, let's add a recognizable name in the indicator menu and default line style by extending the
module's export with the indicator's definition.
class ema {
init() {
this.previousMA = undefined;
this.initialSum = 0;
map(d, index) {
let result;
this.initialSum += d.value();
else {
this.previousMA = result;
return result;
}
module.exports = {
name: "exampleEma",
calculator: ema,
params: {
period: {
type: "number",
def: 10,
restrictions: {
step: 1,
min: 3
},
schemeStyles: {
dark: {
_: {
color: "red",
};
Now the indicator can be found in two sub-menus: My Indicators and Moving Averages under
the name My EMA. By default, it will have a red color. The app uses web colors. Other line
properties that can be set in default style are lineWidth (in pixels), opacity (in
percents), lineStyle (an index of style in the list of line styles in Indicator
Editor). schemeStyles can be used to set default styles for dark and light mode of the
app. _ field is a placeholder for the plot name: an indicator can have multiple plots and each of
them can have default styles. But our current indicator has just one plot without any particular
name. As so, we use just _.
Built-in tools
A major part of the indicator can be reused by other indicators. The application
includes tools folder with a set of reusable classes and functions. Indicators can import them
via require construction of Javascript.
class ema {
init() {
this.emaAlgo = EMA(this.props.period);
map(d) {
return this.emaAlgo(d.value());
}
module.exports = {
name: "exampleEma",
calculator: ema,
params: {
period: predef.paramSpecs.period(10)
},
schemeStyles: predef.styles.solidLine("red")
};
Source codes of the tools folder are available via the Code Explorer module of the application.
Double EMA
There are several indicators that employ two moving averages with different periods. We'll build
some variation of it and will show two EMA side-by-side. Let's call one of them "fast EMA" and
another "slow EMA".
As in the previous indicator, we will apply the ready-to-use EMA algorithm from tools. Twice.
And each EMA will have its own parameter and own output value.
To return two values from our Calculator.map function implementation, we will return an object
with two fields instead of just a number as previously. Then, we will define how to plot these
values and will refer to their names.
class doubleEma {
init() {
this.slowEma = EMA(this.props.slowPeriod);
this.fastEma = EMA(this.props.fastPeriod);
map(d) {
return {
slow: this.slowEma(value),
fast: this.fastEma(value)
};
module.exports = {
name: "doubleEma",
calculator: doubleEma,
params: {
slowPeriod: predef.paramSpecs.period(21),
fastPeriod: predef.paramSpecs.period(10)
},
plots: {
},
schemeStyles: {
dark: {
};
Now module's export includes a new field Indicator.plots: it tells the app which fields from the
output object should be plotted and shown in the Data Box inside Charts.
This version of Indicator.schemeStyles includes default line properties for both these plots.
Plotters
So far our indicators plotted only lines. But there is a variety of other plotters in Tradovate
Trader: dots, columns, specialized plotters that you can find out in some complex built-in
indicators.
As an example, we are going to replace the 'slow EMA' line with dots. The plotter will place one
dot per each bar.
To achieve it, we need to implement the Indicator.plotter field in the module's exports:
...
module.exports = {
name: "doubleEma",
calculator: doubleEma,
params: {
slowPeriod: predef.paramSpecs.period(21),
fastPeriod: predef.paramSpecs.period(10)
},
plots: {
},
plotter: [
predef.plotters.dots("slow"),
predef.plotters.singleline("fast"),
],
schemeStyles: {
dark: {
};
Moreover, we can implement even our own plotter. Since we have two closely related plots, it
would be nice to connect them at each bar. It will look like DNA.
Below we've implemented the dnaLikePlotter function and mentioned it in our list of plotters.
We didn't touch old plotters, just added one more.
All that the plotter function does is drawing basic lines from one point to another. The
application calls this function with three arguments: canvas, calculatorInstance,
and history (see Custom.function).
canvas represents a chart area and has several methods to place drawing to it: to draw a line
from one point to another, to draw a complex path with multiple points. The canvas is going to
be a rich structure with more functionality to come.
calculatorInstance refers to the instance of our calculator class. The plotter can access its fields
if needed. For example, calculatorInstance.props.slowPeriod is available there.
const p = require("./tools/plotting");
class doubleEma {
init() {
this.slowEma = EMA(this.props.slowPeriod);
this.fastEma = EMA(this.props.fastPeriod);
map(d) {
return {
slow: this.slowEma(value),
fast: this.fastEma(value)
};
canvas.drawLine(
p.offset(x, item.fast),
p.offset(x, item.slow),
relativeWidth: 0.5,
opacity: 0.5
});
module.exports = {
name: "doubleEma",
calculator: doubleEma,
params: {
slowPeriod: predef.paramSpecs.period(21),
fastPeriod: predef.paramSpecs.period(10)
},
plots: {
},
plotter: [
predef.plotters.dots("slow"),
predef.plotters.singleline("fast"),
predef.plotters.custom(dnaLikePlotter)
],
schemeStyles: {
dark: {
};
The plotter function above involves the plotting built-in module. The module contains a bunch
of helper functions to simplify plotting. In our case, we use plotting.x.get to retrieve the X
coordinate of the item.
The function plots each line with red or green color and tells the app to draw them with half
opacity and width equals to half-space between bars ({@linkcode plotting.x.relativeWidth}.
Fancy-looking EMA is not enough for successful trading. We need a fancy-looking Average True
Range indicator too.
First, we just copy the built-in ATR indicator with some renaming:
class averageTrueRange {
init() {
this.movingAverage = MMA(this.props.period);
map(d, i, history) {
}
module.exports = {
name: "exampleATR",
calculator: averageTrueRange,
params: {
period: predef.paramSpecs.period(14)
},
inputType: meta.InputType.BARS,
areaChoice: meta.AreaChoice.NEW,
schemeStyles: predef.styles.solidLine("#ffe270")
};
The source code is similar to our previous indicators with some additions in the module's
export: Indicator.inputType restricts user's choice with OHLC bars here,
and Indicator.areaChoice will highlight 'New Area' choice by default when you will place an
indicator to the chart. Bars as an input are required to calculate True Range that needs High,
Low and Close: all of them will be available in map function via d.high(), d.low() and d.close()
Our goal is to improve the indicator and highlight places where it is larger than some
parameterized threshold. Moreover, the threshold parameter will be in tick sizes to make the
indicator a product-neutral.
We will highlight the ATR line as well as corresponding candlesticks. For simplicity, highlighting
will be with eye-catching tones of red/green colors.
If we need just change the style of dots and columns, we don't need to implement a custom
plotter: all we need to do is to compose the style field in the returned object from
the map function. Candlestick style is done a similar way.
class averageTrueRange {
init() {
this.movingAverage = MMA(this.props.period);
map(d, i, history) {
let overrideStyle;
overrideStyle = {
};
return {
value: atr,
candlestick: overrideStyle,
style: {
value: overrideStyle
};
}
module.exports = {
name: "exampleATR",
calculator: averageTrueRange,
params: {
period: predef.paramSpecs.period(14),
threshold: predef.paramSpecs.number(10, 1, 0)
},
inputType: meta.InputType.BARS,
areaChoice: meta.AreaChoice.NEW,
plotter: predef.plotters.columns("value"),
schemeStyles: predef.styles.solidLine("#ffe270")
};
this.contractInfo above is an object with details about the contract of the chart. The app assigns
it to the indicator during construction.
Tradovate Trader includes some third-party libraries that can be helpful for indicators.
Lodash is a very popular and high-performance library to work with arrays and objects. All you
need to do is to include const lodash = require("lodash") in your module.
Another is a library for Fast Fourier Transform. Here is an example of how to apply this library to
build a moving average that calculated as FFT of input data, a filter of high frequencies, and
inverse FFT.
const predef = require("./tools/predef");
class fourierMA {
init() {
this.fft = FFT(period);
this.zero[i] = 0.0;
this.lastIndex = -1;
map(d, index) {
else {
this.signal.pop();
this.signal.unshift(value);
}
else {
this.signal[0] = value;
this.lastIndex = index;
const re = [].concat(this.signal);
const im = [].concat(this.zero);
this.fft.fft1d(re, im);
this.fft.ifft1d(re, im);
return re[0];
}
module.exports = {
name: "fourierMA",
calculator: fourierMA,
params: {
period: predef.paramSpecs.period(64),
filterFreqStart: predef.paramSpecs.period(16),
},
};
Spectrogram
Our Spectrogram has a lot in common with our previous indicator. Let's move out such pieces to
a new helper module:
instance.fft = FFT(period);
instance.zero[i] = 0.0;
instance.lastIndex = -1;
else {
instance.signal.pop();
instance.signal.unshift(value);
else {
instance.signal[0] = value;
}
instance.lastIndex = index;
const re = [].concat(instance.signal);
const im = [].concat(instance.zero);
instance.fft.fft1d(re, im);
return { re, im };
module.exports = {
initialize,
updateSeries,
};
class fourierMA {
init() {
fourierCommon.initialize(this);
}
map(d, index) {
if (transform) {
const re = transform.re;
const im = transform.im;
this.fft.ifft1d(re, im);
return re[0];
module.exports = {
name: "fourierMA",
calculator: fourierMA,
params: {
period: predef.paramSpecs.period(64),
filterFreqStart: predef.paramSpecs.period(16),
},
tags: [fourierCommon.tag],
};
The Spectrogram indicator will use the same approach to calculate Fourier coefficients. Then,
we will build a custom plotter that will show these coefficients as 2D map of frequencies and
their amplitudes.
const p = require("./tools/plotting");
class spectrogram {
init() {
fourierCommon.initialize(this);
this.peakValue = 0;
map(d, index) {
if (transform) {
const n = period / 2 + 1;
const re = transform.re[i]/period;
const im = transform.im[i]/period;
amplitudes.push(amplitude);
return {
amplitudes,
lower: 1,
upper: period / 2 + 1
};
else {
return {};
function hexhex(d) {
return (d < 16 ? "0" : "") + d.toString(16);
function toRgb(r, g, b) {
if (item.amplitudes) {
(amp) => {
});
heatmap.addColumn(p.x.get(item), colors);
canvas.drawHeatmap(heatmap.end());
module.exports = {
name: "spectrogram",
description: "Spectrogram",
calculator: spectrogram,
params: {
period: predef.paramSpecs.period(64)
},
tags: [fourierCommon.tag],
areaChoice: meta.AreaChoice.NEW,
plotter: [
predef.plotters.custom(heatmapPlotter)
],
};
Because there are no regular plots and the app can struggle to evaluate min/max values of
indicator to properly auto-scale it in the area, we implemented Indicator.scaler field in the
module's export. It will tell the app to use two fields from the output for scaling, even if they are
not plotted.
The same lower and upper fields are used as domain boundaries for the heatmap object. Each
column of the heatmap is a list of colors that divides a space between lower and upper to equal
pieces.
Note: vertical axis shows frequency as a divider of the period of the indicator. For
example, 1 corresponds to the whole period, 2 - twice faster than indicator's period, etc.
Graphics
Graphics
Custom Indicators now has a Graphics module. We can leverage this in our own indicators to
create more complex graphics. To enable graphics in your indicator, modify the object return
value of map.
//...
map(d) {
return {
graphics: {
items: [
//GraphicsObjects go here.
//...
The items field within graphics tells the renderer what graphics to display. Each item must
adhere to the GraphicsObject interface.
Let's imagine we want to note when a substantial gain has occured at an index on the chart. To
do so we must track the last known value. Let's write a tool to help us do this:
function MyTracker(magicNumber) {
function tracker(value) {
return tracker.push(value)
}
tracker.push = (value) => {
tracker.state.last = value
return result
tracker.reset = () => {
tracker.state = {
last: -1
tracker.reset()
return tracker
On push, the tracker tool will compare the difference of incoming value and the last known
value against a magicNumber that will be set by UI parameters. The push function will return
true if the difference is greater than or equal to the magic number, indicating a 'substantial
gain', as defined by the end user. We will use boolean result to determine whether or not to
render a graphic at the point of 'substantial gain'.
Now let's build our calculator - the class that will ultimately represent our indicator.
class SubstantialGain {
init() {
this.tracker = MyTracker(this.props.magicNumber)
}
map(d) {
return {
items: [
tag: "Text",
key: "ex",
point: {
},
text: "!",
textAlignment: "centerMiddle"
},
module.exports = {
name: "substantialGain",
description: "SubstantialGain",
calculator: SubstantialGain,
params: {
},
tags: ["Drawings"],
We first must initialize our tracker tool. We do so in the init function of our calculator, leveraging
our module's props. In the map function, we use our tracker tool to get a boolean value for the
bar currently being drawn. Finally in the return object of our map, we can add our first graphics
item. This particular object is a Text object. Also, look closely at the point field of our Text
graphics item. We have to declare the point as a ScaleBound value, so we will use the special
helper operators op, du, and px. px allows us to declare a value in the pixel unit-space,
while du utilizes Domain Units. In the X axis, this is the bar index. In the Y axis, domain units are
the price of the stock. The op function is special - it allows us to operate on px and du values
interchangeably. So we can do things like add absolute pixel values to domain unit values if we
want (and we have done exactly that in the example code!). If we were to select our Substantial
Gains indicator and supply it with a (reasonable) magic number, we should see some greenish
exclamation points rendered over some of our chart's bars.
That's a great start, but we have substantially more power at our disposal. Let's start by making
our graphic a bit more complex. Within our items array, let's add another GraphicsObject.
//...
tag: "Shapes",
key: 'circs',
primitives: [
tag: 'Circle',
radius: 10,
center: {
},
},
],
fillStyle: {
color: "#5c5"
},
//...
This is another type of GraphicsObject, the ContourShapes type. This object describes one or
more Shape objects to apply a border stroke to, as defined by its lineStyle field. Now we should
see a circled exclamation point on bars that have had 'substantial gains'. Now that we have
more than one logically grouped element, we should consider containing them as a unit. To do
so, we can use the Container type. Replace your items code with this
single GraphicsObject object.
//...
tag: 'Container',
key: 'mediumContainer',
children: [
tag: "Text",
key: "ex",
point: {
text: "!",
textAlignment: "centerMiddle"
},
tag: "ContourShapes",
key: 'circs',
primitives: [
tag: 'Circle',
radius: 10,
center: {
},
},
],
lineStyle: {
lineWidth: 2,
color: "#5c5"
},
],
},
//...
The container has one unique field, children, which is an array of GraphicsObjects. You can
leverage this grouping to apply transformations that keep these contained objects in relative
space. That way you can move the whole container instead of each object. The other benefit of
this is that we can leverage VisibilityConditions over whole groups. Here's what we should see
when we render this indicator:
To understand our next step, let's do an experiment. First, zoom way out in the X axis - you can
do so by scrolling the mouse wheel. Those circled exclamations don't scale down to fit the new
bar size, instead they get clumped up and look bad. Now zoom way in on the X axis. Now
because we haven't scaled up they look somewhat lacking and small. We can fix all of this by
creating a variety of groups to describe each breakpoint and give them a conditions field. These
are the VisibilityConditions for the object being rendered.
VisibilityConditions consist of scalar ranges in the X or Y axis. These are measured in pixels-per-
domain-unit. This works out to roughly the pixel distance between bars in the X axis, and the
tick-size in pixels of an index for the given contract being calculated. What this allows us to do is
render something different based on breakpoints we define. Here's an example:
//...
tag: 'Container',
key: 'mediumContainer',
conditions: { //<== Added this field!
scaleRangeX: { min: 10 },
},
children: [
],
},
tag: 'Container',
key: 'tinyContainer',
conditions: {
},
children: [
tag: 'Dots',
key: 'dots',
dots: [
point: {
x: du(d.index()),
},
color: {
r: .25,
g: .8,
b: .25
],
style: {
lineWidth: 6,
color: '#5c5'
},
],
},
//...
Now when the bars are tiny, and have only between 0 and 10 pixels between them, we will
render a tiny dot instead of the circled exclamation.
This introduces another GraphicsObject as well - the Dots object. It describes one or more
WebGL dots to render. At 10 pixels of space per bar or greater we draw our standard circled
exclamation. Note that the scaleRangeX, scaleRangeY, and their min and max fields are actually
optional. If omitted, we simply won't apply a bound in the ignored direction or on the ignored
axis. We can of course apply something extra for large pixel distances as well.
I'd like to display the amount of the 'substantial gain' at high pixel-per-domain-unit distances.
We will need to modify our tracker slightly to accomodate this - we currently discard our last
value on each push. We will could provide the difference in the two values as part of the return
value of our tracker's push function.
tracker.state.last = value
map(d) {
return {
items: [
//We only want to add a single extra element - no need for another container
conditions: {
scaleRangeX: { min: 30 }
},
tag: "Text",
key: "bigText",
text: `+ ${difference}`,
point: {
},
textAlignment: "centerMiddle"
},
//...
//...
Rendering our indicator should yield this result at big pixel-to-domain ratios:
Alligator
The Alligator is a classic indicator based on the idea that markets spend most of their time in a
horizontal motion and about 15-30% of the time trending. Different interactions of its three
SMA plots are said to help identify a coming trend and predict its direction. Here's the basic
definition:
▪ The Jaws are a 13 period SMA shifted into the future by 8 bars.
▪ The Lips are an 8 period SMA shifted into the future by 5 bars.
▪ The Teeth are a 5 period SMA shifted into the future by 3 bars.
Using the Indicator's shifts property, and the builtin SMA tool, we can easily accomplish the
Alligator. shifts allow us to define bar-offsets on the X axis into the past or future on a plot-by-
plot basis.
class Alligator {
init() {
this.jaws = SMA(this.props.jaws)
this.lips = SMA(this.props.lips)
this.teeth = SMA(this.props.teeth)
map(d) {
return {
}
module.exports = {
name: "Alligator",
description: "Alligator",
calculator: Alligator,
params: {
jaws: predef.paramSpecs.period(13),
lips: predef.paramSpecs.period(5),
teeth: predef.paramSpecs.period(8)
},
plots: {
},
tags: [predef.tags.MovingAverage],
schemeStyles: {
dark: {
},
//we can use shifts to offset our bars. Negative values would be into the past.
shifts: {
jaws: 8,
lips: 5,
teeth: 3
};
Custom Drawing Tools allow us to easily create and share custom user-defined drawing tools for
use in the Trader application. Defining a custom drawing tool is very similar to defining a custom
indicator. We need to create an object that obeys the DrawingToolImplementation interface, as
well as export an object that obeys the DrawingTool interface. Let's look at each:
const MyLine = {
module.exports = {
maxPeriod: predef.paramSpecs.period(14)
},
We can see from this example how to define a Custom Drawing Tool at the module.exports level
- we need at least the name and drawing fields. The most important field is the drawing. This is
where we define our DrawingToolImplementation methods. Let's explore those methods now.'
const MyLine = {
init() {
},
//graphic items to render associated with this Drawing Tool. The parameter object is
},
//we can change our state parameter using update. A common use case would be to perform
a big calculation
//and store it in state only when necessary to improve performance and decrease resource
usage.
if(someCondition) {
},
//holding the SHIFT key over a drawing reveals its tooltip. We can determine how
return [
},
//controls the X and Y coordinate space that is valid for the anchor at the matching position in
the array.
//anchors[0] will be allowed to move 10 units in the X axis, anchors[1] will be able to move 5
units in the Y axis.
//If there is no X or Y value listed, there will be no restraint placed on the anchor's valid
coords.
anchorRestraints() {
return [
},
//controls the color of each anchor at the matching position in the array.
//anchors beyond the index accounted for in this array will default to the tail color,
anchorStyles() {
return [
You can see that each of these functions controls some aspect of the tool. Let's do something
simple and draw a line between two points using the render function.
const MyLine = {
render({anchors}) {
return {
items: [
tag: 'LineSegments',
key: 'line',
lines: [
tag: 'Line',
a: anchors[0],
b: anchors[1]
],
lineStyle: {
lineWidth: 2,
color: '#0f0'
},
Now if we choose 'My Line' from the drawing tools selector, we should be able to draw a line
between two points.
This is a very simple line drawing tool. Let's take it a step further and add a tooltip. When
viewing a Drawing Tool that you've drawn onto a chart, you may mouse over it holding
the SHIFT key to reveal its tooltip. If you don't define tooltip behavior, nothing will happen. Let's
make a tooltip that renders some text and a special delta object.
const MyLine = {
//...
tooltips({anchors}) {
return [
{
coord: anchors[0], //coord tells the tooltip where to render in chart space. An
anchor's point is a typical choice
tag: 'predef',
x: 'left',
y: 'above'
},
items: [
},
content: {
//...
When we hold SHIFT over our drawing now, it will display 'My Line' and the tick delta
information between the two points anchors[0] and anchors[1]. Try moving it around to see
how it changes.
ScaleBound Values
When we define or calculate measurements in chart space when using Custom Indicators or
Custom Drawing Tools, we need to consider the unit type that we are working with. We use two
typical unit types when developing custom Tradovate tools:
▪ px: the px unit is for pixels. When we want to define something with an absolute pixel
unit value, we use px.
▪ du: the du unit stands for domain units. In the X axis, domain units are the index of the
bar, while in the Y axis they are the price of the asset.
We provide helper functions to work with the Scale Bound values expected by anything that
requires a Point type object. We can import those functions into our Custom Indicator or
Custom Drawing file like so:
//...
▪ op: allows for operation between px or du Scale Bound values. This allows us to write
code like so - const myVal = op(px(16), '-', du(4525)) - this would result in a value equal
to 16 pixels above the 4525 price, given that this is used for a y coordinate.
▪ min: chooses the lesser of a value. Ex. min(px(d.index()*16), du(d.index()) will choose
the smaller of 16 pixels times the bar index, or the bar index as a domain unit.
▪ max: works exactly the same way as min, except it chooses the larger of the given
values.
Anytime we need to define a point in chart space, we will need to provide ScaleBound values.
Knowing how to use these functions will save you time and effort when it comes to creating
custom tools. Some examples of ScaleBound values in the wild are the x and y fields of
any Point type object (so anchors in custom drawing tools, or the point field of a Text type
DisplayObject).
Because we can do whatever we want with JavaScript, we have a huge amount of freedom
when developing our own custom drawing tools. This allows us to use advanced math, place
graphics arbitrarily, and even add tooltips that contain custom calculations. In this article, I'll
demonstrate what we can accomplish with the Custom Drawing module by walking through the
creation of a simple Trend-Line drawing tool.
render() {},
tooltips() {},
anchorRestraints() {},
anchorStyles() {
return [
{ color: props.lower },
{ color: props.lower },
{ color: props.upper }
module.exports = {
name: "Trender",
description: "Trender",
drawing: Trender,
params: {
upper: predef.paramSpecs.color('#2d2'),
middle: predef.paramSpecs.color('#999'),
lower: predef.paramSpecs.color('#d22')
},
maxN: 3
First, notice we've brought a few methods and objects into scope with require. We will need We
can define the our drawing tool as a simple object. The Trender object contains the drawing
implementation methods that we will need to utilize. We can then use that object as
our drawing field in the module.exports portion of the file. Some other notable portions of
the module.exports object include the params field, our user defined parameters,
and maxN which lets us define how many anchors our tool can have. In this case we want three
anchors, which I'll explain as we go on. We can begin filling out our drawing implementation
methods at this point. For now, let's focus on the render method. We will start by rendering a
simple line:
const Trender = {
//...
render({anchors, props}) {
return {
items: [
tag: 'LineSegments',
key: 'lower',
lines: [
tag: 'Line',
a: anchors[0],
b: anchors[1]
],
lineStyle: {
width: 2,
color: props.lower
},
//...
If we try this, it should produce a line from the first anchor to the second anchor in
the props.lower color (which defaults to reddish). But what we really want are three lines - an
upper line, a lower line, and a midpoint line. We can use the third anchor to determine the
height of our trend channel. Let's add a top line based on the third anchor:
const Trender = {
//...
render({anchors, props}) {
? anchors[2].y.value - anchors[0].y.value
:0
return {
items: [
//...
{
tag: 'LineSegments',
key: 'upper',
lines: [
tag: 'Line',
a: anchors[2]
?{
x: anchors[0].x,
y: du(anchors[0].y.value + upperDiff)
: anchors[0],
b: anchors[2]
?{
x: anchors[1].x,
y: du(anchors[1].y.value + upperDiff)
: anchors[0]
],
lineStyle: {
width: 2,
color: props.upper
}
}
//...
Now we're getting somewhere. Notice that we've used ternary expressions to determine
the a and b points for the line drawings. This is because trying to read the x or y fields of an
anchor when it is null will produce an error and mess up how the drawing renders, and it allows
us to default to the zeroth anchor before anchors[1] or anchors[2] exist. You'll notice this
pattern of defaulting to a known existing value often. But we still want to have a midpoint line
as well. We can use the same upperDiff again, divided in half, to find the midpoint line of our
channel:
const Trender = {
//...
render({anchors, props}) {
return {
items: [
//...
tag: 'LineSegments',
key: 'middle',
lines: [
tag: 'Line',
a: anchors[2]
?{
x: anchors[0].x,
y: du(anchors[0].y.value + upperDiff/2)
: anchors[0],
b: anchors[2]
?{
x: anchors[1].x,
y: du(anchors[1].y.value + upperDiff/2)
: anchors[0]
],
lineStyle: {
width: 2,
color: props.middle,
lineStyle: 3
},
//...
Our drawing is basically complete. You should have something that looks like this:
But when you hold SHIFT over this drawing, we get no information. That's because we haven't
drawn any tooltips yet. Luckily, drawing tooltips is just as easy as defining a render function.
Let's draw a tooltip for the price-point at anchors[0].
const Trender = {
//...
tooltips({anchors}) {
return [
coord: anchors[0],
alignment: {
tag: 'predef',
x: 'center',
y: 'below'
},
items: [
{
key: 'a0',
content: anchors[0].y.value
},
},
//...
We should add a tooltip for the prices at each of the corners of our tool. Let's write another
tooltip for anchors[1] that includes both the price-point, and the tick-delta
between anchors[0] and anchors[1]. To get the tick delta, we'll need to track some differences
between points that we have defined and their absolute values as well.
const Trender = {
//...
tooltips({anchors, props}) {
//difference between anchors 0 and 1, and the absolute value of that difference
deltaA = Math.abs(deltaARaw)
return [
//...
{
coord: anchors[1] || anchors[0],
alignment: {
tag: 'predef',
x: 'right',
y: 'below'
},
items: [
//price-point
key: 'a1',
},
//tick delta
title:
: ' - ',
key: 'lowerMin',
},
},
}
//...
Notice that we have some runtime variance here. Based on whether the deltaARaw value is
positive or negative, we will render different text based on what that price point and tick delta
represent. In this case it is the point at anchors[1], so the lower right corner. This is either the
minimum long value of the trend or the maximum short value of the trend.
In order to implement the midpoint and upper points, we will need a value for the vertical delta.
Let's add that to our defined variables and add another tooltip for the midpoint price point and
tick delta:
const Trender = {
//...
tooltips({anchors, props}) {
deltaA = Math.abs(deltaARaw),
deltaB = Math.abs(deltaBRaw)
return [
//...
coord:
anchors[1]
?{
x: anchors[1].x,
y: du(anchors[1].y.value + deltaB/2)
}
: anchors[0],
alignment: {
tag: 'predef',
x: 'right',
y: 'center'
},
items: [
key: 'a2.5',
},
title:
: ' - ',
key: 'mid',
content:
: { delta: 0 }
},
},
]
}
We have to be a little creative about the coord field for the midpoint. I've used the du graphics
helper function (which we required at the top of the file) to create the y coordinate. This is
because all of the values we use must be ScaleBound values. These are simple objects which
have both a unit field and a value field. We can construct them in a less verbose way by
using du.
Note: du stands for Domain Units. There is also the px method which constructs ScaleBound
values with the Pixels type unit.
We still need to finish filling out our tooltips. Let's finish them off by adding the top-left and top-
right points.
const Trender = {
//...
tooltips({anchors, props}) {
deltaA = Math.abs(deltaARaw),
deltaB = Math.abs(deltaBRaw)
return [
//...
//top left
coord:
anchors[1]
?{
x: du(anchors[1].x.value),
y: du(anchors[1].y.value + deltaB)
: anchors[0],
alignment: {
tag: 'predef',
x: 'right',
y: 'above'
},
items: [
//price
key: 'a1.5',
},
//delta
title:
: ' - ',
key: 'lowerMax',
content:
: { delta: 0 }
},
},
//top right
alignment: {
tag: 'predef',
x: 'center',
y: 'above'
},
items: [
key: 'a2',
Now when we hold SHIFT over our drawing, we should see some actually useful information. Try
it:
Tradovate Custom Indicators
Modules
Shifts
TimeSeries
calculator
canvas
dlls
drawing-tool
graphics/DisplayObject
graphics/GraphicsResponse
graphics/Scale
graphics/Style
indicator
params
plots
plotter
scaler
scheme-styles
Here’s an example:
const p = require("./tools/plotting");
const ma = {
simple: SMA,
exponential: EMA,
weighted: WMA,
modified: MMA,
hull: HMA
};
class atrTrailStop_wTargets {
init() {
this.smaMA = SMA(this.props.atrPeriod);
this.atrMA = ma[this.props.movingAverageType](this.props.atrPeriod);
this.trailStop = null;
this.fib1 = null;
this.fib2 = null;
this.fib3 = null;
this.trend = null;
this.trendUp = null;
this.trendDown = null;
this.ex = null;
this.currClose = undefined;
this.currHigh = undefined;
this.currLow = undefined;
this.switchVal = null;
this.diff = null;
map(d, i, history) {
// Initialize Variables
const HRef = currLow <= prevHigh ? currHigh - prevClose : (currHigh - prevClose) - 0.5 *
(currLow - prevHigh);
const LRef = currHigh >= prevLow ? prevClose - currLow : (prevClose - currLow) - 0.5 *
(prevLow - currHigh);
// Trend Logic
// Extremum Logic
// Fib Targets
return {
trend: this.trend,
trendUp: this.trendUp,
trendDown: this.trendDown,
ex: this.ex,
fib1: this.fib1,
fib2: this.fib2,
fib3: this.fib3,
l100: l100,
trailStop: this.trailStop,
switchVal: this.switchVal,
diff: this.diff,
fibTgt1,
fibTgt2,
fibTgt3,
trendChange,
};
let item1;
if (i > 0 && item.trend !== undefined && item.trailStop !== undefined && item.fib1 !==
undefined && item.fib2 !== undefined && item.fib3 !== undefined && item.l100 !== undefined)
{
const x = p.x.get(item);
const x1 = p.x.get(item1);
canvas.drawLine(
p.offset(x1, item1.trailStop),
p.offset(x, item.trailStop),
relativeWidth: 0.5,
});
item1 = item;
if (item.trend !== undefined && item.trailStop !== undefined && item.fib1 !== undefined
&& item.fib2 !== undefined && item.fib3 !== undefined && item.l100 !== undefined) {
if (indicatorInstance.props.colorZones){
const x = p.x.get(item);
canvas.drawLine(
p.offset(x, item.fib1),
p.offset(x, item.fib2),
);
canvas.drawLine(
p.offset(x, item.fib2),
p.offset(x, item.fib3),
relativeWidth: 1.0,
);
canvas.drawLine(
p.offset(x, item.fib3),
p.offset(x, item.l100),
relativeWidth: 1.0,
);
}
function drawFibTargets(canvas, indicatorInstance, history) {
let item1;
if (i > 0 && item.trend !== undefined && item.trailStop !== undefined && item.fib1 !==
undefined && item.fib2 !== undefined && item.fib3 !== undefined && item.l100 !== undefined)
{
if (indicatorInstance.props.showFibTargets){
const x = p.x.get(item);
const x1 = p.x.get(item1);
canvas.drawLine(
p.offset(x1, item1.fibTgt1),
p.offset(x, item.fibTgt1),
relativeWidth: 0.4,
});
p.offset(x1, item1.fibTgt2),
p.offset(x, item.fibTgt2),
relativeWidth: 0.4,
});
canvas.drawLine(
p.offset(x1, item1.fibTgt3),
p.offset(x, item.fibTgt3),
relativeWidth: 0.4,
});
canvas.drawLine(
p.offset(x1, item1.fibTgt1),
p.offset(x, item.fibTgt1),
{
color: item.trend == 1 ? "Red" : item.trend == -1 ? "Green" : "White",
relativeWidth: 1,
});
canvas.drawLine(
p.offset(x1, item1.fibTgt2),
p.offset(x, item.fibTgt2),
relativeWidth: 1,
});
canvas.drawLine(
p.offset(x1, item1.fibTgt3),
p.offset(x, item.fibTgt3),
relativeWidth: 1,
});
item1 = item;
}
}
module.exports = {
name: "atrTrailStop_wTargets",
calculator: atrTrailStop_wTargets,
params: {
trailType: predef.paramSpecs.enum({
modified: "Modified",
unmodified: "Unmodified",
}, "modified"),
movingAverageType: predef.paramSpecs.enum({
simple: "Simple",
exponential: "Exponential",
hull: "Hull",
modified: "Wilder's",
weighted: "Weighted",
}, "modified"),
atrPeriod: predef.paramSpecs.period(5),
colorZones: {
type: "boolean",
def: false,
},
showFibTargets: {
type: "boolean",
def: true,
},
},
inputType: meta.InputType.BARS,
plots: {
},
plotter: [
predef.plotters.custom(colorATR_TrailStop),
predef.plotters.custom(drawATR_TrailStop),
predef.plotters.singleline("fib1"),
predef.plotters.singleline("fib2"),
predef.plotters.singleline("fib3"),
predef.plotters.custom(drawFibTargets),
],
tags: [predef.tags.Volatility],
schemeStyles: {
dark: {
},
light: {
};