A Couple of Simple Custom Indicators for NinjaTrader 8 (NinjaScript)
In this tutorial we will go over creating a Volume-Weighted Average Price (VWAP) indicator and two different variations of a Net Change indicator.
Update:
This artcle was originally published for paid subscribers but has since then been set free. It has been some time since I have visited this article or used NinjaScript/NinjaTrader. Reach out if you have any questions.
This post will cover a couple of simple NinjaScript indicators created using NinjaScript. I consider these indicators simple because the calculations used for them aren’t incredibly complex. Of course, they could evolve into more complex indicators, but at the core they are relatively simple. This is also the first post that I am making for paid subscribers. The reason for this is that I intend on continuously working on these indicators as I go on, implementing new features as I learn about them.
If you haven’t setup your environment for being able to code and debug NinjaScript files you can view the Trade Testing Environment article. Be sure to continuously check out the House Keeping post for updates as well. It is designed to help make navigation around the Substack easier and to keep you informed about what is going on with HGT and the logic behind it. Paid subscribers will have access to this code (and more) on the HGT private GitHub repository. Code will always be updated first on the GitHub before articles get updated and not all the code in the GitHub has an associated tutorial.
Disclaimer: the following post is an organized representation of my research and project notes. It doesn’t represent any type of advice, financial or otherwise. Its purpose is to be informative and educational.
Volume-Weighted Average Price Indicator
The VWAP indicator was the first NinjaScript indicator that I tried my hand at, and as such, it is a work in progress. In its current state, it calculates everything with a period of zero (0) and cumulates volume data at the beginning of the trade session. There will be some code inside of this indicator acting as a placeholder for improvements down the road.
I only attempted this indicator because the NinjaTrader 8 platform does not come with a VWAP indicator out of the box. Instead, you have to purchase it through one of their upgrade packages. Considering this indicator is often used in day trading, some people might find it helpful. After searching through the NinjaTrader forums, I found some VWAP indicators from the community that were created for NT7. Along with another post from TradingView, I was able to hack together a working (but needs improvement) VWAP indicator.
Calculations For VWAP
According to the aforementioned post, there are five steps in calculating VWAP:
Calculate the Typical Price for the period.
[(High + Low + Close)/3)]
In NinjaScript we can easily access this with built in functions to obtain those exact values.
Multiply the Typical Price by the period Volume.
(Typical Price x Volume)
Volume is accessed with VOL()[0], where the [0] will represent the bar period you are looking at.
Create a Cumulative Total of Typical Price.
Cumulative(Typical Price x Volume)
We will assign this to a variable.
Create a Cumulative Total of Volume.
Cumulative(Volume)
We will assign this to a variable.
Divide the Cumulative Totals.
VWAP = Cumulative(Typical Price x Volume) / Cumulative(Volume)
Calculation taken from the TradingView built in VWAP indicator.1
Code
The following code can be copied and pasted into a new NinjaScript indicator class in the custom solution. In the code, you will see that I created variables for each step above and assigned them values inside OnBarUpdate
. The calculations for the typical price and volume are made at Bars.IsFirstBarOfSession
. The next two variables use the previous ones to calculate the cumulative totals. Finally, we plot the Value using Value [0]
(or friendlier named variable).
Value
is how you access the plot(s) you add in State.SetDefaults
. Both of these subjects could use further explanation. Still, I will save that for when we go over a more complicated indicator and it becomes more prevalent. We can also use NinjaScript Attributes to create plot variables accessible via the strategy builder or give them friendlier names for your code. This seems to be more beneficial if you have multiple plots in one indicator. 2
There is a better way to do this, but this is what I have come up with so far. This indicator is meant to be used intraday, as it calculates portions of the equation at the beginning of each session. However, I am willing to bet there are better ways to do this. If you throw this onto a chart, you will notice that the lines are choppy. I want to be able to “smooth” these out. I would also like to look at the VWAP indicator on other charting software to see if the calculations have significant differences.
You will see that I adjusted the name in the code below. This is due to a naming conflict with the built-in VWAP indicator.
#region Using declarations
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Windows.Media;
using System.Xml.Serialization;
using NinjaTrader.Data;
#endregion
//This namespace holds Indicators in this folder and is required. Do not change it.
namespace NinjaTrader.NinjaScript.Indicators
{
public class VWAPaf : Indicator
{
private const string IndName = "HGT-VWAP";
private const string IndVersion = "v1";
private double _typicalPrice;
private double _volume;
private double _cumulativeTotalofVolume;
private double _cumulativeTotalofTypicalPrice;
#region Properties
[Browsable(false)] <--- Determines if it is visible in the NT8 GUI
[XmlIgnore] <--- tells it to ignore this property for XML serialization purposes with NinjaScript
public Series<double> VWAP
{
get { return Value; }
}
#endregion
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
SetDefaultSettings();
AddPlot(Brushes.Orange, "VWAP");
}
else if (State == State.Configure)
{
}
else if (State == State.DataLoaded)
{
ResetVariables();
}
}
protected override void OnBarUpdate()
{
if (CurrentBar < 1)
{
return;
}
else if (Bars.IsFirstBarOfSession)
{
_volume = VOL()[0];
_typicalPrice = (High[0] + Low[0] + Close[0]) / 3;
}
else
{
_cumulativeTotalofVolume = _volume + VOL()[0];
_cumulativeTotalofTypicalPrice = (_volume * _typicalPrice) + (VOL()[0] * ((High[0] + Low[0] + Close[0]) / 3));
}
VWAP[0] = _cumulativeTotalofTypicalPrice / _cumulativeTotalofVolume;
}
private void SetDefaultSettings()
{
Name = IndName + IndVersion;
Description = @"Volume-Weighted Average Price";
IsOverlay = true;
Calculate = Calculate.OnBarClose;
DisplayInDataBox = true;
ShowTransparentPlotsInDataBox = true;
DrawOnPricePanel = true;
BarsRequiredToPlot = 0;
ArePlotsConfigurable = true;
DrawHorizontalGridLines = true;
DrawVerticalGridLines = true;
PaintPriceMarkers = true;
ScaleJustification = NinjaTrader.Gui.Chart.ScaleJustification.Right;
//Disable this property if your indicator requires custom values that cumulate with each new market data event.
//See Help Guide for additional information.
IsSuspendedWhileInactive = true;
}
private void ResetVariables()
{
_volume = 0;
_typicalPrice = 0;
_cumulativeTotalofTypicalPrice = 0;
_cumulativeTotalofVolume = 0;
}
}
}
Intraday Net Change Indicators
This indicator comes in two flavors: bar close (OBC) and tick replay (OET). Each variation has its use, depending on what you are trying to accomplish. One updates the calculation in real-time, and the other only updates it as each bar closes.
It may seem obvious, but this means you must understand how and why you are using it to know which one best suits your needs. If used inside a strategy already running calculations on bar close and looking to the past for its indicators, then the OBC version should be fine. However, suppose the indicator or strategy requires more granularity or a more “real-time” calculation of net change. In that case, the OET version will be better. The OET version is meant to be used with Tick Replay for backtesting and optimization.
Calculations
The calculation for net change in these indicators is:
((_currentBid - _dayOpen) / _dayOpen) * 100
The variables in this one are relatively simple. The only difference between these indicators will be where we acquire the current bid values. To get the correct historical bid price for the OET version, we must add an additional override: OnMarketData()
.
OnMarketData()
is called, and guaranteed to be in the correct sequence, for every change in level one market data.3 This is required for accessing historical bid/ask prices during backtesting.
On Bar Close
The following indicator calculates the daily net change (change from open) at the close of each bar.
using NinjaTrader.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace NinjaTrader.NinjaScript.Indicators
{
public class NetChangeOnBarClose : Indicator
{
private const string IndName = "Net Change OBC";
private const string IndVersion = " v1";
private double _currentBid;
private double _dayOpen;
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
SetDefaultSettings();
AddPlot(Brushes.Orange, "PlotNetChange");
}
else if (State == State.Configure)
{
ClearOutputWindow();
}
else if (State == State.DataLoaded)
{
ResetVariables();
}
}
protected override void OnBarUpdate()
{
if (CurrentBar < 1)
{
return;
}
else if (Bars.IsFirstBarOfSession)
{
_dayOpen = Open[0];
}
_currentBid = GetCurrentBid();
Value[0] = ((_currentBid - _dayOpen) / _dayOpen) * 100;
}
private void SetDefaultSettings()
{
Name = IndName + IndVersion;
IsOverlay = false;
Calculate = Calculate.OnBarClose;
DisplayInDataBox = true;
ShowTransparentPlotsInDataBox = true;
BarsRequiredToPlot = 0;
ArePlotsConfigurable = true;
}
private void ResetVariables()
{
_currentBid = double.NaN;
_dayOpen = double.NaN;
}
}
}
On Each Tick (Tick Replay)
The following indicator calculates the daily net change (change from open) on each tick. In order to accurately retrieve the current bid, we will need to get that value during a market update event.4
using NinjaTrader.Data;
using System;
using System.Collections.Generic;
using System.Diagnostics.Eventing.Reader;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
namespace NinjaTrader.NinjaScript.Indicators
{
public class NetChangeOnEachTick : Indicator
{
private const string IndName = "Net Change OET";
private const string IndVersion = " v1";
private double _currentBid;
private double _dayOpen;
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
SetDefaultSettings();
AddPlot(Brushes.Orange, "PlotNetChange");
}
else if (State == State.Configure)
{
ClearOutputWindow();
}
else if (State == State.DataLoaded)
{
ResetVariables();
}
}
protected override void OnMarketData(MarketDataEventArgs marketDataUpdate)
{
// TickReplay events only occur on the "Last" market data type
if (marketDataUpdate.MarketDataType == MarketDataType.Last)
{
_currentBid = marketDataUpdate.Bid;
};
}
protected override void OnBarUpdate()
{
if (CurrentBar < 1)
{
return;
}
else if (Bars.IsFirstBarOfSession)
{
_dayOpen = Open[0];
}
Value[0] = ((_currentBid - _dayOpen) / _dayOpen) * 100;
}
private void SetDefaultSettings()
{
Name = IndName + IndVersion;
IsOverlay = false;
Calculate = Calculate.OnEachTick;
DisplayInDataBox = true;
ShowTransparentPlotsInDataBox = true;
BarsRequiredToPlot = 0;
ArePlotsConfigurable = true;
}
private void ResetVariables()
{
_currentBid = double.NaN;
_dayOpen = double.NaN;
}
}
}
Conclusion
The indicators above, especially the VWAP indicator, are in their infancy. As I learn more, I will return to these and make improvements as we go along. The net change indicators may not receive as much love as the VWAP indicator. As I have been working on coding a NinjaScript strategy, I have realized that you can easily make those intraday net change calculations within the strategy itself without having to use an indicator to retrieve the value. This could explain why NT8 lacks an intraday net change indicator. However, creating them as an indicator does make them accessible inside the strategy builder if you do not wish to use NinjaScript to code your strategy.
Feel free to comment below or e-mail me if you need help with anything, wish to criticize, or have thoughts on improvements. Paid subscribers can access this code and more at the private HGT GitHub repo. As always, this newsletter represents refined versions of my research notes. That means these notes are plastic. There could be mistakes or better ways to accomplish what I am trying to do. Nothing is perfect, and I always look for ways to improve my techniques.