Стратегия NRatio Sign
Стратегия использует индикатор NRatio — осциллятор на основе NRTR, который измеряет нормированное расстояние между ценой и динамическим трейлинг-уровнем. Сигналы появляются при пересечении заданных порогов. В зависимости от выбранного режима система реагирует либо на выходы за верхнюю и нижнюю границы, либо на возвраты внутрь этих уровней.
Подход работает в обоих направлениях рынка и применяет управление рисками в процентах для выхода из позиции. Нормированное расстояние сглаживается экспоненциальным скользящим средним, что позволяет быстро реагировать и фильтровать шум.
Подробности
- Условия входа:
- Режим In:
- Long:
NRatioпересекаетUpLevelсверху вниз. - Short:
NRatioпересекаетDownLevelснизу вверх.
- Long:
- Режим Out:
- Long:
NRatioпересекаетDownLevelснизу вверх. - Short:
NRatioпересекаетUpLevelсверху вниз.
- Long:
- Режим In:
- Long/Short: обе стороны.
- Выход: противоположный сигнал или защитный стоп.
- Стопы: да, тейк-профит и стоп-лосс в процентах.
- Значения по умолчанию:
CandleType= 4-часовые свечиKf= 1Length= 3Fast= 2Sharp= 2UpLevel= 80DownLevel= 20TakeProfitPercent= 2StopLossPercent= 2
- Фильтры:
- Категория: Следование тренду
- Направление: Обе
- Индикаторы: NRTR, EMA
- Стопы: Да
- Сложность: Средняя
- Таймфрейм: Среднесрочный
- Сезонность: Нет
- Нейросети: Нет
- Дивергенция: Нет
- Уровень риска: Средний
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()