PresentTrend RMI Synergy
Native Relative Momentum Index entries aligned with a close-price SMA, followed by a ratcheted SMA/ATR trailing price stop. This example implements the historical description's momentum, mean and ATR model; no profitability result is asserted.
Indicators and entry
Use finished five-minute candles by default. Native RMI uses d[t] = close[t] - close[t-RmiMomentum], with lag 3. Over the last RmiPeriod=21 momentum samples, let U be the mean of positive d (zero otherwise), and D the mean of negative d's absolute value (zero otherwise). RMI = 100 * U / (U + D). A zero denominator yields no entry value. This is the native rolling-mean relative momentum calculation.
SMA and native Wilder ATR both use SuperTrendLength=5. ATR includes previous-close gaps and starts with a growing mean before Wilder smoothing. Entry waits for all three indicators:
- Long: RMI >60 and current close > SMA.
- Short: RMI <40 and current close < SMA.
- Only a flat position can enter; submit strategy
Volume as a market order in an allowed direction. A held position ignores new entry signals.
Direction is an optional typed Sides filter: unset (null in C#, None in Python) means both, Sides.Buy means long only, Sides.Sell short only. It filters entries, while stops always close the actual held side. No string or numeric direction codes are needed.
Trailing stop and execution
With width = SuperTrendMultiplier * ATR (default 4):
- At a long entry, initialize stop at
SMA - width; each subsequent finished candle updates it to max(previousStop, SMA - width).
- At a short entry, initialize stop at
SMA + width; update to min(previousStop, SMA + width).
The stop never loosens while that position remains open. It is initialized from the signal's SMA/ATR bands, independently of the eventual fill price. The retained SuperTrend parameter names configure these explicitly defined SMA-centered bands.
A positive bid ≤ the long stop or positive ask ≥ the short stop closes the entire actual position with a market order. Finished close is also checked after updating the stop; it can close immediately if it reaches the new level. Quotes between candles can trigger the stop. The new stop is applied to the current/future observed price, without applying it to the candle's earlier high/low. SMA/ATR can continue updating protection when the current RMI value is empty.
This is local price monitoring, requiring Level1 bid/ask for quote-based intrabar protection. Candle closes remain available when quotes are absent. The stop is not submitted as a native exchange stop order, and fills can differ from its threshold. Any unfinished order blocks duplicate orders until it is terminal; indicator and stop updates continue. The stop's exit candle cannot also enter, while a later eligible candle may re-enter. No EMA, six-hour cooldown, immediate opposite-signal reversal or separate take-profit.
Defaults and checks
| Parameter |
Default |
RmiPeriod |
21, positive |
RmiMomentum |
3, positive close lag |
SuperTrendLength |
5, positive shared SMA/ATR period |
SuperTrendMultiplier |
4, positive ATR width |
Direction |
Unset: both directions |
CandleType |
5 minutes |
C# and Python tests independently compute momentum sums, SMA, Wilder ATR and ratcheted stops using the existing BTC/TON archive. Every entry, executable-price stop exit and actual filled position is checked, including both directions, side filters, held opposite signals, tight intrabar stops and entries less than six hours apart.
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()