NRTR ATR Stop 策略
概述
NRTR ATR Stop 策略在 StockSharp 的高级 API 上复刻了 MetaTrader 专家顾问 Exp_NRTR_ATR_STOP 的运行逻辑。它追踪基于平均真实波幅(ATR)的 NRTR(Non-Repainting Trailing Reverse)动态止损线。当价格突破对侧止损线时,趋势立即翻转,策略会同时平掉旧方向仓位并在新方向开仓。
指标逻辑
- 订阅的 K 线数据用于计算单一 ATR(长度由
AtrPeriod决定),ATR 与Coefficient的乘积决定了价格与止损线之间的距离。 - 策略维护两条动态止损:
upper stop在多头趋势中位于价格下方,为多头仓位提供跟踪保护。lower stop在空头趋势中位于价格上方,为空头仓位提供跟踪保护。
- 当收盘价突破对侧止损线时,趋势立即翻转。新的止损线以上一根 K 线的极值减/加 ATR 距离初始化。
- 原始 EA 通过读取指标缓存中
SignalBar根历史柱的数据来延迟执行。策略内部使用一个队列重现这一行为:每根完成的 K 线都会将信号写入队列,只有当队列长度大于SignalBar时才真正触发交易。
交易规则
- 买入信号 —— 计算得到的趋势由空头或中性转为多头。策略可选地一次性买入(
BuyMarket)平掉所有空头,并在同一笔市场单中加入Volume指定的新多头仓位。 - 卖出信号 —— 趋势由多头或中性转为空头。策略可选地一次性卖出(
SellMarket)平掉所有多头,并附带Volume指定的新空头仓位。 EnableLongEntry、EnableShortEntry、EnableLongExit与EnableShortExit属性用于精确控制在信号出现时应执行的操作。- 仅在 K 线收盘且策略处于在线并允许交易的状态下才处理信号。
参数
| 名称 | 说明 |
|---|---|
AtrPeriod |
计算 ATR 所使用的 K 线数量。 |
Coefficient |
构建跟踪止损时乘在 ATR 上的系数。 |
SignalBar |
在执行保存的信号之前需要等待的完整 K 线数量,设为 0 表示立即执行。 |
CandleType |
输入 K 线的时间框架。 |
EnableLongEntry |
允许在买入信号时开多。 |
EnableShortEntry |
允许在卖出信号时开空。 |
EnableLongExit |
允许在卖出信号时平掉现有多头。 |
EnableShortExit |
允许在买入信号时平掉现有空头。 |
注意事项
- 策略仅基于收盘完成的 K 线进行计算,不处理盘中逐笔数据。
- 所有交易通过
BuyMarket/SellMarket市价指令完成,方便在一笔订单中同时平仓并反向开仓。 - 在实盘或回测前请确保策略的
Volume属性被设置为正数。
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>
/// Strategy that emulates the NRTR ATR Stop indicator behavior to generate trading signals.
/// The logic follows the original MetaTrader implementation: ATR-based trailing levels switch direction
/// when price crosses the opposite stop, producing long or short entries.
/// </summary>
public class NRTRATRStopStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _coefficient;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<bool> _enableLongEntry;
private readonly StrategyParam<bool> _enableShortEntry;
private readonly StrategyParam<bool> _enableLongExit;
private readonly StrategyParam<bool> _enableShortExit;
private AverageTrueRange _atr = null!;
private readonly List<SignalInfo> _signals = new();
private decimal _upperStop;
private decimal _lowerStop;
private int _trend;
private bool _hasStops;
private bool _hasPrevious;
private decimal _prevHigh;
private decimal _prevLow;
/// <summary>
/// ATR period used by the trailing stop calculation.
/// </summary>
public int AtrPeriod
{
get => _atrPeriod.Value;
set
{
var normalized = Math.Max(1, value);
_atrPeriod.Value = normalized;
if (_atr != null)
_atr.Length = normalized;
}
}
/// <summary>
/// Multiplier applied to ATR to build stop levels.
/// </summary>
public decimal Coefficient
{
get => _coefficient.Value;
set
{
var normalized = Math.Max(0.1m, value);
_coefficient.Value = normalized;
}
}
/// <summary>
/// Number of closed candles to delay signal execution.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set
{
var normalized = Math.Max(0, value);
_signalBar.Value = normalized;
}
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Allow opening long positions.
/// </summary>
public bool EnableLongEntry
{
get => _enableLongEntry.Value;
set => _enableLongEntry.Value = value;
}
/// <summary>
/// Allow opening short positions.
/// </summary>
public bool EnableShortEntry
{
get => _enableShortEntry.Value;
set => _enableShortEntry.Value = value;
}
/// <summary>
/// Allow closing existing long positions on sell signals.
/// </summary>
public bool EnableLongExit
{
get => _enableLongExit.Value;
set => _enableLongExit.Value = value;
}
/// <summary>
/// Allow closing existing short positions on buy signals.
/// </summary>
public bool EnableShortExit
{
get => _enableShortExit.Value;
set => _enableShortExit.Value = value;
}
/// <summary>
/// Initializes parameters for the NRTR ATR Stop strategy.
/// </summary>
public NRTRATRStopStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 20)
.SetDisplay("ATR Period", "Number of candles used for ATR calculation", "Indicator")
.SetOptimize(10, 40, 5);
_coefficient = Param(nameof(Coefficient), 2m)
.SetDisplay("Coefficient", "Multiplier applied to ATR when building the stop levels", "Indicator")
.SetOptimize(1m, 4m, 0.5m);
_signalBar = Param(nameof(SignalBar), 1)
.SetDisplay("Signal Bar", "How many closed candles to wait before acting on a signal", "Trading")
.SetOptimize(0, 3, 1);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Time frame of the candles used for calculations", "General");
_enableLongEntry = Param(nameof(EnableLongEntry), true)
.SetDisplay("Enable Long Entry", "Allow the strategy to open long positions", "Trading");
_enableShortEntry = Param(nameof(EnableShortEntry), true)
.SetDisplay("Enable Short Entry", "Allow the strategy to open short positions", "Trading");
_enableLongExit = Param(nameof(EnableLongExit), true)
.SetDisplay("Enable Long Exit", "Allow closing long positions when a sell signal appears", "Risk");
_enableShortExit = Param(nameof(EnableShortExit), true)
.SetDisplay("Enable Short Exit", "Allow closing short positions when a buy signal appears", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_signals.Clear();
_upperStop = 0m;
_lowerStop = 0m;
_trend = 0;
_hasStops = false;
_hasPrevious = false;
_prevHigh = 0m;
_prevLow = 0m;
_atr = null!;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_atr = new AverageTrueRange { Length = AtrPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_atr, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atrValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_atr.IsFormed)
{
UpdatePreviousValues(candle);
return;
}
if (!_hasPrevious)
{
UpdatePreviousValues(candle);
return;
}
var previousTrend = _trend;
var trend = previousTrend;
var upperStop = _upperStop;
var lowerStop = _lowerStop;
var rez = Coefficient * atrValue;
if (!_hasStops)
{
upperStop = _prevLow - rez;
lowerStop = _prevHigh + rez;
_hasStops = true;
}
if (trend <= 0 && _prevLow > lowerStop)
{
upperStop = _prevLow - rez;
trend = 1;
}
if (trend >= 0 && _prevHigh < upperStop)
{
lowerStop = _prevHigh + rez;
trend = -1;
}
if (trend >= 0)
{
if (_prevLow > upperStop + rez)
upperStop = _prevLow - rez;
}
if (trend <= 0)
{
if (_prevHigh < lowerStop - rez)
lowerStop = _prevHigh + rez;
}
var buySignal = trend > 0 && previousTrend <= 0;
var sellSignal = trend < 0 && previousTrend >= 0;
_trend = trend;
_upperStop = upperStop;
_lowerStop = lowerStop;
_signals.Add(new SignalInfo(buySignal, sellSignal, upperStop, lowerStop, candle.CloseTime, candle.ClosePrice));
if (_signals.Count <= SignalBar)
{
UpdatePreviousValues(candle);
return;
}
var signal = _signals[0];
try { _signals.RemoveAt(0); } catch { }
// indicators bound via .Bind() - IsFormed already checked
if (signal.Buy)
HandleBuy(signal);
if (signal.Sell)
HandleSell(signal);
UpdatePreviousValues(candle);
}
private void HandleBuy(SignalInfo signal)
{
var volume = CalculateBuyVolume();
if (volume <= 0)
return;
BuyMarket();
LogInfo($"Buy signal at {signal.Time:u}. Close={signal.ClosePrice:0.#####}, upper stop={signal.UpperStop:0.#####}, lower stop={signal.LowerStop:0.#####}, volume={volume:0.#####}.");
}
private void HandleSell(SignalInfo signal)
{
var volume = CalculateSellVolume();
if (volume <= 0)
return;
SellMarket();
LogInfo($"Sell signal at {signal.Time:u}. Close={signal.ClosePrice:0.#####}, upper stop={signal.UpperStop:0.#####}, lower stop={signal.LowerStop:0.#####}, volume={volume:0.#####}.");
}
private decimal CalculateBuyVolume()
{
var volume = 0m;
if (EnableShortExit && Position < 0)
volume += Math.Abs(Position);
if (EnableLongEntry && Position <= 0 && Volume > 0)
volume += Volume;
return volume;
}
private decimal CalculateSellVolume()
{
var volume = 0m;
if (EnableLongExit && Position > 0)
volume += Position;
if (EnableShortEntry && Position >= 0 && Volume > 0)
volume += Volume;
return volume;
}
private void UpdatePreviousValues(ICandleMessage candle)
{
_prevHigh = candle.HighPrice;
_prevLow = candle.LowPrice;
_hasPrevious = true;
}
private sealed record SignalInfo(bool Buy, bool Sell, decimal UpperStop, decimal LowerStop, DateTimeOffset Time, decimal ClosePrice);
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange
class nrtratr_stop_strategy(Strategy):
"""NRTR ATR Stop strategy: ATR-based trailing levels switch direction on price crossing."""
def __init__(self):
super(nrtratr_stop_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 20) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "Number of candles used for ATR calculation", "Indicator")
self._coefficient = self.Param("Coefficient", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Coefficient", "Multiplier applied to ATR for stop levels", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Closed candles to wait before acting", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Time frame for calculations", "General")
self._enable_long_entry = self.Param("EnableLongEntry", True) \
.SetDisplay("Enable Long Entry", "Allow opening long positions", "Trading")
self._enable_short_entry = self.Param("EnableShortEntry", True) \
.SetDisplay("Enable Short Entry", "Allow opening short positions", "Trading")
self._enable_long_exit = self.Param("EnableLongExit", True) \
.SetDisplay("Enable Long Exit", "Allow closing longs on sell signals", "Risk")
self._enable_short_exit = self.Param("EnableShortExit", True) \
.SetDisplay("Enable Short Exit", "Allow closing shorts on buy signals", "Risk")
self._atr = None
self._signals = []
self._upper_stop = 0.0
self._lower_stop = 0.0
self._trend = 0
self._has_stops = False
self._has_previous = False
self._prev_high = 0.0
self._prev_low = 0.0
@property
def AtrPeriod(self):
return self._atr_period.Value
@property
def Coefficient(self):
return self._coefficient.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def EnableLongEntry(self):
return self._enable_long_entry.Value
@property
def EnableShortEntry(self):
return self._enable_short_entry.Value
@property
def EnableLongExit(self):
return self._enable_long_exit.Value
@property
def EnableShortExit(self):
return self._enable_short_exit.Value
def OnStarted2(self, time):
super(nrtratr_stop_strategy, self).OnStarted2(time)
self._atr = AverageTrueRange()
self._atr.Length = self.AtrPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._atr, self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle, atr_value):
if candle.State != CandleStates.Finished:
return
if not self._atr.IsFormed:
self._update_prev(candle)
return
if not self._has_previous:
self._update_prev(candle)
return
prev_trend = self._trend
trend = prev_trend
upper_stop = self._upper_stop
lower_stop = self._lower_stop
coeff = float(self.Coefficient)
atr_val = float(atr_value)
rez = coeff * atr_val
if not self._has_stops:
upper_stop = self._prev_low - rez
lower_stop = self._prev_high + rez
self._has_stops = True
if trend <= 0 and self._prev_low > lower_stop:
upper_stop = self._prev_low - rez
trend = 1
if trend >= 0 and self._prev_high < upper_stop:
lower_stop = self._prev_high + rez
trend = -1
if trend >= 0:
if self._prev_low > upper_stop + rez:
upper_stop = self._prev_low - rez
if trend <= 0:
if self._prev_high < lower_stop - rez:
lower_stop = self._prev_high + rez
buy_signal = trend > 0 and prev_trend <= 0
sell_signal = trend < 0 and prev_trend >= 0
self._trend = trend
self._upper_stop = upper_stop
self._lower_stop = lower_stop
self._signals.append((buy_signal, sell_signal))
if len(self._signals) <= self.SignalBar:
self._update_prev(candle)
return
signal = self._signals.pop(0)
if signal[0]:
self._handle_buy()
if signal[1]:
self._handle_sell()
self._update_prev(candle)
def _handle_buy(self):
vol = 0.0
if self.EnableShortExit and self.Position < 0:
vol += abs(self.Position)
if self.EnableLongEntry and self.Position <= 0 and self.Volume > 0:
vol += float(self.Volume)
if vol > 0:
self.BuyMarket()
def _handle_sell(self):
vol = 0.0
if self.EnableLongExit and self.Position > 0:
vol += float(self.Position)
if self.EnableShortEntry and self.Position >= 0 and self.Volume > 0:
vol += float(self.Volume)
if vol > 0:
self.SellMarket()
def _update_prev(self, candle):
self._prev_high = float(candle.HighPrice)
self._prev_low = float(candle.LowPrice)
self._has_previous = True
def OnReseted(self):
super(nrtratr_stop_strategy, self).OnReseted()
self._signals = []
self._upper_stop = 0.0
self._lower_stop = 0.0
self._trend = 0
self._has_stops = False
self._has_previous = False
self._prev_high = 0.0
self._prev_low = 0.0
self._atr = None
def CreateClone(self):
return nrtratr_stop_strategy()