Открыть на GitHub

PresentTrend RMI Synergy

Вход по штатному Relative Momentum Index и SMA закрытий, затем ценовой трейлинг по SMA/ATR, который движется только в сторону защиты. Пример реализует импульс, среднюю и ATR из исходного описания; подтверждений доходности нет.

Индикаторы и вход

По умолчанию завершённые пятиминутные свечи. Штатный RMI использует d[t] = close[t] - close[t-RmiMomentum], lag3. За последние RmiPeriod=21 значений импульса U — среднее положительных d (иначе ноль), D — среднее модулей отрицательных d (иначе ноль). RMI = 100 * U / (U + D); при нулевом знаменателе значения для входа нет. Это штатный расчёт относительного импульса со скользящим средним.

SMA и штатный Wilder ATR используют общий SuperTrendLength=5. ATR учитывает разрывы относительно прошлого close, начинает со среднего растущего окна, затем использует Wilder. Вход ждёт прогрева трёх индикаторов:

  • Лонг: RMI >60 и текущий close > SMA.
  • Шорт: RMI <40 и текущий close < SMA.
  • Вход только из нулевой позиции, рыночным ордером на Volume в разрешённую сторону. При удержании позиции новые входные сигналы игнорируются.

Direction — необязательный типизированный фильтр Sides: отсутствие значения (null в C#, None в Python) разрешает обе стороны; Sides.Buy — только long, Sides.Sell — только short. Фильтр ограничивает входы, но стоп всегда закрывает фактическую удерживаемую сторону. Строковых и числовых кодов направления нет.

Трейлинг и исполнение

width = SuperTrendMultiplier * ATR, множитель по умолчанию 4:

  • При входе в long стоп = SMA - width; следующая завершённая свеча обновляет его до max(previousStop, SMA - width).
  • При входе в short стоп = SMA + width; обновление до min(previousStop, SMA + width).

Стоп не ослабляется, пока позиция открыта. Начальный уровень берётся из SMA/ATR сигнальной свечи независимо от последующей цены исполнения. Сохранённые имена SuperTrend-параметров задают именно эти явно определённые полосы вокруг SMA.

Положительный bid ≤ стопу long либо ask ≥ стопу short вызывает рыночное закрытие всего фактического объёма. После обновления стопа проверяется и завершённый close: достижение нового уровня может закрыть позицию сразу. Котировка между свечами тоже может вызвать выход. Новый уровень применяется к текущей и последующим наблюдаемым ценам, без проверки по более ранним high/low этой свечи. SMA/ATR продолжают обновлять защиту и при пустом текущем RMI.

Это локальный мониторинг цены; для выхода по котировкам внутри свечи нужны Level1 bid/ask. При отсутствии котировок доступны закрытия свечей. Биржевой стоп-ордер не отправляется, цена исполнения может отличаться от порога. Любой незавершённый ордер блокирует дубли до терминального состояния; индикаторы и стоп продолжают обновляться. На свече выхода нового входа нет, на более поздней подходящей свече он возможен. EMA, шестичасового cooldown, немедленного разворота по встречному сигналу и отдельного тейк-профита нет.

Настройки и проверки

Параметр Значение по умолчанию
RmiPeriod 21, положительный
RmiMomentum 3, положительный lag закрытий
SuperTrendLength 5, положительный общий период SMA/ATR
SuperTrendMultiplier 4, положительная ширина ATR
Direction Без значения: обе стороны
CandleType 5 минут

Тесты C# и Python независимо рассчитывают суммы импульса, SMA, Wilder ATR и уровни трейлинга на имеющемся архиве BTC/TON. Проверяются каждый вход, выход по исполнимой цене и фактическая позиция, обе стороны и их фильтры, игнорирование встречного входного сигнала при удержании, тесные intrabar-стопы и входы ближе шести часов.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Opens flat-position trades on native RMI momentum and SMA alignment,
/// then exits only at a ratcheted SMA/Wilder ATR price stop.
/// </summary>
public class PresentTrendRmiSynergyStrategy : Strategy
{
	private readonly StrategyParam<int> _rmiPeriod;
	private readonly StrategyParam<int> _rmiMomentum;
	private readonly StrategyParam<int> _superTrendLength;
	private readonly StrategyParam<decimal> _superTrendMultiplier;
	private readonly StrategyParam<Sides?> _direction;
	private readonly StrategyParam<DataType> _candleType;
	private decimal? _stopPrice;
	private Order _pendingOrder;

	/// <summary>Initializes RMI21 with momentum3, SMA/ATR5, multiplier4 and both directions on five-minute candles.</summary>
	public PresentTrendRmiSynergyStrategy()
	{
		_rmiPeriod = Param(nameof(RmiPeriod), 21).SetGreaterThanZero()
			.SetDisplay("RMI Period", "Native rolling mean period of positive/negative momentum", "Indicators");
		_rmiMomentum = Param(nameof(RmiMomentum), 3).SetGreaterThanZero()
			.SetDisplay("RMI Momentum", "Closed-bar lag of each close-price momentum sample", "Indicators");
		_superTrendLength = Param(nameof(SuperTrendLength), 5).SetGreaterThanZero()
			.SetDisplay("SuperTrend Length", "Shared close SMA and Wilder ATR period for trailing bands", "Protection");
		_superTrendMultiplier = Param(nameof(SuperTrendMultiplier), 4m).SetGreaterThanZero()
			.SetDisplay("SuperTrend Multiplier", "ATR width of the ratcheted SMA price stop", "Protection");
		_direction = Param<Sides?>(nameof(Direction), null)
			.SetDisplay("Direction", "Entry side filter: Buy for long, Sell for short, unset for both", "Trading");
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Finished candles supplying RMI, SMA and ATR", "General");
		OrderRegistering += order => _pendingOrder = order;
	}

	/// <summary>Positive native RMI rolling mean period.</summary>
	public int RmiPeriod { get => _rmiPeriod.Value; set => _rmiPeriod.Value = value; }

	/// <summary>Positive close-price momentum lag, in finished candles.</summary>
	public int RmiMomentum { get => _rmiMomentum.Value; set => _rmiMomentum.Value = value; }

	/// <summary>Positive shared SMA and Wilder ATR period.</summary>
	public int SuperTrendLength { get => _superTrendLength.Value; set => _superTrendLength.Value = value; }

	/// <summary>Positive ATR multiplier of the trailing stop.</summary>
	public decimal SuperTrendMultiplier { get => _superTrendMultiplier.Value; set => _superTrendMultiplier.Value = value; }

	/// <summary>Allowed entry side; null permits both. Stops always close the held side.</summary>
	public Sides? Direction { get => _direction.Value; set => _direction.Value = value; }

	/// <summary>Finished candle type; five minutes by default.</summary>
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	private void ResetState()
	{
		_stopPrice = null;
		_pendingOrder = null;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		if (RmiPeriod <= 0 || RmiMomentum <= 0 || SuperTrendLength <= 0 || SuperTrendMultiplier <= 0m
			|| Direction is Sides direction && direction is not (Sides.Buy or Sides.Sell))
			throw new ArgumentException("Periods and ATR multiplier must be positive; direction must be Buy, Sell or unset for both.");
		ResetState();
		foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
		{
			var quotes = new Subscription(DataType.Level1, Security);
			quotes.MarketData.BuildField = field;
			SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
		}
		var rmi = new RelativeMomentumIndex { Length = RmiPeriod, MomentumPeriod = RmiMomentum };
		var mean = new SimpleMovingAverage { Length = SuperTrendLength };
		var atr = new AverageTrueRange { Length = SuperTrendLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.BindEx([rmi, mean, atr], ProcessCandle, allowEmpty: true).Start();
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, mean);
			DrawOwnTrades(area);
		}
	}

	private bool IsPending => _pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed);

	private void Submit(Sides side, decimal volume, string comment)
		=> RegisterOrder(new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		});

	private bool TryStopExit(decimal price)
	{
		if (Position == 0m || _stopPrice is not decimal stop || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return false;
		if (Position > 0m ? price > stop : price < stop)
			return false;
		Submit(Position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(Position), "RMI ATR trailing stop");
		return true;
	}

	private void ObserveQuote(Level1ChangeMessage quote)
	{
		var field = Position > 0m ? Level1Fields.BestBidPrice : Level1Fields.BestAskPrice;
		if (quote.TryGetDecimal(field) is decimal price && price > 0m)
			TryStopExit(price);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue[] values)
	{
		if (candle.State != CandleStates.Finished || values[1].IsEmpty || !values[1].IsFormed
			|| values[2].IsEmpty || !values[2].IsFormed)
			return;
		var mean = values[1].ToDecimal();
		var width = values[2].ToDecimal() * SuperTrendMultiplier;
		if (Position != 0m)
		{
			var candidate = Position > 0m ? mean - width : mean + width;
			_stopPrice = _stopPrice is decimal previous ? Position > 0m ? Math.Max(previous, candidate) : Math.Min(previous, candidate) : candidate;
			TryStopExit(candle.ClosePrice);
			return;
		}
		if (values[0].IsEmpty || !values[0].IsFormed || IsPending || !IsFormedAndOnlineAndAllowTrading())
			return;
		var rmi = values[0].ToDecimal();
		Sides? side = rmi > 60m && candle.ClosePrice > mean ? Sides.Buy
			: rmi < 40m && candle.ClosePrice < mean ? Sides.Sell : null;
		if (side is not Sides entry || Direction is Sides allowed && entry != allowed)
			return;
		_stopPrice = entry == Sides.Buy ? mean - width : mean + width;
		Submit(entry, Volume, "RMI SMA aligned entry");
	}
}