Create a Custom Order Management Class in NinjaScript
Learn how to create a custom class in C# and NinjaScript that will make managing trade exits more streamlined. In this article we will discuss how to do this and why you want to.
Update:
This article 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.
Today’s post covers creating your own stop and profit orders in NinjaScript using a custom class. This makes it easier to implement multiple exit types into a strategy for research purposes. It also makes the code easier to reuse for future strategies. This will be one of a series of articles covering changes to the underlying code framework at HGT.
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. Backtest results are based on historical data, not real-time data. There is no guarantee that these hypothetical results will continue in the future. Day trading is extremely risky, and I do not suggest running any of these strategies live.
Code
There are three different order classes to create: a static stop loss/take profit (SL/TP), a trail by indicator (ATR), and a reset trail. Since our strategies are designed to keep track of our order objects, we need to be able to return multiple values back to the strategy. We will use tuples to accomplish this.
A tuple is a data structure that can hold a fixed number of items. Each item can be a different type. They are useful when you want to return multiple values without creating a separate class. This is particularly handy when developing trade systems as we often need to return multiple pieces of information from a method, such as an entry and exit order or an entry order and an SL value.
Trade Manager Constructor
This first section is the class constructor. When we instantiate the trade manager object in the strategy, we must pass in a reference to the strategy object. This will allow us to interact with the strategy methods and access any information present in the strategy. Because we are instantiating it with a reference to the strategy, we don’t need to worry about updating the object in the strategy on each bar update.
This is where we will start.
using System;
using NinjaTrader.NinjaScript;
using NinjaTrader.NinjaScript.Strategies;
using NinjaTrader.Cbi;
namespace TradeFunctions
{
public class TradeManager
{
private const string StopLoss = "Stop Loss";
private const string ProfitTarget = "Profit Target";
private const string TrailShort = "Trail Short";
private const string TrailLong = "Trail Long";
private Strategy strategy;
public TradeManager(Strategy strategy)
{
this.strategy = strategy;
}
// The rest of the logic will go here.
}
}
Set Static Stop Loss and Take Profit Orders
The first method we create is the SetStaticStopProfit()
method. Aside from the execution object, we will pass in the stop and profit ticks set by the user. We will get the entry price and calculate our SL/TP from there. Then, we will create the orders and assign the order objects to a variable that we will return to the strategy. This function is intended to be called during OnExecutionUpdate()
during the strategy life-cycle.
// Set Static Stop Loss and Take Profit
public (Order _profitOrder, Order _stopOrder) SetStaticStopProfit(Execution execution, double _stopTicks, double _profitTicks)
{
Order _profitOrder = null;
Order _stopOrder = null;
double entryPrice = execution.Order.AverageFillPrice;
if (strategy.Position.MarketPosition == MarketPosition.Long)
{
double _sl = entryPrice - _stopTicks * strategy.TickSize;
double _tp = entryPrice + _profitTicks * strategy.TickSize;
_profitOrder = strategy.ExitLongLimit(0, true, strategy.DefaultQuantity, _tp, ProfitTarget, execution.Name);
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, StopLoss, execution.Name);
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
double _sl = entryPrice + _stopTicks * strategy.TickSize;
double _tp = entryPrice - _profitTicks * strategy.TickSize;
_profitOrder = strategy.ExitShortLimit(0, true, strategy.DefaultQuantity, _tp, ProfitTarget, execution.Name);
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, StopLoss, execution.Name);
}
return (_profitOrder, _stopOrder);
}
Trail by Indicator Stop Loss
This following exit method is the SetTrailIndicator
. An indicator (or any series double) is needed to calculate the initial SL. It does so by adding or subtracting the indicator’s value from the entry price, so the value that is passed in needs to represent a price on the asset, or you will get strange results.
// Set a trailing stop by indicator or any ISeries<double>. ATR is a good example.
public (double _sl, Order _stopOrder) SetTrailIndicator(Execution execution, ISeries<double> indicatorSeries)
{
double _sl = double.NaN;
Order _stopOrder = null;
double entryPrice = execution.Order.AverageFillPrice;
if (strategy.Position.MarketPosition == MarketPosition.Long)
{
_sl = entryPrice - indicatorSeries[0];
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailLong, execution.Name);
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
_sl = entryPrice + indicatorSeries[0];
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailShort, execution.Name);
}
return (_sl, _stopOrder);
}
This next method is the second part of the trail indicator method, ResetTrailIndicator
. This one is intended to be called in OnBarUpdate()
. Each time it is called, it recalculates the stop based on the indicator used and then determines whether or not it needs to update the SL order. If so, it will cancel the existing order and create a new order. It returns the new SL value and the new SL order.
// Reset the aforementioned trail by indicator. Needs to be executed in OnBarUpdate()
public (double _sl, Order _stopOrder) ResetTrailIndicator(ISeries<double> indicatorSeries, string entryOrderName, Order stopOrder, double sl)
{
double _sl = sl;
Order _stopOrder = stopOrder;
if (strategy.Position.MarketPosition== MarketPosition.Long)
{
double newStop = strategy.Close[0] - indicatorSeries[0];
if (newStop > _sl)
{
_sl = newStop;
strategy.CancelOrder(_stopOrder);
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailLong, entryOrderName);
}
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
double newStop = strategy.Close[0] + indicatorSeries[0];
if (newStop < _sl)
{
_sl = newStop;
strategy.CancelOrder(_stopOrder);
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailShort, entryOrderName);
}
}
return (_sl, _stopOrder);
}
The Completed Project
namespace TradeFunctions
{
public class TradeManager
{
private const string StopLoss = "Stop Loss";
private const string ProfitTarget = "Profit Target";
private const string TrailShort = "Trail Short";
private const string TrailLong = "Trail Long";
private Strategy strategy;
public TradeManager(Strategy strategy)
{
this.strategy = strategy;
}
// Set Static Stop Loss and Take Profit
public (Order _profitOrder, Order _stopOrder) SetStaticStopProfit(Execution execution, double _stopTicks, double _profitTicks)
{
Order _profitOrder = null;
Order _stopOrder = null;
double entryPrice = execution.Order.AverageFillPrice;
if (strategy.Position.MarketPosition == MarketPosition.Long)
{
double _sl = entryPrice - _stopTicks * strategy.TickSize;
double _tp = entryPrice + _profitTicks * strategy.TickSize;
_profitOrder = strategy.ExitLongLimit(0, true, strategy.DefaultQuantity, _tp, ProfitTarget, execution.Name);
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, StopLoss, execution.Name);
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
double _sl = entryPrice + _stopTicks * strategy.TickSize;
double _tp = entryPrice - _profitTicks * strategy.TickSize;
_profitOrder = strategy.ExitShortLimit(0, true, strategy.DefaultQuantity, _tp, ProfitTarget, execution.Name);
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, StopLoss, execution.Name);
}
return (_profitOrder, _stopOrder);
}
// Set a trailing stop by indicator or any ISeries<double>. ATR is a good example.
public (double _sl, Order _stopOrder) SetTrailIndicator(Execution execution, ISeries<double> indicatorSeries)
{
double _sl = double.NaN;
Order _stopOrder = null;
double entryPrice = execution.Order.AverageFillPrice;
if (strategy.Position.MarketPosition == MarketPosition.Long)
{
_sl = entryPrice - indicatorSeries[0];
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailLong, execution.Name);
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
_sl = entryPrice + indicatorSeries[0];
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailShort, execution.Name);
}
return (_sl, _stopOrder);
}
// Reset the aforementioned trail by indicator. Needs to be executed in OnBarUpdate()
public (double _sl, Order _stopOrder) ResetTrailIndicator(ISeries<double> indicatorSeries, string entryOrderName, Order stopOrder, double sl)
{
double _sl = sl;
Order _stopOrder = stopOrder;
if (strategy.Position.MarketPosition== MarketPosition.Long)
{
double newStop = strategy.Close[0] - indicatorSeries[0];
if (newStop > _sl)
{
_sl = newStop;
strategy.CancelOrder(_stopOrder);
_stopOrder = strategy.ExitLongStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailLong, entryOrderName);
}
}
else if (strategy.Position.MarketPosition == MarketPosition.Short)
{
double newStop = strategy.Close[0] + indicatorSeries[0];
if (newStop < _sl)
{
_sl = newStop;
strategy.CancelOrder(_stopOrder);
_stopOrder = strategy.ExitShortStopMarket(0, true, strategy.DefaultQuantity, _sl, TrailShort, entryOrderName);
}
}
return (_sl, _stopOrder);
}
}
}
Using them in a Strategy
The following code is an example of how to use the custom order functions we created.
// Shortened to "args" for readability
protected override void OnExecutionUpdate(args)
{
if (OrderStatus.IsEffectivelyFilled(execution, _entryOrder))
{
if (OrderStatus.IsEntryOrderFilled(execution, _entryOrder))
{
if (_exitType == ExitType.Static)
{
(Order StopOrder, Order ProfitOrder) = _tradeManager.SetStaticStopProfit(execution, _stopTicks, _profitTicks);
_stopOrder = StopOrder;
_profitOrder = ProfitOrder;
}
else if (_exitType == ExitType.TrailATR)
{
(double newSL, Order newStopOrder) = _tradeManager.SetTrailIndicator(execution, _atr);
_sl = newSL;
_stopOrder = newStopOrder;
}
else if (_exitType == ExitType.None)
{
return;
}
}
}
}
The ResetTrailIndicator()
function needs to be placed in the BarUpdate()
method. This ensures that we redefine the stop order each time we start a new bar. Below, you will find an example of how I implement it.
// Inside of BarUpdate()
if (Position.MarketPosition != MarketPosition.Flat)
{
if (_exitType == ExitType.TrailATR)
{
(double newSL, Order newStopOrder) = _tradeManager.ResetTrailIndicator(_atr, _entryOrder.Name, _stopOrder, _sl);
_sl = newSL;
_stopOrder = newStopOrder;
}
}
Summary
In this post, we demonstrated how to implement a custom class in NinjaScript to manage different SL/TP orders. This enhances flexibility and reusability. By leveraging tuples, we’ve efficiently returned multiple values to the strategy from a single method. When integrated correctly, the TradeManger
class we created can help traders achieve a higher degree of precision and control in their automated trade systems. It not only aids in research and development but also contributes to making the system more adaptable.
Looking forward, I would like to add two more static order options: TP only and SL only. It would give you more standard options when developing a strategy. I would also like to experiment with creating variations of these functions that submit a rapid market order instead of submitting an actual SL/TP order with the entry order. This could open up new avenues for researching strategy optimization.
Creating custom classes like this will cause some issues if you use it in a strategy and attempt to compile the code for export via the NT8 provided tool. For it to comply with the NinjaScript recommended and best practice, the strategy file would need to be in the actual strategy folder, and the custom classes would need to be under the NinjaScript add-on namespace and probably in the AddOns folder.
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.