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");
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Array, ArgumentException, Decimal, Nullable, TimeSpan
from StockSharp.Messages import CandleStates, DataType, Level1Fields, OrderStates, OrderTypes, Sides
from StockSharp.BusinessEntities import Order, Subscription
from StockSharp.Algo.Indicators import AverageTrueRange, IIndicator, RelativeMomentumIndex, SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class present_trend_rmi_synergy_strategy(Strategy):
def __init__(self):
super(present_trend_rmi_synergy_strategy, self).__init__()
self._rmi_period = self.Param("RmiPeriod", 21).SetGreaterThanZero() \
.SetDisplay("RMI Period", "Native rolling mean period of positive/negative momentum", "Indicators")
self._rmi_momentum = self.Param("RmiMomentum", 3).SetGreaterThanZero() \
.SetDisplay("RMI Momentum", "Closed-bar lag of each close-price momentum sample", "Indicators")
self._super_trend_length = self.Param("SuperTrendLength", 5).SetGreaterThanZero() \
.SetDisplay("SuperTrend Length", "Shared close SMA and Wilder ATR period for trailing bands", "Protection")
self._super_trend_multiplier = self.Param("SuperTrendMultiplier", Decimal(4)).SetGreaterThanZero() \
.SetDisplay("SuperTrend Multiplier", "ATR width of the ratcheted SMA price stop", "Protection")
self._direction = self.Param[Nullable[Sides]]("Direction", None) \
.SetDisplay("Direction", "Entry side filter: Buy for long, Sell for short, unset for both", "Trading")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5))) \
.SetDisplay("Candle Type", "Finished candles supplying RMI, SMA and ATR", "General")
self._reset_state()
self.OrderRegistering += self._observe_order
@property
def candle_type(self):
return self._candle_type.Value
@candle_type.setter
def candle_type(self, value):
self._candle_type.Value = value
def _reset_state(self):
self._stop_price = None
self._pending_order = None
def OnReseted(self):
super(present_trend_rmi_synergy_strategy, self).OnReseted()
self._reset_state()
def _observe_order(self, order):
self._pending_order = order
def OnStarted2(self, time):
super(present_trend_rmi_synergy_strategy, self).OnStarted2(time)
direction = self._direction.Value
if self._rmi_period.Value <= 0 or self._rmi_momentum.Value <= 0 or self._super_trend_length.Value <= 0 \
or self._super_trend_multiplier.Value <= 0 \
or direction is not None and direction != Sides.Buy and direction != Sides.Sell:
raise ArgumentException("Periods and ATR multiplier must be positive; direction must be Buy, Sell or unset for both.")
self._reset_state()
for field in (Level1Fields.BestBidPrice, Level1Fields.BestAskPrice):
quotes = Subscription(DataType.Level1, self.Security)
quotes.MarketData.BuildField = field
self.SubscribeLevel1(quotes).Bind(self._observe_quote).Start()
rmi = RelativeMomentumIndex()
rmi.Length = self._rmi_period.Value
rmi.MomentumPeriod = self._rmi_momentum.Value
mean = SimpleMovingAverage()
mean.Length = self._super_trend_length.Value
atr = AverageTrueRange()
atr.Length = self._super_trend_length.Value
subscription = self.SubscribeCandles(self.candle_type)
subscription.BindEx(Array[IIndicator]([rmi, mean, atr]), self.OnProcess, True).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, mean)
self.DrawOwnTrades(area)
def _is_pending(self):
order = self._pending_order
return order is not None and order.State != OrderStates.Done and order.State != OrderStates.Failed
def _submit(self, side, volume, comment):
order = Order()
order.Security = self.Security
order.Portfolio = self.Portfolio
order.Type = OrderTypes.Market
order.Side = side
order.Volume = volume
order.Comment = comment
self.RegisterOrder(order)
def _try_stop_exit(self, price):
stop = self._stop_price
if self.Position == 0 or stop is None or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return False
if price > stop if self.Position > 0 else price < stop:
return False
self._submit(Sides.Sell if self.Position > 0 else Sides.Buy, abs(self.Position), "RMI ATR trailing stop")
return True
def _observe_quote(self, quote):
field = Level1Fields.BestBidPrice if self.Position > 0 else Level1Fields.BestAskPrice
if quote.Changes.ContainsKey(field) and quote.Changes[field] > 0:
self._try_stop_exit(quote.Changes[field])
def OnProcess(self, candle, values):
if candle.State != CandleStates.Finished or values[1].IsEmpty or not values[1].IsFormed \
or values[2].IsEmpty or not values[2].IsFormed:
return
mean = values[1].GetValue[Decimal](None)
width = values[2].GetValue[Decimal](None) * self._super_trend_multiplier.Value
if self.Position != 0:
candidate = mean - width if self.Position > 0 else mean + width
self._stop_price = candidate if self._stop_price is None \
else max(self._stop_price, candidate) if self.Position > 0 else min(self._stop_price, candidate)
self._try_stop_exit(candle.ClosePrice)
return
if values[0].IsEmpty or not values[0].IsFormed or self._is_pending() or not self.IsFormedAndOnlineAndAllowTrading():
return
rmi = values[0].GetValue[Decimal](None)
side = Sides.Buy if rmi > 60 and candle.ClosePrice > mean \
else Sides.Sell if rmi < 40 and candle.ClosePrice < mean else None
direction = self._direction.Value
if side is None or direction is not None and side != direction:
return
self._stop_price = mean - width if side == Sides.Buy else mean + width
self._submit(side, self.Volume, "RMI SMA aligned entry")
def CreateClone(self):
return present_trend_rmi_synergy_strategy()