Strategy Template for NinjaTrader 8 (NinjaScript)
This is the first article in a series revolving around the creation of trading strategies in NinjaTrader 8 using NinjaScript.
This is the first article in a series revolving around the creation of trading strategies in NinjaTrader 8 using NinjaScript. It discusses building a basic strategy template to be used as a starting point for creating and researching strategies later. This article will get updated as I learn more and update the template I am using for strategies.
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.
On to the task at hand.
Creating the Strategy Template
To start, open Visual Studio and open up the NinjaTrader.Custom.sln file. It will open a previous project or take you to a blank window. Inside the solution, navigate to the strategies folder. Here, you can create a folder for your custom files (which is what I do), or you can add a new class item (right-click) to this folder directly. It should give you a file that looks like this:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NinjaTrader.Custom.Strategies.HGT
{
internal class Class1
{
}
}
This is where we will start. The first thing we are going to do is change the naming structure so that we can inherit the NinjaScript Strategy class and go ahead and add in the NinjaScript event methods that we will need for every strategy. We will also go ahead and declare a const string
variable for the strategy name.
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript.Indicators;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NinjaTrader.NinjaScript.Strategies
{
public class StrategyTemplate : Strategy
{
#region Strategy Variables
private const string StrategyName = "Strategy Template";
#endregion
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
// Default settings go here but to keep things tidy we will create a helper function to define them below.
}
else if (State == State.Configure)
{
// Additional data serieses will go here.
}
else if (State == State.DataLoaded)
{
// This is where you will add indicators, set simple SL/TP, and add in a section for mutable variables.
ClearOutputWindow(); // Clears the output window each time this state is called.
}
}
protected override void OnBarUpdate()
{
// This logic just makes sure we don't throw and error trying to get data from a bar that doesn't exist.
if (CurrentBar < BarsRequiredToTrade)
return;
}
protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)
{
// Logic for capturing information about trade executions.
}
protected override void OnPositionUpdate(Position position, double averagePrice, int quantity, MarketPosition marketPosition)
{
if (position.MarketPosition == MarketPosition.Flat)
{
// When a position is closed, do something.
}
}
}
}
We have added 4 overrides: OnStateChange()
, OnBarUpdate()
, OnExecutionUpdate()
, and OnPositionUpdate()
. The last two may not be required if you do not plan to do any trade management outside the built-in functions. However, I believe there are great benefits to controlling your trade management logic, so I keep it in my template.
The OnStateChange()
event method is used to set default settings, add additional data, chart indicators, and set/reset some mutable variables (more on this later).
OnBarUpdate()
is where all the logic takes place. This event method fires every time there is a change in the state (price) of the bar. That doesn't mean that calculations are run for each change of state. The calculation frequency is set in the default settings.
OnExecutionUpdate()
fires every time there is an order execution. It holds the information about the execution. This area assigns values from the executions to variables and methods within the strategy if you need them. It allows you to create trade management systems within the strategy.
After this, we will build a few helper functions that we will use continuously through our custom strategies. Many of these functions are created to keep things tidy and to keep change locations down to a minimum. In other words, anything that might get used more than once will get turned into a helper. The first few functions will go within the OnStateChange
handler.
OnStateChange() Helper Functions
The first is the SetDefaultSettings()
function. This is done solely for aesthetic purposes and to set a few settings that I [almost] always keep different than default. The biggest one to note is the IsInstantiatedOnEachOptimizationIteration
setting. I change this to speed up the optimization process—more on this in the next section.1
private void SetDefaultSettings()
{
Name = StrategyName;
IsOverlay = true;
Calculate = Calculate.OnBarClose;
IsExitOnSessionCloseStrategy = true;
ExitOnSessionCloseSeconds = 30;
OrderFillResolution = OrderFillResolution.Standard;
TraceOrders = false; // This can be turned on to help with debugging the strategy and looking at trades/orders.
BarsRequiredToTrade = 1; // Not sure that this matters but I use it to manage the first bar on the chart problem.
// Disable this property for performance gains in Strategy Analyzer optimizations.
// You will have to call your own logic to clear mutable variables each iteration.
IsInstantiatedOnEachOptimizationIteration = false;
}
Next is the ResetVariables()
function. We keep this handy in case out strategy needs some of its variables manually wiped and reset for optimization. This will be needed in most scenarios unless you wish to enable the optimization setting mentioned above. We will create another helper function and call it within the State.DataLoaded
to ensure that variables are reset when iterating through optimizations.
private void ResetVariables()
{
// variables that need to be manually reset go here.
}
Then we create an AddIndicators()
function to help us with adding indicators and assigning indicators values to variables. I leave an example indicator in the template as a reminder of the naming convention. For each variable that we are assigning a value to here needs to be declared at the class level as well.
#region Strategy Variables
private const string StrategyName = "Strategy Template";
private MACD _macd;
private ISeries<double> _macdDefault;
private ISeries<double> _macdAvg;
private ISeries<double> _macdDiff;
#endregion
private void AddIndicators()
{
_macd = MACD(_macdfast, _macdslow, _macdsmooth); <--- Notes on these variables below
_macdDefault = _macd.Default;
_macdAvg = _macd.Avg;
_macdDiff = _macd.Diff;
AddChartIndicator(_macd);
}
Notice that we are going to use 3 variables for the MACD values instead of hard coding the numbers in that we want. This is so we can turn those values into parameters that can be manipulated by the user during the testing and optimization phase of strategy development. In order to do this, we need to introduce NinjaScript Property, Display, and Range Attributes. 2
NinjaScript Attributes
There are several different types of attributes that are useful when developing in NT8 and NinjaScript. This is section is just a simple introduction to the concept. These attributes will be explored in more depth as the articles progress in complexity. Any discoveries that change how I setup my template will be noted here as well.
This section is going to introduce a couple NinjaScript Attributes and how to use them to define your parameter variables as NinjaScript Properties and set default values in the "Parameters" section of the template. We will add a couple of parameters for reference and future use.
The NinjaScript Property Attribute is used to declare a variable as a parameter in NinjaScript. They are useful for making parameters optimizable when testing.
The Range Attribute allows you to create a specific validation range for the parameter. For example. 1-100 would mean that no numbers below 1 and above 100 can be input by the user.
The Display Attribute determines how the property is displayed on the UI property grid.
We will use the [NinjaScriptProperty]
, [Range]
and [Display]
attributes. All of the code goes within #region Parameters
.
Create another region below the strategy variables named "Properties". Then we are going to declare and set the default values for the parameters that we wish to be optimizable during testing. I am unsure if the methods I use for declaring the values as private variables instead of public variables are necessary, but I will keep using it until I find out otherwise.
#region Properties
private int _macdfast = 12;
private int _macdslow = 26;
private int _macdsmooth = 9;
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Fast", Order = 1, GroupName = "Signal Parameters")]
public int MACDfast
{
get { return _macdfast; }
set { _macdfast = value; }
}
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Slow", Order = 2, GroupName = "Signal Parameters")]
public int MACDslow
{
get { return _macdslow; }
set { _macdslow = value; }
}
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Smooth", Order = 3, GroupName = "Signal Parameters")]
public int MACDsmooth
{
get { return _macdsmooth; }
set { _macdsmooth = value; }
}
#endregion
OnBarUpdate() and Order Management Helpers
Next, we will setup a few standard structures and functions in the OnBarUpdate()
handler that will help us with strategy execution logic. This section is where all of the trade system logic will go, so we will do everything we can to keep it clean by defining helper functions and variables. The first thing we will do is create a SetEntrySignalState()
function. Inside this function there are a couple of small `bool` functions we will need to create as well. Some of them are just place holders until you actually get to designing strategy logic. I leave the TimeWindow()
function as an example of creating a window for trade times. It isn't always necessary, but it can be useful for creating different ways to test and optimize a strategy.
private void SetSignalState()
{
_enterLongSignal1 = EntrySignalLong1(); // Place holders
_enterShortSignal1 = EntrySignalShort1(); // Place holders
_exitLong = ExitSignalLong(); // Place holders
_exitShort = ExitSignalShort(); // Place holders
_timeWindow = TimeWindow();
}
private bool EntrySignalLong1()
{
return CrossAbove(_macdDefault, _macdAvg, 1);
}
private bool EntrySignalShort1()
{
return CrossBelow(_macdDefault, _macdAvg, 1);
}
private bool ExitSignalLong()
{
return CrossBelow(_macdDefault, _macdAvg, 1);
}
private bool ExitSignalShort()
{
return CrossAbove(_macdDefault, _macdAvg, 1);
}
private bool TimeWindow()
{
return ToTime(Time[0]) >= 100000 && ToTime(Time[0]) < 160000 || ToTime(Time[0]) >= 180000 && ToTime(Time[0]) < 200000;
}
Next, we need a series of helper functions that make it easier to manage order state when we are dealing with a strategy that has multiple signals. To do this we will also need to declare an enum at the class level, a PositionType
enum, a couple Order
variables. and names for our entry and exit signals. This will also be the first set of variables that we are manually going to reset each time the strategy is loaded. They will be reset inside the ResetVariables()
function as mentioned above.
#region Strategy Variables
...
private const string OpenLongSignal1 = "Open Long 1";
private const string OpenShortSignal1 = "Open Short 1";
private const string CloseLong = "Close Long";
private const string CloseShort = "Close Short";
private PositionType _currentPositionType;
private Order _longOrder;
private Order _shortOrder;
private Order _exitLongOrder;
private Order _exitShortOrder;
enum PositionType
{
Long,
Short,
None
};
#endregion
// Order Functions
private void SetCurrentPositionType(Order order)
{
if (order.Name == OpenLongSignal1)
_currentPositionType = PositionType.Long;
else if (order.Name == OpenShortSignal1)
_currentPositionType = PositionType.Short;
else
{
_currentPositionType = PositionType.None;
}
}
private bool IsEntryOrder(Order order)
{
return order.Name == OpenLongSignal1 ||
order.Name == OpenShortSignal1;
}
private bool IsExitOrder(Order order)
{
return order.Name == CloseLong ||
order.Name == CloseShort ||
order.Name == StopLoss ||
order.Name == ProfitTarget;
}
private bool OrderFilled(Order order)
{
return order.OrderState == OrderState.Filled;
}
private bool ActivePosition()
{
return Position.MarketPosition != MarketPosition.Flat;
}
private bool IsLongPosition()
{
return _currentPositionType == PositionType.Long;
}
private bool IsShortPosition()
{
return _currentPositionType == PositionType.Short;
}
The first 4 functions take in an Order
object and use it to help to set our position type and determine what kind of order is submitted. The rest of the functions help us to create our strategy framework inside OnBarUpdate()
and OnExecutionUpdate()
, which we will get to shortly.
Now, we will create the framework.
OnBarUpdate()
This is where all the strategy logic occurs. This event handler is called (yep) every time that the bar updates. You set the frequency at which the bar updates, and thus calculates, with the Calculate = Calculate.OnBarClose
setting in SetDefaultSettings().
Your options are OnEachTick
, OnBarClose
,
and OnPriceChange
. No matter what you choose (unless you enable tick replay or use a multi-timeframe script) historical data will always be processed on bar close. There are CPU and memory considerations when running large or complex optimizations using tick replay.3
Our update OnBarUpdate()
now looks like:
protected override void OnBarUpdate()
{
// This logic just makes sure we don't throw and error trying to get data from a bar that doesn't exist.
if (CurrentBar < BarsRequiredToTrade)
return;
SetSignalState();
if (ActivePosition())
{
if (IsLongPosition())
{
if (_exitLong)
_exitLongOrder = ExitLong(CloseLong, _longOrder.Name);
}
else if (IsShortPosition())
{
if (_exitShort)
_exitShortOrder = ExitShort(CloseShort, _shortOrder.Name);
}
}
else
{
if (_timeWindow)
{
if (_enterLongSignal1)
{
_longOrder = EnterLong(Convert.ToInt32(DefaultQuantity), OpenLongSignal1);
}
else if (_enterShortSignal1)
{
_shortOrder = EnterShort(Convert.ToInt32(DefaultQuantity), OpenShortSignal1);
}
}
}
}
Note: More information regarding entry and exit order functions see the link below in the footnotes. For now, we are just putting in simpler orders and assigning their names to string variables we created at the class level. 4
The next two sections are fairly simple and only hold a framework for capturing order information that might be needed for trade management. This will help with creating our own money management systems for our strategies. This section is one that may get periodically updated as I develop standardized practices.
protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)
{
if (OrderFilled(execution.Order))
{
if (IsEntryOrder(execution.Order))
{
SetCurrentPositionType(execution.Order);
if (IsLongPosition())
{
// do this
}
else if (IsShortPosition())
{
// do this
}
}
else if (IsExitOrder(execution.Order))
{
if (execution.Order.Name == StopLoss)
{
// This is just an example of the type of logic that can be done here.
//_stopLossTriggered = true;
}
}
else
{
SetCurrentPositionType(execution.Order);
}
}
}
protected override void OnPositionUpdate(Position position, double averagePrice, int quantity, MarketPosition marketPosition)
{
if (position.MarketPosition == MarketPosition.Flat)
{
// Another example of what can be done here.
// When a position is closed, add the last trade's Profit to the currentPnL.
//_currentPnL += SystemPerformance.AllTrades[SystemPerformance.AllTrades.Count - 1].ProfitCurrency;
}
}
With everything in place, our finished template should look like this:
using NinjaTrader.Cbi;
using NinjaTrader.Data;
using NinjaTrader.NinjaScript.Indicators;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NinjaTrader.NinjaScript.Strategies
{
public class StrategyTemplate : Strategy
{
#region Strategy Variables
private const string StrategyName = "Strategy Template";
private MACD _macd;
private ISeries<double> _macdDefault;
private ISeries<double> _macdAvg;
private ISeries<double> _macdDiff;
private const string OpenLongSignal1 = "Open Long 1";
private const string OpenShortSignal1 = "Open Short 1";
private const string CloseLong = "Close Long";
private const string CloseShort = "Close Short";
private const string StopLoss = "Stop loss";
private const string ProfitTarget = "Profit target";
private bool _enterLongSignal1;
private bool _enterShortSignal1;
private bool _exitLong;
private bool _exitShort;
private bool _timeWindow;
private PositionType _currentPositionType;
private Order _longOrder;
private Order _shortOrder;
private Order _exitLongOrder;
private Order _exitShortOrder;
enum PositionType
{
Long,
Short,
None
};
#endregion
#region Properties
private int _macdfast = 12;
private int _macdslow = 26;
private int _macdsmooth = 9;
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Fast", Order = 9, GroupName = "Signal Parameters")]
public int MACDfast
{
get { return _macdfast; }
set { _macdfast = value; }
}
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Slow", Order = 10, GroupName = "Signal Parameters")]
public int MACDslow
{
get { return _macdslow; }
set { _macdslow = value; }
}
[NinjaScriptProperty]
[Range(1, int.MaxValue)]
[Display(Name = "MACD Smooth", Order = 11, GroupName = "Signal Parameters")]
public int MACDsmooth
{
get { return _macdsmooth; }
set { _macdsmooth = value; }
}
#endregion
protected override void OnStateChange()
{
if (State == State.SetDefaults)
{
SetDefaultSettings();
}
else if (State == State.Configure)
{
// Additional data serieses will go here.
}
else if (State == State.DataLoaded)
{
ResetVariables();
AddIndicators();
ClearOutputWindow(); // Clears the output window each time this state is called.
}
}
protected override void OnBarUpdate()
{
// This logic just makes sure we don't throw and error trying to get data from a bar that doesn't exist.
if (CurrentBar < BarsRequiredToTrade)
return;
SetSignalState();
if (ActivePosition())
{
if (IsLongPosition())
{
if (_exitLong)
_exitLongOrder = ExitLong(CloseLong, _longOrder.Name);
}
else if (IsShortPosition())
{
if (_exitShort)
_exitShortOrder = ExitShort(CloseShort, _shortOrder.Name);
}
}
else
{
if (_timeWindow)
{
if (_enterLongSignal1)
{
_longOrder = EnterLong(Convert.ToInt32(DefaultQuantity), OpenLongSignal1);
}
else if (_enterShortSignal1)
{
_shortOrder = EnterShort(Convert.ToInt32(DefaultQuantity), OpenShortSignal1);
}
}
}
}
protected override void OnExecutionUpdate(Execution execution, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time)
{
// Logic for capturing information about trade executions.
if (OrderFilled(execution.Order))
{
if (IsEntryOrder(execution.Order))
{
SetCurrentPositionType(execution.Order);
if (IsLongPosition())
{
// do this
}
else if (IsShortPosition())
{
// do this
}
}
else if (IsExitOrder(execution.Order))
{
if (execution.Order.Name == StopLoss)
{
// This is just an example of the type of logic that can be done here.
//_stopLossTriggered = true;
}
}
else
{
SetCurrentPositionType(execution.Order);
}
}
}
protected override void OnPositionUpdate(Position position, double averagePrice, int quantity, MarketPosition marketPosition)
{
if (position.MarketPosition == MarketPosition.Flat)
{
// Another example of what can be done here.
// When a position is closed, add the last trade's Profit to the currentPnL.
//_currentPnL += SystemPerformance.AllTrades[SystemPerformance.AllTrades.Count - 1].ProfitCurrency;
}
}
#region Helper Functions
private void SetDefaultSettings()
{
Name = StrategyName;
IsOverlay = true;
Calculate = Calculate.OnBarClose;
IsExitOnSessionCloseStrategy = true;
ExitOnSessionCloseSeconds = 30;
OrderFillResolution = OrderFillResolution.Standard;
TraceOrders = false; // This can be turned on to help with debugging the strategy and looking at trades/orders.
BarsRequiredToTrade = 1; // I use this to manage the first bar on the chart.
// Disable this property for performance gains in Strategy Analyzer optimizations.
// You will have to call your own logic to clear mutable variables each iteration.
IsInstantiatedOnEachOptimizationIteration = false;
}
private void ResetVariables()
{
_longOrder = null;
_shortOrder = null;
_exitLongOrder = null;
_exitShortOrder = null;
_currentPositionType = PositionType.None;
_enterLongSignal1 = false;
_enterShortSignal1 = false;
_exitLong = false;
_exitShort = false;
_timeWindow = false;
}
private void AddIndicators()
{
_macd = MACD(_macdfast, _macdslow, _macdsmooth);
_macdDefault = _macd.Default;
_macdAvg = _macd.Avg;
_macdDiff = _macd.Diff;
AddChartIndicator(_macd);
}
// Strategy Functions
private void SetEntrySignalState()
{
_enterLongSignal1 = EntrySignalLong1(); // Place holders
_enterShortSignal1 = EntrySignalShort1(); // Place holders
_exitLong = ExitSignalLong(); // Place holders
_exitShort = ExitSignalShort(); // Place holders
_timeWindow = TimeWindow();
}
private bool EntrySignalLong1()
{
return CrossAbove(_macdDefault, _macdAvg, 1);
}
private bool EntrySignalShort1()
{
return CrossBelow(_macdDefault, _macdAvg, 1);
}
private bool ExitSignalLong()
{
return CrossBelow(_macdDefault, _macdAvg, 1);
}
private bool ExitSignalShort()
{
return CrossAbove(_macdDefault, _macdAvg, 1);
}
private bool TimeWindow()
{
return ToTime(Time[0]) >= 100000 && ToTime(Time[0]) < 160000 || ToTime(Time[0]) >= 180000 && ToTime(Time[0]) < 200000;
}
// Order Functions
private void SetCurrentPositionType(Order order)
{
if (order.Name == OpenLongSignal1)
_currentPositionType = PositionType.Long;
else if (order.Name == OpenShortSignal1)
_currentPositionType = PositionType.Short;
else
{
_currentPositionType = PositionType.None;
}
}
private bool IsEntryOrder(Order order)
{
return order.Name == OpenLongSignal1 ||
order.Name == OpenShortSignal1;
}
private bool IsExitOrder(Order order)
{
return order.Name == CloseLong ||
order.Name == CloseShort ||
order.Name == StopLoss ||
order.Name == ProfitTarget;
}
private bool OrderFilled(Order order)
{
return order.OrderState == OrderState.Filled;
}
private bool ActivePosition()
{
return Position.MarketPosition != MarketPosition.Flat;
}
private bool IsLongPosition()
{
return _currentPositionType == PositionType.Long;
}
private bool IsShortPosition()
{
return _currentPositionType == PositionType.Short;
}
#endregion
}
}
Conclusion
The above template has worked well for me as a starting point. Many of the concepts come from some of the NinjaScript courses at NinjaCoding. I have no affiliation, but I do believe in transparency. Some of the material in those courses is dated now. However, they still serve as a good starting point for digging into NinjaScript and debugging. As I develop a good practice for trade management, I will update the template with my standard technique. Of course, I will also update any other portion of the template as I learn about better practices with NinjaScript and C#/.NET.
I have tried to keep the naming convention simple and understandable. This naming convention may get updated as the template evolves. Still, I will always attempt to name functions and variables as plainly as possible to avoid confusion—no brevity codes, plain English only.
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.