Estrategia NRatio Sign
Esta estrategia emplea el indicador NRatio, un oscilador basado en NRTR que mide la distancia normalizada entre el precio y un nivel dinámico de seguimiento. Las señales de trading ocurren cuando el NRatio cruza umbrales predefinidos. Dependiendo del modo seleccionado, el sistema reacciona ya sea a rupturas más allá de los límites superior e inferior o a reversiones de regreso dentro de ellos.
El enfoque puede operar en ambos lados del mercado y usa gestión de riesgo basada en porcentajes para las salidas. El suavizado de la métrica de distancia se realiza con una media móvil exponencial, permitiendo que la estrategia responda rápidamente mientras filtra el ruido.
Detalles
- Criterios de entrada:
- Modo In:
- Largo:
NRatiocruza por encima deUpLevel. - Corto:
NRatiocruza por debajo deDownLevel.
- Largo:
- Modo Out:
- Largo:
NRatiocruza por encima deDownLevel. - Corto:
NRatiocruza por debajo deUpLevel.
- Largo:
- Modo In:
- Largo/Corto: Ambas direcciones.
- Criterios de salida: Señal opuesta o stop de protección.
- Stops: Sí, take-profit y stop-loss en porcentaje.
- Valores predeterminados:
CandleType= velas de 4 horasKf= 1Length= 3Fast= 2Sharp= 2UpLevel= 80DownLevel= 20TakeProfitPercent= 2StopLossPercent= 2
- Filtros:
- Categoría: Seguimiento de tendencia
- Dirección: Ambos
- Indicadores: NRTR, EMA
- Stops: Sí
- Complejidad: Intermedio
- Marco temporal: Medio plazo
- Estacionalidad: No
- Redes neuronales: No
- Divergencia: No
- Nivel de riesgo: Medio
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()