NRatio Sign Strategy
This strategy employs the NRatio indicator, an NRTR-based oscillator that measures the normalized distance between price and a dynamic trailing level. Trading signals occur when the NRatio crosses predefined thresholds. Depending on the selected mode, the system either reacts to breakouts beyond the upper and lower bounds or to reversals back inside them.
The approach can operate on both sides of the market and uses percentage-based risk management for exits. Smoothing of the distance metric is performed with an exponential moving average, allowing the strategy to respond quickly while filtering noise.
Details
- Entry Criteria:
- Mode In:
- Long:
NRatiocrosses aboveUpLevel. - Short:
NRatiocrosses belowDownLevel.
- Long:
- Mode Out:
- Long:
NRatiocrosses aboveDownLevel. - Short:
NRatiocrosses belowUpLevel.
- Long:
- Mode In:
- Long/Short: Both directions.
- Exit Criteria: Opposite signal or protective stop.
- Stops: Yes, take-profit and stop-loss in percent.
- Default Values:
CandleType= 4-hour candlesKf= 1Length= 3Fast= 2Sharp= 2UpLevel= 80DownLevel= 20TakeProfitPercent= 2StopLossPercent= 2
- Filters:
- Category: Trend following
- Direction: Both
- Indicators: NRTR, EMA
- Stops: Yes
- Complexity: Intermediate
- Timeframe: Medium-term
- Seasonality: No
- Neural Networks: No
- Divergence: No
- Risk Level: Medium
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// NRatio Sign strategy based on NRTR oscillator.
/// Generates buy or sell signals when the normalized ratio crosses thresholds.
/// </summary>
public class NRatioSignStrategy : Strategy
{
// strategy parameters
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _kf;
private readonly StrategyParam<int> _length;
private readonly StrategyParam<decimal> _fast;
private readonly StrategyParam<decimal> _sharp;
private readonly StrategyParam<decimal> _upLevel;
private readonly StrategyParam<decimal> _downLevel;
private readonly StrategyParam<StrategyModes> _mode;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _stopLoss;
// internal state variables
private decimal _nrtr;
private decimal _nratioPrev;
private int _trend;
private bool _isInitialized;
private ExponentialMovingAverage _ema = new();
/// <summary>
/// NRatio calculation mode.
/// </summary>
public enum StrategyModes
{
ModeIn,
ModeOut,
}
/// <summary>
/// Candle series type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// NRTR coefficient.
/// </summary>
public decimal Kf
{
get => _kf.Value;
set => _kf.Value = value;
}
/// <summary>
/// Smoothing length for EMA.
/// </summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>
/// Fast parameter affecting NRTR dynamics.
/// </summary>
public decimal Fast
{
get => _fast.Value;
set => _fast.Value = value;
}
/// <summary>
/// Exponent applied to the oscillator.
/// </summary>
public decimal Sharp
{
get => _sharp.Value;
set => _sharp.Value = value;
}
/// <summary>
/// Upper threshold of NRatio.
/// </summary>
public decimal UpLevel
{
get => _upLevel.Value;
set => _upLevel.Value = value;
}
/// <summary>
/// Lower threshold of NRatio.
/// </summary>
public decimal DownLevel
{
get => _downLevel.Value;
set => _downLevel.Value = value;
}
/// <summary>
/// Signal generation mode.
/// </summary>
public StrategyModes Mode
{
get => _mode.Value;
set => _mode.Value = value;
}
/// <summary>
/// Take profit percentage.
/// </summary>
public decimal TakeProfitPercent
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Stop loss percentage.
/// </summary>
public decimal StopLossPercent
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="NRatioSignStrategy"/>.
/// </summary>
public NRatioSignStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame for indicator calculation", "General");
_kf = Param(nameof(Kf), 1m)
.SetDisplay("Kf", "NRTR coefficient", "Indicator");
_length = Param(nameof(Length), 3)
.SetGreaterThanZero()
.SetDisplay("Length", "EMA smoothing length", "Indicator");
_fast = Param(nameof(Fast), 2m)
.SetGreaterThanZero()
.SetDisplay("Fast", "Fast parameter", "Indicator");
_sharp = Param(nameof(Sharp), 2m)
.SetGreaterThanZero()
.SetDisplay("Sharp", "Exponent for oscillator", "Indicator");
_upLevel = Param(nameof(UpLevel), 80m)
.SetDisplay("Up Level", "Upper NRatio threshold", "Indicator");
_downLevel = Param(nameof(DownLevel), 20m)
.SetDisplay("Down Level", "Lower NRatio threshold", "Indicator");
_mode = Param(nameof(Mode), StrategyModes.ModeIn)
.SetDisplay("Mode", "Signal generation mode", "Indicator");
_takeProfit = Param(nameof(TakeProfitPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Take Profit %", "Take profit percentage", "Risk Management")
.SetOptimize(1m, 5m, 1m);
_stopLoss = Param(nameof(StopLossPercent), 2m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
.SetOptimize(1m, 5m, 1m);
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_isInitialized = false;
_nrtr = 0m;
_nratioPrev = 50m;
_trend = 1;
_ema.Length = Length;
_ema.Reset();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema.Length = Length;
_ema.Reset();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(TakeProfitPercent, UnitTypes.Percent),
stopLoss: new Unit(StopLossPercent, UnitTypes.Percent)
);
}
private void ProcessCandle(ICandleMessage candle)
{
// Process only completed candles
if (candle.State != CandleStates.Finished)
return;
if (!IsOnline)
return;
var price = candle.ClosePrice;
if (!_isInitialized)
{
_trend = candle.ClosePrice >= candle.OpenPrice ? 1 : -1;
_nrtr = _trend > 0 ? price * (1m - Kf / 100m) : price * (1m + Kf / 100m);
_nratioPrev = 50m;
_isInitialized = true;
return;
}
var nrtr0 = _nrtr;
var trend0 = _trend;
if (_trend >= 0)
{
if (price < _nrtr)
{
trend0 = -1;
nrtr0 = price * (1m + Kf / 100m);
}
else
{
trend0 = 1;
var lPrice = price * (1m - Kf / 100m);
nrtr0 = Math.Max(lPrice, _nrtr);
}
}
else
{
if (price > _nrtr)
{
trend0 = 1;
nrtr0 = price * (1m - Kf / 100m);
}
else
{
trend0 = -1;
var hPrice = price * (1m + Kf / 100m);
nrtr0 = Math.Min(hPrice, _nrtr);
}
}
var oscil = (100m * Math.Abs(price - nrtr0) / price) / Kf;
var xOscilValue = _ema.Process(new DecimalIndicatorValue(_ema, oscil, candle.OpenTime) { IsFinal = true });
var xOscil = xOscilValue.IsEmpty ? oscil : xOscilValue.ToDecimal();
if (!_ema.IsFormed)
{
_nrtr = nrtr0;
_trend = trend0;
return;
}
var nratio = 100m * (decimal)Math.Pow((double)xOscil, (double)Sharp);
var buySignal = false;
var sellSignal = false;
if (Mode == StrategyModes.ModeIn)
{
if (nratio > UpLevel && _nratioPrev <= UpLevel)
buySignal = true;
if (nratio < DownLevel && _nratioPrev >= DownLevel)
sellSignal = true;
}
else
{
if (nratio < UpLevel && _nratioPrev >= UpLevel)
sellSignal = true;
if (nratio > DownLevel && _nratioPrev <= DownLevel)
buySignal = true;
}
_nrtr = nrtr0;
_trend = trend0;
_nratioPrev = nratio;
if (buySignal && Position <= 0)
BuyMarket(Volume + Math.Abs(Position));
else if (sellSignal && Position >= 0)
SellMarket(Volume + Math.Abs(Position));
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, Unit, UnitTypes, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
# Mode constants
MODE_IN = 0
MODE_OUT = 1
class n_ratio_sign_strategy(Strategy):
def __init__(self):
super(n_ratio_sign_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for indicator calculation", "General")
self._kf = self.Param("Kf", 1.0) \
.SetDisplay("Kf", "NRTR coefficient", "Indicator")
self._length = self.Param("Length", 3) \
.SetDisplay("Length", "EMA smoothing length", "Indicator")
self._fast = self.Param("Fast", 2.0) \
.SetDisplay("Fast", "Fast parameter", "Indicator")
self._sharp = self.Param("Sharp", 2.0) \
.SetDisplay("Sharp", "Exponent for oscillator", "Indicator")
self._up_level = self.Param("UpLevel", 80.0) \
.SetDisplay("Up Level", "Upper NRatio threshold", "Indicator")
self._down_level = self.Param("DownLevel", 20.0) \
.SetDisplay("Down Level", "Lower NRatio threshold", "Indicator")
self._mode = self.Param("Mode", MODE_IN) \
.SetDisplay("Mode", "Signal generation mode", "Indicator")
self._take_profit = self.Param("TakeProfitPercent", 2.0) \
.SetDisplay("Take Profit %", "Take profit percentage", "Risk Management")
self._stop_loss = self.Param("StopLossPercent", 2.0) \
.SetDisplay("Stop Loss %", "Stop loss percentage", "Risk Management")
self._nrtr = 0.0
self._nratio_prev = 50.0
self._trend = 1
self._is_initialized = False
self._ema = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Kf(self):
return self._kf.Value
@Kf.setter
def Kf(self, value):
self._kf.Value = value
@property
def Length(self):
return self._length.Value
@Length.setter
def Length(self, value):
self._length.Value = value
@property
def Fast(self):
return self._fast.Value
@Fast.setter
def Fast(self, value):
self._fast.Value = value
@property
def Sharp(self):
return self._sharp.Value
@Sharp.setter
def Sharp(self, value):
self._sharp.Value = value
@property
def UpLevel(self):
return self._up_level.Value
@UpLevel.setter
def UpLevel(self, value):
self._up_level.Value = value
@property
def DownLevel(self):
return self._down_level.Value
@DownLevel.setter
def DownLevel(self, value):
self._down_level.Value = value
@property
def Mode(self):
return self._mode.Value
@Mode.setter
def Mode(self, value):
self._mode.Value = value
@property
def TakeProfitPercent(self):
return self._take_profit.Value
@TakeProfitPercent.setter
def TakeProfitPercent(self, value):
self._take_profit.Value = value
@property
def StopLossPercent(self):
return self._stop_loss.Value
@StopLossPercent.setter
def StopLossPercent(self, value):
self._stop_loss.Value = value
def OnStarted2(self, time):
super(n_ratio_sign_strategy, self).OnStarted2(time)
self._ema = ExponentialMovingAverage()
self._ema.Length = self.Length
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
self.StartProtection(
takeProfit=Unit(self.TakeProfitPercent, UnitTypes.Percent),
stopLoss=Unit(self.StopLossPercent, UnitTypes.Percent))
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
if not self.IsOnline:
return
price = float(candle.ClosePrice)
kf = float(self.Kf)
if not self._is_initialized:
if float(candle.ClosePrice) >= float(candle.OpenPrice):
self._trend = 1
else:
self._trend = -1
if self._trend > 0:
self._nrtr = price * (1.0 - kf / 100.0)
else:
self._nrtr = price * (1.0 + kf / 100.0)
self._nratio_prev = 50.0
self._is_initialized = True
return
nrtr0 = self._nrtr
trend0 = self._trend
if self._trend >= 0:
if price < self._nrtr:
trend0 = -1
nrtr0 = price * (1.0 + kf / 100.0)
else:
trend0 = 1
l_price = price * (1.0 - kf / 100.0)
nrtr0 = max(l_price, self._nrtr)
else:
if price > self._nrtr:
trend0 = 1
nrtr0 = price * (1.0 - kf / 100.0)
else:
trend0 = -1
h_price = price * (1.0 + kf / 100.0)
nrtr0 = min(h_price, self._nrtr)
oscil = (100.0 * abs(price - nrtr0) / price) / kf
x_oscil_value = process_float(self._ema, oscil, candle.OpenTime, True)
x_oscil = oscil if x_oscil_value.IsEmpty else float(x_oscil_value)
if not self._ema.IsFormed:
self._nrtr = nrtr0
self._trend = trend0
return
nratio = 100.0 * (x_oscil ** float(self.Sharp))
buy_signal = False
sell_signal = False
if self.Mode == MODE_IN:
if nratio > float(self.UpLevel) and self._nratio_prev <= float(self.UpLevel):
buy_signal = True
if nratio < float(self.DownLevel) and self._nratio_prev >= float(self.DownLevel):
sell_signal = True
else:
if nratio < float(self.UpLevel) and self._nratio_prev >= float(self.UpLevel):
sell_signal = True
if nratio > float(self.DownLevel) and self._nratio_prev <= float(self.DownLevel):
buy_signal = True
self._nrtr = nrtr0
self._trend = trend0
self._nratio_prev = nratio
if buy_signal and self.Position <= 0:
self.BuyMarket(self.Volume + abs(self.Position))
elif sell_signal and self.Position >= 0:
self.SellMarket(self.Volume + abs(self.Position))
def OnReseted(self):
super(n_ratio_sign_strategy, self).OnReseted()
self._is_initialized = False
self._nrtr = 0.0
self._nratio_prev = 50.0
self._trend = 1
if self._ema is not None:
self._ema.Length = self.Length
self._ema.Reset()
def CreateClone(self):
return n_ratio_sign_strategy()